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.Application.Common.DTOs;
|
||||||
using DocumentService.Client.Interfaces;
|
using DocumentService.Client.Interfaces;
|
||||||
|
using DocumentService.Client.Models.Results;
|
||||||
|
|
||||||
namespace DocumentService.Client.Clients;
|
namespace DocumentService.Client.Clients;
|
||||||
|
|
||||||
@@ -10,75 +11,34 @@ namespace DocumentService.Client.Clients;
|
|||||||
public class WorkflowsClient(IPdfValidationClient validation, ISwissQrCodeClient swissQrCode) : IWorkflowsClient
|
public class WorkflowsClient(IPdfValidationClient validation, ISwissQrCodeClient swissQrCode) : IWorkflowsClient
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
public async Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
|
||||||
byte[] pdfBytes,
|
byte[] pdfBytes,
|
||||||
bool raw = false,
|
bool raw = false,
|
||||||
bool throwIfInvalid = true,
|
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var validationResult = await validation.ValidateAsync(pdfBytes, cancellationToken);
|
var validationResult = await validation.ValidateAsync(pdfBytes, cancellationToken);
|
||||||
|
var qrCode = validationResult.IsValid
|
||||||
if (!validationResult.IsValid)
|
? await swissQrCode.ExtractAsync(pdfBytes, raw, cancellationToken)
|
||||||
return throwIfInvalid
|
: null;
|
||||||
? throw new InvalidOperationException(
|
return new SwissQrCodeResult(qrCode, validationResult);
|
||||||
$"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " +
|
|
||||||
$"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).")
|
|
||||||
: (SwissQrCodeExtractionResult?)null;
|
|
||||||
|
|
||||||
return await swissQrCode.ExtractAsync(pdfBytes, raw, cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
public async Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
|
||||||
Stream pdfStream,
|
Stream pdfStream,
|
||||||
bool raw = false,
|
bool raw = false,
|
||||||
bool throwIfInvalid = true,
|
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var validationResult = await validation.ValidateAsync(pdfStream, cancellationToken);
|
var validationResult = await validation.ValidateAsync(pdfStream, cancellationToken);
|
||||||
|
|
||||||
if (!validationResult.IsValid)
|
if (!validationResult.IsValid)
|
||||||
return throwIfInvalid
|
return new SwissQrCodeResult(null, validationResult);
|
||||||
? throw new InvalidOperationException(
|
|
||||||
$"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " +
|
|
||||||
$"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).")
|
|
||||||
: (SwissQrCodeExtractionResult?)null;
|
|
||||||
|
|
||||||
// Stream was consumed by validation – reset if possible, otherwise re-open is caller's responsibility.
|
// 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)
|
if (pdfStream.CanSeek)
|
||||||
pdfStream.Position = 0;
|
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>
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
<RepositoryUrl>http://git.dd:3000/AppStd/Rec.git</RepositoryUrl>
|
<RepositoryUrl>http://git.dd:3000/AppStd/Rec.git</RepositoryUrl>
|
||||||
<PackageTags>digital data document service api client</PackageTags>
|
<PackageTags>digital data document service api client</PackageTags>
|
||||||
<Version>1.1.0</Version>
|
<Version>1.2.0</Version>
|
||||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
<AssemblyVersion>1.2.0.0</AssemblyVersion>
|
||||||
<FileVersion>1.1.0.0</FileVersion>
|
<FileVersion>1.2.0.0</FileVersion>
|
||||||
<Description></Description>
|
<Description></Description>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using DocumentService.Application.Common.DTOs;
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
using DocumentService.Client.Models.Results;
|
||||||
|
|
||||||
namespace DocumentService.Client.Interfaces;
|
namespace DocumentService.Client.Interfaces;
|
||||||
|
|
||||||
@@ -9,51 +10,62 @@ namespace DocumentService.Client.Interfaces;
|
|||||||
public interface IWorkflowsClient
|
public interface IWorkflowsClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates the PDF and extracts its Swiss QR Code in a single call.
|
/// Validates the PDF and attempts Swiss QR Code extraction in a single call,
|
||||||
/// Validation runs first; extraction is only attempted on valid documents.
|
/// 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>
|
/// </summary>
|
||||||
/// <param name="pdfBytes">PDF file as byte array.</param>
|
/// <param name="pdfBytes">PDF file as byte array.</param>
|
||||||
/// <param name="raw">
|
/// <param name="raw">
|
||||||
/// <c>false</c> (default) – returns a parsed <see cref="SwissQrCodeExtractionResult.Bill"/> object.<br/>
|
/// <c>false</c> (default) – returns a parsed <see cref="SwissQrCodeExtractionResult.Bill"/> object.<br/>
|
||||||
/// <c>true</c> – returns raw QR text lines without parsing.
|
/// <c>true</c> – returns raw QR text lines without parsing.
|
||||||
/// </param>
|
/// </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>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// Swiss QR Code extraction result, or <c>null</c> if the document is invalid and
|
/// A <see cref="SwissQrCodeResult"/> containing the <see cref="PdfValidationResult"/>
|
||||||
/// <paramref name="throwIfInvalid"/> is <c>false</c>.
|
/// (always populated) and the <see cref="SwissQrCodeExtractionResult"/> (<c>null</c> if the
|
||||||
|
/// document was invalid).
|
||||||
/// </returns>
|
/// </returns>
|
||||||
/// <exception cref="InvalidOperationException">
|
Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
|
||||||
/// Thrown when the PDF fails validation and <paramref name="throwIfInvalid"/> is <c>true</c>.
|
|
||||||
/// </exception>
|
|
||||||
Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
|
||||||
byte[] pdfBytes,
|
byte[] pdfBytes,
|
||||||
bool raw = false,
|
bool raw = false,
|
||||||
bool throwIfInvalid = true,
|
|
||||||
CancellationToken cancellationToken = default);
|
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="pdfStream">PDF file as a stream.</param>
|
||||||
/// <param name="raw"><inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='raw']"/></param>
|
/// <param name="raw"><inheritdoc cref="InspectSwissQrCodeAsync(byte[], 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>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
|
||||||
Stream pdfStream,
|
Stream pdfStream,
|
||||||
bool raw = false,
|
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);
|
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
|
### 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.
|
> 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
|
```csharp
|
||||||
using DocumentService.Client;
|
using DocumentService.Client;
|
||||||
|
using DocumentService.Client.Models.Results;
|
||||||
|
|
||||||
// Variante A: byte[]
|
|
||||||
byte[] pdfBytes = await File.ReadAllBytesAsync("rechnung.pdf");
|
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}");
|
// Validierungsergebnis ist immer verfügbar
|
||||||
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: Stream
|
// Variante A: TryExtract – lesbarerer Code, kein expliziter Null-Check notwendig
|
||||||
using var stream = File.OpenRead("rechnung.pdf");
|
// (optionale Hilfsmethode; Variante B ist gleichwertig)
|
||||||
var result2 = await Client.Workflows.ExtractAsync(stream);
|
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
|
// Variante B: direkter Null-Check auf QrCode
|
||||||
var result3 = await Client.Workflows.ExtractAsync("rechnung.pdf");
|
if (result.QrCode is not null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Betrag: {result.QrCode.Bill?.Amount} {result.QrCode.Bill?.Currency}");
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```vbnet
|
```vbnet
|
||||||
Imports DocumentService.Client
|
Imports DocumentService.Client
|
||||||
|
Imports DocumentService.Client.Models.Results
|
||||||
|
|
||||||
' Variante A: byte[]
|
|
||||||
Dim pdfBytes = System.IO.File.ReadAllBytes("rechnung.pdf")
|
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($"Gültig: {result.Validation.IsValid}")
|
||||||
Console.WriteLine($"Empfänger: {result?.Bill?.Creditor?.Name}")
|
Console.WriteLine($"Seiten: {result.Validation.PageCount}")
|
||||||
|
Console.WriteLine($"Verschlüsselt: {result.Validation.IsEncrypted}")
|
||||||
|
|
||||||
' Variante B: Dateipfad
|
' Variante A: TryExtract
|
||||||
Dim result2 = Await Client.Workflows.ExtractAsync("rechnung.pdf")
|
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
|
```powershell
|
||||||
Add-Type -Path "DocumentService.Client.dll"
|
Add-Type -Path "DocumentService.Client.dll"
|
||||||
|
|
||||||
# Variante A: byte[]
|
|
||||||
$pdfBytes = [System.IO.File]::ReadAllBytes("rechnung.pdf")
|
$pdfBytes = [System.IO.File]::ReadAllBytes("rechnung.pdf")
|
||||||
$result = [DocumentService.Client.Client]::Workflows.ExtractAsync($pdfBytes).GetAwaiter().GetResult()
|
$result = [DocumentService.Client.Client]::Workflows.InspectSwissQrCodeAsync($pdfBytes).GetAwaiter().GetResult()
|
||||||
Write-Host "Betrag: $($result.Bill.Amount) $($result.Bill.Currency)"
|
|
||||||
Write-Host "Empfaenger:$($result.Bill.Creditor.Name)"
|
|
||||||
|
|
||||||
# Variante B: Dateipfad
|
Write-Host "Gueltig: $($result.Validation.IsValid)"
|
||||||
$result2 = [DocumentService.Client.Client]::Workflows.ExtractAsync("rechnung.pdf").GetAwaiter().GetResult()
|
Write-Host "Seiten: $($result.Validation.PageCount)"
|
||||||
```
|
Write-Host "Verschluesselt:$($result.Validation.IsEncrypted)"
|
||||||
|
|
||||||
**Fehlerbehandlung – `throwIfInvalid`:**
|
# TryExtract ist in PowerShell nicht direkt nutzbar (out-Parameter).
|
||||||
|
# Direkter Null-Check ist hier die einzige Option:
|
||||||
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.
|
if ($null -ne $result.QrCode) {
|
||||||
|
Write-Host "Betrag: $($result.QrCode.Bill.Amount) $($result.QrCode.Bill.Currency)"
|
||||||
```csharp
|
Write-Host "Empfaenger:$($result.QrCode.Bill.Creditor.Name)"
|
||||||
// Exception (Standard) – geeignet, wenn ungültige PDFs ein Fehlerfall sind
|
} else {
|
||||||
var result = await Client.Workflows.ExtractAsync(pdfBytes);
|
Write-Host "Kein Swiss QR Code gefunden oder Dokument ungueltig."
|
||||||
|
|
||||||
// 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.");
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```vbnet
|
> **`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.
|
||||||
' 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)).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -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` | `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.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` | `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.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` | `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` | `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.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` | `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.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` | `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.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.
|
> **⏳ 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<IPdfValidationClient> _validation = new();
|
||||||
private readonly Mock<ISwissQrCodeClient> _swissQrCode = new();
|
private readonly Mock<ISwissQrCodeClient> _swissQrCode = new();
|
||||||
private WorkflowsClient Sut => new(_validation.Object, _swissQrCode.Object);
|
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 ─────────────────────────────────────────────────────────
|
// ─── fixture data ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -75,12 +77,9 @@ public class WorkflowsClientTests
|
|||||||
private static SwissQrCodeExtractionResult RawQrResult() =>
|
private static SwissQrCodeExtractionResult RawQrResult() =>
|
||||||
new(Bill: (SwissQrBillDto?)null, RawLines: ["SPC", "0200", "1", "CH9300762011623852957"]);
|
new(Bill: (SwissQrBillDto?)null, RawLines: ["SPC", "0200", "1", "CH9300762011623852957"]);
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════════════
|
|
||||||
// byte[] overload
|
|
||||||
// ═════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Bytes_ValidPdf_CallsValidationThenExtraction_ReturnsResult()
|
public async Task Inspect_Bytes_ValidPdf_ReturnsBothValidationAndQrCode()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var pdfBytes = ValidPdfBytes();
|
var pdfBytes = ValidPdfBytes();
|
||||||
@@ -90,18 +89,56 @@ public class WorkflowsClientTests
|
|||||||
.ReturnsAsync(QrResult());
|
.ReturnsAsync(QrResult());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await Sut.ExtractSwissQrCodeAsync(pdfBytes);
|
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.Should().NotBeNull();
|
result.Validation.IsValid.Should().BeTrue();
|
||||||
result!.Bill.Should().NotBeNull();
|
result.QrCode.Should().NotBeNull();
|
||||||
result.Bill!.Currency.Should().Be("CHF");
|
result.QrCode!.Bill!.Currency.Should().Be("CHF");
|
||||||
_validation.Verify(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()), Times.Once);
|
_validation.Verify(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, false, It.IsAny<CancellationToken>()), Times.Once);
|
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, false, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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
|
// Arrange
|
||||||
var pdfBytes = QrPdfBytes();
|
var pdfBytes = QrPdfBytes();
|
||||||
@@ -111,109 +148,15 @@ public class WorkflowsClientTests
|
|||||||
.ReturnsAsync(RawQrResult());
|
.ReturnsAsync(RawQrResult());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await Sut.ExtractSwissQrCodeAsync(pdfBytes, raw: true);
|
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes, raw: true);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.Should().NotBeNull();
|
result.QrCode!.RawLines.Should().Contain("SPC");
|
||||||
result!.RawLines.Should().Contain("SPC");
|
|
||||||
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, true, It.IsAny<CancellationToken>()), Times.Once);
|
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, true, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Bytes_InvalidPdf_ThrowIfInvalidTrue_ThrowsInvalidOperationException()
|
public async Task Inspect_Bytes_CancellationToken_ForwardedToBothClients()
|
||||||
{
|
|
||||||
// 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()
|
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var pdfBytes = ValidPdfBytes();
|
var pdfBytes = ValidPdfBytes();
|
||||||
@@ -223,21 +166,17 @@ public class WorkflowsClientTests
|
|||||||
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, token)).ReturnsAsync(QrResult());
|
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, token)).ReturnsAsync(QrResult());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await Sut.ExtractSwissQrCodeAsync(pdfBytes, cancellationToken: token);
|
await Sut.InspectSwissQrCodeAsync(pdfBytes, cancellationToken: token);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
_validation.Verify(v => v.ValidateAsync(pdfBytes, token), Times.Once);
|
_validation.Verify(v => v.ValidateAsync(pdfBytes, token), Times.Once);
|
||||||
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, false, token), Times.Once);
|
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, false, token), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════════════
|
|
||||||
// Stream overload
|
|
||||||
// ═════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
[Fact]
|
[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>()))
|
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(ValidResult());
|
.ReturnsAsync(ValidResult());
|
||||||
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()))
|
_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());
|
using var pdfStream = new MemoryStream(ValidPdfBytes());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await Sut.ExtractSwissQrCodeAsync(pdfStream);
|
var result = await Sut.InspectSwissQrCodeAsync(pdfStream);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.Should().NotBeNull();
|
result.Validation.IsValid.Should().BeTrue();
|
||||||
_validation.Verify(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()), Times.Once);
|
result.QrCode.Should().NotBeNull();
|
||||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Stream_SeekableStream_IsResetBetweenValidationAndExtraction()
|
public async Task Inspect_Stream_InvalidPdf_ReturnsValidationWithNullQrCode_NeverThrows()
|
||||||
{
|
|
||||||
// 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()
|
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||||||
@@ -301,173 +202,56 @@ public class WorkflowsClientTests
|
|||||||
using var pdfStream = new MemoryStream(EncryptedPdfBytes());
|
using var pdfStream = new MemoryStream(EncryptedPdfBytes());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await Sut.ExtractSwissQrCodeAsync(pdfStream, throwIfInvalid: false);
|
var result = await Sut.InspectSwissQrCodeAsync(pdfStream);
|
||||||
|
|
||||||
// Assert
|
// 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);
|
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<Stream>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Stream_ValidPdf_RawMode_PassesRawFlagToExtraction()
|
public async Task Inspect_FilePath_ValidPdf_ReturnsBothResults()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange – extension opens a FileStream so mock expects Stream overload
|
||||||
_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
|
|
||||||
var tempPath = Path.GetTempFileName();
|
var tempPath = Path.GetTempFileName();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await File.WriteAllBytesAsync(tempPath, ValidPdfBytes());
|
await File.WriteAllBytesAsync(tempPath, ValidPdfBytes());
|
||||||
|
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||||||
_validation.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
|
||||||
.ReturnsAsync(ValidResult());
|
.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());
|
.ReturnsAsync(QrResult());
|
||||||
|
|
||||||
// Act
|
// Act – calls extension method which opens a FileStream and delegates to Stream overload
|
||||||
var result = await Sut.ExtractSwissQrCodeAsync(tempPath);
|
var result = await SutAsInterface.InspectSwissQrCodeAsync(tempPath);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.Should().NotBeNull();
|
result.Validation.IsValid.Should().BeTrue();
|
||||||
_validation.Verify(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
result.QrCode.Should().NotBeNull();
|
||||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), false, It.IsAny<CancellationToken>()), Times.Once);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
File.Delete(tempPath);
|
|
||||||
}
|
}
|
||||||
|
finally { File.Delete(tempPath); }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task FilePath_InvalidPdf_ThrowIfInvalidTrue_ThrowsWithoutCallingExtraction()
|
public async Task Inspect_FilePath_InvalidPdf_ReturnsValidationWithNullQrCode_NeverThrows()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var tempPath = Path.GetTempFileName();
|
var tempPath = Path.GetTempFileName();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await File.WriteAllBytesAsync(tempPath, EncryptedPdfBytes());
|
await File.WriteAllBytesAsync(tempPath, EncryptedPdfBytes());
|
||||||
|
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||||||
_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>()))
|
|
||||||
.ReturnsAsync(InvalidResult_Encrypted());
|
.ReturnsAsync(InvalidResult_Encrypted());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await Sut.ExtractSwissQrCodeAsync(tempPath, throwIfInvalid: false);
|
var result = await SutAsInterface.InspectSwissQrCodeAsync(tempPath);
|
||||||
|
|
||||||
// Assert
|
// Assert – never throws, validation details always populated
|
||||||
result.Should().BeNull();
|
result.Validation.IsValid.Should().BeFalse();
|
||||||
}
|
result.QrCode.Should().BeNull();
|
||||||
finally
|
|
||||||
{
|
|
||||||
File.Delete(tempPath);
|
|
||||||
}
|
}
|
||||||
|
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