using System.Reflection; using ErsatzTV.Application.ChannelTemplates; using ErsatzTV.Controllers.Api; using ErsatzTV.Controllers.Api.Requests; using ErsatzTV.Core; using ErsatzTV.Core.Api.ChannelTemplates; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; using ErsatzTV.Core.Scheduling; 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 ChannelTemplateControllerTests { private ChannelTemplateController _controller = null!; private IMediator _mediator = null!; [SetUp] public void SetUp() { _mediator = Substitute.For(); _controller = new ChannelTemplateController(_mediator); } [Test] public void Controller_Should_Expose_Named_Rest_Routes() { ShouldHaveActionRoute( nameof(ChannelTemplateController.GetAll), "GET", "/api/channel-templates", "GetChannelTemplates"); ShouldHaveActionRoute( nameof(ChannelTemplateController.GetDefault), "GET", "/api/channel-templates/default", "GetDefaultChannelTemplate"); ShouldHaveActionRoute( nameof(ChannelTemplateController.SetDefault), "PUT", "/api/channel-templates/default/{id:int}", "SetDefaultChannelTemplate"); ShouldHaveActionRoute( nameof(ChannelTemplateController.GetById), "GET", "/api/channel-templates/{id:int}", "GetChannelTemplateById"); ShouldHaveActionRoute( nameof(ChannelTemplateController.Create), "POST", "/api/channel-templates", "CreateChannelTemplate"); ShouldHaveActionRoute( nameof(ChannelTemplateController.Update), "PUT", "/api/channel-templates/{id:int}", "UpdateChannelTemplate"); ShouldHaveActionRoute( nameof(ChannelTemplateController.Delete), "DELETE", "/api/channel-templates/{id:int}", "DeleteChannelTemplate"); } [Test] public async Task GetAll_Should_Return_ChannelTemplates() { List models = [MakeResponse(1, "Standard", isDefault: true)]; _mediator.Send(Arg.Any(), Arg.Any()) .Returns(models); List result = await _controller.GetAll(CancellationToken.None); result.ShouldBe(models); } [Test] public async Task GetDefault_Should_Return_404_When_Default_Missing() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Option.None); IActionResult result = await _controller.GetDefault(CancellationToken.None); result.ShouldBeOfType(); } [Test] public async Task Create_Should_Return_201_With_Location_And_Body() { ChannelTemplateResponseModel vm = MakeResponse(7, "Custom"); _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Right(vm)); IActionResult result = await _controller.Create(MakeCreateRequest("Custom"), CancellationToken.None); var created = result.ShouldBeOfType(); created.Location.ShouldBe("/api/channel-templates/7"); created.Value.ShouldBe(vm); } [Test] public async Task Create_Should_Map_Request_To_Command() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Right(MakeResponse(7, "Custom"))); await _controller.Create(MakeCreateRequest("Custom"), CancellationToken.None); await _mediator.Received(1).Send( Arg.Is(c => c.Name == "Custom" && c.FFmpegProfileId == 1 && c.PreRollFillerId == 2 && c.MidRollFillerId == 3 && c.PostRollFillerId == 4 && c.ShuffleScheduleItems), Arg.Any()); } [Test] public async Task Update_Should_Return_404_For_NotFoundError() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Left(new NotFoundError("missing"))); IActionResult result = await _controller.Update(99, MakeUpdateRequest("Missing"), CancellationToken.None); result.ShouldBeOfType(); } [Test] public async Task SetDefault_Should_Return_200_With_Default_Template() { ChannelTemplateResponseModel vm = MakeResponse(8, "Default", isDefault: true); _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Right(vm)); IActionResult result = await _controller.SetDefault(8, CancellationToken.None); result.ShouldBeOfType().Value.ShouldBe(vm); await _mediator.Received(1).Send( Arg.Is(c => c.ChannelTemplateId == 8), Arg.Any()); } [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_422_When_System_Template() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(Left(BaseError.New("System templates cannot be deleted."))); IActionResult result = await _controller.Delete(9, CancellationToken.None); result.ShouldBeOfType(); } private static void ShouldHaveActionRoute(string actionName, string httpMethod, string route, string name) { MethodInfo action = typeof(ChannelTemplateController).GetMethod(actionName) ?? throw new AssertionException($"Missing action {actionName}"); HttpMethodAttribute attribute = action.GetCustomAttributes(inherit: true).Single(); attribute.HttpMethods.ShouldContain(httpMethod); attribute.Template.ShouldBe(route); attribute.Name.ShouldBe(name); } private static CreateChannelTemplateRequest MakeCreateRequest(string name) => new( name, "Description", 1, null, null, 2, 3, 4, ChannelStreamSelectorMode.Default, string.Empty, string.Empty, string.Empty, ChannelPlayoutSource.Generated, ChannelPlayoutMode.Continuous, StreamingMode.TransportStreamHybrid, string.Empty, ChannelSubtitleMode.None, ChannelMusicVideoCreditsMode.None, string.Empty, ChannelSongVideoMode.Default, ChannelTranscodeMode.OnDemand, ChannelIdleBehavior.StopOnDisconnect, true, false, FixedStartTimeBehavior.Flexible); private static UpdateChannelTemplateRequest MakeUpdateRequest(string name) => new( name, "Description", 1, null, null, null, null, null, ChannelStreamSelectorMode.Default, string.Empty, string.Empty, string.Empty, ChannelPlayoutSource.Generated, ChannelPlayoutMode.Continuous, StreamingMode.TransportStreamHybrid, string.Empty, ChannelSubtitleMode.None, ChannelMusicVideoCreditsMode.None, string.Empty, ChannelSongVideoMode.Default, ChannelTranscodeMode.OnDemand, ChannelIdleBehavior.StopOnDisconnect, false, false, FixedStartTimeBehavior.Flexible); private static ChannelTemplateResponseModel MakeResponse(int id, string name, bool isDefault = false) => new( id, name, "Description", false, isDefault, 1, null, null, null, null, null, ChannelStreamSelectorMode.Default, string.Empty, string.Empty, string.Empty, ChannelPlayoutSource.Generated, ChannelPlayoutMode.Continuous, StreamingMode.TransportStreamHybrid, string.Empty, ChannelSubtitleMode.None, ChannelMusicVideoCreditsMode.None, string.Empty, ChannelSongVideoMode.Default, ChannelTranscodeMode.OnDemand, ChannelIdleBehavior.StopOnDisconnect, false, false, FixedStartTimeBehavior.Flexible); }