Files
DocumentService/DocumentService.Tests/Unit/Client/WorkflowsClientTests.cs
TekH 092d283a2b Refactor WorkflowsClient for unified result handling
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.
2026-08-31 14:44:57 +02:00

257 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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); }
}
}