Compare commits
12 Commits
f9d4e53ddc
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0db28a025f | |||
| 6a321c1022 | |||
| dcb735f8c8 | |||
| 6527cdcbb0 | |||
| 24db1b0d0a | |||
| f7cba05490 | |||
| 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
|
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
|
||||||
@@ -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.2</Version>
|
||||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
<AssemblyVersion>1.2.2.0</AssemblyVersion>
|
||||||
<FileVersion>1.1.0.0</FileVersion>
|
<FileVersion>1.2.2.0</FileVersion>
|
||||||
<Description></Description>
|
<Description></Description>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
@@ -44,7 +44,34 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DocumentService.Application\DocumentService.Application.csproj" />
|
<ProjectReference Include="..\DocumentService.Application\DocumentService.Application.csproj">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="..\DocumentService.Domain\DocumentService.Domain.csproj">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Embed Application and Domain DLLs into the NuGet package lib folder -->
|
||||||
|
<Target Name="IncludeReferencedProjectDlls" BeforeTargets="_GetPackageFiles" Condition="'$(TargetFramework)' != ''">
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="..\DocumentService.Application\bin\$(Configuration)\$(TargetFramework)\DocumentService.Application.dll"
|
||||||
|
Pack="true" PackagePath="lib\$(TargetFramework)\" Visible="false" />
|
||||||
|
<None Include="..\DocumentService.Domain\bin\$(Configuration)\$(TargetFramework)\DocumentService.Domain.dll"
|
||||||
|
Pack="true" PackagePath="lib\$(TargetFramework)\" Visible="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<!-- Trigger inner build per-TFM to pick up IncludeReferencedProjectDlls target -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage</TargetsForTfmSpecificBuildOutput>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="CopyProjectReferencesToPackage" DependsOnTargets="ResolveReferences">
|
||||||
|
<ItemGroup>
|
||||||
|
<BuildOutputInPackage Include="@(ReferenceCopyLocalPaths->WithMetadataValue('ReferenceSourceTarget','ProjectReference'))"
|
||||||
|
Condition="'%(Filename)' == 'DocumentService.Application' Or '%(Filename)' == 'DocumentService.Domain'" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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)).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -339,6 +334,15 @@ $result2 = [DocumentService.Client.Client]::Validation.ValidateAsync($pdfBytes).
|
|||||||
|
|
||||||
#### PDF/A-Konformitätsprüfung
|
#### PDF/A-Konformitätsprüfung
|
||||||
|
|
||||||
|
> **⚠️ Hinweis zur Validierungstiefe:** `ValidatePdfAAsync` führt eine **heuristische PDF/A-Erkennung** durch, keine vollständige ISO-19005-Konformitätsprüfung. Die Implementierung erkennt das PDF/A-Konformitätslevel anhand der XMP-Metadaten (`pdfaid:part`, `pdfaid:conformance`) und prüft auf Verschlüsselung. Folgende Anforderungen der Norm werden **nicht** geprüft:
|
||||||
|
>
|
||||||
|
> - Einbettung aller verwendeten Schriftarten
|
||||||
|
> - Gültigkeit und Vollständigkeit der Farbprofile (ICC)
|
||||||
|
> - Fehlen von JavaScript, eingebetteten Multimedia-Inhalten und externen Referenzen
|
||||||
|
> - Strukturelle Barrierefreiheit (Level A)
|
||||||
|
>
|
||||||
|
> Das Ergebnis `IsValid = true` bedeutet daher: **„Keine erkannten Verstöße"**, nicht **„nachgewiesene ISO-19005-Konformität"**. Für eine zertifizierungsfähige Prüfung ist ein dedizierter Validator (z. B. veraPDF) erforderlich.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
using DocumentService.Client;
|
using DocumentService.Client;
|
||||||
|
|
||||||
@@ -1072,20 +1076,20 @@ Sobald die Endpunkte auf dem Server bereitgestellt werden, entfällt das `[Obsol
|
|||||||
| **Client-Eigenschaft** | **Methode** | **API-Endpunkt** | **Status** | **Beschreibung** |
|
| **Client-Eigenschaft** | **Methode** | **API-Endpunkt** | **Status** | **Beschreibung** |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `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-Erkennung (heuristisch, keine vollständige ISO-19005-Prüfung) |
|
||||||
| `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.
|
||||||
|
|
||||||
|
|||||||
235
DocumentService.Tests.Live/API/PdfAttachmentLiveTests.cs
Normal file
235
DocumentService.Tests.Live/API/PdfAttachmentLiveTests.cs
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
using FluentAssertions;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Live tests for PDF attachment endpoints (check + extract).
|
||||||
|
/// Uses PdfAttachmentClient — routes managed by the client.
|
||||||
|
/// Target: http://172.24.12.39:9393/
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Live API")]
|
||||||
|
public class PdfAttachmentLiveTests : LiveApiTestBase
|
||||||
|
{
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// CheckAsync(Stream) → multipart → /api/pdf/attachments/check
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_PdfWithoutAttachments_ReturnsHasAttachmentsFalse()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.HasAttachments.Should().BeFalse();
|
||||||
|
result.AttachmentCount.Should().Be(0);
|
||||||
|
result.Attachments.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_PdfWith6Attachments_ReturnsAllAttachmentsWithMetadata()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeTrue();
|
||||||
|
result.AttachmentCount.Should().Be(6, "PDF has exactly 6 attachments");
|
||||||
|
result.Attachments.Should().HaveCount(6);
|
||||||
|
|
||||||
|
foreach (var attachment in result.Attachments)
|
||||||
|
{
|
||||||
|
attachment.FileName.Should().NotBeNullOrEmpty();
|
||||||
|
attachment.MimeType.Should().NotBeNullOrEmpty();
|
||||||
|
attachment.Size.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_PdfWithSwissQrCode_HasNoAttachments()
|
||||||
|
{
|
||||||
|
// QR code PDF has no embedded file attachments
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeFalse();
|
||||||
|
result.AttachmentCount.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_CorruptedContent_Throws()
|
||||||
|
{
|
||||||
|
byte[] garbage = "This is not a valid PDF content"u8.ToArray();
|
||||||
|
using var stream = new MemoryStream(garbage);
|
||||||
|
|
||||||
|
var act = async () => await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"corrupted PDF should return 500 → client throws");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// CheckAsync(byte[]) → Base64 JSON → /api/pdf/attachments/check
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Bytes_PdfWithoutAttachments_ReturnsHasAttachmentsFalse()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(bytes);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeFalse();
|
||||||
|
result.AttachmentCount.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Bytes_PdfWith6Attachments_ReturnsAllAttachmentsWithMetadata()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(bytes);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeTrue();
|
||||||
|
result.AttachmentCount.Should().Be(6);
|
||||||
|
result.Attachments.Should().HaveCount(6);
|
||||||
|
|
||||||
|
foreach (var attachment in result.Attachments)
|
||||||
|
{
|
||||||
|
attachment.FileName.Should().NotBeNullOrEmpty();
|
||||||
|
attachment.MimeType.Should().NotBeNullOrEmpty();
|
||||||
|
attachment.Size.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await AttachmentClient.CheckAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ExtractAsync(Stream) → multipart → /api/pdf/attachments/extract
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_PdfWith6Attachments_Returns6NamedStreams()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var attachments = await AttachmentClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
attachments.Should().HaveCount(6, "PDF contains 6 attachments");
|
||||||
|
|
||||||
|
foreach (var (name, content) in attachments)
|
||||||
|
{
|
||||||
|
name.Should().NotBeNullOrEmpty();
|
||||||
|
content.Length.Should().BeGreaterThan(0);
|
||||||
|
content.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_AttachmentNamesAreUnique()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var attachments = await AttachmentClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
attachments.Keys.Should().OnlyHaveUniqueItems(
|
||||||
|
"ZIP entry names must be unique within the archive");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
foreach (var s in attachments.Values) s.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await AttachmentClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_CorruptedContent_Throws()
|
||||||
|
{
|
||||||
|
byte[] garbage = "This is not a valid PDF content"u8.ToArray();
|
||||||
|
using var stream = new MemoryStream(garbage);
|
||||||
|
|
||||||
|
var act = async () => await AttachmentClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ExtractAsync(byte[]) → Base64 JSON → /api/pdf/attachments/extract
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_PdfWith6Attachments_Returns6NamedStreams()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var attachments = await AttachmentClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
attachments.Should().HaveCount(6);
|
||||||
|
|
||||||
|
foreach (var (name, content) in attachments)
|
||||||
|
{
|
||||||
|
name.Should().NotBeNullOrEmpty();
|
||||||
|
content.Length.Should().BeGreaterThan(0);
|
||||||
|
content.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_AttachmentContentsAreNonEmpty()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var attachments = await AttachmentClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var (_, content) in attachments)
|
||||||
|
{
|
||||||
|
((MemoryStream)content).ToArray().Should().NotBeEmpty(
|
||||||
|
"every extracted attachment must have content");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
foreach (var s in attachments.Values) s.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await AttachmentClient.ExtractAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
532
DocumentService.Tests.Live/API/PdfOperationsLiveTests.cs
Normal file
532
DocumentService.Tests.Live/API/PdfOperationsLiveTests.cs
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs.Requests;
|
||||||
|
using DocumentService.Domain.Models.ValueObjects;
|
||||||
|
using FluentAssertions;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Live tests for PDF operations: merge, annotate, stamp.
|
||||||
|
/// Uses PdfOperationsClient — routes managed by the client.
|
||||||
|
/// Target: http://172.24.12.39:9393/
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Live API")]
|
||||||
|
public class PdfOperationsLiveTests : LiveApiTestBase
|
||||||
|
{
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// MERGE — MergeAsync(IEnumerable<Stream>) → multipart
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Streams_TwoPdfs_ReturnsMergedPdfStream()
|
||||||
|
{
|
||||||
|
using var pdf1 = LoadPdfStream("valid.pdf");
|
||||||
|
using var pdf2 = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
await using var merged = await OperationsClient.MergeAsync(new[] { pdf1, pdf2 });
|
||||||
|
|
||||||
|
merged.Should().NotBeNull();
|
||||||
|
merged.Length.Should().BeGreaterThan(0);
|
||||||
|
// Merged must start with PDF magic bytes (%PDF = 0x25 0x50 0x44 0x46)
|
||||||
|
var header = new byte[4];
|
||||||
|
merged.ReadExactly(header, 0, 4);
|
||||||
|
header.Should().Equal(new byte[] { 0x25, 0x50, 0x44, 0x46 },
|
||||||
|
"merged result must be a valid PDF");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Streams_ThreePdfs_ReturnsMergedPdfStream()
|
||||||
|
{
|
||||||
|
using var pdf1 = LoadPdfStream("valid.pdf");
|
||||||
|
using var pdf2 = LoadPdfStream("valid.pdf");
|
||||||
|
using var pdf3 = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
await using var merged = await OperationsClient.MergeAsync(new[] { pdf1, pdf2, pdf3 });
|
||||||
|
|
||||||
|
merged.Should().NotBeNull();
|
||||||
|
merged.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Streams_OnlyOnePdf_Throws()
|
||||||
|
{
|
||||||
|
using var pdf = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.MergeAsync(new[] { pdf });
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"at least 2 PDFs are required; single PDF should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Streams_CorruptedPdf_Throws()
|
||||||
|
{
|
||||||
|
byte[] garbage = "NOT A PDF"u8.ToArray();
|
||||||
|
using var corrupted = new MemoryStream(garbage);
|
||||||
|
using var valid = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.MergeAsync(new[] { corrupted, valid });
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"corrupted PDF should return 500");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// MERGE — MergeAsync(IEnumerable<byte[]>) → Base64 JSON
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Bytes_TwoPdfs_ReturnsMergedPdfStream()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
await using var merged = await OperationsClient.MergeAsync(new[] { pdf, pdf });
|
||||||
|
|
||||||
|
merged.Should().NotBeNull();
|
||||||
|
merged.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Bytes_WithPageRanges_ReturnsMergedPdfStream()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
await using var merged = await OperationsClient.MergeAsync(
|
||||||
|
new[] { pdf, pdf },
|
||||||
|
pageRanges: ["1", "1"]);
|
||||||
|
|
||||||
|
merged.Should().NotBeNull();
|
||||||
|
merged.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Bytes_OnlyOnePdf_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.MergeAsync(new[] { pdf });
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Bytes_PageRangeExceedsPageCount_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf"); // 1-page PDF
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.MergeAsync(
|
||||||
|
new[] { pdf, pdf },
|
||||||
|
pageRanges: ["999-1000", null]);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"out-of-bounds page range should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// ANNOTATE — AnnotateAsync(Stream, ...) → multipart
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_TextMarkupHighlight_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty, // not used in Stream overload
|
||||||
|
AnnotationType = AnnotationType.TextMarkup,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 100, Y1 = 100, X2 = 300, Y2 = 120,
|
||||||
|
TextMarkupStyle = TextMarkupStyle.Highlight,
|
||||||
|
Color = "FFFF00",
|
||||||
|
Author = "LiveTest"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_Underline_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.TextMarkup,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 50, Y1 = 700, X2 = 300, Y2 = 720,
|
||||||
|
TextMarkupStyle = TextMarkupStyle.Underline
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_Circle_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.Circle,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 100, Y1 = 200, X2 = 200, Y2 = 300,
|
||||||
|
Color = "00FF00",
|
||||||
|
Content = "Circle annotation"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_Square_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.Square,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 250, Y1 = 250, X2 = 350, Y2 = 350,
|
||||||
|
Color = "0000FF"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_InvalidPageNumber_Throws()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.FreeText,
|
||||||
|
PageNumber = 9999, // exceeds page count
|
||||||
|
X1 = 0, Y1 = 0, X2 = 100, Y2 = 100,
|
||||||
|
Content = "Test"
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"invalid page number should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// ANNOTATE — AnnotateAsync(byte[], ...) → Base64 JSON
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_FreeText_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty, // client fills this in
|
||||||
|
AnnotationType = AnnotationType.FreeText,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 50, Y1 = 50, X2 = 200, Y2 = 100,
|
||||||
|
Content = "Free text from live test",
|
||||||
|
Author = "LiveTestRunner",
|
||||||
|
Color = "FF0000"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_StickyNote_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.StickyNote,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 300, Y1 = 300, X2 = 320, Y2 = 320,
|
||||||
|
Content = "Sticky note from live test",
|
||||||
|
Author = "Reviewer"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_TextMarkupStrikeout_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.TextMarkup,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 60, Y1 = 600, X2 = 250, Y2 = 620,
|
||||||
|
TextMarkupStyle = TextMarkupStyle.Strikeout,
|
||||||
|
Color = "FF6600"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_FreeTextWithoutContent_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.FreeText,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 0, Y1 = 0, X2 = 100, Y2 = 100
|
||||||
|
// Content intentionally missing
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"FreeText without content should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_TextMarkupWithoutStyle_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.TextMarkup,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 0, Y1 = 0, X2 = 100, Y2 = 100
|
||||||
|
// TextMarkupStyle intentionally missing
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"TextMarkup without style should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// STAMP — StampAsync(Stream, ...) → multipart
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Stream_TextStamp_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 100, Y = 400,
|
||||||
|
Text = "CONFIDENTIAL",
|
||||||
|
FontSize = 24,
|
||||||
|
Color = "FF0000",
|
||||||
|
Opacity = 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(stream, request);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Stream_TextStamp_WithRotation_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 300, Y = 400,
|
||||||
|
Text = "DRAFT",
|
||||||
|
Rotation = 45,
|
||||||
|
Opacity = 0.3,
|
||||||
|
Placement = StampPlacement.Background
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Stream_PredefinedStamp_Confidential_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Predefined,
|
||||||
|
X = 100, Y = 400,
|
||||||
|
PredefinedType = PredefinedStampType.Confidential,
|
||||||
|
Opacity = 0.6
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// STAMP — StampAsync(byte[], ...) → Base64 JSON
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_TextStamp_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 100, Y = 400,
|
||||||
|
Text = "APPROVED",
|
||||||
|
FontName = "Arial",
|
||||||
|
FontSize = 20,
|
||||||
|
Color = "008000",
|
||||||
|
Opacity = 0.7,
|
||||||
|
Placement = StampPlacement.Foreground
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_WatermarkBackground_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 150, Y = 350,
|
||||||
|
Text = "WATERMARK",
|
||||||
|
FontSize = 36,
|
||||||
|
Color = "AAAAAA",
|
||||||
|
Opacity = 0.2,
|
||||||
|
Rotation = 45,
|
||||||
|
Placement = StampPlacement.Background
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_PredefinedStamp_Approved_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Predefined,
|
||||||
|
X = 100, Y = 400,
|
||||||
|
PredefinedType = PredefinedStampType.Approved,
|
||||||
|
Opacity = 0.8
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_PredefinedStamp_Draft_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Predefined,
|
||||||
|
X = 200, Y = 500,
|
||||||
|
PredefinedType = PredefinedStampType.Draft,
|
||||||
|
Opacity = 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_TextStampOnSpecificPage_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
PageNumbers = [1],
|
||||||
|
X = 200, Y = 500,
|
||||||
|
Text = "PAGE 1 ONLY",
|
||||||
|
Opacity = 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_TextStampWithoutText_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 100, Y = 400
|
||||||
|
// Text intentionally missing
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"Text stamp without text should return 400");
|
||||||
|
}
|
||||||
|
}
|
||||||
196
DocumentService.Tests.Live/API/PdfValidationLiveTests.cs
Normal file
196
DocumentService.Tests.Live/API/PdfValidationLiveTests.cs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
using FluentAssertions;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Live tests for PDF validation endpoints.
|
||||||
|
/// Uses PdfValidationClient — routes are managed by the client, not the test.
|
||||||
|
/// Target: http://172.24.12.39:9393/
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Live API")]
|
||||||
|
public class PdfValidationLiveTests : LiveApiTestBase
|
||||||
|
{
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ValidateAsync(Stream) → multipart/form-data → /api/pdf/validation/validate
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_ValidPdf_ReturnsMetadata()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
result.FileSizeBytes.Should().BeGreaterThan(0);
|
||||||
|
result.PdfVersion.Should().NotBeNullOrEmpty();
|
||||||
|
result.HasAttachments.Should().BeFalse();
|
||||||
|
result.AttachmentCount.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_PdfWithAttachments_ReturnsHasAttachmentsTrue()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeTrue();
|
||||||
|
result.AttachmentCount.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_PdfWithSwissQrCode_ReturnsValidResult()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
result.FileSizeBytes.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"empty file should be rejected with 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_CorruptedContent_Throws()
|
||||||
|
{
|
||||||
|
// Valid stream shape, but not a PDF
|
||||||
|
byte[] garbage = "THIS IS NOT A PDF FILE"u8.ToArray();
|
||||||
|
using var stream = new MemoryStream(garbage);
|
||||||
|
|
||||||
|
var act = async () => await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"corrupted content should be rejected (500)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ValidateAsync(byte[]) → Base64 JSON → /api/pdf/validation/validate
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Bytes_ValidPdf_ReturnsMetadata()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(bytes);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
result.FileSizeBytes.Should().BeGreaterThan(0);
|
||||||
|
result.PdfVersion.Should().NotBeNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Bytes_PdfWithAttachments_ReturnsHasAttachmentsTrue()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(bytes);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeTrue();
|
||||||
|
result.AttachmentCount.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await ValidationClient.ValidateAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"empty byte array encodes to empty Base64, which should be rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ValidatePdfAAsync(Stream) → multipart → /api/pdf/validation/validate-pdfa
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Stream_ValidPdf_ReturnsComplianceFields()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidatePdfAAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.IsValid.Should().BeTrue();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
result.PdfVersion.Should().NotBeNullOrEmpty();
|
||||||
|
result.FileSize.Should().BeGreaterThan(0);
|
||||||
|
result.Encrypted.Should().BeFalse();
|
||||||
|
// valid.pdf is a regular PDF, not PDF/A
|
||||||
|
result.PdfACompliant.Should().BeFalse();
|
||||||
|
result.PdfAVersion.Should().BeNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Stream_EncryptedPdf_ReturnsEncryptedTrue_NotCompliant()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("encryptedPdf.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidatePdfAAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Encrypted.Should().BeTrue();
|
||||||
|
result.PdfACompliant.Should().BeFalse("encrypted PDFs cannot be PDF/A compliant");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await ValidationClient.ValidatePdfAAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ValidatePdfAAsync(byte[]) → Base64 JSON → /api/pdf/validation/validate-pdfa
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Bytes_ValidPdf_ReturnsComplianceFields()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidatePdfAAsync(bytes);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.IsValid.Should().BeTrue();
|
||||||
|
result.PdfACompliant.Should().BeFalse();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Bytes_EncryptedPdf_ReturnsEncryptedTrue()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("encryptedPdf.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidatePdfAAsync(bytes);
|
||||||
|
|
||||||
|
result.Encrypted.Should().BeTrue();
|
||||||
|
result.PdfACompliant.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await ValidationClient.ValidatePdfAAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
129
DocumentService.Tests.Live/API/SwissQrCodeLiveTests.cs
Normal file
129
DocumentService.Tests.Live/API/SwissQrCodeLiveTests.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
using FluentAssertions;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Live tests for Swiss QR Code extraction endpoint.
|
||||||
|
/// Uses SwissQrCodeClient — routes managed by the client.
|
||||||
|
/// Target: http://172.24.12.39:9393/
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Live API")]
|
||||||
|
public class SwissQrCodeLiveTests : LiveApiTestBase
|
||||||
|
{
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ExtractAsync(Stream) → multipart → /api/pdf/qr-code/extract-swiss
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_PdfWithQrCode_ReturnsBillDetails()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await QrCodeClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Bill.Should().NotBeNull("bill data must be extracted");
|
||||||
|
result.RawLines.Should().NotBeEmpty("raw QR payload lines must be present");
|
||||||
|
|
||||||
|
// Swiss QR Bill Standard 2.0 payload always starts with "SPC"
|
||||||
|
result.RawLines.Should().Contain(line => line == "SPC",
|
||||||
|
"first SPC line is mandated by Swiss QR Bill Standard 2.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_PdfWithQrCode_BillHasMandatoryFields()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await QrCodeClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
var bill = result.Bill;
|
||||||
|
bill.Should().NotBeNull();
|
||||||
|
bill!.Account.Should().NotBeNullOrEmpty("IBAN/QR-IBAN is mandatory per standard");
|
||||||
|
bill.Creditor.Should().NotBeNull("Creditor is mandatory");
|
||||||
|
bill.Creditor!.Name.Should().NotBeNullOrEmpty("Creditor name is mandatory");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_PdfWithoutQrCode_Throws404()
|
||||||
|
{
|
||||||
|
// valid.pdf has no QR code
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () => await QrCodeClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
// Client calls EnsureSuccessStatusCode → HttpRequestException on 404
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"PDF without QR code must return 404");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await QrCodeClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_WithRawFlag_ReturnsRawLines()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
// raw=true → API returns full unprocessed payload lines
|
||||||
|
var result = await QrCodeClient.ExtractAsync(stream, raw: true);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.RawLines.Should().NotBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ExtractAsync(byte[]) → Base64 JSON → /api/pdf/qr-code/extract-swiss
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_PdfWithQrCode_ReturnsBillDetails()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await QrCodeClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Bill.Should().NotBeNull();
|
||||||
|
result.RawLines.Should().NotBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_PdfWithQrCode_BillHasMandatoryFields()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await QrCodeClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
result.Bill!.Account.Should().NotBeNullOrEmpty();
|
||||||
|
result.Bill.Creditor.Should().NotBeNull();
|
||||||
|
result.Bill.Creditor!.Name.Should().NotBeNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_PdfWithoutQrCode_Throws404()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () => await QrCodeClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"PDF without QR code must return 404");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await QrCodeClient.ExtractAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
47
DocumentService.Tests.Live/DocumentService.Tests.Live.csproj
Normal file
47
DocumentService.Tests.Live/DocumentService.Tests.Live.csproj
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="TestData\Pdfs\pdfWithSwissQRCode.pdf" />
|
||||||
|
<None Remove="TestData\Pdfs\valid.pdf" />
|
||||||
|
<None Remove="TestData\Pdfs\pdfWithMoreThanOneAttachment.pdf" />
|
||||||
|
<None Remove="TestData\Pdfs\encryptedPdf.pdf" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="TestData\Pdfs\pdfWithSwissQRCode.pdf" />
|
||||||
|
<EmbeddedResource Include="TestData\Pdfs\valid.pdf" />
|
||||||
|
<EmbeddedResource Include="TestData\Pdfs\pdfWithMoreThanOneAttachment.pdf" />
|
||||||
|
<EmbeddedResource Include="TestData\Pdfs\encryptedPdf.pdf" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DocumentService.Client\DocumentService.Client.csproj" />
|
||||||
|
<ProjectReference Include="..\DocumentService.Application\DocumentService.Application.csproj" />
|
||||||
|
<ProjectReference Include="..\DocumentService.Domain\DocumentService.Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
64
DocumentService.Tests.Live/LiveApiTestBase.cs
Normal file
64
DocumentService.Tests.Live/LiveApiTestBase.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using DocumentService.Client.Clients;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared base for all live API tests.
|
||||||
|
/// Uses the typed DocumentService.Client classes — no hardcoded routes.
|
||||||
|
///
|
||||||
|
/// Live API: http://172.24.12.39:9393/
|
||||||
|
/// Override via LIVE_API_BASE_URL environment variable if needed.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class LiveApiTestBase : IDisposable
|
||||||
|
{
|
||||||
|
public static readonly string BaseUrl =
|
||||||
|
Environment.GetEnvironmentVariable("LIVE_API_BASE_URL")
|
||||||
|
?? "http://172.24.12.39:9393";
|
||||||
|
|
||||||
|
protected readonly HttpClient HttpClient;
|
||||||
|
|
||||||
|
// Typed clients (route-free — routes live inside the client implementations)
|
||||||
|
protected readonly PdfValidationClient ValidationClient;
|
||||||
|
protected readonly PdfAttachmentClient AttachmentClient;
|
||||||
|
protected readonly PdfOperationsClient OperationsClient;
|
||||||
|
protected readonly SwissQrCodeClient QrCodeClient;
|
||||||
|
|
||||||
|
protected LiveApiTestBase()
|
||||||
|
{
|
||||||
|
HttpClient = new HttpClient { BaseAddress = new Uri(BaseUrl) };
|
||||||
|
|
||||||
|
ValidationClient = new PdfValidationClient(HttpClient, NullLogger<PdfValidationClient>.Instance);
|
||||||
|
AttachmentClient = new PdfAttachmentClient(HttpClient, NullLogger<PdfAttachmentClient>.Instance);
|
||||||
|
OperationsClient = new PdfOperationsClient(HttpClient, NullLogger<PdfOperationsClient>.Instance);
|
||||||
|
QrCodeClient = new SwissQrCodeClient(HttpClient, NullLogger<SwissQrCodeClient>.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// Embedded-resource helpers
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
protected static byte[] LoadPdfBytes(string fileName)
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
var resourceName = $"DocumentService.Tests.Live.TestData.Pdfs.{fileName}";
|
||||||
|
|
||||||
|
using var stream = assembly.GetManifestResourceStream(resourceName)
|
||||||
|
?? throw new FileNotFoundException(
|
||||||
|
$"Embedded resource not found: '{resourceName}'. " +
|
||||||
|
$"Available: {string.Join(", ", assembly.GetManifestResourceNames())}");
|
||||||
|
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
stream.CopyTo(ms);
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Stream LoadPdfStream(string fileName)
|
||||||
|
{
|
||||||
|
var bytes = LoadPdfBytes(fileName);
|
||||||
|
return new MemoryStream(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => HttpClient.Dispose();
|
||||||
|
}
|
||||||
30
DocumentService.Tests.Live/TestData/Pdfs/encryptedPdf.pdf
Normal file
30
DocumentService.Tests.Live/TestData/Pdfs/encryptedPdf.pdf
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
%PDF-1.4
|
||||||
|
1 0 obj
|
||||||
|
<< /Type /Catalog /Pages 2 0 R >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
<< /Filter /Standard /V 2 /R 3 /Length 128 /P -3904 /O (OwnerHash12345678901234567890123456) /U (UserHash12345678901234567890123456) >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
xref
|
||||||
|
0 5
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000009 00000 n
|
||||||
|
0000000058 00000 n
|
||||||
|
0000000115 00000 n
|
||||||
|
0000000206 00000 n
|
||||||
|
|
||||||
|
trailer
|
||||||
|
<< /Size 5 /Root 1 0 R /Encrypt 4 0 R >>
|
||||||
|
startxref
|
||||||
|
370
|
||||||
|
%%EOF
|
||||||
Binary file not shown.
BIN
DocumentService.Tests.Live/TestData/Pdfs/pdfWithSwissQRCode.pdf
Normal file
BIN
DocumentService.Tests.Live/TestData/Pdfs/pdfWithSwissQRCode.pdf
Normal file
Binary file not shown.
BIN
DocumentService.Tests.Live/TestData/Pdfs/valid.pdf
Normal file
BIN
DocumentService.Tests.Live/TestData/Pdfs/valid.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="valid.pdf" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DocumentService.Client" Version="1.2.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
36
DocumentService.Tests.NuGet/Program.cs
Normal file
36
DocumentService.Tests.NuGet/Program.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using DocumentService.Client;
|
||||||
|
|
||||||
|
// 1. Configure the client
|
||||||
|
Client.Configure("http://172.24.12.39:9393/");
|
||||||
|
|
||||||
|
// 2. Load embedded PDF resource
|
||||||
|
var assembly = typeof(Program).Assembly;
|
||||||
|
var resourceName = assembly.GetManifestResourceNames()
|
||||||
|
.FirstOrDefault(n => n.EndsWith("valid.pdf", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (resourceName is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ Embedded resource 'valid.pdf' not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = assembly.GetManifestResourceStream(resourceName)!;
|
||||||
|
var result = await Client.Validation.ValidateAsync(stream);
|
||||||
|
|
||||||
|
Console.WriteLine("✅ Connection successful!");
|
||||||
|
Console.WriteLine($" Page count : {result.PageCount}");
|
||||||
|
Console.WriteLine($" PDF version : {result.PdfVersion}");
|
||||||
|
Console.WriteLine($" Encrypted : {result.IsEncrypted}");
|
||||||
|
Console.WriteLine($" Attachments : {result.AttachmentCount}");
|
||||||
|
Console.WriteLine($" File size : {result.FileSizeMB:F2} MB");
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ HTTP error: " + ex.Message);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ Client error: " + ex.Message);
|
||||||
|
}
|
||||||
BIN
DocumentService.Tests.NuGet/valid.pdf
Normal file
BIN
DocumentService.Tests.NuGet/valid.pdf
Normal file
Binary file not shown.
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -40,36 +40,116 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Domain", "D
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Tests", "DocumentService.Tests\DocumentService.Tests.csproj", "{ECD5BC60-7009-4099-7193-526FDC6E8D99}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Tests", "DocumentService.Tests\DocumentService.Tests.csproj", "{ECD5BC60-7009-4099-7193-526FDC6E8D99}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Tests.Live", "DocumentService.Tests.Live\DocumentService.Tests.Live.csproj", "{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Tests.NuGet", "DocumentService.Tests.NuGet\DocumentService.Tests.NuGet.csproj", "{EE3E7C50-E044-4F77-BB19-74286DDA43F3}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|Any CPU.Build.0 = Release|Any CPU
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|Any CPU.Build.0 = Release|Any CPU
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|Any CPU.Build.0 = Release|Any CPU
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|Any CPU.Build.0 = Release|Any CPU
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|Any CPU.Build.0 = Release|Any CPU
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -85,6 +165,8 @@ Global
|
|||||||
{459EDCC5-240E-A2BC-1330-B5B2326B679C} = {0B80DDC4-A13C-4C5F-8520-AA2F3650E884}
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C} = {0B80DDC4-A13C-4C5F-8520-AA2F3650E884}
|
||||||
{2F2C937F-3732-4E6C-9720-FAB62DC33C63} = {0B80DDC4-A13C-4C5F-8520-AA2F3650E884}
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63} = {0B80DDC4-A13C-4C5F-8520-AA2F3650E884}
|
||||||
{ECD5BC60-7009-4099-7193-526FDC6E8D99} = {D64772B8-6213-4031-8064-2567F1E724F6}
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99} = {D64772B8-6213-4031-8064-2567F1E724F6}
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487} = {D64772B8-6213-4031-8064-2567F1E724F6}
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3} = {D64772B8-6213-4031-8064-2567F1E724F6}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {832CA90A-06D6-4312-9B35-16CC665EB37C}
|
SolutionGuid = {832CA90A-06D6-4312-9B35-16CC665EB37C}
|
||||||
|
|||||||
0
Program.cs
Normal file
0
Program.cs
Normal file
Reference in New Issue
Block a user