mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 03:43:11 +00:00
fix(media): name HEIC in the unsupported-type message and keep ticket keys out of comments
The rejection message now lists every type the media allowlist accepts, and a test fails if the message and the allowlist drift apart.
This commit is contained in:
parent
0d8f32d148
commit
207bf59208
12 changed files with 43 additions and 15 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
Task<VendorCompletionDocument?> GetUpliftEvidenceAsync(int documentId, int dispatchId, int vendorId, CancellationToken cancellationToken);
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<string>> ListActiveContentTypesForWorkOrderAsync(
|
||||
int workOrderId,
|
||||
int? excludingDocumentId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Stored URLs of the work order's non-deleted dispatcher attachments (SH-116 counts).</summary>
|
||||
/// <summary>Stored URLs of the work order's non-deleted dispatcher attachments (photo/video counts).</summary>
|
||||
Task<IReadOnlyList<string>> ListActiveWorkOrderAttachmentUrlsAsync(
|
||||
int workOrderId,
|
||||
CancellationToken cancellationToken);
|
||||
|
|
|
|||
|
|
@ -23,14 +23,14 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
Task<WorkOrderAttachments?> GetTrackedAttachmentAsync(int mediaId, int workOrderId, CancellationToken cancellationToken);
|
||||
void TrackAttachment(WorkOrderAttachments attachment);
|
||||
|
||||
/// <summary>Stored URLs of the work order's non-deleted attachments (SH-116 photo/video counts).</summary>
|
||||
/// <summary>Stored URLs of the work order's non-deleted attachments (photo/video counts).</summary>
|
||||
Task<IReadOnlyList<string>> ListActiveAttachmentUrlsAsync(
|
||||
int workOrderId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<string>> ListActiveVendorMediaContentTypesAsync(
|
||||
int workOrderId,
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ namespace SeaHaven.Services.DTOs
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class PortalMediaCountsDTO
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace SeaHaven.Services.Helpers
|
|||
/// top-level <c>moov</c> box → <c>mvhd</c> timescale and duration. It seeks over box
|
||||
/// headers only (the <c>moov</c> box may sit after a large <c>mdat</c>), 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).
|
||||
/// </summary>
|
||||
public static class VideoDurationProbe
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<VendorCompletionDocument> 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)
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
|
|
|
|||
|
|
@ -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<string>?)typeof(WorkOrderMediaFileRules)
|
||||
.GetField("AllowedContentTypes", privateStatic)?.GetValue(null);
|
||||
var extensionsByType = (Dictionary<string, HashSet<string>>?)typeof(WorkOrderMediaFileRules)
|
||||
.GetField("ExtensionsByContentType", privateStatic)?.GetValue(null);
|
||||
Assert.NotNull(allowedTypes);
|
||||
Assert.NotNull(extensionsByType);
|
||||
Assert.NotEmpty(allowedTypes);
|
||||
|
||||
var ex = Assert.Throws<WorkOrderBoardValidationException>(
|
||||
() => 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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue