Compare commits

..

12 Commits

Author SHA1 Message Date
0db28a025f chore: add empty root-level Program.cs placeholder 2026-09-21 10:10:47 +02:00
6a321c1022 build(sln): register Tests.Live and Tests.NuGet projects; expand platform configurations
- Add DocumentService.Tests.Live (xUnit live tests, GUID CC5A7D7B) to solution
  under the existing Tests solution folder
- Add DocumentService.Tests.NuGet (smoke-test console app, GUID EE3E7C50) to
  solution under the Tests solution folder
- Expand GlobalSection(SolutionConfigurationPlatforms) to include x64 and x86
  variants for Debug and Release alongside the existing Any CPU entries
- Add corresponding ProjectConfigurationPlatforms entries for all projects
  (existing + new) so VS/MSBuild can resolve the correct output paths when
  targeting x64/x86 explicitly
2026-09-21 10:10:39 +02:00
dcb735f8c8 test(live): add live API integration test project (DocumentService.Tests.Live)
End-to-end tests that run against a real, deployed API instance to verify that
all controllers respond correctly over HTTP - bypassing WebApplicationFactory.

Project setup:
- Targets net8.0; xUnit 2.9.3 + FluentAssertions 7.0.0
- References DocumentService.Client, Application and Domain as project refs
- Embeds 4 test PDFs as EmbeddedResources:
    valid.pdf, pdfWithSwissQRCode.pdf, pdfWithMoreThanOneAttachment.pdf,
    encryptedPdf.pdf

LiveApiTestBase:
- Base URL defaults to http://172.24.12.39:9393, overridable via
  LIVE_API_BASE_URL env var
- Constructs typed clients (PdfValidationClient, PdfAttachmentClient,
  PdfOperationsClient, SwissQrCodeClient) backed by a single HttpClient
- LoadPdfBytes / LoadPdfStream helpers for EmbeddedResource access

Test coverage per file:
- PdfValidationLiveTests   : Stream + byte[] overloads for validate and
                             validate-pdfa; empty/corrupted inputs assert 4xx/5xx
- PdfAttachmentLiveTests   : check + extract endpoints; stream and byte[] variants
- PdfOperationsLiveTests   : merge and stamp endpoints
- SwissQrCodeLiveTests     : extract Swiss QR bill; invalid content asserts error

Tests are intentionally skipped in CI (no live server); run manually with
the LIVE_API_BASE_URL variable pointing to a running instance.
2026-09-21 10:10:28 +02:00
6527cdcbb0 test(nuget): add NuGet smoke-test console app (DocumentService.Tests.NuGet)
Standalone console application that references the published DocumentService.Client
NuGet package (v1.2.2) and verifies the full happy-path against a live API instance.

- Targets net8.0; references DocumentService.Client 1.2.2 via NuGet (not project ref)
- Embeds valid.pdf as an EmbeddedResource so no file-system path is required
- Calls Client.Validation.ValidateAsync(Stream) and prints PageCount, PdfVersion,
  IsEncrypted, AttachmentCount, FileSizeBytes to stdout
- Handles HttpRequestException and InvalidOperationException with descriptive output
- Intended to run once after a new package is pushed to confirm the .nupkg bundles
  Application and Domain DLLs correctly (validates the embedding targets added in
  the previous commit)
2026-09-21 10:10:09 +02:00
24db1b0d0a build(client): bump version to 1.2.2 and embed Application/Domain DLLs into NuGet package
- Bump Version, AssemblyVersion, FileVersion from 1.2.0 to 1.2.2
- Mark Application and Domain ProjectReferences as PrivateAssets=all so they
  are not propagated as transitive dependencies to consumers
- Add IncludeReferencedProjectDlls MSBuild target to pack Application.dll and
  Domain.dll into the correct lib/<tfm>/ path inside the .nupkg
- Add CopyProjectReferencesToPackage target (TargetsForTfmSpecificBuildOutput)
  as the per-TFM companion needed for multi-targeting builds to pick up both
  referenced project DLLs correctly
- Fix stray UTF-8 BOM on the opening Project element
2026-09-21 10:09:56 +02:00
f7cba05490 Update README.md with details on PDF/A validation
Added a section explaining the limitations of the `ValidatePdfAAsync` method, highlighting that it performs heuristic PDF/A detection rather than full ISO-19005 compliance checks. Listed specific requirements not validated and clarified the meaning of `IsValid = true`. Updated the API endpoint table to reflect the heuristic nature of the `ValidatePdfAAsync()` method.
2026-08-31 16:06:32 +02:00
a618e3559a Bump version to 1.2.0 and enhance QR code documentation
Updated `DocumentService.Client.csproj` to version 1.2.0, reflecting new features and improvements. Enhanced `README.md` with detailed examples for the new `InspectSwissQrCodeAsync` method, showcasing two approaches (TryExtract and direct null-check) for accessing QR code data. Added PowerShell-specific notes and improved code formatting for better readability. Clarified that `TryExtract` is an optional helper method.
2026-08-31 15:39:36 +02:00
68eeeb1eda Add TryExtract method to SwissQrCodeResult record
Replaced the fully qualified reference to
`DocumentService.Application.Common.DTOs.PdfValidationResult.IsValid`
with the simpler `PdfValidationResult.IsValid`.

Added a `TryExtract` method to the `SwissQrCodeResult` record to
improve code readability and usability. The method follows the
standard `TryXxx` pattern, allowing for an `if/else` branch
instead of a direct null-check on the `QrCode` property. It
returns `true` if the PDF was valid and a Swiss QR Code was
successfully extracted, and `false` otherwise.
2026-08-31 15:39:11 +02:00
0e22ffb2c5 Move WorkflowsClientExtensions to IWorkflowsClient.cs
Relocated the `WorkflowsClientExtensions` class from `WorkflowsClient.cs` to `IWorkflowsClient.cs` to improve discoverability by defining it alongside the `IWorkflowsClient` interface.

The `InspectSwissQrCodeAsync` extension method was moved as part of this change. Its functionality remains unchanged, as it still opens a `FileStream` for the provided file path and delegates to the `IWorkflowsClient.InspectSwissQrCodeAsync(Stream, bool, CancellationToken)` method.
2026-08-31 15:35:14 +02:00
62ab576e69 Update Swiss QR Code API to use InspectSwissQrCodeAsync
Replaced `ExtractSwissQrCodeAsync` with `InspectSwissQrCodeAsync`,
which combines validation and extraction into a single operation.
Updated README examples and code snippets (C#, VB.NET, PowerShell)
to reflect the new method. Removed `throwIfInvalid` parameter and
deprecated the old method with `[Obsolete]` warnings.

Documented the new method's behavior, including always returning
a `SwissQrCodeResult` object and handling invalid documents
gracefully without exceptions. Updated API reference table and
removed outdated error-handling examples. Added a note about the
planned server-side implementation of the new endpoint.
2026-08-31 14:45:37 +02:00
092d283a2b Refactor WorkflowsClient for unified result handling
Refactored `ExtractSwissQrCodeAsync` to `InspectSwissQrCodeAsync`, introducing `SwissQrCodeResult` to encapsulate both validation and extraction results. Removed `throwIfInvalid` parameter, ensuring validation details are always returned without exceptions. Added extension methods for file path handling to improve usability.

Updated tests to reflect the new behavior, ensuring extraction is never attempted on invalid documents and validation results are always populated. Simplified API surface by removing redundant overloads and improving stream handling. Enhanced documentation and performed general code cleanup for better maintainability.
2026-08-31 14:44:57 +02:00
71dd32f11c Update method names and improve API consistency
Replaced `ExtractAsync` with `ExtractSwissQrCodeAsync` in C#, VB.NET, and PowerShell examples for Swiss QR Code extraction. Updated the `throwIfInvalid` parameter to allow handling invalid PDFs without exceptions.

Revised the API endpoint table in README.md to reflect updated method names (`ExtractSwissQrCodeAsync` replaced with `ExtractAsync` for `Client.Attachment`, `Client.SwissQrCode`, and `Client.Zugferd`). Retained `ExtractSwissQrCodeAsync` for `Client.Workflows`.

These changes improve clarity, consistency, and functionality in the API and documentation.
2026-08-31 14:02:59 +02:00
21 changed files with 1642 additions and 443 deletions

View File

@@ -1,5 +1,6 @@
using DocumentService.Application.Common.DTOs;
using DocumentService.Client.Interfaces;
using DocumentService.Client.Models.Results;
namespace DocumentService.Client.Clients;
@@ -10,75 +11,34 @@ namespace DocumentService.Client.Clients;
public class WorkflowsClient(IPdfValidationClient validation, ISwissQrCodeClient swissQrCode) : IWorkflowsClient
{
/// <inheritdoc />
public async Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
public async Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
byte[] pdfBytes,
bool raw = false,
bool throwIfInvalid = true,
CancellationToken cancellationToken = default)
{
var validationResult = await validation.ValidateAsync(pdfBytes, cancellationToken);
if (!validationResult.IsValid)
return throwIfInvalid
? throw new InvalidOperationException(
$"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " +
$"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).")
: (SwissQrCodeExtractionResult?)null;
return await swissQrCode.ExtractAsync(pdfBytes, raw, cancellationToken);
var qrCode = validationResult.IsValid
? await swissQrCode.ExtractAsync(pdfBytes, raw, cancellationToken)
: null;
return new SwissQrCodeResult(qrCode, validationResult);
}
/// <inheritdoc />
public async Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
public async Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
Stream pdfStream,
bool raw = false,
bool throwIfInvalid = true,
CancellationToken cancellationToken = default)
{
var validationResult = await validation.ValidateAsync(pdfStream, cancellationToken);
if (!validationResult.IsValid)
return throwIfInvalid
? throw new InvalidOperationException(
$"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " +
$"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).")
: (SwissQrCodeExtractionResult?)null;
return new SwissQrCodeResult(null, validationResult);
// Stream was consumed by validation – reset if possible, otherwise re-open is caller's responsibility.
// Since callers typically pass a FileStream or MemoryStream, Position reset covers the common case.
if (pdfStream.CanSeek)
pdfStream.Position = 0;
return await swissQrCode.ExtractAsync(pdfStream, raw, cancellationToken);
var qrCode = await swissQrCode.ExtractAsync(pdfStream, raw, cancellationToken);
return new SwissQrCodeResult(qrCode, validationResult);
}
/// <inheritdoc />
public async Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
string filePath,
bool raw = false,
bool throwIfInvalid = true,
CancellationToken cancellationToken = default)
{
#if NETFRAMEWORK
using var stream = File.OpenRead(filePath);
var validationResult = await validation.ValidateAsync(stream, cancellationToken);
#else
var pdfBytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
var validationResult = await validation.ValidateAsync(pdfBytes, cancellationToken);
#endif
if (!validationResult.IsValid)
return throwIfInvalid
? throw new InvalidOperationException(
$"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " +
$"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).")
: (SwissQrCodeExtractionResult?)null;
#if NETFRAMEWORK
using var extractStream = File.OpenRead(filePath);
return await swissQrCode.ExtractAsync(extractStream, raw, cancellationToken);
#else
return await swissQrCode.ExtractAsync(pdfBytes, raw, cancellationToken);
#endif
}
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
@@ -11,9 +11,9 @@
<PackageIcon>icon.png</PackageIcon>
<RepositoryUrl>http://git.dd:3000/AppStd/Rec.git</RepositoryUrl>
<PackageTags>digital data document service api client</PackageTags>
<Version>1.1.0</Version>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<Version>1.2.2</Version>
<AssemblyVersion>1.2.2.0</AssemblyVersion>
<FileVersion>1.2.2.0</FileVersion>
<Description></Description>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -44,7 +44,34 @@
</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>
<!-- 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>

View File

@@ -1,4 +1,5 @@
using DocumentService.Application.Common.DTOs;
using DocumentService.Client.Models.Results;
namespace DocumentService.Client.Interfaces;
@@ -9,51 +10,62 @@ namespace DocumentService.Client.Interfaces;
public interface IWorkflowsClient
{
/// <summary>
/// Validates the PDF and extracts its Swiss QR Code in a single call.
/// Validation runs first; extraction is only attempted on valid documents.
/// Validates the PDF and attempts Swiss QR Code extraction in a single call,
/// returning both results together. The method never throws on invalid documents –
/// the caller receives the full <see cref="PdfValidationResult"/> regardless of outcome,
/// enabling detailed logging, diagnostics, or batch processing without try/catch.
/// </summary>
/// <param name="pdfBytes">PDF file as byte array.</param>
/// <param name="raw">
/// <c>false</c> (default) – returns a parsed <see cref="SwissQrCodeExtractionResult.Bill"/> object.<br/>
/// <c>true</c> – returns raw QR text lines without parsing.
/// </param>
/// <param name="throwIfInvalid">
/// <c>true</c> (default) – throws <see cref="InvalidOperationException"/> when the PDF fails validation.<br/>
/// <c>false</c> – returns <c>null</c> instead, allowing the caller to handle the case without a try/catch.
/// </param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>
/// Swiss QR Code extraction result, or <c>null</c> if the document is invalid and
/// <paramref name="throwIfInvalid"/> is <c>false</c>.
/// A <see cref="SwissQrCodeResult"/> containing the <see cref="PdfValidationResult"/>
/// (always populated) and the <see cref="SwissQrCodeExtractionResult"/> (<c>null</c> if the
/// document was invalid).
/// </returns>
/// <exception cref="InvalidOperationException">
/// Thrown when the PDF fails validation and <paramref name="throwIfInvalid"/> is <c>true</c>.
/// </exception>
Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
byte[] pdfBytes,
bool raw = false,
bool throwIfInvalid = true,
CancellationToken cancellationToken = default);
/// <inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)"/>
/// <inheritdoc cref="InspectSwissQrCodeAsync(byte[], bool, CancellationToken)"/>
/// <param name="pdfStream">PDF file as a stream.</param>
/// <param name="raw"><inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='raw']"/></param>
/// <param name="throwIfInvalid"><inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='throwIfInvalid']"/></param>
/// <param name="raw"><inheritdoc cref="InspectSwissQrCodeAsync(byte[], bool, CancellationToken)" path="/param[@name=''raw'']"/></param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
Stream pdfStream,
bool raw = false,
bool throwIfInvalid = true,
CancellationToken cancellationToken = default);
/// <inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)"/>
/// <param name="filePath">Absolute or relative path to the PDF file.</param>
/// <param name="raw"><inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='raw']"/></param>
/// <param name="throwIfInvalid"><inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='throwIfInvalid']"/></param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
string filePath,
bool raw = false,
bool throwIfInvalid = true,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Convenience extensions for <see cref="IWorkflowsClient"/> that accept a file path as input.
/// Defined alongside <see cref="IWorkflowsClient"/> so they are discoverable without an additional using directive.
/// </summary>
public static class WorkflowsClientExtensions
{
/// <summary>
/// Validates the PDF at <paramref name="filePath"/> and attempts Swiss QR Code extraction
/// in a single call. Opens a <see cref="FileStream"/> and delegates to
/// <see cref="IWorkflowsClient.InspectSwissQrCodeAsync(Stream, bool, CancellationToken)"/>.
/// </summary>
/// <param name="client">The workflows client.</param>
/// <param name="filePath">Absolute or relative path to the PDF file.</param>
/// <param name="raw">
/// <c>false</c> (default) – returns a parsed Bill object.<br/>
/// <c>true</c> – returns raw QR text lines without parsing.
/// </param>
/// <param name="cancellationToken">Cancellation token.</param>
public static async Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
this IWorkflowsClient client,
string filePath,
bool raw = false,
CancellationToken cancellationToken = default)
{
using var stream = File.OpenRead(filePath);
return await client.InspectSwissQrCodeAsync(stream, raw, cancellationToken);
}
}

View 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;
}
}

View File

@@ -183,97 +183,92 @@ Alle Beispiele nutzen ausschließlich den statischen `Client`-Einstiegspunkt. Vo
### Workflows – Kombinierte Operationen
`Client.Workflows` fasst häufig benötigte Abläufe zusammen, die mehrere Einzelendpunkte orchestrieren. Die Validierung läuft dabei immer zuerst – die Folgeaktion wird nur bei einem gültigen Dokument ausgeführt.
`Client.Workflows` fasst häufig benötigte Abläufe zusammen, die mehrere Einzelendpunkte orchestrieren. Validierung und Extraktion laufen in einem einzigen Aufruf – das Ergebnis enthält immer beide Informationen.
> Für Anwendungsfälle wie den **eParser** (Swiss QR Bill aus Eingangsrechnungen lesen) ist dies der empfohlene Einstiegspunkt.
#### Swiss QR Code – mit vorgelagerter Validierung
#### Swiss QR Code – Validierung und Extraktion in einem Schritt
`InspectSwissQrCodeAsync` gibt immer ein `SwissQrCodeResult` zurück, das sowohl das Validierungsergebnis als auch den extrahierten QR-Code enthält. Für den Zugriff auf den QR-Code stehen zwei gleichwertige Varianten zur Verfügung:
```csharp
using DocumentService.Client;
using DocumentService.Client.Models.Results;
// Variante A: byte[]
byte[] pdfBytes = await File.ReadAllBytesAsync("rechnung.pdf");
var result = await Client.Workflows.ExtractAsync(pdfBytes);
SwissQrCodeResult result = await Client.Workflows.InspectSwissQrCodeAsync(pdfBytes);
Console.WriteLine($"Betrag: {result?.Bill?.Amount} {result?.Bill?.Currency}");
Console.WriteLine($"Empfänger: {result?.Bill?.Creditor?.Name}");
// Validierungsergebnis ist immer verfügbar
Console.WriteLine($"Gültig: {result.Validation.IsValid}");
Console.WriteLine($"Seiten: {result.Validation.PageCount}");
Console.WriteLine($"Verschlüsselt: {result.Validation.IsEncrypted}");
// Variante B: Stream
using var stream = File.OpenRead("rechnung.pdf");
var result2 = await Client.Workflows.ExtractAsync(stream);
// Variante A: TryExtract – lesbarerer Code, kein expliziter Null-Check notwendig
// (optionale Hilfsmethode; Variante B ist gleichwertig)
if (result.TryExtract(out var qrCode))
{
Console.WriteLine($"Betrag: {qrCode.Bill?.Amount} {qrCode.Bill?.Currency}");
Console.WriteLine($"Empfänger: {qrCode.Bill?.Creditor?.Name}");
}
else
{
Console.WriteLine("Kein Swiss QR Code gefunden oder Dokument ungültig.");
}
// Variante C: Dateipfad
var result3 = await Client.Workflows.ExtractAsync("rechnung.pdf");
// Variante B: direkter Null-Check auf QrCode
if (result.QrCode is not null)
{
Console.WriteLine($"Betrag: {result.QrCode.Bill?.Amount} {result.QrCode.Bill?.Currency}");
}
```
```vbnet
Imports DocumentService.Client
Imports DocumentService.Client.Models.Results
' Variante A: byte[]
Dim pdfBytes = System.IO.File.ReadAllBytes("rechnung.pdf")
Dim result = Await Client.Workflows.ExtractAsync(pdfBytes)
Dim result As SwissQrCodeResult = Await Client.Workflows.InspectSwissQrCodeAsync(pdfBytes)
Console.WriteLine($"Betrag: {result?.Bill?.Amount} {result?.Bill?.Currency}")
Console.WriteLine($"Empfänger: {result?.Bill?.Creditor?.Name}")
Console.WriteLine($"Gültig: {result.Validation.IsValid}")
Console.WriteLine($"Seiten: {result.Validation.PageCount}")
Console.WriteLine($"Verschlüsselt: {result.Validation.IsEncrypted}")
' Variante B: Dateipfad
Dim result2 = Await Client.Workflows.ExtractAsync("rechnung.pdf")
' Variante A: TryExtract
Dim qrCode As DocumentService.Application.Common.DTOs.SwissQrCodeExtractionResult = Nothing
If result.TryExtract(qrCode) Then
Console.WriteLine($"Betrag: {qrCode.Bill?.Amount} {qrCode.Bill?.Currency}")
Console.WriteLine($"Empfänger: {qrCode.Bill?.Creditor?.Name}")
Else
Console.WriteLine("Kein Swiss QR Code gefunden oder Dokument ungültig.")
End If
' Variante B: direkter Null-Check
If result.QrCode IsNot Nothing Then
Console.WriteLine($"Betrag: {result.QrCode.Bill?.Amount} {result.QrCode.Bill?.Currency}")
End If
```
```powershell
Add-Type -Path "DocumentService.Client.dll"
# Variante A: byte[]
$pdfBytes = [System.IO.File]::ReadAllBytes("rechnung.pdf")
$result = [DocumentService.Client.Client]::Workflows.ExtractAsync($pdfBytes).GetAwaiter().GetResult()
Write-Host "Betrag: $($result.Bill.Amount) $($result.Bill.Currency)"
Write-Host "Empfaenger:$($result.Bill.Creditor.Name)"
$result = [DocumentService.Client.Client]::Workflows.InspectSwissQrCodeAsync($pdfBytes).GetAwaiter().GetResult()
# Variante B: Dateipfad
$result2 = [DocumentService.Client.Client]::Workflows.ExtractAsync("rechnung.pdf").GetAwaiter().GetResult()
```
Write-Host "Gueltig: $($result.Validation.IsValid)"
Write-Host "Seiten: $($result.Validation.PageCount)"
Write-Host "Verschluesselt:$($result.Validation.IsEncrypted)"
**Fehlerbehandlung – `throwIfInvalid`:**
Standardmäßig wirft die Methode eine `InvalidOperationException`, wenn das PDF ungültig ist. Mit `throwIfInvalid: false` wird stattdessen `null` zurückgegeben – geeignet, wenn ungültige Dokumente kein Ausnahmefall sind und ohne try/catch behandelt werden sollen.
```csharp
// Exception (Standard) – geeignet, wenn ungültige PDFs ein Fehlerfall sind
var result = await Client.Workflows.ExtractAsync(pdfBytes);
// Null zurückgeben – geeignet für Batch-Verarbeitung o. Ä.
var result = await Client.Workflows.ExtractAsync(pdfBytes, throwIfInvalid: false);
if (result is null)
{
Console.WriteLine("Dokument ungültig oder verschlüsselt – übersprungen.");
# TryExtract ist in PowerShell nicht direkt nutzbar (out-Parameter).
# Direkter Null-Check ist hier die einzige Option:
if ($null -ne $result.QrCode) {
Write-Host "Betrag: $($result.QrCode.Bill.Amount) $($result.QrCode.Bill.Currency)"
Write-Host "Empfaenger:$($result.QrCode.Bill.Creditor.Name)"
} else {
Write-Host "Kein Swiss QR Code gefunden oder Dokument ungueltig."
}
```
```vbnet
' Exception (Standard)
Dim result = Await Client.Workflows.ExtractAsync(pdfBytes)
' Null zurückgeben
Dim result2 = Await Client.Workflows.ExtractAsync(pdfBytes, throwIfInvalid:=False)
If result2 Is Nothing Then
Console.WriteLine("Dokument ungültig oder verschlüsselt – übersprungen.")
End If
```
```powershell
# Exception (Standard)
$result = [DocumentService.Client.Client]::Workflows.ExtractAsync($pdfBytes).GetAwaiter().GetResult()
# Null zurückgeben ($false = throwIfInvalid:false)
$result2 = [DocumentService.Client.Client]::Workflows.ExtractAsync($pdfBytes, $false, $false).GetAwaiter().GetResult()
if ($null -eq $result2) {
Write-Host "Dokument ungueltig oder verschluesselt – uebersprungen."
}
```
> **Signatur:** `ExtractSwissQrCodeAsync(pdfBytes / Stream / filePath, raw = false, throwIfInvalid = true, ct = default)`
> `raw: true` liefert die Rohtextzeilen des QR-Codes ohne Parsing zurück (siehe [Schweizer QR-Code-Extraktion](#4-schweizer-qr-code-extraktion)).
> **`TryExtract` – Hinweis:** Die Methode ist eine optionale Lesbarkeits-Hilfe und nicht zwingend erforderlich. Der direkte Null-Check auf `result.QrCode` ist gleichwertig. Gibt `TryExtract` `false` zurück, ist der `out`-Parameter `qrCode` `null` – er darf in diesem Fall nicht verwendet werden.
---
@@ -339,6 +334,15 @@ $result2 = [DocumentService.Client.Client]::Validation.ValidateAsync($pdfBytes).
#### 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
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.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` | `ExtractSwissQrCodeAsync()` | `POST /api/pdf/attachments/extract` | ✅ Verfügbar | Anhänge extrahieren als `Dictionary<string, Stream>` |
| `Client.Attachment` | `ExtractAsync()` | `POST /api/pdf/attachments/extract` | ✅ Verfügbar | Anhänge extrahieren als `Dictionary<string, Stream>` |
| `Client.Attachment` | `AddAsync()` | `POST /api/pdf/attachments/add` | ⏳ Geplant | Anhänge in PDF/A-3 einbetten |
| `Client.Operations` | `MergeAsync()` | `POST /api/pdf/operations/merge` | ✅ Verfügbar | Mehrere PDFs zusammenführen |
| `Client.Operations` | `AnnotateAsync()` | `POST /api/pdf/operations/annotate` | ✅ Verfügbar | Annotationen hinzufügen |
| `Client.Operations` | `StampAsync()` | `POST /api/pdf/operations/stamp` | ✅ Verfügbar | Text-/Bildstempel hinzufügen |
| `Client.SwissQrCode` | `ExtractSwissQrCodeAsync()` | `POST /api/pdf/qr-code/extract-swiss` | ✅ Verfügbar | Schweizer QR-Code-Daten extrahieren |
| `Client.SwissQrCode` | `ExtractAsync()` | `POST /api/pdf/qr-code/extract-swiss` | ✅ Verfügbar | Schweizer QR-Code-Daten extrahieren |
| `Client.Zugferd` | `CheckAsync()` | `POST /api/pdf/zugferd/has-zugferd` | ✅ Verfügbar | ZUGFeRD-Einbettung prüfen |
| `Client.Zugferd` | `ExtractSwissQrCodeAsync()` | `POST /api/pdf/zugferd/extract` | ✅ Verfügbar | ZUGFeRD-XML als Stream |
| `Client.Zugferd` | `ExtractAsync()` | `POST /api/pdf/zugferd/extract` | ✅ Verfügbar | ZUGFeRD-XML als Stream |
| `Client.Zugferd` | `ExtractAsResultAsync()` | `POST /api/pdf/zugferd/extract` | ✅ Verfügbar | ZUGFeRD-XML mit Metadaten als Objekt |
| `Client.Conversion` | `ToPdfAAsync()` | `POST /api/pdf/conversion/to-pdfa` | ⏳ Geplant | PDF zu PDF/A konvertieren |
| `Client.Conversion` | `FromPdfAAsync()` | `POST /api/pdf/conversion/from-pdfa` | ⏳ Geplant | PDF/A-Einschränkungen aufheben |
| `Client.Workflows` | `ExtractSwissQrCodeAsync()` | — | ✅ Verfügbar | Validierung + Swiss QR Code Extraktion in einem Schritt |
| `Client.Workflows` | `InspectSwissQrCodeAsync()` | — | ✅ Verfügbar | Validierung + Swiss QR Code Extraktion; gibt immer `SwissQrCodeResult` zurück, wirft nie |
> **⏳ Geplant:** Der Client-Code ist vorhanden und kompiliert fehlerfrei. Ein Aufruf zur Laufzeit wirft jedoch `NotImplementedException`, da der serverseitige Endpunkt noch nicht existiert. Die Methoden sind mit `[Obsolete]` markiert, sodass der Compiler eine Build-Warnung ausgibt.

View 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>();
}
}

View 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");
}
}

View 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>();
}
}

View 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>();
}
}

View 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>

View 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();
}

View 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.

View File

@@ -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>

View 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);
}

Binary file not shown.

View File

@@ -20,6 +20,8 @@ public class WorkflowsClientTests
private readonly Mock<IPdfValidationClient> _validation = new();
private readonly Mock<ISwissQrCodeClient> _swissQrCode = new();
private WorkflowsClient Sut => new(_validation.Object, _swissQrCode.Object);
// Extension methods are defined on IWorkflowsClient — use this for filePath overload tests.
private IWorkflowsClient SutAsInterface => Sut;
// ─── fixture data ─────────────────────────────────────────────────────────
@@ -75,12 +77,9 @@ public class WorkflowsClientTests
private static SwissQrCodeExtractionResult RawQrResult() =>
new(Bill: (SwissQrBillDto?)null, RawLines: ["SPC", "0200", "1", "CH9300762011623852957"]);
// ═════════════════════════════════════════════════════════════════════════
// byte[] overload
// ═════════════════════════════════════════════════════════════════════════
[Fact]
public async Task Bytes_ValidPdf_CallsValidationThenExtraction_ReturnsResult()
public async Task Inspect_Bytes_ValidPdf_ReturnsBothValidationAndQrCode()
{
// Arrange
var pdfBytes = ValidPdfBytes();
@@ -90,18 +89,56 @@ public class WorkflowsClientTests
.ReturnsAsync(QrResult());
// Act
var result = await Sut.ExtractSwissQrCodeAsync(pdfBytes);
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes);
// Assert
result.Should().NotBeNull();
result!.Bill.Should().NotBeNull();
result.Bill!.Currency.Should().Be("CHF");
result.Validation.IsValid.Should().BeTrue();
result.QrCode.Should().NotBeNull();
result.QrCode!.Bill!.Currency.Should().Be("CHF");
_validation.Verify(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()), Times.Once);
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, false, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Bytes_ValidPdf_RawMode_PassesRawFlagToExtraction()
public async Task Inspect_Bytes_InvalidPdf_ReturnsValidationWithNullQrCode_NeverThrows()
{
// Arrange
var pdfBytes = EncryptedPdfBytes();
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
.ReturnsAsync(InvalidResult_Encrypted());
// Act
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes);
// Assert – no exception, validation details always available
result.Validation.IsValid.Should().BeFalse();
result.Validation.IsEncrypted.Should().BeTrue();
result.QrCode.Should().BeNull("extraction must not run on an invalid document");
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task Inspect_Bytes_ValidationAlwaysPopulated_RegardlessOfQrOutcome()
{
// Arrange – valid PDF but QR extraction returns empty lines (no QR code found)
var pdfBytes = ValidPdfBytes();
var emptyQr = new SwissQrCodeExtractionResult(Bill: (SwissQrBillDto?)null, RawLines: []);
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
.ReturnsAsync(ValidResult());
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(emptyQr);
// Act
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes);
// Assert – validation is always there even when QR is empty
result.Validation.Should().NotBeNull();
result.Validation.PageCount.Should().Be(1);
result.QrCode.Should().NotBeNull(); // extraction was attempted
}
[Fact]
public async Task Inspect_Bytes_RawMode_PassesRawFlagToExtraction()
{
// Arrange
var pdfBytes = QrPdfBytes();
@@ -111,109 +148,15 @@ public class WorkflowsClientTests
.ReturnsAsync(RawQrResult());
// Act
var result = await Sut.ExtractSwissQrCodeAsync(pdfBytes, raw: true);
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes, raw: true);
// Assert
result.Should().NotBeNull();
result!.RawLines.Should().Contain("SPC");
result.QrCode!.RawLines.Should().Contain("SPC");
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, true, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Bytes_InvalidPdf_ThrowIfInvalidTrue_ThrowsInvalidOperationException()
{
// Arrange
var pdfBytes = EncryptedPdfBytes();
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
.ReturnsAsync(InvalidResult_Encrypted());
// Act
var act = async () => await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: true);
// Assert
await act.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*PDF validation failed*");
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task Bytes_InvalidPdf_ThrowIfInvalidFalse_ReturnsNull()
{
// Arrange
var pdfBytes = EncryptedPdfBytes();
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
.ReturnsAsync(InvalidResult_Encrypted());
// Act
var result = await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: false);
// Assert
result.Should().BeNull();
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task Bytes_EmptyFilePdf_ThrowIfInvalidTrue_ThrowsInvalidOperationException()
{
// Arrange – PageCount=0 and FileSize=0 → IsValid=false
var pdfBytes = new byte[] { 0x00 };
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
.ReturnsAsync(InvalidResult_EmptyFile());
// Act & Assert
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: true))
.Should().ThrowAsync<InvalidOperationException>();
}
[Fact]
public async Task Bytes_ExceptionMessage_ContainsValidationDetails()
{
// Arrange – ensure the message includes diagnostic fields
var pdfBytes = ValidPdfBytes();
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
.ReturnsAsync(InvalidResult_Encrypted());
// Act
var act = async () => await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: true);
// Assert
var ex = await act.Should().ThrowAsync<InvalidOperationException>();
ex.WithMessage("*IsEncrypted=True*");
}
[Fact]
public async Task Bytes_ValidationThrows_PropagatesHttpRequestException()
{
// Arrange – simulate network/API failure during validation
var pdfBytes = ValidPdfBytes();
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
.ThrowsAsync(new HttpRequestException("API unreachable"));
// Act & Assert
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfBytes))
.Should().ThrowAsync<HttpRequestException>()
.WithMessage("*API unreachable*");
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task Bytes_ExtractionThrows_PropagatesHttpRequestException()
{
// Arrange – validation succeeds but extraction fails
var pdfBytes = QrPdfBytes();
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
.ReturnsAsync(ValidResult());
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new HttpRequestException("Extraction failed"));
// Act & Assert
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfBytes))
.Should().ThrowAsync<HttpRequestException>()
.WithMessage("*Extraction failed*");
}
[Fact]
public async Task Bytes_ValidPdf_CancellationToken_IsForwardedToBothClients()
public async Task Inspect_Bytes_CancellationToken_ForwardedToBothClients()
{
// Arrange
var pdfBytes = ValidPdfBytes();
@@ -223,21 +166,17 @@ public class WorkflowsClientTests
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, token)).ReturnsAsync(QrResult());
// Act
await Sut.ExtractSwissQrCodeAsync(pdfBytes, cancellationToken: token);
await Sut.InspectSwissQrCodeAsync(pdfBytes, cancellationToken: token);
// Assert
_validation.Verify(v => v.ValidateAsync(pdfBytes, token), Times.Once);
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, false, token), Times.Once);
}
// ═════════════════════════════════════════════════════════════════════════
// Stream overload
// ═════════════════════════════════════════════════════════════════════════
[Fact]
public async Task Stream_ValidPdf_CallsValidationWithStreamThenExtractionWithStream_ReturnsResult()
public async Task Inspect_Stream_ValidPdf_ReturnsBothResults()
{
// Arrange – validation client receives a Stream, extraction client receives the same stream reset to 0
// Arrange
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(ValidResult());
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()))
@@ -246,53 +185,15 @@ public class WorkflowsClientTests
using var pdfStream = new MemoryStream(ValidPdfBytes());
// Act
var result = await Sut.ExtractSwissQrCodeAsync(pdfStream);
var result = await Sut.InspectSwissQrCodeAsync(pdfStream);
// Assert
result.Should().NotBeNull();
_validation.Verify(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()), Times.Once);
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()), Times.Once);
result.Validation.IsValid.Should().BeTrue();
result.QrCode.Should().NotBeNull();
}
[Fact]
public async Task Stream_SeekableStream_IsResetBetweenValidationAndExtraction()
{
// Arrange – we verify the stream position is 0 when extraction is called
Stream? streamAtExtraction = null;
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(ValidResult());
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()))
.Callback<Stream, bool, CancellationToken>((s, _, _) => streamAtExtraction = s)
.ReturnsAsync(QrResult());
using var pdfStream = new MemoryStream(ValidPdfBytes());
// Act
await Sut.ExtractSwissQrCodeAsync(pdfStream);
// Assert
streamAtExtraction.Should().NotBeNull();
streamAtExtraction!.Position.Should().Be(0, "stream must be rewound before extraction");
}
[Fact]
public async Task Stream_InvalidPdf_ThrowIfInvalidTrue_ThrowsWithoutCallingExtraction()
{
// Arrange
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(InvalidResult_Encrypted());
using var pdfStream = new MemoryStream(EncryptedPdfBytes());
// Act & Assert
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfStream, throwIfInvalid: true))
.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*PDF validation failed*");
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<Stream>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task Stream_InvalidPdf_ThrowIfInvalidFalse_ReturnsNull()
public async Task Inspect_Stream_InvalidPdf_ReturnsValidationWithNullQrCode_NeverThrows()
{
// Arrange
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
@@ -301,173 +202,56 @@ public class WorkflowsClientTests
using var pdfStream = new MemoryStream(EncryptedPdfBytes());
// Act
var result = await Sut.ExtractSwissQrCodeAsync(pdfStream, throwIfInvalid: false);
var result = await Sut.InspectSwissQrCodeAsync(pdfStream);
// Assert
result.Should().BeNull();
result.Validation.IsValid.Should().BeFalse();
result.QrCode.Should().BeNull();
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<Stream>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task Stream_ValidPdf_RawMode_PassesRawFlagToExtraction()
public async Task Inspect_FilePath_ValidPdf_ReturnsBothResults()
{
// Arrange
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(ValidResult());
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<Stream>(), true, It.IsAny<CancellationToken>()))
.ReturnsAsync(RawQrResult());
using var pdfStream = new MemoryStream(QrPdfBytes());
// Act
var result = await Sut.ExtractSwissQrCodeAsync(pdfStream, raw: true);
// Assert
result.Should().NotBeNull();
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<Stream>(), true, It.IsAny<CancellationToken>()), Times.Once);
}
// ═════════════════════════════════════════════════════════════════════════
// filePath overload
// ═════════════════════════════════════════════════════════════════════════
[Fact]
public async Task FilePath_ValidPdf_ReadsFileAndCallsBothClients_ReturnsResult()
{
// Arrange – write a real temp file so File.ReadAllBytesAsync / File.OpenRead can read it
// Arrange – extension opens a FileStream so mock expects Stream overload
var tempPath = Path.GetTempFileName();
try
{
await File.WriteAllBytesAsync(tempPath, ValidPdfBytes());
_validation.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(ValidResult());
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<byte[]>(), false, It.IsAny<CancellationToken>()))
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()))
.ReturnsAsync(QrResult());
// Act
var result = await Sut.ExtractSwissQrCodeAsync(tempPath);
// Act – calls extension method which opens a FileStream and delegates to Stream overload
var result = await SutAsInterface.InspectSwissQrCodeAsync(tempPath);
// Assert
result.Should().NotBeNull();
_validation.Verify(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Once);
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), false, It.IsAny<CancellationToken>()), Times.Once);
}
finally
{
File.Delete(tempPath);
result.Validation.IsValid.Should().BeTrue();
result.QrCode.Should().NotBeNull();
}
finally { File.Delete(tempPath); }
}
[Fact]
public async Task FilePath_InvalidPdf_ThrowIfInvalidTrue_ThrowsWithoutCallingExtraction()
public async Task Inspect_FilePath_InvalidPdf_ReturnsValidationWithNullQrCode_NeverThrows()
{
// Arrange
var tempPath = Path.GetTempFileName();
try
{
await File.WriteAllBytesAsync(tempPath, EncryptedPdfBytes());
_validation.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(InvalidResult_Encrypted());
// Act & Assert
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(tempPath, throwIfInvalid: true))
.Should().ThrowAsync<InvalidOperationException>();
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
}
finally
{
File.Delete(tempPath);
}
}
[Fact]
public async Task FilePath_InvalidPdf_ThrowIfInvalidFalse_ReturnsNull()
{
// Arrange
var tempPath = Path.GetTempFileName();
try
{
await File.WriteAllBytesAsync(tempPath, EncryptedPdfBytes());
_validation.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(InvalidResult_Encrypted());
// Act
var result = await Sut.ExtractSwissQrCodeAsync(tempPath, throwIfInvalid: false);
var result = await SutAsInterface.InspectSwissQrCodeAsync(tempPath);
// Assert
result.Should().BeNull();
}
finally
{
File.Delete(tempPath);
// Assert – never throws, validation details always populated
result.Validation.IsValid.Should().BeFalse();
result.QrCode.Should().BeNull();
}
finally { File.Delete(tempPath); }
}
[Fact]
public async Task FilePath_NonExistentFile_ThrowsFileNotFoundException()
{
// Arrange
var nonExistent = Path.Combine(Path.GetTempPath(), "does_not_exist_12345.pdf");
// Act & Assert
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(nonExistent))
.Should().ThrowAsync<FileNotFoundException>();
_validation.Verify(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Never);
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task FilePath_ValidPdf_RawMode_PassesRawFlagToExtraction()
{
// Arrange
var tempPath = Path.GetTempFileName();
try
{
await File.WriteAllBytesAsync(tempPath, QrPdfBytes());
_validation.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(ValidResult());
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<byte[]>(), true, It.IsAny<CancellationToken>()))
.ReturnsAsync(RawQrResult());
// Act
var result = await Sut.ExtractSwissQrCodeAsync(tempPath, raw: true);
// Assert
result.Should().NotBeNull();
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), true, It.IsAny<CancellationToken>()), Times.Once);
}
finally
{
File.Delete(tempPath);
}
}
// ═════════════════════════════════════════════════════════════════════════
// Orchestration contract: extraction must NEVER be called on invalid PDF
// ═════════════════════════════════════════════════════════════════════════
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task AnyOverload_InvalidPdf_ExtractionIsNeverCalled_RegardlessOfThrowIfInvalid(bool throwIfInvalid)
{
// Arrange
var pdfBytes = EncryptedPdfBytes();
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
.ReturnsAsync(InvalidResult_Encrypted());
// Act
try { await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: throwIfInvalid); }
catch (InvalidOperationException) { /* expected when throwIfInvalid=true */ }
// Assert — extraction must never be called in either branch
_swissQrCode.Verify(
q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()),
Times.Never,
"extraction must not be attempted on an invalid document");
}
}
}

View File

@@ -40,36 +40,116 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Domain", "D
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Tests", "DocumentService.Tests\DocumentService.Tests.csproj", "{ECD5BC60-7009-4099-7193-526FDC6E8D99}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{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|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.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.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.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.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.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.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.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.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.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.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.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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -85,6 +165,8 @@ Global
{459EDCC5-240E-A2BC-1330-B5B2326B679C} = {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}
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487} = {D64772B8-6213-4031-8064-2567F1E724F6}
{EE3E7C50-E044-4F77-BB19-74286DDA43F3} = {D64772B8-6213-4031-8064-2567F1E724F6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {832CA90A-06D6-4312-9B35-16CC665EB37C}

0
Program.cs Normal file
View File