Refactor: Rename DocumentOperator to DocumentService
Updated project and solution files to rename and restructure `DocumentOperator` to `DocumentService`. Modified project references in `DocumentService.API.csproj`, `DocumentService.Application.csproj`, and `DocumentService.Infrastructure.csproj` to reflect the new naming convention. Updated `DocumentService.sln` to remove old project references, add new ones, and adjust configuration and nested project mappings accordingly. This change aligns the project structure with the new naming convention.
This commit is contained in:
@@ -0,0 +1,905 @@
|
||||
using System.Reflection;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using DocumentService.Infrastructure.Services.PdfProcessing;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace DocumentService.Tests.Unit.Infrastructure.Services.PdfProcessing;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for DevExpressPdfProcessor.
|
||||
/// Tests PDF validation and metadata extraction.
|
||||
/// </summary>
|
||||
public class DevExpressPdfProcessorTests
|
||||
{
|
||||
private readonly IPdfProcessor _sut; // SUT = System Under Test
|
||||
|
||||
public DevExpressPdfProcessorTests()
|
||||
{
|
||||
// Arrange: Create instance (wird später implementiert)
|
||||
_sut = new DevExpressPdfProcessor();
|
||||
}
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// Loads a test PDF from embedded resources.
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the PDF file (e.g., "valid.pdf")</param>
|
||||
/// <returns>PDF content as byte array</returns>
|
||||
private static byte[] LoadTestPdf(string filename)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = $"DocumentService.Tests.TestData.Pdfs.{filename}";
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
|
||||
if (stream == null)
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
$"Embedded resource '{resourceName}' not found. " +
|
||||
$"Available resources: {string.Join(", ", assembly.GetManifestResourceNames())}");
|
||||
}
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
stream.CopyTo(memoryStream);
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a test PDF from embedded resources as Stream.
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the PDF file (e.g., "valid.pdf")</param>
|
||||
/// <returns>PDF content as MemoryStream (caller must dispose)</returns>
|
||||
private static MemoryStream LoadTestPdfAsStream(string filename)
|
||||
{
|
||||
byte[] pdfBytes = LoadTestPdf(filename);
|
||||
return new MemoryStream(pdfBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts byte array to MemoryStream for testing
|
||||
/// </summary>
|
||||
private static MemoryStream ToStream(byte[] bytes) => new MemoryStream(bytes);
|
||||
|
||||
#endregion
|
||||
|
||||
#region ValidateAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_ValidPdf_ReturnsPdfValidationResult()
|
||||
{
|
||||
// Arrange
|
||||
byte[] pdfBytes = LoadTestPdf("valid.pdf");
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateAsync(ToStream(pdfBytes));
|
||||
|
||||
// Assert
|
||||
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]
|
||||
public async Task ValidateAsync_ValidPdf_ReturnsCorrectPageCount()
|
||||
{
|
||||
// Arrange
|
||||
byte[] pdfBytes = LoadTestPdf("valid.pdf");
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateAsync(ToStream(pdfBytes));
|
||||
|
||||
// Assert
|
||||
result.PageCount.Should().BeInRange(1, 100, "test PDF should have reasonable page count");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_NullStream_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
Stream? pdfStream = null;
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await _sut.ValidateAsync(pdfStream!);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentNullException>()
|
||||
.WithMessage("*pdfStream*", "null input should be rejected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_EmptyStream_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange
|
||||
var pdfStream = new MemoryStream();
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await _sut.ValidateAsync(pdfStream);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*empty*", "empty input should be rejected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_CorruptedPdf_ThrowsPdfProcessingException()
|
||||
{
|
||||
// Arrange
|
||||
byte[] pdfBytes = "This is not a valid PDF content"u8.ToArray();
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await _sut.ValidateAsync(ToStream(pdfBytes));
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<Exception>()
|
||||
.WithMessage("*valid pdf*", "corrupted PDF should throw exception");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Metadata Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_ValidPdf_FileSizeMBCalculatedCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
byte[] pdfBytes = LoadTestPdf("valid.pdf");
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateAsync(ToStream(pdfBytes));
|
||||
|
||||
// Assert
|
||||
double expectedSizeMB = pdfBytes.Length / 1024.0 / 1024.0;
|
||||
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]
|
||||
public async Task ValidateAsync_ValidPdf_DetectsAttachmentsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
byte[] pdfBytes = LoadTestPdf("valid.pdf");
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateAsync(ToStream(pdfBytes));
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_PdfWithMultipleAttachments_ReturnsCorrectCount()
|
||||
{
|
||||
// Arrange
|
||||
byte[] pdfBytes = LoadTestPdf("pdfWithMoreThanOneAttachment.pdf");
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateAsync(ToStream(pdfBytes));
|
||||
|
||||
// Assert
|
||||
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");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CheckAttachmentsAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAttachmentsAsync_PdfWithoutAttachments_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
byte[] pdfBytes = LoadTestPdf("pdfWithSwissQRCode.pdf");
|
||||
|
||||
// Act
|
||||
var attachmentInfo = await _sut.CheckAttachmentsAsync(ToStream(pdfBytes));
|
||||
|
||||
// Assert
|
||||
attachmentInfo.Should().NotBeNull("CheckAttachmentsAsync should return non-null result");
|
||||
attachmentInfo.HasAttachments.Should().BeFalse("Swiss QR PDF has no attachments");
|
||||
attachmentInfo.AttachmentCount.Should().Be(0, "Swiss QR PDF has no attachments");
|
||||
attachmentInfo.Attachments.Should().BeEmpty("Swiss QR PDF has no attachments");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAttachmentsAsync_PdfWithMultipleAttachments_ReturnsCorrectMetadata()
|
||||
{
|
||||
// Arrange
|
||||
byte[] pdfBytes = LoadTestPdf("pdfWithMoreThanOneAttachment.pdf");
|
||||
|
||||
// Act
|
||||
var attachmentInfo = await _sut.CheckAttachmentsAsync(ToStream(pdfBytes));
|
||||
|
||||
// Assert
|
||||
attachmentInfo.Should().NotBeNull("CheckAttachmentAsync should return non-null result");
|
||||
attachmentInfo.HasAttachments.Should().BeTrue("PDF has 6 attachments");
|
||||
attachmentInfo.AttachmentCount.Should().Be(6, "PDF has exactly 6 attachments");
|
||||
attachmentInfo.Attachments.Should().HaveCount(6, "Attachments list should contain 6 items");
|
||||
|
||||
// Verify each attachment has required properties
|
||||
foreach (var attachment in attachmentInfo.Attachments)
|
||||
{
|
||||
attachment.FileName.Should().NotBeNullOrEmpty("each attachment must have a file name");
|
||||
attachment.MimeType.Should().NotBeNullOrEmpty("each attachment must have a MIME type");
|
||||
attachment.SizeBytes.Should().BeGreaterThan(0, "each attachment must have a size > 0");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAttachmentsAsync_ValidPdf_HasAttachments()
|
||||
{
|
||||
// Arrange
|
||||
byte[] pdfBytes = LoadTestPdf("valid.pdf");
|
||||
|
||||
// Act
|
||||
var attachmentInfo = await _sut.CheckAttachmentsAsync(ToStream(pdfBytes));
|
||||
|
||||
// Assert
|
||||
attachmentInfo.Should().NotBeNull("CheckAttachmentsAsync should return non-null result");
|
||||
// Note: valid.pdf contains embedded files (tested and confirmed)
|
||||
attachmentInfo.HasAttachments.Should().BeTrue("valid.pdf contains attachments");
|
||||
attachmentInfo.AttachmentCount.Should().BeGreaterThan(0, "valid.pdf has at least one attachment");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAttachmentsAsync_EmptyBytes_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange
|
||||
var pdfStream = new MemoryStream();
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await _sut.CheckAttachmentsAsync(pdfStream);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*empty*", "empty input should be rejected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAttachmentsAsync_CorruptedPdf_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
byte[] pdfBytes = "This is not a valid PDF content"u8.ToArray();
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await _sut.CheckAttachmentsAsync(ToStream(pdfBytes));
|
||||
|
||||
// Assert
|
||||
// DevExpress throws ArgumentException for invalid PDF data
|
||||
await act.Should().ThrowAsync<Exception>("corrupted PDF should throw exception");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ExtractAttachmentsAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractAttachmentsAsync_PdfWithAttachments_ReturnsValidZip()
|
||||
{
|
||||
// Arrange
|
||||
byte[] pdfBytes = LoadTestPdf("pdfWithMoreThanOneAttachment.pdf");
|
||||
|
||||
// Act
|
||||
byte[] zipBytes = await _sut.ExtractAttachmentsAsync(ToStream(pdfBytes));
|
||||
|
||||
// Assert
|
||||
zipBytes.Should().NotBeEmpty("ZIP should contain data");
|
||||
|
||||
// Verify ZIP header (PK signature)
|
||||
zipBytes[0].Should().Be(0x50, "ZIP magic bytes start with 'P'");
|
||||
zipBytes[1].Should().Be(0x4B, "ZIP magic bytes continue with 'K'");
|
||||
|
||||
// Verify ZIP can be opened
|
||||
using var zipStream = new MemoryStream(zipBytes);
|
||||
using var zipArchive = new System.IO.Compression.ZipArchive(zipStream, System.IO.Compression.ZipArchiveMode.Read);
|
||||
|
||||
zipArchive.Entries.Should().HaveCount(6, "PDF contains 6 attachments");
|
||||
}
|
||||
|
||||
[Fact(Skip = "valid.pdf may contain attachments - need dedicated PDF without attachments for this test")]
|
||||
public async Task ExtractAttachmentsAsync_PdfWithoutAttachments_ThrowsNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
byte[] pdfBytes = LoadTestPdf("valid.pdf"); // No attachments
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await _sut.ExtractAttachmentsAsync(ToStream(pdfBytes));
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<NotFoundException>()
|
||||
.WithMessage("*attachments*", "PDF without attachments should throw NotFoundException");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractAttachmentsAsync_ExtractedFilesHaveCorrectNames()
|
||||
{
|
||||
// Arrange
|
||||
byte[] pdfBytes = LoadTestPdf("pdfWithMoreThanOneAttachment.pdf");
|
||||
|
||||
// Act
|
||||
byte[] zipBytes = await _sut.ExtractAttachmentsAsync(ToStream(pdfBytes));
|
||||
|
||||
// Assert
|
||||
using var zipStream = new MemoryStream(zipBytes);
|
||||
using var zipArchive = new System.IO.Compression.ZipArchive(zipStream, System.IO.Compression.ZipArchiveMode.Read);
|
||||
|
||||
// Verify all entries have valid names (not null/empty)
|
||||
foreach (var entry in zipArchive.Entries)
|
||||
{
|
||||
entry.Name.Should().NotBeNullOrEmpty("each file should have a name");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractAttachmentsAsync_EmptyStream_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange
|
||||
using var emptyStream = new MemoryStream();
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await _sut.ExtractAttachmentsAsync(emptyStream);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*empty*", "empty stream should be rejected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractAttachmentsAsync_CorruptedPdf_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
byte[] pdfBytes = "This is not a valid PDF content"u8.ToArray();
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await _sut.ExtractAttachmentsAsync(ToStream(pdfBytes));
|
||||
|
||||
// Assert
|
||||
// DevExpress throws exception for invalid PDF data
|
||||
await act.Should().ThrowAsync<Exception>("corrupted PDF should throw exception");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region MergePdfsAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task MergePdfsAsync_TwoPdfs_ReturnsMergedPdf()
|
||||
{
|
||||
// Arrange
|
||||
using var pdf1 = LoadTestPdfAsStream("valid.pdf");
|
||||
using var pdf2 = LoadTestPdfAsStream("pdfWithSwissQRCode.pdf");
|
||||
var streams = new List<Stream> { pdf1, pdf2 };
|
||||
|
||||
// Act
|
||||
byte[] mergedPdf = await _sut.MergePdfsAsync(streams);
|
||||
|
||||
// Assert
|
||||
mergedPdf.Should().NotBeEmpty("merged PDF should contain data");
|
||||
mergedPdf.Length.Should().BeGreaterThan(1000, "merged PDF should be reasonably sized");
|
||||
|
||||
// Verify it's a valid PDF by loading it
|
||||
using var verifyStream = new MemoryStream(mergedPdf);
|
||||
using var verifyProcessor = new DevExpress.Pdf.PdfDocumentProcessor();
|
||||
verifyProcessor.LoadDocument(verifyStream);
|
||||
verifyProcessor.Document.Pages.Count.Should().BeGreaterOrEqualTo(2, "merged PDF should have at least 2 pages");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergePdfsAsync_ThreePdfs_ReturnsMergedPdf()
|
||||
{
|
||||
// Arrange
|
||||
using var pdf1 = LoadTestPdfAsStream("valid.pdf");
|
||||
using var pdf2 = LoadTestPdfAsStream("pdfWithSwissQRCode.pdf");
|
||||
using var pdf3 = LoadTestPdfAsStream("pdfWithMoreThanOneAttachment.pdf");
|
||||
var streams = new List<Stream> { pdf1, pdf2, pdf3 };
|
||||
|
||||
// Act
|
||||
byte[] mergedPdf = await _sut.MergePdfsAsync(streams);
|
||||
|
||||
// Assert
|
||||
mergedPdf.Should().NotBeEmpty();
|
||||
|
||||
// Verify merged PDF
|
||||
using var verifyStream = new MemoryStream(mergedPdf);
|
||||
using var verifyProcessor = new DevExpress.Pdf.PdfDocumentProcessor();
|
||||
verifyProcessor.LoadDocument(verifyStream);
|
||||
verifyProcessor.Document.Pages.Count.Should().BeGreaterOrEqualTo(3, "merged PDF should have at least 3 pages");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergePdfsAsync_WithPageRanges_MergesSelectedPages()
|
||||
{
|
||||
// Arrange: Merge first page of each PDF
|
||||
using var pdf1 = LoadTestPdfAsStream("pdfWithSwissQRCode.pdf"); // Multi-page
|
||||
using var pdf2 = LoadTestPdfAsStream("pdfWithMoreThanOneAttachment.pdf"); // Multi-page
|
||||
var streams = new List<Stream> { pdf1, pdf2 };
|
||||
var pageRanges = new List<string?> { "1", "1" }; // Only first page from each
|
||||
|
||||
// Act
|
||||
byte[] mergedPdf = await _sut.MergePdfsAsync(streams, pageRanges);
|
||||
|
||||
// Assert
|
||||
mergedPdf.Should().NotBeEmpty();
|
||||
|
||||
// Verify merged PDF has exactly 2 pages
|
||||
using var verifyStream = new MemoryStream(mergedPdf);
|
||||
using var verifyProcessor = new DevExpress.Pdf.PdfDocumentProcessor();
|
||||
verifyProcessor.LoadDocument(verifyStream);
|
||||
verifyProcessor.Document.Pages.Count.Should().Be(2, "should have exactly 2 pages (1 from each PDF)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergePdfsAsync_WithRangeFormat_MergesSelectedPages()
|
||||
{
|
||||
// Arrange: Merge 2 PDFs, second PDF with page range "1" (first page only)
|
||||
using var pdf1 = LoadTestPdfAsStream("valid.pdf"); // 1 page
|
||||
using var pdf2 = LoadTestPdfAsStream("pdfWithMoreThanOneAttachment.pdf"); // PDF with attachments
|
||||
var streams = new List<Stream> { pdf1, pdf2 };
|
||||
var pageRanges = new List<string?> { null, "1" }; // All pages from first, page 1 only from second
|
||||
|
||||
// Act
|
||||
byte[] mergedPdf = await _sut.MergePdfsAsync(streams, pageRanges);
|
||||
|
||||
// Assert
|
||||
mergedPdf.Should().NotBeEmpty();
|
||||
|
||||
// Verify merged PDF has at least 2 pages (1 from first + 1 from second)
|
||||
using var verifyStream = new MemoryStream(mergedPdf);
|
||||
using var verifyProcessor = new DevExpress.Pdf.PdfDocumentProcessor();
|
||||
verifyProcessor.LoadDocument(verifyStream);
|
||||
verifyProcessor.Document.Pages.Count.Should().BeGreaterOrEqualTo(2, "should have at least 2 pages");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergePdfsAsync_SinglePdf_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange: Only one PDF (minimum 2 required)
|
||||
using var pdf1 = LoadTestPdfAsStream("valid.pdf");
|
||||
var streams = new List<Stream> { pdf1 };
|
||||
|
||||
// Act
|
||||
var act = async () => await _sut.MergePdfsAsync(streams);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*at least 2*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergePdfsAsync_EmptyStream_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange: One valid PDF + one empty stream
|
||||
using var pdf1 = LoadTestPdfAsStream("valid.pdf");
|
||||
using var emptyStream = new MemoryStream();
|
||||
var streams = new List<Stream> { pdf1, emptyStream };
|
||||
|
||||
// Act
|
||||
var act = async () => await _sut.MergePdfsAsync(streams);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*empty*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergePdfsAsync_StreamNotAtBeginning_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange: Valid PDFs but second stream not at Position=0
|
||||
using var pdf1 = LoadTestPdfAsStream("valid.pdf");
|
||||
using var pdf2 = LoadTestPdfAsStream("pdfWithSwissQRCode.pdf");
|
||||
pdf2.Position = 10; // Move position away from beginning
|
||||
var streams = new List<Stream> { pdf1, pdf2 };
|
||||
|
||||
// Act
|
||||
var act = async () => await _sut.MergePdfsAsync(streams);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergePdfsAsync_InvalidPageRange_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange: Invalid page range format
|
||||
using var pdf1 = LoadTestPdfAsStream("valid.pdf");
|
||||
using var pdf2 = LoadTestPdfAsStream("pdfWithSwissQRCode.pdf");
|
||||
var streams = new List<Stream> { pdf1, pdf2 };
|
||||
var pageRanges = new List<string?> { "1", "abc" }; // Invalid format
|
||||
|
||||
// Act
|
||||
var act = async () => await _sut.MergePdfsAsync(streams, pageRanges);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*Invalid page*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergePdfsAsync_PageRangeExceedsPageCount_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange: Page range exceeds document page count
|
||||
using var pdf1 = LoadTestPdfAsStream("valid.pdf"); // 1 page
|
||||
using var pdf2 = LoadTestPdfAsStream("pdfWithSwissQRCode.pdf");
|
||||
var streams = new List<Stream> { pdf1, pdf2 };
|
||||
var pageRanges = new List<string?> { "1-99", null }; // 99 exceeds pdf1 page count
|
||||
|
||||
// Act
|
||||
var act = async () => await _sut.MergePdfsAsync(streams, pageRanges);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*exceeds document page count*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergePdfsAsync_PageRangesMismatch_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange: Page ranges count doesn't match streams count
|
||||
using var pdf1 = LoadTestPdfAsStream("valid.pdf");
|
||||
using var pdf2 = LoadTestPdfAsStream("pdfWithSwissQRCode.pdf");
|
||||
var streams = new List<Stream> { pdf1, pdf2 };
|
||||
var pageRanges = new List<string?> { "1" }; // Only 1 range for 2 PDFs
|
||||
|
||||
// Act
|
||||
var act = async () => await _sut.MergePdfsAsync(streams, pageRanges);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*must match*");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AddAnnotationAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task AddAnnotationAsync_TextMarkupHighlight_ReturnsAnnotatedPdf()
|
||||
{
|
||||
// Arrange
|
||||
using var pdfStream = LoadTestPdfAsStream("valid.pdf");
|
||||
var annotationType = Domain.Models.ValueObjects.AnnotationType.TextMarkup;
|
||||
var rectangle = (X1: 100.0, Y1: 100.0, X2: 200.0, Y2: 120.0);
|
||||
var textMarkupStyle = Domain.Models.ValueObjects.TextMarkupStyle.Highlight;
|
||||
|
||||
// Act
|
||||
byte[] result = await _sut.AddAnnotationAsync(
|
||||
pdfStream,
|
||||
annotationType,
|
||||
pageNumber: 1,
|
||||
rectangle,
|
||||
content: "Important text",
|
||||
author: "Test Author",
|
||||
color: "FFFF00", // Yellow
|
||||
textMarkupStyle);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeEmpty();
|
||||
result.Length.Should().BeGreaterThan((int)pdfStream.Length); // Annotation adds bytes
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAnnotationAsync_FreeText_ReturnsAnnotatedPdf()
|
||||
{
|
||||
// Arrange
|
||||
using var pdfStream = LoadTestPdfAsStream("valid.pdf");
|
||||
var annotationType = Domain.Models.ValueObjects.AnnotationType.FreeText;
|
||||
var rectangle = (X1: 50.0, Y1: 50.0, X2: 150.0, Y2: 100.0);
|
||||
|
||||
// Act
|
||||
byte[] result = await _sut.AddAnnotationAsync(
|
||||
pdfStream,
|
||||
annotationType,
|
||||
pageNumber: 1,
|
||||
rectangle,
|
||||
content: "Free text annotation",
|
||||
author: "John Doe",
|
||||
color: "FF0000"); // Red
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeEmpty();
|
||||
result.Length.Should().BeGreaterThan((int)pdfStream.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAnnotationAsync_StickyNote_ReturnsAnnotatedPdf()
|
||||
{
|
||||
// Arrange
|
||||
using var pdfStream = LoadTestPdfAsStream("valid.pdf");
|
||||
var annotationType = Domain.Models.ValueObjects.AnnotationType.StickyNote;
|
||||
var rectangle = (X1: 300.0, Y1: 300.0, X2: 320.0, Y2: 320.0);
|
||||
|
||||
// Act
|
||||
byte[] result = await _sut.AddAnnotationAsync(
|
||||
pdfStream,
|
||||
annotationType,
|
||||
pageNumber: 1,
|
||||
rectangle,
|
||||
content: "Please review this section",
|
||||
author: "Reviewer");
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAnnotationAsync_Circle_ReturnsAnnotatedPdf()
|
||||
{
|
||||
// Arrange
|
||||
using var pdfStream = LoadTestPdfAsStream("valid.pdf");
|
||||
var annotationType = Domain.Models.ValueObjects.AnnotationType.Circle;
|
||||
var rectangle = (X1: 100.0, Y1: 200.0, X2: 200.0, Y2: 300.0);
|
||||
|
||||
// Act
|
||||
byte[] result = await _sut.AddAnnotationAsync(
|
||||
pdfStream,
|
||||
annotationType,
|
||||
pageNumber: 1,
|
||||
rectangle,
|
||||
content: "Circle annotation",
|
||||
color: "00FF00"); // Green
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAnnotationAsync_Square_ReturnsAnnotatedPdf()
|
||||
{
|
||||
// Arrange
|
||||
using var pdfStream = LoadTestPdfAsStream("valid.pdf");
|
||||
var annotationType = Domain.Models.ValueObjects.AnnotationType.Square;
|
||||
var rectangle = (X1: 250.0, Y1: 250.0, X2: 350.0, Y2: 350.0);
|
||||
|
||||
// Act
|
||||
byte[] result = await _sut.AddAnnotationAsync(
|
||||
pdfStream,
|
||||
annotationType,
|
||||
pageNumber: 1,
|
||||
rectangle,
|
||||
color: "0000FF"); // Blue
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAnnotationAsync_EmptyStream_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange
|
||||
using var emptyStream = new MemoryStream();
|
||||
|
||||
// Act
|
||||
var act = async () => await _sut.AddAnnotationAsync(
|
||||
emptyStream,
|
||||
Domain.Models.ValueObjects.AnnotationType.FreeText,
|
||||
pageNumber: 1,
|
||||
rectangle: (0, 0, 100, 100),
|
||||
content: "Test");
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*cannot be empty*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAnnotationAsync_StreamNotAtPositionZero_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange
|
||||
using var pdfStream = LoadTestPdfAsStream("valid.pdf");
|
||||
pdfStream.Position = 10; // Move stream position
|
||||
|
||||
// Act
|
||||
var act = async () => await _sut.AddAnnotationAsync(
|
||||
pdfStream,
|
||||
Domain.Models.ValueObjects.AnnotationType.Circle,
|
||||
pageNumber: 1,
|
||||
rectangle: (0, 0, 100, 100));
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*must be at position 0*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAnnotationAsync_InvalidPageNumber_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange
|
||||
using var pdfStream = LoadTestPdfAsStream("valid.pdf");
|
||||
|
||||
// Act
|
||||
var act = async () => await _sut.AddAnnotationAsync(
|
||||
pdfStream,
|
||||
Domain.Models.ValueObjects.AnnotationType.FreeText,
|
||||
pageNumber: 999, // Exceeds page count
|
||||
rectangle: (0, 0, 100, 100),
|
||||
content: "Test");
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*exceeds document page count*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAnnotationAsync_FreeTextWithoutContent_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange
|
||||
using var pdfStream = LoadTestPdfAsStream("valid.pdf");
|
||||
|
||||
// Act
|
||||
var act = async () => await _sut.AddAnnotationAsync(
|
||||
pdfStream,
|
||||
Domain.Models.ValueObjects.AnnotationType.FreeText,
|
||||
pageNumber: 1,
|
||||
rectangle: (0, 0, 100, 100),
|
||||
content: null); // Missing required content
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*requires content*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAnnotationAsync_StickyNoteWithoutContent_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange
|
||||
using var pdfStream = LoadTestPdfAsStream("valid.pdf");
|
||||
|
||||
// Act
|
||||
var act = async () => await _sut.AddAnnotationAsync(
|
||||
pdfStream,
|
||||
Domain.Models.ValueObjects.AnnotationType.StickyNote,
|
||||
pageNumber: 1,
|
||||
rectangle: (0, 0, 20, 20),
|
||||
content: ""); // Empty content
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*requires content*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAnnotationAsync_TextMarkupWithoutStyle_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange
|
||||
using var pdfStream = LoadTestPdfAsStream("valid.pdf");
|
||||
|
||||
// Act
|
||||
var act = async () => await _sut.AddAnnotationAsync(
|
||||
pdfStream,
|
||||
Domain.Models.ValueObjects.AnnotationType.TextMarkup,
|
||||
pageNumber: 1,
|
||||
rectangle: (0, 0, 100, 100),
|
||||
textMarkupStyle: null); // Missing required style
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*requires textMarkupStyle*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAnnotationAsync_InvalidColorFormat_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange
|
||||
using var pdfStream = LoadTestPdfAsStream("valid.pdf");
|
||||
|
||||
// Act
|
||||
var act = async () => await _sut.AddAnnotationAsync(
|
||||
pdfStream,
|
||||
Domain.Models.ValueObjects.AnnotationType.Circle,
|
||||
pageNumber: 1,
|
||||
rectangle: (0, 0, 100, 100),
|
||||
color: "INVALID"); // Invalid hex format
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<BadRequestException>()
|
||||
.WithMessage("*Invalid color format*");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user