Compare commits
6 Commits
f9d4e53ddc
...
a618e3559a
| Author | SHA1 | Date | |
|---|---|---|---|
| a618e3559a | |||
| 68eeeb1eda | |||
| 0e22ffb2c5 | |||
| 62ab576e69 | |||
| 092d283a2b | |||
| 71dd32f11c |
@@ -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,34 @@ namespace DocumentService.Client.Clients;
|
||||
public class WorkflowsClient(IPdfValidationClient validation, ISwissQrCodeClient swissQrCode) : IWorkflowsClient
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
||||
public async Task<SwissQrCodeResult> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
||||
public async Task<SwissQrCodeResult> 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);
|
||||
var qrCode = await swissQrCode.ExtractAsync(pdfStream, raw, cancellationToken);
|
||||
return new SwissQrCodeResult(qrCode, validationResult);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SwissQrCodeExtractionResult?> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,9 @@
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<RepositoryUrl>http://git.dd:3000/AppStd/Rec.git</RepositoryUrl>
|
||||
<PackageTags>digital data document service api client</PackageTags>
|
||||
<Version>1.1.0</Version>
|
||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
||||
<FileVersion>1.1.0.0</FileVersion>
|
||||
<Version>1.2.0</Version>
|
||||
<AssemblyVersion>1.2.0.0</AssemblyVersion>
|
||||
<FileVersion>1.2.0.0</FileVersion>
|
||||
<Description></Description>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Client.Models.Results;
|
||||
|
||||
namespace DocumentService.Client.Interfaces;
|
||||
|
||||
@@ -9,51 +10,62 @@ namespace DocumentService.Client.Interfaces;
|
||||
public interface IWorkflowsClient
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="PdfValidationResult"/> regardless of outcome,
|
||||
/// enabling detailed logging, diagnostics, or batch processing without try/catch.
|
||||
/// </summary>
|
||||
/// <param name="pdfBytes">PDF file as byte array.</param>
|
||||
/// <param name="raw">
|
||||
/// <c>false</c> (default) – returns a parsed <see cref="SwissQrCodeExtractionResult.Bill"/> object.<br/>
|
||||
/// <c>true</c> – returns raw QR text lines without parsing.
|
||||
/// </param>
|
||||
/// <param name="throwIfInvalid">
|
||||
/// <c>true</c> (default) – throws <see cref="InvalidOperationException"/> when the PDF fails validation.<br/>
|
||||
/// <c>false</c> – returns <c>null</c> instead, allowing the caller to handle the case without a try/catch.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>
|
||||
/// Swiss QR Code extraction result, or <c>null</c> if the document is invalid and
|
||||
/// <paramref name="throwIfInvalid"/> is <c>false</c>.
|
||||
/// A <see cref="SwissQrCodeResult"/> containing the <see cref="PdfValidationResult"/>
|
||||
/// (always populated) and the <see cref="SwissQrCodeExtractionResult"/> (<c>null</c> if the
|
||||
/// document was invalid).
|
||||
/// </returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the PDF fails validation and <paramref name="throwIfInvalid"/> is <c>true</c>.
|
||||
/// </exception>
|
||||
Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
||||
Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
|
||||
byte[] pdfBytes,
|
||||
bool raw = false,
|
||||
bool throwIfInvalid = true,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)"/>
|
||||
/// <inheritdoc cref="InspectSwissQrCodeAsync(byte[], bool, CancellationToken)"/>
|
||||
/// <param name="pdfStream">PDF file as a stream.</param>
|
||||
/// <param name="raw"><inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='raw']"/></param>
|
||||
/// <param name="throwIfInvalid"><inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='throwIfInvalid']"/></param>
|
||||
/// <param name="raw"><inheritdoc cref="InspectSwissQrCodeAsync(byte[], bool, CancellationToken)" path="/param[@name=''raw'']"/></param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
||||
Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
|
||||
Stream pdfStream,
|
||||
bool raw = false,
|
||||
bool throwIfInvalid = true,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)"/>
|
||||
/// <param name="filePath">Absolute or relative path to the PDF file.</param>
|
||||
/// <param name="raw"><inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='raw']"/></param>
|
||||
/// <param name="throwIfInvalid"><inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='throwIfInvalid']"/></param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
||||
string filePath,
|
||||
bool raw = false,
|
||||
bool throwIfInvalid = true,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience extensions for <see cref="IWorkflowsClient"/> that accept a file path as input.
|
||||
/// Defined alongside <see cref="IWorkflowsClient"/> so they are discoverable without an additional using directive.
|
||||
/// </summary>
|
||||
public static class WorkflowsClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the PDF at <paramref name="filePath"/> and attempts Swiss QR Code extraction
|
||||
/// in a single call. Opens a <see cref="FileStream"/> and delegates to
|
||||
/// <see cref="IWorkflowsClient.InspectSwissQrCodeAsync(Stream, bool, CancellationToken)"/>.
|
||||
/// </summary>
|
||||
/// <param name="client">The workflows client.</param>
|
||||
/// <param name="filePath">Absolute or relative path to the PDF file.</param>
|
||||
/// <param name="raw">
|
||||
/// <c>false</c> (default) – returns a parsed Bill object.<br/>
|
||||
/// <c>true</c> – returns raw QR text lines without parsing.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public static async Task<SwissQrCodeResult> 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);
|
||||
}
|
||||
}
|
||||
43
DocumentService.Client/Models/Results/SwissQrCodeResult.cs
Normal file
43
DocumentService.Client/Models/Results/SwissQrCodeResult.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
|
||||
namespace DocumentService.Client.Models.Results;
|
||||
|
||||
/// <summary>
|
||||
/// Combined result of a PDF validation followed by a Swiss QR Code extraction.
|
||||
/// Both operations are always executed; <see cref="QrCode"/> is <c>null</c> when
|
||||
/// the document fails validation.
|
||||
/// </summary>
|
||||
/// <param name="QrCode">
|
||||
/// Extracted Swiss QR Code data, or <c>null</c> if the PDF was invalid or did not
|
||||
/// contain a recognisable Swiss QR Code.
|
||||
/// </param>
|
||||
/// <param name="Validation">
|
||||
/// PDF validation result. Always populated regardless of whether extraction succeeded.
|
||||
/// Use <see cref="PdfValidationResult.IsValid"/>
|
||||
/// to determine whether the extraction was attempted.
|
||||
/// </param>
|
||||
public record SwissQrCodeResult(
|
||||
SwissQrCodeExtractionResult? QrCode,
|
||||
PdfValidationResult Validation)
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional convenience method for code readability. Follows the standard
|
||||
/// <c>TryXxx</c> pattern (e.g. <see cref="int.TryParse(string, out int)"/>,
|
||||
/// <see cref="Dictionary{TKey,TValue}.TryGetValue"/>).
|
||||
/// Use it when you prefer an <c>if/else</c> branch over a null-check on <see cref="QrCode"/>;
|
||||
/// it is not required – both approaches are equivalent.
|
||||
/// </summary>
|
||||
/// <param name="qrCode">
|
||||
/// When this method returns <c>true</c>, contains the extracted QR Code data.
|
||||
/// When this method returns <c>false</c>, this value is <c>null</c> and must not be used.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the PDF was valid and a Swiss QR Code was successfully extracted;
|
||||
/// <c>false</c> otherwise.
|
||||
/// </returns>
|
||||
public bool TryExtract(out SwissQrCodeExtractionResult qrCode)
|
||||
{
|
||||
qrCode = QrCode ?? null!;
|
||||
return Validation.IsValid && QrCode is not null;
|
||||
}
|
||||
}
|
||||
@@ -183,97 +183,92 @@ Alle Beispiele nutzen ausschließlich den statischen `Client`-Einstiegspunkt. Vo
|
||||
|
||||
### Workflows – Kombinierte Operationen
|
||||
|
||||
`Client.Workflows` fasst häufig benötigte Abläufe zusammen, die mehrere Einzelendpunkte orchestrieren. Die Validierung läuft dabei immer zuerst – die Folgeaktion wird nur bei einem gültigen Dokument ausgeführt.
|
||||
`Client.Workflows` fasst häufig benötigte Abläufe zusammen, die mehrere Einzelendpunkte orchestrieren. Validierung und Extraktion laufen in einem einzigen Aufruf – das Ergebnis enthält immer beide Informationen.
|
||||
|
||||
> Für Anwendungsfälle wie den **eParser** (Swiss QR Bill aus Eingangsrechnungen lesen) ist dies der empfohlene Einstiegspunkt.
|
||||
|
||||
#### Swiss QR Code – mit vorgelagerter Validierung
|
||||
#### Swiss QR Code – Validierung und Extraktion in einem Schritt
|
||||
|
||||
`InspectSwissQrCodeAsync` gibt immer ein `SwissQrCodeResult` zurück, das sowohl das Validierungsergebnis als auch den extrahierten QR-Code enthält. Für den Zugriff auf den QR-Code stehen zwei gleichwertige Varianten zur Verfügung:
|
||||
|
||||
```csharp
|
||||
using DocumentService.Client;
|
||||
using DocumentService.Client.Models.Results;
|
||||
|
||||
// Variante A: byte[]
|
||||
byte[] pdfBytes = await File.ReadAllBytesAsync("rechnung.pdf");
|
||||
var result = await Client.Workflows.ExtractAsync(pdfBytes);
|
||||
SwissQrCodeResult result = await Client.Workflows.InspectSwissQrCodeAsync(pdfBytes);
|
||||
|
||||
Console.WriteLine($"Betrag: {result?.Bill?.Amount} {result?.Bill?.Currency}");
|
||||
Console.WriteLine($"Empfänger: {result?.Bill?.Creditor?.Name}");
|
||||
// Validierungsergebnis ist immer verfügbar
|
||||
Console.WriteLine($"Gültig: {result.Validation.IsValid}");
|
||||
Console.WriteLine($"Seiten: {result.Validation.PageCount}");
|
||||
Console.WriteLine($"Verschlüsselt: {result.Validation.IsEncrypted}");
|
||||
|
||||
// Variante B: Stream
|
||||
using var stream = File.OpenRead("rechnung.pdf");
|
||||
var result2 = await Client.Workflows.ExtractAsync(stream);
|
||||
// Variante A: TryExtract – lesbarerer Code, kein expliziter Null-Check notwendig
|
||||
// (optionale Hilfsmethode; Variante B ist gleichwertig)
|
||||
if (result.TryExtract(out var qrCode))
|
||||
{
|
||||
Console.WriteLine($"Betrag: {qrCode.Bill?.Amount} {qrCode.Bill?.Currency}");
|
||||
Console.WriteLine($"Empfänger: {qrCode.Bill?.Creditor?.Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Kein Swiss QR Code gefunden oder Dokument ungültig.");
|
||||
}
|
||||
|
||||
// Variante C: Dateipfad
|
||||
var result3 = await Client.Workflows.ExtractAsync("rechnung.pdf");
|
||||
// Variante B: direkter Null-Check auf QrCode
|
||||
if (result.QrCode is not null)
|
||||
{
|
||||
Console.WriteLine($"Betrag: {result.QrCode.Bill?.Amount} {result.QrCode.Bill?.Currency}");
|
||||
}
|
||||
```
|
||||
|
||||
```vbnet
|
||||
Imports DocumentService.Client
|
||||
Imports DocumentService.Client.Models.Results
|
||||
|
||||
' Variante A: byte[]
|
||||
Dim pdfBytes = System.IO.File.ReadAllBytes("rechnung.pdf")
|
||||
Dim result = Await Client.Workflows.ExtractAsync(pdfBytes)
|
||||
Dim result As SwissQrCodeResult = Await Client.Workflows.InspectSwissQrCodeAsync(pdfBytes)
|
||||
|
||||
Console.WriteLine($"Betrag: {result?.Bill?.Amount} {result?.Bill?.Currency}")
|
||||
Console.WriteLine($"Empfänger: {result?.Bill?.Creditor?.Name}")
|
||||
Console.WriteLine($"Gültig: {result.Validation.IsValid}")
|
||||
Console.WriteLine($"Seiten: {result.Validation.PageCount}")
|
||||
Console.WriteLine($"Verschlüsselt: {result.Validation.IsEncrypted}")
|
||||
|
||||
' Variante B: Dateipfad
|
||||
Dim result2 = Await Client.Workflows.ExtractAsync("rechnung.pdf")
|
||||
' Variante A: TryExtract
|
||||
Dim qrCode As DocumentService.Application.Common.DTOs.SwissQrCodeExtractionResult = Nothing
|
||||
If result.TryExtract(qrCode) Then
|
||||
Console.WriteLine($"Betrag: {qrCode.Bill?.Amount} {qrCode.Bill?.Currency}")
|
||||
Console.WriteLine($"Empfänger: {qrCode.Bill?.Creditor?.Name}")
|
||||
Else
|
||||
Console.WriteLine("Kein Swiss QR Code gefunden oder Dokument ungültig.")
|
||||
End If
|
||||
|
||||
' Variante B: direkter Null-Check
|
||||
If result.QrCode IsNot Nothing Then
|
||||
Console.WriteLine($"Betrag: {result.QrCode.Bill?.Amount} {result.QrCode.Bill?.Currency}")
|
||||
End If
|
||||
```
|
||||
|
||||
```powershell
|
||||
Add-Type -Path "DocumentService.Client.dll"
|
||||
|
||||
# Variante A: byte[]
|
||||
$pdfBytes = [System.IO.File]::ReadAllBytes("rechnung.pdf")
|
||||
$result = [DocumentService.Client.Client]::Workflows.ExtractAsync($pdfBytes).GetAwaiter().GetResult()
|
||||
Write-Host "Betrag: $($result.Bill.Amount) $($result.Bill.Currency)"
|
||||
Write-Host "Empfaenger:$($result.Bill.Creditor.Name)"
|
||||
$result = [DocumentService.Client.Client]::Workflows.InspectSwissQrCodeAsync($pdfBytes).GetAwaiter().GetResult()
|
||||
|
||||
# Variante B: Dateipfad
|
||||
$result2 = [DocumentService.Client.Client]::Workflows.ExtractAsync("rechnung.pdf").GetAwaiter().GetResult()
|
||||
```
|
||||
Write-Host "Gueltig: $($result.Validation.IsValid)"
|
||||
Write-Host "Seiten: $($result.Validation.PageCount)"
|
||||
Write-Host "Verschluesselt:$($result.Validation.IsEncrypted)"
|
||||
|
||||
**Fehlerbehandlung – `throwIfInvalid`:**
|
||||
|
||||
Standardmäßig wirft die Methode eine `InvalidOperationException`, wenn das PDF ungültig ist. Mit `throwIfInvalid: false` wird stattdessen `null` zurückgegeben – geeignet, wenn ungültige Dokumente kein Ausnahmefall sind und ohne try/catch behandelt werden sollen.
|
||||
|
||||
```csharp
|
||||
// Exception (Standard) – geeignet, wenn ungültige PDFs ein Fehlerfall sind
|
||||
var result = await Client.Workflows.ExtractAsync(pdfBytes);
|
||||
|
||||
// Null zurückgeben – geeignet für Batch-Verarbeitung o. Ä.
|
||||
var result = await Client.Workflows.ExtractAsync(pdfBytes, throwIfInvalid: false);
|
||||
if (result is null)
|
||||
{
|
||||
Console.WriteLine("Dokument ungültig oder verschlüsselt – übersprungen.");
|
||||
# TryExtract ist in PowerShell nicht direkt nutzbar (out-Parameter).
|
||||
# Direkter Null-Check ist hier die einzige Option:
|
||||
if ($null -ne $result.QrCode) {
|
||||
Write-Host "Betrag: $($result.QrCode.Bill.Amount) $($result.QrCode.Bill.Currency)"
|
||||
Write-Host "Empfaenger:$($result.QrCode.Bill.Creditor.Name)"
|
||||
} else {
|
||||
Write-Host "Kein Swiss QR Code gefunden oder Dokument ungueltig."
|
||||
}
|
||||
```
|
||||
|
||||
```vbnet
|
||||
' Exception (Standard)
|
||||
Dim result = Await Client.Workflows.ExtractAsync(pdfBytes)
|
||||
|
||||
' Null zurückgeben
|
||||
Dim result2 = Await Client.Workflows.ExtractAsync(pdfBytes, throwIfInvalid:=False)
|
||||
If result2 Is Nothing Then
|
||||
Console.WriteLine("Dokument ungültig oder verschlüsselt – übersprungen.")
|
||||
End If
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Exception (Standard)
|
||||
$result = [DocumentService.Client.Client]::Workflows.ExtractAsync($pdfBytes).GetAwaiter().GetResult()
|
||||
|
||||
# Null zurückgeben ($false = throwIfInvalid:false)
|
||||
$result2 = [DocumentService.Client.Client]::Workflows.ExtractAsync($pdfBytes, $false, $false).GetAwaiter().GetResult()
|
||||
if ($null -eq $result2) {
|
||||
Write-Host "Dokument ungueltig oder verschluesselt – uebersprungen."
|
||||
}
|
||||
```
|
||||
|
||||
> **Signatur:** `ExtractSwissQrCodeAsync(pdfBytes / Stream / filePath, raw = false, throwIfInvalid = true, ct = default)`
|
||||
> `raw: true` liefert die Rohtextzeilen des QR-Codes ohne Parsing zurück (siehe [Schweizer QR-Code-Extraktion](#4-schweizer-qr-code-extraktion)).
|
||||
> **`TryExtract` – Hinweis:** Die Methode ist eine optionale Lesbarkeits-Hilfe und nicht zwingend erforderlich. Der direkte Null-Check auf `result.QrCode` ist gleichwertig. Gibt `TryExtract` `false` zurück, ist der `out`-Parameter `qrCode` `null` – er darf in diesem Fall nicht verwendet werden.
|
||||
|
||||
---
|
||||
|
||||
@@ -1074,18 +1069,18 @@ Sobald die Endpunkte auf dem Server bereitgestellt werden, entfällt das `[Obsol
|
||||
| `Client.Validation` | `ValidateAsync()` | `POST /api/pdf/validation/validate` | ✅ Verfügbar | PDF validieren, Metadaten abrufen |
|
||||
| `Client.Validation` | `ValidatePdfAAsync()` | `POST /api/pdf/validation/validate-pdfa` | ✅ Verfügbar | PDF/A-Konformität prüfen |
|
||||
| `Client.Attachment` | `CheckAsync()` | `POST /api/pdf/attachments/check` | ✅ Verfügbar | Eingebettete Anhänge erkennen |
|
||||
| `Client.Attachment` | `ExtractSwissQrCodeAsync()` | `POST /api/pdf/attachments/extract` | ✅ Verfügbar | Anhänge extrahieren als `Dictionary<string, Stream>` |
|
||||
| `Client.Attachment` | `ExtractAsync()` | `POST /api/pdf/attachments/extract` | ✅ Verfügbar | Anhänge extrahieren als `Dictionary<string, Stream>` |
|
||||
| `Client.Attachment` | `AddAsync()` | `POST /api/pdf/attachments/add` | ⏳ Geplant | Anhänge in PDF/A-3 einbetten |
|
||||
| `Client.Operations` | `MergeAsync()` | `POST /api/pdf/operations/merge` | ✅ Verfügbar | Mehrere PDFs zusammenführen |
|
||||
| `Client.Operations` | `AnnotateAsync()` | `POST /api/pdf/operations/annotate` | ✅ Verfügbar | Annotationen hinzufügen |
|
||||
| `Client.Operations` | `StampAsync()` | `POST /api/pdf/operations/stamp` | ✅ Verfügbar | Text-/Bildstempel hinzufügen |
|
||||
| `Client.SwissQrCode` | `ExtractSwissQrCodeAsync()` | `POST /api/pdf/qr-code/extract-swiss` | ✅ Verfügbar | Schweizer QR-Code-Daten extrahieren |
|
||||
| `Client.SwissQrCode` | `ExtractAsync()` | `POST /api/pdf/qr-code/extract-swiss` | ✅ Verfügbar | Schweizer QR-Code-Daten extrahieren |
|
||||
| `Client.Zugferd` | `CheckAsync()` | `POST /api/pdf/zugferd/has-zugferd` | ✅ Verfügbar | ZUGFeRD-Einbettung prüfen |
|
||||
| `Client.Zugferd` | `ExtractSwissQrCodeAsync()` | `POST /api/pdf/zugferd/extract` | ✅ Verfügbar | ZUGFeRD-XML als Stream |
|
||||
| `Client.Zugferd` | `ExtractAsync()` | `POST /api/pdf/zugferd/extract` | ✅ Verfügbar | ZUGFeRD-XML als Stream |
|
||||
| `Client.Zugferd` | `ExtractAsResultAsync()` | `POST /api/pdf/zugferd/extract` | ✅ Verfügbar | ZUGFeRD-XML mit Metadaten als Objekt |
|
||||
| `Client.Conversion` | `ToPdfAAsync()` | `POST /api/pdf/conversion/to-pdfa` | ⏳ Geplant | PDF zu PDF/A konvertieren |
|
||||
| `Client.Conversion` | `FromPdfAAsync()` | `POST /api/pdf/conversion/from-pdfa` | ⏳ Geplant | PDF/A-Einschränkungen aufheben |
|
||||
| `Client.Workflows` | `ExtractSwissQrCodeAsync()` | — | ✅ Verfügbar | Validierung + Swiss QR Code Extraktion in einem Schritt |
|
||||
| `Client.Workflows` | `InspectSwissQrCodeAsync()` | — | ✅ Verfügbar | Validierung + Swiss QR Code Extraktion; gibt immer `SwissQrCodeResult` zurück, wirft nie |
|
||||
|
||||
> **⏳ Geplant:** Der Client-Code ist vorhanden und kompiliert fehlerfrei. Ein Aufruf zur Laufzeit wirft jedoch `NotImplementedException`, da der serverseitige Endpunkt noch nicht existiert. Die Methoden sind mit `[Obsolete]` markiert, sodass der Compiler eine Build-Warnung ausgibt.
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ public class WorkflowsClientTests
|
||||
private readonly Mock<IPdfValidationClient> _validation = new();
|
||||
private readonly Mock<ISwissQrCodeClient> _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<CancellationToken>()), Times.Once);
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, false, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
|
||||
.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<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), 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<CancellationToken>()))
|
||||
.ReturnsAsync(ValidResult());
|
||||
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, It.IsAny<CancellationToken>()))
|
||||
.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<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bytes_InvalidPdf_ThrowIfInvalidTrue_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var pdfBytes = EncryptedPdfBytes();
|
||||
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(InvalidResult_Encrypted());
|
||||
|
||||
// Act
|
||||
var act = async () => await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: true);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*PDF validation failed*");
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bytes_InvalidPdf_ThrowIfInvalidFalse_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var pdfBytes = EncryptedPdfBytes();
|
||||
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(InvalidResult_Encrypted());
|
||||
|
||||
// Act
|
||||
var result = await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: false);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), 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<CancellationToken>()))
|
||||
.ReturnsAsync(InvalidResult_EmptyFile());
|
||||
|
||||
// Act & Assert
|
||||
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: true))
|
||||
.Should().ThrowAsync<InvalidOperationException>();
|
||||
}
|
||||
|
||||
[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<CancellationToken>()))
|
||||
.ReturnsAsync(InvalidResult_Encrypted());
|
||||
|
||||
// Act
|
||||
var act = async () => await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: true);
|
||||
|
||||
// Assert
|
||||
var ex = await act.Should().ThrowAsync<InvalidOperationException>();
|
||||
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<CancellationToken>()))
|
||||
.ThrowsAsync(new HttpRequestException("API unreachable"));
|
||||
|
||||
// Act & Assert
|
||||
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfBytes))
|
||||
.Should().ThrowAsync<HttpRequestException>()
|
||||
.WithMessage("*API unreachable*");
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), 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<CancellationToken>()))
|
||||
.ReturnsAsync(ValidResult());
|
||||
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new HttpRequestException("Extraction failed"));
|
||||
|
||||
// Act & Assert
|
||||
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfBytes))
|
||||
.Should().ThrowAsync<HttpRequestException>()
|
||||
.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<Stream>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ValidResult());
|
||||
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()))
|
||||
@@ -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<Stream>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()), 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<Stream>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ValidResult());
|
||||
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()))
|
||||
.Callback<Stream, bool, CancellationToken>((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<Stream>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(InvalidResult_Encrypted());
|
||||
|
||||
using var pdfStream = new MemoryStream(EncryptedPdfBytes());
|
||||
|
||||
// Act & Assert
|
||||
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfStream, throwIfInvalid: true))
|
||||
.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*PDF validation failed*");
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<Stream>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), 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<Stream>(), It.IsAny<CancellationToken>()))
|
||||
@@ -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<Stream>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), 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<Stream>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ValidResult());
|
||||
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<Stream>(), true, It.IsAny<CancellationToken>()))
|
||||
.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<Stream>(), true, It.IsAny<CancellationToken>()), 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<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ValidResult());
|
||||
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<byte[]>(), false, It.IsAny<CancellationToken>()))
|
||||
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()))
|
||||
.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<byte[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), false, It.IsAny<CancellationToken>()), 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<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(InvalidResult_Encrypted());
|
||||
|
||||
// Act & Assert
|
||||
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(tempPath, throwIfInvalid: true))
|
||||
.Should().ThrowAsync<InvalidOperationException>();
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), 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<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||||
.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<FileNotFoundException>();
|
||||
_validation.Verify(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), 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<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ValidResult());
|
||||
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<byte[]>(), true, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(RawQrResult());
|
||||
|
||||
// Act
|
||||
var result = await Sut.ExtractSwissQrCodeAsync(tempPath, raw: true);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), true, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
|
||||
.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<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never,
|
||||
"extraction must not be attempted on an invalid document");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user