From fbaefe1fd2e7a875de63888ac35fdb78d81d963c Mon Sep 17 00:00:00 2001 From: TekH Date: Fri, 28 Aug 2026 11:46:18 +0200 Subject: [PATCH] test: add Application handler tests for all missing features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New test files (4 tests each — 2 success + 2 failure): - MergePdfsCommandHandlerTests: TwoStreams, WithPageRanges, BadRequest, GenericException - AddAnnotationHandlerTests: TextMarkup, FreeText, BadRequest (page OOB), GenericException - AddStampHandlerTests: TextStamp, PredefinedStamp, BadRequest (opacity), GenericException - ExtractPdfAttachmentsHandlerTests: ReturnsZip, NotFoundException, BadRequest, GenericException - ConvertPdfAHandlerTests (ConvertTo + ConvertFrom): ConvertTo: ValidLevel, NotImplemented, BadRequest ConvertFrom: ValidPdfA, BadRequest, GenericException --- .../AddAnnotationHandlerTests.cs | 126 +++++++++++++++++ .../Features/AddStamp/AddStampHandlerTests.cs | 133 ++++++++++++++++++ .../ConvertPdfA/ConvertPdfAHandlerTests.cs | 121 ++++++++++++++++ .../ExtractPdfAttachmentsHandlerTests.cs | 80 +++++++++++ .../MergePdfs/MergePdfsCommandHandlerTests.cs | 95 +++++++++++++ 5 files changed, 555 insertions(+) create mode 100644 DocumentOperator.Tests/Unit/Application/Features/AddAnnotation/AddAnnotationHandlerTests.cs create mode 100644 DocumentOperator.Tests/Unit/Application/Features/AddStamp/AddStampHandlerTests.cs create mode 100644 DocumentOperator.Tests/Unit/Application/Features/ConvertPdfA/ConvertPdfAHandlerTests.cs create mode 100644 DocumentOperator.Tests/Unit/Application/Features/ExtractAttachments/ExtractPdfAttachmentsHandlerTests.cs create mode 100644 DocumentOperator.Tests/Unit/Application/Features/MergePdfs/MergePdfsCommandHandlerTests.cs diff --git a/DocumentOperator.Tests/Unit/Application/Features/AddAnnotation/AddAnnotationHandlerTests.cs b/DocumentOperator.Tests/Unit/Application/Features/AddAnnotation/AddAnnotationHandlerTests.cs new file mode 100644 index 0000000..aedc7fd --- /dev/null +++ b/DocumentOperator.Tests/Unit/Application/Features/AddAnnotation/AddAnnotationHandlerTests.cs @@ -0,0 +1,126 @@ +using DocumentService.Application.AddAnnotation; +using DocumentService.Application.Common.Interfaces; +using DocumentService.Domain.Common.Exceptions; +using DocumentService.Domain.Models.ValueObjects; +using FluentAssertions; +using Moq; +using Xunit; + +namespace DocumentService.Tests.Unit.Application.Features.AddAnnotation; + +public class AddAnnotationHandlerTests +{ + private readonly Mock _mockProcessor; + private readonly AddAnnotationHandler _handler; + private static readonly byte[] FakePdfBytes = "%PDF"u8.ToArray(); + + public AddAnnotationHandlerTests() + { + _mockProcessor = new Mock(); + _handler = new AddAnnotationHandler(_mockProcessor.Object); + } + + [Fact] + public async Task Handle_TextMarkupAnnotation_ReturnsAnnotatedBytes() + { + // Arrange + var command = new AddAnnotationCommand + { + PdfStream = new MemoryStream(FakePdfBytes), + AnnotationType = AnnotationType.TextMarkup, + PageNumber = 1, + Rectangle = (10, 20, 200, 50), + TextMarkupStyle = TextMarkupStyle.Highlight, + Author = "TestUser", + Color = "FFFF00" + }; + var expected = new byte[] { 0x25, 0x50, 0x44, 0x46, 0x01 }; + _mockProcessor.Setup(x => x.AddAnnotationAsync( + It.IsAny(), AnnotationType.TextMarkup, 1, + It.IsAny<(double, double, double, double)>(), + null, "TestUser", "FFFF00", TextMarkupStyle.Highlight, AnnotationOrigin.BottomLeft)) + .ReturnsAsync(expected); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Should().BeEquivalentTo(expected); + _mockProcessor.Verify(x => x.AddAnnotationAsync( + It.IsAny(), AnnotationType.TextMarkup, 1, + It.IsAny<(double, double, double, double)>(), + null, "TestUser", "FFFF00", TextMarkupStyle.Highlight, AnnotationOrigin.BottomLeft), Times.Once); + } + + [Fact] + public async Task Handle_FreeTextAnnotation_PassesContentToProcessor() + { + // Arrange + var command = new AddAnnotationCommand + { + PdfStream = new MemoryStream(FakePdfBytes), + AnnotationType = AnnotationType.FreeText, + PageNumber = 2, + Rectangle = (50, 100, 300, 150), + Content = "Review this section" + }; + var expected = new byte[] { 0x25, 0x50, 0x44, 0x46, 0x02 }; + _mockProcessor.Setup(x => x.AddAnnotationAsync( + It.IsAny(), AnnotationType.FreeText, 2, + It.IsAny<(double, double, double, double)>(), + "Review this section", null, null, null, AnnotationOrigin.BottomLeft)) + .ReturnsAsync(expected); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Handle_ProcessorThrowsBadRequest_PropagatesException() + { + // Arrange + var command = new AddAnnotationCommand + { + PdfStream = new MemoryStream(FakePdfBytes), + AnnotationType = AnnotationType.Circle, + PageNumber = 99, + Rectangle = (0, 0, 100, 100) + }; + _mockProcessor.Setup(x => x.AddAnnotationAsync( + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny<(double, double, double, double)>(), + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ThrowsAsync(new BadRequestException("Page number 99 exceeds document page count")); + + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => _handler.Handle(command, CancellationToken.None)); + ex.Message.Should().Contain("Page number 99"); + } + + [Fact] + public async Task Handle_ProcessorThrowsGenericException_PropagatesException() + { + // Arrange + var command = new AddAnnotationCommand + { + PdfStream = new MemoryStream(FakePdfBytes), + AnnotationType = AnnotationType.Square, + PageNumber = 1, + Rectangle = (0, 0, 100, 100) + }; + _mockProcessor.Setup(x => x.AddAnnotationAsync( + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny<(double, double, double, double)>(), + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception("Unexpected error")); + + // Act & Assert + await Assert.ThrowsAsync(() => _handler.Handle(command, CancellationToken.None)); + } +} diff --git a/DocumentOperator.Tests/Unit/Application/Features/AddStamp/AddStampHandlerTests.cs b/DocumentOperator.Tests/Unit/Application/Features/AddStamp/AddStampHandlerTests.cs new file mode 100644 index 0000000..f2d1d57 --- /dev/null +++ b/DocumentOperator.Tests/Unit/Application/Features/AddStamp/AddStampHandlerTests.cs @@ -0,0 +1,133 @@ +using DocumentService.Application.AddStamp; +using DocumentService.Application.Common.Interfaces; +using DocumentService.Domain.Common.Exceptions; +using DocumentService.Domain.Models.ValueObjects; +using FluentAssertions; +using Moq; +using Xunit; + +namespace DocumentService.Tests.Unit.Application.Features.AddStamp; + +public class AddStampHandlerTests +{ + private readonly Mock _mockProcessor; + private readonly AddStampHandler _handler; + private static readonly byte[] FakePdfBytes = "%PDF"u8.ToArray(); + + public AddStampHandlerTests() + { + _mockProcessor = new Mock(); + _handler = new AddStampHandler(_mockProcessor.Object); + } + + [Fact] + public async Task Handle_TextStamp_ReturnsStampedBytes() + { + // Arrange + var command = new AddStampCommand + { + PdfStream = new MemoryStream(FakePdfBytes), + StampType = StampType.Text, + Position = (100, 200), + Text = "CONFIDENTIAL" + }; + var expected = new byte[] { 0x25, 0x50, 0x44, 0x46, 0x03 }; + _mockProcessor.Setup(x => x.AddStampAsync( + It.IsAny(), StampType.Text, null, + It.IsAny<(double, double)>(), null, + AnnotationOrigin.BottomLeft, "CONFIDENTIAL", + null, null, null, null, null, + StampPlacement.Foreground, null, null)) + .ReturnsAsync(expected); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Should().BeEquivalentTo(expected); + _mockProcessor.Verify(x => x.AddStampAsync( + It.IsAny(), StampType.Text, null, + It.IsAny<(double, double)>(), null, + AnnotationOrigin.BottomLeft, "CONFIDENTIAL", + null, null, null, null, null, + StampPlacement.Foreground, null, null), Times.Once); + } + + [Fact] + public async Task Handle_PredefinedStamp_PassesPredefinedTypeToProcessor() + { + // Arrange + var command = new AddStampCommand + { + PdfStream = new MemoryStream(FakePdfBytes), + StampType = StampType.Predefined, + Position = (50, 50), + PredefinedType = PredefinedStampType.Approved, + Opacity = 0.8 + }; + var expected = new byte[] { 0x25, 0x50, 0x44, 0x46, 0x04 }; + _mockProcessor.Setup(x => x.AddStampAsync( + It.IsAny(), StampType.Predefined, null, + It.IsAny<(double, double)>(), null, + AnnotationOrigin.BottomLeft, null, null, null, null, + 0.8, null, StampPlacement.Foreground, null, PredefinedStampType.Approved)) + .ReturnsAsync(expected); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Handle_ProcessorThrowsBadRequest_PropagatesException() + { + // Arrange + var command = new AddStampCommand + { + PdfStream = new MemoryStream(FakePdfBytes), + StampType = StampType.Text, + Position = (0, 0), + Text = "DRAFT", + Opacity = 2.0 // invalid + }; + _mockProcessor.Setup(x => x.AddStampAsync( + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny<(double, double)>(), It.IsAny<(double, double)?>(), + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ThrowsAsync(new BadRequestException("Opacity must be between 0.0 and 1.0")); + + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => _handler.Handle(command, CancellationToken.None)); + ex.Message.Should().Contain("Opacity"); + } + + [Fact] + public async Task Handle_ProcessorThrowsGenericException_PropagatesException() + { + // Arrange + var command = new AddStampCommand + { + PdfStream = new MemoryStream(FakePdfBytes), + StampType = StampType.Text, + Position = (0, 0), + Text = "TEST" + }; + _mockProcessor.Setup(x => x.AddStampAsync( + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny<(double, double)>(), It.IsAny<(double, double)?>(), + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception("Unexpected")); + + // Act & Assert + await Assert.ThrowsAsync(() => _handler.Handle(command, CancellationToken.None)); + } +} diff --git a/DocumentOperator.Tests/Unit/Application/Features/ConvertPdfA/ConvertPdfAHandlerTests.cs b/DocumentOperator.Tests/Unit/Application/Features/ConvertPdfA/ConvertPdfAHandlerTests.cs new file mode 100644 index 0000000..29ce6d0 --- /dev/null +++ b/DocumentOperator.Tests/Unit/Application/Features/ConvertPdfA/ConvertPdfAHandlerTests.cs @@ -0,0 +1,121 @@ +using DocumentService.Application.Common.Interfaces; +using DocumentService.Application.ConvertToPdfA; +using DocumentService.Application.ConvertFromPdfA; +using DocumentService.Domain.Common.Exceptions; +using FluentAssertions; +using Moq; +using Xunit; + +namespace DocumentService.Tests.Unit.Application.Features.ConvertPdfA; + +public class ConvertToPdfAHandlerTests +{ + private readonly Mock _mockProcessor; + private readonly ConvertToPdfACommandHandler _handler; + private static readonly byte[] FakePdfBytes = "%PDF"u8.ToArray(); + + public ConvertToPdfAHandlerTests() + { + _mockProcessor = new Mock(); + _handler = new ConvertToPdfACommandHandler(_mockProcessor.Object); + } + + [Fact] + public async Task Handle_ValidPdfAndLevel_ReturnsConvertedBytes() + { + // Arrange + var command = new ConvertToPdfACommand { PdfStream = new MemoryStream(FakePdfBytes), PdfALevel = "PDF/A-3b" }; + var expected = new byte[] { 0x25, 0x50, 0x44, 0x46, 0x0A }; + _mockProcessor.Setup(x => x.ConvertToPdfAAsync(It.IsAny(), "PDF/A-3b")).ReturnsAsync(expected); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Should().BeEquivalentTo(expected); + _mockProcessor.Verify(x => x.ConvertToPdfAAsync(It.IsAny(), "PDF/A-3b"), Times.Once); + } + + [Fact] + public async Task Handle_ProcessorThrowsNotImplemented_PropagatesException() + { + // Arrange + var command = new ConvertToPdfACommand { PdfStream = new MemoryStream(FakePdfBytes), PdfALevel = "PDF/A-1b" }; + _mockProcessor.Setup(x => x.ConvertToPdfAAsync(It.IsAny(), "PDF/A-1b")) + .ThrowsAsync(new NotImplementedException("PDF/A conversion not yet implemented")); + + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => _handler.Handle(command, CancellationToken.None)); + ex.Message.Should().Contain("not yet implemented"); + } + + [Fact] + public async Task Handle_ProcessorThrowsBadRequest_PropagatesException() + { + // Arrange + var command = new ConvertToPdfACommand { PdfStream = new MemoryStream(FakePdfBytes), PdfALevel = "PDF/A-2b" }; + _mockProcessor.Setup(x => x.ConvertToPdfAAsync(It.IsAny(), "PDF/A-2b")) + .ThrowsAsync(new BadRequestException("PDF stream cannot be empty")); + + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => _handler.Handle(command, CancellationToken.None)); + ex.Message.Should().Contain("empty"); + } +} + +public class ConvertFromPdfAHandlerTests +{ + private readonly Mock _mockProcessor; + private readonly ConvertFromPdfACommandHandler _handler; + private static readonly byte[] FakePdfBytes = "%PDF"u8.ToArray(); + + public ConvertFromPdfAHandlerTests() + { + _mockProcessor = new Mock(); + _handler = new ConvertFromPdfACommandHandler(_mockProcessor.Object); + } + + [Fact] + public async Task Handle_ValidPdfA_ReturnsStandardPdfBytes() + { + // Arrange + var command = new ConvertFromPdfACommand { PdfStream = new MemoryStream(FakePdfBytes) }; + var expected = new byte[] { 0x25, 0x50, 0x44, 0x46, 0x0B }; + _mockProcessor.Setup(x => x.ConvertFromPdfAAsync(It.IsAny())).ReturnsAsync(expected); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Should().BeEquivalentTo(expected); + _mockProcessor.Verify(x => x.ConvertFromPdfAAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_ProcessorThrowsBadRequest_PropagatesException() + { + // Arrange + var command = new ConvertFromPdfACommand { PdfStream = new MemoryStream(FakePdfBytes) }; + _mockProcessor.Setup(x => x.ConvertFromPdfAAsync(It.IsAny())) + .ThrowsAsync(new BadRequestException("PDF stream must be at position 0")); + + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => _handler.Handle(command, CancellationToken.None)); + ex.Message.Should().Contain("position 0"); + } + + [Fact] + public async Task Handle_ProcessorThrowsGenericException_PropagatesException() + { + // Arrange + var command = new ConvertFromPdfACommand { PdfStream = new MemoryStream(FakePdfBytes) }; + _mockProcessor.Setup(x => x.ConvertFromPdfAAsync(It.IsAny())) + .ThrowsAsync(new Exception("Unexpected error")); + + // Act & Assert + await Assert.ThrowsAsync(() => _handler.Handle(command, CancellationToken.None)); + } +} diff --git a/DocumentOperator.Tests/Unit/Application/Features/ExtractAttachments/ExtractPdfAttachmentsHandlerTests.cs b/DocumentOperator.Tests/Unit/Application/Features/ExtractAttachments/ExtractPdfAttachmentsHandlerTests.cs new file mode 100644 index 0000000..18a1845 --- /dev/null +++ b/DocumentOperator.Tests/Unit/Application/Features/ExtractAttachments/ExtractPdfAttachmentsHandlerTests.cs @@ -0,0 +1,80 @@ +using DocumentService.Application.Common.Interfaces; +using DocumentService.Application.ExtractPdfAttachments; +using DocumentService.Domain.Common.Exceptions; +using FluentAssertions; +using Moq; +using Xunit; + +namespace DocumentService.Tests.Unit.Application.Features.ExtractAttachments; + +public class ExtractPdfAttachmentsHandlerTests +{ + private readonly Mock _mockProcessor; + private readonly ExtractPdfAttachmentsCommandHandler _handler; + private static readonly byte[] FakePdfBytes = "%PDF"u8.ToArray(); + + public ExtractPdfAttachmentsHandlerTests() + { + _mockProcessor = new Mock(); + _handler = new ExtractPdfAttachmentsCommandHandler(_mockProcessor.Object); + } + + [Fact] + public async Task Handle_PdfWithAttachments_ReturnsZipBytes() + { + // Arrange + var command = new ExtractPdfAttachmentsCommand { PdfStream = new MemoryStream(FakePdfBytes) }; + // ZIP magic bytes PK + var expected = new byte[] { 0x50, 0x4B, 0x03, 0x04, 0x00, 0x00 }; + _mockProcessor.Setup(x => x.ExtractAttachmentsAsync(It.IsAny())).ReturnsAsync(expected); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Should().NotBeEmpty(); + result[0].Should().Be(0x50, "ZIP starts with 'P'"); + result[1].Should().Be(0x4B, "ZIP continues with 'K'"); + _mockProcessor.Verify(x => x.ExtractAttachmentsAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_PdfWithNoAttachments_ThrowsNotFoundException() + { + // Arrange + var command = new ExtractPdfAttachmentsCommand { PdfStream = new MemoryStream(FakePdfBytes) }; + _mockProcessor.Setup(x => x.ExtractAttachmentsAsync(It.IsAny())) + .ThrowsAsync(new NotFoundException("PDF does not contain any attachments")); + + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => _handler.Handle(command, CancellationToken.None)); + ex.Message.Should().Contain("attachments"); + } + + [Fact] + public async Task Handle_EmptyStream_ThrowsBadRequestException() + { + // Arrange + var command = new ExtractPdfAttachmentsCommand { PdfStream = new MemoryStream(FakePdfBytes) }; + _mockProcessor.Setup(x => x.ExtractAttachmentsAsync(It.IsAny())) + .ThrowsAsync(new BadRequestException("PDF stream cannot be empty")); + + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => _handler.Handle(command, CancellationToken.None)); + ex.Message.Should().Contain("empty"); + } + + [Fact] + public async Task Handle_ProcessorThrowsGenericException_PropagatesException() + { + // Arrange + var command = new ExtractPdfAttachmentsCommand { PdfStream = new MemoryStream(FakePdfBytes) }; + _mockProcessor.Setup(x => x.ExtractAttachmentsAsync(It.IsAny())) + .ThrowsAsync(new Exception("Unexpected error")); + + // Act & Assert + await Assert.ThrowsAsync(() => _handler.Handle(command, CancellationToken.None)); + } +} diff --git a/DocumentOperator.Tests/Unit/Application/Features/MergePdfs/MergePdfsCommandHandlerTests.cs b/DocumentOperator.Tests/Unit/Application/Features/MergePdfs/MergePdfsCommandHandlerTests.cs new file mode 100644 index 0000000..fe91f60 --- /dev/null +++ b/DocumentOperator.Tests/Unit/Application/Features/MergePdfs/MergePdfsCommandHandlerTests.cs @@ -0,0 +1,95 @@ +using DocumentService.Application.Common.Interfaces; +using DocumentService.Application.MergePdfs; +using DocumentService.Domain.Common.Exceptions; +using FluentAssertions; +using Moq; +using Xunit; + +namespace DocumentService.Tests.Unit.Application.Features.MergePdfs; + +public class MergePdfsCommandHandlerTests +{ + private readonly Mock _mockProcessor; + private readonly MergePdfsCommandHandler _handler; + + public MergePdfsCommandHandlerTests() + { + _mockProcessor = new Mock(); + _handler = new MergePdfsCommandHandler(_mockProcessor.Object); + } + + [Fact] + public async Task Handle_TwoStreams_ReturnsMergedBytes() + { + // Arrange + var stream1 = new MemoryStream("%PDF-1"u8.ToArray()); + var stream2 = new MemoryStream("%PDF-2"u8.ToArray()); + var command = new MergePdfsCommand { PdfStreams = [stream1, stream2] }; + var expected = "%PDF-merged"u8.ToArray(); + + _mockProcessor.Setup(x => x.MergePdfsAsync(It.IsAny>(), null)) + .ReturnsAsync(expected); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull().And.NotBeEmpty(); + result.Should().BeEquivalentTo(expected); + _mockProcessor.Verify(x => x.MergePdfsAsync(It.IsAny>(), null), Times.Once); + } + + [Fact] + public async Task Handle_WithPageRanges_PassesRangesToProcessor() + { + // Arrange + var stream1 = new MemoryStream("%PDF-1"u8.ToArray()); + var stream2 = new MemoryStream("%PDF-2"u8.ToArray()); + var pageRanges = new List { "1-2", null }; + var command = new MergePdfsCommand { PdfStreams = [stream1, stream2], PageRanges = pageRanges }; + var expected = new byte[] { 0x25, 0x50, 0x44, 0x46 }; + + _mockProcessor.Setup(x => x.MergePdfsAsync(It.IsAny>(), pageRanges)) + .ReturnsAsync(expected); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Should().BeEquivalentTo(expected); + _mockProcessor.Verify(x => x.MergePdfsAsync(It.IsAny>(), pageRanges), Times.Once); + } + + [Fact] + public async Task Handle_ProcessorThrowsBadRequest_PropagatesException() + { + // Arrange + var command = new MergePdfsCommand + { + PdfStreams = [new MemoryStream("%PDF-1"u8.ToArray()), new MemoryStream("%PDF-2"u8.ToArray())] + }; + _mockProcessor.Setup(x => x.MergePdfsAsync(It.IsAny>(), null)) + .ThrowsAsync(new BadRequestException("At least 2 PDF files are required")); + + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => _handler.Handle(command, CancellationToken.None)); + ex.Message.Should().Be("At least 2 PDF files are required"); + } + + [Fact] + public async Task Handle_ProcessorThrowsGenericException_PropagatesException() + { + // Arrange + var command = new MergePdfsCommand + { + PdfStreams = [new MemoryStream("%PDF"u8.ToArray()), new MemoryStream("%PDF"u8.ToArray())] + }; + _mockProcessor.Setup(x => x.MergePdfsAsync(It.IsAny>(), null)) + .ThrowsAsync(new InvalidOperationException("Unexpected error")); + + // Act & Assert + await Assert.ThrowsAsync( + () => _handler.Handle(command, CancellationToken.None)); + } +}