diff --git a/DocumentOperator.Tests/Unit/Infrastructure/Services/PdfProcessing/DevExpressPdfProcessorTests.cs b/DocumentOperator.Tests/Unit/Infrastructure/Services/PdfProcessing/DevExpressPdfProcessorTests.cs index d185666..b15378b 100644 --- a/DocumentOperator.Tests/Unit/Infrastructure/Services/PdfProcessing/DevExpressPdfProcessorTests.cs +++ b/DocumentOperator.Tests/Unit/Infrastructure/Services/PdfProcessing/DevExpressPdfProcessorTests.cs @@ -68,19 +68,20 @@ public class DevExpressPdfProcessorTests #region ValidateAsync Tests [Fact] - public async Task ValidateAsync_ValidPdf_ReturnsPdfMetadata() + public async Task ValidateAsync_ValidPdf_ReturnsPdfValidationResult() { // Arrange byte[] pdfBytes = LoadTestPdf("valid.pdf"); // Act - var metadata = await _sut.ValidateAsync(ToStream(pdfBytes)); + var result = await _sut.ValidateAsync(ToStream(pdfBytes)); // Assert - metadata.Should().NotBeNull("a valid PDF should return metadata"); - metadata.PageCount.Should().BeGreaterThan(0, "PDF must have at least one page"); - metadata.FileSizeBytes.Should().Be(pdfBytes.Length, "file size should match input"); - metadata.PdfVersion.Should().NotBeNullOrEmpty("PDF version should be detected"); + result.Should().NotBeNull("a valid PDF should return a result"); + result.PageCount.Should().BeGreaterThan(0, "PDF must have at least one page"); + result.FileSizeBytes.Should().Be(pdfBytes.Length, "file size must match input"); + result.PdfVersion.Should().NotBeNullOrEmpty("PDF version should be detected"); + result.FileSizeMB.Should().BeApproximately(pdfBytes.Length / 1024.0 / 1024.0, 0.01, "FileSizeMB should be derived from FileSizeBytes"); } [Fact] @@ -90,11 +91,10 @@ public class DevExpressPdfProcessorTests byte[] pdfBytes = LoadTestPdf("valid.pdf"); // Act - var metadata = await _sut.ValidateAsync(ToStream(pdfBytes)); + var result = await _sut.ValidateAsync(ToStream(pdfBytes)); // Assert - // Deine valid.pdf hat wahrscheinlich 1-5 Seiten - passe an! - metadata.PageCount.Should().BeInRange(1, 100, "test PDF should have reasonable page count"); + result.PageCount.Should().BeInRange(1, 100, "test PDF should have reasonable page count"); } [Fact] @@ -150,16 +150,112 @@ public class DevExpressPdfProcessorTests byte[] pdfBytes = LoadTestPdf("valid.pdf"); // Act - var metadata = await _sut.ValidateAsync(ToStream(pdfBytes)); + var result = await _sut.ValidateAsync(ToStream(pdfBytes)); // Assert double expectedSizeMB = pdfBytes.Length / 1024.0 / 1024.0; - metadata.FileSizeMB.Should().BeApproximately(expectedSizeMB, 0.01, + result.FileSizeMB.Should().BeApproximately(expectedSizeMB, 0.01, "FileSizeMB should be calculated correctly from bytes"); } #endregion + #region Encryption Detection Tests + + [Fact] + public async Task ValidateAsync_NormalPdf_IsEncryptedFalse() + { + // Arrange + byte[] pdfBytes = LoadTestPdf("valid.pdf"); + + // Act + var result = await _sut.ValidateAsync(ToStream(pdfBytes)); + + // Assert + result.IsEncrypted.Should().BeFalse("valid.pdf is not password-protected"); + } + + [Fact] + public async Task ValidatePdfAAsync_NormalPdf_EncryptedFalse() + { + // Arrange + byte[] pdfBytes = LoadTestPdf("valid.pdf"); + + // Act + var result = await _sut.ValidatePdfAAsync(ToStream(pdfBytes)); + + // Assert + result.Encrypted.Should().BeFalse("valid.pdf is not password-protected"); + } + + [Fact] + public async Task DetectEncryption_TrailerWithEncryptEntry_ReturnsTrueViaRawBytes() + { + // Validates the encryption detection logic directly by crafting PDF bytes + // that contain /Encrypt in the trailer section (last 2KB). + // The byte-level detection must identify this as encrypted. + + // Build a minimal fake PDF where the trailer contains /Encrypt + // (we are not loading it with DevExpress — just testing the byte scanner) + var trailerBytes = System.Text.Encoding.Latin1.GetBytes( + "\ntrailer\n<< /Size 5 /Root 1 0 R /Encrypt 4 0 R >>\nstartxref\n370\n%%EOF\n"); + + // Pad with enough bytes so the trailer is within last 2KB + var padding = new byte[100]; + Array.Fill(padding, (byte)0x20); + var pdfBytes = padding.Concat(trailerBytes).ToArray(); + + // Access private method via reflection to test it in isolation + var processorType = typeof(DocumentService.Infrastructure.Services.PdfProcessing.DevExpressPdfProcessor); + var detectMethod = processorType.GetMethod("DetectEncryption", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + + detectMethod.Should().NotBeNull("DetectEncryption method should exist"); + var detected = (bool)detectMethod!.Invoke(null, [pdfBytes])!; + detected.Should().BeTrue("trailer with /Encrypt 4 0 R should be detected as encrypted"); + } + + [Fact] + public async Task DetectEncryption_NoEncryptEntry_ReturnsFalseViaRawBytes() + { + // A PDF without /Encrypt in trailer should return false + var pdfBytes = System.Text.Encoding.Latin1.GetBytes( + "%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\nxref\n0 2\ntrailer\n<< /Size 2 /Root 1 0 R >>\nstartxref\n10\n%%EOF\n"); + + var processorType = typeof(DocumentService.Infrastructure.Services.PdfProcessing.DevExpressPdfProcessor); + var detectMethod = processorType.GetMethod("DetectEncryption", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + + var detected = (bool)detectMethod!.Invoke(null, [pdfBytes])!; + detected.Should().BeFalse("PDF without /Encrypt entry should not be detected as encrypted"); + } + + [Fact] + public async Task DetectEncryption_EncryptedKeywordNotAsStandaloneToken_ReturnsFalse() + { + // "/Encrypt" is NOT standalone if the next byte is a letter (e.g. "/Encryption", "/EncryptedData"). + // Strategy 3 (full-file) checks token terminators. + // To isolate Strategy 3, we put the keyword well inside the file + // (not in first or last 2KB) so Strategies 1 and 2 don't fire. + + // Build: 3KB prefix + "/Encryption more" + 3KB suffix + minimal trailer (no /Encrypt) + var prefix = new string('A', 3000); + var fakeContent = " /Encryption some data "; // /Encrypt followed by 'i' — not a terminator + var suffix = new string('B', 3000); + var normalTrailer = "\ntrailer\n<< /Size 2 /Root 1 0 R >>\nstartxref\n10\n%%EOF\n"; + var pdfBytes = System.Text.Encoding.Latin1.GetBytes(prefix + fakeContent + suffix + normalTrailer); + + var processorType = typeof(DocumentService.Infrastructure.Services.PdfProcessing.DevExpressPdfProcessor); + var detectMethod = processorType.GetMethod("DetectEncryption", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + + var detected = (bool)detectMethod!.Invoke(null, [pdfBytes])!; + // /Encryption has 'i' after /Encrypt — 'i' is not a PDF token terminator → should NOT match + detected.Should().BeFalse("'/Encryption' (with letter suffix) should not match the '/Encrypt ' token"); + } + + #endregion + #region Attachment Detection Tests [Fact] @@ -169,13 +265,10 @@ public class DevExpressPdfProcessorTests byte[] pdfBytes = LoadTestPdf("valid.pdf"); // Act - var metadata = await _sut.ValidateAsync(ToStream(pdfBytes)); + var result = await _sut.ValidateAsync(ToStream(pdfBytes)); // Assert - // Note: valid.pdf actually contains /EmbeddedFiles reference (15 0 R) - // This test just verifies that attachment detection doesn't crash - // The exact count depends on the PDF content - metadata.Should().NotBeNull(); + result.Should().NotBeNull(); } [Fact] @@ -185,18 +278,14 @@ public class DevExpressPdfProcessorTests byte[] pdfBytes = LoadTestPdf("pdfWithMoreThanOneAttachment.pdf"); // Act - var metadata = await _sut.ValidateAsync(ToStream(pdfBytes)); + var result = await _sut.ValidateAsync(ToStream(pdfBytes)); // Assert - metadata.HasAttachments.Should().BeTrue("PDF has multiple attachments"); - metadata.AttachmentCount.Should().BeGreaterThan(1, "PDF has more than 1 attachment"); + result.HasAttachments.Should().BeTrue("PDF has multiple attachments"); + result.AttachmentCount.Should().BeGreaterThan(1, "PDF has more than 1 attachment"); + result.IsEncrypted.Should().BeFalse("this PDF is not encrypted"); } - // Note: Testing PDF with attachments requires a real ZUGFeRD PDF file - // as DevExpress PdfDocumentProcessor doesn't expose a simple API to create attachments. - // This test will be added when a ZUGFeRD test PDF is available. - // For now, we verify that attachment detection works (returns false for PDFs without attachments). - #endregion #region CheckAttachmentsAsync Tests