Compare commits
6 Commits
d1fdbb494b
...
f9d4e53ddc
| Author | SHA1 | Date | |
|---|---|---|---|
| f9d4e53ddc | |||
| d92e7f0b07 | |||
| 189622c476 | |||
| f51fde6a23 | |||
| f4d87b42f3 | |||
| 0c27e4fd91 |
@@ -17,4 +17,7 @@ public record PdfValidationResult(
|
||||
string PdfVersion,
|
||||
bool HasAttachments,
|
||||
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 IZugferdClient Zugferd => GetRequiredServiceOfScope<IZugferdClient>();
|
||||
|
||||
public static IWorkflowsClient Workflows => GetRequiredServiceOfScope<IWorkflowsClient>();
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public class PdfAttachmentClient(HttpClient httpClient, ILogger<PdfAttachmentCli
|
||||
: BaseDocumentClient(httpClient, logger), IPdfAttachmentClient
|
||||
{
|
||||
/// <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)");
|
||||
|
||||
@@ -28,7 +28,7 @@ public class PdfAttachmentClient(HttpClient httpClient, ILogger<PdfAttachmentCli
|
||||
}
|
||||
|
||||
/// <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)");
|
||||
|
||||
@@ -43,7 +43,7 @@ public class PdfAttachmentClient(HttpClient httpClient, ILogger<PdfAttachmentCli
|
||||
}
|
||||
|
||||
/// <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)");
|
||||
|
||||
@@ -57,7 +57,7 @@ public class PdfAttachmentClient(HttpClient httpClient, ILogger<PdfAttachmentCli
|
||||
}
|
||||
|
||||
/// <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)");
|
||||
|
||||
@@ -73,7 +73,7 @@ public class PdfAttachmentClient(HttpClient httpClient, ILogger<PdfAttachmentCli
|
||||
|
||||
/// <inheritdoc />
|
||||
[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");
|
||||
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 />
|
||||
[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");
|
||||
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 />
|
||||
[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");
|
||||
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 />
|
||||
[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");
|
||||
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 />
|
||||
[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");
|
||||
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 />
|
||||
[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");
|
||||
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
|
||||
{
|
||||
/// <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>(
|
||||
"/api/pdf/validation/validate",
|
||||
@@ -24,7 +24,7 @@ public class PdfValidationClient(HttpClient httpClient, ILogger<PdfValidationCli
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace DocumentService.Client.Clients;
|
||||
public class SwissQrCodeClient(HttpClient httpClient, ILogger<SwissQrCodeClient> logger) : BaseDocumentClient(httpClient, logger), ISwissQrCodeClient
|
||||
{
|
||||
/// <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}";
|
||||
|
||||
@@ -26,7 +26,7 @@ public class SwissQrCodeClient(HttpClient httpClient, ILogger<SwissQrCodeClient>
|
||||
}
|
||||
|
||||
/// <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}";
|
||||
|
||||
|
||||
84
DocumentService.Client/Clients/WorkflowsClient.cs
Normal file
84
DocumentService.Client/Clients/WorkflowsClient.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Client.Interfaces;
|
||||
|
||||
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<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
||||
byte[] pdfBytes,
|
||||
bool raw = false,
|
||||
bool throwIfInvalid = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var validationResult = await validation.ValidateAsync(pdfBytes, cancellationToken);
|
||||
|
||||
if (!validationResult.IsValid)
|
||||
return throwIfInvalid
|
||||
? throw new InvalidOperationException(
|
||||
$"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " +
|
||||
$"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).")
|
||||
: (SwissQrCodeExtractionResult?)null;
|
||||
|
||||
return await swissQrCode.ExtractAsync(pdfBytes, raw, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
||||
Stream pdfStream,
|
||||
bool raw = false,
|
||||
bool throwIfInvalid = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var validationResult = await validation.ValidateAsync(pdfStream, cancellationToken);
|
||||
|
||||
if (!validationResult.IsValid)
|
||||
return throwIfInvalid
|
||||
? throw new InvalidOperationException(
|
||||
$"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " +
|
||||
$"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).")
|
||||
: (SwissQrCodeExtractionResult?)null;
|
||||
|
||||
// Stream was consumed by validation – reset if possible, otherwise re-open is caller's responsibility.
|
||||
// Since callers typically pass a FileStream or MemoryStream, Position reset covers the common case.
|
||||
if (pdfStream.CanSeek)
|
||||
pdfStream.Position = 0;
|
||||
|
||||
return await swissQrCode.ExtractAsync(pdfStream, raw, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
||||
string filePath,
|
||||
bool raw = false,
|
||||
bool throwIfInvalid = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
using var stream = File.OpenRead(filePath);
|
||||
var validationResult = await validation.ValidateAsync(stream, cancellationToken);
|
||||
#else
|
||||
var pdfBytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
|
||||
var validationResult = await validation.ValidateAsync(pdfBytes, cancellationToken);
|
||||
#endif
|
||||
|
||||
if (!validationResult.IsValid)
|
||||
return throwIfInvalid
|
||||
? throw new InvalidOperationException(
|
||||
$"PDF validation failed (IsEncrypted={validationResult.IsEncrypted}, " +
|
||||
$"PageCount={validationResult.PageCount}, FileSize={validationResult.FileSizeBytes}).")
|
||||
: (SwissQrCodeExtractionResult?)null;
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using var extractStream = File.OpenRead(filePath);
|
||||
return await swissQrCode.ExtractAsync(extractStream, raw, cancellationToken);
|
||||
#else
|
||||
return await swissQrCode.ExtractAsync(pdfBytes, raw, cancellationToken);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace DocumentService.Client.Clients;
|
||||
public class ZugferdClient(HttpClient httpClient, ILogger<ZugferdClient> logger) : BaseDocumentClient(httpClient, logger), IZugferdClient
|
||||
{
|
||||
/// <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>(
|
||||
"/api/pdf/zugferd/has-zugferd",
|
||||
@@ -25,7 +25,7 @@ public class ZugferdClient(HttpClient httpClient, ILogger<ZugferdClient> logger)
|
||||
}
|
||||
|
||||
/// <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) };
|
||||
|
||||
@@ -38,7 +38,7 @@ public class ZugferdClient(HttpClient httpClient, ILogger<ZugferdClient> logger)
|
||||
}
|
||||
|
||||
/// <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)
|
||||
return await SendMultipartForStreamAsync(
|
||||
@@ -49,7 +49,7 @@ public class ZugferdClient(HttpClient httpClient, ILogger<ZugferdClient> logger)
|
||||
}
|
||||
|
||||
/// <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) };
|
||||
|
||||
@@ -61,7 +61,7 @@ public class ZugferdClient(HttpClient httpClient, ILogger<ZugferdClient> logger)
|
||||
}
|
||||
|
||||
/// <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
|
||||
var result = await SendMultipartAsync<ZugferdExtractionResult>(
|
||||
@@ -74,7 +74,7 @@ public class ZugferdClient(HttpClient httpClient, ILogger<ZugferdClient> logger)
|
||||
}
|
||||
|
||||
/// <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) };
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<RepositoryUrl>http://git.dd:3000/AppStd/Rec.git</RepositoryUrl>
|
||||
<PackageTags>digital data document service api client</PackageTags>
|
||||
<Version>1.0.0</Version>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<Version>1.1.0</Version>
|
||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
||||
<FileVersion>1.1.0.0</FileVersion>
|
||||
<Description></Description>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -58,6 +58,9 @@ public static class ServiceCollectionExtensions
|
||||
services.AddHttpClient<IPdfConversionClient, PdfConversionClient>(ConfigureClient)
|
||||
.ConfigurePrimaryHttpMessageHandler(configureHandler);
|
||||
|
||||
// WorkflowsClient composes existing clients – no HttpClient needed.
|
||||
services.AddScoped<IWorkflowsClient, WorkflowsClient>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public interface IPdfAttachmentClient
|
||||
/// <param name="pdfStream">PDF file stream</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Attachment check result with metadata</returns>
|
||||
Task<AttachmentCheckResult> CheckAttachmentsAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||
Task<AttachmentCheckResult> CheckAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if PDF contains attachments (Base64).
|
||||
@@ -22,7 +22,7 @@ public interface IPdfAttachmentClient
|
||||
/// <param name="pdfBytes">PDF file as byte array</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Attachment check result with metadata</returns>
|
||||
Task<AttachmentCheckResult> CheckAttachmentsAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
Task<AttachmentCheckResult> CheckAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all embedded attachments from a PDF (multipart).
|
||||
@@ -31,7 +31,7 @@ public interface IPdfAttachmentClient
|
||||
/// <param name="pdfStream">PDF file stream</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <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>
|
||||
/// 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="cancellationToken">Cancellation token</param>
|
||||
/// <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>
|
||||
/// Adds attachments to PDF (multipart). ?? Not implemented yet in API.
|
||||
@@ -50,7 +50,7 @@ public interface IPdfAttachmentClient
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Modified PDF as stream</returns>
|
||||
[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>
|
||||
/// Adds attachments to PDF (Base64). ?? Not implemented yet in API.
|
||||
@@ -60,5 +60,5 @@ public interface IPdfAttachmentClient
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Modified PDF as stream</returns>
|
||||
[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>
|
||||
/// <returns>PDF/A document as stream</returns>
|
||||
[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>
|
||||
/// Converts a standard PDF to PDF/A format (Base64).
|
||||
@@ -27,7 +27,7 @@ public interface IPdfConversionClient
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>PDF/A document as stream</returns>
|
||||
[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>
|
||||
/// Converts a PDF/A document to standard PDF (multipart).
|
||||
@@ -36,7 +36,7 @@ public interface IPdfConversionClient
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Standard PDF as stream</returns>
|
||||
[Obsolete("API endpoint not implemented yet")]
|
||||
Task<Stream> ConvertFromPdfAAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||
Task<Stream> FromPdfAAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a PDF/A document to standard PDF (Base64).
|
||||
@@ -45,5 +45,5 @@ public interface IPdfConversionClient
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Standard PDF as stream</returns>
|
||||
[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="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Validation result with metadata</returns>
|
||||
Task<PdfValidationResult> ValidatePdfAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||
Task<PdfValidationResult> ValidateAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 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="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Validation result with metadata</returns>
|
||||
Task<PdfValidationResult> ValidatePdfAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
Task<PdfValidationResult> ValidateAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 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="cancellationToken">Cancellation token</param>
|
||||
/// <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>
|
||||
/// 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="cancellationToken">Cancellation token</param>
|
||||
/// <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);
|
||||
}
|
||||
|
||||
|
||||
59
DocumentService.Client/Interfaces/IWorkflowsClient.cs
Normal file
59
DocumentService.Client/Interfaces/IWorkflowsClient.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
|
||||
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 extracts its Swiss QR Code in a single call.
|
||||
/// Validation runs first; extraction is only attempted on valid documents.
|
||||
/// </summary>
|
||||
/// <param name="pdfBytes">PDF file as byte array.</param>
|
||||
/// <param name="raw">
|
||||
/// <c>false</c> (default) – returns a parsed <see cref="SwissQrCodeExtractionResult.Bill"/> object.<br/>
|
||||
/// <c>true</c> – returns raw QR text lines without parsing.
|
||||
/// </param>
|
||||
/// <param name="throwIfInvalid">
|
||||
/// <c>true</c> (default) – throws <see cref="InvalidOperationException"/> when the PDF fails validation.<br/>
|
||||
/// <c>false</c> – returns <c>null</c> instead, allowing the caller to handle the case without a try/catch.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>
|
||||
/// Swiss QR Code extraction result, or <c>null</c> if the document is invalid and
|
||||
/// <paramref name="throwIfInvalid"/> is <c>false</c>.
|
||||
/// </returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the PDF fails validation and <paramref name="throwIfInvalid"/> is <c>true</c>.
|
||||
/// </exception>
|
||||
Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
||||
byte[] pdfBytes,
|
||||
bool raw = false,
|
||||
bool throwIfInvalid = true,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)"/>
|
||||
/// <param name="pdfStream">PDF file as a stream.</param>
|
||||
/// <param name="raw"><inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='raw']"/></param>
|
||||
/// <param name="throwIfInvalid"><inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='throwIfInvalid']"/></param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
||||
Stream pdfStream,
|
||||
bool raw = false,
|
||||
bool throwIfInvalid = true,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)"/>
|
||||
/// <param name="filePath">Absolute or relative path to the PDF file.</param>
|
||||
/// <param name="raw"><inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='raw']"/></param>
|
||||
/// <param name="throwIfInvalid"><inheritdoc cref="ExtractSwissQrCodeAsync(byte[], bool, bool, CancellationToken)" path="/param[@name='throwIfInvalid']"/></param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task<SwissQrCodeExtractionResult?> ExtractSwissQrCodeAsync(
|
||||
string filePath,
|
||||
bool raw = false,
|
||||
bool throwIfInvalid = true,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ public interface IZugferdClient
|
||||
/// <param name="pdfStream">PDF file stream</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ZUGFeRD check result</returns>
|
||||
Task<ZugferdCheckResult> HasZugferdAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||
Task<ZugferdCheckResult> CheckAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 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="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ZUGFeRD check result</returns>
|
||||
Task<ZugferdCheckResult> HasZugferdAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
Task<ZugferdCheckResult> CheckAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 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="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ZUGFeRD XML content as stream</returns>
|
||||
Task<Stream> ExtractZugferdAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||
Task<Stream> ExtractAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 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="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ZUGFeRD XML content as stream</returns>
|
||||
Task<Stream> ExtractZugferdAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
Task<Stream> ExtractAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 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="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Structured ZUGFeRD extraction result</returns>
|
||||
Task<ZugferdExtractionResult> ExtractZugferdAsResultAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||
Task<ZugferdExtractionResult> ExtractAsResultAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 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="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Structured ZUGFeRD extraction result</returns>
|
||||
Task<ZugferdExtractionResult> ExtractZugferdAsResultAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
Task<ZugferdExtractionResult> ExtractAsResultAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
- **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
|
||||
- **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
|
||||
|
||||
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/`
|
||||
**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
|
||||
# NuGet-Paket herunterladen (.nupkg ist ein ZIP-Archiv)
|
||||
$nupkgUrl = "http://172.24.12.35:5000/packages/documentservice.client/1.0.0"
|
||||
$nupkgPath = "$env:TEMP\DocumentService.Client.1.0.0.nupkg"
|
||||
$nupkgUrl = "http://172.24.12.35:5000/packages/documentservice.client/1.1.0"
|
||||
$nupkgPath = "$env:TEMP\DocumentService.Client.1.1.0.nupkg"
|
||||
$extractDir = "$env:TEMP\DocumentService.Client"
|
||||
|
||||
Invoke-WebRequest -Uri $nupkgUrl -OutFile $nupkgPath
|
||||
@@ -155,6 +156,7 @@ Client.OnReconfigure = OnReconfigure.Ignore
|
||||
| `Client.SwissQrCode` | `ISwissQrCodeClient` | Schweizer QR-Code |
|
||||
| `Client.Zugferd` | `IZugferdClient` | ZUGFeRD-Rechnungen |
|
||||
| `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.
|
||||
|
||||
@@ -179,6 +181,102 @@ Alle Beispiele nutzen ausschließlich den statischen `Client`-Einstiegspunkt. Vo
|
||||
|
||||
---
|
||||
|
||||
### Workflows – Kombinierte Operationen
|
||||
|
||||
`Client.Workflows` fasst häufig benötigte Abläufe zusammen, die mehrere Einzelendpunkte orchestrieren. Die Validierung läuft dabei immer zuerst – die Folgeaktion wird nur bei einem gültigen Dokument ausgeführt.
|
||||
|
||||
> Für Anwendungsfälle wie den **eParser** (Swiss QR Bill aus Eingangsrechnungen lesen) ist dies der empfohlene Einstiegspunkt.
|
||||
|
||||
#### Swiss QR Code – mit vorgelagerter Validierung
|
||||
|
||||
```csharp
|
||||
using DocumentService.Client;
|
||||
|
||||
// Variante A: byte[]
|
||||
byte[] pdfBytes = await File.ReadAllBytesAsync("rechnung.pdf");
|
||||
var result = await Client.Workflows.ExtractAsync(pdfBytes);
|
||||
|
||||
Console.WriteLine($"Betrag: {result?.Bill?.Amount} {result?.Bill?.Currency}");
|
||||
Console.WriteLine($"Empfänger: {result?.Bill?.Creditor?.Name}");
|
||||
|
||||
// Variante B: Stream
|
||||
using var stream = File.OpenRead("rechnung.pdf");
|
||||
var result2 = await Client.Workflows.ExtractAsync(stream);
|
||||
|
||||
// Variante C: Dateipfad
|
||||
var result3 = await Client.Workflows.ExtractAsync("rechnung.pdf");
|
||||
```
|
||||
|
||||
```vbnet
|
||||
Imports DocumentService.Client
|
||||
|
||||
' Variante A: byte[]
|
||||
Dim pdfBytes = System.IO.File.ReadAllBytes("rechnung.pdf")
|
||||
Dim result = Await Client.Workflows.ExtractAsync(pdfBytes)
|
||||
|
||||
Console.WriteLine($"Betrag: {result?.Bill?.Amount} {result?.Bill?.Currency}")
|
||||
Console.WriteLine($"Empfänger: {result?.Bill?.Creditor?.Name}")
|
||||
|
||||
' Variante B: Dateipfad
|
||||
Dim result2 = Await Client.Workflows.ExtractAsync("rechnung.pdf")
|
||||
```
|
||||
|
||||
```powershell
|
||||
Add-Type -Path "DocumentService.Client.dll"
|
||||
|
||||
# Variante A: byte[]
|
||||
$pdfBytes = [System.IO.File]::ReadAllBytes("rechnung.pdf")
|
||||
$result = [DocumentService.Client.Client]::Workflows.ExtractAsync($pdfBytes).GetAwaiter().GetResult()
|
||||
Write-Host "Betrag: $($result.Bill.Amount) $($result.Bill.Currency)"
|
||||
Write-Host "Empfaenger:$($result.Bill.Creditor.Name)"
|
||||
|
||||
# Variante B: Dateipfad
|
||||
$result2 = [DocumentService.Client.Client]::Workflows.ExtractAsync("rechnung.pdf").GetAwaiter().GetResult()
|
||||
```
|
||||
|
||||
**Fehlerbehandlung – `throwIfInvalid`:**
|
||||
|
||||
Standardmäßig wirft die Methode eine `InvalidOperationException`, wenn das PDF ungültig ist. Mit `throwIfInvalid: false` wird stattdessen `null` zurückgegeben – geeignet, wenn ungültige Dokumente kein Ausnahmefall sind und ohne try/catch behandelt werden sollen.
|
||||
|
||||
```csharp
|
||||
// Exception (Standard) – geeignet, wenn ungültige PDFs ein Fehlerfall sind
|
||||
var result = await Client.Workflows.ExtractAsync(pdfBytes);
|
||||
|
||||
// Null zurückgeben – geeignet für Batch-Verarbeitung o. Ä.
|
||||
var result = await Client.Workflows.ExtractAsync(pdfBytes, throwIfInvalid: false);
|
||||
if (result is null)
|
||||
{
|
||||
Console.WriteLine("Dokument ungültig oder verschlüsselt – übersprungen.");
|
||||
}
|
||||
```
|
||||
|
||||
```vbnet
|
||||
' Exception (Standard)
|
||||
Dim result = Await Client.Workflows.ExtractAsync(pdfBytes)
|
||||
|
||||
' Null zurückgeben
|
||||
Dim result2 = Await Client.Workflows.ExtractAsync(pdfBytes, throwIfInvalid:=False)
|
||||
If result2 Is Nothing Then
|
||||
Console.WriteLine("Dokument ungültig oder verschlüsselt – übersprungen.")
|
||||
End If
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Exception (Standard)
|
||||
$result = [DocumentService.Client.Client]::Workflows.ExtractAsync($pdfBytes).GetAwaiter().GetResult()
|
||||
|
||||
# Null zurückgeben ($false = throwIfInvalid:false)
|
||||
$result2 = [DocumentService.Client.Client]::Workflows.ExtractAsync($pdfBytes, $false, $false).GetAwaiter().GetResult()
|
||||
if ($null -eq $result2) {
|
||||
Write-Host "Dokument ungueltig oder verschluesselt – uebersprungen."
|
||||
}
|
||||
```
|
||||
|
||||
> **Signatur:** `ExtractSwissQrCodeAsync(pdfBytes / Stream / filePath, raw = false, throwIfInvalid = true, ct = default)`
|
||||
> `raw: true` liefert die Rohtextzeilen des QR-Codes ohne Parsing zurück (siehe [Schweizer QR-Code-Extraktion](#4-schweizer-qr-code-extraktion)).
|
||||
|
||||
---
|
||||
|
||||
### 1. PDF-Validierung
|
||||
|
||||
```csharp
|
||||
@@ -186,7 +284,7 @@ using DocumentService.Client;
|
||||
|
||||
// Variante A: Stream (multipart/form-data – empfohlen für große Dateien)
|
||||
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($"PDF-Version: {result.PdfVersion}");
|
||||
@@ -196,7 +294,7 @@ Console.WriteLine($"Dateigröße: {result.FileSizeMB:F2} MB");
|
||||
|
||||
// Variante B: byte[] (Base64 JSON)
|
||||
byte[] pdfBytes = await File.ReadAllBytesAsync("rechnung.pdf");
|
||||
var result2 = await Client.Validation.ValidatePdfAsync(pdfBytes);
|
||||
var result2 = await Client.Validation.ValidateAsync(pdfBytes);
|
||||
```
|
||||
|
||||
```vbnet
|
||||
@@ -204,7 +302,7 @@ Imports DocumentService.Client
|
||||
|
||||
' Variante A: Stream
|
||||
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()
|
||||
|
||||
Console.WriteLine($"Seiten: {result.PageCount}")
|
||||
@@ -215,7 +313,7 @@ Console.WriteLine($"Dateigröße: {result.FileSizeMB:F2} MB")
|
||||
|
||||
' Variante B: byte[]
|
||||
Dim pdfBytes = System.IO.File.ReadAllBytes("rechnung.pdf")
|
||||
Dim result2 = Await Client.Validation.ValidatePdfAsync(pdfBytes)
|
||||
Dim result2 = Await Client.Validation.ValidateAsync(pdfBytes)
|
||||
```
|
||||
|
||||
```powershell
|
||||
@@ -223,7 +321,7 @@ Add-Type -Path "DocumentService.Client.dll"
|
||||
|
||||
# Variante A: Stream
|
||||
$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()
|
||||
|
||||
Write-Host "Seiten: $($result.PageCount)"
|
||||
@@ -234,7 +332,7 @@ Write-Host "Dateigroesse: $($result.FileSizeMB.ToString('F2')) MB"
|
||||
|
||||
# Variante B: byte[]
|
||||
$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()
|
||||
```
|
||||
|
||||
---
|
||||
@@ -324,7 +422,7 @@ if ($result.Warnings.Count -gt 0) {
|
||||
using DocumentService.Client;
|
||||
|
||||
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($"Anzahl: {check.AttachmentCount}");
|
||||
@@ -337,7 +435,7 @@ foreach (var attachment in check.Attachments)
|
||||
Imports DocumentService.Client
|
||||
|
||||
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($"Anzahl: {check.AttachmentCount}")
|
||||
@@ -351,7 +449,7 @@ Next
|
||||
Add-Type -Path "DocumentService.Client.dll"
|
||||
|
||||
$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 "Anzahl: $($check.AttachmentCount)"
|
||||
@@ -369,7 +467,7 @@ Rückgabe: `Dictionary<string, Stream>` – Schlüssel = ursprünglicher Dateina
|
||||
using DocumentService.Client;
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -386,7 +484,7 @@ foreach (var (fileName, fileStream) in files)
|
||||
Imports DocumentService.Client
|
||||
|
||||
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()
|
||||
|
||||
For Each entry In files
|
||||
@@ -405,7 +503,7 @@ Next
|
||||
Add-Type -Path "DocumentService.Client.dll"
|
||||
|
||||
$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()
|
||||
|
||||
New-Item -ItemType Directory -Force -Path "output" | Out-Null
|
||||
@@ -424,7 +522,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 +841,7 @@ using DocumentService.Client;
|
||||
|
||||
// Geparste Rechnungsstruktur gemäß Swiss QR Bill Standard 2.0
|
||||
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($"Konto: {result.Bill?.Account}");
|
||||
@@ -756,7 +854,7 @@ Console.WriteLine($"Mitteilung:{result.Bill?.UnstructuredMessage}");
|
||||
Imports DocumentService.Client
|
||||
|
||||
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($"Konto: {result.Bill?.Account}")
|
||||
@@ -769,7 +867,7 @@ Console.WriteLine($"Mitteilung:{result.Bill?.UnstructuredMessage}")
|
||||
Add-Type -Path "DocumentService.Client.dll"
|
||||
|
||||
$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 "Konto: $($result.Bill.Account)"
|
||||
@@ -785,7 +883,7 @@ Write-Host "Mitteilung:$($result.Bill.UnstructuredMessage)"
|
||||
```csharp
|
||||
// Ohne Parsing – direkt aus dem QR-Code-Inhalt
|
||||
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)
|
||||
Console.WriteLine(line);
|
||||
@@ -793,7 +891,7 @@ foreach (var line in result.RawLines)
|
||||
|
||||
```vbnet
|
||||
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()
|
||||
|
||||
For Each line In result.RawLines
|
||||
@@ -803,7 +901,7 @@ Next
|
||||
|
||||
```powershell
|
||||
$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()
|
||||
|
||||
foreach ($line in $result.RawLines) { Write-Host $line }
|
||||
@@ -821,7 +919,7 @@ foreach ($line in $result.RawLines) { Write-Host $line }
|
||||
using DocumentService.Client;
|
||||
|
||||
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}");
|
||||
|
||||
@@ -837,7 +935,7 @@ if (check.HasZugferd)
|
||||
Imports DocumentService.Client
|
||||
|
||||
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}")
|
||||
|
||||
@@ -852,7 +950,7 @@ End If
|
||||
Add-Type -Path "DocumentService.Client.dll"
|
||||
|
||||
$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)"
|
||||
|
||||
@@ -867,7 +965,7 @@ if ($check.HasZugferd) {
|
||||
|
||||
```csharp
|
||||
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 xmlStream.CopyToAsync(output);
|
||||
@@ -876,7 +974,7 @@ Console.WriteLine("ZUGFeRD-XML gespeichert.");
|
||||
|
||||
```vbnet
|
||||
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()
|
||||
|
||||
Using output = System.IO.File.Create("factur-x.xml")
|
||||
@@ -888,7 +986,7 @@ Console.WriteLine("ZUGFeRD-XML gespeichert.")
|
||||
|
||||
```powershell
|
||||
$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()
|
||||
|
||||
$output = [System.IO.File]::Create("factur-x.xml")
|
||||
@@ -902,7 +1000,7 @@ Write-Host "ZUGFeRD-XML gespeichert."
|
||||
|
||||
```csharp
|
||||
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($"Größe: {result.FileSize} Bytes");
|
||||
@@ -914,7 +1012,7 @@ var doc = XDocument.Parse(result.XmlContent);
|
||||
|
||||
```vbnet
|
||||
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($"Größe: {result.FileSize} Bytes")
|
||||
@@ -926,7 +1024,7 @@ Dim doc = System.Xml.Linq.XDocument.Parse(result.XmlContent)
|
||||
|
||||
```powershell
|
||||
$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 "Groesse: $($result.FileSize) Bytes"
|
||||
@@ -949,20 +1047,20 @@ $doc = [System.Xml.Linq.XDocument]::Parse($result.XmlContent)
|
||||
```csharp
|
||||
// NICHT VERWENDEN – wirft NotImplementedException
|
||||
#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
|
||||
```
|
||||
|
||||
```vbnet
|
||||
' NICHT VERWENDEN – wirft NotImplementedException
|
||||
#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
|
||||
```
|
||||
|
||||
```powershell
|
||||
# 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.
|
||||
@@ -973,20 +1071,21 @@ Sobald die Endpunkte auf dem Server bereitgestellt werden, entfällt das `[Obsol
|
||||
|
||||
| **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.Attachment` | `CheckAttachmentsAsync()` | `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` | `AddAttachmentsAsync()` | `POST /api/pdf/attachments/add` | ⏳ Geplant | Anhänge in PDF/A-3 einbetten |
|
||||
| `Client.Attachment` | `CheckAsync()` | `POST /api/pdf/attachments/check` | ✅ Verfügbar | Eingebettete Anhänge erkennen |
|
||||
| `Client.Attachment` | `ExtractSwissQrCodeAsync()` | `POST /api/pdf/attachments/extract` | ✅ Verfügbar | Anhänge extrahieren als `Dictionary<string, Stream>` |
|
||||
| `Client.Attachment` | `AddAsync()` | `POST /api/pdf/attachments/add` | ⏳ Geplant | Anhänge in PDF/A-3 einbetten |
|
||||
| `Client.Operations` | `MergeAsync()` | `POST /api/pdf/operations/merge` | ✅ Verfügbar | Mehrere PDFs zusammenführen |
|
||||
| `Client.Operations` | `AnnotateAsync()` | `POST /api/pdf/operations/annotate` | ✅ Verfügbar | Annotationen hinzufügen |
|
||||
| `Client.Operations` | `StampAsync()` | `POST /api/pdf/operations/stamp` | ✅ Verfügbar | Text-/Bildstempel hinzufügen |
|
||||
| `Client.SwissQrCode` | `ExtractSwissQrCodeAsync()` | `POST /api/pdf/qr-code/extract-swiss` | ✅ Verfügbar | Schweizer QR-Code-Daten extrahieren |
|
||||
| `Client.Zugferd` | `HasZugferdAsync()` | `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` | `ExtractZugferdAsResultAsync()` | `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` | `ConvertFromPdfAAsync()` | `POST /api/pdf/conversion/from-pdfa` | ⏳ Geplant | PDF/A-Einschränkungen aufheben |
|
||||
| `Client.Zugferd` | `CheckAsync()` | `POST /api/pdf/zugferd/has-zugferd` | ✅ Verfügbar | ZUGFeRD-Einbettung prüfen |
|
||||
| `Client.Zugferd` | `ExtractSwissQrCodeAsync()` | `POST /api/pdf/zugferd/extract` | ✅ Verfügbar | ZUGFeRD-XML als Stream |
|
||||
| `Client.Zugferd` | `ExtractAsResultAsync()` | `POST /api/pdf/zugferd/extract` | ✅ Verfügbar | ZUGFeRD-XML mit Metadaten als Objekt |
|
||||
| `Client.Conversion` | `ToPdfAAsync()` | `POST /api/pdf/conversion/to-pdfa` | ⏳ Geplant | PDF zu PDF/A konvertieren |
|
||||
| `Client.Conversion` | `FromPdfAAsync()` | `POST /api/pdf/conversion/from-pdfa` | ⏳ Geplant | PDF/A-Einschränkungen aufheben |
|
||||
| `Client.Workflows` | `ExtractSwissQrCodeAsync()` | — | ✅ Verfügbar | Validierung + Swiss QR Code Extraktion in einem Schritt |
|
||||
|
||||
> **⏳ 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 +1099,7 @@ using DocumentService.Client;
|
||||
try
|
||||
{
|
||||
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}");
|
||||
}
|
||||
catch (InvalidOperationException ex) when (ex.Message.Contains("not configured"))
|
||||
@@ -1030,7 +1129,7 @@ Imports DocumentService.Client
|
||||
|
||||
Try
|
||||
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()
|
||||
Console.WriteLine($"Seiten: {result.PageCount}")
|
||||
Catch ex As InvalidOperationException When ex.Message.Contains("not configured")
|
||||
@@ -1049,7 +1148,7 @@ Add-Type -Path "DocumentService.Client.dll"
|
||||
|
||||
try {
|
||||
$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()
|
||||
Write-Host "Seiten: $($result.PageCount)"
|
||||
}
|
||||
|
||||
@@ -54,10 +54,10 @@ public class PdfAttachmentClientTests
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
// ?? CheckAttachmentsAsync (Stream) ???????????????????????????????????????
|
||||
// ?? CheckAsync (Stream) ???????????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAttachmentsAsync_Stream_SendsMultipartPost()
|
||||
public async Task CheckAsync_Stream_SendsMultipartPost()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new AttachmentCheckResult
|
||||
@@ -72,7 +72,7 @@ public class PdfAttachmentClientTests
|
||||
var (client, handler) = BuildJson(expected);
|
||||
|
||||
// Act
|
||||
var result = await client.CheckAttachmentsAsync(new MemoryStream(FakePdfBytes()));
|
||||
var result = await client.CheckAsync(new MemoryStream(FakePdfBytes()));
|
||||
|
||||
// Assert
|
||||
result.HasAttachments.Should().BeTrue();
|
||||
@@ -83,14 +83,14 @@ public class PdfAttachmentClientTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAttachmentsAsync_Stream_PdfWithNoAttachments_ReturnsFalse()
|
||||
public async Task CheckAsync_Stream_PdfWithNoAttachments_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new AttachmentCheckResult { HasAttachments = false, AttachmentCount = 0, Attachments = [] };
|
||||
var (client, _) = BuildJson(expected);
|
||||
|
||||
// Act
|
||||
var result = await client.CheckAttachmentsAsync(new MemoryStream(FakePdfBytes()));
|
||||
var result = await client.CheckAsync(new MemoryStream(FakePdfBytes()));
|
||||
|
||||
// Assert
|
||||
result.HasAttachments.Should().BeFalse();
|
||||
@@ -98,17 +98,17 @@ public class PdfAttachmentClientTests
|
||||
result.Attachments.Should().BeEmpty();
|
||||
}
|
||||
|
||||
// ?? CheckAttachmentsAsync (byte[]) ???????????????????????????????????????
|
||||
// ?? CheckAsync (byte[]) ???????????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAttachmentsAsync_Bytes_SendsJsonWithBase64()
|
||||
public async Task CheckAsync_Bytes_SendsJsonWithBase64()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new AttachmentCheckResult { HasAttachments = false };
|
||||
var (client, handler) = BuildJson(expected);
|
||||
|
||||
// Act
|
||||
await client.CheckAttachmentsAsync(FakePdfBytes());
|
||||
await client.CheckAsync(FakePdfBytes());
|
||||
|
||||
// Assert
|
||||
handler.LastRequest!.Content.Should().NotBeNull();
|
||||
@@ -118,10 +118,10 @@ public class PdfAttachmentClientTests
|
||||
doc.RootElement.GetProperty("base64Pdf").GetString().Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
// ?? ExtractAttachmentsAsync (Stream) <20> ZIP unzip ?????????????????????????
|
||||
// ?? ExtractAsync (Stream) <20> ZIP unzip ?????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractAttachmentsAsync_Stream_UnzipsAndReturnsDictionary()
|
||||
public async Task ExtractAsync_Stream_UnzipsAndReturnsDictionary()
|
||||
{
|
||||
// Arrange
|
||||
var zipBytes = BuildZip(new Dictionary<string, string>
|
||||
@@ -132,7 +132,7 @@ public class PdfAttachmentClientTests
|
||||
var (client, handler) = BuildBytes(zipBytes);
|
||||
|
||||
// Act
|
||||
var result = await client.ExtractAttachmentsAsync(new MemoryStream(FakePdfBytes()));
|
||||
var result = await client.ExtractAsync(new MemoryStream(FakePdfBytes()));
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
@@ -150,7 +150,7 @@ public class PdfAttachmentClientTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractAttachmentsAsync_Stream_WithSingleEntry_ReturnsOneItem()
|
||||
public async Task ExtractAsync_Stream_WithSingleEntry_ReturnsOneItem()
|
||||
{
|
||||
// Arrange
|
||||
var zipBytes = BuildZip(new Dictionary<string, string>
|
||||
@@ -160,7 +160,7 @@ public class PdfAttachmentClientTests
|
||||
var (client, _) = BuildBytes(zipBytes);
|
||||
|
||||
// Act
|
||||
var result = await client.ExtractAttachmentsAsync(new MemoryStream(FakePdfBytes()));
|
||||
var result = await client.ExtractAsync(new MemoryStream(FakePdfBytes()));
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(1);
|
||||
@@ -169,10 +169,10 @@ public class PdfAttachmentClientTests
|
||||
foreach (var s in result.Values) s.Dispose();
|
||||
}
|
||||
|
||||
// ?? ExtractAttachmentsAsync (byte[]) <20> ZIP unzip ?????????????????????????
|
||||
// ?? ExtractAsync (byte[]) <20> ZIP unzip ?????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractAttachmentsAsync_Bytes_SendsJsonAndUnzips()
|
||||
public async Task ExtractAsync_Bytes_SendsJsonAndUnzips()
|
||||
{
|
||||
// Arrange
|
||||
var zipBytes = BuildZip(new Dictionary<string, string>
|
||||
@@ -182,7 +182,7 @@ public class PdfAttachmentClientTests
|
||||
var (client, handler) = BuildBytes(zipBytes);
|
||||
|
||||
// Act
|
||||
var result = await client.ExtractAttachmentsAsync(FakePdfBytes());
|
||||
var result = await client.ExtractAsync(FakePdfBytes());
|
||||
|
||||
// Assert
|
||||
result.Should().ContainKey("invoice.xml");
|
||||
@@ -195,7 +195,7 @@ public class PdfAttachmentClientTests
|
||||
// ?? HTTP error propagation ???????????????????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAttachmentsAsync_WhenApiReturns404_ThrowsHttpRequestException()
|
||||
public async Task CheckAsync_WhenApiReturns404_ThrowsHttpRequestException()
|
||||
{
|
||||
// Arrange
|
||||
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.NotFound);
|
||||
@@ -203,12 +203,12 @@ public class PdfAttachmentClientTests
|
||||
var client = new PdfAttachmentClient(httpClient, NullLogger<PdfAttachmentClient>.Instance);
|
||||
|
||||
// Act & Assert
|
||||
await client.Invoking(c => c.CheckAttachmentsAsync(new MemoryStream(FakePdfBytes())))
|
||||
await client.Invoking(c => c.CheckAsync(new MemoryStream(FakePdfBytes())))
|
||||
.Should().ThrowAsync<HttpRequestException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractAttachmentsAsync_WhenApiReturns500_ThrowsHttpRequestException()
|
||||
public async Task ExtractAsync_WhenApiReturns500_ThrowsHttpRequestException()
|
||||
{
|
||||
// Arrange
|
||||
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.InternalServerError);
|
||||
@@ -216,7 +216,7 @@ public class PdfAttachmentClientTests
|
||||
var client = new PdfAttachmentClient(httpClient, NullLogger<PdfAttachmentClient>.Instance);
|
||||
|
||||
// Act & Assert
|
||||
await client.Invoking(c => c.ExtractAttachmentsAsync(new MemoryStream(FakePdfBytes())))
|
||||
await client.Invoking(c => c.ExtractAsync(new MemoryStream(FakePdfBytes())))
|
||||
.Should().ThrowAsync<HttpRequestException>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,15 +28,15 @@ public class PdfValidationClientTests
|
||||
|
||||
private static byte[] FakePdfBytes() => "fake-pdf"u8.ToArray();
|
||||
|
||||
// ── ValidatePdfAsync (Stream) ────────────────────────────────────────────
|
||||
// ── ValidateAsync (Stream) ────────────────────────────────────────────
|
||||
|
||||
[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 (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.PdfVersion.Should().Be("1.7");
|
||||
@@ -48,52 +48,52 @@ public class PdfValidationClientTests
|
||||
}
|
||||
|
||||
[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 (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");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidatePdfAsync_Stream_ThrowsWhenApiReturnsNull()
|
||||
public async Task ValidateAsync_Stream_ThrowsWhenApiReturnsNull()
|
||||
{
|
||||
var handler = MockHttpMessageHandler.ReturningJson<PdfValidationResult?>(null);
|
||||
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") };
|
||||
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>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidatePdfAsync_Stream_WhenApiReturns400_ThrowsHttpRequestException()
|
||||
public async Task ValidateAsync_Stream_WhenApiReturns400_ThrowsHttpRequestException()
|
||||
{
|
||||
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>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidatePdfAsync_Stream_WhenApiReturns500_ThrowsHttpRequestException()
|
||||
public async Task ValidateAsync_Stream_WhenApiReturns500_ThrowsHttpRequestException()
|
||||
{
|
||||
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>();
|
||||
}
|
||||
|
||||
// ── ValidatePdfAsync (byte[]) ────────────────────────────────────────────
|
||||
// ── ValidateAsync (byte[]) ────────────────────────────────────────────
|
||||
|
||||
[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 (client, handler) = Build(expected);
|
||||
|
||||
var result = await client.ValidatePdfAsync(FakePdfBytes());
|
||||
var result = await client.ValidateAsync(FakePdfBytes());
|
||||
|
||||
result.PageCount.Should().Be(1);
|
||||
result.HasAttachments.Should().BeTrue();
|
||||
@@ -105,10 +105,10 @@ public class PdfValidationClientTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidatePdfAsync_Bytes_WhenApiReturns400_ThrowsHttpRequestException()
|
||||
public async Task ValidateAsync_Bytes_WhenApiReturns400_ThrowsHttpRequestException()
|
||||
{
|
||||
var (client, _) = BuildError(HttpStatusCode.BadRequest);
|
||||
await client.Invoking(c => c.ValidatePdfAsync(FakePdfBytes()))
|
||||
await client.Invoking(c => c.ValidateAsync(FakePdfBytes()))
|
||||
.Should().ThrowAsync<HttpRequestException>();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace DocumentService.Tests.Unit.Client;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public class SwissQrCodeClientTests
|
||||
{
|
||||
@@ -27,10 +27,10 @@ public class SwissQrCodeClientTests
|
||||
|
||||
private static byte[] FakePdfBytes() => "fake-pdf-content"u8.ToArray();
|
||||
|
||||
// ?? ExtractSwissQrCodeAsync (Stream) — parsed Bill ??????????????????????
|
||||
// ?? ExtractAsync (Stream) <EFBFBD> parsed Bill ??????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractSwissQrCodeAsync_Stream_ParsedMode_SendsMultipartToCorrectEndpoint()
|
||||
public async Task ExtractAsync_Stream_ParsedMode_SendsMultipartToCorrectEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new SwissQrCodeExtractionResult(
|
||||
@@ -47,7 +47,7 @@ public class SwissQrCodeClientTests
|
||||
var (client, handler) = Build(expected);
|
||||
|
||||
// Act
|
||||
var result = await client.ExtractSwissQrCodeAsync(new MemoryStream(FakePdfBytes()), raw: false);
|
||||
var result = await client.ExtractAsync(new MemoryStream(FakePdfBytes()), raw: false);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
@@ -56,31 +56,31 @@ public class SwissQrCodeClientTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractSwissQrCodeAsync_Stream_RawMode_SendsRawFlagInUrl()
|
||||
public async Task ExtractAsync_Stream_RawMode_SendsRawFlagInUrl()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new SwissQrCodeExtractionResult(Bill: null, RawLines: ["SPC", "0200", "1"]);
|
||||
var (client, handler) = Build(expected);
|
||||
|
||||
// Act
|
||||
var result = await client.ExtractSwissQrCodeAsync(new MemoryStream(FakePdfBytes()), raw: true);
|
||||
var result = await client.ExtractAsync(new MemoryStream(FakePdfBytes()), raw: true);
|
||||
|
||||
// Assert
|
||||
result.RawLines.Should().HaveCount(3).And.StartWith("SPC");
|
||||
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/qr-code/extract-swiss?raw=True");
|
||||
}
|
||||
|
||||
// ?? ExtractSwissQrCodeAsync (byte[]) ????????????????????????????????????
|
||||
// ?? ExtractAsync (byte[]) ????????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractSwissQrCodeAsync_Bytes_SendsJsonWithBase64()
|
||||
public async Task ExtractAsync_Bytes_SendsJsonWithBase64()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new SwissQrCodeExtractionResult(Bill: null, RawLines: []);
|
||||
var (client, handler) = Build(expected);
|
||||
|
||||
// Act
|
||||
await client.ExtractSwissQrCodeAsync(FakePdfBytes(), raw: false);
|
||||
await client.ExtractAsync(FakePdfBytes(), raw: false);
|
||||
|
||||
// Assert
|
||||
handler.LastRequest!.Content.Should().NotBeNull();
|
||||
@@ -91,14 +91,14 @@ public class SwissQrCodeClientTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractSwissQrCodeAsync_Bytes_RawMode_IncludesRawFlagInUrl()
|
||||
public async Task ExtractAsync_Bytes_RawMode_IncludesRawFlagInUrl()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new SwissQrCodeExtractionResult(Bill: null, RawLines: ["SPC"]);
|
||||
var (client, handler) = Build(expected);
|
||||
|
||||
// Act
|
||||
await client.ExtractSwissQrCodeAsync(FakePdfBytes(), raw: true);
|
||||
await client.ExtractAsync(FakePdfBytes(), raw: true);
|
||||
|
||||
// Assert
|
||||
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/qr-code/extract-swiss?raw=True");
|
||||
@@ -107,7 +107,7 @@ public class SwissQrCodeClientTests
|
||||
// ?? HTTP error propagation ??????????????????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractSwissQrCodeAsync_WhenApiReturns404_ThrowsHttpRequestException()
|
||||
public async Task ExtractAsync_WhenApiReturns404_ThrowsHttpRequestException()
|
||||
{
|
||||
// Arrange
|
||||
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.NotFound);
|
||||
@@ -115,12 +115,12 @@ public class SwissQrCodeClientTests
|
||||
var client = new SwissQrCodeClient(httpClient, NullLogger<SwissQrCodeClient>.Instance);
|
||||
|
||||
// Act & Assert
|
||||
await client.Invoking(c => c.ExtractSwissQrCodeAsync(new MemoryStream(FakePdfBytes())))
|
||||
await client.Invoking(c => c.ExtractAsync(new MemoryStream(FakePdfBytes())))
|
||||
.Should().ThrowAsync<HttpRequestException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractSwissQrCodeAsync_WhenApiReturns500_ThrowsHttpRequestException()
|
||||
public async Task ExtractAsync_WhenApiReturns500_ThrowsHttpRequestException()
|
||||
{
|
||||
// Arrange
|
||||
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.InternalServerError);
|
||||
@@ -128,7 +128,7 @@ public class SwissQrCodeClientTests
|
||||
var client = new SwissQrCodeClient(httpClient, NullLogger<SwissQrCodeClient>.Instance);
|
||||
|
||||
// Act & Assert
|
||||
await client.Invoking(c => c.ExtractSwissQrCodeAsync(FakePdfBytes()))
|
||||
await client.Invoking(c => c.ExtractAsync(FakePdfBytes()))
|
||||
.Should().ThrowAsync<HttpRequestException>();
|
||||
}
|
||||
}
|
||||
|
||||
473
DocumentService.Tests/Unit/Client/WorkflowsClientTests.cs
Normal file
473
DocumentService.Tests/Unit/Client/WorkflowsClientTests.cs
Normal file
@@ -0,0 +1,473 @@
|
||||
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);
|
||||
|
||||
// ─── 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"]);
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// byte[] overload
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Bytes_ValidPdf_CallsValidationThenExtraction_ReturnsResult()
|
||||
{
|
||||
// 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.ExtractSwissQrCodeAsync(pdfBytes);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Bill.Should().NotBeNull();
|
||||
result.Bill!.Currency.Should().Be("CHF");
|
||||
_validation.Verify(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()), Times.Once);
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, false, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bytes_ValidPdf_RawMode_PassesRawFlagToExtraction()
|
||||
{
|
||||
// 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.ExtractSwissQrCodeAsync(pdfBytes, raw: true);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.RawLines.Should().Contain("SPC");
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, true, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bytes_InvalidPdf_ThrowIfInvalidTrue_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var pdfBytes = EncryptedPdfBytes();
|
||||
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(InvalidResult_Encrypted());
|
||||
|
||||
// Act
|
||||
var act = async () => await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: true);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*PDF validation failed*");
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bytes_InvalidPdf_ThrowIfInvalidFalse_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var pdfBytes = EncryptedPdfBytes();
|
||||
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(InvalidResult_Encrypted());
|
||||
|
||||
// Act
|
||||
var result = await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: false);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bytes_EmptyFilePdf_ThrowIfInvalidTrue_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange – PageCount=0 and FileSize=0 → IsValid=false
|
||||
var pdfBytes = new byte[] { 0x00 };
|
||||
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(InvalidResult_EmptyFile());
|
||||
|
||||
// Act & Assert
|
||||
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: true))
|
||||
.Should().ThrowAsync<InvalidOperationException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bytes_ExceptionMessage_ContainsValidationDetails()
|
||||
{
|
||||
// Arrange – ensure the message includes diagnostic fields
|
||||
var pdfBytes = ValidPdfBytes();
|
||||
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(InvalidResult_Encrypted());
|
||||
|
||||
// Act
|
||||
var act = async () => await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: true);
|
||||
|
||||
// Assert
|
||||
var ex = await act.Should().ThrowAsync<InvalidOperationException>();
|
||||
ex.WithMessage("*IsEncrypted=True*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bytes_ValidationThrows_PropagatesHttpRequestException()
|
||||
{
|
||||
// Arrange – simulate network/API failure during validation
|
||||
var pdfBytes = ValidPdfBytes();
|
||||
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new HttpRequestException("API unreachable"));
|
||||
|
||||
// Act & Assert
|
||||
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfBytes))
|
||||
.Should().ThrowAsync<HttpRequestException>()
|
||||
.WithMessage("*API unreachable*");
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bytes_ExtractionThrows_PropagatesHttpRequestException()
|
||||
{
|
||||
// Arrange – validation succeeds but extraction fails
|
||||
var pdfBytes = QrPdfBytes();
|
||||
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ValidResult());
|
||||
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new HttpRequestException("Extraction failed"));
|
||||
|
||||
// Act & Assert
|
||||
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfBytes))
|
||||
.Should().ThrowAsync<HttpRequestException>()
|
||||
.WithMessage("*Extraction failed*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bytes_ValidPdf_CancellationToken_IsForwardedToBothClients()
|
||||
{
|
||||
// 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.ExtractSwissQrCodeAsync(pdfBytes, cancellationToken: token);
|
||||
|
||||
// Assert
|
||||
_validation.Verify(v => v.ValidateAsync(pdfBytes, token), Times.Once);
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, false, token), Times.Once);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// Stream overload
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Stream_ValidPdf_CallsValidationWithStreamThenExtractionWithStream_ReturnsResult()
|
||||
{
|
||||
// Arrange – validation client receives a Stream, extraction client receives the same stream reset to 0
|
||||
_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.ExtractSwissQrCodeAsync(pdfStream);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
_validation.Verify(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stream_SeekableStream_IsResetBetweenValidationAndExtraction()
|
||||
{
|
||||
// Arrange – we verify the stream position is 0 when extraction is called
|
||||
Stream? streamAtExtraction = null;
|
||||
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ValidResult());
|
||||
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<Stream>(), false, It.IsAny<CancellationToken>()))
|
||||
.Callback<Stream, bool, CancellationToken>((s, _, _) => streamAtExtraction = s)
|
||||
.ReturnsAsync(QrResult());
|
||||
|
||||
using var pdfStream = new MemoryStream(ValidPdfBytes());
|
||||
|
||||
// Act
|
||||
await Sut.ExtractSwissQrCodeAsync(pdfStream);
|
||||
|
||||
// Assert
|
||||
streamAtExtraction.Should().NotBeNull();
|
||||
streamAtExtraction!.Position.Should().Be(0, "stream must be rewound before extraction");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stream_InvalidPdf_ThrowIfInvalidTrue_ThrowsWithoutCallingExtraction()
|
||||
{
|
||||
// Arrange
|
||||
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(InvalidResult_Encrypted());
|
||||
|
||||
using var pdfStream = new MemoryStream(EncryptedPdfBytes());
|
||||
|
||||
// Act & Assert
|
||||
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(pdfStream, throwIfInvalid: true))
|
||||
.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*PDF validation failed*");
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<Stream>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stream_InvalidPdf_ThrowIfInvalidFalse_ReturnsNull()
|
||||
{
|
||||
// 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.ExtractSwissQrCodeAsync(pdfStream, throwIfInvalid: false);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<Stream>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stream_ValidPdf_RawMode_PassesRawFlagToExtraction()
|
||||
{
|
||||
// Arrange
|
||||
_validation.Setup(v => v.ValidateAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ValidResult());
|
||||
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<Stream>(), true, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(RawQrResult());
|
||||
|
||||
using var pdfStream = new MemoryStream(QrPdfBytes());
|
||||
|
||||
// Act
|
||||
var result = await Sut.ExtractSwissQrCodeAsync(pdfStream, raw: true);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<Stream>(), true, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// filePath overload
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task FilePath_ValidPdf_ReadsFileAndCallsBothClients_ReturnsResult()
|
||||
{
|
||||
// Arrange – write a real temp file so File.ReadAllBytesAsync / File.OpenRead can read it
|
||||
var tempPath = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllBytesAsync(tempPath, ValidPdfBytes());
|
||||
|
||||
_validation.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ValidResult());
|
||||
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<byte[]>(), false, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(QrResult());
|
||||
|
||||
// Act
|
||||
var result = await Sut.ExtractSwissQrCodeAsync(tempPath);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
_validation.Verify(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), false, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FilePath_InvalidPdf_ThrowIfInvalidTrue_ThrowsWithoutCallingExtraction()
|
||||
{
|
||||
// Arrange
|
||||
var tempPath = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllBytesAsync(tempPath, EncryptedPdfBytes());
|
||||
|
||||
_validation.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(InvalidResult_Encrypted());
|
||||
|
||||
// Act & Assert
|
||||
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(tempPath, throwIfInvalid: true))
|
||||
.Should().ThrowAsync<InvalidOperationException>();
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FilePath_InvalidPdf_ThrowIfInvalidFalse_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var tempPath = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllBytesAsync(tempPath, EncryptedPdfBytes());
|
||||
|
||||
_validation.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(InvalidResult_Encrypted());
|
||||
|
||||
// Act
|
||||
var result = await Sut.ExtractSwissQrCodeAsync(tempPath, throwIfInvalid: false);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FilePath_NonExistentFile_ThrowsFileNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistent = Path.Combine(Path.GetTempPath(), "does_not_exist_12345.pdf");
|
||||
|
||||
// Act & Assert
|
||||
await Sut.Invoking(s => s.ExtractSwissQrCodeAsync(nonExistent))
|
||||
.Should().ThrowAsync<FileNotFoundException>();
|
||||
_validation.Verify(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FilePath_ValidPdf_RawMode_PassesRawFlagToExtraction()
|
||||
{
|
||||
// Arrange
|
||||
var tempPath = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllBytesAsync(tempPath, QrPdfBytes());
|
||||
|
||||
_validation.Setup(v => v.ValidateAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ValidResult());
|
||||
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny<byte[]>(), true, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(RawQrResult());
|
||||
|
||||
// Act
|
||||
var result = await Sut.ExtractSwissQrCodeAsync(tempPath, raw: true);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny<byte[]>(), true, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// Orchestration contract: extraction must NEVER be called on invalid PDF
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task AnyOverload_InvalidPdf_ExtractionIsNeverCalled_RegardlessOfThrowIfInvalid(bool throwIfInvalid)
|
||||
{
|
||||
// Arrange
|
||||
var pdfBytes = EncryptedPdfBytes();
|
||||
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(InvalidResult_Encrypted());
|
||||
|
||||
// Act
|
||||
try { await Sut.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: throwIfInvalid); }
|
||||
catch (InvalidOperationException) { /* expected when throwIfInvalid=true */ }
|
||||
|
||||
// Assert — extraction must never be called in either branch
|
||||
_swissQrCode.Verify(
|
||||
q => q.ExtractAsync(It.IsAny<byte[]>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never,
|
||||
"extraction must not be attempted on an invalid document");
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace DocumentService.Tests.Unit.Client;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public class ZugferdClientTests
|
||||
{
|
||||
@@ -37,17 +37,17 @@ public class ZugferdClientTests
|
||||
private static byte[] FakePdfBytes() => "fake-pdf-content"u8.ToArray();
|
||||
private static byte[] FakeXmlBytes() => "<invoice>test</invoice>"u8.ToArray();
|
||||
|
||||
// ?? HasZugferdAsync (Stream) ?????????????????????????????????????????????
|
||||
// ?? CheckAsync (Stream) ?????????????????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task HasZugferdAsync_Stream_WhenZugferdPresent_ReturnsTrue()
|
||||
public async Task CheckAsync_Stream_WhenZugferdPresent_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new ZugferdCheckResult { HasZugferd = true, ZugferdFileName = "factur-x.xml" };
|
||||
var (client, handler) = BuildJson(expected);
|
||||
|
||||
// Act
|
||||
var result = await client.HasZugferdAsync(new MemoryStream(FakePdfBytes()));
|
||||
var result = await client.CheckAsync(new MemoryStream(FakePdfBytes()));
|
||||
|
||||
// Assert
|
||||
result.HasZugferd.Should().BeTrue();
|
||||
@@ -56,31 +56,31 @@ public class ZugferdClientTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HasZugferdAsync_Stream_WhenNoZugferd_ReturnsFalse()
|
||||
public async Task CheckAsync_Stream_WhenNoZugferd_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new ZugferdCheckResult { HasZugferd = false };
|
||||
var (client, _) = BuildJson(expected);
|
||||
|
||||
// Act
|
||||
var result = await client.HasZugferdAsync(new MemoryStream(FakePdfBytes()));
|
||||
var result = await client.CheckAsync(new MemoryStream(FakePdfBytes()));
|
||||
|
||||
// Assert
|
||||
result.HasZugferd.Should().BeFalse();
|
||||
result.ZugferdFileName.Should().BeNull();
|
||||
}
|
||||
|
||||
// ?? HasZugferdAsync (byte[]) ?????????????????????????????????????????????
|
||||
// ?? CheckAsync (byte[]) ?????????????????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task HasZugferdAsync_Bytes_SendsJsonWithBase64()
|
||||
public async Task CheckAsync_Bytes_SendsJsonWithBase64()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new ZugferdCheckResult { HasZugferd = true };
|
||||
var (client, handler) = BuildJson(expected);
|
||||
|
||||
// Act
|
||||
await client.HasZugferdAsync(FakePdfBytes());
|
||||
await client.CheckAsync(FakePdfBytes());
|
||||
|
||||
// Assert
|
||||
handler.LastRequest!.Content.Should().NotBeNull();
|
||||
@@ -90,16 +90,16 @@ public class ZugferdClientTests
|
||||
doc.RootElement.GetProperty("base64Pdf").GetString().Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
// ?? ExtractZugferdAsync (Stream) — raw XML stream ?????????????????????????
|
||||
// ?? ExtractAsync (Stream) <EFBFBD> raw XML stream ?????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractZugferdAsync_Stream_SendsMultipartWithAsFileTrue()
|
||||
public async Task ExtractAsync_Stream_SendsMultipartWithAsFileTrue()
|
||||
{
|
||||
// Arrange
|
||||
var (client, handler) = BuildBytes(FakeXmlBytes());
|
||||
|
||||
// Act
|
||||
using var result = await client.ExtractZugferdAsync(new MemoryStream(FakePdfBytes()));
|
||||
using var result = await client.ExtractAsync(new MemoryStream(FakePdfBytes()));
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
@@ -108,30 +108,30 @@ public class ZugferdClientTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractZugferdAsync_Stream_ReturnsXmlContent()
|
||||
public async Task ExtractAsync_Stream_ReturnsXmlContent()
|
||||
{
|
||||
// Arrange
|
||||
var xmlBytes = "<root><invoice/></root>"u8.ToArray();
|
||||
var (client, _) = BuildBytes(xmlBytes);
|
||||
|
||||
// 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();
|
||||
|
||||
// Assert
|
||||
content.Should().Be("<root><invoice/></root>");
|
||||
}
|
||||
|
||||
// ?? ExtractZugferdAsync (byte[]) ?????????????????????????????????????????
|
||||
// ?? ExtractAsync (byte[]) ?????????????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractZugferdAsync_Bytes_SendsJsonWithFormatFile()
|
||||
public async Task ExtractAsync_Bytes_SendsJsonWithFormatFile()
|
||||
{
|
||||
// Arrange
|
||||
var (client, handler) = BuildBytes(FakeXmlBytes());
|
||||
|
||||
// Act
|
||||
using var result = await client.ExtractZugferdAsync(FakePdfBytes());
|
||||
using var result = await client.ExtractAsync(FakePdfBytes());
|
||||
|
||||
// Assert
|
||||
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");
|
||||
}
|
||||
|
||||
// ?? ExtractZugferdAsResultAsync (Stream) ?????????????????????????????????
|
||||
// ?? ExtractAsResultAsync (Stream) ?????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractZugferdAsResultAsync_Stream_ReturnsStructuredResult()
|
||||
public async Task ExtractAsResultAsync_Stream_ReturnsStructuredResult()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new ZugferdExtractionResult
|
||||
@@ -153,7 +153,7 @@ public class ZugferdClientTests
|
||||
var (client, handler) = BuildJson(expected);
|
||||
|
||||
// Act
|
||||
var result = await client.ExtractZugferdAsResultAsync(new MemoryStream(FakePdfBytes()));
|
||||
var result = await client.ExtractAsResultAsync(new MemoryStream(FakePdfBytes()));
|
||||
|
||||
// Assert
|
||||
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");
|
||||
}
|
||||
|
||||
// ?? ExtractZugferdAsResultAsync (byte[]) ?????????????????????????????????
|
||||
// ?? ExtractAsResultAsync (byte[]) ?????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractZugferdAsResultAsync_Bytes_SendsJsonWithFormatJson()
|
||||
public async Task ExtractAsResultAsync_Bytes_SendsJsonWithFormatJson()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new ZugferdExtractionResult { FileName = "zugferd.xml", XmlContent = "<x/>" };
|
||||
var (client, handler) = BuildJson(expected);
|
||||
|
||||
// Act
|
||||
await client.ExtractZugferdAsResultAsync(FakePdfBytes());
|
||||
await client.ExtractAsResultAsync(FakePdfBytes());
|
||||
|
||||
// Assert
|
||||
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/zugferd/extract?format=json");
|
||||
@@ -182,7 +182,7 @@ public class ZugferdClientTests
|
||||
// ?? HTTP error propagation ???????????????????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task HasZugferdAsync_WhenApiReturns400_ThrowsHttpRequestException()
|
||||
public async Task CheckAsync_WhenApiReturns400_ThrowsHttpRequestException()
|
||||
{
|
||||
// Arrange
|
||||
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.BadRequest);
|
||||
@@ -190,7 +190,7 @@ public class ZugferdClientTests
|
||||
var client = new ZugferdClient(httpClient, NullLogger<ZugferdClient>.Instance);
|
||||
|
||||
// Act & Assert
|
||||
await client.Invoking(c => c.HasZugferdAsync(new MemoryStream(FakePdfBytes())))
|
||||
await client.Invoking(c => c.CheckAsync(new MemoryStream(FakePdfBytes())))
|
||||
.Should().ThrowAsync<HttpRequestException>();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user