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; /// /// Unit tests for . /// Both and are mocked – /// no real HTTP calls are made. Tests cover every overload, every branch, and all edge cases. /// public class WorkflowsClientTests { // ─── test doubles ──────────────────────────────────────────────────────── private readonly Mock _validation = new(); private readonly Mock _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())) .ReturnsAsync(ValidResult()); _swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, It.IsAny())) .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()), Times.Once); _swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, false, It.IsAny()), Times.Once); } [Fact] public async Task Inspect_Bytes_InvalidPdf_ReturnsValidationWithNullQrCode_NeverThrows() { // Arrange var pdfBytes = EncryptedPdfBytes(); _validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny())) .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(), It.IsAny(), It.IsAny()), 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())) .ReturnsAsync(ValidResult()); _swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, It.IsAny())) .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())) .ReturnsAsync(ValidResult()); _swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, true, It.IsAny())) .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()), 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(), It.IsAny())) .ReturnsAsync(ValidResult()); _swissQrCode.Setup(q => q.ExtractAsync(It.IsAny(), false, It.IsAny())) .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(), It.IsAny())) .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(), It.IsAny(), It.IsAny()), 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(), It.IsAny())) .ReturnsAsync(ValidResult()); _swissQrCode.Setup(q => q.ExtractAsync(It.IsAny(), false, It.IsAny())) .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(), It.IsAny())) .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); } } }