mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
fix(media): check the file type before the size cap on media upload
AddMedia sized a file before validating its type, and ValidateSize's Unknown arm returned the video message, so a 150 MB .exe sent as application/octet-stream was rejected as FileTooLarge with "Videos must be 100 MB or smaller." EnsureAllowed now runs first, so an unsupported file always reports UnsupportedMediaType, and the Unknown arm uses a type-neutral message. The oversize controller tests now use real file headers so they pass the type check before reaching the size cap.
This commit is contained in:
parent
f3ef11b504
commit
0d8f32d148
3 changed files with 55 additions and 22 deletions
|
|
@ -56,14 +56,11 @@ public class WorkOrderMediaControllerTests
|
|||
[Fact]
|
||||
public async Task AddMedia_VideoOverHundredMegabytes_ReturnsStableUnprocessableEntity()
|
||||
{
|
||||
var file = new Mock<IFormFile>();
|
||||
file.SetupGet(candidate => candidate.Length).Returns(100_000_001);
|
||||
file.SetupGet(candidate => candidate.FileName).Returns("clip.mp4");
|
||||
file.SetupGet(candidate => candidate.ContentType).Returns("video/mp4");
|
||||
var file = OversizeFile(100_000_001, "clip.mp4", "video/mp4", FtypHeader);
|
||||
var storage = new Mock<IFileStoragePort>(MockBehavior.Strict);
|
||||
var controller = CreateController(storage: storage);
|
||||
|
||||
var result = await controller.AddMedia(1, null, file.Object, CancellationToken.None);
|
||||
var result = await controller.AddMedia(1, null, file, CancellationToken.None);
|
||||
|
||||
var response = Assert.IsType<UnprocessableEntityObjectResult>(result);
|
||||
var error = Assert.IsType<WorkOrderBoardValidationErrorDto>(response.Value);
|
||||
|
|
@ -75,14 +72,11 @@ public class WorkOrderMediaControllerTests
|
|||
[Fact]
|
||||
public async Task AddMedia_PhotoOverTenMegabytes_ReturnsStableUnprocessableEntity()
|
||||
{
|
||||
var file = new Mock<IFormFile>();
|
||||
file.SetupGet(candidate => candidate.Length).Returns(10_000_001);
|
||||
file.SetupGet(candidate => candidate.FileName).Returns("photo.jpg");
|
||||
file.SetupGet(candidate => candidate.ContentType).Returns("image/jpeg");
|
||||
var file = OversizeFile(10_000_001, "photo.jpg", "image/jpeg", JpegHeader);
|
||||
var storage = new Mock<IFileStoragePort>(MockBehavior.Strict);
|
||||
var controller = CreateController(storage: storage);
|
||||
|
||||
var result = await controller.AddMedia(1, null, file.Object, CancellationToken.None);
|
||||
var result = await controller.AddMedia(1, null, file, CancellationToken.None);
|
||||
|
||||
var response = Assert.IsType<UnprocessableEntityObjectResult>(result);
|
||||
var error = Assert.IsType<WorkOrderBoardValidationErrorDto>(response.Value);
|
||||
|
|
@ -96,14 +90,11 @@ public class WorkOrderMediaControllerTests
|
|||
{
|
||||
// A .jpg with a foreign video type resolves to image/jpeg for validation, so it must
|
||||
// also be sized as a photo, not given the 100 MB video allowance.
|
||||
var file = new Mock<IFormFile>();
|
||||
file.SetupGet(candidate => candidate.Length).Returns(60_000_000);
|
||||
file.SetupGet(candidate => candidate.FileName).Returns("photo.jpg");
|
||||
file.SetupGet(candidate => candidate.ContentType).Returns("video/3gpp");
|
||||
var file = OversizeFile(60_000_000, "photo.jpg", "video/3gpp", JpegHeader);
|
||||
var storage = new Mock<IFileStoragePort>(MockBehavior.Strict);
|
||||
var controller = CreateController(storage: storage);
|
||||
|
||||
var result = await controller.AddMedia(1, null, file.Object, CancellationToken.None);
|
||||
var result = await controller.AddMedia(1, null, file, CancellationToken.None);
|
||||
|
||||
var response = Assert.IsType<UnprocessableEntityObjectResult>(result);
|
||||
var error = Assert.IsType<WorkOrderBoardValidationErrorDto>(response.Value);
|
||||
|
|
@ -115,14 +106,11 @@ public class WorkOrderMediaControllerTests
|
|||
[Fact]
|
||||
public async Task AddMedia_DocumentOverFiftyMegabytes_ReturnsStableUnprocessableEntity()
|
||||
{
|
||||
var file = new Mock<IFormFile>();
|
||||
file.SetupGet(candidate => candidate.Length).Returns(50_000_001);
|
||||
file.SetupGet(candidate => candidate.FileName).Returns("report.pdf");
|
||||
file.SetupGet(candidate => candidate.ContentType).Returns("application/pdf");
|
||||
var file = OversizeFile(50_000_001, "report.pdf", "application/pdf", PdfHeader);
|
||||
var storage = new Mock<IFileStoragePort>(MockBehavior.Strict);
|
||||
var controller = CreateController(storage: storage);
|
||||
|
||||
var result = await controller.AddMedia(1, WorkOrderMediaCategory.Extra, file.Object, CancellationToken.None);
|
||||
var result = await controller.AddMedia(1, WorkOrderMediaCategory.Extra, file, CancellationToken.None);
|
||||
|
||||
var response = Assert.IsType<UnprocessableEntityObjectResult>(result);
|
||||
var error = Assert.IsType<WorkOrderBoardValidationErrorDto>(response.Value);
|
||||
|
|
@ -131,6 +119,48 @@ public class WorkOrderMediaControllerTests
|
|||
storage.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddMedia_OversizeUnsupportedType_ReportsUnsupportedTypeNotOversizeVideo()
|
||||
{
|
||||
var file = OversizeFile(150_000_000, "payload.exe", "application/octet-stream", [0x4D, 0x5A]);
|
||||
var storage = new Mock<IFileStoragePort>(MockBehavior.Strict);
|
||||
var controller = CreateController(storage: storage);
|
||||
|
||||
var result = await controller.AddMedia(1, null, file, CancellationToken.None);
|
||||
|
||||
var response = Assert.IsType<UnprocessableEntityObjectResult>(result);
|
||||
var error = Assert.IsType<WorkOrderBoardValidationErrorDto>(response.Value);
|
||||
Assert.Equal("UnsupportedMediaType", error.Code);
|
||||
storage.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateSize_OversizeUnknownKind_UsesTypeNeutralMessage()
|
||||
{
|
||||
Assert.Equal(
|
||||
"Files must be 100 MB or smaller.",
|
||||
WorkOrderMediaContract.ValidateSize(150_000_000, WorkOrderMediaContract.UploadKind.Unknown));
|
||||
}
|
||||
|
||||
private static readonly byte[] FtypHeader = [0, 0, 0, 0x18, (byte)'f', (byte)'t', (byte)'y', (byte)'p', (byte)'i', (byte)'s', (byte)'o', (byte)'m'];
|
||||
private static readonly byte[] JpegHeader = [0xFF, 0xD8, 0xFF, 0xE0];
|
||||
private static readonly byte[] PdfHeader = [0x25, 0x50, 0x44, 0x46, 0x2D];
|
||||
|
||||
/// <summary>
|
||||
/// A file that reports <paramref name="length"/> bytes but only backs the 512-byte header the
|
||||
/// type rules read, so oversize cases run through real signature validation cheaply.
|
||||
/// </summary>
|
||||
private static FormFile OversizeFile(long length, string fileName, string contentType, byte[] header)
|
||||
{
|
||||
var bytes = new byte[512];
|
||||
header.CopyTo(bytes, 0);
|
||||
return new FormFile(new MemoryStream(bytes), 0, length, "file", fileName)
|
||||
{
|
||||
Headers = new HeaderDictionary(),
|
||||
ContentType = contentType
|
||||
};
|
||||
}
|
||||
|
||||
private static FormFile VideoFormFile(int size, string fileName, string contentType)
|
||||
{
|
||||
var bytes = new byte[size];
|
||||
|
|
|
|||
|
|
@ -88,6 +88,10 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
string? fileUrl = null;
|
||||
try
|
||||
{
|
||||
// Type first, so an unsupported file always reports UnsupportedMediaType instead
|
||||
// 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).
|
||||
// Classify by the same resolved type EnsureAllowed validates against.
|
||||
var sizeMessage = WorkOrderMediaContract.ValidateSize(
|
||||
|
|
@ -97,7 +101,6 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
throw new WorkOrderBoardValidationException("FileTooLarge", sizeMessage);
|
||||
}
|
||||
|
||||
WorkOrderMediaFileRules.EnsureAllowed(file, category);
|
||||
if (WorkOrderMediaContract.ResolveKind(
|
||||
WorkOrderMediaFileRules.ResolveUploadContentType(file), file.FileName)
|
||||
== WorkOrderMediaContract.UploadKind.Video)
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ namespace SeaHaven.Services.Helpers
|
|||
UploadKind.Document when length > MaxDocumentBytes =>
|
||||
"Documents must be 50 MB or smaller.",
|
||||
UploadKind.Unknown when length > MaxVideoBytes =>
|
||||
"Videos must be 100 MB or smaller.",
|
||||
"Files must be 100 MB or smaller.",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue