Files
ersatztv/ErsatzTV.Tests/Controllers/ChannelTemplateControllerTests.cs
T
timothy 7858ac002a
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m34s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m58s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
feat(api): add channel templates
2026-07-06 18:43:39 +02:00

277 lines
9.4 KiB
C#

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<IMediator>();
_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<ChannelTemplateResponseModel> models = [MakeResponse(1, "Standard", isDefault: true)];
_mediator.Send(Arg.Any<GetAllChannelTemplates>(), Arg.Any<CancellationToken>())
.Returns(models);
List<ChannelTemplateResponseModel> result = await _controller.GetAll(CancellationToken.None);
result.ShouldBe(models);
}
[Test]
public async Task GetDefault_Should_Return_404_When_Default_Missing()
{
_mediator.Send(Arg.Any<GetDefaultChannelTemplate>(), Arg.Any<CancellationToken>())
.Returns(Option<ChannelTemplateResponseModel>.None);
IActionResult result = await _controller.GetDefault(CancellationToken.None);
result.ShouldBeOfType<NotFoundObjectResult>();
}
[Test]
public async Task Create_Should_Return_201_With_Location_And_Body()
{
ChannelTemplateResponseModel vm = MakeResponse(7, "Custom");
_mediator.Send(Arg.Any<CreateChannelTemplate>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, ChannelTemplateResponseModel>(vm));
IActionResult result = await _controller.Create(MakeCreateRequest("Custom"), CancellationToken.None);
var created = result.ShouldBeOfType<CreatedResult>();
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<CreateChannelTemplate>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, ChannelTemplateResponseModel>(MakeResponse(7, "Custom")));
await _controller.Create(MakeCreateRequest("Custom"), CancellationToken.None);
await _mediator.Received(1).Send(
Arg.Is<CreateChannelTemplate>(c =>
c.Name == "Custom" &&
c.FFmpegProfileId == 1 &&
c.PreRollFillerId == 2 &&
c.MidRollFillerId == 3 &&
c.PostRollFillerId == 4 &&
c.ShuffleScheduleItems),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Update_Should_Return_404_For_NotFoundError()
{
_mediator.Send(Arg.Any<UpdateChannelTemplate>(), Arg.Any<CancellationToken>())
.Returns(Left<BaseError, ChannelTemplateResponseModel>(new NotFoundError("missing")));
IActionResult result = await _controller.Update(99, MakeUpdateRequest("Missing"), CancellationToken.None);
result.ShouldBeOfType<NotFoundObjectResult>();
}
[Test]
public async Task SetDefault_Should_Return_200_With_Default_Template()
{
ChannelTemplateResponseModel vm = MakeResponse(8, "Default", isDefault: true);
_mediator.Send(Arg.Any<SetDefaultChannelTemplate>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, ChannelTemplateResponseModel>(vm));
IActionResult result = await _controller.SetDefault(8, CancellationToken.None);
result.ShouldBeOfType<OkObjectResult>().Value.ShouldBe(vm);
await _mediator.Received(1).Send(
Arg.Is<SetDefaultChannelTemplate>(c => c.ChannelTemplateId == 8),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Delete_Should_Return_204_On_Success()
{
_mediator.Send(Arg.Any<DeleteChannelTemplate>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, Unit>(Unit.Default));
IActionResult result = await _controller.Delete(9, CancellationToken.None);
result.ShouldBeOfType<NoContentResult>();
}
[Test]
public async Task Delete_Should_Return_422_When_System_Template()
{
_mediator.Send(Arg.Any<DeleteChannelTemplate>(), Arg.Any<CancellationToken>())
.Returns(Left<BaseError, Unit>(BaseError.New("System templates cannot be deleted.")));
IActionResult result = await _controller.Delete(9, CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
}
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<HttpMethodAttribute>(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);
}