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.
533 lines
18 KiB
C#
533 lines
18 KiB
C#
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");
|
|
}
|
|
}
|