Refactored `ExtractSwissQrCodeAsync` to `InspectSwissQrCodeAsync`, introducing `SwissQrCodeResult` to encapsulate both validation and extraction results. Removed `throwIfInvalid` parameter, ensuring validation details are always returned without exceptions. Added extension methods for file path handling to improve usability. Updated tests to reflect the new behavior, ensuring extraction is never attempted on invalid documents and validation results are always populated. Simplified API surface by removing redundant overloads and improving stream handling. Enhanced documentation and performed general code cleanup for better maintainability.
257 lines
11 KiB
C#
257 lines
11 KiB
C#
using DocumentService.Application.Common.DTOs;
|
||
using DocumentService.Client.Clients;
|
||
using DocumentService.Client.Interfaces;
|
||
using FluentAssertions;
|
||
using Moq;
|
||
using System.Net.Http;
|
||
using System.Reflection;
|
||
|
||
namespace DocumentService.Tests.Unit.Client;
|
||
|
||
/// <summary>
|
||
/// Unit tests for <see cref="WorkflowsClient"/>.
|
||
/// Both <see cref="IPdfValidationClient"/> and <see cref="ISwissQrCodeClient"/> are mocked –
|
||
/// no real HTTP calls are made. Tests cover every overload, every branch, and all edge cases.
|
||
/// </summary>
|
||
public class WorkflowsClientTests
|
||
{
|
||
// ─── test doubles ────────────────────────────────────────────────────────
|
||
|
||
private readonly Mock<IPdfValidationClient> _validation = new();
|
||
private readonly Mock<ISwissQrCodeClient> _swissQrCode = new();
|
||
private WorkflowsClient Sut => new(_validation.Object, _swissQrCode.Object);
|
||
// Extension methods are defined on IWorkflowsClient — use this for filePath overload tests.
|
||
private IWorkflowsClient SutAsInterface => Sut;
|
||
|
||
// ─── fixture data ─────────────────────────────────────────────────────────
|
||
|
||
private static byte[] ValidPdfBytes() => LoadTestPdf("valid.pdf");
|
||
private static byte[] EncryptedPdfBytes() => LoadTestPdf("encryptedPdf.pdf");
|
||
private static byte[] QrPdfBytes() => LoadTestPdf("pdfWithSwissQRCode.pdf");
|
||
|
||
private static byte[] LoadTestPdf(string filename)
|
||
{
|
||
var assembly = Assembly.GetExecutingAssembly();
|
||
var resourceName = $"DocumentService.Tests.TestData.Pdfs.{filename}";
|
||
using var stream = assembly.GetManifestResourceStream(resourceName)
|
||
?? throw new FileNotFoundException(
|
||
$"Embedded resource '{resourceName}' not found. " +
|
||
$"Available: {string.Join(", ", assembly.GetManifestResourceNames())}");
|
||
using var ms = new MemoryStream();
|
||
stream.CopyTo(ms);
|
||
return ms.ToArray();
|
||
}
|
||
|
||
// ─── valid PdfValidationResult helpers ───────────────────────────────────
|
||
|
||
private static PdfValidationResult ValidResult() =>
|
||
new(PageCount: 1, FileSizeBytes: 1024, FileSizeMB: 0.001,
|
||
PdfVersion: "1.4", HasAttachments: false, AttachmentCount: 0,
|
||
IsEncrypted: false);
|
||
|
||
private static PdfValidationResult InvalidResult_Encrypted() =>
|
||
new(PageCount: 1, FileSizeBytes: 1024, FileSizeMB: 0.001,
|
||
PdfVersion: "1.4", HasAttachments: false, AttachmentCount: 0,
|
||
IsEncrypted: true);
|
||
|
||
private static PdfValidationResult InvalidResult_EmptyFile() =>
|
||
new(PageCount: 0, FileSizeBytes: 0, FileSizeMB: 0,
|
||
PdfVersion: "", HasAttachments: false, AttachmentCount: 0,
|
||
IsEncrypted: false);
|
||
|
||
private static SwissQrCodeExtractionResult QrResult() =>
|
||
new(
|
||
Bill: new SwissQrBillDto
|
||
{
|
||
Version = "V2_0",
|
||
Currency = "CHF",
|
||
Amount = 630.20m,
|
||
Account = "CH9300762011623852957",
|
||
Creditor = new AddressDto { Name = "ALMAT AG", Town = "Tagelswangen", CountryCode = "CH", Type = "S", PostalCode = "8317" },
|
||
ReferenceType = "QRR",
|
||
Reference = "000000000000000000252080824"
|
||
},
|
||
RawLines: ["SPC", "0200", "1"]
|
||
);
|
||
|
||
private static SwissQrCodeExtractionResult RawQrResult() =>
|
||
new(Bill: (SwissQrBillDto?)null, RawLines: ["SPC", "0200", "1", "CH9300762011623852957"]);
|
||
|
||
|
||
[Fact]
|
||
public async Task Inspect_Bytes_ValidPdf_ReturnsBothValidationAndQrCode()
|
||
{
|
||
// Arrange
|
||
var pdfBytes = ValidPdfBytes();
|
||
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(ValidResult());
|
||
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(QrResult());
|
||
|
||
// Act
|
||
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes);
|
||
|
||
// Assert
|
||
result.Validation.IsValid.Should().BeTrue();
|
||
result.QrCode.Should().NotBeNull();
|
||
result.QrCode!.Bill!.Currency.Should().Be("CHF");
|
||
_validation.Verify(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()), Times.Once);
|
||
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, false, It.IsAny<CancellationToken>()), Times.Once);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Inspect_Bytes_InvalidPdf_ReturnsValidationWithNullQrCode_NeverThrows()
|
||
{
|
||
// Arrange
|
||
var pdfBytes = EncryptedPdfBytes();
|
||
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(InvalidResult_Encrypted());
|
||
|
||
// Act
|
||
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes);
|
||
|
||
// Assert – no exception, validation details always available
|
||
result.Validation.IsValid.Should().BeFalse();
|
||
result.Validation.IsEncrypted.Should().BeTrue();
|
||
result.QrCode.Should().BeNull("extraction must not run on an invalid document");
|
||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Inspect_Bytes_ValidationAlwaysPopulated_RegardlessOfQrOutcome()
|
||
{
|
||
// Arrange – valid PDF but QR extraction returns empty lines (no QR code found)
|
||
var pdfBytes = ValidPdfBytes();
|
||
var emptyQr = new SwissQrCodeExtractionResult(Bill: (SwissQrBillDto?)null, RawLines: []);
|
||
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(ValidResult());
|
||
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(emptyQr);
|
||
|
||
// Act
|
||
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes);
|
||
|
||
// Assert – validation is always there even when QR is empty
|
||
result.Validation.Should().NotBeNull();
|
||
result.Validation.PageCount.Should().Be(1);
|
||
result.QrCode.Should().NotBeNull(); // extraction was attempted
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Inspect_Bytes_RawMode_PassesRawFlagToExtraction()
|
||
{
|
||
// Arrange
|
||
var pdfBytes = QrPdfBytes();
|
||
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(ValidResult());
|
||
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, true, It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(RawQrResult());
|
||
|
||
// Act
|
||
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes, raw: true);
|
||
|
||
// Assert
|
||
result.QrCode!.RawLines.Should().Contain("SPC");
|
||
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, true, It.IsAny<CancellationToken>()), Times.Once);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Inspect_Bytes_CancellationToken_ForwardedToBothClients()
|
||
{
|
||
// Arrange
|
||
var pdfBytes = ValidPdfBytes();
|
||
var cts = new CancellationTokenSource();
|
||
var token = cts.Token;
|
||
_validation.Setup(v => v.ValidateAsync(pdfBytes, token)).ReturnsAsync(ValidResult());
|
||
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, token)).ReturnsAsync(QrResult());
|
||
|
||
// Act
|
||
await Sut.InspectSwissQrCodeAsync(pdfBytes, cancellationToken: token);
|
||
|
||
// Assert
|
||
_validation.Verify(v => v.ValidateAsync(pdfBytes, token), Times.Once);
|
||
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, false, token), Times.Once);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Inspect_Stream_ValidPdf_ReturnsBothResults()
|
||
{
|
||
// Arrange
|
||
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(ValidResult());
|
||
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(QrResult());
|
||
|
||
using var pdfStream = new MemoryStream(ValidPdfBytes());
|
||
|
||
// Act
|
||
var result = await Sut.InspectSwissQrCodeAsync(pdfStream);
|
||
|
||
// Assert
|
||
result.Validation.IsValid.Should().BeTrue();
|
||
result.QrCode.Should().NotBeNull();
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Inspect_Stream_InvalidPdf_ReturnsValidationWithNullQrCode_NeverThrows()
|
||
{
|
||
// Arrange
|
||
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(InvalidResult_Encrypted());
|
||
|
||
using var pdfStream = new MemoryStream(EncryptedPdfBytes());
|
||
|
||
// Act
|
||
var result = await Sut.InspectSwissQrCodeAsync(pdfStream);
|
||
|
||
// Assert
|
||
result.Validation.IsValid.Should().BeFalse();
|
||
result.QrCode.Should().BeNull();
|
||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<Stream>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Inspect_FilePath_ValidPdf_ReturnsBothResults()
|
||
{
|
||
// Arrange – extension opens a FileStream so mock expects Stream overload
|
||
var tempPath = Path.GetTempFileName();
|
||
try
|
||
{
|
||
await File.WriteAllBytesAsync(tempPath, ValidPdfBytes());
|
||
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(ValidResult());
|
||
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(QrResult());
|
||
|
||
// Act – calls extension method which opens a FileStream and delegates to Stream overload
|
||
var result = await SutAsInterface.InspectSwissQrCodeAsync(tempPath);
|
||
|
||
// Assert
|
||
result.Validation.IsValid.Should().BeTrue();
|
||
result.QrCode.Should().NotBeNull();
|
||
}
|
||
finally { File.Delete(tempPath); }
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Inspect_FilePath_InvalidPdf_ReturnsValidationWithNullQrCode_NeverThrows()
|
||
{
|
||
// Arrange
|
||
var tempPath = Path.GetTempFileName();
|
||
try
|
||
{
|
||
await File.WriteAllBytesAsync(tempPath, EncryptedPdfBytes());
|
||
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(InvalidResult_Encrypted());
|
||
|
||
// Act
|
||
var result = await SutAsInterface.InspectSwissQrCodeAsync(tempPath);
|
||
|
||
// Assert – never throws, validation details always populated
|
||
result.Validation.IsValid.Should().BeFalse();
|
||
result.QrCode.Should().BeNull();
|
||
}
|
||
finally { File.Delete(tempPath); }
|
||
}
|
||
|
||
} |