Files
ersatztv/ErsatzTV.Tests/Controllers/PlayoutControllerTests.cs
T
timothyandClaude Fable 5 7787b17495 test(api): controller + handler tests for block history and schedule validation
Route assertions, paging clamp, 404/422/400 paths and mediator interaction
for the new PlayoutController/TroubleshootController endpoints; handler tests
for ValidateSequentialSchedule (valid/invalid/throwing) and
GetPlayoutHistoryDetails (found/not-found/malformed JSON via in-memory SQLite).

Refs #145 #158

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:10:46 +02:00

1050 lines
46 KiB
C#

using System.Reflection;
using ErsatzTV.Application.Playouts;
using ErsatzTV.Application.ProgramSchedules;
using ErsatzTV.Application.Scheduling;
using ErsatzTV.Application.Troubleshooting;
using ErsatzTV.Application.Troubleshooting.Queries;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.Playouts;
using ErsatzTV.Core.Api.Scheduling;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
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 PlayoutControllerTests
{
private PlayoutController _controller = null!;
private IMediator _mediator = null!;
[SetUp]
public void SetUp()
{
_mediator = Substitute.For<IMediator>();
_controller = new PlayoutController(_mediator);
}
[Test]
public void Controller_Should_Expose_Idiomatic_Rest_Routes()
{
ShouldHaveActionRoute(nameof(PlayoutController.GetAll), "GET", "/api/playouts");
ShouldHaveActionRoute(nameof(PlayoutController.GetById), "GET", "/api/playouts/{id:int}");
ShouldHaveActionRoute(nameof(PlayoutController.GetItems), "GET", "/api/playouts/{id:int}/items");
ShouldHaveActionRoute(nameof(PlayoutController.GetWarningsCount), "GET", "/api/playouts/warnings/count");
ShouldHaveActionRoute(nameof(PlayoutController.Create), "POST", "/api/playouts");
ShouldHaveActionRoute(nameof(PlayoutController.Update), "PUT", "/api/playouts/{id:int}");
ShouldHaveActionRoute(nameof(PlayoutController.ResetAll), "POST", "/api/playouts/reset-all");
ShouldHaveActionRoute(nameof(PlayoutController.Delete), "DELETE", "/api/playouts/{id:int}");
ShouldHaveActionRoute(nameof(PlayoutController.GetAlternateSchedules), "GET", "/api/playouts/{id:int}/alternate-schedules");
ShouldHaveActionRoute(nameof(PlayoutController.ReplaceAlternateSchedules), "PUT", "/api/playouts/{id:int}/alternate-schedules");
ShouldHaveActionRoute(nameof(PlayoutController.GetTemplates), "GET", "/api/playouts/{id:int}/templates");
ShouldHaveActionRoute(nameof(PlayoutController.ReplaceTemplates), "PUT", "/api/playouts/{id:int}/templates");
ShouldHaveActionRoute(nameof(PlayoutController.GetBlocks), "GET", "/api/playouts/{id:int}/blocks");
ShouldHaveActionRoute(nameof(PlayoutController.GetBlockHistory), "GET", "/api/playouts/{id:int}/blocks/{blockId:int}/history");
ShouldHaveActionRoute(nameof(PlayoutController.GetHistoryDetails), "GET", "/api/playouts/history/{id:int}");
}
[Test]
public void Create_Should_Use_Stable_Request_Dto()
{
MethodInfo action = typeof(PlayoutController).GetMethod(nameof(PlayoutController.Create))
?? throw new AssertionException($"Missing action {nameof(PlayoutController.Create)}");
action.GetParameters()[0].ParameterType.ShouldBe(typeof(CreatePlayoutRequest));
}
[Test]
public async Task Create_Should_Return_201_With_Location_And_Body()
{
_mediator.Send(Arg.Any<CreateClassicPlayout>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, CreatePlayoutResponse>(new CreatePlayoutResponse(9)));
PlayoutNameViewModel vm = MakePlayout(9);
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(vm));
IActionResult result = await _controller.Create(
new CreatePlayoutRequest(3, PlayoutScheduleKind.Classic, 4, null),
CancellationToken.None);
var created = result.ShouldBeOfType<CreatedResult>();
created.StatusCode.ShouldBe(201);
created.Location.ShouldBe("/api/playouts/9");
created.Value.ShouldBe(ToResponse(vm));
}
[Test]
public async Task Create_Should_Map_Request_To_Classic_Playout_Command()
{
_mediator.Send(Arg.Any<CreateClassicPlayout>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, CreatePlayoutResponse>(new CreatePlayoutResponse(9)));
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9)));
await _controller.Create(new CreatePlayoutRequest(3, PlayoutScheduleKind.Classic, 4, null), CancellationToken.None);
await _mediator.Received(1).Send(
Arg.Is<CreateClassicPlayout>(c => c.ChannelId == 3 && c.ProgramScheduleId == 4),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Create_Should_Return_404_For_NotFoundError_With_ProblemDetails()
{
_mediator.Send(Arg.Any<CreateClassicPlayout>(), Arg.Any<CancellationToken>())
.Returns(Left<BaseError, CreatePlayoutResponse>(new NotFoundError("missing")));
IActionResult result = await _controller.Create(
new CreatePlayoutRequest(404, PlayoutScheduleKind.Classic, 4, null),
CancellationToken.None);
var notFound = result.ShouldBeOfType<NotFoundObjectResult>();
var problem = notFound.Value.ShouldBeOfType<ProblemDetails>();
problem.Status.ShouldBe(404);
problem.Title.ShouldBe("Resource not found");
}
[Test]
public async Task Create_Should_Return_422_On_Validation_Error_With_ProblemDetails()
{
_mediator.Send(Arg.Any<CreateClassicPlayout>(), Arg.Any<CancellationToken>())
.Returns(Left<BaseError, CreatePlayoutResponse>(BaseError.New("bad")));
IActionResult result = await _controller.Create(
new CreatePlayoutRequest(3, PlayoutScheduleKind.Classic, 4, null),
CancellationToken.None);
var unprocessable = result.ShouldBeOfType<UnprocessableEntityObjectResult>();
var problem = unprocessable.Value.ShouldBeOfType<ProblemDetails>();
problem.Status.ShouldBe(422);
problem.Title.ShouldBe("Validation failed");
}
[Test]
public async Task Create_Should_Return_422_When_Classic_Missing_ProgramScheduleId()
{
IActionResult result = await _controller.Create(
new CreatePlayoutRequest(3, PlayoutScheduleKind.Classic, null, null),
CancellationToken.None);
var unprocessable = result.ShouldBeOfType<UnprocessableEntityObjectResult>();
var problem = unprocessable.Value.ShouldBeOfType<ProblemDetails>();
problem.Status.ShouldBe(422);
await _mediator.DidNotReceive().Send(Arg.Any<CreateClassicPlayout>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task Create_Should_Map_Block_Kind_With_No_Extra_Fields()
{
_mediator.Send(Arg.Any<CreateBlockPlayout>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, CreatePlayoutResponse>(new CreatePlayoutResponse(9)));
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9)));
await _controller.Create(new CreatePlayoutRequest(3, PlayoutScheduleKind.Block, null, null), CancellationToken.None);
await _mediator.Received(1).Send(
Arg.Is<CreateBlockPlayout>(c => c.ChannelId == 3),
Arg.Any<CancellationToken>());
}
[TestCase(PlayoutScheduleKind.Sequential)]
[TestCase(PlayoutScheduleKind.Scripted)]
[TestCase(PlayoutScheduleKind.ExternalJson)]
public async Task Create_Should_Return_422_For_File_Backed_Kinds_Missing_ScheduleFile(PlayoutScheduleKind kind)
{
IActionResult result = await _controller.Create(
new CreatePlayoutRequest(3, kind, null, null),
CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
}
[Test]
public async Task Create_Should_Map_Sequential_Kind_With_ScheduleFile()
{
_mediator.Send(Arg.Any<CreateSequentialPlayout>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, CreatePlayoutResponse>(new CreatePlayoutResponse(9)));
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9)));
await _controller.Create(
new CreatePlayoutRequest(3, PlayoutScheduleKind.Sequential, null, "/config/schedule.yml"),
CancellationToken.None);
await _mediator.Received(1).Send(
Arg.Is<CreateSequentialPlayout>(c => c.ChannelId == 3 && c.ScheduleFile == "/config/schedule.yml"),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Update_Should_Return_404_When_Playout_Missing()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.None);
IActionResult result = await _controller.Update(
404,
new UpdatePlayoutDetailsRequest(null, null),
CancellationToken.None);
var notFound = result.ShouldBeOfType<NotFoundObjectResult>();
var problem = notFound.Value.ShouldBeOfType<ProblemDetails>();
problem.Status.ShouldBe(404);
await _mediator.DidNotReceive().Send(Arg.Any<UpdatePlayout>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task Update_Should_Apply_DailyRebuildTime_And_Return_200()
{
PlayoutNameViewModel vm = MakePlayout(9);
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(vm));
_mediator.Send(Arg.Any<UpdatePlayout>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, PlayoutNameViewModel>(vm with { DbDailyRebuildTime = TimeSpan.FromHours(4) }));
IActionResult result = await _controller.Update(
9,
new UpdatePlayoutDetailsRequest(TimeSpan.FromHours(4), null),
CancellationToken.None);
result.ShouldBeOfType<OkObjectResult>();
await _mediator.Received(1).Send(
Arg.Is<UpdatePlayout>(c => c.PlayoutId == 9 && c.DailyRebuildTime == Some(TimeSpan.FromHours(4))),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Update_Should_Clear_DailyRebuildTime_When_Null()
{
PlayoutNameViewModel vm = MakePlayout(9);
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(vm));
_mediator.Send(Arg.Any<UpdatePlayout>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, PlayoutNameViewModel>(vm));
await _controller.Update(9, new UpdatePlayoutDetailsRequest(null, null), CancellationToken.None);
await _mediator.Received(1).Send(
Arg.Is<UpdatePlayout>(c => c.PlayoutId == 9 && c.DailyRebuildTime == Option<TimeSpan>.None),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Update_Should_Return_422_When_ScheduleFile_Set_For_Classic_Playout()
{
PlayoutNameViewModel vm = MakePlayout(9);
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(vm));
IActionResult result = await _controller.Update(
9,
new UpdatePlayoutDetailsRequest(null, "/config/schedule.yml"),
CancellationToken.None);
var unprocessable = result.ShouldBeOfType<UnprocessableEntityObjectResult>();
var problem = unprocessable.Value.ShouldBeOfType<ProblemDetails>();
problem.Status.ShouldBe(422);
await _mediator.DidNotReceive().Send(Arg.Any<UpdatePlayout>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task Update_Should_Dispatch_UpdateSequentialPlayout_For_Sequential_Kind()
{
PlayoutNameViewModel vm = MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Sequential };
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(vm));
_mediator.Send(Arg.Any<UpdatePlayout>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, PlayoutNameViewModel>(vm));
_mediator.Send(Arg.Any<UpdateSequentialPlayout>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, PlayoutNameViewModel>(vm with { ScheduleFile = "/config/new.yml" }));
IActionResult result = await _controller.Update(
9,
new UpdatePlayoutDetailsRequest(null, "/config/new.yml"),
CancellationToken.None);
result.ShouldBeOfType<OkObjectResult>();
await _mediator.Received(1).Send(
Arg.Is<UpdateSequentialPlayout>(c => c.PlayoutId == 9 && c.ScheduleFile == "/config/new.yml"),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Delete_Should_Return_204_On_Success()
{
_mediator.Send(Arg.Any<DeletePlayout>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, Unit>(Unit.Default));
IActionResult result = await _controller.Delete(9, CancellationToken.None);
result.ShouldBeOfType<NoContentResult>();
await _mediator.Received(1).Send(
Arg.Is<DeletePlayout>(c => c.PlayoutId == 9),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Delete_Should_Return_404_For_NotFoundError_With_ProblemDetails()
{
_mediator.Send(Arg.Any<DeletePlayout>(), Arg.Any<CancellationToken>())
.Returns(Left<BaseError, Unit>(new NotFoundError("missing")));
IActionResult result = await _controller.Delete(404, CancellationToken.None);
var notFound = result.ShouldBeOfType<NotFoundObjectResult>();
var problem = notFound.Value.ShouldBeOfType<ProblemDetails>();
problem.Status.ShouldBe(404);
problem.Title.ShouldBe("Resource not found");
}
[Test]
public async Task GetById_Should_Return_200_For_Some()
{
PlayoutNameViewModel vm = MakePlayout(9);
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(vm));
IActionResult result = await _controller.GetById(9, CancellationToken.None);
result.ShouldBeOfType<OkObjectResult>().Value.ShouldBe(ToResponse(vm));
}
[Test]
public async Task GetById_Should_Return_404_For_None_With_ProblemDetails()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.None);
IActionResult result = await _controller.GetById(9, CancellationToken.None);
var notFound = result.ShouldBeOfType<NotFoundObjectResult>();
var problem = notFound.Value.ShouldBeOfType<ProblemDetails>();
problem.Status.ShouldBe(404);
problem.Title.ShouldBe("Resource not found");
}
[Test]
public async Task GetAll_Should_Project_Paged_List_With_BuildStatus()
{
var buildStatus = new PlayoutBuildStatus
{
LastBuild = new DateTimeOffset(2026, 7, 2, 10, 0, 0, TimeSpan.Zero),
Success = false,
Message = "boom"
};
PlayoutNameViewModel vm = MakePlayout(9) with
{
BuildStatus = buildStatus,
DbDailyRebuildTime = TimeSpan.FromHours(4)
};
_mediator.Send(Arg.Any<GetPagedPlayouts>(), Arg.Any<CancellationToken>())
.Returns(new PagedPlayoutsViewModel(1, [vm]));
PagedPlayoutsResponseModel result = await _controller.GetAll("q", 2, 25, CancellationToken.None);
result.TotalCount.ShouldBe(1);
PlayoutListItemResponseModel item = result.Page.Single();
item.Id.ShouldBe(9);
item.ChannelNumber.ShouldBe("101");
item.ChannelName.ShouldBe("Channel");
item.ScheduleKind.ShouldBe(PlayoutScheduleKind.Classic);
item.ScheduleName.ShouldBe("Schedule");
item.DailyRebuildTime.ShouldBe(TimeSpan.FromHours(4));
item.BuildStatus.ShouldNotBeNull();
item.BuildStatus.Success.ShouldBeFalse();
item.BuildStatus.Message.ShouldBe("boom");
item.BuildStatus.LastBuild.ShouldBe(buildStatus.LastBuild);
await _mediator.Received(1).Send(
Arg.Is<GetPagedPlayouts>(q => q.Query == "q" && q.PageNum == 2 && q.PageSize == 25),
Arg.Any<CancellationToken>());
}
[Test]
public async Task GetAll_Should_Emit_Null_BuildStatus_When_Absent()
{
PlayoutNameViewModel vm = MakePlayout(9) with { BuildStatus = null };
_mediator.Send(Arg.Any<GetPagedPlayouts>(), Arg.Any<CancellationToken>())
.Returns(new PagedPlayoutsViewModel(1, [vm]));
PagedPlayoutsResponseModel result = await _controller.GetAll("", 0, 100, CancellationToken.None);
result.Page.Single().BuildStatus.ShouldBeNull();
}
[Test]
public async Task GetItems_Should_Project_Items_And_Null_FillerKind_For_Gaps()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9)));
var item = new PlayoutItemViewModel(
"Movie",
new DateTimeOffset(2026, 7, 2, 12, 0, 0, TimeSpan.Zero),
new DateTimeOffset(2026, 7, 2, 13, 0, 0, TimeSpan.Zero),
"1:00:00",
string.Empty,
Some(FillerKind.MidRoll));
var gap = new PlayoutItemViewModel(
"UNSCHEDULED",
new DateTimeOffset(2026, 7, 2, 13, 0, 0, TimeSpan.Zero),
new DateTimeOffset(2026, 7, 2, 13, 30, 0, TimeSpan.Zero),
"30:00",
string.Empty,
Option<FillerKind>.None);
_mediator.Send(Arg.Any<GetFuturePlayoutItemsById>(), Arg.Any<CancellationToken>())
.Returns(new PagedPlayoutItemsViewModel(2, [item, gap]));
IActionResult actionResult = await _controller.GetItems(9, showFiller: true, 1, 10, CancellationToken.None);
var result = actionResult.ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<PagedPlayoutItemsResponseModel>();
result.TotalCount.ShouldBe(2);
result.Page[0].Title.ShouldBe("Movie");
result.Page[0].Duration.ShouldBe("1:00:00");
result.Page[0].FillerKind.ShouldBe(FillerKind.MidRoll);
result.Page[1].Title.ShouldBe("UNSCHEDULED");
result.Page[1].FillerKind.ShouldBeNull();
await _mediator.Received(1).Send(
Arg.Is<GetFuturePlayoutItemsById>(q =>
q.PlayoutId == 9 && q.ShowFiller && q.PageNum == 1 && q.PageSize == 10),
Arg.Any<CancellationToken>());
}
[Test]
public async Task GetItems_Should_Return_404_For_Unknown_Playout_With_ProblemDetails()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.None);
IActionResult result = await _controller.GetItems(404, showFiller: false, 0, 100, CancellationToken.None);
var notFound = result.ShouldBeOfType<NotFoundObjectResult>();
var problem = notFound.Value.ShouldBeOfType<ProblemDetails>();
problem.Status.ShouldBe(404);
problem.Title.ShouldBe("Resource not found");
await _mediator.DidNotReceive().Send(Arg.Any<GetFuturePlayoutItemsById>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task GetWarningsCount_Should_Return_Count()
{
_mediator.Send(Arg.Any<GetPlayoutWarningsCount>(), Arg.Any<CancellationToken>())
.Returns(7);
int result = await _controller.GetWarningsCount(CancellationToken.None);
result.ShouldBe(7);
}
[Test]
public async Task ResetAll_Should_Return_202_And_Send_Command()
{
IActionResult result = await _controller.ResetAll(CancellationToken.None);
result.ShouldBeOfType<AcceptedResult>();
await _mediator.Received(1).Send(Arg.Any<ResetAllPlayouts>(), Arg.Any<CancellationToken>());
}
// ----- Alternate schedules -----
[Test]
public async Task GetAlternateSchedules_Should_Return_404_When_Playout_Missing()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.None);
IActionResult result = await _controller.GetAlternateSchedules(404, CancellationToken.None);
result.ShouldBeOfType<NotFoundObjectResult>();
await _mediator.DidNotReceive().Send(Arg.Any<GetPlayoutAlternateSchedules>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task GetAlternateSchedules_Should_Return_422_For_NonClassic_Playout()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block }));
IActionResult result = await _controller.GetAlternateSchedules(9, CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
await _mediator.DidNotReceive().Send(Arg.Any<GetPlayoutAlternateSchedules>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task GetAlternateSchedules_Should_Project_Ordered_List()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9)));
_mediator.Send(Arg.Any<GetPlayoutAlternateSchedules>(), Arg.Any<CancellationToken>())
.Returns([MakeAltVm(2, 1, 8), MakeAltVm(1, 0, 7)]);
IActionResult actionResult = await _controller.GetAlternateSchedules(9, CancellationToken.None);
var list = actionResult.ShouldBeOfType<OkObjectResult>().Value
.ShouldBeOfType<List<PlayoutAlternateScheduleResponseModel>>();
list.Count.ShouldBe(2);
list[0].Index.ShouldBe(0);
list[0].ProgramScheduleId.ShouldBe(7);
list[1].Index.ShouldBe(1);
list[1].ProgramScheduleId.ShouldBe(8);
}
[Test]
public async Task ReplaceAlternateSchedules_Should_Return_404_When_Playout_Missing()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.None);
IActionResult result = await _controller.ReplaceAlternateSchedules(
404,
new ReplacePlayoutAlternateSchedulesRequest([MakeAltRequest(7)]),
CancellationToken.None);
result.ShouldBeOfType<NotFoundObjectResult>();
await _mediator.DidNotReceive()
.Send(Arg.Any<ReplacePlayoutAlternateScheduleItems>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task ReplaceAlternateSchedules_Should_Return_422_For_NonClassic_Playout()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block }));
IActionResult result = await _controller.ReplaceAlternateSchedules(
9,
new ReplacePlayoutAlternateSchedulesRequest([MakeAltRequest(7)]),
CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
await _mediator.DidNotReceive()
.Send(Arg.Any<ReplacePlayoutAlternateScheduleItems>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task ReplaceAlternateSchedules_Should_Return_422_When_Items_Empty()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9)));
IActionResult result = await _controller.ReplaceAlternateSchedules(
9,
new ReplacePlayoutAlternateSchedulesRequest([]),
CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
await _mediator.DidNotReceive()
.Send(Arg.Any<ReplacePlayoutAlternateScheduleItems>(), Arg.Any<CancellationToken>());
}
[TestCase(0, 1, 12, 31)]
[TestCase(13, 1, 12, 31)]
[TestCase(1, 1, 0, 31)]
[TestCase(1, 1, 13, 31)]
public async Task ReplaceAlternateSchedules_Should_Return_422_For_OutOfRange_Month_Without_Dispatch(
int startMonth,
int startDay,
int endMonth,
int endDay)
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9)));
var request = new ReplacePlayoutAlternateSchedulesRequest(
[new PlayoutAlternateScheduleItemRequest(0, 7, [], [], [], true, startMonth, startDay, null, endMonth, endDay, null)]);
IActionResult result = await _controller.ReplaceAlternateSchedules(9, request, CancellationToken.None);
var unprocessable = result.ShouldBeOfType<UnprocessableEntityObjectResult>();
var problem = unprocessable.Value.ShouldBeOfType<ProblemDetails>();
problem.Status.ShouldBe(422);
await _mediator.DidNotReceive().Send(Arg.Any<GetAllProgramSchedules>(), Arg.Any<CancellationToken>());
await _mediator.DidNotReceive()
.Send(Arg.Any<ReplacePlayoutAlternateScheduleItems>(), Arg.Any<CancellationToken>());
}
[TestCase(0, 1, 12, 31)]
[TestCase(1, 32, 12, 31)]
public async Task ReplaceAlternateSchedules_Should_Return_422_For_OutOfRange_Day_Without_Dispatch(
int startMonth,
int startDay,
int endMonth,
int endDay)
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9)));
var request = new ReplacePlayoutAlternateSchedulesRequest(
[new PlayoutAlternateScheduleItemRequest(0, 7, [], [], [], true, startMonth, startDay, null, endMonth, endDay, null)]);
IActionResult result = await _controller.ReplaceAlternateSchedules(9, request, CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
await _mediator.DidNotReceive()
.Send(Arg.Any<ReplacePlayoutAlternateScheduleItems>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task ReplaceAlternateSchedules_Should_Ignore_DateRange_Fields_When_LimitToDateRange_Is_False()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9)));
_mediator.Send(Arg.Any<GetAllProgramSchedules>(), Arg.Any<CancellationToken>())
.Returns([MakeScheduleVm(7)]);
_mediator.Send(Arg.Any<ReplacePlayoutAlternateScheduleItems>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, Unit>(Unit.Default));
_mediator.Send(Arg.Any<GetPlayoutAlternateSchedules>(), Arg.Any<CancellationToken>())
.Returns([MakeAltVm(1, 0, 7)]);
// LimitToDateRange is false, so the out-of-range month/day here must not block the save.
var request = new ReplacePlayoutAlternateSchedulesRequest(
[new PlayoutAlternateScheduleItemRequest(0, 7, [], [], [], false, 0, 0, null, 13, 32, null)]);
IActionResult result = await _controller.ReplaceAlternateSchedules(9, request, CancellationToken.None);
result.ShouldBeOfType<OkObjectResult>();
await _mediator.Received(1)
.Send(Arg.Any<ReplacePlayoutAlternateScheduleItems>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task ReplaceAlternateSchedules_Should_Return_422_When_ProgramScheduleId_Unknown()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9)));
_mediator.Send(Arg.Any<GetAllProgramSchedules>(), Arg.Any<CancellationToken>())
.Returns([MakeScheduleVm(7)]);
IActionResult result = await _controller.ReplaceAlternateSchedules(
9,
new ReplacePlayoutAlternateSchedulesRequest([MakeAltRequest(7), MakeAltRequest(999)]),
CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
await _mediator.DidNotReceive()
.Send(Arg.Any<ReplacePlayoutAlternateScheduleItems>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task ReplaceAlternateSchedules_Should_Assign_Index_From_Order_And_Return_200()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9)));
_mediator.Send(Arg.Any<GetAllProgramSchedules>(), Arg.Any<CancellationToken>())
.Returns([MakeScheduleVm(7), MakeScheduleVm(8)]);
_mediator.Send(Arg.Any<ReplacePlayoutAlternateScheduleItems>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, Unit>(Unit.Default));
_mediator.Send(Arg.Any<GetPlayoutAlternateSchedules>(), Arg.Any<CancellationToken>())
.Returns([MakeAltVm(1, 0, 7), MakeAltVm(2, 1, 8)]);
IActionResult result = await _controller.ReplaceAlternateSchedules(
9,
new ReplacePlayoutAlternateSchedulesRequest([MakeAltRequest(7), MakeAltRequest(8)]),
CancellationToken.None);
result.ShouldBeOfType<OkObjectResult>();
await _mediator.Received(1).Send(
Arg.Is<ReplacePlayoutAlternateScheduleItems>(c =>
c.PlayoutId == 9 &&
c.Items.Count == 2 &&
c.Items[0].Index == 0 && c.Items[0].ProgramScheduleId == 7 &&
c.Items[1].Index == 1 && c.Items[1].ProgramScheduleId == 8),
Arg.Any<CancellationToken>());
}
// ----- Playout templates -----
[Test]
public async Task GetTemplates_Should_Return_404_When_Playout_Missing()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.None);
IActionResult result = await _controller.GetTemplates(404, CancellationToken.None);
result.ShouldBeOfType<NotFoundObjectResult>();
await _mediator.DidNotReceive().Send(Arg.Any<GetPlayoutTemplates>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task GetTemplates_Should_Return_422_For_NonBlock_Playout()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9)));
IActionResult result = await _controller.GetTemplates(9, CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
await _mediator.DidNotReceive().Send(Arg.Any<GetPlayoutTemplates>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task GetTemplates_Should_Project_Ordered_List()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block }));
_mediator.Send(Arg.Any<GetPlayoutTemplates>(), Arg.Any<CancellationToken>())
.Returns([MakeTemplateVm(2, 1, 8, 5), MakeTemplateVm(1, 0, 7, null)]);
IActionResult actionResult = await _controller.GetTemplates(9, CancellationToken.None);
var list = actionResult.ShouldBeOfType<OkObjectResult>().Value
.ShouldBeOfType<List<PlayoutTemplateResponseModel>>();
list.Count.ShouldBe(2);
list[0].Index.ShouldBe(0);
list[0].TemplateId.ShouldBe(7);
list[0].DecoTemplateId.ShouldBeNull();
list[1].Index.ShouldBe(1);
list[1].TemplateId.ShouldBe(8);
list[1].DecoTemplateId.ShouldBe(5);
}
[Test]
public async Task ReplaceTemplates_Should_Return_404_When_Playout_Missing()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.None);
IActionResult result = await _controller.ReplaceTemplates(
404,
new ReplacePlayoutTemplatesRequest([MakeTemplateRequest(7, null)]),
CancellationToken.None);
result.ShouldBeOfType<NotFoundObjectResult>();
await _mediator.DidNotReceive().Send(Arg.Any<ReplacePlayoutTemplateItems>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task ReplaceTemplates_Should_Return_422_For_NonBlock_Playout()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9)));
IActionResult result = await _controller.ReplaceTemplates(
9,
new ReplacePlayoutTemplatesRequest([MakeTemplateRequest(7, null)]),
CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
await _mediator.DidNotReceive().Send(Arg.Any<ReplacePlayoutTemplateItems>(), Arg.Any<CancellationToken>());
}
[TestCase(0, 1, 12, 31)]
[TestCase(13, 1, 12, 31)]
[TestCase(1, 1, 0, 31)]
[TestCase(1, 1, 13, 31)]
[TestCase(1, 0, 12, 31)]
[TestCase(1, 32, 12, 31)]
public async Task ReplaceTemplates_Should_Return_422_For_OutOfRange_DateRange_Without_Dispatch(
int startMonth,
int startDay,
int endMonth,
int endDay)
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block }));
var request = new ReplacePlayoutTemplatesRequest(
[new PlayoutTemplateItemRequest(0, 7, null, [], [], [], true, startMonth, startDay, null, endMonth, endDay, null)]);
IActionResult result = await _controller.ReplaceTemplates(9, request, CancellationToken.None);
var unprocessable = result.ShouldBeOfType<UnprocessableEntityObjectResult>();
var problem = unprocessable.Value.ShouldBeOfType<ProblemDetails>();
problem.Status.ShouldBe(422);
await _mediator.DidNotReceive().Send(Arg.Any<GetAllTemplates>(), Arg.Any<CancellationToken>());
await _mediator.DidNotReceive().Send(Arg.Any<ReplacePlayoutTemplateItems>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task ReplaceTemplates_Should_Ignore_DateRange_Fields_When_LimitToDateRange_Is_False()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block }));
_mediator.Send(Arg.Any<GetAllTemplates>(), Arg.Any<CancellationToken>())
.Returns([MakeTemplateViewModel(7)]);
_mediator.Send(Arg.Any<ReplacePlayoutTemplateItems>(), Arg.Any<CancellationToken>())
.Returns(Option<BaseError>.None);
_mediator.Send(Arg.Any<GetPlayoutTemplates>(), Arg.Any<CancellationToken>())
.Returns([MakeTemplateVm(1, 0, 7, null)]);
// LimitToDateRange is false, so the out-of-range month/day here must not block the save.
var request = new ReplacePlayoutTemplatesRequest(
[new PlayoutTemplateItemRequest(0, 7, null, [], [], [], false, 0, 0, null, 13, 32, null)]);
IActionResult result = await _controller.ReplaceTemplates(9, request, CancellationToken.None);
result.ShouldBeOfType<OkObjectResult>();
await _mediator.Received(1).Send(Arg.Any<ReplacePlayoutTemplateItems>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task ReplaceTemplates_Should_Return_422_When_TemplateId_Unknown()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block }));
_mediator.Send(Arg.Any<GetAllTemplates>(), Arg.Any<CancellationToken>())
.Returns([MakeTemplateViewModel(7)]);
IActionResult result = await _controller.ReplaceTemplates(
9,
new ReplacePlayoutTemplatesRequest([MakeTemplateRequest(999, null)]),
CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
await _mediator.DidNotReceive().Send(Arg.Any<ReplacePlayoutTemplateItems>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task ReplaceTemplates_Should_Return_422_When_DecoTemplateId_Unknown()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block }));
_mediator.Send(Arg.Any<GetAllTemplates>(), Arg.Any<CancellationToken>())
.Returns([MakeTemplateViewModel(7)]);
_mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
.Returns(Option<DecoTemplateViewModel>.None);
IActionResult result = await _controller.ReplaceTemplates(
9,
new ReplacePlayoutTemplatesRequest([MakeTemplateRequest(7, 999)]),
CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
await _mediator.DidNotReceive().Send(Arg.Any<ReplacePlayoutTemplateItems>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task ReplaceTemplates_Should_Assign_Index_From_Order_And_Return_200()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block }));
_mediator.Send(Arg.Any<GetAllTemplates>(), Arg.Any<CancellationToken>())
.Returns([MakeTemplateViewModel(7), MakeTemplateViewModel(8)]);
_mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
.Returns(Option<DecoTemplateViewModel>.Some(new DecoTemplateViewModel(5, 1, "G", "DT")));
_mediator.Send(Arg.Any<ReplacePlayoutTemplateItems>(), Arg.Any<CancellationToken>())
.Returns(Option<BaseError>.None);
_mediator.Send(Arg.Any<GetPlayoutTemplates>(), Arg.Any<CancellationToken>())
.Returns([MakeTemplateVm(1, 0, 7, null), MakeTemplateVm(2, 1, 8, 5)]);
IActionResult result = await _controller.ReplaceTemplates(
9,
new ReplacePlayoutTemplatesRequest([MakeTemplateRequest(7, null), MakeTemplateRequest(8, 5)]),
CancellationToken.None);
result.ShouldBeOfType<OkObjectResult>();
await _mediator.Received(1).Send(
Arg.Is<ReplacePlayoutTemplateItems>(c =>
c.PlayoutId == 9 &&
c.Items.Count == 2 &&
c.Items[0].Index == 0 && c.Items[0].TemplateId == 7 && c.Items[0].DecoTemplateId == null &&
c.Items[1].Index == 1 && c.Items[1].TemplateId == 8 && c.Items[1].DecoTemplateId == 5),
Arg.Any<CancellationToken>());
}
[Test]
public async Task GetBlocks_Should_Return_404_When_Playout_Missing()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.None);
IActionResult result = await _controller.GetBlocks(404, CancellationToken.None);
result.ShouldBeOfType<NotFoundObjectResult>();
await _mediator.DidNotReceive().Send(Arg.Any<GetAllBlocksForPlayout>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task GetBlocks_Should_Map_Blocks_To_Response_And_Return_200()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block }));
_mediator.Send(Arg.Any<GetAllBlocksForPlayout>(), Arg.Any<CancellationToken>())
.Returns([new BlockViewModel(10, 1, "Morning", "Toons", 60, BlockStopScheduling.AfterDurationEnd)]);
IActionResult result = await _controller.GetBlocks(9, CancellationToken.None);
var list = result.ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<List<BlockResponseModel>>();
list.Count.ShouldBe(1);
list[0].Id.ShouldBe(10);
list[0].Name.ShouldBe("Toons");
list[0].GroupName.ShouldBe("Morning");
}
[Test]
public async Task GetBlockHistory_Should_Return_404_When_Playout_Missing()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.None);
IActionResult result = await _controller.GetBlockHistory(404, 10, cancellationToken: CancellationToken.None);
result.ShouldBeOfType<NotFoundObjectResult>();
await _mediator.DidNotReceive().Send(Arg.Any<GetBlockPlayoutHistory>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task GetBlockHistory_Should_Clamp_Paging_And_Map_Entries()
{
_mediator.Send(Arg.Any<GetPlayoutById>(), Arg.Any<CancellationToken>())
.Returns(Option<PlayoutNameViewModel>.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block }));
_mediator.Send(Arg.Any<GetBlockPlayoutHistory>(), Arg.Any<CancellationToken>())
.Returns(new PagedPlayoutHistoryViewModel(
1,
[new PlayoutHistoryViewModel(99, DateTimeOffset.UnixEpoch, DateTimeOffset.UnixEpoch.AddHours(1), "k", "d")]));
IActionResult result = await _controller.GetBlockHistory(9, 10, -5, 999, CancellationToken.None);
var paged = result.ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<PagedPlayoutHistoryResponseModel>();
paged.TotalCount.ShouldBe(1);
paged.Page.Count.ShouldBe(1);
paged.Page[0].Id.ShouldBe(99);
paged.Page[0].Key.ShouldBe("k");
await _mediator.Received(1).Send(
Arg.Is<GetBlockPlayoutHistory>(q =>
q.PlayoutId == 9 && q.BlockId == 10 && q.PageNum == 0 && q.PageSize == 100),
Arg.Any<CancellationToken>());
}
[Test]
public async Task GetHistoryDetails_Should_Return_200_With_Decoded_Details()
{
_mediator.Send(Arg.Any<GetPlayoutHistoryDetails>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, PlayoutHistoryDetailsViewModel>(
new PlayoutHistoryDetailsViewModel(
PlaybackOrder.Shuffle,
CollectionType.Collection,
"Cartoons",
"Episode",
"S1E1")));
IActionResult result = await _controller.GetHistoryDetails(42, CancellationToken.None);
var details = result.ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<PlayoutHistoryDetailsResponseModel>();
details.PlaybackOrder.ShouldBe(PlaybackOrder.Shuffle);
details.CollectionType.ShouldBe(CollectionType.Collection);
details.Name.ShouldBe("Cartoons");
details.MediaItemType.ShouldBe("Episode");
details.MediaItemTitle.ShouldBe("S1E1");
}
[Test]
public async Task GetHistoryDetails_Should_Return_404_For_NotFoundError()
{
_mediator.Send(Arg.Any<GetPlayoutHistoryDetails>(), Arg.Any<CancellationToken>())
.Returns(Left<BaseError, PlayoutHistoryDetailsViewModel>(new NotFoundError("missing")));
IActionResult result = await _controller.GetHistoryDetails(404, CancellationToken.None);
var notFound = result.ShouldBeOfType<NotFoundObjectResult>();
notFound.Value.ShouldBeOfType<ProblemDetails>().Status.ShouldBe(404);
}
[Test]
public async Task GetHistoryDetails_Should_Return_422_For_Decode_Error()
{
_mediator.Send(Arg.Any<GetPlayoutHistoryDetails>(), Arg.Any<CancellationToken>())
.Returns(Left<BaseError, PlayoutHistoryDetailsViewModel>(BaseError.New("bad json")));
IActionResult result = await _controller.GetHistoryDetails(42, CancellationToken.None);
var unprocessable = result.ShouldBeOfType<UnprocessableEntityObjectResult>();
unprocessable.Value.ShouldBeOfType<ProblemDetails>().Status.ShouldBe(422);
}
private static PlayoutAlternateScheduleViewModel MakeAltVm(int id, int index, int programScheduleId) =>
new(id, index, programScheduleId, [], [], [], false, 1, 1, null, 12, 31, null);
private static PlayoutAlternateScheduleItemRequest MakeAltRequest(int programScheduleId) =>
new(0, programScheduleId, [], [], [], false, 1, 1, null, 12, 31, null);
private static ProgramScheduleViewModel MakeScheduleVm(int id) =>
new(id, $"Schedule {id}", false, false, false, false, FixedStartTimeBehavior.Strict);
private static TemplateViewModel MakeTemplateViewModel(int id) =>
new(id, 1, "Group", $"Template {id}");
private static PlayoutTemplateViewModel MakeTemplateVm(int id, int index, int templateId, int? decoTemplateId) =>
new(
id,
new TemplateViewModel(templateId, 1, "Group", $"Template {templateId}"),
decoTemplateId is { } dtId ? new DecoTemplateViewModel(dtId, 1, "DGroup", $"Deco {dtId}") : null,
index,
[],
[],
[],
false,
1,
1,
null,
12,
31,
null);
private static PlayoutTemplateItemRequest MakeTemplateRequest(int templateId, int? decoTemplateId) =>
new(0, templateId, decoTemplateId, [], [], [], false, 1, 1, null, 12, 31, null);
private static PlayoutNameViewModel MakePlayout(int id) =>
new(
id,
PlayoutScheduleKind.Classic,
"Channel",
"101",
ChannelPlayoutMode.Continuous,
"Schedule",
string.Empty,
null,
new PlayoutBuildStatus(),
null,
null);
private static PlayoutResponseModel ToResponse(PlayoutNameViewModel vm) =>
PlayoutResponseModel.From(
vm.PlayoutId,
vm.ScheduleKind,
vm.ChannelName,
vm.ChannelNumber,
vm.PlayoutMode,
vm.ScheduleName,
vm.ScheduleFile,
vm.DbDailyRebuildTime,
vm.BuildStatus is null
? null
: new PlayoutBuildStatusResponseModel(
vm.BuildStatus.LastBuild,
vm.BuildStatus.Success,
vm.BuildStatus.Message),
vm.DecoId,
vm.DecoName);
private static void ShouldHaveActionRoute(string actionName, string httpMethod, string route)
{
MethodInfo action = typeof(PlayoutController).GetMethod(actionName)
?? throw new AssertionException($"Missing action {actionName}");
HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
attribute.HttpMethods.ShouldContain(httpMethod);
attribute.Template.ShouldBe(route);
}
}