using DocumentService.Client.Clients; using DocumentService.Client.Interfaces; using DocumentService.Client.Models.Requests; using DocumentService.Client.Models.ValueObjects; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using System.Net; using System.Net.Http; using System.Text.Json; namespace DocumentService.Tests.Unit.Client; /// /// Unit tests for . /// All tests use a fake — no real HTTP calls are made. /// public class PdfOperationsClientTests { // ?? helpers ????????????????????????????????????????????????????????????? private static (PdfOperationsClient client, MockHttpMessageHandler handler) BuildBytes(byte[] bytes = null!) { var handler = MockHttpMessageHandler.ReturningBytes(bytes ?? "merged-pdf"u8.ToArray(), "application/pdf"); var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; var client = new PdfOperationsClient(httpClient, NullLogger.Instance); return (client, handler); } private static byte[] FakePdfBytes() => "fake-pdf-content"u8.ToArray(); private static AddAnnotationBase64Request FakeAnnotationRequest() => new() { Base64Pdf = string.Empty, AnnotationType = AnnotationType.TextMarkup, PageNumber = 1, X1 = 10, Y1 = 20, Width = 100, Height = 30, Color = "FFFF00", TextMarkupStyle = TextMarkupStyle.Highlight, Origin = AnnotationOrigin.TopLeft }; private static AddStampBase64Request FakeStampRequest() => new() { Base64Pdf = string.Empty, StampType = StampType.Text, X = 100, Y = 50, Text = "CONFIDENTIAL", FontSize = 24, Color = "FF0000", Opacity = 0.5, Placement = StampPlacement.Foreground, Origin = AnnotationOrigin.BottomLeft }; // ?? MergeAsync (Streams) ????????????????????????????????????????????????? [Fact] public async Task MergeAsync_Streams_SendsMultipartWithAllFiles() { // Arrange var (client, handler) = BuildBytes(); var streams = new List { new MemoryStream(FakePdfBytes()), new MemoryStream(FakePdfBytes()) }; // Act using var result = await client.MergeAsync(streams); // Assert result.Should().NotBeNull(); handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/operations/merge"); handler.LastRequest.Content.Should().BeOfType(); foreach (var s in streams) s.Dispose(); } [Fact] public async Task MergeAsync_Streams_ReturnsMergedPdfStream() { // Arrange var expectedBytes = "merged-content"u8.ToArray(); var (client, _) = BuildBytes(expectedBytes); // Act using var result = await client.MergeAsync(new[] { new MemoryStream(FakePdfBytes()), new MemoryStream(FakePdfBytes()) }); var actualBytes = await result.ReadAllBytesAsync(); // Assert actualBytes.Should().BeEquivalentTo(expectedBytes); } // ?? MergeAsync (byte[][]) ???????????????????????????????????????????????? [Fact] public async Task MergeAsync_ByteArrays_SendsJsonWithBase64List() { // Arrange var (client, handler) = BuildBytes(); // Act using var result = await client.MergeAsync(new[] { FakePdfBytes(), FakePdfBytes() }); // Assert handler.LastRequest!.Content.Should().NotBeNull(); handler.LastRequest.Content!.Headers.ContentType!.MediaType.Should().Be("application/json"); var body = await handler.LastRequest.Content!.ReadAsStringAsync(); var doc = JsonDocument.Parse(body); doc.RootElement.GetProperty("base64Pdfs").GetArrayLength().Should().Be(2); } [Fact] public async Task MergeAsync_ByteArrays_WithPageRanges_IncludesPageRangesInJson() { // Arrange var (client, handler) = BuildBytes(); var pageRanges = new List { "1-2", null }; // Act await client.MergeAsync(new[] { FakePdfBytes(), FakePdfBytes() }, pageRanges); // Assert handler.LastRequest!.Content.Should().NotBeNull(); var body = await handler.LastRequest!.Content!.ReadAsStringAsync(); var doc = JsonDocument.Parse(body); doc.RootElement.GetProperty("pageRanges").GetArrayLength().Should().Be(2); } // ?? AnnotateAsync (Stream) ???????????????????????????????????????????????? [Fact] public async Task AnnotateAsync_Stream_SendsMultipartWithAnnotationFields() { // Arrange var (client, handler) = BuildBytes(); var request = FakeAnnotationRequest(); // Act using var result = await client.AnnotateAsync(new MemoryStream(FakePdfBytes()), request); // Assert result.Should().NotBeNull(); handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/operations/annotate"); handler.LastRequest.Content.Should().BeOfType(); } // ?? AnnotateAsync (byte[]) ???????????????????????????????????????????????? [Fact] public async Task AnnotateAsync_Bytes_SendsJsonWithBase64Pdf() { // Arrange var (client, handler) = BuildBytes(); var request = FakeAnnotationRequest(); // Act await client.AnnotateAsync(FakePdfBytes(), request); // Assert handler.LastRequest!.Content.Should().NotBeNull(); handler.LastRequest.Content!.Headers.ContentType!.MediaType.Should().Be("application/json"); var body = await handler.LastRequest.Content!.ReadAsStringAsync(); var doc = JsonDocument.Parse(body); doc.RootElement.GetProperty("base64Pdf").GetString().Should().NotBeNullOrEmpty(); // annotationType serializes as integer by default (TextMarkup = 0) doc.RootElement.GetProperty("annotationType").GetInt32().Should().Be((int)AnnotationType.TextMarkup); } // ?? StampAsync (Stream) ??????????????????????????????????????????????????? [Fact] public async Task StampAsync_Stream_SendsMultipartWithStampFields() { // Arrange var (client, handler) = BuildBytes(); var request = FakeStampRequest(); // Act using var result = await client.StampAsync(new MemoryStream(FakePdfBytes()), request); // Assert result.Should().NotBeNull(); handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/operations/stamp"); handler.LastRequest.Content.Should().BeOfType(); } // ?? StampAsync (byte[]) ??????????????????????????????????????????????????? [Fact] public async Task StampAsync_Bytes_SendsJsonWithBase64Pdf() { // Arrange var (client, handler) = BuildBytes(); var request = FakeStampRequest(); // Act await client.StampAsync(FakePdfBytes(), request); // Assert handler.LastRequest!.Content.Should().NotBeNull(); handler.LastRequest.Content!.Headers.ContentType!.MediaType.Should().Be("application/json"); var body = await handler.LastRequest.Content!.ReadAsStringAsync(); var doc = JsonDocument.Parse(body); doc.RootElement.GetProperty("base64Pdf").GetString().Should().NotBeNullOrEmpty(); // stampType serializes as integer by default (Text = 0) doc.RootElement.GetProperty("stampType").GetInt32().Should().Be((int)StampType.Text); } // ?? HTTP error propagation ??????????????????????????????????????????????? [Fact] public async Task MergeAsync_WhenApiReturns400_ThrowsHttpRequestException() { // Arrange var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.BadRequest); var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; var client = new PdfOperationsClient(httpClient, NullLogger.Instance); // Act & Assert await client.Invoking(c => c.MergeAsync(new[] { new MemoryStream(FakePdfBytes()), new MemoryStream(FakePdfBytes()) })) .Should().ThrowAsync(); } } // ?? local helper extension ??????????????????????????????????????????????????? file static class StreamHelper { public static async Task ReadAllBytesAsync(this Stream stream) { using var ms = new MemoryStream(); await stream.CopyToAsync(ms); return ms.ToArray(); } }