Updated project and solution files to rename and restructure `DocumentOperator` to `DocumentService`. Modified project references in `DocumentService.API.csproj`, `DocumentService.Application.csproj`, and `DocumentService.Infrastructure.csproj` to reflect the new naming convention. Updated `DocumentService.sln` to remove old project references, add new ones, and adjust configuration and nested project mappings accordingly. This change aligns the project structure with the new naming convention.
224 lines
7.7 KiB
C#
224 lines
7.7 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Unit tests for DevExpressSwissQrCodeProcessor.
|
|
/// Tests Swiss QR Code extraction and parsing from PDFs.
|
|
/// </summary>
|
|
public class DevExpressSwissQrCodeProcessorTests
|
|
{
|
|
private readonly ISwissQrCodeProcessor _sut; // SUT = System Under Test
|
|
|
|
public DevExpressSwissQrCodeProcessorTests()
|
|
{
|
|
// Arrange: Create instance
|
|
_sut = new DevExpressSwissQrCodeProcessor();
|
|
}
|
|
|
|
#region Helper Methods
|
|
|
|
/// <summary>
|
|
/// Loads a test PDF from embedded resources as a Stream.
|
|
/// </summary>
|
|
/// <param name="filename">Name of the PDF file (e.g., "pdfWithSwissQRCode.pdf")</param>
|
|
/// <returns>PDF content as MemoryStream</returns>
|
|
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<NotFoundException>()
|
|
.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<Exception>()
|
|
.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<ArgumentNullException>();
|
|
}
|
|
|
|
[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<ArgumentException>();
|
|
}
|
|
|
|
[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<BadRequestException>();
|
|
}
|
|
|
|
#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
|
|
}
|
|
|