1
0

M3-AP-008: Testabdeckung vervollständigt

This commit is contained in:
2026-04-01 22:50:25 +02:00
parent 092e83340f
commit c0cdd0ed6e
2 changed files with 87 additions and 0 deletions

View File

@@ -15,6 +15,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -125,6 +126,28 @@ class PdfTextExtractionPortAdapterTest {
assertNotNull(success.extractedText()); // May be empty, but not null assertNotNull(success.extractedText()); // May be empty, but not null
} }
@Test
void testCorruptedPdfReturnsTechnicalError() throws Exception {
// Create a file with binary garbage content exists on disk but is not a valid PDF
Path corruptFile = tempDir.resolve("corrupt.pdf");
Files.write(corruptFile, "THIS IS NOT A PDF - RANDOM GARBAGE XYZ123!@#".getBytes());
SourceDocumentCandidate candidate = new SourceDocumentCandidate(
"corrupt.pdf",
Files.size(corruptFile),
new SourceDocumentLocator(corruptFile.toAbsolutePath().toString())
);
PdfExtractionResult result = adapter.extractTextAndPageCount(candidate);
// PDFBox cannot load a garbage file; the adapter must catch this and return TechnicalError
assertInstanceOf(PdfExtractionTechnicalError.class, result,
"Binary garbage file (not a real PDF) must yield TechnicalError, not Success");
PdfExtractionTechnicalError error = (PdfExtractionTechnicalError) result;
assertNotNull(error.errorMessage(), "TechnicalError must carry a non-null error message");
assertFalse(error.errorMessage().isBlank(), "TechnicalError message must not be blank");
}
// --- Helper methods to create test PDFs --- // --- Helper methods to create test PDFs ---
/** /**

View File

@@ -24,7 +24,9 @@ import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Instant; import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@@ -258,6 +260,45 @@ class M2BatchRunProcessingUseCaseTest {
assertTrue(lockPort.wasReleaseCalled(), "Lock should be released even when source access fails"); assertTrue(lockPort.wasReleaseCalled(), "Lock should be released even when source access fails");
} }
/**
* Mixed-batch test: one document per M3 outcome type in a single run.
* Proves that no individual outcome aborts the overall batch (AP-008 explicit contract).
*/
@Test
void execute_m3MixedBatch_allOutcomeTypes_batchOverallSucceeds() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort();
// maxPages=3 in buildConfig; pageLimitCandidate has 10 pages → exceeds limit
StartConfiguration config = buildConfig(tempDir);
SourceDocumentCandidate goodCandidate = makeCandidate("good.pdf");
SourceDocumentCandidate noTextCandidate = makeCandidate("notext.pdf");
SourceDocumentCandidate pageLimitCandidate = makeCandidate("toobig.pdf");
SourceDocumentCandidate technicalErrorCandidate = makeCandidate("broken.pdf");
SourceDocumentCandidate contentErrorCandidate = makeCandidate("encrypted.pdf");
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(
goodCandidate, noTextCandidate, pageLimitCandidate,
technicalErrorCandidate, contentErrorCandidate));
MappedExtractionPort extractionPort = new MappedExtractionPort()
.with(goodCandidate, new PdfExtractionSuccess("Invoice text", new PdfPageCount(1)))
.with(noTextCandidate, new PdfExtractionSuccess(" ", new PdfPageCount(1)))
.with(pageLimitCandidate, new PdfExtractionSuccess("Some text", new PdfPageCount(10)))
.with(technicalErrorCandidate, new PdfExtractionTechnicalError("I/O error", null))
.with(contentErrorCandidate, new PdfExtractionContentError("PDF is encrypted"));
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase(
config, lockPort, candidatesPort, extractionPort);
BatchRunContext context = new BatchRunContext(new RunId("m3-mixed"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
assertTrue(outcome.isSuccess(),
"Mixed batch with all M3 outcome types must yield batch SUCCESS");
assertEquals(5, extractionPort.callCount(),
"Extraction must be attempted for each of the 5 candidates");
}
@Test @Test
void execute_m3MultipleCandidates_allProcessed_batchSucceeds() throws Exception { void execute_m3MultipleCandidates_allProcessed_batchSucceeds() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort(); MockRunLockPort lockPort = new MockRunLockPort();
@@ -416,4 +457,27 @@ class M2BatchRunProcessingUseCaseTest {
throw new UnsupportedOperationException("Should not be called"); throw new UnsupportedOperationException("Should not be called");
} }
} }
/** Per-candidate extraction port: maps each candidate to a fixed result; counts calls. */
private static class MappedExtractionPort implements PdfTextExtractionPort {
private final Map<SourceDocumentCandidate, PdfExtractionResult> resultMap = new LinkedHashMap<>();
private int calls = 0;
MappedExtractionPort with(SourceDocumentCandidate candidate, PdfExtractionResult result) {
resultMap.put(candidate, result);
return this;
}
@Override
public PdfExtractionResult extractTextAndPageCount(SourceDocumentCandidate candidate) {
calls++;
PdfExtractionResult result = resultMap.get(candidate);
if (result == null) {
throw new IllegalStateException("No extraction result mapped for candidate: " + candidate);
}
return result;
}
int callCount() { return calls; }
}
} }