196 lines
7.4 KiB
C#
196 lines
7.4 KiB
C#
using System.Reflection;
|
|
using ErsatzTV.Application.MediaCollections;
|
|
using ErsatzTV.Controllers.Api;
|
|
using ErsatzTV.Controllers.Api.Requests;
|
|
using ErsatzTV.Core;
|
|
using ErsatzTV.Core.Api.SmartCollections;
|
|
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 SmartCollectionControllerTests
|
|
{
|
|
private SmartCollectionController _controller = null!;
|
|
private IMediator _mediator = null!;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
_mediator = Substitute.For<IMediator>();
|
|
_controller = new SmartCollectionController(_mediator);
|
|
}
|
|
|
|
[Test]
|
|
public void Controller_Should_Expose_Idiomatic_Rest_Routes()
|
|
{
|
|
ShouldHaveActionRoute(nameof(SmartCollectionController.GetAll), "GET", "/api/smart-collections");
|
|
ShouldHaveActionRoute(nameof(SmartCollectionController.GetById), "GET", "/api/smart-collections/{id:int}");
|
|
ShouldHaveActionRoute(nameof(SmartCollectionController.Create), "POST", "/api/smart-collections");
|
|
ShouldHaveActionRoute(nameof(SmartCollectionController.Update), "PUT", "/api/smart-collections/{id:int}");
|
|
ShouldHaveActionRoute(nameof(SmartCollectionController.Delete), "DELETE", "/api/smart-collections/{id:int}");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Create_Should_Return_201_With_Location_And_Body()
|
|
{
|
|
var vm = new SmartCollectionViewModel(7, "Kids", "tag:family");
|
|
_mediator.Send(Arg.Any<CreateSmartCollection>(), Arg.Any<CancellationToken>())
|
|
.Returns(Right<BaseError, SmartCollectionViewModel>(vm));
|
|
|
|
IActionResult result = await _controller.Create(
|
|
new CreateSmartCollectionRequest("Kids", "tag:family"),
|
|
CancellationToken.None);
|
|
|
|
var created = result.ShouldBeOfType<CreatedResult>();
|
|
created.StatusCode.ShouldBe(201);
|
|
created.Location.ShouldBe("/api/smart-collections/7");
|
|
created.Value.ShouldBe(vm);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Create_Should_Return_422_On_Validation_Error()
|
|
{
|
|
_mediator.Send(Arg.Any<CreateSmartCollection>(), Arg.Any<CancellationToken>())
|
|
.Returns(Left<BaseError, SmartCollectionViewModel>(BaseError.New("bad")));
|
|
|
|
IActionResult result = await _controller.Create(
|
|
new CreateSmartCollectionRequest(string.Empty, string.Empty),
|
|
CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Create_Should_Map_Request_To_Command()
|
|
{
|
|
_mediator.Send(Arg.Any<CreateSmartCollection>(), Arg.Any<CancellationToken>())
|
|
.Returns(Right<BaseError, SmartCollectionViewModel>(
|
|
new SmartCollectionViewModel(7, "Kids", "tag:family")));
|
|
|
|
await _controller.Create(new CreateSmartCollectionRequest("Kids", "tag:family"), CancellationToken.None);
|
|
|
|
await _mediator.Received(1).Send(
|
|
Arg.Is<CreateSmartCollection>(c => c.Name == "Kids" && c.Query == "tag:family"),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Test]
|
|
public async Task Update_Should_Return_200_And_Map_Route_Id()
|
|
{
|
|
_mediator.Send(Arg.Any<UpdateSmartCollection>(), Arg.Any<CancellationToken>())
|
|
.Returns(Right<BaseError, UpdateSmartCollectionResult>(new UpdateSmartCollectionResult(8)));
|
|
var vm = new SmartCollectionViewModel(8, "Updated", "tag:updated");
|
|
_mediator.Send(Arg.Any<GetSmartCollectionById>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<SmartCollectionViewModel>.Some(vm));
|
|
|
|
IActionResult result = await _controller.Update(
|
|
8,
|
|
new UpdateSmartCollectionRequest("Updated", "tag:updated"),
|
|
CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<OkObjectResult>().Value.ShouldBe(vm);
|
|
await _mediator.Received(1).Send(
|
|
Arg.Is<UpdateSmartCollection>(c =>
|
|
c.Id == 8 && c.Name == "Updated" && c.Query == "tag:updated"),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Test]
|
|
public async Task Update_Should_Return_404_For_NotFoundError()
|
|
{
|
|
_mediator.Send(Arg.Any<UpdateSmartCollection>(), Arg.Any<CancellationToken>())
|
|
.Returns(Left<BaseError, UpdateSmartCollectionResult>(new NotFoundError("missing")));
|
|
|
|
IActionResult result = await _controller.Update(
|
|
99,
|
|
new UpdateSmartCollectionRequest("Missing", "tag:missing"),
|
|
CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<NotFoundObjectResult>();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Delete_Should_Return_204_On_Success()
|
|
{
|
|
_mediator.Send(Arg.Any<DeleteSmartCollection>(), 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_404_For_NotFoundError()
|
|
{
|
|
_mediator.Send(Arg.Any<DeleteSmartCollection>(), Arg.Any<CancellationToken>())
|
|
.Returns(Left<BaseError, Unit>(new NotFoundError("missing")));
|
|
|
|
IActionResult result = await _controller.Delete(9, CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<NotFoundObjectResult>();
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetAll_Should_Return_SmartCollections()
|
|
{
|
|
List<SmartCollectionResponseModel> vms =
|
|
[
|
|
new SmartCollectionResponseModel(1, "Kids", "tag:kids"),
|
|
new SmartCollectionResponseModel(2, "News", "tag:news")
|
|
];
|
|
_mediator.Send(Arg.Any<GetAllSmartCollectionsForApi>(), Arg.Any<CancellationToken>())
|
|
.Returns(vms);
|
|
|
|
List<SmartCollectionResponseModel> result = await _controller.GetAll(CancellationToken.None);
|
|
|
|
result.ShouldBe(vms);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetById_Should_Return_200_For_Some()
|
|
{
|
|
var vm = new SmartCollectionViewModel(4, "Kids", "tag:kids");
|
|
_mediator.Send(Arg.Any<GetSmartCollectionById>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<SmartCollectionViewModel>.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<GetSmartCollectionById>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<SmartCollectionViewModel>.None);
|
|
|
|
IActionResult result = await _controller.GetById(4, CancellationToken.None);
|
|
|
|
var notFound = result.ShouldBeOfType<NotFoundObjectResult>();
|
|
var problemDetails = notFound.Value.ShouldBeOfType<ProblemDetails>();
|
|
problemDetails.Status.ShouldBe(404);
|
|
problemDetails.Title.ShouldBe("Resource not found");
|
|
}
|
|
|
|
private static void ShouldHaveActionRoute(string actionName, string httpMethod, string route)
|
|
{
|
|
MethodInfo action = typeof(SmartCollectionController).GetMethod(actionName)
|
|
?? throw new AssertionException($"Missing action {actionName}");
|
|
|
|
HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
|
|
attribute.HttpMethods.ShouldContain(httpMethod);
|
|
attribute.Template.ShouldBe(route);
|
|
}
|
|
}
|