diff --git a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/pdfextraction/PdfTextExtractionPortAdapterTest.java b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/pdfextraction/PdfTextExtractionPortAdapterTest.java index 3b63805..93263c6 100644 --- a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/pdfextraction/PdfTextExtractionPortAdapterTest.java +++ b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/pdfextraction/PdfTextExtractionPortAdapterTest.java @@ -200,6 +200,50 @@ class PdfTextExtractionPortAdapterTest { assertFalse(error.errorMessage().isBlank(), "TechnicalError message must not be blank"); } + @Test + void testExtractingLargePdfReturnsSuccess() throws Exception { + // Create a large PDF with many pages + Path largePdfFile = tempDir.resolve("large.pdf"); + createMultiPagePdf(largePdfFile, 50); + + SourceDocumentCandidate candidate = new SourceDocumentCandidate( + "large.pdf", + Files.size(largePdfFile), + new SourceDocumentLocator(largePdfFile.toAbsolutePath().toString()) + ); + + PdfExtractionResult result = adapter.extractTextAndPageCount(candidate); + + assertInstanceOf(PdfExtractionSuccess.class, result); + PdfExtractionSuccess success = (PdfExtractionSuccess) result; + assertEquals(50, success.pageCount().value()); + assertNotNull(success.extractedText()); + } + + @Test + void testPartiallyCorruptedPdfStillReturnsError() throws Exception { + // Create a file that starts like PDF but has corrupted content + Path partialCorruptFile = tempDir.resolve("partial-corrupt.pdf"); + byte[] pdfSignature = new byte[] {0x25, 0x50, 0x44, 0x46}; // %PDF in hex + byte[] corruptData = "This is corrupted PDF content that will fail parsing".getBytes(); + byte[] combined = new byte[pdfSignature.length + corruptData.length]; + System.arraycopy(pdfSignature, 0, combined, 0, pdfSignature.length); + System.arraycopy(corruptData, 0, combined, pdfSignature.length, corruptData.length); + Files.write(partialCorruptFile, combined); + + SourceDocumentCandidate candidate = new SourceDocumentCandidate( + "partial-corrupt.pdf", + Files.size(partialCorruptFile), + new SourceDocumentLocator(partialCorruptFile.toAbsolutePath().toString()) + ); + + PdfExtractionResult result = adapter.extractTextAndPageCount(candidate); + + assertInstanceOf(PdfExtractionTechnicalError.class, result); + PdfExtractionTechnicalError error = (PdfExtractionTechnicalError) result; + assertNotNull(error.errorMessage()); + } + // --- Helper methods to create test PDFs --- /** diff --git a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteDocumentRecordRepositoryAdapterTest.java b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteDocumentRecordRepositoryAdapterTest.java index be265a8..12e7cfa 100644 --- a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteDocumentRecordRepositoryAdapterTest.java +++ b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteDocumentRecordRepositoryAdapterTest.java @@ -399,4 +399,158 @@ class SqliteDocumentRecordRepositoryAdapterTest { assertThat(foundRecord.failureCounters().transientErrorCount()).isEqualTo(3); assertThat(foundRecord.lastFailureInstant()).isEqualTo(failureInstant); } + + @Test + void findByFingerprint_shouldThrowNullPointerException_whenFingerprintIsNull() { + // Given + DocumentFingerprint nullFingerprint = null; + + // When / Then + assertThatThrownBy(() -> repository.findByFingerprint(nullFingerprint)) + .isInstanceOf(NullPointerException.class); + } + + @Test + void create_shouldThrowNullPointerException_whenRecordIsNull() { + // When / Then + assertThatThrownBy(() -> repository.create(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + void update_shouldThrowNullPointerException_whenRecordIsNull() { + // When / Then + assertThatThrownBy(() -> repository.update(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + void findByFingerprint_shouldReturnDocumentKnownProcessable_whenStatusIsSkippedAlreadyProcessed() { + // Given + DocumentFingerprint fingerprint = new DocumentFingerprint( + "9999999999999999999999999999999999999999999999999999999999999999"); + Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS); + DocumentRecord record = new DocumentRecord( + fingerprint, + new SourceDocumentLocator("/source/skipped.pdf"), + "skipped.pdf", + ProcessingStatus.SKIPPED_ALREADY_PROCESSED, + FailureCounters.zero(), + null, + null, + now, + now + ); + repository.create(record); + + // When + DocumentRecordLookupResult result = repository.findByFingerprint(fingerprint); + + // Then: SKIPPED_ALREADY_PROCESSED is not terminal, should be DocumentKnownProcessable + assertThat(result).isInstanceOf(DocumentKnownProcessable.class); + DocumentKnownProcessable known = (DocumentKnownProcessable) result; + assertThat(known.record().overallStatus()).isEqualTo(ProcessingStatus.SKIPPED_ALREADY_PROCESSED); + } + + @Test + void findByFingerprint_shouldReturnDocumentKnownProcessable_whenStatusIsSkippedFinalFailure() { + // Given + DocumentFingerprint fingerprint = new DocumentFingerprint( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS); + DocumentRecord record = new DocumentRecord( + fingerprint, + new SourceDocumentLocator("/source/final-skipped.pdf"), + "final-skipped.pdf", + ProcessingStatus.SKIPPED_FINAL_FAILURE, + new FailureCounters(2, 0), + now.minusSeconds(60), + null, + now, + now + ); + repository.create(record); + + // When + DocumentRecordLookupResult result = repository.findByFingerprint(fingerprint); + + // Then: SKIPPED_FINAL_FAILURE is not terminal, should be DocumentKnownProcessable + assertThat(result).isInstanceOf(DocumentKnownProcessable.class); + DocumentKnownProcessable known = (DocumentKnownProcessable) result; + assertThat(known.record().overallStatus()).isEqualTo(ProcessingStatus.SKIPPED_FINAL_FAILURE); + } + + @Test + void create_and_update_shouldPreserveNullTimestamps() { + // Given: create with null timestamps + DocumentFingerprint fingerprint = new DocumentFingerprint( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS); + DocumentRecord record = new DocumentRecord( + fingerprint, + new SourceDocumentLocator("/source/no-timestamps.pdf"), + "no-timestamps.pdf", + ProcessingStatus.PROCESSING, + FailureCounters.zero(), + null, // lastFailureInstant is null + null, // lastSuccessInstant is null + now, + now + ); + repository.create(record); + + // When + DocumentRecordLookupResult result = repository.findByFingerprint(fingerprint); + + // Then + assertThat(result).isInstanceOf(DocumentKnownProcessable.class); + DocumentKnownProcessable known = (DocumentKnownProcessable) result; + assertThat(known.record().lastFailureInstant()).isNull(); + assertThat(known.record().lastSuccessInstant()).isNull(); + } + + @Test + void update_shouldPreserveCreatedAtTimestamp() { + // Given: create with specific createdAt + DocumentFingerprint fingerprint = new DocumentFingerprint( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"); + Instant createdAt = Instant.now().minusSeconds(1000).truncatedTo(ChronoUnit.MICROS); + Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS); + + DocumentRecord initialRecord = new DocumentRecord( + fingerprint, + new SourceDocumentLocator("/source/test.pdf"), + "test.pdf", + ProcessingStatus.PROCESSING, + FailureCounters.zero(), + null, + null, + createdAt, // Much older createdAt + createdAt + ); + repository.create(initialRecord); + + // Update with new timestamps + DocumentRecord updated = new DocumentRecord( + fingerprint, + new SourceDocumentLocator("/source/test.pdf"), + "test.pdf", + ProcessingStatus.SUCCESS, + FailureCounters.zero(), + null, + now, + createdAt, // createdAt should remain unchanged + now + ); + + // When + repository.update(updated); + DocumentRecordLookupResult result = repository.findByFingerprint(fingerprint); + + // Then: createdAt should be preserved + assertThat(result).isInstanceOf(DocumentTerminalSuccess.class); + DocumentTerminalSuccess success = (DocumentTerminalSuccess) result; + assertThat(success.record().createdAt()).isEqualTo(createdAt); + assertThat(success.record().updatedAt()).isEqualTo(now); + } } \ No newline at end of file diff --git a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteUnitOfWorkAdapterTest.java b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteUnitOfWorkAdapterTest.java index 8479432..b37dea7 100644 --- a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteUnitOfWorkAdapterTest.java +++ b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteUnitOfWorkAdapterTest.java @@ -179,4 +179,15 @@ class SqliteUnitOfWorkAdapterTest { assertTrue(lookupResult instanceof de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown, "DocumentRecord should be rolled back on runtime exception"); } + + /** + * Verifies that null operations Consumer throws NullPointerException. + */ + @Test + void executeInTransaction_throwsNullPointerExceptionForNullOperations() { + assertThrows(NullPointerException.class, () -> { + unitOfWorkAdapter.executeInTransaction(null); + }); + } + }