using DocumentService.Client.Clients; using DocumentService.Client.Interfaces; using DocumentService.Client.Models.Requests; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using System.IO.Compression; using System.Net; using System.Net.Http; using System.Text; 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 PdfAttachmentClientTests { // ?? helpers ????????????????????????????????????????????????????????????? private static (PdfAttachmentClient client, MockHttpMessageHandler handler) BuildJson(T body) { var handler = MockHttpMessageHandler.ReturningJson(body); var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; var client = new PdfAttachmentClient(httpClient, NullLogger.Instance); return (client, handler); } private static (PdfAttachmentClient client, MockHttpMessageHandler handler) BuildBytes(byte[] bytes, string mediaType = "application/zip") { var handler = MockHttpMessageHandler.ReturningBytes(bytes, mediaType); var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; var client = new PdfAttachmentClient(httpClient, NullLogger.Instance); return (client, handler); } private static byte[] FakePdfBytes() => "fake-pdf-content"u8.ToArray(); /// Builds a minimal valid ZIP containing the given entries. private static byte[] BuildZip(Dictionary entries) { using var ms = new MemoryStream(); using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) { foreach (var (name, content) in entries) { var entry = archive.CreateEntry(name); using var writer = new StreamWriter(entry.Open()); writer.Write(content); } } return ms.ToArray(); } // ?? CheckAttachmentsAsync (Stream) ??????????????????????????????????????? [Fact] public async Task CheckAttachmentsAsync_Stream_SendsMultipartPost() { // Arrange var expected = new AttachmentCheckResult { HasAttachments = true, AttachmentCount = 1, Attachments = new List { new() { FileName = "factur-x.xml", MimeType = "application/xml", Size = 512 } } }; var (client, handler) = BuildJson(expected); // Act var result = await client.CheckAttachmentsAsync(new MemoryStream(FakePdfBytes())); // Assert result.HasAttachments.Should().BeTrue(); result.AttachmentCount.Should().Be(1); result.Attachments[0].FileName.Should().Be("factur-x.xml"); handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/attachments/check"); handler.LastRequest.Content.Should().BeOfType(); } [Fact] public async Task CheckAttachmentsAsync_Stream_PdfWithNoAttachments_ReturnsFalse() { // Arrange var expected = new AttachmentCheckResult { HasAttachments = false, AttachmentCount = 0 }; var (client, _) = BuildJson(expected); // Act var result = await client.CheckAttachmentsAsync(new MemoryStream(FakePdfBytes())); // Assert result.HasAttachments.Should().BeFalse(); result.AttachmentCount.Should().Be(0); result.Attachments.Should().BeEmpty(); } // ?? CheckAttachmentsAsync (byte[]) ??????????????????????????????????????? [Fact] public async Task CheckAttachmentsAsync_Bytes_SendsJsonWithBase64() { // Arrange var expected = new AttachmentCheckResult { HasAttachments = false }; var (client, handler) = BuildJson(expected); // Act await client.CheckAttachmentsAsync(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("base64Pdf").GetString().Should().NotBeNullOrEmpty(); } // ?? ExtractAttachmentsAsync (Stream) — ZIP unzip ????????????????????????? [Fact] public async Task ExtractAttachmentsAsync_Stream_UnzipsAndReturnsDictionary() { // Arrange var zipBytes = BuildZip(new Dictionary { ["factur-x.xml"] = "test", ["readme.txt"] = "Hello World" }); var (client, handler) = BuildBytes(zipBytes); // Act var result = await client.ExtractAttachmentsAsync(new MemoryStream(FakePdfBytes())); // Assert result.Should().HaveCount(2); result.Should().ContainKey("factur-x.xml"); result.Should().ContainKey("readme.txt"); using var xmlStream = result["factur-x.xml"]; var xmlContent = await new StreamReader(xmlStream).ReadToEndAsync(); xmlContent.Should().Be("test"); handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/attachments/extract"); // Cleanup foreach (var s in result.Values) s.Dispose(); } [Fact] public async Task ExtractAttachmentsAsync_Stream_WithSingleEntry_ReturnsOneItem() { // Arrange var zipBytes = BuildZip(new Dictionary { ["data.xml"] = "" }); var (client, _) = BuildBytes(zipBytes); // Act var result = await client.ExtractAttachmentsAsync(new MemoryStream(FakePdfBytes())); // Assert result.Should().HaveCount(1); result.Should().ContainKey("data.xml"); foreach (var s in result.Values) s.Dispose(); } // ?? ExtractAttachmentsAsync (byte[]) — ZIP unzip ????????????????????????? [Fact] public async Task ExtractAttachmentsAsync_Bytes_SendsJsonAndUnzips() { // Arrange var zipBytes = BuildZip(new Dictionary { ["invoice.xml"] = "" }); var (client, handler) = BuildBytes(zipBytes); // Act var result = await client.ExtractAttachmentsAsync(FakePdfBytes()); // Assert result.Should().ContainKey("invoice.xml"); handler.LastRequest!.Content.Should().NotBeNull(); handler.LastRequest.Content!.Headers.ContentType!.MediaType.Should().Be("application/json"); foreach (var s in result.Values) s.Dispose(); } // ?? HTTP error propagation ??????????????????????????????????????????????? [Fact] public async Task CheckAttachmentsAsync_WhenApiReturns404_ThrowsHttpRequestException() { // Arrange var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.NotFound); var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; var client = new PdfAttachmentClient(httpClient, NullLogger.Instance); // Act & Assert await client.Invoking(c => c.CheckAttachmentsAsync(new MemoryStream(FakePdfBytes()))) .Should().ThrowAsync(); } [Fact] public async Task ExtractAttachmentsAsync_WhenApiReturns500_ThrowsHttpRequestException() { // Arrange var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.InternalServerError); var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; var client = new PdfAttachmentClient(httpClient, NullLogger.Instance); // Act & Assert await client.Invoking(c => c.ExtractAttachmentsAsync(new MemoryStream(FakePdfBytes()))) .Should().ThrowAsync(); } }