using DocumentService.API.Controllers; // For CheckPdfAttachmentsRequest DTO using DocumentService.Application.Common.DTOs; using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using System.Net; using System.Net.Http.Json; using Xunit; namespace DocumentService.Tests.Integration.API; /// /// Integration tests for PdfAttachmentController. /// Tests /api/pdf/attachments/check endpoint with both multipart and Base64 input. /// public class PdfAttachmentControllerTests : IClassFixture> { private readonly WebApplicationFactory _factory; private readonly HttpClient _client; public PdfAttachmentControllerTests(WebApplicationFactory factory) { _factory = factory; _client = _factory.CreateClient(); } #region Helper Methods /// /// Loads a test PDF from embedded resources. /// private static async Task LoadTestPdfAsync(string filename) { var assembly = typeof(PdfAttachmentControllerTests).Assembly; var resourceName = $"DocumentService.Tests.TestData.Pdfs.{filename}"; using var stream = assembly.GetManifestResourceStream(resourceName); if (stream == null) { throw new InvalidOperationException($"Test resource '{resourceName}' not found"); } using var ms = new MemoryStream(); await stream.CopyToAsync(ms); return ms.ToArray(); } #endregion #region Base64 JSON Tests [Fact] public async Task POST_CheckAttachments_Base64_PdfWithoutAttachments_Returns200() { // Arrange byte[] pdfBytes = await LoadTestPdfAsync("pdfWithSwissQRCode.pdf"); string base64Pdf = Convert.ToBase64String(pdfBytes); var request = new CheckPdfAttachmentsRequest { Base64Pdf = base64Pdf }; // Act var response = await _client.PostAsJsonAsync("/api/pdf/attachments/check", request); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content.ReadFromJsonAsync(); result.Should().NotBeNull(); result!.HasAttachments.Should().BeFalse(); result.AttachmentCount.Should().Be(0); result.Attachments.Should().BeEmpty(); } [Fact] public async Task POST_CheckAttachments_Base64_PdfWithMultipleAttachments_Returns200() { // Arrange byte[] pdfBytes = await LoadTestPdfAsync("pdfWithMoreThanOneAttachment.pdf"); string base64Pdf = Convert.ToBase64String(pdfBytes); var request = new CheckPdfAttachmentsRequest { Base64Pdf = base64Pdf }; // Act var response = await _client.PostAsJsonAsync("/api/pdf/attachments/check", request); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content.ReadFromJsonAsync(); result.Should().NotBeNull(); result!.HasAttachments.Should().BeTrue(); result.AttachmentCount.Should().Be(6, "PDF has exactly 6 attachments"); result.Attachments.Should().HaveCount(6); // Verify each attachment has required properties foreach (var attachment in result.Attachments) { attachment.FileName.Should().NotBeNullOrEmpty(); attachment.MimeType.Should().NotBeNullOrEmpty(); attachment.Size.Should().BeGreaterThan(0); } } [Fact] public async Task POST_CheckAttachments_Base64_InvalidBase64_Returns400() { // Arrange var request = new CheckPdfAttachmentsRequest { Base64Pdf = "invalid-base64!!!" }; // Act var response = await _client.PostAsJsonAsync("/api/pdf/attachments/check", request); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); var problemDetails = await response.Content.ReadAsStringAsync(); // FormatException message contains "Base-64" (with hyphen) problemDetails.Should().MatchRegex("(?i)base.?64", "should contain Base64 validation error"); } [Fact] public async Task POST_CheckAttachments_Base64_EmptyPdf_Returns400() { // Arrange var request = new CheckPdfAttachmentsRequest { Base64Pdf = string.Empty }; // Act var response = await _client.PostAsJsonAsync("/api/pdf/attachments/check", request); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); var problemDetails = await response.Content.ReadAsStringAsync(); // Empty Base64 causes FormatException or empty stream error problemDetails.Should().MatchRegex("(Base64|empty|stream)", "should contain validation error message"); } #endregion #region Multipart/Form-Data Tests [Fact] public async Task POST_CheckAttachments_Multipart_PdfWithoutAttachments_Returns200() { // Arrange byte[] pdfBytes = await LoadTestPdfAsync("pdfWithSwissQRCode.pdf"); using var content = new MultipartFormDataContent(); var fileContent = new ByteArrayContent(pdfBytes); fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf"); content.Add(fileContent, "file", "pdfWithSwissQRCode.pdf"); // Act var response = await _client.PostAsync("/api/pdf/attachments/check", content); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content.ReadFromJsonAsync(); result.Should().NotBeNull(); result!.HasAttachments.Should().BeFalse(); result.AttachmentCount.Should().Be(0); result.Attachments.Should().BeEmpty(); } [Fact] public async Task POST_CheckAttachments_Multipart_PdfWithMultipleAttachments_Returns200() { // Arrange byte[] pdfBytes = await LoadTestPdfAsync("pdfWithMoreThanOneAttachment.pdf"); using var content = new MultipartFormDataContent(); var fileContent = new ByteArrayContent(pdfBytes); fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf"); content.Add(fileContent, "file", "pdfWithMoreThanOneAttachment.pdf"); // Act var response = await _client.PostAsync("/api/pdf/attachments/check", content); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content.ReadFromJsonAsync(); result.Should().NotBeNull(); result!.HasAttachments.Should().BeTrue(); result.AttachmentCount.Should().Be(6); result.Attachments.Should().HaveCount(6); // Verify first attachment details var firstAttachment = result.Attachments.First(); firstAttachment.FileName.Should().NotBeNullOrEmpty(); firstAttachment.MimeType.Should().NotBeNullOrEmpty(); firstAttachment.Size.Should().BeGreaterThan(0); } [Fact] public async Task POST_CheckAttachments_Multipart_MissingFile_Returns400() { // Arrange using var content = new MultipartFormDataContent(); // No file added // Act var response = await _client.PostAsync("/api/pdf/attachments/check", content); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [Fact] public async Task POST_CheckAttachments_Multipart_CorruptedPdf_Returns500() { // Arrange byte[] corruptedBytes = "This is not a valid PDF content"u8.ToArray(); using var content = new MultipartFormDataContent(); var fileContent = new ByteArrayContent(corruptedBytes); fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf"); content.Add(fileContent, "file", "corrupted.pdf"); // Act var response = await _client.PostAsync("/api/pdf/attachments/check", content); // Assert response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); var problemDetails = await response.Content.ReadAsStringAsync(); // Note: Generic error message for security reasons (doesn't expose internal details) problemDetails.Should().Contain("error"); } #endregion #region Edge Cases [Fact] public async Task POST_CheckAttachments_Base64_PdfWithSwissQrCode_Returns200() { // Arrange byte[] pdfBytes = await LoadTestPdfAsync("pdfWithSwissQRCode.pdf"); string base64Pdf = Convert.ToBase64String(pdfBytes); var request = new CheckPdfAttachmentsRequest { Base64Pdf = base64Pdf }; // Act var response = await _client.PostAsJsonAsync("/api/pdf/attachments/check", request); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content.ReadFromJsonAsync(); result.Should().NotBeNull(); result!.HasAttachments.Should().BeFalse("Swiss QR PDF has no attachments"); result.AttachmentCount.Should().Be(0); } #endregion #region Extract Attachments Tests (Multipart) [Fact] public async Task POST_ExtractAttachments_Multipart_ValidPdfWithAttachments_Returns200WithZip() { // Arrange byte[] pdfBytes = await LoadTestPdfAsync("pdfWithMoreThanOneAttachment.pdf"); using var content = new MultipartFormDataContent(); var fileContent = new ByteArrayContent(pdfBytes); fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf"); content.Add(fileContent, "file", "test.pdf"); // Act var response = await _client.PostAsync("/api/pdf/attachments/extract", content); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); response.Content.Headers.ContentType?.MediaType.Should().Be("application/zip"); response.Content.Headers.ContentDisposition?.FileName.Should().Be("attachments.zip"); byte[] zipBytes = await response.Content.ReadAsByteArrayAsync(); zipBytes.Should().NotBeEmpty("ZIP should contain data"); // Verify ZIP structure using var zipStream = new MemoryStream(zipBytes); using var zipArchive = new System.IO.Compression.ZipArchive(zipStream, System.IO.Compression.ZipArchiveMode.Read); zipArchive.Entries.Should().HaveCount(6, "PDF contains 6 attachments"); } [Fact] public async Task POST_ExtractAttachments_Multipart_EmptyPdf_Returns400() { // Arrange using var content = new MultipartFormDataContent(); var fileContent = new ByteArrayContent(Array.Empty()); fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf"); content.Add(fileContent, "file", "empty.pdf"); // Act var response = await _client.PostAsync("/api/pdf/attachments/extract", content); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [Fact] public async Task POST_ExtractAttachments_Multipart_CorruptedPdf_Returns500() { // Arrange byte[] corruptedBytes = "This is not a valid PDF content"u8.ToArray(); using var content = new MultipartFormDataContent(); var fileContent = new ByteArrayContent(corruptedBytes); fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf"); content.Add(fileContent, "file", "corrupted.pdf"); // Act var response = await _client.PostAsync("/api/pdf/attachments/extract", content); // Assert response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); } #endregion #region Extract Attachments Tests (Base64) [Fact] public async Task POST_ExtractAttachments_Base64_ValidPdfWithAttachments_Returns200WithZip() { // Arrange byte[] pdfBytes = await LoadTestPdfAsync("pdfWithMoreThanOneAttachment.pdf"); string base64Pdf = Convert.ToBase64String(pdfBytes); var request = new ExtractPdfAttachmentsRequest { Base64Pdf = base64Pdf }; // Act var response = await _client.PostAsJsonAsync("/api/pdf/attachments/extract", request); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); response.Content.Headers.ContentType?.MediaType.Should().Be("application/zip"); response.Content.Headers.ContentDisposition?.FileName.Should().Be("attachments.zip"); byte[] zipBytes = await response.Content.ReadAsByteArrayAsync(); zipBytes.Should().NotBeEmpty("ZIP should contain data"); // Verify ZIP structure using var zipStream = new MemoryStream(zipBytes); using var zipArchive = new System.IO.Compression.ZipArchive(zipStream, System.IO.Compression.ZipArchiveMode.Read); zipArchive.Entries.Should().HaveCount(6, "PDF contains 6 attachments"); } [Fact] public async Task POST_ExtractAttachments_Base64_InvalidBase64_Returns400() { // Arrange var request = new ExtractPdfAttachmentsRequest { Base64Pdf = "invalid-base64!!!" }; // Act var response = await _client.PostAsJsonAsync("/api/pdf/attachments/extract", request); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); var problemDetails = await response.Content.ReadAsStringAsync(); // FormatException message contains "Base-64" (with hyphen) problemDetails.Should().MatchRegex("(?i)base.?64", "should contain Base64 validation error"); } [Fact] public async Task POST_ExtractAttachments_Base64_EmptyPdf_Returns400() { // Arrange var request = new ExtractPdfAttachmentsRequest { Base64Pdf = string.Empty }; // Act var response = await _client.PostAsJsonAsync("/api/pdf/attachments/extract", request); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); var problemDetails = await response.Content.ReadAsStringAsync(); problemDetails.Should().MatchRegex("(Base64|empty|stream)", "should contain validation error message"); } #endregion }