From b1421de0b502a91d0af18927596de2cef5afcf91 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 24 Jul 2026 11:49:57 -0300 Subject: [PATCH] fix(vendors): address roadmap review findings --- .../VendorPortalDocumentTests.cs | 30 +++++++++++++++++++ .../Controllers/VendorPortalController.cs | 6 ++-- .../appsettings.Production.json | 6 ++++ Api.SeaHavenIndustries/appsettings.json | 7 +++++ ...260723220614_AddVendorRoadmapOperations.cs | 6 ++-- 5 files changed, 49 insertions(+), 6 deletions(-) diff --git a/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs b/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs index e6b6167..be73fbb 100644 --- a/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs +++ b/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs @@ -124,6 +124,36 @@ public sealed class VendorPortalDocumentTests : IDisposable 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)) diff --git a/Api.SeaHavenIndustries/Controllers/VendorPortalController.cs b/Api.SeaHavenIndustries/Controllers/VendorPortalController.cs index eba515f..53fbcb1 100644 --- a/Api.SeaHavenIndustries/Controllers/VendorPortalController.cs +++ b/Api.SeaHavenIndustries/Controllers/VendorPortalController.cs @@ -803,11 +803,11 @@ namespace Api.SeaHavenIndustries.Controllers { var bytes = new byte[8]; var count = await stream.ReadAsync(bytes); - if (contentType == "application/pdf") + if (string.Equals(contentType, "application/pdf", StringComparison.OrdinalIgnoreCase)) return count >= 5 && bytes[0] == 0x25 && bytes[1] == 0x50 && bytes[2] == 0x44 && bytes[3] == 0x46 && bytes[4] == 0x2D; - if (contentType == "image/jpeg") + if (string.Equals(contentType, "image/jpeg", StringComparison.OrdinalIgnoreCase)) return count >= 3 && bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF; - if (contentType == "image/png") + if (string.Equals(contentType, "image/png", StringComparison.OrdinalIgnoreCase)) return count >= 8 && bytes.SequenceEqual(new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }); return false; } diff --git a/Api.SeaHavenIndustries/appsettings.Production.json b/Api.SeaHavenIndustries/appsettings.Production.json index 651d68c..b4aae5c 100644 --- a/Api.SeaHavenIndustries/appsettings.Production.json +++ b/Api.SeaHavenIndustries/appsettings.Production.json @@ -14,6 +14,12 @@ // Provide the real value via environment variable SendGrid__ApiKey or the instance secret store. "ApiKey": "${SENDGRID_API_KEY}" }, + // Production must supply a reachable ClamAV Host (environment variable ClamAV__Host). + // While Host is empty the scanner is unavailable and uploads stay quarantined (423 Locked). + "ClamAV": { + "Host": "", + "Port": 3310 + }, "AllowedHosts": "*", "JWT": { "ValidAudience": "http://console.seahavenind.com", diff --git a/Api.SeaHavenIndustries/appsettings.json b/Api.SeaHavenIndustries/appsettings.json index 2d5dc80..426d85b 100644 --- a/Api.SeaHavenIndustries/appsettings.json +++ b/Api.SeaHavenIndustries/appsettings.json @@ -17,6 +17,13 @@ // Provide the real value via environment variable SendGrid__ApiKey or user-secrets. "ApiKey": "${SENDGRID_API_KEY}" }, + // Malware scanner for vendor completion document uploads. While Host is empty the + // scanner is considered unavailable and uploaded files stay quarantined (download + // returns 423 Locked until a scan passes). Production must supply a real Host. + "ClamAV": { + "Host": "", + "Port": 3310 + }, "AllowedHosts": "*", "JWT": { "ValidAudience": "http://localhost:4200", diff --git a/Data.SeaHavenIndustries/Migrations/20260723220614_AddVendorRoadmapOperations.cs b/Data.SeaHavenIndustries/Migrations/20260723220614_AddVendorRoadmapOperations.cs index bea27f8..4125639 100644 --- a/Data.SeaHavenIndustries/Migrations/20260723220614_AddVendorRoadmapOperations.cs +++ b/Data.SeaHavenIndustries/Migrations/20260723220614_AddVendorRoadmapOperations.cs @@ -16,7 +16,7 @@ namespace Data.SeaHavenIndustries.Migrations table: "Vendors", type: "nvarchar(max)", nullable: false, - defaultValue: ""); + defaultValue: "Unknown"); migrationBuilder.AddColumn( name: "AvailabilityUpdatedAt", @@ -54,14 +54,14 @@ namespace Data.SeaHavenIndustries.Migrations table: "Dispatches", type: "nvarchar(max)", nullable: false, - defaultValue: ""); + defaultValue: "Not Submitted"); migrationBuilder.AddColumn( name: "PaymentStatus", table: "Dispatches", type: "nvarchar(max)", nullable: false, - defaultValue: ""); + defaultValue: "Unavailable"); migrationBuilder.CreateTable( name: "SitePreferredVendors",