using Api.SeaHavenIndustries.Controllers; using Api.SeaHavenIndustries.Helper; using Api.SeaHavenIndustries.Infrastructure; using Data.SeaHavenIndustries; using FluentAssertions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; using SeaHaven.DataServices.Implementation; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.Configuration; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; using Xunit; namespace Api.SeaHavenIndustries.Tests; public sealed class VendorPortalDocumentTests : IDisposable { private const string Token = "vendor-document-test-token"; private readonly string _contentRoot = Path.Combine( Path.GetTempPath(), $"seahaven-vendor-documents-{Guid.NewGuid():N}"); private static ApplicationDbContext NewContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; return new ApplicationDbContext(options); } private VendorPortalController NewController(ApplicationDbContext context) { var vendorData = new VendorDataService(context); var tokenService = new VendorPortalTokenService( vendorData, Microsoft.Extensions.Options.Options.Create(new VendorPortalOptions())); var documentData = new VendorDocumentDataService(context); var storageEnvironment = new Mock(); storageEnvironment.SetupGet(item => item.ContentRootPath).Returns(_contentRoot); var storage = new VendorDocumentStorageAdapter(storageEnvironment.Object); var service = new VendorPortalService( tokenService, new DispatchDataService(context), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), documentData, storage, Microsoft.Extensions.Options.Options.Create(new FrontendOptions()), Microsoft.Extensions.Options.Options.Create(new ApprovalsOptions()), Microsoft.Extensions.Options.Options.Create(new VendorDocumentsOptions())); var controller = new VendorPortalController(service, Mock.Of>()); controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }; controller.Request.Headers["X-Vendor-Token"] = Token; return controller; } private static async Task<(Vendor Vendor, Dispatch Dispatch)> SeedDispatch( ApplicationDbContext context) { var vendor = new Vendor { CompanyName = "Gateway", IsActive = true }; var workOrder = new WorkOrder { WorkerOrderTitle = "Repair" }; context.AddRange(vendor, workOrder); await context.SaveChangesAsync(); var dispatch = new Dispatch { VendorId = vendor.Id, WorkOrderId = workOrder.Id, Status = "Completed" }; context.Dispatches.Add(dispatch); context.VendorAccessTokens.Add(new VendorAccessToken { VendorId = vendor.Id, Token = Token, IssuedAt = DateTime.UtcNow, ExpiresAt = DateTime.UtcNow.AddDays(1) }); await context.SaveChangesAsync(); return (vendor, dispatch); } private static FormFile FormFile(byte[] bytes, string fileName, string contentType) { return new FormFile(new MemoryStream(bytes), 0, bytes.Length, "file", fileName) { Headers = new HeaderDictionary(), ContentType = contentType }; } [Fact] public async Task UploadCompletionDocument_RejectsMismatchedFileSignature() { using var context = NewContext(); var (_, dispatch) = await SeedDispatch(context); var result = await NewController(context).UploadCompletionDocument( dispatch.Id, FormFile("not a pdf"u8.ToArray(), "completion.pdf", "application/pdf")); result.Should().BeOfType(); context.VendorCompletionDocuments.Should().BeEmpty(); } [Fact] public async Task UploadCompletionDocument_QuarantinesValidFileUntilScanPasses() { using var context = NewContext(); var (_, dispatch) = await SeedDispatch(context); var controller = NewController(context); var pdf = "%PDF-1.4\nvendor completion"u8.ToArray(); var upload = await controller.UploadCompletionDocument( dispatch.Id, FormFile(pdf, "completion.pdf", "application/pdf")); upload.Should().BeOfType(); var document = await context.VendorCompletionDocuments.SingleAsync(); document.ScanStatus.Should().Be("Pending"); document.ReviewStatus.Should().Be("Processing"); File.Exists(VendorDocumentStorage.ResolvePath(_contentRoot, document)).Should().BeTrue(); var download = await controller.DownloadCompletionDocument(dispatch.Id, document.Id); var locked = download.Should().BeOfType().Subject; locked.StatusCode.Should().Be(StatusCodes.Status423Locked); } [Fact] public async Task UploadCompletionDocument_AcceptsMixedCasePdfContentType() { using var context = NewContext(); var (_, dispatch) = await SeedDispatch(context); var pdf = "%PDF-1.4\nvendor completion"u8.ToArray(); var result = await NewController(context).UploadCompletionDocument( dispatch.Id, FormFile(pdf, "completion.pdf", "Application/PDF")); result.Should().BeOfType(); context.VendorCompletionDocuments.Should().HaveCount(1); } [Fact] public async Task UploadCompletionDocument_AcceptsMixedCaseImageContentType() { using var context = NewContext(); var (_, dispatch) = await SeedDispatch(context); var png = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00 }; var result = await NewController(context).UploadCompletionDocument( dispatch.Id, FormFile(png, "photo.png", "Image/PNG")); result.Should().BeOfType(); context.VendorCompletionDocuments.Should().HaveCount(1); } public void Dispose() { if (Directory.Exists(_contentRoot)) { Directory.Delete(_contentRoot, recursive: true); } } }