From b6b8d2e58f9ca2315a7be169592af8e1dfd8af1a Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Tue, 8 Sep 2026 11:06:05 -0300 Subject: [PATCH] feat(work-orders): cap media uploads at 50MB --- .../WorkOrderMediaControllerTests.cs | 75 +++++++++++++++++++ .../Controllers/WorkOrderMediaController.cs | 13 +++- 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 Api.SeaHavenIndustries.Tests/WorkOrderMediaControllerTests.cs diff --git a/Api.SeaHavenIndustries.Tests/WorkOrderMediaControllerTests.cs b/Api.SeaHavenIndustries.Tests/WorkOrderMediaControllerTests.cs new file mode 100644 index 0000000..18d1a31 --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/WorkOrderMediaControllerTests.cs @@ -0,0 +1,75 @@ +using Api.SeaHavenIndustries.Controllers; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SeaHaven.Services.DTOs; +using SeaHaven.Services.Interfaces; +using Xunit; + +namespace Api.SeaHavenIndustries.Tests; + +public class WorkOrderMediaControllerTests +{ + private static WorkOrderMediaController CreateController( + Mock? service = null, + Mock? storage = null) + { + var controller = new WorkOrderMediaController( + (service ?? new Mock()).Object, + (storage ?? new Mock()).Object); + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + }; + return controller; + } + + [Fact] + public void AddMedia_HasFiftyMegabyteRequestLimit() + { + var method = typeof(WorkOrderMediaController).GetMethod(nameof(WorkOrderMediaController.AddMedia)); + var attribute = Assert.Single( + method!.CustomAttributes, + candidate => candidate.AttributeType == typeof(RequestSizeLimitAttribute)); + var bytes = Assert.Single(attribute.ConstructorArguments); + + Assert.Equal(50_000_000L, bytes.Value); + } + + [Fact] + public async Task AddMedia_FileOverLimit_ReturnsStableUnprocessableEntity() + { + var file = new Mock(); + file.SetupGet(candidate => candidate.Length).Returns(50_000_001); + var storage = new Mock(MockBehavior.Strict); + var controller = CreateController(storage: storage); + + var result = await controller.AddMedia(1, null, file.Object, CancellationToken.None); + + var response = Assert.IsType(result); + var error = Assert.IsType(response.Value); + Assert.Equal("FileTooLarge", error.Code); + Assert.Equal("The uploaded file must not exceed 50 MB.", error.Message); + storage.VerifyNoOtherCalls(); + } + + [Fact] + public async Task AddMedia_UnsupportedType_ReturnsUnprocessableEntity() + { + var bytes = new byte[] { 1, 2, 3 }; + var file = new FormFile(new MemoryStream(bytes), 0, bytes.Length, "file", "payload.exe") + { + Headers = new HeaderDictionary(), + ContentType = "application/octet-stream" + }; + var storage = new Mock(MockBehavior.Strict); + var controller = CreateController(storage: storage); + + var result = await controller.AddMedia(1, null, file, CancellationToken.None); + + var response = Assert.IsType(result); + var error = Assert.IsType(response.Value); + Assert.Equal("UnsupportedMediaType", error.Code); + storage.VerifyNoOtherCalls(); + } +} diff --git a/Api.SeaHavenIndustries/Controllers/WorkOrderMediaController.cs b/Api.SeaHavenIndustries/Controllers/WorkOrderMediaController.cs index 0867b1a..b9bc629 100644 --- a/Api.SeaHavenIndustries/Controllers/WorkOrderMediaController.cs +++ b/Api.SeaHavenIndustries/Controllers/WorkOrderMediaController.cs @@ -17,6 +17,8 @@ namespace Api.SeaHavenIndustries.Controllers [Route("api/workorders")] public class WorkOrderMediaController : Controller { + public const long MaxUploadBytes = 50_000_000; + private readonly IWorkOrderMediaService _workOrderMediaService; private readonly IFileStoragePort _fileStorage; @@ -51,7 +53,7 @@ namespace Api.SeaHavenIndustries.Controllers } [HttpPost("{id:int}/media")] - [RequestSizeLimit(30_000_000)] + [RequestSizeLimit(MaxUploadBytes)] public async Task AddMedia( int id, [FromForm] WorkOrderMediaCategory? category, @@ -64,7 +66,14 @@ namespace Api.SeaHavenIndustries.Controllers string? fileUrl = null; try { - WorkOrderMediaFileRules.EnsureAllowed(file); + if (file.Length > MaxUploadBytes) + { + throw new WorkOrderBoardValidationException( + "FileTooLarge", + "The uploaded file must not exceed 50 MB."); + } + + WorkOrderMediaFileRules.EnsureAllowed(file, category); var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier); await _workOrderMediaService.EnsureCanMutateMediaAsync(id, User, actorId, cancellationToken, category);