Compare commits
18 Commits
d1fdbb494b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0db28a025f | |||
| 6a321c1022 | |||
| dcb735f8c8 | |||
| 6527cdcbb0 | |||
| 24db1b0d0a | |||
| f7cba05490 | |||
| a618e3559a | |||
| 68eeeb1eda | |||
| 0e22ffb2c5 | |||
| 62ab576e69 | |||
| 092d283a2b | |||
| 71dd32f11c | |||
| f9d4e53ddc | |||
| d92e7f0b07 | |||
| 189622c476 | |||
| f51fde6a23 | |||
| f4d87b42f3 | |||
| 0c27e4fd91 |
@@ -17,4 +17,7 @@ public record PdfValidationResult(
|
|||||||
string PdfVersion,
|
string PdfVersion,
|
||||||
bool HasAttachments,
|
bool HasAttachments,
|
||||||
int AttachmentCount,
|
int AttachmentCount,
|
||||||
bool IsEncrypted);
|
bool IsEncrypted)
|
||||||
|
{
|
||||||
|
public bool IsValid => !IsEncrypted && PageCount > 0 && FileSizeBytes > 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -89,5 +89,7 @@ public static class Client
|
|||||||
public static ISwissQrCodeClient SwissQrCode => GetRequiredServiceOfScope<ISwissQrCodeClient>();
|
public static ISwissQrCodeClient SwissQrCode => GetRequiredServiceOfScope<ISwissQrCodeClient>();
|
||||||
|
|
||||||
public static IZugferdClient Zugferd => GetRequiredServiceOfScope<IZugferdClient>();
|
public static IZugferdClient Zugferd => GetRequiredServiceOfScope<IZugferdClient>();
|
||||||
|
|
||||||
|
public static IWorkflowsClient Workflows => GetRequiredServiceOfScope<IWorkflowsClient>();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public class PdfAttachmentClient(HttpClient httpClient, ILogger<PdfAttachmentCli
|
|||||||
: BaseDocumentClient(httpClient, logger), IPdfAttachmentClient
|
: BaseDocumentClient(httpClient, logger), IPdfAttachmentClient
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<AttachmentCheckResult> CheckAttachmentsAsync(Stream pdfStream, CancellationToken cancellationToken = default)
|
public async Task<AttachmentCheckResult> CheckAsync(Stream pdfStream, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Checking PDF attachments from stream (multipart)");
|
Logger.LogDebug("Checking PDF attachments from stream (multipart)");
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ public class PdfAttachmentClient(HttpClient httpClient, ILogger<PdfAttachmentCli
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<AttachmentCheckResult> CheckAttachmentsAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
public async Task<AttachmentCheckResult> CheckAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Checking PDF attachments from byte array (Base64 JSON)");
|
Logger.LogDebug("Checking PDF attachments from byte array (Base64 JSON)");
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ public class PdfAttachmentClient(HttpClient httpClient, ILogger<PdfAttachmentCli
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<Dictionary<string, Stream>> ExtractAttachmentsAsync(Stream pdfStream, CancellationToken cancellationToken = default)
|
public async Task<Dictionary<string, Stream>> ExtractAsync(Stream pdfStream, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Extracting PDF attachments from stream (multipart)");
|
Logger.LogDebug("Extracting PDF attachments from stream (multipart)");
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ public class PdfAttachmentClient(HttpClient httpClient, ILogger<PdfAttachmentCli
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<Dictionary<string, Stream>> ExtractAttachmentsAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
public async Task<Dictionary<string, Stream>> ExtractAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Extracting PDF attachments from byte array (Base64 JSON)");
|
Logger.LogDebug("Extracting PDF attachments from byte array (Base64 JSON)");
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ public class PdfAttachmentClient(HttpClient httpClient, ILogger<PdfAttachmentCli
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[Obsolete("API endpoint not implemented yet")]
|
[Obsolete("API endpoint not implemented yet")]
|
||||||
public Task<Stream> AddAttachmentsAsync(Stream pdfStream, List<AttachmentRequestDto> attachments, CancellationToken cancellationToken = default)
|
public Task<Stream> AddAsync(Stream pdfStream, List<AttachmentRequestDto> attachments, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Logger.LogWarning("AddAttachments endpoint is not implemented yet in API");
|
Logger.LogWarning("AddAttachments endpoint is not implemented yet in API");
|
||||||
throw new NotImplementedException("API endpoint /api/pdf/attachments/add is not implemented yet");
|
throw new NotImplementedException("API endpoint /api/pdf/attachments/add is not implemented yet");
|
||||||
@@ -81,7 +81,7 @@ public class PdfAttachmentClient(HttpClient httpClient, ILogger<PdfAttachmentCli
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[Obsolete("API endpoint not implemented yet")]
|
[Obsolete("API endpoint not implemented yet")]
|
||||||
public Task<Stream> AddAttachmentsAsync(byte[] pdfBytes, List<AttachmentRequestDto> attachments, CancellationToken cancellationToken = default)
|
public Task<Stream> AddAsync(byte[] pdfBytes, List<AttachmentRequestDto> attachments, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Logger.LogWarning("AddAttachments endpoint is not implemented yet in API");
|
Logger.LogWarning("AddAttachments endpoint is not implemented yet in API");
|
||||||
throw new NotImplementedException("API endpoint /api/pdf/attachments/add is not implemented yet");
|
throw new NotImplementedException("API endpoint /api/pdf/attachments/add is not implemented yet");
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class PdfConversionClient(HttpClient httpClient, ILogger<PdfConversionCli
|
|||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[Obsolete("API endpoint not implemented yet")]
|
[Obsolete("API endpoint not implemented yet")]
|
||||||
public Task<Stream> ConvertToPdfAAsync(Stream pdfStream, string pdfALevel = "PDF/A-3b", CancellationToken cancellationToken = default)
|
public Task<Stream> ToPdfAAsync(Stream pdfStream, string pdfALevel = "PDF/A-3b", CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Logger.LogWarning("ConvertToPdfA endpoint is not implemented yet in API");
|
Logger.LogWarning("ConvertToPdfA endpoint is not implemented yet in API");
|
||||||
throw new NotImplementedException("API endpoint POST /api/pdf/conversion/to-pdfa is not implemented yet");
|
throw new NotImplementedException("API endpoint POST /api/pdf/conversion/to-pdfa is not implemented yet");
|
||||||
@@ -24,7 +24,7 @@ public class PdfConversionClient(HttpClient httpClient, ILogger<PdfConversionCli
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[Obsolete("API endpoint not implemented yet")]
|
[Obsolete("API endpoint not implemented yet")]
|
||||||
public Task<Stream> ConvertToPdfAAsync(byte[] pdfBytes, string pdfALevel = "PDF/A-3b", CancellationToken cancellationToken = default)
|
public Task<Stream> ToPdfAAsync(byte[] pdfBytes, string pdfALevel = "PDF/A-3b", CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Logger.LogWarning("ConvertToPdfA endpoint is not implemented yet in API");
|
Logger.LogWarning("ConvertToPdfA endpoint is not implemented yet in API");
|
||||||
throw new NotImplementedException("API endpoint POST /api/pdf/conversion/to-pdfa is not implemented yet");
|
throw new NotImplementedException("API endpoint POST /api/pdf/conversion/to-pdfa is not implemented yet");
|
||||||
@@ -32,7 +32,7 @@ public class PdfConversionClient(HttpClient httpClient, ILogger<PdfConversionCli
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[Obsolete("API endpoint not implemented yet")]
|
[Obsolete("API endpoint not implemented yet")]
|
||||||
public Task<Stream> ConvertFromPdfAAsync(Stream pdfStream, CancellationToken cancellationToken = default)
|
public Task<Stream> FromPdfAAsync(Stream pdfStream, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Logger.LogWarning("ConvertFromPdfA endpoint is not implemented yet in API");
|
Logger.LogWarning("ConvertFromPdfA endpoint is not implemented yet in API");
|
||||||
throw new NotImplementedException("API endpoint POST /api/pdf/conversion/from-pdfa is not implemented yet");
|
throw new NotImplementedException("API endpoint POST /api/pdf/conversion/from-pdfa is not implemented yet");
|
||||||
@@ -40,7 +40,7 @@ public class PdfConversionClient(HttpClient httpClient, ILogger<PdfConversionCli
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[Obsolete("API endpoint not implemented yet")]
|
[Obsolete("API endpoint not implemented yet")]
|
||||||
public Task<Stream> ConvertFromPdfAAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
public Task<Stream> FromPdfAAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Logger.LogWarning("ConvertFromPdfA endpoint is not implemented yet in API");
|
Logger.LogWarning("ConvertFromPdfA endpoint is not implemented yet in API");
|
||||||
throw new NotImplementedException("API endpoint POST /api/pdf/conversion/from-pdfa is not implemented yet");
|
throw new NotImplementedException("API endpoint POST /api/pdf/conversion/from-pdfa is not implemented yet");
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace DocumentService.Client.Clients;
|
|||||||
public class PdfValidationClient(HttpClient httpClient, ILogger<PdfValidationClient> logger) : BaseDocumentClient(httpClient, logger), IPdfValidationClient
|
public class PdfValidationClient(HttpClient httpClient, ILogger<PdfValidationClient> logger) : BaseDocumentClient(httpClient, logger), IPdfValidationClient
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<PdfValidationResult> ValidatePdfAsync(Stream pdfStream, CancellationToken cancellationToken = default)
|
public async Task<PdfValidationResult> ValidateAsync(Stream pdfStream, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var result = await SendMultipartAsync<PdfValidationResult>(
|
var result = await SendMultipartAsync<PdfValidationResult>(
|
||||||
"/api/pdf/validation/validate",
|
"/api/pdf/validation/validate",
|
||||||
@@ -24,7 +24,7 @@ public class PdfValidationClient(HttpClient httpClient, ILogger<PdfValidationCli
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<PdfValidationResult> ValidatePdfAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
public async Task<PdfValidationResult> ValidateAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var request = new ValidatePdfBase64Request
|
var request = new ValidatePdfBase64Request
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace DocumentService.Client.Clients;
|
|||||||
public class SwissQrCodeClient(HttpClient httpClient, ILogger<SwissQrCodeClient> logger) : BaseDocumentClient(httpClient, logger), ISwissQrCodeClient
|
public class SwissQrCodeClient(HttpClient httpClient, ILogger<SwissQrCodeClient> logger) : BaseDocumentClient(httpClient, logger), ISwissQrCodeClient
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<SwissQrCodeExtractionResult> ExtractSwissQrCodeAsync(Stream pdfStream, bool raw = false, CancellationToken cancellationToken = default)
|
public async Task<SwissQrCodeExtractionResult> ExtractAsync(Stream pdfStream, bool raw = false, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var endpoint = $"/api/pdf/qr-code/extract-swiss?raw={raw}";
|
var endpoint = $"/api/pdf/qr-code/extract-swiss?raw={raw}";
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ public class SwissQrCodeClient(HttpClient httpClient, ILogger<SwissQrCodeClient>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<SwissQrCodeExtractionResult> ExtractSwissQrCodeAsync(byte[] pdfBytes, bool raw = false, CancellationToken cancellationToken = default)
|
public async Task<SwissQrCodeExtractionResult> ExtractAsync(byte[] pdfBytes, bool raw = false, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var endpoint = $"/api/pdf/qr-code/extract-swiss?raw={raw}";
|
var endpoint = $"/api/pdf/qr-code/extract-swiss?raw={raw}";
|
||||||
|
|
||||||
|
|||||||
44
DocumentService.Client/Clients/WorkflowsClient.cs
Normal file
44
DocumentService.Client/Clients/WorkflowsClient.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
using DocumentService.Client.Interfaces;
|
||||||
|
using DocumentService.Client.Models.Results;
|
||||||
|
|
||||||
|
namespace DocumentService.Client.Clients;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Orchestrated multi-step workflows that compose multiple DocumentService clients
|
||||||
|
/// into single, higher-level operations.
|
||||||
|
/// </summary>
|
||||||
|
public class WorkflowsClient(IPdfValidationClient validation, ISwissQrCodeClient swissQrCode) : IWorkflowsClient
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
|
||||||
|
byte[] pdfBytes,
|
||||||
|
bool raw = false,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var validationResult = await validation.ValidateAsync(pdfBytes, cancellationToken);
|
||||||
|
var qrCode = validationResult.IsValid
|
||||||
|
? await swissQrCode.ExtractAsync(pdfBytes, raw, cancellationToken)
|
||||||
|
: null;
|
||||||
|
return new SwissQrCodeResult(qrCode, validationResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
|
||||||
|
Stream pdfStream,
|
||||||
|
bool raw = false,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var validationResult = await validation.ValidateAsync(pdfStream, cancellationToken);
|
||||||
|
|
||||||
|
if (!validationResult.IsValid)
|
||||||
|
return new SwissQrCodeResult(null, validationResult);
|
||||||
|
|
||||||
|
// Stream was consumed by validation – reset if possible, otherwise re-open is caller's responsibility.
|
||||||
|
if (pdfStream.CanSeek)
|
||||||
|
pdfStream.Position = 0;
|
||||||
|
|
||||||
|
var qrCode = await swissQrCode.ExtractAsync(pdfStream, raw, cancellationToken);
|
||||||
|
return new SwissQrCodeResult(qrCode, validationResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ namespace DocumentService.Client.Clients;
|
|||||||
public class ZugferdClient(HttpClient httpClient, ILogger<ZugferdClient> logger) : BaseDocumentClient(httpClient, logger), IZugferdClient
|
public class ZugferdClient(HttpClient httpClient, ILogger<ZugferdClient> logger) : BaseDocumentClient(httpClient, logger), IZugferdClient
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<ZugferdCheckResult> HasZugferdAsync(Stream pdfStream, CancellationToken cancellationToken = default)
|
public async Task<ZugferdCheckResult> CheckAsync(Stream pdfStream, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var result = await SendMultipartAsync<ZugferdCheckResult>(
|
var result = await SendMultipartAsync<ZugferdCheckResult>(
|
||||||
"/api/pdf/zugferd/has-zugferd",
|
"/api/pdf/zugferd/has-zugferd",
|
||||||
@@ -25,7 +25,7 @@ public class ZugferdClient(HttpClient httpClient, ILogger<ZugferdClient> logger)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<ZugferdCheckResult> HasZugferdAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
public async Task<ZugferdCheckResult> CheckAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var request = new HasZugferdRequest { Base64Pdf = ToBase64(pdfBytes) };
|
var request = new HasZugferdRequest { Base64Pdf = ToBase64(pdfBytes) };
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ public class ZugferdClient(HttpClient httpClient, ILogger<ZugferdClient> logger)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<Stream> ExtractZugferdAsync(Stream pdfStream, CancellationToken cancellationToken = default)
|
public async Task<Stream> ExtractAsync(Stream pdfStream, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// asFile=true returns the XML file directly (application/xml stream)
|
// asFile=true returns the XML file directly (application/xml stream)
|
||||||
return await SendMultipartForStreamAsync(
|
return await SendMultipartForStreamAsync(
|
||||||
@@ -49,7 +49,7 @@ public class ZugferdClient(HttpClient httpClient, ILogger<ZugferdClient> logger)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<Stream> ExtractZugferdAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
public async Task<Stream> ExtractAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var request = new ExtractZugferdRequest { Base64Pdf = ToBase64(pdfBytes) };
|
var request = new ExtractZugferdRequest { Base64Pdf = ToBase64(pdfBytes) };
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ public class ZugferdClient(HttpClient httpClient, ILogger<ZugferdClient> logger)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<ZugferdExtractionResult> ExtractZugferdAsResultAsync(Stream pdfStream, CancellationToken cancellationToken = default)
|
public async Task<ZugferdExtractionResult> ExtractAsResultAsync(Stream pdfStream, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// asFile=false returns JSON with metadata + XML content
|
// asFile=false returns JSON with metadata + XML content
|
||||||
var result = await SendMultipartAsync<ZugferdExtractionResult>(
|
var result = await SendMultipartAsync<ZugferdExtractionResult>(
|
||||||
@@ -74,7 +74,7 @@ public class ZugferdClient(HttpClient httpClient, ILogger<ZugferdClient> logger)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<ZugferdExtractionResult> ExtractZugferdAsResultAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
public async Task<ZugferdExtractionResult> ExtractAsResultAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var request = new ExtractZugferdRequest { Base64Pdf = ToBase64(pdfBytes) };
|
var request = new ExtractZugferdRequest { Base64Pdf = ToBase64(pdfBytes) };
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
|
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
|
||||||
@@ -11,9 +11,9 @@
|
|||||||
<PackageIcon>icon.png</PackageIcon>
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
<RepositoryUrl>http://git.dd:3000/AppStd/Rec.git</RepositoryUrl>
|
<RepositoryUrl>http://git.dd:3000/AppStd/Rec.git</RepositoryUrl>
|
||||||
<PackageTags>digital data document service api client</PackageTags>
|
<PackageTags>digital data document service api client</PackageTags>
|
||||||
<Version>1.0.0</Version>
|
<Version>1.2.2</Version>
|
||||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
<AssemblyVersion>1.2.2.0</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.0</FileVersion>
|
<FileVersion>1.2.2.0</FileVersion>
|
||||||
<Description></Description>
|
<Description></Description>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
@@ -44,7 +44,34 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DocumentService.Application\DocumentService.Application.csproj" />
|
<ProjectReference Include="..\DocumentService.Application\DocumentService.Application.csproj">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="..\DocumentService.Domain\DocumentService.Domain.csproj">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Embed Application and Domain DLLs into the NuGet package lib folder -->
|
||||||
|
<Target Name="IncludeReferencedProjectDlls" BeforeTargets="_GetPackageFiles" Condition="'$(TargetFramework)' != ''">
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="..\DocumentService.Application\bin\$(Configuration)\$(TargetFramework)\DocumentService.Application.dll"
|
||||||
|
Pack="true" PackagePath="lib\$(TargetFramework)\" Visible="false" />
|
||||||
|
<None Include="..\DocumentService.Domain\bin\$(Configuration)\$(TargetFramework)\DocumentService.Domain.dll"
|
||||||
|
Pack="true" PackagePath="lib\$(TargetFramework)\" Visible="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<!-- Trigger inner build per-TFM to pick up IncludeReferencedProjectDlls target -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage</TargetsForTfmSpecificBuildOutput>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="CopyProjectReferencesToPackage" DependsOnTargets="ResolveReferences">
|
||||||
|
<ItemGroup>
|
||||||
|
<BuildOutputInPackage Include="@(ReferenceCopyLocalPaths->WithMetadataValue('ReferenceSourceTarget','ProjectReference'))"
|
||||||
|
Condition="'%(Filename)' == 'DocumentService.Application' Or '%(Filename)' == 'DocumentService.Domain'" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddHttpClient<IPdfConversionClient, PdfConversionClient>(ConfigureClient)
|
services.AddHttpClient<IPdfConversionClient, PdfConversionClient>(ConfigureClient)
|
||||||
.ConfigurePrimaryHttpMessageHandler(configureHandler);
|
.ConfigurePrimaryHttpMessageHandler(configureHandler);
|
||||||
|
|
||||||
|
// WorkflowsClient composes existing clients – no HttpClient needed.
|
||||||
|
services.AddScoped<IWorkflowsClient, WorkflowsClient>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public interface IPdfAttachmentClient
|
|||||||
/// <param name="pdfStream">PDF file stream</param>
|
/// <param name="pdfStream">PDF file stream</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Attachment check result with metadata</returns>
|
/// <returns>Attachment check result with metadata</returns>
|
||||||
Task<AttachmentCheckResult> CheckAttachmentsAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
Task<AttachmentCheckResult> CheckAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if PDF contains attachments (Base64).
|
/// Checks if PDF contains attachments (Base64).
|
||||||
@@ -22,7 +22,7 @@ public interface IPdfAttachmentClient
|
|||||||
/// <param name="pdfBytes">PDF file as byte array</param>
|
/// <param name="pdfBytes">PDF file as byte array</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Attachment check result with metadata</returns>
|
/// <returns>Attachment check result with metadata</returns>
|
||||||
Task<AttachmentCheckResult> CheckAttachmentsAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
Task<AttachmentCheckResult> CheckAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts all embedded attachments from a PDF (multipart).
|
/// Extracts all embedded attachments from a PDF (multipart).
|
||||||
@@ -31,7 +31,7 @@ public interface IPdfAttachmentClient
|
|||||||
/// <param name="pdfStream">PDF file stream</param>
|
/// <param name="pdfStream">PDF file stream</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Dictionary of file name ? content stream for each extracted attachment</returns>
|
/// <returns>Dictionary of file name ? content stream for each extracted attachment</returns>
|
||||||
Task<Dictionary<string, Stream>> ExtractAttachmentsAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
Task<Dictionary<string, Stream>> ExtractAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts all embedded attachments from a PDF (Base64).
|
/// Extracts all embedded attachments from a PDF (Base64).
|
||||||
@@ -40,7 +40,7 @@ public interface IPdfAttachmentClient
|
|||||||
/// <param name="pdfBytes">PDF file as byte array</param>
|
/// <param name="pdfBytes">PDF file as byte array</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Dictionary of file name ? content stream for each extracted attachment</returns>
|
/// <returns>Dictionary of file name ? content stream for each extracted attachment</returns>
|
||||||
Task<Dictionary<string, Stream>> ExtractAttachmentsAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
Task<Dictionary<string, Stream>> ExtractAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds attachments to PDF (multipart). ?? Not implemented yet in API.
|
/// Adds attachments to PDF (multipart). ?? Not implemented yet in API.
|
||||||
@@ -50,7 +50,7 @@ public interface IPdfAttachmentClient
|
|||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Modified PDF as stream</returns>
|
/// <returns>Modified PDF as stream</returns>
|
||||||
[Obsolete("API endpoint not implemented yet")]
|
[Obsolete("API endpoint not implemented yet")]
|
||||||
Task<Stream> AddAttachmentsAsync(Stream pdfStream, List<AttachmentRequestDto> attachments, CancellationToken cancellationToken = default);
|
Task<Stream> AddAsync(Stream pdfStream, List<AttachmentRequestDto> attachments, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds attachments to PDF (Base64). ?? Not implemented yet in API.
|
/// Adds attachments to PDF (Base64). ?? Not implemented yet in API.
|
||||||
@@ -60,5 +60,5 @@ public interface IPdfAttachmentClient
|
|||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Modified PDF as stream</returns>
|
/// <returns>Modified PDF as stream</returns>
|
||||||
[Obsolete("API endpoint not implemented yet")]
|
[Obsolete("API endpoint not implemented yet")]
|
||||||
Task<Stream> AddAttachmentsAsync(byte[] pdfBytes, List<AttachmentRequestDto> attachments, CancellationToken cancellationToken = default);
|
Task<Stream> AddAsync(byte[] pdfBytes, List<AttachmentRequestDto> attachments, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ public interface IPdfConversionClient
|
|||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>PDF/A document as stream</returns>
|
/// <returns>PDF/A document as stream</returns>
|
||||||
[Obsolete("API endpoint not implemented yet")]
|
[Obsolete("API endpoint not implemented yet")]
|
||||||
Task<Stream> ConvertToPdfAAsync(Stream pdfStream, string pdfALevel = "PDF/A-3b", CancellationToken cancellationToken = default);
|
Task<Stream> ToPdfAAsync(Stream pdfStream, string pdfALevel = "PDF/A-3b", CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a standard PDF to PDF/A format (Base64).
|
/// Converts a standard PDF to PDF/A format (Base64).
|
||||||
@@ -27,7 +27,7 @@ public interface IPdfConversionClient
|
|||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>PDF/A document as stream</returns>
|
/// <returns>PDF/A document as stream</returns>
|
||||||
[Obsolete("API endpoint not implemented yet")]
|
[Obsolete("API endpoint not implemented yet")]
|
||||||
Task<Stream> ConvertToPdfAAsync(byte[] pdfBytes, string pdfALevel = "PDF/A-3b", CancellationToken cancellationToken = default);
|
Task<Stream> ToPdfAAsync(byte[] pdfBytes, string pdfALevel = "PDF/A-3b", CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a PDF/A document to standard PDF (multipart).
|
/// Converts a PDF/A document to standard PDF (multipart).
|
||||||
@@ -36,7 +36,7 @@ public interface IPdfConversionClient
|
|||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Standard PDF as stream</returns>
|
/// <returns>Standard PDF as stream</returns>
|
||||||
[Obsolete("API endpoint not implemented yet")]
|
[Obsolete("API endpoint not implemented yet")]
|
||||||
Task<Stream> ConvertFromPdfAAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
Task<Stream> FromPdfAAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a PDF/A document to standard PDF (Base64).
|
/// Converts a PDF/A document to standard PDF (Base64).
|
||||||
@@ -45,5 +45,5 @@ public interface IPdfConversionClient
|
|||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Standard PDF as stream</returns>
|
/// <returns>Standard PDF as stream</returns>
|
||||||
[Obsolete("API endpoint not implemented yet")]
|
[Obsolete("API endpoint not implemented yet")]
|
||||||
Task<Stream> ConvertFromPdfAAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
Task<Stream> FromPdfAAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public interface IPdfValidationClient
|
|||||||
/// <param name="pdfStream">PDF file stream</param>
|
/// <param name="pdfStream">PDF file stream</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Validation result with metadata</returns>
|
/// <returns>Validation result with metadata</returns>
|
||||||
Task<PdfValidationResult> ValidatePdfAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
Task<PdfValidationResult> ValidateAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates a PDF from byte array (Base64 JSON).
|
/// Validates a PDF from byte array (Base64 JSON).
|
||||||
@@ -21,7 +21,7 @@ public interface IPdfValidationClient
|
|||||||
/// <param name="pdfBytes">PDF file as byte array</param>
|
/// <param name="pdfBytes">PDF file as byte array</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Validation result with metadata</returns>
|
/// <returns>Validation result with metadata</returns>
|
||||||
Task<PdfValidationResult> ValidatePdfAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
Task<PdfValidationResult> ValidateAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates a PDF/A from stream (multipart upload).
|
/// Validates a PDF/A from stream (multipart upload).
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public interface ISwissQrCodeClient
|
|||||||
/// <param name="raw">If true, returns raw QR text lines instead of parsed Bill object</param>
|
/// <param name="raw">If true, returns raw QR text lines instead of parsed Bill object</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Swiss QR Code extraction result</returns>
|
/// <returns>Swiss QR Code extraction result</returns>
|
||||||
Task<SwissQrCodeExtractionResult> ExtractSwissQrCodeAsync(Stream pdfStream, bool raw = false, CancellationToken cancellationToken = default);
|
Task<SwissQrCodeExtractionResult> ExtractAsync(Stream pdfStream, bool raw = false, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts Swiss QR Code from PDF (Base64).
|
/// Extracts Swiss QR Code from PDF (Base64).
|
||||||
@@ -23,6 +23,6 @@ public interface ISwissQrCodeClient
|
|||||||
/// <param name="raw">If true, returns raw QR text lines instead of parsed Bill object</param>
|
/// <param name="raw">If true, returns raw QR text lines instead of parsed Bill object</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Swiss QR Code extraction result</returns>
|
/// <returns>Swiss QR Code extraction result</returns>
|
||||||
Task<SwissQrCodeExtractionResult> ExtractSwissQrCodeAsync(byte[] pdfBytes, bool raw = false, CancellationToken cancellationToken = default);
|
Task<SwissQrCodeExtractionResult> ExtractAsync(byte[] pdfBytes, bool raw = false, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
71
DocumentService.Client/Interfaces/IWorkflowsClient.cs
Normal file
71
DocumentService.Client/Interfaces/IWorkflowsClient.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
using DocumentService.Client.Models.Results;
|
||||||
|
|
||||||
|
namespace DocumentService.Client.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Orchestrated multi-step workflows that combine multiple DocumentService operations into
|
||||||
|
/// a single, higher-level call. Prefer these over manually chaining individual clients.
|
||||||
|
/// </summary>
|
||||||
|
public interface IWorkflowsClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// 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>
|
||||||
|
Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
|
||||||
|
byte[] pdfBytes,
|
||||||
|
bool raw = false,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="InspectSwissQrCodeAsync(byte[], bool, CancellationToken)"/>
|
||||||
|
/// <param name="pdfStream">PDF file as a stream.</param>
|
||||||
|
/// <param name="raw"><inheritdoc cref="InspectSwissQrCodeAsync(byte[], bool, CancellationToken)" path="/param[@name=''raw'']"/></param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
Task<SwissQrCodeResult> InspectSwissQrCodeAsync(
|
||||||
|
Stream pdfStream,
|
||||||
|
bool raw = false,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ public interface IZugferdClient
|
|||||||
/// <param name="pdfStream">PDF file stream</param>
|
/// <param name="pdfStream">PDF file stream</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>ZUGFeRD check result</returns>
|
/// <returns>ZUGFeRD check result</returns>
|
||||||
Task<ZugferdCheckResult> HasZugferdAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
Task<ZugferdCheckResult> CheckAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks whether a PDF contains a ZUGFeRD XML attachment (Base64).
|
/// Checks whether a PDF contains a ZUGFeRD XML attachment (Base64).
|
||||||
@@ -22,7 +22,7 @@ public interface IZugferdClient
|
|||||||
/// <param name="pdfBytes">PDF file as byte array</param>
|
/// <param name="pdfBytes">PDF file as byte array</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>ZUGFeRD check result</returns>
|
/// <returns>ZUGFeRD check result</returns>
|
||||||
Task<ZugferdCheckResult> HasZugferdAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
Task<ZugferdCheckResult> CheckAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts the ZUGFeRD XML from a PDF and returns the raw XML stream (multipart).
|
/// Extracts the ZUGFeRD XML from a PDF and returns the raw XML stream (multipart).
|
||||||
@@ -30,7 +30,7 @@ public interface IZugferdClient
|
|||||||
/// <param name="pdfStream">PDF file stream</param>
|
/// <param name="pdfStream">PDF file stream</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>ZUGFeRD XML content as stream</returns>
|
/// <returns>ZUGFeRD XML content as stream</returns>
|
||||||
Task<Stream> ExtractZugferdAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
Task<Stream> ExtractAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts the ZUGFeRD XML from a PDF and returns the raw XML stream (Base64).
|
/// Extracts the ZUGFeRD XML from a PDF and returns the raw XML stream (Base64).
|
||||||
@@ -38,7 +38,7 @@ public interface IZugferdClient
|
|||||||
/// <param name="pdfBytes">PDF file as byte array</param>
|
/// <param name="pdfBytes">PDF file as byte array</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>ZUGFeRD XML content as stream</returns>
|
/// <returns>ZUGFeRD XML content as stream</returns>
|
||||||
Task<Stream> ExtractZugferdAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
Task<Stream> ExtractAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts ZUGFeRD metadata and XML content as a structured result (multipart).
|
/// Extracts ZUGFeRD metadata and XML content as a structured result (multipart).
|
||||||
@@ -46,7 +46,7 @@ public interface IZugferdClient
|
|||||||
/// <param name="pdfStream">PDF file stream</param>
|
/// <param name="pdfStream">PDF file stream</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Structured ZUGFeRD extraction result</returns>
|
/// <returns>Structured ZUGFeRD extraction result</returns>
|
||||||
Task<ZugferdExtractionResult> ExtractZugferdAsResultAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
Task<ZugferdExtractionResult> ExtractAsResultAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts ZUGFeRD metadata and XML content as a structured result (Base64).
|
/// Extracts ZUGFeRD metadata and XML content as a structured result (Base64).
|
||||||
@@ -54,5 +54,5 @@ public interface IZugferdClient
|
|||||||
/// <param name="pdfBytes">PDF file as byte array</param>
|
/// <param name="pdfBytes">PDF file as byte array</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Structured ZUGFeRD extraction result</returns>
|
/// <returns>Structured ZUGFeRD extraction result</returns>
|
||||||
Task<ZugferdExtractionResult> ExtractZugferdAsResultAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
Task<ZugferdExtractionResult> ExtractAsResultAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
43
DocumentService.Client/Models/Results/SwissQrCodeResult.cs
Normal file
43
DocumentService.Client/Models/Results/SwissQrCodeResult.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
|
||||||
|
namespace DocumentService.Client.Models.Results;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Combined result of a PDF validation followed by a Swiss QR Code extraction.
|
||||||
|
/// Both operations are always executed; <see cref="QrCode"/> is <c>null</c> when
|
||||||
|
/// the document fails validation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="QrCode">
|
||||||
|
/// Extracted Swiss QR Code data, or <c>null</c> if the PDF was invalid or did not
|
||||||
|
/// contain a recognisable Swiss QR Code.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="Validation">
|
||||||
|
/// PDF validation result. Always populated regardless of whether extraction succeeded.
|
||||||
|
/// Use <see cref="PdfValidationResult.IsValid"/>
|
||||||
|
/// to determine whether the extraction was attempted.
|
||||||
|
/// </param>
|
||||||
|
public record SwissQrCodeResult(
|
||||||
|
SwissQrCodeExtractionResult? QrCode,
|
||||||
|
PdfValidationResult Validation)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Optional convenience method for code readability. Follows the standard
|
||||||
|
/// <c>TryXxx</c> pattern (e.g. <see cref="int.TryParse(string, out int)"/>,
|
||||||
|
/// <see cref="Dictionary{TKey,TValue}.TryGetValue"/>).
|
||||||
|
/// Use it when you prefer an <c>if/else</c> branch over a null-check on <see cref="QrCode"/>;
|
||||||
|
/// it is not required – both approaches are equivalent.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="qrCode">
|
||||||
|
/// When this method returns <c>true</c>, contains the extracted QR Code data.
|
||||||
|
/// When this method returns <c>false</c>, this value is <c>null</c> and must not be used.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// <c>true</c> if the PDF was valid and a Swiss QR Code was successfully extracted;
|
||||||
|
/// <c>false</c> otherwise.
|
||||||
|
/// </returns>
|
||||||
|
public bool TryExtract(out SwissQrCodeExtractionResult qrCode)
|
||||||
|
{
|
||||||
|
qrCode = QrCode ?? null!;
|
||||||
|
return Validation.IsValid && QrCode is not null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,12 +9,13 @@
|
|||||||
- **Separate Clients pro Fachbereich**: `Client.Validation`, `Client.Attachment`, `Client.Operations`, `Client.SwissQrCode`, `Client.Zugferd`, `Client.Conversion`
|
- **Separate Clients pro Fachbereich**: `Client.Validation`, `Client.Attachment`, `Client.Operations`, `Client.SwissQrCode`, `Client.Zugferd`, `Client.Conversion`
|
||||||
- **Dualer Eingabe-Support**: Multipart (Stream) und Base64 (byte[]) für alle Endpunkte
|
- **Dualer Eingabe-Support**: Multipart (Stream) und Base64 (byte[]) für alle Endpunkte
|
||||||
- **Stark typisierte Modelle**: Gemeinsame Request/Response-DTOs mit XML-Dokumentation
|
- **Stark typisierte Modelle**: Gemeinsame Request/Response-DTOs mit XML-Dokumentation
|
||||||
|
- **Workflows**: `Client.Workflows` – kombinierte Operationen mit vorgelagerter Validierung (z. B. Swiss QR Code mit automatischer PDF-Prüfung)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Das Paket wird über einen internen NuGet-Feed bereitgestellt und ist **nicht** auf nuget.org verfügbar.
|
Das Paket wird über einen internen NuGet-Feed bereitgestellt und ist **nicht** auf nuget.org verfügbar.
|
||||||
|
|
||||||
**NuGet-Paket:** `http://172.24.12.35:5000/packages/documentservice.client/1.0.0`
|
**NuGet-Paket:** `http://172.24.12.35:5000/packages/documentservice.client/1.1.0`
|
||||||
**API-Basis-URL:** `http://172.24.12.39:9393/`
|
**API-Basis-URL:** `http://172.24.12.39:9393/`
|
||||||
**Swagger-Dokumentation:** [http://172.24.12.39:9393/swagger/index.html](http://172.24.12.39:9393/swagger/index.html)
|
**Swagger-Dokumentation:** [http://172.24.12.39:9393/swagger/index.html](http://172.24.12.39:9393/swagger/index.html)
|
||||||
|
|
||||||
@@ -30,8 +31,8 @@ Für PowerShell-Skripte ohne zugehöriges .NET-Projekt muss die Assembly manuell
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# NuGet-Paket herunterladen (.nupkg ist ein ZIP-Archiv)
|
# NuGet-Paket herunterladen (.nupkg ist ein ZIP-Archiv)
|
||||||
$nupkgUrl = "http://172.24.12.35:5000/packages/documentservice.client/1.0.0"
|
$nupkgUrl = "http://172.24.12.35:5000/packages/documentservice.client/1.1.0"
|
||||||
$nupkgPath = "$env:TEMP\DocumentService.Client.1.0.0.nupkg"
|
$nupkgPath = "$env:TEMP\DocumentService.Client.1.1.0.nupkg"
|
||||||
$extractDir = "$env:TEMP\DocumentService.Client"
|
$extractDir = "$env:TEMP\DocumentService.Client"
|
||||||
|
|
||||||
Invoke-WebRequest -Uri $nupkgUrl -OutFile $nupkgPath
|
Invoke-WebRequest -Uri $nupkgUrl -OutFile $nupkgPath
|
||||||
@@ -155,6 +156,7 @@ Client.OnReconfigure = OnReconfigure.Ignore
|
|||||||
| `Client.SwissQrCode` | `ISwissQrCodeClient` | Schweizer QR-Code |
|
| `Client.SwissQrCode` | `ISwissQrCodeClient` | Schweizer QR-Code |
|
||||||
| `Client.Zugferd` | `IZugferdClient` | ZUGFeRD-Rechnungen |
|
| `Client.Zugferd` | `IZugferdClient` | ZUGFeRD-Rechnungen |
|
||||||
| `Client.Conversion` | `IPdfConversionClient` | PDF-Konvertierung *(geplant)* |
|
| `Client.Conversion` | `IPdfConversionClient` | PDF-Konvertierung *(geplant)* |
|
||||||
|
| `Client.Workflows` | `IWorkflowsClient` | Kombinierte Operationen |
|
||||||
|
|
||||||
Jeder Zugriff auf eine dieser Eigenschaften erstellt einen neuen DI-Scope. Clients sind **nicht** für die dauerhafte Speicherung als Felder gedacht – jeder Aufruf holt sich eine frische Instanz.
|
Jeder Zugriff auf eine dieser Eigenschaften erstellt einen neuen DI-Scope. Clients sind **nicht** für die dauerhafte Speicherung als Felder gedacht – jeder Aufruf holt sich eine frische Instanz.
|
||||||
|
|
||||||
@@ -179,6 +181,97 @@ 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. 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 – 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;
|
||||||
|
|
||||||
|
byte[] pdfBytes = await File.ReadAllBytesAsync("rechnung.pdf");
|
||||||
|
SwissQrCodeResult result = await Client.Workflows.InspectSwissQrCodeAsync(pdfBytes);
|
||||||
|
|
||||||
|
// 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 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 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
|
||||||
|
|
||||||
|
Dim pdfBytes = System.IO.File.ReadAllBytes("rechnung.pdf")
|
||||||
|
Dim result As SwissQrCodeResult = Await Client.Workflows.InspectSwissQrCodeAsync(pdfBytes)
|
||||||
|
|
||||||
|
Console.WriteLine($"Gültig: {result.Validation.IsValid}")
|
||||||
|
Console.WriteLine($"Seiten: {result.Validation.PageCount}")
|
||||||
|
Console.WriteLine($"Verschlüsselt: {result.Validation.IsEncrypted}")
|
||||||
|
|
||||||
|
' 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"
|
||||||
|
|
||||||
|
$pdfBytes = [System.IO.File]::ReadAllBytes("rechnung.pdf")
|
||||||
|
$result = [DocumentService.Client.Client]::Workflows.InspectSwissQrCodeAsync($pdfBytes).GetAwaiter().GetResult()
|
||||||
|
|
||||||
|
Write-Host "Gueltig: $($result.Validation.IsValid)"
|
||||||
|
Write-Host "Seiten: $($result.Validation.PageCount)"
|
||||||
|
Write-Host "Verschluesselt:$($result.Validation.IsEncrypted)"
|
||||||
|
|
||||||
|
# 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."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **`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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 1. PDF-Validierung
|
### 1. PDF-Validierung
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
@@ -186,7 +279,7 @@ using DocumentService.Client;
|
|||||||
|
|
||||||
// Variante A: Stream (multipart/form-data – empfohlen für große Dateien)
|
// Variante A: Stream (multipart/form-data – empfohlen für große Dateien)
|
||||||
using var stream = File.OpenRead("rechnung.pdf");
|
using var stream = File.OpenRead("rechnung.pdf");
|
||||||
var result = await Client.Validation.ValidatePdfAsync(stream);
|
var result = await Client.Validation.ValidateAsync(stream);
|
||||||
|
|
||||||
Console.WriteLine($"Seiten: {result.PageCount}");
|
Console.WriteLine($"Seiten: {result.PageCount}");
|
||||||
Console.WriteLine($"PDF-Version: {result.PdfVersion}");
|
Console.WriteLine($"PDF-Version: {result.PdfVersion}");
|
||||||
@@ -196,7 +289,7 @@ Console.WriteLine($"Dateigröße: {result.FileSizeMB:F2} MB");
|
|||||||
|
|
||||||
// Variante B: byte[] (Base64 JSON)
|
// Variante B: byte[] (Base64 JSON)
|
||||||
byte[] pdfBytes = await File.ReadAllBytesAsync("rechnung.pdf");
|
byte[] pdfBytes = await File.ReadAllBytesAsync("rechnung.pdf");
|
||||||
var result2 = await Client.Validation.ValidatePdfAsync(pdfBytes);
|
var result2 = await Client.Validation.ValidateAsync(pdfBytes);
|
||||||
```
|
```
|
||||||
|
|
||||||
```vbnet
|
```vbnet
|
||||||
@@ -204,7 +297,7 @@ Imports DocumentService.Client
|
|||||||
|
|
||||||
' Variante A: Stream
|
' Variante A: Stream
|
||||||
Dim stream As New System.IO.FileStream("rechnung.pdf", System.IO.FileMode.Open)
|
Dim stream As New System.IO.FileStream("rechnung.pdf", System.IO.FileMode.Open)
|
||||||
Dim result = Await Client.Validation.ValidatePdfAsync(stream)
|
Dim result = Await Client.Validation.ValidateAsync(stream)
|
||||||
stream.Dispose()
|
stream.Dispose()
|
||||||
|
|
||||||
Console.WriteLine($"Seiten: {result.PageCount}")
|
Console.WriteLine($"Seiten: {result.PageCount}")
|
||||||
@@ -215,7 +308,7 @@ Console.WriteLine($"Dateigröße: {result.FileSizeMB:F2} MB")
|
|||||||
|
|
||||||
' Variante B: byte[]
|
' Variante B: byte[]
|
||||||
Dim pdfBytes = System.IO.File.ReadAllBytes("rechnung.pdf")
|
Dim pdfBytes = System.IO.File.ReadAllBytes("rechnung.pdf")
|
||||||
Dim result2 = Await Client.Validation.ValidatePdfAsync(pdfBytes)
|
Dim result2 = Await Client.Validation.ValidateAsync(pdfBytes)
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -223,7 +316,7 @@ Add-Type -Path "DocumentService.Client.dll"
|
|||||||
|
|
||||||
# Variante A: Stream
|
# Variante A: Stream
|
||||||
$stream = [System.IO.File]::OpenRead("rechnung.pdf")
|
$stream = [System.IO.File]::OpenRead("rechnung.pdf")
|
||||||
$result = [DocumentService.Client.Client]::Validation.ValidatePdfAsync($stream).GetAwaiter().GetResult()
|
$result = [DocumentService.Client.Client]::Validation.ValidateAsync($stream).GetAwaiter().GetResult()
|
||||||
$stream.Dispose()
|
$stream.Dispose()
|
||||||
|
|
||||||
Write-Host "Seiten: $($result.PageCount)"
|
Write-Host "Seiten: $($result.PageCount)"
|
||||||
@@ -234,13 +327,22 @@ Write-Host "Dateigroesse: $($result.FileSizeMB.ToString('F2')) MB"
|
|||||||
|
|
||||||
# Variante B: byte[]
|
# Variante B: byte[]
|
||||||
$pdfBytes = [System.IO.File]::ReadAllBytes("rechnung.pdf")
|
$pdfBytes = [System.IO.File]::ReadAllBytes("rechnung.pdf")
|
||||||
$result2 = [DocumentService.Client.Client]::Validation.ValidatePdfAsync($pdfBytes).GetAwaiter().GetResult()
|
$result2 = [DocumentService.Client.Client]::Validation.ValidateAsync($pdfBytes).GetAwaiter().GetResult()
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### PDF/A-Konformitätsprüfung
|
#### PDF/A-Konformitätsprüfung
|
||||||
|
|
||||||
|
> **⚠️ Hinweis zur Validierungstiefe:** `ValidatePdfAAsync` führt eine **heuristische PDF/A-Erkennung** durch, keine vollständige ISO-19005-Konformitätsprüfung. Die Implementierung erkennt das PDF/A-Konformitätslevel anhand der XMP-Metadaten (`pdfaid:part`, `pdfaid:conformance`) und prüft auf Verschlüsselung. Folgende Anforderungen der Norm werden **nicht** geprüft:
|
||||||
|
>
|
||||||
|
> - Einbettung aller verwendeten Schriftarten
|
||||||
|
> - Gültigkeit und Vollständigkeit der Farbprofile (ICC)
|
||||||
|
> - Fehlen von JavaScript, eingebetteten Multimedia-Inhalten und externen Referenzen
|
||||||
|
> - Strukturelle Barrierefreiheit (Level A)
|
||||||
|
>
|
||||||
|
> Das Ergebnis `IsValid = true` bedeutet daher: **„Keine erkannten Verstöße"**, nicht **„nachgewiesene ISO-19005-Konformität"**. Für eine zertifizierungsfähige Prüfung ist ein dedizierter Validator (z. B. veraPDF) erforderlich.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
using DocumentService.Client;
|
using DocumentService.Client;
|
||||||
|
|
||||||
@@ -324,7 +426,7 @@ if ($result.Warnings.Count -gt 0) {
|
|||||||
using DocumentService.Client;
|
using DocumentService.Client;
|
||||||
|
|
||||||
byte[] pdfBytes = await File.ReadAllBytesAsync("dokument.pdf");
|
byte[] pdfBytes = await File.ReadAllBytesAsync("dokument.pdf");
|
||||||
var check = await Client.Attachment.CheckAttachmentsAsync(pdfBytes);
|
var check = await Client.Attachment.CheckAsync(pdfBytes);
|
||||||
|
|
||||||
Console.WriteLine($"Hat Anhänge: {check.HasAttachments}");
|
Console.WriteLine($"Hat Anhänge: {check.HasAttachments}");
|
||||||
Console.WriteLine($"Anzahl: {check.AttachmentCount}");
|
Console.WriteLine($"Anzahl: {check.AttachmentCount}");
|
||||||
@@ -337,7 +439,7 @@ foreach (var attachment in check.Attachments)
|
|||||||
Imports DocumentService.Client
|
Imports DocumentService.Client
|
||||||
|
|
||||||
Dim pdfBytes = System.IO.File.ReadAllBytes("dokument.pdf")
|
Dim pdfBytes = System.IO.File.ReadAllBytes("dokument.pdf")
|
||||||
Dim check = Await Client.Attachment.CheckAttachmentsAsync(pdfBytes)
|
Dim check = Await Client.Attachment.CheckAsync(pdfBytes)
|
||||||
|
|
||||||
Console.WriteLine($"Hat Anhänge: {check.HasAttachments}")
|
Console.WriteLine($"Hat Anhänge: {check.HasAttachments}")
|
||||||
Console.WriteLine($"Anzahl: {check.AttachmentCount}")
|
Console.WriteLine($"Anzahl: {check.AttachmentCount}")
|
||||||
@@ -351,7 +453,7 @@ Next
|
|||||||
Add-Type -Path "DocumentService.Client.dll"
|
Add-Type -Path "DocumentService.Client.dll"
|
||||||
|
|
||||||
$pdfBytes = [System.IO.File]::ReadAllBytes("dokument.pdf")
|
$pdfBytes = [System.IO.File]::ReadAllBytes("dokument.pdf")
|
||||||
$check = [DocumentService.Client.Client]::Attachment.CheckAttachmentsAsync($pdfBytes).GetAwaiter().GetResult()
|
$check = [DocumentService.Client.Client]::Attachment.CheckAsync($pdfBytes).GetAwaiter().GetResult()
|
||||||
|
|
||||||
Write-Host "Hat Anhaenge: $($check.HasAttachments)"
|
Write-Host "Hat Anhaenge: $($check.HasAttachments)"
|
||||||
Write-Host "Anzahl: $($check.AttachmentCount)"
|
Write-Host "Anzahl: $($check.AttachmentCount)"
|
||||||
@@ -369,7 +471,7 @@ Rückgabe: `Dictionary<string, Stream>` – Schlüssel = ursprünglicher Dateina
|
|||||||
using DocumentService.Client;
|
using DocumentService.Client;
|
||||||
|
|
||||||
using var pdfStream = File.OpenRead("dokument_mit_anhaengen.pdf");
|
using var pdfStream = File.OpenRead("dokument_mit_anhaengen.pdf");
|
||||||
var files = await Client.Attachment.ExtractAttachmentsAsync(pdfStream);
|
var files = await Client.Attachment.ExtractAsync(pdfStream);
|
||||||
|
|
||||||
foreach (var (fileName, fileStream) in files)
|
foreach (var (fileName, fileStream) in files)
|
||||||
{
|
{
|
||||||
@@ -386,7 +488,7 @@ foreach (var (fileName, fileStream) in files)
|
|||||||
Imports DocumentService.Client
|
Imports DocumentService.Client
|
||||||
|
|
||||||
Dim pdfStream As New System.IO.FileStream("dokument_mit_anhaengen.pdf", System.IO.FileMode.Open)
|
Dim pdfStream As New System.IO.FileStream("dokument_mit_anhaengen.pdf", System.IO.FileMode.Open)
|
||||||
Dim files = Await Client.Attachment.ExtractAttachmentsAsync(pdfStream)
|
Dim files = Await Client.Attachment.ExtractAsync(pdfStream)
|
||||||
pdfStream.Dispose()
|
pdfStream.Dispose()
|
||||||
|
|
||||||
For Each entry In files
|
For Each entry In files
|
||||||
@@ -405,7 +507,7 @@ Next
|
|||||||
Add-Type -Path "DocumentService.Client.dll"
|
Add-Type -Path "DocumentService.Client.dll"
|
||||||
|
|
||||||
$pdfStream = [System.IO.File]::OpenRead("dokument_mit_anhaengen.pdf")
|
$pdfStream = [System.IO.File]::OpenRead("dokument_mit_anhaengen.pdf")
|
||||||
$files = [DocumentService.Client.Client]::Attachment.ExtractAttachmentsAsync($pdfStream).GetAwaiter().GetResult()
|
$files = [DocumentService.Client.Client]::Attachment.ExtractAsync($pdfStream).GetAwaiter().GetResult()
|
||||||
$pdfStream.Dispose()
|
$pdfStream.Dispose()
|
||||||
|
|
||||||
New-Item -ItemType Directory -Force -Path "output" | Out-Null
|
New-Item -ItemType Directory -Force -Path "output" | Out-Null
|
||||||
@@ -424,7 +526,7 @@ foreach ($entry in $files.GetEnumerator()) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Hinweis – `AddAttachmentsAsync`:** Das Einbetten von Anhängen in PDF/A-3-Dokumente ist für eine zukünftige Version geplant. Die Methode ist bereits im Interface definiert, jedoch als `[Obsolete]` markiert und wirft zur Laufzeit eine `NotImplementedException`. Der API-Endpunkt `POST /api/pdf/attachments/add` ist noch nicht implementiert.
|
> **Hinweis – `AddAsync`:** Das Einbetten von Anhängen in PDF/A-3-Dokumente ist für eine zukünftige Version geplant. Die Methode ist bereits im Interface definiert, jedoch als `[Obsolete]` markiert und wirft zur Laufzeit eine `NotImplementedException`. Der API-Endpunkt `POST /api/pdf/attachments/add` ist noch nicht implementiert.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -743,7 +845,7 @@ using DocumentService.Client;
|
|||||||
|
|
||||||
// Geparste Rechnungsstruktur gemäß Swiss QR Bill Standard 2.0
|
// Geparste Rechnungsstruktur gemäß Swiss QR Bill Standard 2.0
|
||||||
byte[] pdfBytes = await File.ReadAllBytesAsync("qr-rechnung.pdf");
|
byte[] pdfBytes = await File.ReadAllBytesAsync("qr-rechnung.pdf");
|
||||||
var result = await Client.SwissQrCode.ExtractSwissQrCodeAsync(pdfBytes, raw: false);
|
var result = await Client.SwissQrCode.ExtractAsync(pdfBytes, raw: false);
|
||||||
|
|
||||||
Console.WriteLine($"Betrag: {result.Bill?.Amount} {result.Bill?.Currency}");
|
Console.WriteLine($"Betrag: {result.Bill?.Amount} {result.Bill?.Currency}");
|
||||||
Console.WriteLine($"Konto: {result.Bill?.Account}");
|
Console.WriteLine($"Konto: {result.Bill?.Account}");
|
||||||
@@ -756,7 +858,7 @@ Console.WriteLine($"Mitteilung:{result.Bill?.UnstructuredMessage}");
|
|||||||
Imports DocumentService.Client
|
Imports DocumentService.Client
|
||||||
|
|
||||||
Dim pdfBytes = System.IO.File.ReadAllBytes("qr-rechnung.pdf")
|
Dim pdfBytes = System.IO.File.ReadAllBytes("qr-rechnung.pdf")
|
||||||
Dim result = Await Client.SwissQrCode.ExtractSwissQrCodeAsync(pdfBytes, raw:=False)
|
Dim result = Await Client.SwissQrCode.ExtractAsync(pdfBytes, raw:=False)
|
||||||
|
|
||||||
Console.WriteLine($"Betrag: {result.Bill?.Amount} {result.Bill?.Currency}")
|
Console.WriteLine($"Betrag: {result.Bill?.Amount} {result.Bill?.Currency}")
|
||||||
Console.WriteLine($"Konto: {result.Bill?.Account}")
|
Console.WriteLine($"Konto: {result.Bill?.Account}")
|
||||||
@@ -769,7 +871,7 @@ Console.WriteLine($"Mitteilung:{result.Bill?.UnstructuredMessage}")
|
|||||||
Add-Type -Path "DocumentService.Client.dll"
|
Add-Type -Path "DocumentService.Client.dll"
|
||||||
|
|
||||||
$pdfBytes = [System.IO.File]::ReadAllBytes("qr-rechnung.pdf")
|
$pdfBytes = [System.IO.File]::ReadAllBytes("qr-rechnung.pdf")
|
||||||
$result = [DocumentService.Client.Client]::SwissQrCode.ExtractSwissQrCodeAsync($pdfBytes, $false).GetAwaiter().GetResult()
|
$result = [DocumentService.Client.Client]::SwissQrCode.ExtractAsync($pdfBytes, $false).GetAwaiter().GetResult()
|
||||||
|
|
||||||
Write-Host "Betrag: $($result.Bill.Amount) $($result.Bill.Currency)"
|
Write-Host "Betrag: $($result.Bill.Amount) $($result.Bill.Currency)"
|
||||||
Write-Host "Konto: $($result.Bill.Account)"
|
Write-Host "Konto: $($result.Bill.Account)"
|
||||||
@@ -785,7 +887,7 @@ Write-Host "Mitteilung:$($result.Bill.UnstructuredMessage)"
|
|||||||
```csharp
|
```csharp
|
||||||
// Ohne Parsing – direkt aus dem QR-Code-Inhalt
|
// Ohne Parsing – direkt aus dem QR-Code-Inhalt
|
||||||
using var pdfStream = File.OpenRead("qr-rechnung.pdf");
|
using var pdfStream = File.OpenRead("qr-rechnung.pdf");
|
||||||
var result = await Client.SwissQrCode.ExtractSwissQrCodeAsync(pdfStream, raw: true);
|
var result = await Client.SwissQrCode.ExtractAsync(pdfStream, raw: true);
|
||||||
|
|
||||||
foreach (var line in result.RawLines)
|
foreach (var line in result.RawLines)
|
||||||
Console.WriteLine(line);
|
Console.WriteLine(line);
|
||||||
@@ -793,7 +895,7 @@ foreach (var line in result.RawLines)
|
|||||||
|
|
||||||
```vbnet
|
```vbnet
|
||||||
Dim pdfStream As New System.IO.FileStream("qr-rechnung.pdf", System.IO.FileMode.Open)
|
Dim pdfStream As New System.IO.FileStream("qr-rechnung.pdf", System.IO.FileMode.Open)
|
||||||
Dim result = Await Client.SwissQrCode.ExtractSwissQrCodeAsync(pdfStream, raw:=True)
|
Dim result = Await Client.SwissQrCode.ExtractAsync(pdfStream, raw:=True)
|
||||||
pdfStream.Dispose()
|
pdfStream.Dispose()
|
||||||
|
|
||||||
For Each line In result.RawLines
|
For Each line In result.RawLines
|
||||||
@@ -803,7 +905,7 @@ Next
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$pdfStream = [System.IO.File]::OpenRead("qr-rechnung.pdf")
|
$pdfStream = [System.IO.File]::OpenRead("qr-rechnung.pdf")
|
||||||
$result = [DocumentService.Client.Client]::SwissQrCode.ExtractSwissQrCodeAsync($pdfStream, $true).GetAwaiter().GetResult()
|
$result = [DocumentService.Client.Client]::SwissQrCode.ExtractAsync($pdfStream, $true).GetAwaiter().GetResult()
|
||||||
$pdfStream.Dispose()
|
$pdfStream.Dispose()
|
||||||
|
|
||||||
foreach ($line in $result.RawLines) { Write-Host $line }
|
foreach ($line in $result.RawLines) { Write-Host $line }
|
||||||
@@ -821,7 +923,7 @@ foreach ($line in $result.RawLines) { Write-Host $line }
|
|||||||
using DocumentService.Client;
|
using DocumentService.Client;
|
||||||
|
|
||||||
byte[] pdfBytes = await File.ReadAllBytesAsync("eingangsrechnung.pdf");
|
byte[] pdfBytes = await File.ReadAllBytesAsync("eingangsrechnung.pdf");
|
||||||
var check = await Client.Zugferd.HasZugferdAsync(pdfBytes);
|
var check = await Client.Zugferd.CheckAsync(pdfBytes);
|
||||||
|
|
||||||
Console.WriteLine($"Enthält ZUGFeRD: {check.HasZugferd}");
|
Console.WriteLine($"Enthält ZUGFeRD: {check.HasZugferd}");
|
||||||
|
|
||||||
@@ -837,7 +939,7 @@ if (check.HasZugferd)
|
|||||||
Imports DocumentService.Client
|
Imports DocumentService.Client
|
||||||
|
|
||||||
Dim pdfBytes = System.IO.File.ReadAllBytes("eingangsrechnung.pdf")
|
Dim pdfBytes = System.IO.File.ReadAllBytes("eingangsrechnung.pdf")
|
||||||
Dim check = Await Client.Zugferd.HasZugferdAsync(pdfBytes)
|
Dim check = Await Client.Zugferd.CheckAsync(pdfBytes)
|
||||||
|
|
||||||
Console.WriteLine($"Enthält ZUGFeRD: {check.HasZugferd}")
|
Console.WriteLine($"Enthält ZUGFeRD: {check.HasZugferd}")
|
||||||
|
|
||||||
@@ -852,7 +954,7 @@ End If
|
|||||||
Add-Type -Path "DocumentService.Client.dll"
|
Add-Type -Path "DocumentService.Client.dll"
|
||||||
|
|
||||||
$pdfBytes = [System.IO.File]::ReadAllBytes("eingangsrechnung.pdf")
|
$pdfBytes = [System.IO.File]::ReadAllBytes("eingangsrechnung.pdf")
|
||||||
$check = [DocumentService.Client.Client]::Zugferd.HasZugferdAsync($pdfBytes).GetAwaiter().GetResult()
|
$check = [DocumentService.Client.Client]::Zugferd.CheckAsync($pdfBytes).GetAwaiter().GetResult()
|
||||||
|
|
||||||
Write-Host "Enthaelt ZUGFeRD: $($check.HasZugferd)"
|
Write-Host "Enthaelt ZUGFeRD: $($check.HasZugferd)"
|
||||||
|
|
||||||
@@ -867,7 +969,7 @@ if ($check.HasZugferd) {
|
|||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
using var pdfStream = File.OpenRead("eingangsrechnung.pdf");
|
using var pdfStream = File.OpenRead("eingangsrechnung.pdf");
|
||||||
using var xmlStream = await Client.Zugferd.ExtractZugferdAsync(pdfStream);
|
using var xmlStream = await Client.Zugferd.ExtractAsync(pdfStream);
|
||||||
|
|
||||||
await using var output = File.Create("factur-x.xml");
|
await using var output = File.Create("factur-x.xml");
|
||||||
await xmlStream.CopyToAsync(output);
|
await xmlStream.CopyToAsync(output);
|
||||||
@@ -876,7 +978,7 @@ Console.WriteLine("ZUGFeRD-XML gespeichert.");
|
|||||||
|
|
||||||
```vbnet
|
```vbnet
|
||||||
Dim pdfStream As New System.IO.FileStream("eingangsrechnung.pdf", System.IO.FileMode.Open)
|
Dim pdfStream As New System.IO.FileStream("eingangsrechnung.pdf", System.IO.FileMode.Open)
|
||||||
Dim xmlStream = Await Client.Zugferd.ExtractZugferdAsync(pdfStream)
|
Dim xmlStream = Await Client.Zugferd.ExtractAsync(pdfStream)
|
||||||
pdfStream.Dispose()
|
pdfStream.Dispose()
|
||||||
|
|
||||||
Using output = System.IO.File.Create("factur-x.xml")
|
Using output = System.IO.File.Create("factur-x.xml")
|
||||||
@@ -888,7 +990,7 @@ Console.WriteLine("ZUGFeRD-XML gespeichert.")
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$pdfStream = [System.IO.File]::OpenRead("eingangsrechnung.pdf")
|
$pdfStream = [System.IO.File]::OpenRead("eingangsrechnung.pdf")
|
||||||
$xmlStream = [DocumentService.Client.Client]::Zugferd.ExtractZugferdAsync($pdfStream).GetAwaiter().GetResult()
|
$xmlStream = [DocumentService.Client.Client]::Zugferd.ExtractAsync($pdfStream).GetAwaiter().GetResult()
|
||||||
$pdfStream.Dispose()
|
$pdfStream.Dispose()
|
||||||
|
|
||||||
$output = [System.IO.File]::Create("factur-x.xml")
|
$output = [System.IO.File]::Create("factur-x.xml")
|
||||||
@@ -902,7 +1004,7 @@ Write-Host "ZUGFeRD-XML gespeichert."
|
|||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
byte[] pdfBytes = await File.ReadAllBytesAsync("eingangsrechnung.pdf");
|
byte[] pdfBytes = await File.ReadAllBytesAsync("eingangsrechnung.pdf");
|
||||||
var result = await Client.Zugferd.ExtractZugferdAsResultAsync(pdfBytes);
|
var result = await Client.Zugferd.ExtractAsResultAsync(pdfBytes);
|
||||||
|
|
||||||
Console.WriteLine($"Dateiname: {result.FileName}");
|
Console.WriteLine($"Dateiname: {result.FileName}");
|
||||||
Console.WriteLine($"Größe: {result.FileSize} Bytes");
|
Console.WriteLine($"Größe: {result.FileSize} Bytes");
|
||||||
@@ -914,7 +1016,7 @@ var doc = XDocument.Parse(result.XmlContent);
|
|||||||
|
|
||||||
```vbnet
|
```vbnet
|
||||||
Dim pdfBytes = System.IO.File.ReadAllBytes("eingangsrechnung.pdf")
|
Dim pdfBytes = System.IO.File.ReadAllBytes("eingangsrechnung.pdf")
|
||||||
Dim result = Await Client.Zugferd.ExtractZugferdAsResultAsync(pdfBytes)
|
Dim result = Await Client.Zugferd.ExtractAsResultAsync(pdfBytes)
|
||||||
|
|
||||||
Console.WriteLine($"Dateiname: {result.FileName}")
|
Console.WriteLine($"Dateiname: {result.FileName}")
|
||||||
Console.WriteLine($"Größe: {result.FileSize} Bytes")
|
Console.WriteLine($"Größe: {result.FileSize} Bytes")
|
||||||
@@ -926,7 +1028,7 @@ Dim doc = System.Xml.Linq.XDocument.Parse(result.XmlContent)
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$pdfBytes = [System.IO.File]::ReadAllBytes("eingangsrechnung.pdf")
|
$pdfBytes = [System.IO.File]::ReadAllBytes("eingangsrechnung.pdf")
|
||||||
$result = [DocumentService.Client.Client]::Zugferd.ExtractZugferdAsResultAsync($pdfBytes).GetAwaiter().GetResult()
|
$result = [DocumentService.Client.Client]::Zugferd.ExtractAsResultAsync($pdfBytes).GetAwaiter().GetResult()
|
||||||
|
|
||||||
Write-Host "Dateiname: $($result.FileName)"
|
Write-Host "Dateiname: $($result.FileName)"
|
||||||
Write-Host "Groesse: $($result.FileSize) Bytes"
|
Write-Host "Groesse: $($result.FileSize) Bytes"
|
||||||
@@ -949,20 +1051,20 @@ $doc = [System.Xml.Linq.XDocument]::Parse($result.XmlContent)
|
|||||||
```csharp
|
```csharp
|
||||||
// NICHT VERWENDEN – wirft NotImplementedException
|
// NICHT VERWENDEN – wirft NotImplementedException
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
using var pdfAStream = await Client.Conversion.ConvertToPdfAAsync(pdfStream, pdfALevel: "PDF/A-3b");
|
using var pdfAStream = await Client.Conversion.ToPdfAAsync(pdfStream, pdfALevel: "PDF/A-3b");
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
```
|
```
|
||||||
|
|
||||||
```vbnet
|
```vbnet
|
||||||
' NICHT VERWENDEN – wirft NotImplementedException
|
' NICHT VERWENDEN – wirft NotImplementedException
|
||||||
#Disable Warning BC40000
|
#Disable Warning BC40000
|
||||||
Dim pdfAStream = Await Client.Conversion.ConvertToPdfAAsync(pdfStream, pdfALevel:="PDF/A-3b")
|
Dim pdfAStream = Await Client.Conversion.ToPdfAAsync(pdfStream, pdfALevel:="PDF/A-3b")
|
||||||
#Enable Warning BC40000
|
#Enable Warning BC40000
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# NICHT VERWENDEN – wirft NotImplementedException
|
# NICHT VERWENDEN – wirft NotImplementedException
|
||||||
$pdfAStream = [DocumentService.Client.Client]::Conversion.ConvertToPdfAAsync($pdfStream, "PDF/A-3b").GetAwaiter().GetResult()
|
$pdfAStream = [DocumentService.Client.Client]::Conversion.ToPdfAAsync($pdfStream, "PDF/A-3b").GetAwaiter().GetResult()
|
||||||
```
|
```
|
||||||
|
|
||||||
Sobald die Endpunkte auf dem Server bereitgestellt werden, entfällt das `[Obsolete]`-Attribut und die Methoden sind ohne weiteren Anpassungsbedarf auf Clientseite verwendbar.
|
Sobald die Endpunkte auf dem Server bereitgestellt werden, entfällt das `[Obsolete]`-Attribut und die Methoden sind ohne weiteren Anpassungsbedarf auf Clientseite verwendbar.
|
||||||
@@ -973,20 +1075,21 @@ Sobald die Endpunkte auf dem Server bereitgestellt werden, entfällt das `[Obsol
|
|||||||
|
|
||||||
| **Client-Eigenschaft** | **Methode** | **API-Endpunkt** | **Status** | **Beschreibung** |
|
| **Client-Eigenschaft** | **Methode** | **API-Endpunkt** | **Status** | **Beschreibung** |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `Client.Validation` | `ValidatePdfAsync()` | `POST /api/pdf/validation/validate` | ✅ Verfügbar | PDF validieren, Metadaten abrufen |
|
| `Client.Validation` | `ValidateAsync()` | `POST /api/pdf/validation/validate` | ✅ Verfügbar | PDF validieren, Metadaten abrufen |
|
||||||
| `Client.Validation` | `ValidatePdfAAsync()` | `POST /api/pdf/validation/validate-pdfa` | ✅ Verfügbar | PDF/A-Konformität prüfen |
|
| `Client.Validation` | `ValidatePdfAAsync()` | `POST /api/pdf/validation/validate-pdfa` | ✅ Verfügbar | PDF/A-Erkennung (heuristisch, keine vollständige ISO-19005-Prüfung) |
|
||||||
| `Client.Attachment` | `CheckAttachmentsAsync()` | `POST /api/pdf/attachments/check` | ✅ Verfügbar | Eingebettete Anhänge erkennen |
|
| `Client.Attachment` | `CheckAsync()` | `POST /api/pdf/attachments/check` | ✅ Verfügbar | Eingebettete Anhänge erkennen |
|
||||||
| `Client.Attachment` | `ExtractAttachmentsAsync()` | `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` | `AddAttachmentsAsync()` | `POST /api/pdf/attachments/add` | ⏳ Geplant | Anhänge in PDF/A-3 einbetten |
|
| `Client.Attachment` | `AddAsync()` | `POST /api/pdf/attachments/add` | ⏳ Geplant | Anhänge in PDF/A-3 einbetten |
|
||||||
| `Client.Operations` | `MergeAsync()` | `POST /api/pdf/operations/merge` | ✅ Verfügbar | Mehrere PDFs zusammenführen |
|
| `Client.Operations` | `MergeAsync()` | `POST /api/pdf/operations/merge` | ✅ Verfügbar | Mehrere PDFs zusammenführen |
|
||||||
| `Client.Operations` | `AnnotateAsync()` | `POST /api/pdf/operations/annotate` | ✅ Verfügbar | Annotationen hinzufügen |
|
| `Client.Operations` | `AnnotateAsync()` | `POST /api/pdf/operations/annotate` | ✅ Verfügbar | Annotationen hinzufügen |
|
||||||
| `Client.Operations` | `StampAsync()` | `POST /api/pdf/operations/stamp` | ✅ Verfügbar | Text-/Bildstempel hinzufügen |
|
| `Client.Operations` | `StampAsync()` | `POST /api/pdf/operations/stamp` | ✅ Verfügbar | Text-/Bildstempel hinzufügen |
|
||||||
| `Client.SwissQrCode` | `ExtractSwissQrCodeAsync()` | `POST /api/pdf/qr-code/extract-swiss` | ✅ Verfügbar | Schweizer QR-Code-Daten extrahieren |
|
| `Client.SwissQrCode` | `ExtractAsync()` | `POST /api/pdf/qr-code/extract-swiss` | ✅ Verfügbar | Schweizer QR-Code-Daten extrahieren |
|
||||||
| `Client.Zugferd` | `HasZugferdAsync()` | `POST /api/pdf/zugferd/has-zugferd` | ✅ Verfügbar | ZUGFeRD-Einbettung prüfen |
|
| `Client.Zugferd` | `CheckAsync()` | `POST /api/pdf/zugferd/has-zugferd` | ✅ Verfügbar | ZUGFeRD-Einbettung prüfen |
|
||||||
| `Client.Zugferd` | `ExtractZugferdAsync()` | `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` | `ExtractZugferdAsResultAsync()` | `POST /api/pdf/zugferd/extract` | ✅ Verfügbar | ZUGFeRD-XML mit Metadaten als Objekt |
|
| `Client.Zugferd` | `ExtractAsResultAsync()` | `POST /api/pdf/zugferd/extract` | ✅ Verfügbar | ZUGFeRD-XML mit Metadaten als Objekt |
|
||||||
| `Client.Conversion` | `ConvertToPdfAAsync()` | `POST /api/pdf/conversion/to-pdfa` | ⏳ Geplant | PDF zu PDF/A konvertieren |
|
| `Client.Conversion` | `ToPdfAAsync()` | `POST /api/pdf/conversion/to-pdfa` | ⏳ Geplant | PDF zu PDF/A konvertieren |
|
||||||
| `Client.Conversion` | `ConvertFromPdfAAsync()` | `POST /api/pdf/conversion/from-pdfa` | ⏳ Geplant | PDF/A-Einschränkungen aufheben |
|
| `Client.Conversion` | `FromPdfAAsync()` | `POST /api/pdf/conversion/from-pdfa` | ⏳ Geplant | PDF/A-Einschränkungen aufheben |
|
||||||
|
| `Client.Workflows` | `InspectSwissQrCodeAsync()` | — | ✅ Verfügbar | Validierung + Swiss QR Code Extraktion; gibt immer `SwissQrCodeResult` zurück, wirft nie |
|
||||||
|
|
||||||
> **⏳ Geplant:** Der Client-Code ist vorhanden und kompiliert fehlerfrei. Ein Aufruf zur Laufzeit wirft jedoch `NotImplementedException`, da der serverseitige Endpunkt noch nicht existiert. Die Methoden sind mit `[Obsolete]` markiert, sodass der Compiler eine Build-Warnung ausgibt.
|
> **⏳ Geplant:** Der Client-Code ist vorhanden und kompiliert fehlerfrei. Ein Aufruf zur Laufzeit wirft jedoch `NotImplementedException`, da der serverseitige Endpunkt noch nicht existiert. Die Methoden sind mit `[Obsolete]` markiert, sodass der Compiler eine Build-Warnung ausgibt.
|
||||||
|
|
||||||
@@ -1000,7 +1103,7 @@ using DocumentService.Client;
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var stream = File.OpenRead("dokument.pdf");
|
using var stream = File.OpenRead("dokument.pdf");
|
||||||
var result = await Client.Validation.ValidatePdfAsync(stream);
|
var result = await Client.Validation.ValidateAsync(stream);
|
||||||
Console.WriteLine($"Seiten: {result.PageCount}");
|
Console.WriteLine($"Seiten: {result.PageCount}");
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex) when (ex.Message.Contains("not configured"))
|
catch (InvalidOperationException ex) when (ex.Message.Contains("not configured"))
|
||||||
@@ -1030,7 +1133,7 @@ Imports DocumentService.Client
|
|||||||
|
|
||||||
Try
|
Try
|
||||||
Dim stream As New System.IO.FileStream("dokument.pdf", System.IO.FileMode.Open)
|
Dim stream As New System.IO.FileStream("dokument.pdf", System.IO.FileMode.Open)
|
||||||
Dim result = Await Client.Validation.ValidatePdfAsync(stream)
|
Dim result = Await Client.Validation.ValidateAsync(stream)
|
||||||
stream.Dispose()
|
stream.Dispose()
|
||||||
Console.WriteLine($"Seiten: {result.PageCount}")
|
Console.WriteLine($"Seiten: {result.PageCount}")
|
||||||
Catch ex As InvalidOperationException When ex.Message.Contains("not configured")
|
Catch ex As InvalidOperationException When ex.Message.Contains("not configured")
|
||||||
@@ -1049,7 +1152,7 @@ Add-Type -Path "DocumentService.Client.dll"
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$stream = [System.IO.File]::OpenRead("dokument.pdf")
|
$stream = [System.IO.File]::OpenRead("dokument.pdf")
|
||||||
$result = [DocumentService.Client.Client]::Validation.ValidatePdfAsync($stream).GetAwaiter().GetResult()
|
$result = [DocumentService.Client.Client]::Validation.ValidateAsync($stream).GetAwaiter().GetResult()
|
||||||
$stream.Dispose()
|
$stream.Dispose()
|
||||||
Write-Host "Seiten: $($result.PageCount)"
|
Write-Host "Seiten: $($result.PageCount)"
|
||||||
}
|
}
|
||||||
|
|||||||
235
DocumentService.Tests.Live/API/PdfAttachmentLiveTests.cs
Normal file
235
DocumentService.Tests.Live/API/PdfAttachmentLiveTests.cs
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
using FluentAssertions;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Live tests for PDF attachment endpoints (check + extract).
|
||||||
|
/// Uses PdfAttachmentClient — routes managed by the client.
|
||||||
|
/// Target: http://172.24.12.39:9393/
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Live API")]
|
||||||
|
public class PdfAttachmentLiveTests : LiveApiTestBase
|
||||||
|
{
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// CheckAsync(Stream) → multipart → /api/pdf/attachments/check
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_PdfWithoutAttachments_ReturnsHasAttachmentsFalse()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.HasAttachments.Should().BeFalse();
|
||||||
|
result.AttachmentCount.Should().Be(0);
|
||||||
|
result.Attachments.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_PdfWith6Attachments_ReturnsAllAttachmentsWithMetadata()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeTrue();
|
||||||
|
result.AttachmentCount.Should().Be(6, "PDF has exactly 6 attachments");
|
||||||
|
result.Attachments.Should().HaveCount(6);
|
||||||
|
|
||||||
|
foreach (var attachment in result.Attachments)
|
||||||
|
{
|
||||||
|
attachment.FileName.Should().NotBeNullOrEmpty();
|
||||||
|
attachment.MimeType.Should().NotBeNullOrEmpty();
|
||||||
|
attachment.Size.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_PdfWithSwissQrCode_HasNoAttachments()
|
||||||
|
{
|
||||||
|
// QR code PDF has no embedded file attachments
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeFalse();
|
||||||
|
result.AttachmentCount.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_CorruptedContent_Throws()
|
||||||
|
{
|
||||||
|
byte[] garbage = "This is not a valid PDF content"u8.ToArray();
|
||||||
|
using var stream = new MemoryStream(garbage);
|
||||||
|
|
||||||
|
var act = async () => await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"corrupted PDF should return 500 → client throws");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// CheckAsync(byte[]) → Base64 JSON → /api/pdf/attachments/check
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Bytes_PdfWithoutAttachments_ReturnsHasAttachmentsFalse()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(bytes);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeFalse();
|
||||||
|
result.AttachmentCount.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Bytes_PdfWith6Attachments_ReturnsAllAttachmentsWithMetadata()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(bytes);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeTrue();
|
||||||
|
result.AttachmentCount.Should().Be(6);
|
||||||
|
result.Attachments.Should().HaveCount(6);
|
||||||
|
|
||||||
|
foreach (var attachment in result.Attachments)
|
||||||
|
{
|
||||||
|
attachment.FileName.Should().NotBeNullOrEmpty();
|
||||||
|
attachment.MimeType.Should().NotBeNullOrEmpty();
|
||||||
|
attachment.Size.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await AttachmentClient.CheckAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ExtractAsync(Stream) → multipart → /api/pdf/attachments/extract
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_PdfWith6Attachments_Returns6NamedStreams()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var attachments = await AttachmentClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
attachments.Should().HaveCount(6, "PDF contains 6 attachments");
|
||||||
|
|
||||||
|
foreach (var (name, content) in attachments)
|
||||||
|
{
|
||||||
|
name.Should().NotBeNullOrEmpty();
|
||||||
|
content.Length.Should().BeGreaterThan(0);
|
||||||
|
content.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_AttachmentNamesAreUnique()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var attachments = await AttachmentClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
attachments.Keys.Should().OnlyHaveUniqueItems(
|
||||||
|
"ZIP entry names must be unique within the archive");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
foreach (var s in attachments.Values) s.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await AttachmentClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_CorruptedContent_Throws()
|
||||||
|
{
|
||||||
|
byte[] garbage = "This is not a valid PDF content"u8.ToArray();
|
||||||
|
using var stream = new MemoryStream(garbage);
|
||||||
|
|
||||||
|
var act = async () => await AttachmentClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ExtractAsync(byte[]) → Base64 JSON → /api/pdf/attachments/extract
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_PdfWith6Attachments_Returns6NamedStreams()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var attachments = await AttachmentClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
attachments.Should().HaveCount(6);
|
||||||
|
|
||||||
|
foreach (var (name, content) in attachments)
|
||||||
|
{
|
||||||
|
name.Should().NotBeNullOrEmpty();
|
||||||
|
content.Length.Should().BeGreaterThan(0);
|
||||||
|
content.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_AttachmentContentsAreNonEmpty()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var attachments = await AttachmentClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var (_, content) in attachments)
|
||||||
|
{
|
||||||
|
((MemoryStream)content).ToArray().Should().NotBeEmpty(
|
||||||
|
"every extracted attachment must have content");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
foreach (var s in attachments.Values) s.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await AttachmentClient.ExtractAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
532
DocumentService.Tests.Live/API/PdfOperationsLiveTests.cs
Normal file
532
DocumentService.Tests.Live/API/PdfOperationsLiveTests.cs
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs.Requests;
|
||||||
|
using DocumentService.Domain.Models.ValueObjects;
|
||||||
|
using FluentAssertions;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Live tests for PDF operations: merge, annotate, stamp.
|
||||||
|
/// Uses PdfOperationsClient — routes managed by the client.
|
||||||
|
/// Target: http://172.24.12.39:9393/
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Live API")]
|
||||||
|
public class PdfOperationsLiveTests : LiveApiTestBase
|
||||||
|
{
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// MERGE — MergeAsync(IEnumerable<Stream>) → multipart
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Streams_TwoPdfs_ReturnsMergedPdfStream()
|
||||||
|
{
|
||||||
|
using var pdf1 = LoadPdfStream("valid.pdf");
|
||||||
|
using var pdf2 = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
await using var merged = await OperationsClient.MergeAsync(new[] { pdf1, pdf2 });
|
||||||
|
|
||||||
|
merged.Should().NotBeNull();
|
||||||
|
merged.Length.Should().BeGreaterThan(0);
|
||||||
|
// Merged must start with PDF magic bytes (%PDF = 0x25 0x50 0x44 0x46)
|
||||||
|
var header = new byte[4];
|
||||||
|
merged.ReadExactly(header, 0, 4);
|
||||||
|
header.Should().Equal(new byte[] { 0x25, 0x50, 0x44, 0x46 },
|
||||||
|
"merged result must be a valid PDF");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Streams_ThreePdfs_ReturnsMergedPdfStream()
|
||||||
|
{
|
||||||
|
using var pdf1 = LoadPdfStream("valid.pdf");
|
||||||
|
using var pdf2 = LoadPdfStream("valid.pdf");
|
||||||
|
using var pdf3 = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
await using var merged = await OperationsClient.MergeAsync(new[] { pdf1, pdf2, pdf3 });
|
||||||
|
|
||||||
|
merged.Should().NotBeNull();
|
||||||
|
merged.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Streams_OnlyOnePdf_Throws()
|
||||||
|
{
|
||||||
|
using var pdf = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.MergeAsync(new[] { pdf });
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"at least 2 PDFs are required; single PDF should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Streams_CorruptedPdf_Throws()
|
||||||
|
{
|
||||||
|
byte[] garbage = "NOT A PDF"u8.ToArray();
|
||||||
|
using var corrupted = new MemoryStream(garbage);
|
||||||
|
using var valid = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.MergeAsync(new[] { corrupted, valid });
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"corrupted PDF should return 500");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// MERGE — MergeAsync(IEnumerable<byte[]>) → Base64 JSON
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Bytes_TwoPdfs_ReturnsMergedPdfStream()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
await using var merged = await OperationsClient.MergeAsync(new[] { pdf, pdf });
|
||||||
|
|
||||||
|
merged.Should().NotBeNull();
|
||||||
|
merged.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Bytes_WithPageRanges_ReturnsMergedPdfStream()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
await using var merged = await OperationsClient.MergeAsync(
|
||||||
|
new[] { pdf, pdf },
|
||||||
|
pageRanges: ["1", "1"]);
|
||||||
|
|
||||||
|
merged.Should().NotBeNull();
|
||||||
|
merged.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Bytes_OnlyOnePdf_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.MergeAsync(new[] { pdf });
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Bytes_PageRangeExceedsPageCount_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf"); // 1-page PDF
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.MergeAsync(
|
||||||
|
new[] { pdf, pdf },
|
||||||
|
pageRanges: ["999-1000", null]);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"out-of-bounds page range should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// ANNOTATE — AnnotateAsync(Stream, ...) → multipart
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_TextMarkupHighlight_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty, // not used in Stream overload
|
||||||
|
AnnotationType = AnnotationType.TextMarkup,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 100, Y1 = 100, X2 = 300, Y2 = 120,
|
||||||
|
TextMarkupStyle = TextMarkupStyle.Highlight,
|
||||||
|
Color = "FFFF00",
|
||||||
|
Author = "LiveTest"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_Underline_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.TextMarkup,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 50, Y1 = 700, X2 = 300, Y2 = 720,
|
||||||
|
TextMarkupStyle = TextMarkupStyle.Underline
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_Circle_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.Circle,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 100, Y1 = 200, X2 = 200, Y2 = 300,
|
||||||
|
Color = "00FF00",
|
||||||
|
Content = "Circle annotation"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_Square_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.Square,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 250, Y1 = 250, X2 = 350, Y2 = 350,
|
||||||
|
Color = "0000FF"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_InvalidPageNumber_Throws()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.FreeText,
|
||||||
|
PageNumber = 9999, // exceeds page count
|
||||||
|
X1 = 0, Y1 = 0, X2 = 100, Y2 = 100,
|
||||||
|
Content = "Test"
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"invalid page number should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// ANNOTATE — AnnotateAsync(byte[], ...) → Base64 JSON
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_FreeText_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty, // client fills this in
|
||||||
|
AnnotationType = AnnotationType.FreeText,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 50, Y1 = 50, X2 = 200, Y2 = 100,
|
||||||
|
Content = "Free text from live test",
|
||||||
|
Author = "LiveTestRunner",
|
||||||
|
Color = "FF0000"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_StickyNote_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.StickyNote,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 300, Y1 = 300, X2 = 320, Y2 = 320,
|
||||||
|
Content = "Sticky note from live test",
|
||||||
|
Author = "Reviewer"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_TextMarkupStrikeout_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.TextMarkup,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 60, Y1 = 600, X2 = 250, Y2 = 620,
|
||||||
|
TextMarkupStyle = TextMarkupStyle.Strikeout,
|
||||||
|
Color = "FF6600"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_FreeTextWithoutContent_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.FreeText,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 0, Y1 = 0, X2 = 100, Y2 = 100
|
||||||
|
// Content intentionally missing
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"FreeText without content should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_TextMarkupWithoutStyle_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.TextMarkup,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 0, Y1 = 0, X2 = 100, Y2 = 100
|
||||||
|
// TextMarkupStyle intentionally missing
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"TextMarkup without style should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// STAMP — StampAsync(Stream, ...) → multipart
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Stream_TextStamp_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 100, Y = 400,
|
||||||
|
Text = "CONFIDENTIAL",
|
||||||
|
FontSize = 24,
|
||||||
|
Color = "FF0000",
|
||||||
|
Opacity = 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(stream, request);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Stream_TextStamp_WithRotation_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 300, Y = 400,
|
||||||
|
Text = "DRAFT",
|
||||||
|
Rotation = 45,
|
||||||
|
Opacity = 0.3,
|
||||||
|
Placement = StampPlacement.Background
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Stream_PredefinedStamp_Confidential_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Predefined,
|
||||||
|
X = 100, Y = 400,
|
||||||
|
PredefinedType = PredefinedStampType.Confidential,
|
||||||
|
Opacity = 0.6
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// STAMP — StampAsync(byte[], ...) → Base64 JSON
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_TextStamp_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 100, Y = 400,
|
||||||
|
Text = "APPROVED",
|
||||||
|
FontName = "Arial",
|
||||||
|
FontSize = 20,
|
||||||
|
Color = "008000",
|
||||||
|
Opacity = 0.7,
|
||||||
|
Placement = StampPlacement.Foreground
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_WatermarkBackground_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 150, Y = 350,
|
||||||
|
Text = "WATERMARK",
|
||||||
|
FontSize = 36,
|
||||||
|
Color = "AAAAAA",
|
||||||
|
Opacity = 0.2,
|
||||||
|
Rotation = 45,
|
||||||
|
Placement = StampPlacement.Background
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_PredefinedStamp_Approved_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Predefined,
|
||||||
|
X = 100, Y = 400,
|
||||||
|
PredefinedType = PredefinedStampType.Approved,
|
||||||
|
Opacity = 0.8
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_PredefinedStamp_Draft_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Predefined,
|
||||||
|
X = 200, Y = 500,
|
||||||
|
PredefinedType = PredefinedStampType.Draft,
|
||||||
|
Opacity = 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_TextStampOnSpecificPage_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
PageNumbers = [1],
|
||||||
|
X = 200, Y = 500,
|
||||||
|
Text = "PAGE 1 ONLY",
|
||||||
|
Opacity = 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_TextStampWithoutText_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 100, Y = 400
|
||||||
|
// Text intentionally missing
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"Text stamp without text should return 400");
|
||||||
|
}
|
||||||
|
}
|
||||||
196
DocumentService.Tests.Live/API/PdfValidationLiveTests.cs
Normal file
196
DocumentService.Tests.Live/API/PdfValidationLiveTests.cs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
using FluentAssertions;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Live tests for PDF validation endpoints.
|
||||||
|
/// Uses PdfValidationClient — routes are managed by the client, not the test.
|
||||||
|
/// Target: http://172.24.12.39:9393/
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Live API")]
|
||||||
|
public class PdfValidationLiveTests : LiveApiTestBase
|
||||||
|
{
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ValidateAsync(Stream) → multipart/form-data → /api/pdf/validation/validate
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_ValidPdf_ReturnsMetadata()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
result.FileSizeBytes.Should().BeGreaterThan(0);
|
||||||
|
result.PdfVersion.Should().NotBeNullOrEmpty();
|
||||||
|
result.HasAttachments.Should().BeFalse();
|
||||||
|
result.AttachmentCount.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_PdfWithAttachments_ReturnsHasAttachmentsTrue()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeTrue();
|
||||||
|
result.AttachmentCount.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_PdfWithSwissQrCode_ReturnsValidResult()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
result.FileSizeBytes.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"empty file should be rejected with 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_CorruptedContent_Throws()
|
||||||
|
{
|
||||||
|
// Valid stream shape, but not a PDF
|
||||||
|
byte[] garbage = "THIS IS NOT A PDF FILE"u8.ToArray();
|
||||||
|
using var stream = new MemoryStream(garbage);
|
||||||
|
|
||||||
|
var act = async () => await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"corrupted content should be rejected (500)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ValidateAsync(byte[]) → Base64 JSON → /api/pdf/validation/validate
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Bytes_ValidPdf_ReturnsMetadata()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(bytes);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
result.FileSizeBytes.Should().BeGreaterThan(0);
|
||||||
|
result.PdfVersion.Should().NotBeNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Bytes_PdfWithAttachments_ReturnsHasAttachmentsTrue()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(bytes);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeTrue();
|
||||||
|
result.AttachmentCount.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await ValidationClient.ValidateAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"empty byte array encodes to empty Base64, which should be rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ValidatePdfAAsync(Stream) → multipart → /api/pdf/validation/validate-pdfa
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Stream_ValidPdf_ReturnsComplianceFields()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidatePdfAAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.IsValid.Should().BeTrue();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
result.PdfVersion.Should().NotBeNullOrEmpty();
|
||||||
|
result.FileSize.Should().BeGreaterThan(0);
|
||||||
|
result.Encrypted.Should().BeFalse();
|
||||||
|
// valid.pdf is a regular PDF, not PDF/A
|
||||||
|
result.PdfACompliant.Should().BeFalse();
|
||||||
|
result.PdfAVersion.Should().BeNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Stream_EncryptedPdf_ReturnsEncryptedTrue_NotCompliant()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("encryptedPdf.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidatePdfAAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Encrypted.Should().BeTrue();
|
||||||
|
result.PdfACompliant.Should().BeFalse("encrypted PDFs cannot be PDF/A compliant");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await ValidationClient.ValidatePdfAAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ValidatePdfAAsync(byte[]) → Base64 JSON → /api/pdf/validation/validate-pdfa
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Bytes_ValidPdf_ReturnsComplianceFields()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidatePdfAAsync(bytes);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.IsValid.Should().BeTrue();
|
||||||
|
result.PdfACompliant.Should().BeFalse();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Bytes_EncryptedPdf_ReturnsEncryptedTrue()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("encryptedPdf.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidatePdfAAsync(bytes);
|
||||||
|
|
||||||
|
result.Encrypted.Should().BeTrue();
|
||||||
|
result.PdfACompliant.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await ValidationClient.ValidatePdfAAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
129
DocumentService.Tests.Live/API/SwissQrCodeLiveTests.cs
Normal file
129
DocumentService.Tests.Live/API/SwissQrCodeLiveTests.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
using FluentAssertions;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Live tests for Swiss QR Code extraction endpoint.
|
||||||
|
/// Uses SwissQrCodeClient — routes managed by the client.
|
||||||
|
/// Target: http://172.24.12.39:9393/
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Live API")]
|
||||||
|
public class SwissQrCodeLiveTests : LiveApiTestBase
|
||||||
|
{
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ExtractAsync(Stream) → multipart → /api/pdf/qr-code/extract-swiss
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_PdfWithQrCode_ReturnsBillDetails()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await QrCodeClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Bill.Should().NotBeNull("bill data must be extracted");
|
||||||
|
result.RawLines.Should().NotBeEmpty("raw QR payload lines must be present");
|
||||||
|
|
||||||
|
// Swiss QR Bill Standard 2.0 payload always starts with "SPC"
|
||||||
|
result.RawLines.Should().Contain(line => line == "SPC",
|
||||||
|
"first SPC line is mandated by Swiss QR Bill Standard 2.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_PdfWithQrCode_BillHasMandatoryFields()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await QrCodeClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
var bill = result.Bill;
|
||||||
|
bill.Should().NotBeNull();
|
||||||
|
bill!.Account.Should().NotBeNullOrEmpty("IBAN/QR-IBAN is mandatory per standard");
|
||||||
|
bill.Creditor.Should().NotBeNull("Creditor is mandatory");
|
||||||
|
bill.Creditor!.Name.Should().NotBeNullOrEmpty("Creditor name is mandatory");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_PdfWithoutQrCode_Throws404()
|
||||||
|
{
|
||||||
|
// valid.pdf has no QR code
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () => await QrCodeClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
// Client calls EnsureSuccessStatusCode → HttpRequestException on 404
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"PDF without QR code must return 404");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await QrCodeClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_WithRawFlag_ReturnsRawLines()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
// raw=true → API returns full unprocessed payload lines
|
||||||
|
var result = await QrCodeClient.ExtractAsync(stream, raw: true);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.RawLines.Should().NotBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ExtractAsync(byte[]) → Base64 JSON → /api/pdf/qr-code/extract-swiss
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_PdfWithQrCode_ReturnsBillDetails()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await QrCodeClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Bill.Should().NotBeNull();
|
||||||
|
result.RawLines.Should().NotBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_PdfWithQrCode_BillHasMandatoryFields()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await QrCodeClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
result.Bill!.Account.Should().NotBeNullOrEmpty();
|
||||||
|
result.Bill.Creditor.Should().NotBeNull();
|
||||||
|
result.Bill.Creditor!.Name.Should().NotBeNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_PdfWithoutQrCode_Throws404()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () => await QrCodeClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"PDF without QR code must return 404");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await QrCodeClient.ExtractAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
47
DocumentService.Tests.Live/DocumentService.Tests.Live.csproj
Normal file
47
DocumentService.Tests.Live/DocumentService.Tests.Live.csproj
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="TestData\Pdfs\pdfWithSwissQRCode.pdf" />
|
||||||
|
<None Remove="TestData\Pdfs\valid.pdf" />
|
||||||
|
<None Remove="TestData\Pdfs\pdfWithMoreThanOneAttachment.pdf" />
|
||||||
|
<None Remove="TestData\Pdfs\encryptedPdf.pdf" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="TestData\Pdfs\pdfWithSwissQRCode.pdf" />
|
||||||
|
<EmbeddedResource Include="TestData\Pdfs\valid.pdf" />
|
||||||
|
<EmbeddedResource Include="TestData\Pdfs\pdfWithMoreThanOneAttachment.pdf" />
|
||||||
|
<EmbeddedResource Include="TestData\Pdfs\encryptedPdf.pdf" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DocumentService.Client\DocumentService.Client.csproj" />
|
||||||
|
<ProjectReference Include="..\DocumentService.Application\DocumentService.Application.csproj" />
|
||||||
|
<ProjectReference Include="..\DocumentService.Domain\DocumentService.Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
64
DocumentService.Tests.Live/LiveApiTestBase.cs
Normal file
64
DocumentService.Tests.Live/LiveApiTestBase.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using DocumentService.Client.Clients;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared base for all live API tests.
|
||||||
|
/// Uses the typed DocumentService.Client classes — no hardcoded routes.
|
||||||
|
///
|
||||||
|
/// Live API: http://172.24.12.39:9393/
|
||||||
|
/// Override via LIVE_API_BASE_URL environment variable if needed.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class LiveApiTestBase : IDisposable
|
||||||
|
{
|
||||||
|
public static readonly string BaseUrl =
|
||||||
|
Environment.GetEnvironmentVariable("LIVE_API_BASE_URL")
|
||||||
|
?? "http://172.24.12.39:9393";
|
||||||
|
|
||||||
|
protected readonly HttpClient HttpClient;
|
||||||
|
|
||||||
|
// Typed clients (route-free — routes live inside the client implementations)
|
||||||
|
protected readonly PdfValidationClient ValidationClient;
|
||||||
|
protected readonly PdfAttachmentClient AttachmentClient;
|
||||||
|
protected readonly PdfOperationsClient OperationsClient;
|
||||||
|
protected readonly SwissQrCodeClient QrCodeClient;
|
||||||
|
|
||||||
|
protected LiveApiTestBase()
|
||||||
|
{
|
||||||
|
HttpClient = new HttpClient { BaseAddress = new Uri(BaseUrl) };
|
||||||
|
|
||||||
|
ValidationClient = new PdfValidationClient(HttpClient, NullLogger<PdfValidationClient>.Instance);
|
||||||
|
AttachmentClient = new PdfAttachmentClient(HttpClient, NullLogger<PdfAttachmentClient>.Instance);
|
||||||
|
OperationsClient = new PdfOperationsClient(HttpClient, NullLogger<PdfOperationsClient>.Instance);
|
||||||
|
QrCodeClient = new SwissQrCodeClient(HttpClient, NullLogger<SwissQrCodeClient>.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// Embedded-resource helpers
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
protected static byte[] LoadPdfBytes(string fileName)
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
var resourceName = $"DocumentService.Tests.Live.TestData.Pdfs.{fileName}";
|
||||||
|
|
||||||
|
using var stream = assembly.GetManifestResourceStream(resourceName)
|
||||||
|
?? throw new FileNotFoundException(
|
||||||
|
$"Embedded resource not found: '{resourceName}'. " +
|
||||||
|
$"Available: {string.Join(", ", assembly.GetManifestResourceNames())}");
|
||||||
|
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
stream.CopyTo(ms);
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Stream LoadPdfStream(string fileName)
|
||||||
|
{
|
||||||
|
var bytes = LoadPdfBytes(fileName);
|
||||||
|
return new MemoryStream(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => HttpClient.Dispose();
|
||||||
|
}
|
||||||
30
DocumentService.Tests.Live/TestData/Pdfs/encryptedPdf.pdf
Normal file
30
DocumentService.Tests.Live/TestData/Pdfs/encryptedPdf.pdf
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
%PDF-1.4
|
||||||
|
1 0 obj
|
||||||
|
<< /Type /Catalog /Pages 2 0 R >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
<< /Filter /Standard /V 2 /R 3 /Length 128 /P -3904 /O (OwnerHash12345678901234567890123456) /U (UserHash12345678901234567890123456) >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
xref
|
||||||
|
0 5
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000009 00000 n
|
||||||
|
0000000058 00000 n
|
||||||
|
0000000115 00000 n
|
||||||
|
0000000206 00000 n
|
||||||
|
|
||||||
|
trailer
|
||||||
|
<< /Size 5 /Root 1 0 R /Encrypt 4 0 R >>
|
||||||
|
startxref
|
||||||
|
370
|
||||||
|
%%EOF
|
||||||
Binary file not shown.
BIN
DocumentService.Tests.Live/TestData/Pdfs/pdfWithSwissQRCode.pdf
Normal file
BIN
DocumentService.Tests.Live/TestData/Pdfs/pdfWithSwissQRCode.pdf
Normal file
Binary file not shown.
BIN
DocumentService.Tests.Live/TestData/Pdfs/valid.pdf
Normal file
BIN
DocumentService.Tests.Live/TestData/Pdfs/valid.pdf
Normal file
Binary file not shown.
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="valid.pdf" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DocumentService.Client" Version="1.2.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
36
DocumentService.Tests.NuGet/Program.cs
Normal file
36
DocumentService.Tests.NuGet/Program.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using DocumentService.Client;
|
||||||
|
|
||||||
|
// 1. Configure the client
|
||||||
|
Client.Configure("http://172.24.12.39:9393/");
|
||||||
|
|
||||||
|
// 2. Load embedded PDF resource
|
||||||
|
var assembly = typeof(Program).Assembly;
|
||||||
|
var resourceName = assembly.GetManifestResourceNames()
|
||||||
|
.FirstOrDefault(n => n.EndsWith("valid.pdf", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (resourceName is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ Embedded resource 'valid.pdf' not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = assembly.GetManifestResourceStream(resourceName)!;
|
||||||
|
var result = await Client.Validation.ValidateAsync(stream);
|
||||||
|
|
||||||
|
Console.WriteLine("✅ Connection successful!");
|
||||||
|
Console.WriteLine($" Page count : {result.PageCount}");
|
||||||
|
Console.WriteLine($" PDF version : {result.PdfVersion}");
|
||||||
|
Console.WriteLine($" Encrypted : {result.IsEncrypted}");
|
||||||
|
Console.WriteLine($" Attachments : {result.AttachmentCount}");
|
||||||
|
Console.WriteLine($" File size : {result.FileSizeMB:F2} MB");
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ HTTP error: " + ex.Message);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ Client error: " + ex.Message);
|
||||||
|
}
|
||||||
BIN
DocumentService.Tests.NuGet/valid.pdf
Normal file
BIN
DocumentService.Tests.NuGet/valid.pdf
Normal file
Binary file not shown.
@@ -54,10 +54,10 @@ public class PdfAttachmentClientTests
|
|||||||
return ms.ToArray();
|
return ms.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ?? CheckAttachmentsAsync (Stream) ???????????????????????????????????????
|
// ?? CheckAsync (Stream) ???????????????????????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAttachmentsAsync_Stream_SendsMultipartPost()
|
public async Task CheckAsync_Stream_SendsMultipartPost()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new AttachmentCheckResult
|
var expected = new AttachmentCheckResult
|
||||||
@@ -72,7 +72,7 @@ public class PdfAttachmentClientTests
|
|||||||
var (client, handler) = BuildJson(expected);
|
var (client, handler) = BuildJson(expected);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await client.CheckAttachmentsAsync(new MemoryStream(FakePdfBytes()));
|
var result = await client.CheckAsync(new MemoryStream(FakePdfBytes()));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.HasAttachments.Should().BeTrue();
|
result.HasAttachments.Should().BeTrue();
|
||||||
@@ -83,14 +83,14 @@ public class PdfAttachmentClientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAttachmentsAsync_Stream_PdfWithNoAttachments_ReturnsFalse()
|
public async Task CheckAsync_Stream_PdfWithNoAttachments_ReturnsFalse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new AttachmentCheckResult { HasAttachments = false, AttachmentCount = 0, Attachments = [] };
|
var expected = new AttachmentCheckResult { HasAttachments = false, AttachmentCount = 0, Attachments = [] };
|
||||||
var (client, _) = BuildJson(expected);
|
var (client, _) = BuildJson(expected);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await client.CheckAttachmentsAsync(new MemoryStream(FakePdfBytes()));
|
var result = await client.CheckAsync(new MemoryStream(FakePdfBytes()));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.HasAttachments.Should().BeFalse();
|
result.HasAttachments.Should().BeFalse();
|
||||||
@@ -98,17 +98,17 @@ public class PdfAttachmentClientTests
|
|||||||
result.Attachments.Should().BeEmpty();
|
result.Attachments.Should().BeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ?? CheckAttachmentsAsync (byte[]) ???????????????????????????????????????
|
// ?? CheckAsync (byte[]) ???????????????????????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAttachmentsAsync_Bytes_SendsJsonWithBase64()
|
public async Task CheckAsync_Bytes_SendsJsonWithBase64()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new AttachmentCheckResult { HasAttachments = false };
|
var expected = new AttachmentCheckResult { HasAttachments = false };
|
||||||
var (client, handler) = BuildJson(expected);
|
var (client, handler) = BuildJson(expected);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await client.CheckAttachmentsAsync(FakePdfBytes());
|
await client.CheckAsync(FakePdfBytes());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
handler.LastRequest!.Content.Should().NotBeNull();
|
handler.LastRequest!.Content.Should().NotBeNull();
|
||||||
@@ -118,10 +118,10 @@ public class PdfAttachmentClientTests
|
|||||||
doc.RootElement.GetProperty("base64Pdf").GetString().Should().NotBeNullOrEmpty();
|
doc.RootElement.GetProperty("base64Pdf").GetString().Should().NotBeNullOrEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ?? ExtractAttachmentsAsync (Stream) <20> ZIP unzip ?????????????????????????
|
// ?? ExtractAsync (Stream) <20> ZIP unzip ?????????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractAttachmentsAsync_Stream_UnzipsAndReturnsDictionary()
|
public async Task ExtractAsync_Stream_UnzipsAndReturnsDictionary()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var zipBytes = BuildZip(new Dictionary<string, string>
|
var zipBytes = BuildZip(new Dictionary<string, string>
|
||||||
@@ -132,7 +132,7 @@ public class PdfAttachmentClientTests
|
|||||||
var (client, handler) = BuildBytes(zipBytes);
|
var (client, handler) = BuildBytes(zipBytes);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await client.ExtractAttachmentsAsync(new MemoryStream(FakePdfBytes()));
|
var result = await client.ExtractAsync(new MemoryStream(FakePdfBytes()));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.Should().HaveCount(2);
|
result.Should().HaveCount(2);
|
||||||
@@ -150,7 +150,7 @@ public class PdfAttachmentClientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractAttachmentsAsync_Stream_WithSingleEntry_ReturnsOneItem()
|
public async Task ExtractAsync_Stream_WithSingleEntry_ReturnsOneItem()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var zipBytes = BuildZip(new Dictionary<string, string>
|
var zipBytes = BuildZip(new Dictionary<string, string>
|
||||||
@@ -160,7 +160,7 @@ public class PdfAttachmentClientTests
|
|||||||
var (client, _) = BuildBytes(zipBytes);
|
var (client, _) = BuildBytes(zipBytes);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await client.ExtractAttachmentsAsync(new MemoryStream(FakePdfBytes()));
|
var result = await client.ExtractAsync(new MemoryStream(FakePdfBytes()));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.Should().HaveCount(1);
|
result.Should().HaveCount(1);
|
||||||
@@ -169,10 +169,10 @@ public class PdfAttachmentClientTests
|
|||||||
foreach (var s in result.Values) s.Dispose();
|
foreach (var s in result.Values) s.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ?? ExtractAttachmentsAsync (byte[]) <20> ZIP unzip ?????????????????????????
|
// ?? ExtractAsync (byte[]) <20> ZIP unzip ?????????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractAttachmentsAsync_Bytes_SendsJsonAndUnzips()
|
public async Task ExtractAsync_Bytes_SendsJsonAndUnzips()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var zipBytes = BuildZip(new Dictionary<string, string>
|
var zipBytes = BuildZip(new Dictionary<string, string>
|
||||||
@@ -182,7 +182,7 @@ public class PdfAttachmentClientTests
|
|||||||
var (client, handler) = BuildBytes(zipBytes);
|
var (client, handler) = BuildBytes(zipBytes);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await client.ExtractAttachmentsAsync(FakePdfBytes());
|
var result = await client.ExtractAsync(FakePdfBytes());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.Should().ContainKey("invoice.xml");
|
result.Should().ContainKey("invoice.xml");
|
||||||
@@ -195,7 +195,7 @@ public class PdfAttachmentClientTests
|
|||||||
// ?? HTTP error propagation ???????????????????????????????????????????????
|
// ?? HTTP error propagation ???????????????????????????????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAttachmentsAsync_WhenApiReturns404_ThrowsHttpRequestException()
|
public async Task CheckAsync_WhenApiReturns404_ThrowsHttpRequestException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.NotFound);
|
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.NotFound);
|
||||||
@@ -203,12 +203,12 @@ public class PdfAttachmentClientTests
|
|||||||
var client = new PdfAttachmentClient(httpClient, NullLogger<PdfAttachmentClient>.Instance);
|
var client = new PdfAttachmentClient(httpClient, NullLogger<PdfAttachmentClient>.Instance);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await client.Invoking(c => c.CheckAttachmentsAsync(new MemoryStream(FakePdfBytes())))
|
await client.Invoking(c => c.CheckAsync(new MemoryStream(FakePdfBytes())))
|
||||||
.Should().ThrowAsync<HttpRequestException>();
|
.Should().ThrowAsync<HttpRequestException>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractAttachmentsAsync_WhenApiReturns500_ThrowsHttpRequestException()
|
public async Task ExtractAsync_WhenApiReturns500_ThrowsHttpRequestException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.InternalServerError);
|
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.InternalServerError);
|
||||||
@@ -216,7 +216,7 @@ public class PdfAttachmentClientTests
|
|||||||
var client = new PdfAttachmentClient(httpClient, NullLogger<PdfAttachmentClient>.Instance);
|
var client = new PdfAttachmentClient(httpClient, NullLogger<PdfAttachmentClient>.Instance);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await client.Invoking(c => c.ExtractAttachmentsAsync(new MemoryStream(FakePdfBytes())))
|
await client.Invoking(c => c.ExtractAsync(new MemoryStream(FakePdfBytes())))
|
||||||
.Should().ThrowAsync<HttpRequestException>();
|
.Should().ThrowAsync<HttpRequestException>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,15 +28,15 @@ public class PdfValidationClientTests
|
|||||||
|
|
||||||
private static byte[] FakePdfBytes() => "fake-pdf"u8.ToArray();
|
private static byte[] FakePdfBytes() => "fake-pdf"u8.ToArray();
|
||||||
|
|
||||||
// ── ValidatePdfAsync (Stream) ────────────────────────────────────────────
|
// ── ValidateAsync (Stream) ────────────────────────────────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidatePdfAsync_Stream_SendsMultipartPost()
|
public async Task ValidateAsync_Stream_SendsMultipartPost()
|
||||||
{
|
{
|
||||||
var expected = new PdfValidationResult(PageCount: 3, FileSizeBytes: 2048, FileSizeMB: 0, PdfVersion: "1.7", HasAttachments: false, AttachmentCount: 0, IsEncrypted: false);
|
var expected = new PdfValidationResult(PageCount: 3, FileSizeBytes: 2048, FileSizeMB: 0, PdfVersion: "1.7", HasAttachments: false, AttachmentCount: 0, IsEncrypted: false);
|
||||||
var (client, handler) = Build(expected);
|
var (client, handler) = Build(expected);
|
||||||
|
|
||||||
var result = await client.ValidatePdfAsync(new MemoryStream(FakePdfBytes()));
|
var result = await client.ValidateAsync(new MemoryStream(FakePdfBytes()));
|
||||||
|
|
||||||
result.PageCount.Should().Be(3);
|
result.PageCount.Should().Be(3);
|
||||||
result.PdfVersion.Should().Be("1.7");
|
result.PdfVersion.Should().Be("1.7");
|
||||||
@@ -48,52 +48,52 @@ public class PdfValidationClientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidatePdfAsync_Stream_EncryptedPdf_DeserializesIsEncryptedTrue()
|
public async Task ValidateAsync_Stream_EncryptedPdf_DeserializesIsEncryptedTrue()
|
||||||
{
|
{
|
||||||
var expected = new PdfValidationResult(PageCount: 1, FileSizeBytes: 0, FileSizeMB: 0, PdfVersion: "1.6", HasAttachments: false, AttachmentCount: 0, IsEncrypted: true);
|
var expected = new PdfValidationResult(PageCount: 1, FileSizeBytes: 0, FileSizeMB: 0, PdfVersion: "1.6", HasAttachments: false, AttachmentCount: 0, IsEncrypted: true);
|
||||||
var (client, _) = Build(expected);
|
var (client, _) = Build(expected);
|
||||||
|
|
||||||
var result = await client.ValidatePdfAsync(new MemoryStream(FakePdfBytes()));
|
var result = await client.ValidateAsync(new MemoryStream(FakePdfBytes()));
|
||||||
|
|
||||||
result.IsEncrypted.Should().BeTrue("encrypted PDFs should surface IsEncrypted=true");
|
result.IsEncrypted.Should().BeTrue("encrypted PDFs should surface IsEncrypted=true");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidatePdfAsync_Stream_ThrowsWhenApiReturnsNull()
|
public async Task ValidateAsync_Stream_ThrowsWhenApiReturnsNull()
|
||||||
{
|
{
|
||||||
var handler = MockHttpMessageHandler.ReturningJson<PdfValidationResult?>(null);
|
var handler = MockHttpMessageHandler.ReturningJson<PdfValidationResult?>(null);
|
||||||
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") };
|
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") };
|
||||||
var client = new PdfValidationClient(httpClient, NullLogger<PdfValidationClient>.Instance);
|
var client = new PdfValidationClient(httpClient, NullLogger<PdfValidationClient>.Instance);
|
||||||
|
|
||||||
await client.Invoking(c => c.ValidatePdfAsync(new MemoryStream(FakePdfBytes())))
|
await client.Invoking(c => c.ValidateAsync(new MemoryStream(FakePdfBytes())))
|
||||||
.Should().ThrowAsync<InvalidOperationException>();
|
.Should().ThrowAsync<InvalidOperationException>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidatePdfAsync_Stream_WhenApiReturns400_ThrowsHttpRequestException()
|
public async Task ValidateAsync_Stream_WhenApiReturns400_ThrowsHttpRequestException()
|
||||||
{
|
{
|
||||||
var (client, _) = BuildError(HttpStatusCode.BadRequest);
|
var (client, _) = BuildError(HttpStatusCode.BadRequest);
|
||||||
await client.Invoking(c => c.ValidatePdfAsync(new MemoryStream(FakePdfBytes())))
|
await client.Invoking(c => c.ValidateAsync(new MemoryStream(FakePdfBytes())))
|
||||||
.Should().ThrowAsync<HttpRequestException>();
|
.Should().ThrowAsync<HttpRequestException>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidatePdfAsync_Stream_WhenApiReturns500_ThrowsHttpRequestException()
|
public async Task ValidateAsync_Stream_WhenApiReturns500_ThrowsHttpRequestException()
|
||||||
{
|
{
|
||||||
var (client, _) = BuildError(HttpStatusCode.InternalServerError);
|
var (client, _) = BuildError(HttpStatusCode.InternalServerError);
|
||||||
await client.Invoking(c => c.ValidatePdfAsync(new MemoryStream(FakePdfBytes())))
|
await client.Invoking(c => c.ValidateAsync(new MemoryStream(FakePdfBytes())))
|
||||||
.Should().ThrowAsync<HttpRequestException>();
|
.Should().ThrowAsync<HttpRequestException>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ValidatePdfAsync (byte[]) ────────────────────────────────────────────
|
// ── ValidateAsync (byte[]) ────────────────────────────────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidatePdfAsync_Bytes_SendsJsonWithBase64()
|
public async Task ValidateAsync_Bytes_SendsJsonWithBase64()
|
||||||
{
|
{
|
||||||
var expected = new PdfValidationResult(PageCount: 1, FileSizeBytes: 0, FileSizeMB: 0, PdfVersion: "", HasAttachments: true, AttachmentCount: 2, IsEncrypted: false);
|
var expected = new PdfValidationResult(PageCount: 1, FileSizeBytes: 0, FileSizeMB: 0, PdfVersion: "", HasAttachments: true, AttachmentCount: 2, IsEncrypted: false);
|
||||||
var (client, handler) = Build(expected);
|
var (client, handler) = Build(expected);
|
||||||
|
|
||||||
var result = await client.ValidatePdfAsync(FakePdfBytes());
|
var result = await client.ValidateAsync(FakePdfBytes());
|
||||||
|
|
||||||
result.PageCount.Should().Be(1);
|
result.PageCount.Should().Be(1);
|
||||||
result.HasAttachments.Should().BeTrue();
|
result.HasAttachments.Should().BeTrue();
|
||||||
@@ -105,10 +105,10 @@ public class PdfValidationClientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidatePdfAsync_Bytes_WhenApiReturns400_ThrowsHttpRequestException()
|
public async Task ValidateAsync_Bytes_WhenApiReturns400_ThrowsHttpRequestException()
|
||||||
{
|
{
|
||||||
var (client, _) = BuildError(HttpStatusCode.BadRequest);
|
var (client, _) = BuildError(HttpStatusCode.BadRequest);
|
||||||
await client.Invoking(c => c.ValidatePdfAsync(FakePdfBytes()))
|
await client.Invoking(c => c.ValidateAsync(FakePdfBytes()))
|
||||||
.Should().ThrowAsync<HttpRequestException>();
|
.Should().ThrowAsync<HttpRequestException>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace DocumentService.Tests.Unit.Client;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unit tests for <see cref="SwissQrCodeClient"/>.
|
/// Unit tests for <see cref="SwissQrCodeClient"/>.
|
||||||
/// All tests use a fake <see cref="MockHttpMessageHandler"/> — no real HTTP calls are made.
|
/// All tests use a fake <see cref="MockHttpMessageHandler"/> <EFBFBD> no real HTTP calls are made.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SwissQrCodeClientTests
|
public class SwissQrCodeClientTests
|
||||||
{
|
{
|
||||||
@@ -27,10 +27,10 @@ public class SwissQrCodeClientTests
|
|||||||
|
|
||||||
private static byte[] FakePdfBytes() => "fake-pdf-content"u8.ToArray();
|
private static byte[] FakePdfBytes() => "fake-pdf-content"u8.ToArray();
|
||||||
|
|
||||||
// ?? ExtractSwissQrCodeAsync (Stream) — parsed Bill ??????????????????????
|
// ?? ExtractAsync (Stream) <EFBFBD> parsed Bill ??????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractSwissQrCodeAsync_Stream_ParsedMode_SendsMultipartToCorrectEndpoint()
|
public async Task ExtractAsync_Stream_ParsedMode_SendsMultipartToCorrectEndpoint()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new SwissQrCodeExtractionResult(
|
var expected = new SwissQrCodeExtractionResult(
|
||||||
@@ -47,7 +47,7 @@ public class SwissQrCodeClientTests
|
|||||||
var (client, handler) = Build(expected);
|
var (client, handler) = Build(expected);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await client.ExtractSwissQrCodeAsync(new MemoryStream(FakePdfBytes()), raw: false);
|
var result = await client.ExtractAsync(new MemoryStream(FakePdfBytes()), raw: false);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.Should().NotBeNull();
|
result.Should().NotBeNull();
|
||||||
@@ -56,31 +56,31 @@ public class SwissQrCodeClientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractSwissQrCodeAsync_Stream_RawMode_SendsRawFlagInUrl()
|
public async Task ExtractAsync_Stream_RawMode_SendsRawFlagInUrl()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new SwissQrCodeExtractionResult(Bill: null, RawLines: ["SPC", "0200", "1"]);
|
var expected = new SwissQrCodeExtractionResult(Bill: null, RawLines: ["SPC", "0200", "1"]);
|
||||||
var (client, handler) = Build(expected);
|
var (client, handler) = Build(expected);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await client.ExtractSwissQrCodeAsync(new MemoryStream(FakePdfBytes()), raw: true);
|
var result = await client.ExtractAsync(new MemoryStream(FakePdfBytes()), raw: true);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.RawLines.Should().HaveCount(3).And.StartWith("SPC");
|
result.RawLines.Should().HaveCount(3).And.StartWith("SPC");
|
||||||
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/qr-code/extract-swiss?raw=True");
|
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/qr-code/extract-swiss?raw=True");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ?? ExtractSwissQrCodeAsync (byte[]) ????????????????????????????????????
|
// ?? ExtractAsync (byte[]) ????????????????????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractSwissQrCodeAsync_Bytes_SendsJsonWithBase64()
|
public async Task ExtractAsync_Bytes_SendsJsonWithBase64()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new SwissQrCodeExtractionResult(Bill: null, RawLines: []);
|
var expected = new SwissQrCodeExtractionResult(Bill: null, RawLines: []);
|
||||||
var (client, handler) = Build(expected);
|
var (client, handler) = Build(expected);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await client.ExtractSwissQrCodeAsync(FakePdfBytes(), raw: false);
|
await client.ExtractAsync(FakePdfBytes(), raw: false);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
handler.LastRequest!.Content.Should().NotBeNull();
|
handler.LastRequest!.Content.Should().NotBeNull();
|
||||||
@@ -91,14 +91,14 @@ public class SwissQrCodeClientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractSwissQrCodeAsync_Bytes_RawMode_IncludesRawFlagInUrl()
|
public async Task ExtractAsync_Bytes_RawMode_IncludesRawFlagInUrl()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new SwissQrCodeExtractionResult(Bill: null, RawLines: ["SPC"]);
|
var expected = new SwissQrCodeExtractionResult(Bill: null, RawLines: ["SPC"]);
|
||||||
var (client, handler) = Build(expected);
|
var (client, handler) = Build(expected);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await client.ExtractSwissQrCodeAsync(FakePdfBytes(), raw: true);
|
await client.ExtractAsync(FakePdfBytes(), raw: true);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/qr-code/extract-swiss?raw=True");
|
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/qr-code/extract-swiss?raw=True");
|
||||||
@@ -107,7 +107,7 @@ public class SwissQrCodeClientTests
|
|||||||
// ?? HTTP error propagation ??????????????????????????????????????????????
|
// ?? HTTP error propagation ??????????????????????????????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractSwissQrCodeAsync_WhenApiReturns404_ThrowsHttpRequestException()
|
public async Task ExtractAsync_WhenApiReturns404_ThrowsHttpRequestException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.NotFound);
|
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.NotFound);
|
||||||
@@ -115,12 +115,12 @@ public class SwissQrCodeClientTests
|
|||||||
var client = new SwissQrCodeClient(httpClient, NullLogger<SwissQrCodeClient>.Instance);
|
var client = new SwissQrCodeClient(httpClient, NullLogger<SwissQrCodeClient>.Instance);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await client.Invoking(c => c.ExtractSwissQrCodeAsync(new MemoryStream(FakePdfBytes())))
|
await client.Invoking(c => c.ExtractAsync(new MemoryStream(FakePdfBytes())))
|
||||||
.Should().ThrowAsync<HttpRequestException>();
|
.Should().ThrowAsync<HttpRequestException>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractSwissQrCodeAsync_WhenApiReturns500_ThrowsHttpRequestException()
|
public async Task ExtractAsync_WhenApiReturns500_ThrowsHttpRequestException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.InternalServerError);
|
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.InternalServerError);
|
||||||
@@ -128,7 +128,7 @@ public class SwissQrCodeClientTests
|
|||||||
var client = new SwissQrCodeClient(httpClient, NullLogger<SwissQrCodeClient>.Instance);
|
var client = new SwissQrCodeClient(httpClient, NullLogger<SwissQrCodeClient>.Instance);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await client.Invoking(c => c.ExtractSwissQrCodeAsync(FakePdfBytes()))
|
await client.Invoking(c => c.ExtractAsync(FakePdfBytes()))
|
||||||
.Should().ThrowAsync<HttpRequestException>();
|
.Should().ThrowAsync<HttpRequestException>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
257
DocumentService.Tests/Unit/Client/WorkflowsClientTests.cs
Normal file
257
DocumentService.Tests/Unit/Client/WorkflowsClientTests.cs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
using DocumentService.Client.Clients;
|
||||||
|
using DocumentService.Client.Interfaces;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Unit.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unit tests for <see cref="WorkflowsClient"/>.
|
||||||
|
/// Both <see cref="IPdfValidationClient"/> and <see cref="ISwissQrCodeClient"/> are mocked –
|
||||||
|
/// no real HTTP calls are made. Tests cover every overload, every branch, and all edge cases.
|
||||||
|
/// </summary>
|
||||||
|
public class WorkflowsClientTests
|
||||||
|
{
|
||||||
|
// ─── test doubles ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
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 ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static byte[] ValidPdfBytes() => LoadTestPdf("valid.pdf");
|
||||||
|
private static byte[] EncryptedPdfBytes() => LoadTestPdf("encryptedPdf.pdf");
|
||||||
|
private static byte[] QrPdfBytes() => LoadTestPdf("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
private static byte[] LoadTestPdf(string filename)
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
var resourceName = $"DocumentService.Tests.TestData.Pdfs.{filename}";
|
||||||
|
using var stream = assembly.GetManifestResourceStream(resourceName)
|
||||||
|
?? throw new FileNotFoundException(
|
||||||
|
$"Embedded resource '{resourceName}' not found. " +
|
||||||
|
$"Available: {string.Join(", ", assembly.GetManifestResourceNames())}");
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
stream.CopyTo(ms);
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── valid PdfValidationResult helpers ───────────────────────────────────
|
||||||
|
|
||||||
|
private static PdfValidationResult ValidResult() =>
|
||||||
|
new(PageCount: 1, FileSizeBytes: 1024, FileSizeMB: 0.001,
|
||||||
|
PdfVersion: "1.4", HasAttachments: false, AttachmentCount: 0,
|
||||||
|
IsEncrypted: false);
|
||||||
|
|
||||||
|
private static PdfValidationResult InvalidResult_Encrypted() =>
|
||||||
|
new(PageCount: 1, FileSizeBytes: 1024, FileSizeMB: 0.001,
|
||||||
|
PdfVersion: "1.4", HasAttachments: false, AttachmentCount: 0,
|
||||||
|
IsEncrypted: true);
|
||||||
|
|
||||||
|
private static PdfValidationResult InvalidResult_EmptyFile() =>
|
||||||
|
new(PageCount: 0, FileSizeBytes: 0, FileSizeMB: 0,
|
||||||
|
PdfVersion: "", HasAttachments: false, AttachmentCount: 0,
|
||||||
|
IsEncrypted: false);
|
||||||
|
|
||||||
|
private static SwissQrCodeExtractionResult QrResult() =>
|
||||||
|
new(
|
||||||
|
Bill: new SwissQrBillDto
|
||||||
|
{
|
||||||
|
Version = "V2_0",
|
||||||
|
Currency = "CHF",
|
||||||
|
Amount = 630.20m,
|
||||||
|
Account = "CH9300762011623852957",
|
||||||
|
Creditor = new AddressDto { Name = "ALMAT AG", Town = "Tagelswangen", CountryCode = "CH", Type = "S", PostalCode = "8317" },
|
||||||
|
ReferenceType = "QRR",
|
||||||
|
Reference = "000000000000000000252080824"
|
||||||
|
},
|
||||||
|
RawLines: ["SPC", "0200", "1"]
|
||||||
|
);
|
||||||
|
|
||||||
|
private static SwissQrCodeExtractionResult RawQrResult() =>
|
||||||
|
new(Bill: (SwissQrBillDto?)null, RawLines: ["SPC", "0200", "1", "CH9300762011623852957"]);
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Inspect_Bytes_ValidPdf_ReturnsBothValidationAndQrCode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var pdfBytes = ValidPdfBytes();
|
||||||
|
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(ValidResult());
|
||||||
|
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(QrResult());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
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 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();
|
||||||
|
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(ValidResult());
|
||||||
|
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, true, It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(RawQrResult());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes, raw: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.QrCode!.RawLines.Should().Contain("SPC");
|
||||||
|
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, true, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Inspect_Bytes_CancellationToken_ForwardedToBothClients()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var pdfBytes = ValidPdfBytes();
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
var token = cts.Token;
|
||||||
|
_validation.Setup(v => v.ValidateAsync(pdfBytes, token)).ReturnsAsync(ValidResult());
|
||||||
|
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, token)).ReturnsAsync(QrResult());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Inspect_Stream_ValidPdf_ReturnsBothResults()
|
||||||
|
{
|
||||||
|
// 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>()))
|
||||||
|
.ReturnsAsync(QrResult());
|
||||||
|
|
||||||
|
using var pdfStream = new MemoryStream(ValidPdfBytes());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await Sut.InspectSwissQrCodeAsync(pdfStream);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Validation.IsValid.Should().BeTrue();
|
||||||
|
result.QrCode.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Inspect_Stream_InvalidPdf_ReturnsValidationWithNullQrCode_NeverThrows()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(InvalidResult_Encrypted());
|
||||||
|
|
||||||
|
using var pdfStream = new MemoryStream(EncryptedPdfBytes());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await Sut.InspectSwissQrCodeAsync(pdfStream);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
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 Inspect_FilePath_ValidPdf_ReturnsBothResults()
|
||||||
|
{
|
||||||
|
// 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<Stream>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(ValidResult());
|
||||||
|
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(QrResult());
|
||||||
|
|
||||||
|
// Act – calls extension method which opens a FileStream and delegates to Stream overload
|
||||||
|
var result = await SutAsInterface.InspectSwissQrCodeAsync(tempPath);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Validation.IsValid.Should().BeTrue();
|
||||||
|
result.QrCode.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
finally { File.Delete(tempPath); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
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<Stream>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(InvalidResult_Encrypted());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await SutAsInterface.InspectSwissQrCodeAsync(tempPath);
|
||||||
|
|
||||||
|
// Assert – never throws, validation details always populated
|
||||||
|
result.Validation.IsValid.Should().BeFalse();
|
||||||
|
result.QrCode.Should().BeNull();
|
||||||
|
}
|
||||||
|
finally { File.Delete(tempPath); }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ namespace DocumentService.Tests.Unit.Client;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unit tests for <see cref="ZugferdClient"/>.
|
/// Unit tests for <see cref="ZugferdClient"/>.
|
||||||
/// All tests use a fake <see cref="MockHttpMessageHandler"/> — no real HTTP calls are made.
|
/// All tests use a fake <see cref="MockHttpMessageHandler"/> <EFBFBD> no real HTTP calls are made.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ZugferdClientTests
|
public class ZugferdClientTests
|
||||||
{
|
{
|
||||||
@@ -37,17 +37,17 @@ public class ZugferdClientTests
|
|||||||
private static byte[] FakePdfBytes() => "fake-pdf-content"u8.ToArray();
|
private static byte[] FakePdfBytes() => "fake-pdf-content"u8.ToArray();
|
||||||
private static byte[] FakeXmlBytes() => "<invoice>test</invoice>"u8.ToArray();
|
private static byte[] FakeXmlBytes() => "<invoice>test</invoice>"u8.ToArray();
|
||||||
|
|
||||||
// ?? HasZugferdAsync (Stream) ?????????????????????????????????????????????
|
// ?? CheckAsync (Stream) ?????????????????????????????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HasZugferdAsync_Stream_WhenZugferdPresent_ReturnsTrue()
|
public async Task CheckAsync_Stream_WhenZugferdPresent_ReturnsTrue()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new ZugferdCheckResult { HasZugferd = true, ZugferdFileName = "factur-x.xml" };
|
var expected = new ZugferdCheckResult { HasZugferd = true, ZugferdFileName = "factur-x.xml" };
|
||||||
var (client, handler) = BuildJson(expected);
|
var (client, handler) = BuildJson(expected);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await client.HasZugferdAsync(new MemoryStream(FakePdfBytes()));
|
var result = await client.CheckAsync(new MemoryStream(FakePdfBytes()));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.HasZugferd.Should().BeTrue();
|
result.HasZugferd.Should().BeTrue();
|
||||||
@@ -56,31 +56,31 @@ public class ZugferdClientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HasZugferdAsync_Stream_WhenNoZugferd_ReturnsFalse()
|
public async Task CheckAsync_Stream_WhenNoZugferd_ReturnsFalse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new ZugferdCheckResult { HasZugferd = false };
|
var expected = new ZugferdCheckResult { HasZugferd = false };
|
||||||
var (client, _) = BuildJson(expected);
|
var (client, _) = BuildJson(expected);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await client.HasZugferdAsync(new MemoryStream(FakePdfBytes()));
|
var result = await client.CheckAsync(new MemoryStream(FakePdfBytes()));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.HasZugferd.Should().BeFalse();
|
result.HasZugferd.Should().BeFalse();
|
||||||
result.ZugferdFileName.Should().BeNull();
|
result.ZugferdFileName.Should().BeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ?? HasZugferdAsync (byte[]) ?????????????????????????????????????????????
|
// ?? CheckAsync (byte[]) ?????????????????????????????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HasZugferdAsync_Bytes_SendsJsonWithBase64()
|
public async Task CheckAsync_Bytes_SendsJsonWithBase64()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new ZugferdCheckResult { HasZugferd = true };
|
var expected = new ZugferdCheckResult { HasZugferd = true };
|
||||||
var (client, handler) = BuildJson(expected);
|
var (client, handler) = BuildJson(expected);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await client.HasZugferdAsync(FakePdfBytes());
|
await client.CheckAsync(FakePdfBytes());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
handler.LastRequest!.Content.Should().NotBeNull();
|
handler.LastRequest!.Content.Should().NotBeNull();
|
||||||
@@ -90,16 +90,16 @@ public class ZugferdClientTests
|
|||||||
doc.RootElement.GetProperty("base64Pdf").GetString().Should().NotBeNullOrEmpty();
|
doc.RootElement.GetProperty("base64Pdf").GetString().Should().NotBeNullOrEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ?? ExtractZugferdAsync (Stream) — raw XML stream ?????????????????????????
|
// ?? ExtractAsync (Stream) <EFBFBD> raw XML stream ?????????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractZugferdAsync_Stream_SendsMultipartWithAsFileTrue()
|
public async Task ExtractAsync_Stream_SendsMultipartWithAsFileTrue()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (client, handler) = BuildBytes(FakeXmlBytes());
|
var (client, handler) = BuildBytes(FakeXmlBytes());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
using var result = await client.ExtractZugferdAsync(new MemoryStream(FakePdfBytes()));
|
using var result = await client.ExtractAsync(new MemoryStream(FakePdfBytes()));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.Should().NotBeNull();
|
result.Should().NotBeNull();
|
||||||
@@ -108,30 +108,30 @@ public class ZugferdClientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractZugferdAsync_Stream_ReturnsXmlContent()
|
public async Task ExtractAsync_Stream_ReturnsXmlContent()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var xmlBytes = "<root><invoice/></root>"u8.ToArray();
|
var xmlBytes = "<root><invoice/></root>"u8.ToArray();
|
||||||
var (client, _) = BuildBytes(xmlBytes);
|
var (client, _) = BuildBytes(xmlBytes);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
using var result = await client.ExtractZugferdAsync(new MemoryStream(FakePdfBytes()));
|
using var result = await client.ExtractAsync(new MemoryStream(FakePdfBytes()));
|
||||||
var content = await new StreamReader(result).ReadToEndAsync();
|
var content = await new StreamReader(result).ReadToEndAsync();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
content.Should().Be("<root><invoice/></root>");
|
content.Should().Be("<root><invoice/></root>");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ?? ExtractZugferdAsync (byte[]) ?????????????????????????????????????????
|
// ?? ExtractAsync (byte[]) ?????????????????????????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractZugferdAsync_Bytes_SendsJsonWithFormatFile()
|
public async Task ExtractAsync_Bytes_SendsJsonWithFormatFile()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (client, handler) = BuildBytes(FakeXmlBytes());
|
var (client, handler) = BuildBytes(FakeXmlBytes());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
using var result = await client.ExtractZugferdAsync(FakePdfBytes());
|
using var result = await client.ExtractAsync(FakePdfBytes());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/zugferd/extract?format=file");
|
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/zugferd/extract?format=file");
|
||||||
@@ -139,10 +139,10 @@ public class ZugferdClientTests
|
|||||||
handler.LastRequest.Content!.Headers.ContentType!.MediaType.Should().Be("application/json");
|
handler.LastRequest.Content!.Headers.ContentType!.MediaType.Should().Be("application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ?? ExtractZugferdAsResultAsync (Stream) ?????????????????????????????????
|
// ?? ExtractAsResultAsync (Stream) ?????????????????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractZugferdAsResultAsync_Stream_ReturnsStructuredResult()
|
public async Task ExtractAsResultAsync_Stream_ReturnsStructuredResult()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new ZugferdExtractionResult
|
var expected = new ZugferdExtractionResult
|
||||||
@@ -153,7 +153,7 @@ public class ZugferdClientTests
|
|||||||
var (client, handler) = BuildJson(expected);
|
var (client, handler) = BuildJson(expected);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await client.ExtractZugferdAsResultAsync(new MemoryStream(FakePdfBytes()));
|
var result = await client.ExtractAsResultAsync(new MemoryStream(FakePdfBytes()));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.FileName.Should().Be("factur-x.xml");
|
result.FileName.Should().Be("factur-x.xml");
|
||||||
@@ -161,17 +161,17 @@ public class ZugferdClientTests
|
|||||||
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/zugferd/extract?asFile=false");
|
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/zugferd/extract?asFile=false");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ?? ExtractZugferdAsResultAsync (byte[]) ?????????????????????????????????
|
// ?? ExtractAsResultAsync (byte[]) ?????????????????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractZugferdAsResultAsync_Bytes_SendsJsonWithFormatJson()
|
public async Task ExtractAsResultAsync_Bytes_SendsJsonWithFormatJson()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new ZugferdExtractionResult { FileName = "zugferd.xml", XmlContent = "<x/>" };
|
var expected = new ZugferdExtractionResult { FileName = "zugferd.xml", XmlContent = "<x/>" };
|
||||||
var (client, handler) = BuildJson(expected);
|
var (client, handler) = BuildJson(expected);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await client.ExtractZugferdAsResultAsync(FakePdfBytes());
|
await client.ExtractAsResultAsync(FakePdfBytes());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/zugferd/extract?format=json");
|
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/zugferd/extract?format=json");
|
||||||
@@ -182,7 +182,7 @@ public class ZugferdClientTests
|
|||||||
// ?? HTTP error propagation ???????????????????????????????????????????????
|
// ?? HTTP error propagation ???????????????????????????????????????????????
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HasZugferdAsync_WhenApiReturns400_ThrowsHttpRequestException()
|
public async Task CheckAsync_WhenApiReturns400_ThrowsHttpRequestException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.BadRequest);
|
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.BadRequest);
|
||||||
@@ -190,7 +190,7 @@ public class ZugferdClientTests
|
|||||||
var client = new ZugferdClient(httpClient, NullLogger<ZugferdClient>.Instance);
|
var client = new ZugferdClient(httpClient, NullLogger<ZugferdClient>.Instance);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await client.Invoking(c => c.HasZugferdAsync(new MemoryStream(FakePdfBytes())))
|
await client.Invoking(c => c.CheckAsync(new MemoryStream(FakePdfBytes())))
|
||||||
.Should().ThrowAsync<HttpRequestException>();
|
.Should().ThrowAsync<HttpRequestException>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,36 +40,116 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Domain", "D
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Tests", "DocumentService.Tests\DocumentService.Tests.csproj", "{ECD5BC60-7009-4099-7193-526FDC6E8D99}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Tests", "DocumentService.Tests\DocumentService.Tests.csproj", "{ECD5BC60-7009-4099-7193-526FDC6E8D99}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Tests.Live", "DocumentService.Tests.Live\DocumentService.Tests.Live.csproj", "{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Tests.NuGet", "DocumentService.Tests.NuGet\DocumentService.Tests.NuGet.csproj", "{EE3E7C50-E044-4F77-BB19-74286DDA43F3}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|Any CPU.Build.0 = Release|Any CPU
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|Any CPU.Build.0 = Release|Any CPU
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|Any CPU.Build.0 = Release|Any CPU
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|Any CPU.Build.0 = Release|Any CPU
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|Any CPU.Build.0 = Release|Any CPU
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -85,6 +165,8 @@ Global
|
|||||||
{459EDCC5-240E-A2BC-1330-B5B2326B679C} = {0B80DDC4-A13C-4C5F-8520-AA2F3650E884}
|
{459EDCC5-240E-A2BC-1330-B5B2326B679C} = {0B80DDC4-A13C-4C5F-8520-AA2F3650E884}
|
||||||
{2F2C937F-3732-4E6C-9720-FAB62DC33C63} = {0B80DDC4-A13C-4C5F-8520-AA2F3650E884}
|
{2F2C937F-3732-4E6C-9720-FAB62DC33C63} = {0B80DDC4-A13C-4C5F-8520-AA2F3650E884}
|
||||||
{ECD5BC60-7009-4099-7193-526FDC6E8D99} = {D64772B8-6213-4031-8064-2567F1E724F6}
|
{ECD5BC60-7009-4099-7193-526FDC6E8D99} = {D64772B8-6213-4031-8064-2567F1E724F6}
|
||||||
|
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487} = {D64772B8-6213-4031-8064-2567F1E724F6}
|
||||||
|
{EE3E7C50-E044-4F77-BB19-74286DDA43F3} = {D64772B8-6213-4031-8064-2567F1E724F6}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {832CA90A-06D6-4312-9B35-16CC665EB37C}
|
SolutionGuid = {832CA90A-06D6-4312-9B35-16CC665EB37C}
|
||||||
|
|||||||
0
Program.cs
Normal file
0
Program.cs
Normal file
Reference in New Issue
Block a user