diff --git a/.platform/nginx/conf.d/01_upload_body_size.conf b/.platform/nginx/conf.d/01_upload_body_size.conf index a7bd889..da18278 100644 --- a/.platform/nginx/conf.d/01_upload_body_size.conf +++ b/.platform/nginx/conf.d/01_upload_body_size.conf @@ -1,4 +1,4 @@ -# SH-383: the Elastic Beanstalk nginx proxy defaults client_max_body_size to 1m, +# The Elastic Beanstalk nginx proxy defaults client_max_body_size to 1m, # which returned 413 for every media upload over ~1 MB before the request reached # the API. The cap sits above the API's own request limit # (WorkOrderMediaContract.MaxUploadRequestBytes = 110 MB) so oversize uploads get diff --git a/Api.SeaHavenIndustries/Controllers/WorkOrderMediaController.cs b/Api.SeaHavenIndustries/Controllers/WorkOrderMediaController.cs index fd7dac1..32122ca 100644 --- a/Api.SeaHavenIndustries/Controllers/WorkOrderMediaController.cs +++ b/Api.SeaHavenIndustries/Controllers/WorkOrderMediaController.cs @@ -92,7 +92,7 @@ namespace Api.SeaHavenIndustries.Controllers // of being sized under a kind it does not have. WorkOrderMediaFileRules.EnsureAllowed(file, category); - // SH-116 contract: per-kind caps (photos 10 MB, videos 100 MB, documents 50 MB). + // Media contract: per-kind caps (photos 10 MB, videos 100 MB, documents 50 MB). // Classify by the same resolved type EnsureAllowed validates against. var sizeMessage = WorkOrderMediaContract.ValidateSize( WorkOrderMediaFileRules.ResolveUploadContentType(file), file.FileName, file.Length); diff --git a/SeaHaven.DataServices/Interfaces/IVendorDocumentDataService.cs b/SeaHaven.DataServices/Interfaces/IVendorDocumentDataService.cs index 6986ef2..46a4274 100644 --- a/SeaHaven.DataServices/Interfaces/IVendorDocumentDataService.cs +++ b/SeaHaven.DataServices/Interfaces/IVendorDocumentDataService.cs @@ -11,14 +11,14 @@ namespace SeaHaven.DataServices.Interfaces Task GetUpliftEvidenceAsync(int documentId, int dispatchId, int vendorId, CancellationToken cancellationToken); /// /// Content types of the work order's current vendor documents (not deleted, not replaced by a - /// newer completion version), optionally excluding one being replaced. SH-116 photo/video counts. + /// newer completion version), optionally excluding one being replaced. Feeds the per-work-order photo/video counts. /// Task> ListActiveContentTypesForWorkOrderAsync( int workOrderId, int? excludingDocumentId, CancellationToken cancellationToken); - /// Stored URLs of the work order's non-deleted dispatcher attachments (SH-116 counts). + /// Stored URLs of the work order's non-deleted dispatcher attachments (photo/video counts). Task> ListActiveWorkOrderAttachmentUrlsAsync( int workOrderId, CancellationToken cancellationToken); diff --git a/SeaHaven.DataServices/Interfaces/IWorkOrderMediaDataService.cs b/SeaHaven.DataServices/Interfaces/IWorkOrderMediaDataService.cs index caf6071..144a521 100644 --- a/SeaHaven.DataServices/Interfaces/IWorkOrderMediaDataService.cs +++ b/SeaHaven.DataServices/Interfaces/IWorkOrderMediaDataService.cs @@ -23,14 +23,14 @@ namespace SeaHaven.DataServices.Interfaces Task GetTrackedAttachmentAsync(int mediaId, int workOrderId, CancellationToken cancellationToken); void TrackAttachment(WorkOrderAttachments attachment); - /// Stored URLs of the work order's non-deleted attachments (SH-116 photo/video counts). + /// Stored URLs of the work order's non-deleted attachments (photo/video counts). Task> ListActiveAttachmentUrlsAsync( int workOrderId, CancellationToken cancellationToken); /// /// Content types of the work order's current vendor-portal documents (not deleted, not - /// replaced by a newer completion version). SH-116 counts span both upload surfaces. + /// replaced by a newer completion version). The counts span both upload surfaces. /// Task> ListActiveVendorMediaContentTypesAsync( int workOrderId, diff --git a/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs b/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs index ace32c8..088c761 100644 --- a/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs +++ b/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs @@ -138,7 +138,7 @@ namespace SeaHaven.Services.DTOs } /// - /// SH-116 per-work-order photo/video usage, so the portal can pre-check an upload + /// Per-work-order photo/video usage, so the portal can pre-check an upload /// before sending it. The server still enforces the limit on upload. /// public class PortalMediaCountsDTO diff --git a/SeaHaven.Services/Helpers/VideoDurationProbe.cs b/SeaHaven.Services/Helpers/VideoDurationProbe.cs index 8355c46..1818c50 100644 --- a/SeaHaven.Services/Helpers/VideoDurationProbe.cs +++ b/SeaHaven.Services/Helpers/VideoDurationProbe.cs @@ -8,7 +8,7 @@ namespace SeaHaven.Services.Helpers /// top-level moov box → mvhd timescale and duration. It seeks over box /// headers only (the moov box may sit after a large mdat), never decodes /// media and never allocates more than a few bytes. Returns null whenever the structure - /// cannot be read, so callers can let unreadable files through (SH-116: unreadable + /// cannot be read, so callers can let unreadable files through (per the media contract: unreadable /// metadata never blocks an upload). /// public static class VideoDurationProbe diff --git a/SeaHaven.Services/Helpers/WorkOrderMediaContract.cs b/SeaHaven.Services/Helpers/WorkOrderMediaContract.cs index e064b02..46865bf 100644 --- a/SeaHaven.Services/Helpers/WorkOrderMediaContract.cs +++ b/SeaHaven.Services/Helpers/WorkOrderMediaContract.cs @@ -1,7 +1,7 @@ namespace SeaHaven.Services.Helpers { /// - /// SH-116 client-confirmed media contract (2026-09-22): photos up to 10 MB (JPEG/PNG/HEIC), + /// Client-confirmed media contract (2026-09-22): photos up to 10 MB (JPEG/PNG/HEIC), /// videos up to 100 MB and 90 seconds (MP4/MOV), at most 10 photos and 3 videos per work /// order, across the dispatcher media modal, the completion-doc media tab and the vendor /// portal upload. Documents (PDF/DOC/DOCX) keep the 50 MB document cap. Duration is read diff --git a/SeaHaven.Services/Helpers/WorkOrderMediaFileRules.cs b/SeaHaven.Services/Helpers/WorkOrderMediaFileRules.cs index c438759..b4db302 100644 --- a/SeaHaven.Services/Helpers/WorkOrderMediaFileRules.cs +++ b/SeaHaven.Services/Helpers/WorkOrderMediaFileRules.cs @@ -140,7 +140,7 @@ namespace SeaHaven.Services.Helpers { throw new Exceptions.WorkOrderBoardValidationException( "UnsupportedMediaType", - "Supported file types are JPG, PNG, MP4, MOV, PDF, DOC, and DOCX for Extra Docs; photos accept media only."); + "Supported file types are JPG, PNG, HEIC, MP4, MOV, PDF, DOC, and DOCX for Extra Docs; photos accept media only."); } } diff --git a/SeaHaven.Services/Implementation/VendorPortalService.cs b/SeaHaven.Services/Implementation/VendorPortalService.cs index 782f446..a98c57d 100644 --- a/SeaHaven.Services/Implementation/VendorPortalService.cs +++ b/SeaHaven.Services/Implementation/VendorPortalService.cs @@ -885,7 +885,7 @@ namespace SeaHaven.Services.Implementation throw new InvalidOperationException("A completion document file is required."); } - // SH-116 contract: photos up to 10 MB (JPEG/PNG/HEIC), videos up to 100 MB + // Media contract: photos up to 10 MB (JPEG/PNG/HEIC), videos up to 100 MB // (MP4/MOV). Documents keep the configured VendorDocuments cap. var contentType = ResolveUploadContentType(file); if (contentType == null) @@ -980,7 +980,7 @@ namespace SeaHaven.Services.Implementation async Task PersistAsync(CancellationToken ct) { - // SH-116: the 10-photo / 3-video limit is per work order across the dispatcher and + // The 10-photo / 3-video limit is per work order across the dispatcher and // vendor surfaces. A new completion version replaces its predecessor, so that one // does not count against the upload that supersedes it. if (dispatch.WorkOrderId is int workOrderId) diff --git a/SeaHaven.Services/Implementation/WorkOrderMediaService.cs b/SeaHaven.Services/Implementation/WorkOrderMediaService.cs index 0a0d1d5..d2c266d 100644 --- a/SeaHaven.Services/Implementation/WorkOrderMediaService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderMediaService.cs @@ -156,7 +156,7 @@ namespace SeaHaven.Services.Implementation }; } - // SH-116 photo/video caps are a work-order aggregate shared with the vendor portal + // Photo/video caps are a work-order aggregate shared with the vendor portal // upload, so the count and the insert run under the per-work-order mutation lock. var attachment = await _upliftData.ExecuteWorkOrderMutationAsync( workOrderId, @@ -363,7 +363,7 @@ namespace SeaHaven.Services.Implementation } /// - /// SH-116 contract: at most 10 photos and 3 videos per work order, counted over the + /// Media contract: at most 10 photos and 3 videos per work order, counted over the /// stored attachment rows plus the work order's current vendor-portal documents. Legacy /// Before/After column slots replace in place and are not counted. Documents have no /// per-work-order count limit. diff --git a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs index 224d73b..ea77fb4 100644 --- a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs @@ -1,6 +1,8 @@ using System.Security.Claims; using System.Text; using System.IO.Compression; +using System.Reflection; +using System.Text.RegularExpressions; using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.AspNetCore.Http; @@ -2373,6 +2375,32 @@ public class WorkOrderMediaFileRulesTests return stream.ToArray(); } + [Fact] + public void EnsureAllowed_RejectionMessage_NamesEveryAllowedType() + { + const BindingFlags privateStatic = BindingFlags.NonPublic | BindingFlags.Static; + var allowedTypes = (HashSet?)typeof(WorkOrderMediaFileRules) + .GetField("AllowedContentTypes", privateStatic)?.GetValue(null); + var extensionsByType = (Dictionary>?)typeof(WorkOrderMediaFileRules) + .GetField("ExtensionsByContentType", privateStatic)?.GetValue(null); + Assert.NotNull(allowedTypes); + Assert.NotNull(extensionsByType); + Assert.NotEmpty(allowedTypes); + + var ex = Assert.Throws( + () => WorkOrderMediaFileRules.EnsureAllowed(FormFile(Encoding.UTF8.GetBytes("plain text"), "notes.txt", "text/plain"))); + + foreach (var contentType in allowedTypes) + { + var canonical = contentType.Equals("image/jpg", StringComparison.OrdinalIgnoreCase) ? "image/jpeg" : contentType; + Assert.True(extensionsByType.TryGetValue(canonical, out var extensions), $"No extensions mapped for {contentType}."); + var labels = extensions.Select(extension => extension.TrimStart('.').ToUpperInvariant()).ToList(); + Assert.True( + labels.Any(label => Regex.IsMatch(ex.Message, $@"\b{Regex.Escape(label)}\b")), + $"Rejection message does not name {contentType} ({string.Join("/", labels)}): {ex.Message}"); + } + } + [Fact] public void IsAllowed_ValidJpeg_ReturnsTrue() { diff --git a/scripts/validate-elastic-beanstalk-bundle.sh b/scripts/validate-elastic-beanstalk-bundle.sh index 1d88367..83fcc9e 100755 --- a/scripts/validate-elastic-beanstalk-bundle.sh +++ b/scripts/validate-elastic-beanstalk-bundle.sh @@ -32,7 +32,7 @@ grep -Fxq \ ' WorkOrderWebhook__SecretId: arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB' \ "$webhook_file" -# Without this override the platform nginx caps request bodies at 1 MB (SH-383). +# Without this override the platform nginx caps request bodies at 1 MB. unzip -p "$BUNDLE" .platform/nginx/conf.d/01_upload_body_size.conf > "$nginx_file" grep -Fxq 'client_max_body_size 120M;' "$nginx_file"