using System.Reflection; using ErsatzTV.Application.Filler; using ErsatzTV.Controllers.Api; using ErsatzTV.Controllers.Api.Requests; using ErsatzTV.Core; using ErsatzTV.Core.Api.Filler; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Errors; using LanguageExt; using MediatR; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; using NSubstitute; using NUnit.Framework; using Shouldly; using static LanguageExt.Prelude; using Unit = LanguageExt.Unit; namespace ErsatzTV.Tests.Controllers; [TestFixture] public class FillerPresetControllerTests { [SetUp] public void SetUp() { _mediator = Substitute.For(); _controller = new FillerPresetController(_mediator); } private FillerPresetController _controller = null!; private IMediator _mediator = null!; [Test] public void Controller_Should_Expose_Idiomatic_Rest_Routes() { ShouldHaveActionRoute("GET", "/api/v1/filler-presets"); ShouldHaveActionRoute("GET", "/api/v1/filler-presets/{id:int}"); ShouldHaveActionRoute("POST", "/api/v1/filler-presets"); ShouldHaveActionRoute("PUT", "/api/v1/filler-presets/{id:int}"); ShouldHaveActionRoute("DELETE", "/api/v1/filler-presets/{id:int}"); } [Test] public void Mutations_Should_Use_Request_Dtos_For_Wire_Contract() { ParameterInfo createRequest = typeof(FillerPresetController) .GetMethod(nameof(FillerPresetController.Create))! .GetParameters() .Single(p => p.Name == "request"); ParameterInfo updateRequest = typeof(FillerPresetController) .GetMethod(nameof(FillerPresetController.Update))! .GetParameters() .Single(p => p.Name == "request"); createRequest.ParameterType.ShouldBe(typeof(CreateFillerPresetRequest)); updateRequest.ParameterType.ShouldBe(typeof(UpdateFillerPresetRequest)); } [Test] public async Task GetAll_Should_Return_FillerPresets() { List models = [ new FillerPresetResponseModel(1, "Intro", FillerKind.PreRoll), new FillerPresetResponseModel(2, "Outro", FillerKind.PostRoll) ]; _mediator.Send(Arg.Any(), Arg.Any()) .Returns(models); List result = await _controller.GetAll(null, CancellationToken.None); result.ShouldBe(models); } [Test] public async Task GetAll_Should_Forward_FillerKind_Filter() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns([]); await _controller.GetAll(FillerKind.MidRoll, CancellationToken.None); await _mediator.Received(1).Send( Arg.Is(q => q.FillerKind == FillerKind.MidRoll), Arg.Any()); } [Test] public async Task GetById_Should_Return_200_For_Some() { FillerPresetFullResponseModel vm = MakeVm(4); _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Option.Some(vm)); IActionResult result = await _controller.GetById(4, CancellationToken.None); result.ShouldBeOfType().Value.ShouldBe(vm); } [Test] public async Task GetById_Should_Return_404_For_None() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Option.None); IActionResult result = await _controller.GetById(4, CancellationToken.None); var notFound = result.ShouldBeOfType(); var problemDetails = notFound.Value.ShouldBeOfType(); problemDetails.Status.ShouldBe(404); } [Test] public async Task Create_Should_Return_201_With_Location_And_Body() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Right(new CreateFillerPresetResult(7))); FillerPresetFullResponseModel vm = MakeVm(7); _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Option.Some(vm)); IActionResult result = await _controller.Create(MakeCreateRequest(), CancellationToken.None); var created = result.ShouldBeOfType(); created.StatusCode.ShouldBe(201); created.Location.ShouldBe("/api/v1/filler-presets/7"); created.Value.ShouldBe(vm); } [Test] public async Task Create_Should_Return_422_On_Validation_Error() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Left(BaseError.New("bad"))); IActionResult result = await _controller.Create(MakeCreateRequest(), CancellationToken.None); result.ShouldBeOfType(); } [Test] public async Task Update_Should_Return_200_And_Map_Route_Id_To_Command() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Right(Unit.Default)); FillerPresetFullResponseModel vm = MakeVm(8); _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Option.Some(vm)); IActionResult result = await _controller.Update(8, MakeUpdateRequest(), CancellationToken.None); result.ShouldBeOfType().Value.ShouldBe(vm); await _mediator.Received(1).Send( Arg.Is(c => c.Id == 8), Arg.Any()); } [Test] public async Task Update_Should_Return_422_For_Error() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Left(BaseError.New("nope"))); IActionResult result = await _controller.Update(99, MakeUpdateRequest(), CancellationToken.None); result.ShouldBeOfType(); } [Test] public async Task Delete_Should_Return_204_On_Success() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Right(Unit.Default)); IActionResult result = await _controller.Delete(9, CancellationToken.None); result.ShouldBeOfType(); } [Test] public async Task Delete_Should_Return_404_For_NotFoundError() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Left(new NotFoundError("missing"))); IActionResult result = await _controller.Delete(9, CancellationToken.None); result.ShouldBeOfType(); } private static void ShouldHaveActionRoute(string httpMethod, string route) { bool exists = typeof(FillerPresetController) .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .SelectMany(m => m.GetCustomAttributes(inherit: true)) .Any(a => a.HttpMethods.Contains(httpMethod) && a.Template == route); exists.ShouldBeTrue($"Missing route {httpMethod} {route}"); } private static FillerPresetFullResponseModel MakeVm(int id) => new( id, "Intro", FillerKind.PreRoll, FillerMode.Count, null, 3, null, true, CollectionType.Collection, 5, null, null, null, null, null, false); private static CreateFillerPresetRequest MakeCreateRequest() => new( "Intro", FillerKind.PreRoll, FillerMode.Count, null, 3, null, true, CollectionType.Collection, 5, null, null, null, null, null, false); private static UpdateFillerPresetRequest MakeUpdateRequest() => new( "Intro", FillerKind.PreRoll, FillerMode.Count, null, 3, null, true, CollectionType.Collection, 5, null, null, null, null, null, false); }