using System.Reflection; using ErsatzTV.Application.Artworks; using ErsatzTV.Controllers.Api; using ErsatzTV.Core; using ErsatzTV.Core.Api.Artwork; using ErsatzTV.Core.Domain; using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; using NSubstitute; using NUnit.Framework; using Shouldly; using static LanguageExt.Prelude; namespace ErsatzTV.Tests.Controllers; [TestFixture] public class ArtworkUploadControllerTests { private ArtworkUploadController _controller = null!; private IMediator _mediator = null!; [SetUp] public void SetUp() { _mediator = Substitute.For(); _controller = new ArtworkUploadController(_mediator); } [Test] public void Controller_Should_Expose_Idiomatic_Rest_Route() { MethodInfo action = typeof(ArtworkUploadController).GetMethod(nameof(ArtworkUploadController.Upload)) ?? throw new AssertionException("Missing action Upload"); HttpMethodAttribute attribute = action.GetCustomAttributes(inherit: true).Single(); attribute.HttpMethods.ShouldContain("POST"); attribute.Template.ShouldBe("/api/v1/artwork/uploads"); attribute.Name.ShouldBe("UploadArtwork"); } [Test] public void Action_Should_Consume_Multipart_Form_Data() { MethodInfo action = typeof(ArtworkUploadController).GetMethod(nameof(ArtworkUploadController.Upload)) ?? throw new AssertionException("Missing action Upload"); var consumes = action.GetCustomAttribute(); consumes.ShouldNotBeNull(); consumes.ContentTypes.ShouldContain("multipart/form-data"); } [Test] public async Task Upload_Should_Return_422_When_File_Missing() { IActionResult result = await _controller.Upload(null!, "logo", CancellationToken.None); var unprocessable = result.ShouldBeOfType(); var problem = unprocessable.Value.ShouldBeOfType(); problem.Status.ShouldBe(422); problem.Title.ShouldBe("Validation failed"); await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); } [Test] public async Task Upload_Should_Return_422_When_File_Empty() { IFormFile emptyFile = MakeFormFile([], "image/png"); IActionResult result = await _controller.Upload(emptyFile, "logo", CancellationToken.None); result.ShouldBeOfType(); await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); } [Test] public async Task Upload_Should_Return_422_When_File_Exceeds_Maximum_Size() { var oversizeBytes = new byte[(SystemEnvironment.MaximumUploadMb * 1024 * 1024) + 1]; IFormFile oversizeFile = MakeFormFile(oversizeBytes, "image/png"); IActionResult result = await _controller.Upload(oversizeFile, "logo", CancellationToken.None); var unprocessable = result.ShouldBeOfType(); var problem = unprocessable.Value.ShouldBeOfType(); problem.Detail.ShouldContain("maximum allowed size"); await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); } [Test] public async Task Upload_Should_Return_422_For_Unknown_Target() { IFormFile file = MakeFormFile([1, 2, 3], "image/png"); IActionResult result = await _controller.Upload(file, "poster", CancellationToken.None); var unprocessable = result.ShouldBeOfType(); var problem = unprocessable.Value.ShouldBeOfType(); problem.Detail.ShouldContain("Unknown upload target"); await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); } [Test] public async Task Upload_Should_Send_UploadArtwork_With_Logo_Kind_And_Return_201() { IFormFile file = MakeFormFile([1, 2, 3], "image/png"); _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Right( new ArtworkUploadResponseModel("iptv/logos/abc.png", "image/png"))); IActionResult result = await _controller.Upload(file, "logo", CancellationToken.None); var created = result.ShouldBeOfType(); created.StatusCode.ShouldBe(201); // No ?contentType= — the serve route sniffs the stored file (issue #283). created.Location.ShouldBe("/iptv/logos/abc.png"); created.Value.ShouldBeOfType() .Path.ShouldBe("iptv/logos/abc.png"); await _mediator.Received(1).Send( Arg.Is(c => c.ArtworkKind == ArtworkKind.Logo), Arg.Any()); } [Test] public async Task Upload_Should_Send_UploadArtwork_With_Watermark_Kind_And_Return_201() { IFormFile file = MakeFormFile([1, 2, 3], "image/webp"); _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Right( new ArtworkUploadResponseModel("def.webp", "image/webp"))); IActionResult result = await _controller.Upload(file, "watermark", CancellationToken.None); var created = result.ShouldBeOfType(); created.Location.ShouldBe("/artwork/watermarks/def.webp"); await _mediator.Received(1).Send( Arg.Is(c => c.ArtworkKind == ArtworkKind.Watermark), Arg.Any()); } [Test] public async Task Upload_Should_Return_422_On_Handler_Validation_Error() { IFormFile file = MakeFormFile([1, 2, 3], "image/bmp"); _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Left(BaseError.New("unsupported content type"))); IActionResult result = await _controller.Upload(file, "logo", CancellationToken.None); var unprocessable = result.ShouldBeOfType(); var problem = unprocessable.Value.ShouldBeOfType(); problem.Status.ShouldBe(422); problem.Title.ShouldBe("Validation failed"); } private static IFormFile MakeFormFile(byte[] bytes, string contentType) => new FormFile(new MemoryStream(bytes), 0, bytes.Length, "file", "upload.bin") { Headers = new HeaderDictionary(), ContentType = contentType }; }