diff --git a/DocumentService.Tests.Live/API/PdfAttachmentLiveTests.cs b/DocumentService.Tests.Live/API/PdfAttachmentLiveTests.cs new file mode 100644 index 0000000..f92919f --- /dev/null +++ b/DocumentService.Tests.Live/API/PdfAttachmentLiveTests.cs @@ -0,0 +1,235 @@ +using DocumentService.Application.Common.DTOs; +using FluentAssertions; + +namespace DocumentService.Tests.Live.API; + +/// +/// Live tests for PDF attachment endpoints (check + extract). +/// Uses PdfAttachmentClient — routes managed by the client. +/// Target: http://172.24.12.39:9393/ +/// +[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()); + + var act = async () => await AttachmentClient.CheckAsync(stream); + + await act.Should().ThrowAsync(); + } + + [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( + "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()); + + await act.Should().ThrowAsync(); + } + + // ────────────────────────────────────────────────────────────── + // 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()); + + var act = async () => await AttachmentClient.ExtractAsync(stream); + + await act.Should().ThrowAsync(); + } + + [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(); + } + + // ────────────────────────────────────────────────────────────── + // 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()); + + await act.Should().ThrowAsync(); + } +} diff --git a/DocumentService.Tests.Live/API/PdfOperationsLiveTests.cs b/DocumentService.Tests.Live/API/PdfOperationsLiveTests.cs new file mode 100644 index 0000000..d676f88 --- /dev/null +++ b/DocumentService.Tests.Live/API/PdfOperationsLiveTests.cs @@ -0,0 +1,532 @@ +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"); + } +} diff --git a/DocumentService.Tests.Live/API/PdfValidationLiveTests.cs b/DocumentService.Tests.Live/API/PdfValidationLiveTests.cs new file mode 100644 index 0000000..e10f427 --- /dev/null +++ b/DocumentService.Tests.Live/API/PdfValidationLiveTests.cs @@ -0,0 +1,196 @@ +using DocumentService.Application.Common.DTOs; +using FluentAssertions; + +namespace DocumentService.Tests.Live.API; + +/// +/// Live tests for PDF validation endpoints. +/// Uses PdfValidationClient — routes are managed by the client, not the test. +/// Target: http://172.24.12.39:9393/ +/// +[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()); + + var act = async () => await ValidationClient.ValidateAsync(stream); + + await act.Should().ThrowAsync( + "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( + "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()); + + await act.Should().ThrowAsync( + "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()); + + var act = async () => await ValidationClient.ValidatePdfAAsync(stream); + + await act.Should().ThrowAsync(); + } + + // ────────────────────────────────────────────────────────────── + // 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()); + + await act.Should().ThrowAsync(); + } +} diff --git a/DocumentService.Tests.Live/API/SwissQrCodeLiveTests.cs b/DocumentService.Tests.Live/API/SwissQrCodeLiveTests.cs new file mode 100644 index 0000000..2473316 --- /dev/null +++ b/DocumentService.Tests.Live/API/SwissQrCodeLiveTests.cs @@ -0,0 +1,129 @@ +using DocumentService.Application.Common.DTOs; +using FluentAssertions; + +namespace DocumentService.Tests.Live.API; + +/// +/// Live tests for Swiss QR Code extraction endpoint. +/// Uses SwissQrCodeClient — routes managed by the client. +/// Target: http://172.24.12.39:9393/ +/// +[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( + "PDF without QR code must return 404"); + } + + [Fact] + public async Task Extract_Stream_EmptyStream_Throws() + { + using var stream = new MemoryStream(Array.Empty()); + + var act = async () => await QrCodeClient.ExtractAsync(stream); + + await act.Should().ThrowAsync(); + } + + [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( + "PDF without QR code must return 404"); + } + + [Fact] + public async Task Extract_Bytes_EmptyArray_Throws() + { + var act = async () => await QrCodeClient.ExtractAsync(Array.Empty()); + + await act.Should().ThrowAsync(); + } +} diff --git a/DocumentService.Tests.Live/DocumentService.Tests.Live.csproj b/DocumentService.Tests.Live/DocumentService.Tests.Live.csproj new file mode 100644 index 0000000..939da9b --- /dev/null +++ b/DocumentService.Tests.Live/DocumentService.Tests.Live.csproj @@ -0,0 +1,47 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + diff --git a/DocumentService.Tests.Live/LiveApiTestBase.cs b/DocumentService.Tests.Live/LiveApiTestBase.cs new file mode 100644 index 0000000..f61a10d --- /dev/null +++ b/DocumentService.Tests.Live/LiveApiTestBase.cs @@ -0,0 +1,64 @@ +using DocumentService.Client.Clients; +using Microsoft.Extensions.Logging.Abstractions; +using System.Reflection; + +namespace DocumentService.Tests.Live; + +/// +/// 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. +/// +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.Instance); + AttachmentClient = new PdfAttachmentClient(HttpClient, NullLogger.Instance); + OperationsClient = new PdfOperationsClient(HttpClient, NullLogger.Instance); + QrCodeClient = new SwissQrCodeClient(HttpClient, NullLogger.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(); +} diff --git a/DocumentService.Tests.Live/TestData/Pdfs/encryptedPdf.pdf b/DocumentService.Tests.Live/TestData/Pdfs/encryptedPdf.pdf new file mode 100644 index 0000000..cd36c06 --- /dev/null +++ b/DocumentService.Tests.Live/TestData/Pdfs/encryptedPdf.pdf @@ -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 \ No newline at end of file diff --git a/DocumentService.Tests.Live/TestData/Pdfs/pdfWithMoreThanOneAttachment.pdf b/DocumentService.Tests.Live/TestData/Pdfs/pdfWithMoreThanOneAttachment.pdf new file mode 100644 index 0000000..15e173e Binary files /dev/null and b/DocumentService.Tests.Live/TestData/Pdfs/pdfWithMoreThanOneAttachment.pdf differ diff --git a/DocumentService.Tests.Live/TestData/Pdfs/pdfWithSwissQRCode.pdf b/DocumentService.Tests.Live/TestData/Pdfs/pdfWithSwissQRCode.pdf new file mode 100644 index 0000000..9f57af2 Binary files /dev/null and b/DocumentService.Tests.Live/TestData/Pdfs/pdfWithSwissQRCode.pdf differ diff --git a/DocumentService.Tests.Live/TestData/Pdfs/valid.pdf b/DocumentService.Tests.Live/TestData/Pdfs/valid.pdf new file mode 100644 index 0000000..e8974ea Binary files /dev/null and b/DocumentService.Tests.Live/TestData/Pdfs/valid.pdf differ