using System.Reflection; using ErsatzTV.Application.Images; using ErsatzTV.Controllers.Api; using ErsatzTV.Controllers.Api.Requests; using ErsatzTV.Core.Api.Images; using LanguageExt; using MediatR; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; using NSubstitute; using NUnit.Framework; using Shouldly; namespace ErsatzTV.Tests.Controllers; [TestFixture] public class ImagesControllerTests { private ImagesController _controller = null!; private IMediator _mediator = null!; [SetUp] public void SetUp() { _mediator = Substitute.For(); _controller = new ImagesController(_mediator); } [Test] public void Controller_Should_Expose_Idiomatic_Rest_Routes() { ShouldHaveActionRoute(nameof(ImagesController.GetFolders), "GET", "/api/v1/images/folders"); ShouldHaveActionRoute( nameof(ImagesController.UpdateDuration), "PUT", "/api/v1/images/folders/{id:int}/duration"); } [Test] public async Task GetFolders_Should_Map_Duration_Option_To_Nullable() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns( [ new ImageFolderViewModel(1, "Root", "/images", 2, 5, Option.None), new ImageFolderViewModel(2, "Child", "/images/child", 0, 3, Option.Some(4.5)) ]); List result = await _controller.GetFolders(null, CancellationToken.None); result.Count.ShouldBe(2); result[0].DurationSeconds.ShouldBeNull(); result[1].DurationSeconds.ShouldBe(4.5); result[1].Name.ShouldBe("Child"); } [Test] public async Task GetFolders_Should_Pass_None_When_ParentId_Omitted() { _mediator.Send(Arg.Any(), Arg.Any()).Returns([]); await _controller.GetFolders(null, CancellationToken.None); await _mediator.Received().Send( Arg.Is(q => q.LibraryFolderId.IsNone), Arg.Any()); } [Test] public async Task GetFolders_Should_Pass_Some_When_ParentId_Given() { _mediator.Send(Arg.Any(), Arg.Any()).Returns([]); await _controller.GetFolders(42, CancellationToken.None); await _mediator.Received().Send( Arg.Is(q => q.LibraryFolderId == Option.Some(42)), Arg.Any()); } [Test] public async Task UpdateDuration_Should_Return_400_For_Non_Positive_Duration() { IActionResult result = await _controller.UpdateDuration( 1, new UpdateImageFolderDurationRequest(0), CancellationToken.None); result.ShouldBeOfType().StatusCode.ShouldBe(400); await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); } [Test] public async Task UpdateDuration_Should_Return_404_When_Folder_Missing() { _mediator.Send(Arg.Any(), Arg.Any()).Returns(false); IActionResult result = await _controller.UpdateDuration( 1, new UpdateImageFolderDurationRequest(3.0), CancellationToken.None); result.ShouldBeOfType().StatusCode.ShouldBe(404); await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); } [Test] public async Task UpdateDuration_Should_Return_200_And_Update_When_Valid() { _mediator.Send(Arg.Any(), Arg.Any()).Returns(true); _mediator.Send(Arg.Any(), Arg.Any()).Returns(3.0); IActionResult result = await _controller.UpdateDuration( 1, new UpdateImageFolderDurationRequest(3.0), CancellationToken.None); var ok = result.ShouldBeOfType(); ok.Value.ShouldBeOfType().DurationSeconds.ShouldBe(3.0); } [Test] public async Task UpdateDuration_Should_Allow_Null_To_Clear() { _mediator.Send(Arg.Any(), Arg.Any()).Returns(true); _mediator.Send(Arg.Any(), Arg.Any()) .Returns((double?)null); IActionResult result = await _controller.UpdateDuration( 1, new UpdateImageFolderDurationRequest(null), CancellationToken.None); result.ShouldBeOfType() .Value.ShouldBeOfType().DurationSeconds.ShouldBeNull(); } private static void ShouldHaveActionRoute(string actionName, string httpMethod, string route) { MethodInfo action = typeof(ImagesController).GetMethod(actionName) ?? throw new AssertionException($"Missing action {actionName}"); HttpMethodAttribute attribute = action.GetCustomAttributes(inherit: true).Single(); attribute.HttpMethods.ShouldContain(httpMethod); attribute.Template.ShouldBe(route); } }