using Api.SeaHavenIndustries.Controllers; using Data.SeaHavenIndustries; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Moq; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; using SeaHaven.Services.Helpers; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; using System.Security.Claims; 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(); } [Fact] public async Task GetMediaContent_DeletedMedia_ReturnsNotFound() { var service = new Mock(MockBehavior.Strict); service.Setup(candidate => candidate.GetMediaContentAsync( 1, 10, It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new WorkOrderBoardValidationException("NotFound", "Media not found.")); var storage = new Mock(MockBehavior.Strict); var controller = CreateController(service, storage); var result = await controller.GetMediaContent(1, 10, CancellationToken.None); var response = Assert.IsType(result); var error = Assert.IsType(response.Value); Assert.Equal("NotFound", error.Code); storage.Verify(candidate => candidate.TryDelete(It.IsAny()), Times.Never); storage.VerifyNoOtherCalls(); } [Fact] public async Task DeleteMedia_Success_DoesNotCallTryDelete() { var service = new Mock(MockBehavior.Strict); service.Setup(candidate => candidate.DeleteMediaAsync( 1, 10, null, It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); var storage = new Mock(MockBehavior.Strict); var controller = CreateController(service, storage); var result = await controller.DeleteMedia(1, 10); Assert.IsType(result); storage.Verify(candidate => candidate.TryDelete(It.IsAny()), Times.Never); storage.VerifyNoOtherCalls(); } } public class WorkOrderCompletionControllerUploadLimitTests { [Fact] public void UploadCompletionDoc_HasFiftyMegabyteRequestLimit() { var method = typeof(WorkOrderCompletionController).GetMethod( nameof(WorkOrderCompletionController.UploadCompletionDoc)); var attribute = Assert.Single( method!.CustomAttributes, candidate => candidate.AttributeType == typeof(RequestSizeLimitAttribute)); var bytes = Assert.Single(attribute.ConstructorArguments); Assert.Equal(50_000_000L, bytes.Value); } } public class WorkOrderMediaServiceCancellationTests { [Fact] public async Task GetMediaContent_ForwardsCancellationTokenToAllDataReads() { using var source = new CancellationTokenSource(); var token = source.Token; var mediaData = new Mock(MockBehavior.Strict); mediaData.Setup(candidate => candidate.GetWorkOrderForMediaAuthAsync(1, null, token)) .ReturnsAsync(new WorkOrder { Id = 1 }); mediaData.Setup(candidate => candidate.GetAttachmentForReadAsync(10, 1, token)) .ReturnsAsync(new WorkOrderAttachments { Id = 10, WorkorderId = 1, Attachments = "https://example.test/Assets/Documents/report.pdf" }); var storage = new Mock(MockBehavior.Strict); storage.Setup(candidate => candidate.OpenRead( "https://example.test/Assets/Documents/report.pdf")) .Returns(new MemoryStream([1, 2, 3])); var service = new WorkOrderMediaService( mediaData.Object, new Mock(MockBehavior.Strict).Object, new Mock(MockBehavior.Strict).Object, storage.Object); var user = new ClaimsPrincipal(new ClaimsIdentity( new[] { new Claim(ClaimTypes.NameIdentifier, "actor-1"), new Claim(ClaimTypes.Role, "Admin"), new Claim(SeaHavenClaimTypes.OrgScope, SeaHavenClaimTypes.OrgScopeAll) }, "Test")); var result = await service.GetMediaContentAsync(1, 10, user, "actor-1", token); result.Content.Dispose(); mediaData.Verify(candidate => candidate.GetWorkOrderForMediaAuthAsync(1, null, token), Times.Once); mediaData.Verify(candidate => candidate.GetAttachmentForReadAsync(10, 1, token), Times.Once); mediaData.VerifyNoOtherCalls(); } }