using System.Reflection; using DocumentService.Application.Common.Interfaces; using DocumentService.Domain.Common.Exceptions; using DocumentService.Infrastructure.Services.QrCodeProcessing; using FluentAssertions; namespace DocumentService.Tests.Unit.Infrastructure.Services.QrCodeProcessing; /// /// Unit tests for DevExpressSwissQrCodeProcessor. /// Tests Swiss QR Code extraction and parsing from PDFs. /// public class DevExpressSwissQrCodeProcessorTests { private readonly ISwissQrCodeProcessor _sut; // SUT = System Under Test public DevExpressSwissQrCodeProcessorTests() { // Arrange: Create instance _sut = new DevExpressSwissQrCodeProcessor(); } #region Helper Methods /// /// Loads a test PDF from embedded resources as a Stream. /// /// Name of the PDF file (e.g., "pdfWithSwissQRCode.pdf") /// PDF content as MemoryStream private static Stream LoadTestPdf(string filename) { var assembly = Assembly.GetExecutingAssembly(); var resourceName = $"DocumentService.Tests.TestData.Pdfs.{filename}"; var stream = assembly.GetManifestResourceStream(resourceName); if (stream == null) { throw new FileNotFoundException( $"Embedded resource '{resourceName}' not found. " + $"Available resources: {string.Join(", ", assembly.GetManifestResourceNames())}"); } // Copy to MemoryStream so caller can reuse/seek var memoryStream = new MemoryStream(); stream.CopyTo(memoryStream); memoryStream.Position = 0; // Reset position for reading return memoryStream; } #endregion #region ExtractSwissQrCodeAsync Tests [Fact(Skip = "Current test PDF uses QR format not compatible with ZXing library. Requires real Swiss QR Bill PDF from https://www.swiss-qr-invoice.org/")] public async Task ExtractSwissQrCodeAsync_PdfWithSwissQrCode_ReturnsQrCodeData() { // Arrange using var pdfStream = LoadTestPdf("pdfWithSwissQRCode.pdf"); // Act var (bill, rawLines) = await _sut.ExtractSwissQrCodeAsync(pdfStream); // Assert bill.Should().NotBeNull("PDF contains a Swiss QR Code"); bill.Account.Should().NotBeNullOrEmpty("QR Code should contain IBAN"); bill.Currency.Should().NotBeNullOrEmpty("QR Code should contain currency"); bill.Creditor.Should().NotBeNull("QR Code should contain creditor"); bill.Creditor.Name.Should().NotBeNullOrEmpty("QR Code should contain creditor name"); rawLines.Should().NotBeEmpty("Raw lines should contain parsed QR code text"); } [Fact(Skip = "Test PDF incompatible with ZXing - requires real Swiss QR Bill PDF")] public async Task ExtractSwissQrCodeAsync_PdfWithSwissQrCode_ReturnsValidIban() { // Arrange using var pdfStream = LoadTestPdf("pdfWithSwissQRCode.pdf"); // Act var (bill, _) = await _sut.ExtractSwissQrCodeAsync(pdfStream); // Assert bill.Account.Should().MatchRegex(@"^CH\d{2}[A-Z0-9]{17}$", "Swiss IBAN should match pattern: CH + 2 digits + 17 chars"); } [Fact(Skip = "Test PDF incompatible with ZXing - requires real Swiss QR Bill PDF")] public async Task ExtractSwissQrCodeAsync_PdfWithSwissQrCode_ReturnsCurrency() { // Arrange using var pdfStream = LoadTestPdf("pdfWithSwissQRCode.pdf"); // Act var (bill, _) = await _sut.ExtractSwissQrCodeAsync(pdfStream); // Assert bill.Currency.Should().BeOneOf("CHF", "EUR", "Swiss QR Bill supports CHF and EUR currencies"); } [Fact] public async Task ExtractSwissQrCodeAsync_PdfWithoutQrCode_ThrowsSwissQrCodeNotFoundException() { // Arrange: valid.pdf doesn't contain a Swiss QR Code using var pdfStream = LoadTestPdf("valid.pdf"); // Act & Assert var act = async () => await _sut.ExtractSwissQrCodeAsync(pdfStream); await act.Should().ThrowAsync() .WithMessage("*No valid Swiss QR Code found*"); } [Fact] public async Task ExtractSwissQrCodeAsync_EmptyPdf_ThrowsException() { // Arrange: Empty stream using var emptyStream = new MemoryStream(); // Act & Assert var act = async () => await _sut.ExtractSwissQrCodeAsync(emptyStream); await act.Should().ThrowAsync() .Where(ex => ex is ArgumentException); } [Fact] public async Task ExtractSwissQrCodeAsync_NullInput_ThrowsArgumentNullException() { // Arrange Stream nullStream = null!; // Act & Assert var act = async () => await _sut.ExtractSwissQrCodeAsync(nullStream); await act.Should().ThrowAsync(); } [Fact] public async Task ExtractSwissQrCodeAsync_InvalidPdfBytes_ThrowsPdfProcessingException() { // Arrange: Random bytes that are not a valid PDF using var invalidStream = new MemoryStream("This is not a PDF file"u8.ToArray()); // Act & Assert var act = async () => await _sut.ExtractSwissQrCodeAsync(invalidStream); await act.Should().ThrowAsync(); } [Fact] public async Task ExtractSwissQrCodeAsync_StreamNotAtBeginning_ThrowsBadRequestException() { // Arrange: Valid PDF but stream position is not at 0 using var pdfStream = LoadTestPdf("valid.pdf"); pdfStream.Position = 10; // Move position away from beginning // Act & Assert var act = async () => await _sut.ExtractSwissQrCodeAsync(pdfStream); await act.Should().ThrowAsync(); } #endregion #region Swiss QR Code Content Validation Tests [Fact(Skip = "Test PDF incompatible with ZXing - requires real Swiss QR Bill PDF")] public async Task ExtractSwissQrCodeAsync_ValidQrCode_ParsesCreditorInformation() { // Arrange using var pdfStream = LoadTestPdf("pdfWithSwissQRCode.pdf"); // Act var (bill, _) = await _sut.ExtractSwissQrCodeAsync(pdfStream); // Assert bill.Creditor.Should().NotBeNull("Creditor information is required"); bill.Creditor.Name.Should().NotBeNullOrEmpty("Creditor name is required"); // Swiss QR Bill allows optional address fields } [Fact(Skip = "Test PDF incompatible with ZXing - requires real Swiss QR Bill PDF")] public async Task ExtractSwissQrCodeAsync_ValidQrCode_ParsesDebtorInformationIfPresent() { // Arrange using var pdfStream = LoadTestPdf("pdfWithSwissQRCode.pdf"); // Act var (bill, _) = await _sut.ExtractSwissQrCodeAsync(pdfStream); // Assert // Debtor information is OPTIONAL in Swiss QR Bill Standard 2.0 // So we just check that the property exists (can be null) bill.Should().NotBeNull(); // If debtor is present, it should have a name if (bill.Debtor != null) { bill.Debtor.Name.Should().NotBeNullOrWhiteSpace(); } } [Fact(Skip = "Test PDF incompatible with ZXing - requires real Swiss QR Bill PDF")] public async Task ExtractSwissQrCodeAsync_ValidQrCode_ParsesAmountIfPresent() { // Arrange using var pdfStream = LoadTestPdf("pdfWithSwissQRCode.pdf"); // Act var (bill, _) = await _sut.ExtractSwissQrCodeAsync(pdfStream); // Assert // Amount is OPTIONAL in Swiss QR Bill (can be 0.00 or null for payment slips) if (bill.Amount.HasValue) { bill.Amount.Value.Should().BeGreaterOrEqualTo(0.01m, "If amount is specified, it should be >= 0.01"); } } #endregion }