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();
}
}