using DocumentService.Application.Common.DTOs.Requests; using DocumentService.Domain.Models.ValueObjects; using FluentAssertions; namespace DocumentService.Tests.Live.API; /// /// Live tests for PDF operations: merge, annotate, stamp. /// Uses PdfOperationsClient — routes managed by the client. /// Target: http://172.24.12.39:9393/ /// [Collection("Live API")] public class PdfOperationsLiveTests : LiveApiTestBase { // ══════════════════════════════════════════════════════════════ // MERGE — MergeAsync(IEnumerable) → 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( "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( "corrupted PDF should return 500"); } // ══════════════════════════════════════════════════════════════ // MERGE — MergeAsync(IEnumerable) → 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(); } [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( "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( "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( "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( "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( "Text stamp without text should return 400"); } }