test(live): add live API integration test project (DocumentService.Tests.Live)
End-to-end tests that run against a real, deployed API instance to verify that
all controllers respond correctly over HTTP - bypassing WebApplicationFactory.
Project setup:
- Targets net8.0; xUnit 2.9.3 + FluentAssertions 7.0.0
- References DocumentService.Client, Application and Domain as project refs
- Embeds 4 test PDFs as EmbeddedResources:
valid.pdf, pdfWithSwissQRCode.pdf, pdfWithMoreThanOneAttachment.pdf,
encryptedPdf.pdf
LiveApiTestBase:
- Base URL defaults to http://172.24.12.39:9393, overridable via
LIVE_API_BASE_URL env var
- Constructs typed clients (PdfValidationClient, PdfAttachmentClient,
PdfOperationsClient, SwissQrCodeClient) backed by a single HttpClient
- LoadPdfBytes / LoadPdfStream helpers for EmbeddedResource access
Test coverage per file:
- PdfValidationLiveTests : Stream + byte[] overloads for validate and
validate-pdfa; empty/corrupted inputs assert 4xx/5xx
- PdfAttachmentLiveTests : check + extract endpoints; stream and byte[] variants
- PdfOperationsLiveTests : merge and stamp endpoints
- SwissQrCodeLiveTests : extract Swiss QR bill; invalid content asserts error
Tests are intentionally skipped in CI (no live server); run manually with
the LIVE_API_BASE_URL variable pointing to a running instance.
This commit is contained in:
235
DocumentService.Tests.Live/API/PdfAttachmentLiveTests.cs
Normal file
235
DocumentService.Tests.Live/API/PdfAttachmentLiveTests.cs
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
using FluentAssertions;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Live tests for PDF attachment endpoints (check + extract).
|
||||||
|
/// Uses PdfAttachmentClient — routes managed by the client.
|
||||||
|
/// Target: http://172.24.12.39:9393/
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Live API")]
|
||||||
|
public class PdfAttachmentLiveTests : LiveApiTestBase
|
||||||
|
{
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// CheckAsync(Stream) → multipart → /api/pdf/attachments/check
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_PdfWithoutAttachments_ReturnsHasAttachmentsFalse()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.HasAttachments.Should().BeFalse();
|
||||||
|
result.AttachmentCount.Should().Be(0);
|
||||||
|
result.Attachments.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_PdfWith6Attachments_ReturnsAllAttachmentsWithMetadata()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeTrue();
|
||||||
|
result.AttachmentCount.Should().Be(6, "PDF has exactly 6 attachments");
|
||||||
|
result.Attachments.Should().HaveCount(6);
|
||||||
|
|
||||||
|
foreach (var attachment in result.Attachments)
|
||||||
|
{
|
||||||
|
attachment.FileName.Should().NotBeNullOrEmpty();
|
||||||
|
attachment.MimeType.Should().NotBeNullOrEmpty();
|
||||||
|
attachment.Size.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_PdfWithSwissQrCode_HasNoAttachments()
|
||||||
|
{
|
||||||
|
// QR code PDF has no embedded file attachments
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeFalse();
|
||||||
|
result.AttachmentCount.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Stream_CorruptedContent_Throws()
|
||||||
|
{
|
||||||
|
byte[] garbage = "This is not a valid PDF content"u8.ToArray();
|
||||||
|
using var stream = new MemoryStream(garbage);
|
||||||
|
|
||||||
|
var act = async () => await AttachmentClient.CheckAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"corrupted PDF should return 500 → client throws");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// CheckAsync(byte[]) → Base64 JSON → /api/pdf/attachments/check
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Bytes_PdfWithoutAttachments_ReturnsHasAttachmentsFalse()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(bytes);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeFalse();
|
||||||
|
result.AttachmentCount.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Bytes_PdfWith6Attachments_ReturnsAllAttachmentsWithMetadata()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var result = await AttachmentClient.CheckAsync(bytes);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeTrue();
|
||||||
|
result.AttachmentCount.Should().Be(6);
|
||||||
|
result.Attachments.Should().HaveCount(6);
|
||||||
|
|
||||||
|
foreach (var attachment in result.Attachments)
|
||||||
|
{
|
||||||
|
attachment.FileName.Should().NotBeNullOrEmpty();
|
||||||
|
attachment.MimeType.Should().NotBeNullOrEmpty();
|
||||||
|
attachment.Size.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Check_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await AttachmentClient.CheckAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ExtractAsync(Stream) → multipart → /api/pdf/attachments/extract
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_PdfWith6Attachments_Returns6NamedStreams()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var attachments = await AttachmentClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
attachments.Should().HaveCount(6, "PDF contains 6 attachments");
|
||||||
|
|
||||||
|
foreach (var (name, content) in attachments)
|
||||||
|
{
|
||||||
|
name.Should().NotBeNullOrEmpty();
|
||||||
|
content.Length.Should().BeGreaterThan(0);
|
||||||
|
content.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_AttachmentNamesAreUnique()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var attachments = await AttachmentClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
attachments.Keys.Should().OnlyHaveUniqueItems(
|
||||||
|
"ZIP entry names must be unique within the archive");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
foreach (var s in attachments.Values) s.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await AttachmentClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_CorruptedContent_Throws()
|
||||||
|
{
|
||||||
|
byte[] garbage = "This is not a valid PDF content"u8.ToArray();
|
||||||
|
using var stream = new MemoryStream(garbage);
|
||||||
|
|
||||||
|
var act = async () => await AttachmentClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ExtractAsync(byte[]) → Base64 JSON → /api/pdf/attachments/extract
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_PdfWith6Attachments_Returns6NamedStreams()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var attachments = await AttachmentClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
attachments.Should().HaveCount(6);
|
||||||
|
|
||||||
|
foreach (var (name, content) in attachments)
|
||||||
|
{
|
||||||
|
name.Should().NotBeNullOrEmpty();
|
||||||
|
content.Length.Should().BeGreaterThan(0);
|
||||||
|
content.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_AttachmentContentsAreNonEmpty()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var attachments = await AttachmentClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var (_, content) in attachments)
|
||||||
|
{
|
||||||
|
((MemoryStream)content).ToArray().Should().NotBeEmpty(
|
||||||
|
"every extracted attachment must have content");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
foreach (var s in attachments.Values) s.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await AttachmentClient.ExtractAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
532
DocumentService.Tests.Live/API/PdfOperationsLiveTests.cs
Normal file
532
DocumentService.Tests.Live/API/PdfOperationsLiveTests.cs
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs.Requests;
|
||||||
|
using DocumentService.Domain.Models.ValueObjects;
|
||||||
|
using FluentAssertions;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Live tests for PDF operations: merge, annotate, stamp.
|
||||||
|
/// Uses PdfOperationsClient — routes managed by the client.
|
||||||
|
/// Target: http://172.24.12.39:9393/
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Live API")]
|
||||||
|
public class PdfOperationsLiveTests : LiveApiTestBase
|
||||||
|
{
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// MERGE — MergeAsync(IEnumerable<Stream>) → multipart
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Streams_TwoPdfs_ReturnsMergedPdfStream()
|
||||||
|
{
|
||||||
|
using var pdf1 = LoadPdfStream("valid.pdf");
|
||||||
|
using var pdf2 = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
await using var merged = await OperationsClient.MergeAsync(new[] { pdf1, pdf2 });
|
||||||
|
|
||||||
|
merged.Should().NotBeNull();
|
||||||
|
merged.Length.Should().BeGreaterThan(0);
|
||||||
|
// Merged must start with PDF magic bytes (%PDF = 0x25 0x50 0x44 0x46)
|
||||||
|
var header = new byte[4];
|
||||||
|
merged.ReadExactly(header, 0, 4);
|
||||||
|
header.Should().Equal(new byte[] { 0x25, 0x50, 0x44, 0x46 },
|
||||||
|
"merged result must be a valid PDF");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Streams_ThreePdfs_ReturnsMergedPdfStream()
|
||||||
|
{
|
||||||
|
using var pdf1 = LoadPdfStream("valid.pdf");
|
||||||
|
using var pdf2 = LoadPdfStream("valid.pdf");
|
||||||
|
using var pdf3 = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
await using var merged = await OperationsClient.MergeAsync(new[] { pdf1, pdf2, pdf3 });
|
||||||
|
|
||||||
|
merged.Should().NotBeNull();
|
||||||
|
merged.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Streams_OnlyOnePdf_Throws()
|
||||||
|
{
|
||||||
|
using var pdf = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.MergeAsync(new[] { pdf });
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"at least 2 PDFs are required; single PDF should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Streams_CorruptedPdf_Throws()
|
||||||
|
{
|
||||||
|
byte[] garbage = "NOT A PDF"u8.ToArray();
|
||||||
|
using var corrupted = new MemoryStream(garbage);
|
||||||
|
using var valid = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.MergeAsync(new[] { corrupted, valid });
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"corrupted PDF should return 500");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// MERGE — MergeAsync(IEnumerable<byte[]>) → Base64 JSON
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Bytes_TwoPdfs_ReturnsMergedPdfStream()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
await using var merged = await OperationsClient.MergeAsync(new[] { pdf, pdf });
|
||||||
|
|
||||||
|
merged.Should().NotBeNull();
|
||||||
|
merged.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Bytes_WithPageRanges_ReturnsMergedPdfStream()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
await using var merged = await OperationsClient.MergeAsync(
|
||||||
|
new[] { pdf, pdf },
|
||||||
|
pageRanges: ["1", "1"]);
|
||||||
|
|
||||||
|
merged.Should().NotBeNull();
|
||||||
|
merged.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Bytes_OnlyOnePdf_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.MergeAsync(new[] { pdf });
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Merge_Bytes_PageRangeExceedsPageCount_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf"); // 1-page PDF
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.MergeAsync(
|
||||||
|
new[] { pdf, pdf },
|
||||||
|
pageRanges: ["999-1000", null]);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"out-of-bounds page range should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// ANNOTATE — AnnotateAsync(Stream, ...) → multipart
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_TextMarkupHighlight_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty, // not used in Stream overload
|
||||||
|
AnnotationType = AnnotationType.TextMarkup,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 100, Y1 = 100, X2 = 300, Y2 = 120,
|
||||||
|
TextMarkupStyle = TextMarkupStyle.Highlight,
|
||||||
|
Color = "FFFF00",
|
||||||
|
Author = "LiveTest"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_Underline_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.TextMarkup,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 50, Y1 = 700, X2 = 300, Y2 = 720,
|
||||||
|
TextMarkupStyle = TextMarkupStyle.Underline
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_Circle_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.Circle,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 100, Y1 = 200, X2 = 200, Y2 = 300,
|
||||||
|
Color = "00FF00",
|
||||||
|
Content = "Circle annotation"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_Square_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.Square,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 250, Y1 = 250, X2 = 350, Y2 = 350,
|
||||||
|
Color = "0000FF"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Stream_InvalidPageNumber_Throws()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.FreeText,
|
||||||
|
PageNumber = 9999, // exceeds page count
|
||||||
|
X1 = 0, Y1 = 0, X2 = 100, Y2 = 100,
|
||||||
|
Content = "Test"
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(stream, request);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"invalid page number should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// ANNOTATE — AnnotateAsync(byte[], ...) → Base64 JSON
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_FreeText_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty, // client fills this in
|
||||||
|
AnnotationType = AnnotationType.FreeText,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 50, Y1 = 50, X2 = 200, Y2 = 100,
|
||||||
|
Content = "Free text from live test",
|
||||||
|
Author = "LiveTestRunner",
|
||||||
|
Color = "FF0000"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_StickyNote_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.StickyNote,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 300, Y1 = 300, X2 = 320, Y2 = 320,
|
||||||
|
Content = "Sticky note from live test",
|
||||||
|
Author = "Reviewer"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_TextMarkupStrikeout_ReturnsAnnotatedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.TextMarkup,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 60, Y1 = 600, X2 = 250, Y2 = 620,
|
||||||
|
TextMarkupStyle = TextMarkupStyle.Strikeout,
|
||||||
|
Color = "FF6600"
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_FreeTextWithoutContent_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.FreeText,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 0, Y1 = 0, X2 = 100, Y2 = 100
|
||||||
|
// Content intentionally missing
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"FreeText without content should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Annotate_Bytes_TextMarkupWithoutStyle_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddAnnotationBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
AnnotationType = AnnotationType.TextMarkup,
|
||||||
|
PageNumber = 1,
|
||||||
|
X1 = 0, Y1 = 0, X2 = 100, Y2 = 100
|
||||||
|
// TextMarkupStyle intentionally missing
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"TextMarkup without style should return 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// STAMP — StampAsync(Stream, ...) → multipart
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Stream_TextStamp_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 100, Y = 400,
|
||||||
|
Text = "CONFIDENTIAL",
|
||||||
|
FontSize = 24,
|
||||||
|
Color = "FF0000",
|
||||||
|
Opacity = 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(stream, request);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Stream_TextStamp_WithRotation_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 300, Y = 400,
|
||||||
|
Text = "DRAFT",
|
||||||
|
Rotation = 45,
|
||||||
|
Opacity = 0.3,
|
||||||
|
Placement = StampPlacement.Background
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Stream_PredefinedStamp_Confidential_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Predefined,
|
||||||
|
X = 100, Y = 400,
|
||||||
|
PredefinedType = PredefinedStampType.Confidential,
|
||||||
|
Opacity = 0.6
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(stream, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// STAMP — StampAsync(byte[], ...) → Base64 JSON
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_TextStamp_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 100, Y = 400,
|
||||||
|
Text = "APPROVED",
|
||||||
|
FontName = "Arial",
|
||||||
|
FontSize = 20,
|
||||||
|
Color = "008000",
|
||||||
|
Opacity = 0.7,
|
||||||
|
Placement = StampPlacement.Foreground
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_WatermarkBackground_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 150, Y = 350,
|
||||||
|
Text = "WATERMARK",
|
||||||
|
FontSize = 36,
|
||||||
|
Color = "AAAAAA",
|
||||||
|
Opacity = 0.2,
|
||||||
|
Rotation = 45,
|
||||||
|
Placement = StampPlacement.Background
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_PredefinedStamp_Approved_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Predefined,
|
||||||
|
X = 100, Y = 400,
|
||||||
|
PredefinedType = PredefinedStampType.Approved,
|
||||||
|
Opacity = 0.8
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_PredefinedStamp_Draft_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Predefined,
|
||||||
|
X = 200, Y = 500,
|
||||||
|
PredefinedType = PredefinedStampType.Draft,
|
||||||
|
Opacity = 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_TextStampOnSpecificPage_ReturnsStampedPdf()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
PageNumbers = [1],
|
||||||
|
X = 200, Y = 500,
|
||||||
|
Text = "PAGE 1 ONLY",
|
||||||
|
Opacity = 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
|
||||||
|
result.Length.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Stamp_Bytes_TextStampWithoutText_Throws()
|
||||||
|
{
|
||||||
|
byte[] pdf = LoadPdfBytes("valid.pdf");
|
||||||
|
var request = new AddStampBase64Request
|
||||||
|
{
|
||||||
|
Base64Pdf = string.Empty,
|
||||||
|
StampType = StampType.Text,
|
||||||
|
X = 100, Y = 400
|
||||||
|
// Text intentionally missing
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () =>
|
||||||
|
{
|
||||||
|
await using var result = await OperationsClient.StampAsync(pdf, request);
|
||||||
|
};
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"Text stamp without text should return 400");
|
||||||
|
}
|
||||||
|
}
|
||||||
196
DocumentService.Tests.Live/API/PdfValidationLiveTests.cs
Normal file
196
DocumentService.Tests.Live/API/PdfValidationLiveTests.cs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
using FluentAssertions;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Live tests for PDF validation endpoints.
|
||||||
|
/// Uses PdfValidationClient — routes are managed by the client, not the test.
|
||||||
|
/// Target: http://172.24.12.39:9393/
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Live API")]
|
||||||
|
public class PdfValidationLiveTests : LiveApiTestBase
|
||||||
|
{
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ValidateAsync(Stream) → multipart/form-data → /api/pdf/validation/validate
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_ValidPdf_ReturnsMetadata()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
result.FileSizeBytes.Should().BeGreaterThan(0);
|
||||||
|
result.PdfVersion.Should().NotBeNullOrEmpty();
|
||||||
|
result.HasAttachments.Should().BeFalse();
|
||||||
|
result.AttachmentCount.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_PdfWithAttachments_ReturnsHasAttachmentsTrue()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeTrue();
|
||||||
|
result.AttachmentCount.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_PdfWithSwissQrCode_ReturnsValidResult()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
result.FileSizeBytes.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"empty file should be rejected with 400");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Stream_CorruptedContent_Throws()
|
||||||
|
{
|
||||||
|
// Valid stream shape, but not a PDF
|
||||||
|
byte[] garbage = "THIS IS NOT A PDF FILE"u8.ToArray();
|
||||||
|
using var stream = new MemoryStream(garbage);
|
||||||
|
|
||||||
|
var act = async () => await ValidationClient.ValidateAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"corrupted content should be rejected (500)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ValidateAsync(byte[]) → Base64 JSON → /api/pdf/validation/validate
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Bytes_ValidPdf_ReturnsMetadata()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(bytes);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
result.FileSizeBytes.Should().BeGreaterThan(0);
|
||||||
|
result.PdfVersion.Should().NotBeNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Bytes_PdfWithAttachments_ReturnsHasAttachmentsTrue()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidateAsync(bytes);
|
||||||
|
|
||||||
|
result.HasAttachments.Should().BeTrue();
|
||||||
|
result.AttachmentCount.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Validate_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await ValidationClient.ValidateAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"empty byte array encodes to empty Base64, which should be rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ValidatePdfAAsync(Stream) → multipart → /api/pdf/validation/validate-pdfa
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Stream_ValidPdf_ReturnsComplianceFields()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidatePdfAAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.IsValid.Should().BeTrue();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
result.PdfVersion.Should().NotBeNullOrEmpty();
|
||||||
|
result.FileSize.Should().BeGreaterThan(0);
|
||||||
|
result.Encrypted.Should().BeFalse();
|
||||||
|
// valid.pdf is a regular PDF, not PDF/A
|
||||||
|
result.PdfACompliant.Should().BeFalse();
|
||||||
|
result.PdfAVersion.Should().BeNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Stream_EncryptedPdf_ReturnsEncryptedTrue_NotCompliant()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("encryptedPdf.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidatePdfAAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Encrypted.Should().BeTrue();
|
||||||
|
result.PdfACompliant.Should().BeFalse("encrypted PDFs cannot be PDF/A compliant");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await ValidationClient.ValidatePdfAAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ValidatePdfAAsync(byte[]) → Base64 JSON → /api/pdf/validation/validate-pdfa
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Bytes_ValidPdf_ReturnsComplianceFields()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidatePdfAAsync(bytes);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.IsValid.Should().BeTrue();
|
||||||
|
result.PdfACompliant.Should().BeFalse();
|
||||||
|
result.PageCount.Should().BeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Bytes_EncryptedPdf_ReturnsEncryptedTrue()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("encryptedPdf.pdf");
|
||||||
|
|
||||||
|
var result = await ValidationClient.ValidatePdfAAsync(bytes);
|
||||||
|
|
||||||
|
result.Encrypted.Should().BeTrue();
|
||||||
|
result.PdfACompliant.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidatePdfA_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await ValidationClient.ValidatePdfAAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
129
DocumentService.Tests.Live/API/SwissQrCodeLiveTests.cs
Normal file
129
DocumentService.Tests.Live/API/SwissQrCodeLiveTests.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using DocumentService.Application.Common.DTOs;
|
||||||
|
using FluentAssertions;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Live tests for Swiss QR Code extraction endpoint.
|
||||||
|
/// Uses SwissQrCodeClient — routes managed by the client.
|
||||||
|
/// Target: http://172.24.12.39:9393/
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Live API")]
|
||||||
|
public class SwissQrCodeLiveTests : LiveApiTestBase
|
||||||
|
{
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ExtractAsync(Stream) → multipart → /api/pdf/qr-code/extract-swiss
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_PdfWithQrCode_ReturnsBillDetails()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await QrCodeClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Bill.Should().NotBeNull("bill data must be extracted");
|
||||||
|
result.RawLines.Should().NotBeEmpty("raw QR payload lines must be present");
|
||||||
|
|
||||||
|
// Swiss QR Bill Standard 2.0 payload always starts with "SPC"
|
||||||
|
result.RawLines.Should().Contain(line => line == "SPC",
|
||||||
|
"first SPC line is mandated by Swiss QR Bill Standard 2.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_PdfWithQrCode_BillHasMandatoryFields()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await QrCodeClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
var bill = result.Bill;
|
||||||
|
bill.Should().NotBeNull();
|
||||||
|
bill!.Account.Should().NotBeNullOrEmpty("IBAN/QR-IBAN is mandatory per standard");
|
||||||
|
bill.Creditor.Should().NotBeNull("Creditor is mandatory");
|
||||||
|
bill.Creditor!.Name.Should().NotBeNullOrEmpty("Creditor name is mandatory");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_PdfWithoutQrCode_Throws404()
|
||||||
|
{
|
||||||
|
// valid.pdf has no QR code
|
||||||
|
using var stream = LoadPdfStream("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () => await QrCodeClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
// Client calls EnsureSuccessStatusCode → HttpRequestException on 404
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"PDF without QR code must return 404");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_EmptyStream_Throws()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Array.Empty<byte>());
|
||||||
|
|
||||||
|
var act = async () => await QrCodeClient.ExtractAsync(stream);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Stream_WithRawFlag_ReturnsRawLines()
|
||||||
|
{
|
||||||
|
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
// raw=true → API returns full unprocessed payload lines
|
||||||
|
var result = await QrCodeClient.ExtractAsync(stream, raw: true);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.RawLines.Should().NotBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// ExtractAsync(byte[]) → Base64 JSON → /api/pdf/qr-code/extract-swiss
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_PdfWithQrCode_ReturnsBillDetails()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await QrCodeClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Bill.Should().NotBeNull();
|
||||||
|
result.RawLines.Should().NotBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_PdfWithQrCode_BillHasMandatoryFields()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("pdfWithSwissQRCode.pdf");
|
||||||
|
|
||||||
|
var result = await QrCodeClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
result.Bill!.Account.Should().NotBeNullOrEmpty();
|
||||||
|
result.Bill.Creditor.Should().NotBeNull();
|
||||||
|
result.Bill.Creditor!.Name.Should().NotBeNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_PdfWithoutQrCode_Throws404()
|
||||||
|
{
|
||||||
|
byte[] bytes = LoadPdfBytes("valid.pdf");
|
||||||
|
|
||||||
|
var act = async () => await QrCodeClient.ExtractAsync(bytes);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>(
|
||||||
|
"PDF without QR code must return 404");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Extract_Bytes_EmptyArray_Throws()
|
||||||
|
{
|
||||||
|
var act = async () => await QrCodeClient.ExtractAsync(Array.Empty<byte>());
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
47
DocumentService.Tests.Live/DocumentService.Tests.Live.csproj
Normal file
47
DocumentService.Tests.Live/DocumentService.Tests.Live.csproj
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="TestData\Pdfs\pdfWithSwissQRCode.pdf" />
|
||||||
|
<None Remove="TestData\Pdfs\valid.pdf" />
|
||||||
|
<None Remove="TestData\Pdfs\pdfWithMoreThanOneAttachment.pdf" />
|
||||||
|
<None Remove="TestData\Pdfs\encryptedPdf.pdf" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="TestData\Pdfs\pdfWithSwissQRCode.pdf" />
|
||||||
|
<EmbeddedResource Include="TestData\Pdfs\valid.pdf" />
|
||||||
|
<EmbeddedResource Include="TestData\Pdfs\pdfWithMoreThanOneAttachment.pdf" />
|
||||||
|
<EmbeddedResource Include="TestData\Pdfs\encryptedPdf.pdf" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DocumentService.Client\DocumentService.Client.csproj" />
|
||||||
|
<ProjectReference Include="..\DocumentService.Application\DocumentService.Application.csproj" />
|
||||||
|
<ProjectReference Include="..\DocumentService.Domain\DocumentService.Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
64
DocumentService.Tests.Live/LiveApiTestBase.cs
Normal file
64
DocumentService.Tests.Live/LiveApiTestBase.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using DocumentService.Client.Clients;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace DocumentService.Tests.Live;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared base for all live API tests.
|
||||||
|
/// Uses the typed DocumentService.Client classes — no hardcoded routes.
|
||||||
|
///
|
||||||
|
/// Live API: http://172.24.12.39:9393/
|
||||||
|
/// Override via LIVE_API_BASE_URL environment variable if needed.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class LiveApiTestBase : IDisposable
|
||||||
|
{
|
||||||
|
public static readonly string BaseUrl =
|
||||||
|
Environment.GetEnvironmentVariable("LIVE_API_BASE_URL")
|
||||||
|
?? "http://172.24.12.39:9393";
|
||||||
|
|
||||||
|
protected readonly HttpClient HttpClient;
|
||||||
|
|
||||||
|
// Typed clients (route-free — routes live inside the client implementations)
|
||||||
|
protected readonly PdfValidationClient ValidationClient;
|
||||||
|
protected readonly PdfAttachmentClient AttachmentClient;
|
||||||
|
protected readonly PdfOperationsClient OperationsClient;
|
||||||
|
protected readonly SwissQrCodeClient QrCodeClient;
|
||||||
|
|
||||||
|
protected LiveApiTestBase()
|
||||||
|
{
|
||||||
|
HttpClient = new HttpClient { BaseAddress = new Uri(BaseUrl) };
|
||||||
|
|
||||||
|
ValidationClient = new PdfValidationClient(HttpClient, NullLogger<PdfValidationClient>.Instance);
|
||||||
|
AttachmentClient = new PdfAttachmentClient(HttpClient, NullLogger<PdfAttachmentClient>.Instance);
|
||||||
|
OperationsClient = new PdfOperationsClient(HttpClient, NullLogger<PdfOperationsClient>.Instance);
|
||||||
|
QrCodeClient = new SwissQrCodeClient(HttpClient, NullLogger<SwissQrCodeClient>.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
// Embedded-resource helpers
|
||||||
|
// ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
protected static byte[] LoadPdfBytes(string fileName)
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
var resourceName = $"DocumentService.Tests.Live.TestData.Pdfs.{fileName}";
|
||||||
|
|
||||||
|
using var stream = assembly.GetManifestResourceStream(resourceName)
|
||||||
|
?? throw new FileNotFoundException(
|
||||||
|
$"Embedded resource not found: '{resourceName}'. " +
|
||||||
|
$"Available: {string.Join(", ", assembly.GetManifestResourceNames())}");
|
||||||
|
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
stream.CopyTo(ms);
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Stream LoadPdfStream(string fileName)
|
||||||
|
{
|
||||||
|
var bytes = LoadPdfBytes(fileName);
|
||||||
|
return new MemoryStream(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => HttpClient.Dispose();
|
||||||
|
}
|
||||||
30
DocumentService.Tests.Live/TestData/Pdfs/encryptedPdf.pdf
Normal file
30
DocumentService.Tests.Live/TestData/Pdfs/encryptedPdf.pdf
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
%PDF-1.4
|
||||||
|
1 0 obj
|
||||||
|
<< /Type /Catalog /Pages 2 0 R >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
<< /Filter /Standard /V 2 /R 3 /Length 128 /P -3904 /O (OwnerHash12345678901234567890123456) /U (UserHash12345678901234567890123456) >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
xref
|
||||||
|
0 5
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000009 00000 n
|
||||||
|
0000000058 00000 n
|
||||||
|
0000000115 00000 n
|
||||||
|
0000000206 00000 n
|
||||||
|
|
||||||
|
trailer
|
||||||
|
<< /Size 5 /Root 1 0 R /Encrypt 4 0 R >>
|
||||||
|
startxref
|
||||||
|
370
|
||||||
|
%%EOF
|
||||||
Binary file not shown.
BIN
DocumentService.Tests.Live/TestData/Pdfs/pdfWithSwissQRCode.pdf
Normal file
BIN
DocumentService.Tests.Live/TestData/Pdfs/pdfWithSwissQRCode.pdf
Normal file
Binary file not shown.
BIN
DocumentService.Tests.Live/TestData/Pdfs/valid.pdf
Normal file
BIN
DocumentService.Tests.Live/TestData/Pdfs/valid.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user