388 lines
15 KiB
C#
388 lines
15 KiB
C#
using System.Reflection;
|
|
using ErsatzTV.Application.ProgramSchedules;
|
|
using ErsatzTV.Controllers.Api;
|
|
using ErsatzTV.Controllers.Api.Requests;
|
|
using ErsatzTV.Core;
|
|
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 ScheduleControllerTests
|
|
{
|
|
private ScheduleController _controller = null!;
|
|
private IMediator _mediator = null!;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
_mediator = Substitute.For<IMediator>();
|
|
_controller = new ScheduleController(_mediator);
|
|
}
|
|
|
|
[Test]
|
|
public void Controller_Should_Expose_Idiomatic_Rest_Routes()
|
|
{
|
|
ShouldHaveActionRoute(nameof(ScheduleController.GetAll), "GET", "/api/schedules");
|
|
ShouldHaveActionRoute(nameof(ScheduleController.GetById), "GET", "/api/schedules/{id:int}");
|
|
ShouldHaveActionRoute(nameof(ScheduleController.Create), "POST", "/api/schedules");
|
|
ShouldHaveActionRoute(nameof(ScheduleController.Update), "PUT", "/api/schedules/{id:int}");
|
|
ShouldHaveActionRoute(nameof(ScheduleController.Delete), "DELETE", "/api/schedules/{id:int}");
|
|
ShouldHaveActionRoute(nameof(ScheduleController.GetItems), "GET", "/api/schedules/{id:int}/items");
|
|
ShouldHaveActionRoute(nameof(ScheduleController.AddItem), "POST", "/api/schedules/{id:int}/items");
|
|
ShouldHaveActionRoute(nameof(ScheduleController.ReplaceItems), "PUT", "/api/schedules/{id:int}/items");
|
|
ShouldHaveActionRoute(
|
|
nameof(ScheduleController.DeleteItem),
|
|
"DELETE",
|
|
"/api/schedules/{id:int}/items/{itemId:int}");
|
|
}
|
|
|
|
[Test]
|
|
public void Update_Should_Use_Stable_Update_Request_Dto()
|
|
{
|
|
MethodInfo action = typeof(ScheduleController).GetMethod(nameof(ScheduleController.Update))
|
|
?? throw new AssertionException($"Missing action {nameof(ScheduleController.Update)}");
|
|
|
|
action.GetParameters()[1].ParameterType.ShouldBe(typeof(UpdateScheduleRequest));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Create_Should_Return_201_With_Location_And_Body()
|
|
{
|
|
_mediator.Send(Arg.Any<CreateProgramSchedule>(), Arg.Any<CancellationToken>())
|
|
.Returns(Right<BaseError, CreateProgramScheduleResult>(new CreateProgramScheduleResult(5)));
|
|
ProgramScheduleViewModel vm = MakeSchedule(5, "Daily");
|
|
_mediator.Send(Arg.Any<GetProgramScheduleById>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<ProgramScheduleViewModel>.Some(vm));
|
|
|
|
IActionResult result = await _controller.Create(MakeScheduleRequest("Daily"), CancellationToken.None);
|
|
|
|
var created = result.ShouldBeOfType<CreatedResult>();
|
|
created.StatusCode.ShouldBe(201);
|
|
created.Location.ShouldBe("/api/schedules/5");
|
|
created.Value.ShouldBe(vm);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Create_Should_Return_422_On_Validation_Error_With_ProblemDetails()
|
|
{
|
|
_mediator.Send(Arg.Any<CreateProgramSchedule>(), Arg.Any<CancellationToken>())
|
|
.Returns(Left<BaseError, CreateProgramScheduleResult>(BaseError.New("bad")));
|
|
|
|
IActionResult result = await _controller.Create(MakeScheduleRequest(string.Empty), 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_Map_Request_To_Command()
|
|
{
|
|
_mediator.Send(Arg.Any<CreateProgramSchedule>(), Arg.Any<CancellationToken>())
|
|
.Returns(Right<BaseError, CreateProgramScheduleResult>(new CreateProgramScheduleResult(5)));
|
|
_mediator.Send(Arg.Any<GetProgramScheduleById>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<ProgramScheduleViewModel>.Some(MakeSchedule(5, "Daily")));
|
|
|
|
await _controller.Create(MakeScheduleRequest("Daily"), CancellationToken.None);
|
|
|
|
await _mediator.Received(1).Send(
|
|
Arg.Is<CreateProgramSchedule>(c =>
|
|
c.Name == "Daily" &&
|
|
c.KeepMultiPartEpisodesTogether &&
|
|
c.TreatCollectionsAsShows &&
|
|
c.ShuffleScheduleItems &&
|
|
c.RandomStartPoint &&
|
|
c.FixedStartTimeBehavior == FixedStartTimeBehavior.Flexible),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Test]
|
|
public async Task Update_Should_Return_200_And_Map_Route_Id()
|
|
{
|
|
_mediator.Send(Arg.Any<UpdateProgramSchedule>(), Arg.Any<CancellationToken>())
|
|
.Returns(Right<BaseError, UpdateProgramScheduleResult>(new UpdateProgramScheduleResult(7)));
|
|
ProgramScheduleViewModel vm = MakeSchedule(7, "Updated");
|
|
_mediator.Send(Arg.Any<GetProgramScheduleById>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<ProgramScheduleViewModel>.Some(vm));
|
|
|
|
IActionResult result = await _controller.Update(7, MakeUpdateScheduleRequest("Updated"), CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<OkObjectResult>().Value.ShouldBe(vm);
|
|
await _mediator.Received(1).Send(
|
|
Arg.Is<UpdateProgramSchedule>(c => c.ProgramScheduleId == 7 && c.Name == "Updated"),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Test]
|
|
public async Task Update_Should_Return_404_For_NotFoundError_With_ProblemDetails()
|
|
{
|
|
_mediator.Send(Arg.Any<UpdateProgramSchedule>(), Arg.Any<CancellationToken>())
|
|
.Returns(Left<BaseError, UpdateProgramScheduleResult>(new NotFoundError("missing")));
|
|
|
|
IActionResult result = await _controller.Update(99, MakeUpdateScheduleRequest("Missing"), 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 Delete_Should_Return_204_On_Success()
|
|
{
|
|
_mediator.Send(Arg.Any<DeleteProgramSchedule>(), Arg.Any<CancellationToken>())
|
|
.Returns(Right<BaseError, Unit>(Unit.Default));
|
|
|
|
IActionResult result = await _controller.Delete(3, CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<NoContentResult>();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Delete_Should_Return_404_For_NotFoundError()
|
|
{
|
|
_mediator.Send(Arg.Any<DeleteProgramSchedule>(), Arg.Any<CancellationToken>())
|
|
.Returns(Left<BaseError, Unit>(new NotFoundError("missing")));
|
|
|
|
IActionResult result = await _controller.Delete(99, CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<NotFoundObjectResult>();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetById_Should_Return_200_For_Some()
|
|
{
|
|
ProgramScheduleViewModel vm = MakeSchedule(4, "Daily");
|
|
_mediator.Send(Arg.Any<GetProgramScheduleById>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<ProgramScheduleViewModel>.Some(vm));
|
|
|
|
IActionResult result = await _controller.GetById(4, CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<OkObjectResult>().Value.ShouldBe(vm);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetById_Should_Return_404_For_None()
|
|
{
|
|
_mediator.Send(Arg.Any<GetProgramScheduleById>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<ProgramScheduleViewModel>.None);
|
|
|
|
IActionResult result = await _controller.GetById(4, CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<NotFoundObjectResult>();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetItems_Should_Return_200_With_Items()
|
|
{
|
|
List<ProgramScheduleItemViewModel> items = [MakeOneItem(11)];
|
|
var response = new ProgramScheduleItemsWithDurationViewModel(items, TimeSpan.FromMinutes(25));
|
|
_mediator.Send(Arg.Any<GetProgramScheduleById>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<ProgramScheduleViewModel>.Some(MakeSchedule(4, "Daily")));
|
|
_mediator.Send(Arg.Any<GetProgramScheduleItemsWithDurations>(), Arg.Any<CancellationToken>())
|
|
.Returns(response);
|
|
|
|
IActionResult result = await _controller.GetItems(4, CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<OkObjectResult>().Value.ShouldBe(response);
|
|
await _mediator.Received(1).Send(
|
|
Arg.Is<GetProgramScheduleItemsWithDurations>(q => q.Id == 4),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetItems_Should_Return_404_When_Schedule_Missing()
|
|
{
|
|
_mediator.Send(Arg.Any<GetProgramScheduleById>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<ProgramScheduleViewModel>.None);
|
|
|
|
IActionResult result = await _controller.GetItems(4, CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<NotFoundObjectResult>();
|
|
}
|
|
|
|
[Test]
|
|
public async Task AddItem_Should_Return_201_With_Location_And_Map_Route_Id()
|
|
{
|
|
ProgramScheduleItemViewModel item = MakeOneItem(12);
|
|
_mediator.Send(Arg.Any<AddProgramScheduleItem>(), Arg.Any<CancellationToken>())
|
|
.Returns(Right<BaseError, ProgramScheduleItemViewModel>(item));
|
|
|
|
IActionResult result = await _controller.AddItem(4, MakeItemRequest(PlayoutMode.One), CancellationToken.None);
|
|
|
|
var created = result.ShouldBeOfType<CreatedResult>();
|
|
created.Location.ShouldBe("/api/schedules/4/items/12");
|
|
created.Value.ShouldBe(item);
|
|
await _mediator.Received(1).Send(
|
|
Arg.Is<AddProgramScheduleItem>(c => c.ProgramScheduleId == 4 && c.PlayoutMode == PlayoutMode.One),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Test]
|
|
public async Task AddItem_Should_Return_422_On_Validation_Error()
|
|
{
|
|
_mediator.Send(Arg.Any<AddProgramScheduleItem>(), Arg.Any<CancellationToken>())
|
|
.Returns(Left<BaseError, ProgramScheduleItemViewModel>(BaseError.New("bad")));
|
|
|
|
IActionResult result = await _controller.AddItem(4, MakeItemRequest(PlayoutMode.Duration), CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
|
|
}
|
|
|
|
[Test]
|
|
public async Task ReplaceItems_Should_Return_200_With_Items_And_Map_Route_Id()
|
|
{
|
|
List<ProgramScheduleItemViewModel> items = [MakeOneItem(21), MakeOneItem(22)];
|
|
_mediator.Send(Arg.Any<ReplaceProgramScheduleItems>(), Arg.Any<CancellationToken>())
|
|
.Returns(Right<BaseError, IEnumerable<ProgramScheduleItemViewModel>>(items));
|
|
|
|
IActionResult result = await _controller.ReplaceItems(
|
|
4,
|
|
new ReplaceScheduleItemsRequest([MakeItemRequest(PlayoutMode.One), MakeItemRequest(PlayoutMode.Multiple)]),
|
|
CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<OkObjectResult>().Value.ShouldBe(items);
|
|
await _mediator.Received(1).Send(
|
|
Arg.Is<ReplaceProgramScheduleItems>(c =>
|
|
c.ProgramScheduleId == 4 &&
|
|
c.Items.Count == 2 &&
|
|
c.Items[0].Index == 0 &&
|
|
c.Items[1].PlayoutMode == PlayoutMode.Multiple),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Test]
|
|
public async Task DeleteItem_Should_Return_204_And_Map_Route_Ids()
|
|
{
|
|
_mediator.Send(Arg.Any<DeleteProgramScheduleItem>(), Arg.Any<CancellationToken>())
|
|
.Returns(Right<BaseError, Unit>(Unit.Default));
|
|
|
|
IActionResult result = await _controller.DeleteItem(4, 12, CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<NoContentResult>();
|
|
await _mediator.Received(1).Send(
|
|
Arg.Is<DeleteProgramScheduleItem>(c => c.ProgramScheduleId == 4 && c.ProgramScheduleItemId == 12),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
private static CreateScheduleRequest MakeScheduleRequest(string name) =>
|
|
new(
|
|
name,
|
|
KeepMultiPartEpisodesTogether: true,
|
|
TreatCollectionsAsShows: true,
|
|
ShuffleScheduleItems: true,
|
|
RandomStartPoint: true,
|
|
FixedStartTimeBehavior: FixedStartTimeBehavior.Flexible);
|
|
|
|
private static UpdateScheduleRequest MakeUpdateScheduleRequest(string name) =>
|
|
new(
|
|
name,
|
|
KeepMultiPartEpisodesTogether: true,
|
|
TreatCollectionsAsShows: true,
|
|
ShuffleScheduleItems: true,
|
|
RandomStartPoint: true,
|
|
FixedStartTimeBehavior: FixedStartTimeBehavior.Flexible);
|
|
|
|
private static ScheduleItemRequest MakeItemRequest(PlayoutMode playoutMode) =>
|
|
new(
|
|
StartType.Dynamic,
|
|
StartTime: null,
|
|
FixedStartTimeBehavior: null,
|
|
playoutMode,
|
|
CollectionType.SearchQuery,
|
|
CollectionId: null,
|
|
MultiCollectionId: null,
|
|
SmartCollectionId: null,
|
|
RerunCollectionId: null,
|
|
MediaItemId: null,
|
|
PlaylistId: null,
|
|
SearchTitle: "News",
|
|
SearchQuery: "news",
|
|
PlaybackOrder: PlaybackOrder.Shuffle,
|
|
MarathonGroupBy: MarathonGroupBy.None,
|
|
MarathonShuffleGroups: false,
|
|
MarathonShuffleItems: false,
|
|
MarathonBatchSize: null,
|
|
FillWithGroupMode: FillWithGroupMode.None,
|
|
MultipleMode: MultipleMode.Count,
|
|
MultipleCount: "2",
|
|
PlayoutDuration: TimeSpan.FromMinutes(30),
|
|
TailMode: TailMode.None,
|
|
DiscardToFillAttempts: 3,
|
|
CustomTitle: null,
|
|
GuideMode: GuideMode.Normal,
|
|
PreRollFillerId: null,
|
|
MidRollFillerId: null,
|
|
PostRollFillerId: null,
|
|
TailFillerId: null,
|
|
FallbackFillerId: null,
|
|
WatermarkIds: [],
|
|
GraphicsElementIds: [],
|
|
PreferredAudioLanguageCode: null,
|
|
PreferredAudioTitle: null,
|
|
PreferredSubtitleLanguageCode: null,
|
|
SubtitleMode: null);
|
|
|
|
private static ProgramScheduleViewModel MakeSchedule(int id, string name) =>
|
|
new(id, name, true, true, false, false, FixedStartTimeBehavior.Flexible);
|
|
|
|
private static ProgramScheduleItemOneViewModel MakeOneItem(int id) =>
|
|
new(
|
|
id,
|
|
0,
|
|
StartType.Dynamic,
|
|
null,
|
|
null,
|
|
CollectionType.SearchQuery,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
"News",
|
|
"news",
|
|
PlaybackOrder.Shuffle,
|
|
MarathonGroupBy.None,
|
|
false,
|
|
false,
|
|
null,
|
|
FillWithGroupMode.None,
|
|
null,
|
|
GuideMode.Normal,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
[],
|
|
[],
|
|
null,
|
|
null,
|
|
null,
|
|
null);
|
|
|
|
private static void ShouldHaveActionRoute(string actionName, string httpMethod, string route)
|
|
{
|
|
MethodInfo action = typeof(ScheduleController).GetMethod(actionName)
|
|
?? throw new AssertionException($"Missing action {actionName}");
|
|
|
|
HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
|
|
attribute.HttpMethods.ShouldContain(httpMethod);
|
|
attribute.Template.ShouldBe(route);
|
|
}
|
|
}
|