test: add Application handler tests for all missing features

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
This commit is contained in:
2026-08-28 11:46:18 +02:00
parent 9040aed1da
commit fbaefe1fd2
5 changed files with 555 additions and 0 deletions

View File

@@ -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<IPdfProcessor> _mockProcessor;
private readonly AddAnnotationHandler _handler;
private static readonly byte[] FakePdfBytes = "%PDF"u8.ToArray();
public AddAnnotationHandlerTests()
{
_mockProcessor = new Mock<IPdfProcessor>();
_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<Stream>(), 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<Stream>(), 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<Stream>(), 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<Stream>(), It.IsAny<AnnotationType>(), It.IsAny<int>(),
It.IsAny<(double, double, double, double)>(),
It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(),
It.IsAny<TextMarkupStyle?>(), It.IsAny<AnnotationOrigin>()))
.ThrowsAsync(new BadRequestException("Page number 99 exceeds document page count"));
// Act & Assert
var ex = await Assert.ThrowsAsync<BadRequestException>(
() => _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<Stream>(), It.IsAny<AnnotationType>(), It.IsAny<int>(),
It.IsAny<(double, double, double, double)>(),
It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<string?>(),
It.IsAny<TextMarkupStyle?>(), It.IsAny<AnnotationOrigin>()))
.ThrowsAsync(new Exception("Unexpected error"));
// Act & Assert
await Assert.ThrowsAsync<Exception>(() => _handler.Handle(command, CancellationToken.None));
}
}

View File

@@ -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<IPdfProcessor> _mockProcessor;
private readonly AddStampHandler _handler;
private static readonly byte[] FakePdfBytes = "%PDF"u8.ToArray();
public AddStampHandlerTests()
{
_mockProcessor = new Mock<IPdfProcessor>();
_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<Stream>(), 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<Stream>(), 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<Stream>(), 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<Stream>(), It.IsAny<StampType>(), It.IsAny<int[]?>(),
It.IsAny<(double, double)>(), It.IsAny<(double, double)?>(),
It.IsAny<AnnotationOrigin>(), It.IsAny<string?>(), It.IsAny<string?>(),
It.IsAny<double?>(), It.IsAny<string?>(), It.IsAny<double?>(),
It.IsAny<double?>(), It.IsAny<StampPlacement>(),
It.IsAny<byte[]?>(), It.IsAny<PredefinedStampType?>()))
.ThrowsAsync(new BadRequestException("Opacity must be between 0.0 and 1.0"));
// Act & Assert
var ex = await Assert.ThrowsAsync<BadRequestException>(
() => _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<Stream>(), It.IsAny<StampType>(), It.IsAny<int[]?>(),
It.IsAny<(double, double)>(), It.IsAny<(double, double)?>(),
It.IsAny<AnnotationOrigin>(), It.IsAny<string?>(), It.IsAny<string?>(),
It.IsAny<double?>(), It.IsAny<string?>(), It.IsAny<double?>(),
It.IsAny<double?>(), It.IsAny<StampPlacement>(),
It.IsAny<byte[]?>(), It.IsAny<PredefinedStampType?>()))
.ThrowsAsync(new Exception("Unexpected"));
// Act & Assert
await Assert.ThrowsAsync<Exception>(() => _handler.Handle(command, CancellationToken.None));
}
}

View File

@@ -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<IPdfProcessor> _mockProcessor;
private readonly ConvertToPdfACommandHandler _handler;
private static readonly byte[] FakePdfBytes = "%PDF"u8.ToArray();
public ConvertToPdfAHandlerTests()
{
_mockProcessor = new Mock<IPdfProcessor>();
_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<Stream>(), "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<Stream>(), "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<Stream>(), "PDF/A-1b"))
.ThrowsAsync(new NotImplementedException("PDF/A conversion not yet implemented"));
// Act & Assert
var ex = await Assert.ThrowsAsync<NotImplementedException>(
() => _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<Stream>(), "PDF/A-2b"))
.ThrowsAsync(new BadRequestException("PDF stream cannot be empty"));
// Act & Assert
var ex = await Assert.ThrowsAsync<BadRequestException>(
() => _handler.Handle(command, CancellationToken.None));
ex.Message.Should().Contain("empty");
}
}
public class ConvertFromPdfAHandlerTests
{
private readonly Mock<IPdfProcessor> _mockProcessor;
private readonly ConvertFromPdfACommandHandler _handler;
private static readonly byte[] FakePdfBytes = "%PDF"u8.ToArray();
public ConvertFromPdfAHandlerTests()
{
_mockProcessor = new Mock<IPdfProcessor>();
_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<Stream>())).ReturnsAsync(expected);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Should().BeEquivalentTo(expected);
_mockProcessor.Verify(x => x.ConvertFromPdfAAsync(It.IsAny<Stream>()), 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<Stream>()))
.ThrowsAsync(new BadRequestException("PDF stream must be at position 0"));
// Act & Assert
var ex = await Assert.ThrowsAsync<BadRequestException>(
() => _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<Stream>()))
.ThrowsAsync(new Exception("Unexpected error"));
// Act & Assert
await Assert.ThrowsAsync<Exception>(() => _handler.Handle(command, CancellationToken.None));
}
}

View File

@@ -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<IPdfProcessor> _mockProcessor;
private readonly ExtractPdfAttachmentsCommandHandler _handler;
private static readonly byte[] FakePdfBytes = "%PDF"u8.ToArray();
public ExtractPdfAttachmentsHandlerTests()
{
_mockProcessor = new Mock<IPdfProcessor>();
_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<Stream>())).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<Stream>()), 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<Stream>()))
.ThrowsAsync(new NotFoundException("PDF does not contain any attachments"));
// Act & Assert
var ex = await Assert.ThrowsAsync<NotFoundException>(
() => _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<Stream>()))
.ThrowsAsync(new BadRequestException("PDF stream cannot be empty"));
// Act & Assert
var ex = await Assert.ThrowsAsync<BadRequestException>(
() => _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<Stream>()))
.ThrowsAsync(new Exception("Unexpected error"));
// Act & Assert
await Assert.ThrowsAsync<Exception>(() => _handler.Handle(command, CancellationToken.None));
}
}

View File

@@ -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<IPdfProcessor> _mockProcessor;
private readonly MergePdfsCommandHandler _handler;
public MergePdfsCommandHandlerTests()
{
_mockProcessor = new Mock<IPdfProcessor>();
_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<IReadOnlyList<Stream>>(), 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<IReadOnlyList<Stream>>(), 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<string?> { "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<IReadOnlyList<Stream>>(), pageRanges))
.ReturnsAsync(expected);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Should().BeEquivalentTo(expected);
_mockProcessor.Verify(x => x.MergePdfsAsync(It.IsAny<IReadOnlyList<Stream>>(), 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<IReadOnlyList<Stream>>(), null))
.ThrowsAsync(new BadRequestException("At least 2 PDF files are required"));
// Act & Assert
var ex = await Assert.ThrowsAsync<BadRequestException>(
() => _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<IReadOnlyList<Stream>>(), null))
.ThrowsAsync(new InvalidOperationException("Unexpected error"));
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => _handler.Handle(command, CancellationToken.None));
}
}