diff --git a/DocumentOperator.Tests/Unit/Client/MockHttpMessageHandler.cs b/DocumentOperator.Tests/Unit/Client/MockHttpMessageHandler.cs new file mode 100644 index 0000000..230beb7 --- /dev/null +++ b/DocumentOperator.Tests/Unit/Client/MockHttpMessageHandler.cs @@ -0,0 +1,56 @@ +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; + +namespace DocumentService.Tests.Unit.Client; + +/// +/// Reusable fake for unit-testing HTTP clients. +/// Captures the outgoing request and returns the configured response. +/// +internal sealed class MockHttpMessageHandler : HttpMessageHandler +{ + private readonly HttpResponseMessage _response; + + /// The last request that was sent through this handler. + public HttpRequestMessage? LastRequest { get; private set; } + + public MockHttpMessageHandler(HttpResponseMessage response) + { + _response = response; + } + + // ?? convenience factories ???????????????????????????????????????????????? + + /// Creates a handler that returns 200 OK with a JSON-serialised body. + public static MockHttpMessageHandler ReturningJson(T body, HttpStatusCode status = HttpStatusCode.OK) + { + var json = JsonSerializer.Serialize(body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + var response = new HttpResponseMessage(status) + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + return new MockHttpMessageHandler(response); + } + + /// Creates a handler that returns 200 OK with raw bytes as the body. + public static MockHttpMessageHandler ReturningBytes(byte[] bytes, string mediaType = "application/octet-stream", HttpStatusCode status = HttpStatusCode.OK) + { + var response = new HttpResponseMessage(status) + { + Content = new ByteArrayContent(bytes) { Headers = { ContentType = new(mediaType) } } + }; + return new MockHttpMessageHandler(response); + } + + /// Creates a handler that returns the given status code with no body. + public static MockHttpMessageHandler ReturningStatus(HttpStatusCode status) + => new(new HttpResponseMessage(status)); + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + LastRequest = request; + return Task.FromResult(_response); + } +} diff --git a/DocumentOperator.Tests/Unit/Client/PdfAttachmentClientTests.cs b/DocumentOperator.Tests/Unit/Client/PdfAttachmentClientTests.cs new file mode 100644 index 0000000..81c9d65 --- /dev/null +++ b/DocumentOperator.Tests/Unit/Client/PdfAttachmentClientTests.cs @@ -0,0 +1,221 @@ +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(); + } +} diff --git a/DocumentOperator.Tests/Unit/Client/PdfOperationsClientTests.cs b/DocumentOperator.Tests/Unit/Client/PdfOperationsClientTests.cs new file mode 100644 index 0000000..9288c15 --- /dev/null +++ b/DocumentOperator.Tests/Unit/Client/PdfOperationsClientTests.cs @@ -0,0 +1,236 @@ +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(); + } +} diff --git a/DocumentOperator.Tests/Unit/Client/PdfValidationClientTests.cs b/DocumentOperator.Tests/Unit/Client/PdfValidationClientTests.cs new file mode 100644 index 0000000..dedca82 --- /dev/null +++ b/DocumentOperator.Tests/Unit/Client/PdfValidationClientTests.cs @@ -0,0 +1,168 @@ +using DocumentService.Client.Clients; +using DocumentService.Client.Interfaces; +using DocumentService.Client.Models.Requests; +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 PdfValidationClientTests +{ + // ?? helpers ????????????????????????????????????????????????????????????? + + private static (PdfValidationClient client, MockHttpMessageHandler handler) Build(T responseBody) + { + var handler = MockHttpMessageHandler.ReturningJson(responseBody); + var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; + var client = new PdfValidationClient(httpClient, NullLogger.Instance); + return (client, handler); + } + + private static byte[] FakePdfBytes() => "fake-pdf-content"u8.ToArray(); + + // ?? ValidatePdfAsync (Stream) ???????????????????????????????????????????? + + [Fact] + public async Task ValidatePdfAsync_Stream_SendsMultipartPost() + { + // Arrange + var expected = new PdfValidationResult { PageCount = 3, PdfVersion = "1.7", FileSizeBytes = 2048 }; + var (client, handler) = Build(expected); + + // Act + var result = await client.ValidatePdfAsync(new MemoryStream(FakePdfBytes())); + + // Assert + result.PageCount.Should().Be(3); + result.PdfVersion.Should().Be("1.7"); + handler.LastRequest!.Method.Should().Be(HttpMethod.Post); + handler.LastRequest.RequestUri!.PathAndQuery.Should().Be("/api/pdf/validation/validate"); + handler.LastRequest.Content.Should().BeOfType(); + } + + [Fact] + public async Task ValidatePdfAsync_Stream_ThrowsWhenApiReturnsNull() + { + // Arrange — API returns JSON null + var handler = MockHttpMessageHandler.ReturningJson(null); + var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; + var client = new PdfValidationClient(httpClient, NullLogger.Instance); + + // Act & Assert + await client.Invoking(c => c.ValidatePdfAsync(new MemoryStream(FakePdfBytes()))) + .Should().ThrowAsync(); + } + + // ?? ValidatePdfAsync (byte[]) ???????????????????????????????????????????? + + [Fact] + public async Task ValidatePdfAsync_Bytes_SendsJsonWithBase64() + { + // Arrange + var expected = new PdfValidationResult { PageCount = 1, IsEncrypted = false }; + var (client, handler) = Build(expected); + + // Act + var result = await client.ValidatePdfAsync(FakePdfBytes()); + + // Assert + result.PageCount.Should().Be(1); + 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(); + } + + // ?? ValidatePdfAAsync (Stream) ??????????????????????????????????????????? + + [Fact] + public async Task ValidatePdfAAsync_Stream_SendsMultipartPost() + { + // Arrange + var expected = new PdfAValidationResult { IsValid = true, PdfAVersion = "PDF/A-3b", PageCount = 2 }; + var (client, handler) = Build(expected); + + // Act + var result = await client.ValidatePdfAAsync(new MemoryStream(FakePdfBytes())); + + // Assert + result.IsValid.Should().BeTrue(); + result.PdfAVersion.Should().Be("PDF/A-3b"); + handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/validation/validate-pdfa"); + handler.LastRequest.Content.Should().BeOfType(); + } + + [Fact] + public async Task ValidatePdfAAsync_Stream_WithErrors_ReturnsErrors() + { + // Arrange + var expected = new PdfAValidationResult + { + IsValid = false, + Errors = new List { "Missing embedded font", "Encryption not allowed" } + }; + var (client, _) = Build(expected); + + // Act + var result = await client.ValidatePdfAAsync(new MemoryStream(FakePdfBytes())); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().HaveCount(2).And.Contain("Missing embedded font"); + } + + // ?? ValidatePdfAAsync (byte[]) ??????????????????????????????????????????? + + [Fact] + public async Task ValidatePdfAAsync_Bytes_SendsJson() + { + // Arrange + var expected = new PdfAValidationResult { IsValid = true }; + var (client, handler) = Build(expected); + + // Act + await client.ValidatePdfAAsync(FakePdfBytes()); + + // Assert + handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/validation/validate-pdfa"); + handler.LastRequest.Content.Should().NotBeNull(); + handler.LastRequest.Content!.Headers.ContentType!.MediaType.Should().Be("application/json"); + } + + // ?? HTTP error propagation ??????????????????????????????????????????????? + + [Fact] + public async Task ValidatePdfAsync_WhenApiReturns400_ThrowsHttpRequestException() + { + // Arrange + var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.BadRequest); + var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; + var client = new PdfValidationClient(httpClient, NullLogger.Instance); + + // Act & Assert + await client.Invoking(c => c.ValidatePdfAsync(new MemoryStream(FakePdfBytes()))) + .Should().ThrowAsync(); + } + + [Fact] + public async Task ValidatePdfAsync_WhenApiReturns500_ThrowsHttpRequestException() + { + // Arrange + var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.InternalServerError); + var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; + var client = new PdfValidationClient(httpClient, NullLogger.Instance); + + // Act & Assert + await client.Invoking(c => c.ValidatePdfAsync(new MemoryStream(FakePdfBytes()))) + .Should().ThrowAsync(); + } +} diff --git a/DocumentOperator.Tests/Unit/Client/SwissQrCodeClientTests.cs b/DocumentOperator.Tests/Unit/Client/SwissQrCodeClientTests.cs new file mode 100644 index 0000000..d252a29 --- /dev/null +++ b/DocumentOperator.Tests/Unit/Client/SwissQrCodeClientTests.cs @@ -0,0 +1,123 @@ +using DocumentService.Client.Clients; +using DocumentService.Client.Interfaces; +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 SwissQrCodeClientTests +{ + // ?? helpers ????????????????????????????????????????????????????????????? + + private static (SwissQrCodeClient client, MockHttpMessageHandler handler) Build(T body) + { + var handler = MockHttpMessageHandler.ReturningJson(body); + var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; + var client = new SwissQrCodeClient(httpClient, NullLogger.Instance); + return (client, handler); + } + + private static byte[] FakePdfBytes() => "fake-pdf-content"u8.ToArray(); + + // ?? ExtractSwissQrCodeAsync (Stream) — parsed Bill ??????????????????????? + + [Fact] + public async Task ExtractSwissQrCodeAsync_Stream_ParsedMode_SendsMultipartToCorrectEndpoint() + { + // Arrange + var expected = new SwissQrCodeExtractionResult { Bill = new { Iban = "CH93-0076-2011-6238-5295-7" } }; + var (client, handler) = Build(expected); + + // Act + var result = await client.ExtractSwissQrCodeAsync(new MemoryStream(FakePdfBytes()), raw: false); + + // Assert + result.Should().NotBeNull(); + handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/qr-code/extract-swiss?raw=False"); + handler.LastRequest.Content.Should().BeOfType(); + } + + [Fact] + public async Task ExtractSwissQrCodeAsync_Stream_RawMode_SendsRawFlagInUrl() + { + // Arrange + var expected = new SwissQrCodeExtractionResult { RawLines = new List { "SPC", "0200", "1" } }; + var (client, handler) = Build(expected); + + // Act + var result = await client.ExtractSwissQrCodeAsync(new MemoryStream(FakePdfBytes()), raw: true); + + // Assert + result.RawLines.Should().HaveCount(3).And.StartWith("SPC"); + handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/qr-code/extract-swiss?raw=True"); + } + + // ?? ExtractSwissQrCodeAsync (byte[]) ????????????????????????????????????? + + [Fact] + public async Task ExtractSwissQrCodeAsync_Bytes_SendsJsonWithBase64() + { + // Arrange + var expected = new SwissQrCodeExtractionResult(); + var (client, handler) = Build(expected); + + // Act + await client.ExtractSwissQrCodeAsync(FakePdfBytes(), raw: false); + + // 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(); + } + + [Fact] + public async Task ExtractSwissQrCodeAsync_Bytes_RawMode_IncludesRawFlagInUrl() + { + // Arrange + var expected = new SwissQrCodeExtractionResult { RawLines = new List { "SPC" } }; + var (client, handler) = Build(expected); + + // Act + await client.ExtractSwissQrCodeAsync(FakePdfBytes(), raw: true); + + // Assert + handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/qr-code/extract-swiss?raw=True"); + } + + // ?? HTTP error propagation ??????????????????????????????????????????????? + + [Fact] + public async Task ExtractSwissQrCodeAsync_WhenApiReturns404_ThrowsHttpRequestException() + { + // Arrange + var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.NotFound); + var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; + var client = new SwissQrCodeClient(httpClient, NullLogger.Instance); + + // Act & Assert + await client.Invoking(c => c.ExtractSwissQrCodeAsync(new MemoryStream(FakePdfBytes()))) + .Should().ThrowAsync(); + } + + [Fact] + public async Task ExtractSwissQrCodeAsync_WhenApiReturns500_ThrowsHttpRequestException() + { + // Arrange + var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.InternalServerError); + var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; + var client = new SwissQrCodeClient(httpClient, NullLogger.Instance); + + // Act & Assert + await client.Invoking(c => c.ExtractSwissQrCodeAsync(FakePdfBytes())) + .Should().ThrowAsync(); + } +} diff --git a/DocumentOperator.Tests/Unit/Client/ZugferdClientTests.cs b/DocumentOperator.Tests/Unit/Client/ZugferdClientTests.cs new file mode 100644 index 0000000..cc852c0 --- /dev/null +++ b/DocumentOperator.Tests/Unit/Client/ZugferdClientTests.cs @@ -0,0 +1,199 @@ +using DocumentService.Client.Clients; +using DocumentService.Client.Interfaces; +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 ZugferdClientTests +{ + // ?? helpers ????????????????????????????????????????????????????????????? + + private static (ZugferdClient client, MockHttpMessageHandler handler) BuildJson(T body) + { + var handler = MockHttpMessageHandler.ReturningJson(body); + var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; + var client = new ZugferdClient(httpClient, NullLogger.Instance); + return (client, handler); + } + + private static (ZugferdClient client, MockHttpMessageHandler handler) BuildBytes(byte[] bytes, string mediaType = "application/xml") + { + var handler = MockHttpMessageHandler.ReturningBytes(bytes, mediaType); + var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; + var client = new ZugferdClient(httpClient, NullLogger.Instance); + return (client, handler); + } + + private static byte[] FakePdfBytes() => "fake-pdf-content"u8.ToArray(); + private static byte[] FakeXmlBytes() => "test"u8.ToArray(); + + // ?? HasZugferdAsync (Stream) ????????????????????????????????????????????? + + [Fact] + public async Task HasZugferdAsync_Stream_WhenZugferdPresent_ReturnsTrue() + { + // Arrange + var expected = new ZugferdCheckResult { HasZugferd = true, Version = "2.1", Profile = "EN 16931" }; + var (client, handler) = BuildJson(expected); + + // Act + var result = await client.HasZugferdAsync(new MemoryStream(FakePdfBytes())); + + // Assert + result.HasZugferd.Should().BeTrue(); + result.Version.Should().Be("2.1"); + result.Profile.Should().Be("EN 16931"); + handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/zugferd/has-zugferd"); + handler.LastRequest.Content.Should().BeOfType(); + } + + [Fact] + public async Task HasZugferdAsync_Stream_WhenNoZugferd_ReturnsFalse() + { + // Arrange + var expected = new ZugferdCheckResult { HasZugferd = false }; + var (client, _) = BuildJson(expected); + + // Act + var result = await client.HasZugferdAsync(new MemoryStream(FakePdfBytes())); + + // Assert + result.HasZugferd.Should().BeFalse(); + result.Version.Should().BeNull(); + } + + // ?? HasZugferdAsync (byte[]) ????????????????????????????????????????????? + + [Fact] + public async Task HasZugferdAsync_Bytes_SendsJsonWithBase64() + { + // Arrange + var expected = new ZugferdCheckResult { HasZugferd = true }; + var (client, handler) = BuildJson(expected); + + // Act + await client.HasZugferdAsync(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(); + } + + // ?? ExtractZugferdAsync (Stream) — raw XML stream ????????????????????????? + + [Fact] + public async Task ExtractZugferdAsync_Stream_SendsMultipartWithAsFileTrue() + { + // Arrange + var (client, handler) = BuildBytes(FakeXmlBytes()); + + // Act + using var result = await client.ExtractZugferdAsync(new MemoryStream(FakePdfBytes())); + + // Assert + result.Should().NotBeNull(); + handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/zugferd/extract?asFile=true"); + handler.LastRequest.Content.Should().BeOfType(); + } + + [Fact] + public async Task ExtractZugferdAsync_Stream_ReturnsXmlContent() + { + // Arrange + var xmlBytes = ""u8.ToArray(); + var (client, _) = BuildBytes(xmlBytes); + + // Act + using var result = await client.ExtractZugferdAsync(new MemoryStream(FakePdfBytes())); + var content = await new StreamReader(result).ReadToEndAsync(); + + // Assert + content.Should().Be(""); + } + + // ?? ExtractZugferdAsync (byte[]) ????????????????????????????????????????? + + [Fact] + public async Task ExtractZugferdAsync_Bytes_SendsJsonWithFormatFile() + { + // Arrange + var (client, handler) = BuildBytes(FakeXmlBytes()); + + // Act + using var result = await client.ExtractZugferdAsync(FakePdfBytes()); + + // Assert + handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/zugferd/extract?format=file"); + handler.LastRequest.Content.Should().NotBeNull(); + handler.LastRequest.Content!.Headers.ContentType!.MediaType.Should().Be("application/json"); + } + + // ?? ExtractZugferdAsResultAsync (Stream) ????????????????????????????????? + + [Fact] + public async Task ExtractZugferdAsResultAsync_Stream_ReturnsStructuredResult() + { + // Arrange + var expected = new ZugferdExtractionResult + { + FileName = "factur-x.xml", + XmlContent = "", + Version = "2.1", + Profile = "EN 16931" + }; + var (client, handler) = BuildJson(expected); + + // Act + var result = await client.ExtractZugferdAsResultAsync(new MemoryStream(FakePdfBytes())); + + // Assert + result.FileName.Should().Be("factur-x.xml"); + result.XmlContent.Should().Be(""); + result.Version.Should().Be("2.1"); + handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/zugferd/extract?asFile=false"); + } + + // ?? ExtractZugferdAsResultAsync (byte[]) ????????????????????????????????? + + [Fact] + public async Task ExtractZugferdAsResultAsync_Bytes_SendsJsonWithFormatJson() + { + // Arrange + var expected = new ZugferdExtractionResult { FileName = "zugferd.xml", XmlContent = "" }; + var (client, handler) = BuildJson(expected); + + // Act + await client.ExtractZugferdAsResultAsync(FakePdfBytes()); + + // Assert + handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/zugferd/extract?format=json"); + handler.LastRequest.Content.Should().NotBeNull(); + handler.LastRequest.Content!.Headers.ContentType!.MediaType.Should().Be("application/json"); + } + + // ?? HTTP error propagation ??????????????????????????????????????????????? + + [Fact] + public async Task HasZugferdAsync_WhenApiReturns400_ThrowsHttpRequestException() + { + // Arrange + var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.BadRequest); + var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }; + var client = new ZugferdClient(httpClient, NullLogger.Instance); + + // Act & Assert + await client.Invoking(c => c.HasZugferdAsync(new MemoryStream(FakePdfBytes()))) + .Should().ThrowAsync(); + } +}