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
}