diff --git a/DocumentService.Client/Clients/WorkflowsClient.cs b/DocumentService.Client/Clients/WorkflowsClient.cs
index 58a37a4..bc3e17a 100644
--- a/DocumentService.Client/Clients/WorkflowsClient.cs
+++ b/DocumentService.Client/Clients/WorkflowsClient.cs
@@ -1,5 +1,6 @@
using DocumentService.Application.Common.DTOs;
using DocumentService.Client.Interfaces;
+using DocumentService.Client.Models.Results;
namespace DocumentService.Client.Clients;
@@ -10,75 +11,63 @@ namespace DocumentService.Client.Clients;
public class WorkflowsClient(IPdfValidationClient validation, ISwissQrCodeClient swissQrCode) : IWorkflowsClient
{
///
- public async Task ExtractSwissQrCodeAsync(
+ public async Task InspectSwissQrCodeAsync(
byte[] pdfBytes,
bool raw = false,
- bool throwIfInvalid = true,
CancellationToken cancellationToken = default)
{
var validationResult = await validation.ValidateAsync(pdfBytes, cancellationToken);
-
- if (!validationResult.IsValid)
- return throwIfInvalid
- ? throw new InvalidOperationException(
- $"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " +
- $"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).")
- : (SwissQrCodeExtractionResult?)null;
-
- return await swissQrCode.ExtractAsync(pdfBytes, raw, cancellationToken);
+ var qrCode = validationResult.IsValid
+ ? await swissQrCode.ExtractAsync(pdfBytes, raw, cancellationToken)
+ : null;
+ return new SwissQrCodeResult(qrCode, validationResult);
}
///
- public async Task ExtractSwissQrCodeAsync(
+ public async Task InspectSwissQrCodeAsync(
Stream pdfStream,
bool raw = false,
- bool throwIfInvalid = true,
CancellationToken cancellationToken = default)
{
var validationResult = await validation.ValidateAsync(pdfStream, cancellationToken);
if (!validationResult.IsValid)
- return throwIfInvalid
- ? throw new InvalidOperationException(
- $"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " +
- $"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).")
- : (SwissQrCodeExtractionResult?)null;
+ return new SwissQrCodeResult(null, validationResult);
// Stream was consumed by validation – reset if possible, otherwise re-open is caller's responsibility.
- // Since callers typically pass a FileStream or MemoryStream, Position reset covers the common case.
if (pdfStream.CanSeek)
pdfStream.Position = 0;
- return await swissQrCode.ExtractAsync(pdfStream, raw, cancellationToken);
- }
-
- ///
- public async Task ExtractSwissQrCodeAsync(
- string filePath,
- bool raw = false,
- bool throwIfInvalid = true,
- CancellationToken cancellationToken = default)
- {
-#if NETFRAMEWORK
- using var stream = File.OpenRead(filePath);
- var validationResult = await validation.ValidateAsync(stream, cancellationToken);
-#else
- var pdfBytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
- var validationResult = await validation.ValidateAsync(pdfBytes, cancellationToken);
-#endif
-
- if (!validationResult.IsValid)
- return throwIfInvalid
- ? throw new InvalidOperationException(
- $"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " +
- $"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).")
- : (SwissQrCodeExtractionResult?)null;
-
-#if NETFRAMEWORK
- using var extractStream = File.OpenRead(filePath);
- return await swissQrCode.ExtractAsync(extractStream, raw, cancellationToken);
-#else
- return await swissQrCode.ExtractAsync(pdfBytes, raw, cancellationToken);
-#endif
+ var qrCode = await swissQrCode.ExtractAsync(pdfStream, raw, cancellationToken);
+ return new SwissQrCodeResult(qrCode, validationResult);
+ }
+}
+
+///
+/// Convenience extensions for that accept a file path as input.
+/// Defined alongside so they are discoverable without an additional using directive.
+///
+public static class WorkflowsClientExtensions
+{
+ ///
+ /// Validates the PDF at and attempts Swiss QR Code extraction
+ /// in a single call. Opens a and delegates to
+ /// .
+ ///
+ /// The workflows client.
+ /// Absolute or relative path to the PDF file.
+ ///
+ /// false (default) – returns a parsed Bill object.
+ /// true – returns raw QR text lines without parsing.
+ ///
+ /// Cancellation token.
+ public static async Task InspectSwissQrCodeAsync(
+ this IWorkflowsClient client,
+ string filePath,
+ bool raw = false,
+ CancellationToken cancellationToken = default)
+ {
+ using var stream = File.OpenRead(filePath);
+ return await client.InspectSwissQrCodeAsync(stream, raw, cancellationToken);
}
}
diff --git a/DocumentService.Client/Interfaces/IWorkflowsClient.cs b/DocumentService.Client/Interfaces/IWorkflowsClient.cs
index 25b957f..93400cc 100644
--- a/DocumentService.Client/Interfaces/IWorkflowsClient.cs
+++ b/DocumentService.Client/Interfaces/IWorkflowsClient.cs
@@ -1,4 +1,5 @@
using DocumentService.Application.Common.DTOs;
+using DocumentService.Client.Models.Results;
namespace DocumentService.Client.Interfaces;
@@ -9,51 +10,33 @@ namespace DocumentService.Client.Interfaces;
public interface IWorkflowsClient
{
///
- /// Validates the PDF and extracts its Swiss QR Code in a single call.
- /// Validation runs first; extraction is only attempted on valid documents.
+ /// Validates the PDF and attempts Swiss QR Code extraction in a single call,
+ /// returning both results together. The method never throws on invalid documents –
+ /// the caller receives the full regardless of outcome,
+ /// enabling detailed logging, diagnostics, or batch processing without try/catch.
///
/// PDF file as byte array.
///
/// false (default) – returns a parsed object.
/// true – returns raw QR text lines without parsing.
///
- ///
- /// true (default) – throws when the PDF fails validation.
- /// false – returns null instead, allowing the caller to handle the case without a try/catch.
- ///
/// Cancellation token.
///
- /// Swiss QR Code extraction result, or null if the document is invalid and
- /// is false.
+ /// A containing the
+ /// (always populated) and the (null if the
+ /// document was invalid).
///
- ///
- /// Thrown when the PDF fails validation and is true.
- ///
- Task ExtractSwissQrCodeAsync(
+ Task InspectSwissQrCodeAsync(
byte[] pdfBytes,
bool raw = false,
- bool throwIfInvalid = true,
CancellationToken cancellationToken = default);
- ///
+ ///
/// PDF file as a stream.
- ///
- ///
+ ///
/// Cancellation token.
- Task ExtractSwissQrCodeAsync(
+ Task InspectSwissQrCodeAsync(
Stream pdfStream,
bool raw = false,
- bool throwIfInvalid = true,
- CancellationToken cancellationToken = default);
-
- ///
- /// Absolute or relative path to the PDF file.
- ///
- ///
- /// Cancellation token.
- Task ExtractSwissQrCodeAsync(
- string filePath,
- bool raw = false,
- bool throwIfInvalid = true,
CancellationToken cancellationToken = default);
}
diff --git a/DocumentService.Client/Models/Results/SwissQrCodeResult.cs b/DocumentService.Client/Models/Results/SwissQrCodeResult.cs
new file mode 100644
index 0000000..9656353
--- /dev/null
+++ b/DocumentService.Client/Models/Results/SwissQrCodeResult.cs
@@ -0,0 +1,21 @@
+using DocumentService.Application.Common.DTOs;
+
+namespace DocumentService.Client.Models.Results;
+
+///
+/// Combined result of a PDF validation followed by a Swiss QR Code extraction.
+/// Both operations are always executed; is null when
+/// the document fails validation.
+///
+///
+/// Extracted Swiss QR Code data, or null if the PDF was invalid or did not
+/// contain a recognisable Swiss QR Code.
+///
+///
+/// PDF validation result. Always populated regardless of whether extraction succeeded.
+/// Use
+/// to determine whether the extraction was attempted.
+///
+public record SwissQrCodeResult(
+ SwissQrCodeExtractionResult? QrCode,
+ PdfValidationResult Validation);
diff --git a/DocumentService.Tests/Unit/Client/WorkflowsClientTests.cs b/DocumentService.Tests/Unit/Client/WorkflowsClientTests.cs
index 6dffc01..86343e8 100644
--- a/DocumentService.Tests/Unit/Client/WorkflowsClientTests.cs
+++ b/DocumentService.Tests/Unit/Client/WorkflowsClientTests.cs
@@ -20,6 +20,8 @@ public class WorkflowsClientTests
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 ─────────────────────────────────────────────────────────
@@ -75,12 +77,9 @@ public class WorkflowsClientTests
private static SwissQrCodeExtractionResult RawQrResult() =>
new(Bill: (SwissQrBillDto?)null, RawLines: ["SPC", "0200", "1", "CH9300762011623852957"]);
- // ═════════════════════════════════════════════════════════════════════════
- // byte[] overload
- // ═════════════════════════════════════════════════════════════════════════
[Fact]
- public async Task Bytes_ValidPdf_CallsValidationThenExtraction_ReturnsResult()
+ public async Task Inspect_Bytes_ValidPdf_ReturnsBothValidationAndQrCode()
{
// Arrange
var pdfBytes = ValidPdfBytes();
@@ -90,18 +89,56 @@ public class WorkflowsClientTests
.ReturnsAsync(QrResult());
// Act
- var result = await Sut.ExtractSwissQrCodeAsync(pdfBytes);
+ var result = await Sut.InspectSwissQrCodeAsync(pdfBytes);
// Assert
- result.Should().NotBeNull();
- result!.Bill.Should().NotBeNull();
- result.Bill!.Currency.Should().Be("CHF");
+ 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 Bytes_ValidPdf_RawMode_PassesRawFlagToExtraction()
+ 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();
@@ -111,109 +148,15 @@ public class WorkflowsClientTests
.ReturnsAsync(RawQrResult());
// Act
- var result = await Sut.ExtractSwissQrCodeAsync(pdfBytes, raw: true);
+ var result = await Sut.InspectSwissQrCodeAsync(pdfBytes, raw: true);
// Assert
- result.Should().NotBeNull();
- result!.RawLines.Should().Contain("SPC");
+ result.QrCode!.RawLines.Should().Contain("SPC");
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, true, It.IsAny()), Times.Once);
}
[Fact]
- public async Task Bytes_InvalidPdf_ThrowIfInvalidTrue_ThrowsInvalidOperationException()
- {
- // Arrange
- var pdfBytes = EncryptedPdfBytes();
- _validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny()))
- .ReturnsAsync(InvalidResult_Encrypted());
-
- // Act
- var act = async () => await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: true);
-
- // Assert
- await act.Should().ThrowAsync()
- .WithMessage("*PDF validation failed*");
- _swissQrCode.Verify(q => q.ExtractAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
- }
-
- [Fact]
- public async Task Bytes_InvalidPdf_ThrowIfInvalidFalse_ReturnsNull()
- {
- // Arrange
- var pdfBytes = EncryptedPdfBytes();
- _validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny()))
- .ReturnsAsync(InvalidResult_Encrypted());
-
- // Act
- var result = await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: false);
-
- // Assert
- result.Should().BeNull();
- _swissQrCode.Verify(q => q.ExtractAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
- }
-
- [Fact]
- public async Task Bytes_EmptyFilePdf_ThrowIfInvalidTrue_ThrowsInvalidOperationException()
- {
- // Arrange – PageCount=0 and FileSize=0 → IsValid=false
- var pdfBytes = new byte[] { 0x00 };
- _validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny()))
- .ReturnsAsync(InvalidResult_EmptyFile());
-
- // Act & Assert
- await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: true))
- .Should().ThrowAsync();
- }
-
- [Fact]
- public async Task Bytes_ExceptionMessage_ContainsValidationDetails()
- {
- // Arrange – ensure the message includes diagnostic fields
- var pdfBytes = ValidPdfBytes();
- _validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny()))
- .ReturnsAsync(InvalidResult_Encrypted());
-
- // Act
- var act = async () => await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: true);
-
- // Assert
- var ex = await act.Should().ThrowAsync();
- ex.WithMessage("*IsEncrypted=True*");
- }
-
- [Fact]
- public async Task Bytes_ValidationThrows_PropagatesHttpRequestException()
- {
- // Arrange – simulate network/API failure during validation
- var pdfBytes = ValidPdfBytes();
- _validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny()))
- .ThrowsAsync(new HttpRequestException("API unreachable"));
-
- // Act & Assert
- await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfBytes))
- .Should().ThrowAsync()
- .WithMessage("*API unreachable*");
- _swissQrCode.Verify(q => q.ExtractAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
- }
-
- [Fact]
- public async Task Bytes_ExtractionThrows_PropagatesHttpRequestException()
- {
- // Arrange – validation succeeds but extraction fails
- var pdfBytes = QrPdfBytes();
- _validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny()))
- .ReturnsAsync(ValidResult());
- _swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, It.IsAny()))
- .ThrowsAsync(new HttpRequestException("Extraction failed"));
-
- // Act & Assert
- await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfBytes))
- .Should().ThrowAsync()
- .WithMessage("*Extraction failed*");
- }
-
- [Fact]
- public async Task Bytes_ValidPdf_CancellationToken_IsForwardedToBothClients()
+ public async Task Inspect_Bytes_CancellationToken_ForwardedToBothClients()
{
// Arrange
var pdfBytes = ValidPdfBytes();
@@ -223,21 +166,17 @@ public class WorkflowsClientTests
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, token)).ReturnsAsync(QrResult());
// Act
- await Sut.ExtractSwissQrCodeAsync(pdfBytes, cancellationToken: token);
+ 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);
}
- // ═════════════════════════════════════════════════════════════════════════
- // Stream overload
- // ═════════════════════════════════════════════════════════════════════════
-
[Fact]
- public async Task Stream_ValidPdf_CallsValidationWithStreamThenExtractionWithStream_ReturnsResult()
+ public async Task Inspect_Stream_ValidPdf_ReturnsBothResults()
{
- // Arrange – validation client receives a Stream, extraction client receives the same stream reset to 0
+ // Arrange
_validation.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(ValidResult());
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny(), false, It.IsAny()))
@@ -246,53 +185,15 @@ public class WorkflowsClientTests
using var pdfStream = new MemoryStream(ValidPdfBytes());
// Act
- var result = await Sut.ExtractSwissQrCodeAsync(pdfStream);
+ var result = await Sut.InspectSwissQrCodeAsync(pdfStream);
// Assert
- result.Should().NotBeNull();
- _validation.Verify(v => v.ValidateAsync(It.IsAny(), It.IsAny()), Times.Once);
- _swissQrCode.Verify(q => q.ExtractAsync(It.IsAny(), false, It.IsAny()), Times.Once);
+ result.Validation.IsValid.Should().BeTrue();
+ result.QrCode.Should().NotBeNull();
}
[Fact]
- public async Task Stream_SeekableStream_IsResetBetweenValidationAndExtraction()
- {
- // Arrange – we verify the stream position is 0 when extraction is called
- Stream? streamAtExtraction = null;
- _validation.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny()))
- .ReturnsAsync(ValidResult());
- _swissQrCode.Setup(q => q.ExtractAsync(It.IsAny(), false, It.IsAny()))
- .Callback((s, _, _) => streamAtExtraction = s)
- .ReturnsAsync(QrResult());
-
- using var pdfStream = new MemoryStream(ValidPdfBytes());
-
- // Act
- await Sut.ExtractSwissQrCodeAsync(pdfStream);
-
- // Assert
- streamAtExtraction.Should().NotBeNull();
- streamAtExtraction!.Position.Should().Be(0, "stream must be rewound before extraction");
- }
-
- [Fact]
- public async Task Stream_InvalidPdf_ThrowIfInvalidTrue_ThrowsWithoutCallingExtraction()
- {
- // Arrange
- _validation.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny()))
- .ReturnsAsync(InvalidResult_Encrypted());
-
- using var pdfStream = new MemoryStream(EncryptedPdfBytes());
-
- // Act & Assert
- await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfStream, throwIfInvalid: true))
- .Should().ThrowAsync()
- .WithMessage("*PDF validation failed*");
- _swissQrCode.Verify(q => q.ExtractAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
- }
-
- [Fact]
- public async Task Stream_InvalidPdf_ThrowIfInvalidFalse_ReturnsNull()
+ public async Task Inspect_Stream_InvalidPdf_ReturnsValidationWithNullQrCode_NeverThrows()
{
// Arrange
_validation.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny()))
@@ -301,173 +202,56 @@ public class WorkflowsClientTests
using var pdfStream = new MemoryStream(EncryptedPdfBytes());
// Act
- var result = await Sut.ExtractSwissQrCodeAsync(pdfStream, throwIfInvalid: false);
+ var result = await Sut.InspectSwissQrCodeAsync(pdfStream);
// Assert
- result.Should().BeNull();
+ 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 Stream_ValidPdf_RawMode_PassesRawFlagToExtraction()
+ public async Task Inspect_FilePath_ValidPdf_ReturnsBothResults()
{
- // Arrange
- _validation.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny()))
- .ReturnsAsync(ValidResult());
- _swissQrCode.Setup(q => q.ExtractAsync(It.IsAny(), true, It.IsAny()))
- .ReturnsAsync(RawQrResult());
-
- using var pdfStream = new MemoryStream(QrPdfBytes());
-
- // Act
- var result = await Sut.ExtractSwissQrCodeAsync(pdfStream, raw: true);
-
- // Assert
- result.Should().NotBeNull();
- _swissQrCode.Verify(q => q.ExtractAsync(It.IsAny(), true, It.IsAny()), Times.Once);
- }
-
- // ═════════════════════════════════════════════════════════════════════════
- // filePath overload
- // ═════════════════════════════════════════════════════════════════════════
-
- [Fact]
- public async Task FilePath_ValidPdf_ReadsFileAndCallsBothClients_ReturnsResult()
- {
- // Arrange – write a real temp file so File.ReadAllBytesAsync / File.OpenRead can read it
+ // 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()))
+ _validation.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(ValidResult());
- _swissQrCode.Setup(q => q.ExtractAsync(It.IsAny(), false, It.IsAny()))
+ _swissQrCode.Setup(q => q.ExtractAsync(It.IsAny(), false, It.IsAny()))
.ReturnsAsync(QrResult());
- // Act
- var result = await Sut.ExtractSwissQrCodeAsync(tempPath);
+ // Act – calls extension method which opens a FileStream and delegates to Stream overload
+ var result = await SutAsInterface.InspectSwissQrCodeAsync(tempPath);
// Assert
- result.Should().NotBeNull();
- _validation.Verify(v => v.ValidateAsync(It.IsAny(), It.IsAny()), Times.Once);
- _swissQrCode.Verify(q => q.ExtractAsync(It.IsAny(), false, It.IsAny()), Times.Once);
- }
- finally
- {
- File.Delete(tempPath);
+ result.Validation.IsValid.Should().BeTrue();
+ result.QrCode.Should().NotBeNull();
}
+ finally { File.Delete(tempPath); }
}
[Fact]
- public async Task FilePath_InvalidPdf_ThrowIfInvalidTrue_ThrowsWithoutCallingExtraction()
+ 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 & Assert
- await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(tempPath, throwIfInvalid: true))
- .Should().ThrowAsync();
- _swissQrCode.Verify(q => q.ExtractAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
- }
- finally
- {
- File.Delete(tempPath);
- }
- }
-
- [Fact]
- public async Task FilePath_InvalidPdf_ThrowIfInvalidFalse_ReturnsNull()
- {
- // Arrange
- var tempPath = Path.GetTempFileName();
- try
- {
- await File.WriteAllBytesAsync(tempPath, EncryptedPdfBytes());
-
- _validation.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny()))
+ _validation.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(InvalidResult_Encrypted());
// Act
- var result = await Sut.ExtractSwissQrCodeAsync(tempPath, throwIfInvalid: false);
+ var result = await SutAsInterface.InspectSwissQrCodeAsync(tempPath);
- // Assert
- result.Should().BeNull();
- }
- finally
- {
- File.Delete(tempPath);
+ // Assert – never throws, validation details always populated
+ result.Validation.IsValid.Should().BeFalse();
+ result.QrCode.Should().BeNull();
}
+ finally { File.Delete(tempPath); }
}
- [Fact]
- public async Task FilePath_NonExistentFile_ThrowsFileNotFoundException()
- {
- // Arrange
- var nonExistent = Path.Combine(Path.GetTempPath(), "does_not_exist_12345.pdf");
-
- // Act & Assert
- await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(nonExistent))
- .Should().ThrowAsync();
- _validation.Verify(v => v.ValidateAsync(It.IsAny(), It.IsAny()), Times.Never);
- _swissQrCode.Verify(q => q.ExtractAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
- }
-
- [Fact]
- public async Task FilePath_ValidPdf_RawMode_PassesRawFlagToExtraction()
- {
- // Arrange
- var tempPath = Path.GetTempFileName();
- try
- {
- await File.WriteAllBytesAsync(tempPath, QrPdfBytes());
-
- _validation.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny()))
- .ReturnsAsync(ValidResult());
- _swissQrCode.Setup(q => q.ExtractAsync(It.IsAny(), true, It.IsAny()))
- .ReturnsAsync(RawQrResult());
-
- // Act
- var result = await Sut.ExtractSwissQrCodeAsync(tempPath, raw: true);
-
- // Assert
- result.Should().NotBeNull();
- _swissQrCode.Verify(q => q.ExtractAsync(It.IsAny(), true, It.IsAny()), Times.Once);
- }
- finally
- {
- File.Delete(tempPath);
- }
- }
-
- // ═════════════════════════════════════════════════════════════════════════
- // Orchestration contract: extraction must NEVER be called on invalid PDF
- // ═════════════════════════════════════════════════════════════════════════
-
- [Theory]
- [InlineData(true)]
- [InlineData(false)]
- public async Task AnyOverload_InvalidPdf_ExtractionIsNeverCalled_RegardlessOfThrowIfInvalid(bool throwIfInvalid)
- {
- // Arrange
- var pdfBytes = EncryptedPdfBytes();
- _validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny()))
- .ReturnsAsync(InvalidResult_Encrypted());
-
- // Act
- try { await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: throwIfInvalid); }
- catch (InvalidOperationException) { /* expected when throwIfInvalid=true */ }
-
- // Assert — extraction must never be called in either branch
- _swissQrCode.Verify(
- q => q.ExtractAsync(It.IsAny(), It.IsAny(), It.IsAny()),
- Times.Never,
- "extraction must not be attempted on an invalid document");
- }
-}
+}
\ No newline at end of file