WatermarkResponseModel gains ImageSource so a client can identify logo-driven presets generically instead of matching a user-editable name. Additive under the frozen-additive /api/v1 contract (#286). Adding a positional record parameter is source-breaking for existing constructor call sites, so the two test files that built the DTO positionally are updated. WatermarkHandlerTests now seeds its two rows with DIFFERENT image sources so the round-trip assertion proves the field is actually carried through the mapper rather than matching a constant on both. Regenerated v1.json, endpoint-index.md and v1.d.ts; check:api clean. Stripped the inherited UTF-8 BOM from Mapper.cs (#311 fix-as-you-touch). Refs #67
248 lines
8.3 KiB
C#
248 lines
8.3 KiB
C#
using System.Reflection;
|
|
using ErsatzTV.Application.Watermarks;
|
|
using ErsatzTV.Controllers.Api;
|
|
using ErsatzTV.Controllers.Api.Requests;
|
|
using ErsatzTV.Core;
|
|
using ErsatzTV.Core.Api.Watermarks;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Core.Errors;
|
|
using ErsatzTV.FFmpeg.State;
|
|
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 WatermarkControllerTests
|
|
{
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
_mediator = Substitute.For<IMediator>();
|
|
_controller = new WatermarkController(_mediator);
|
|
}
|
|
|
|
private WatermarkController _controller = null!;
|
|
private IMediator _mediator = null!;
|
|
|
|
[Test]
|
|
public void Controller_Should_Expose_Idiomatic_Rest_Routes()
|
|
{
|
|
ShouldHaveActionRoute("GET", "/api/v1/watermarks");
|
|
ShouldHaveActionRoute("GET", "/api/v1/watermarks/{id:int}");
|
|
ShouldHaveActionRoute("POST", "/api/v1/watermarks");
|
|
ShouldHaveActionRoute("PUT", "/api/v1/watermarks/{id:int}");
|
|
ShouldHaveActionRoute("DELETE", "/api/v1/watermarks/{id:int}");
|
|
}
|
|
|
|
[Test]
|
|
public void Mutations_Should_Use_Request_Dtos_For_Wire_Contract()
|
|
{
|
|
ParameterInfo createRequest = typeof(WatermarkController)
|
|
.GetMethod(nameof(WatermarkController.Create))!
|
|
.GetParameters()
|
|
.Single(p => p.Name == "request");
|
|
ParameterInfo updateRequest = typeof(WatermarkController)
|
|
.GetMethod(nameof(WatermarkController.Update))!
|
|
.GetParameters()
|
|
.Single(p => p.Name == "request");
|
|
|
|
createRequest.ParameterType.ShouldBe(typeof(CreateWatermarkRequest));
|
|
updateRequest.ParameterType.ShouldBe(typeof(UpdateWatermarkRequest));
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetAll_Should_Return_Watermarks()
|
|
{
|
|
List<WatermarkResponseModel> models =
|
|
[
|
|
new WatermarkResponseModel(1, "Corner Logo", ChannelWatermarkImageSource.Custom),
|
|
new WatermarkResponseModel(2, "Ticker", ChannelWatermarkImageSource.ChannelLogo)
|
|
];
|
|
_mediator.Send(Arg.Any<GetAllWatermarksForApi>(), Arg.Any<CancellationToken>())
|
|
.Returns(models);
|
|
|
|
List<WatermarkResponseModel> result = await _controller.GetAll(CancellationToken.None);
|
|
|
|
result.ShouldBe(models);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetById_Should_Return_200_For_Some()
|
|
{
|
|
WatermarkFullResponseModel vm = MakeVm(4);
|
|
_mediator.Send(Arg.Any<GetWatermarkByIdForApi>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<WatermarkFullResponseModel>.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<GetWatermarkByIdForApi>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<WatermarkFullResponseModel>.None);
|
|
|
|
IActionResult result = await _controller.GetById(4, CancellationToken.None);
|
|
|
|
var notFound = result.ShouldBeOfType<NotFoundObjectResult>();
|
|
var problemDetails = notFound.Value.ShouldBeOfType<ProblemDetails>();
|
|
problemDetails.Status.ShouldBe(404);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Create_Should_Return_201_With_Location_And_Body()
|
|
{
|
|
_mediator.Send(Arg.Any<CreateWatermark>(), Arg.Any<CancellationToken>())
|
|
.Returns(Right<BaseError, CreateWatermarkResult>(new CreateWatermarkResult(7)));
|
|
WatermarkFullResponseModel vm = MakeVm(7);
|
|
_mediator.Send(Arg.Any<GetWatermarkByIdForApi>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<WatermarkFullResponseModel>.Some(vm));
|
|
|
|
IActionResult result = await _controller.Create(MakeCreateRequest(), CancellationToken.None);
|
|
|
|
var created = result.ShouldBeOfType<CreatedResult>();
|
|
created.StatusCode.ShouldBe(201);
|
|
created.Location.ShouldBe("/api/v1/watermarks/7");
|
|
created.Value.ShouldBe(vm);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Create_Should_Return_422_On_Validation_Error()
|
|
{
|
|
_mediator.Send(Arg.Any<CreateWatermark>(), Arg.Any<CancellationToken>())
|
|
.Returns(Left<BaseError, CreateWatermarkResult>(BaseError.New("bad")));
|
|
|
|
IActionResult result = await _controller.Create(MakeCreateRequest(), CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Update_Should_Return_200_And_Map_Route_Id_To_Command()
|
|
{
|
|
_mediator.Send(Arg.Any<UpdateWatermark>(), Arg.Any<CancellationToken>())
|
|
.Returns(Right<BaseError, UpdateWatermarkResult>(new UpdateWatermarkResult(8)));
|
|
WatermarkFullResponseModel vm = MakeVm(8);
|
|
_mediator.Send(Arg.Any<GetWatermarkByIdForApi>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<WatermarkFullResponseModel>.Some(vm));
|
|
|
|
IActionResult result = await _controller.Update(8, MakeUpdateRequest(), CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<OkObjectResult>().Value.ShouldBe(vm);
|
|
await _mediator.Received(1).Send(
|
|
Arg.Is<UpdateWatermark>(c => c.Id == 8),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Test]
|
|
public async Task Update_Should_Return_404_For_NotFoundError()
|
|
{
|
|
_mediator.Send(Arg.Any<UpdateWatermark>(), Arg.Any<CancellationToken>())
|
|
.Returns(Left<BaseError, UpdateWatermarkResult>(new NotFoundError("missing")));
|
|
|
|
IActionResult result = await _controller.Update(99, MakeUpdateRequest(), CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<NotFoundObjectResult>();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Delete_Should_Return_204_On_Success()
|
|
{
|
|
_mediator.Send(Arg.Any<DeleteWatermark>(), 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<DeleteWatermark>(), Arg.Any<CancellationToken>())
|
|
.Returns(Left<BaseError, Unit>(new NotFoundError("missing")));
|
|
|
|
IActionResult result = await _controller.Delete(9, CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<NotFoundObjectResult>();
|
|
}
|
|
|
|
private static void ShouldHaveActionRoute(string httpMethod, string route)
|
|
{
|
|
bool exists = typeof(WatermarkController)
|
|
.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
|
|
.SelectMany(m => m.GetCustomAttributes<HttpMethodAttribute>(inherit: true))
|
|
.Any(a => a.HttpMethods.Contains(httpMethod) && a.Template == route);
|
|
|
|
exists.ShouldBeTrue($"Missing route {httpMethod} {route}");
|
|
}
|
|
|
|
private static WatermarkFullResponseModel MakeVm(int id) =>
|
|
new(
|
|
id,
|
|
"Corner Logo",
|
|
ChannelWatermarkMode.Permanent,
|
|
ChannelWatermarkImageSource.Custom,
|
|
"abc.png",
|
|
"image/png",
|
|
WatermarkLocation.BottomRight,
|
|
WatermarkSize.Scaled,
|
|
15,
|
|
5,
|
|
5,
|
|
15,
|
|
10,
|
|
80,
|
|
null,
|
|
0,
|
|
false);
|
|
|
|
private static CreateWatermarkRequest MakeCreateRequest() =>
|
|
new(
|
|
"Corner Logo",
|
|
ChannelWatermarkMode.Permanent,
|
|
ChannelWatermarkImageSource.Custom,
|
|
"abc.png",
|
|
"image/png",
|
|
WatermarkLocation.BottomRight,
|
|
WatermarkSize.Scaled,
|
|
15,
|
|
5,
|
|
5,
|
|
15,
|
|
10,
|
|
80,
|
|
null,
|
|
0,
|
|
false);
|
|
|
|
private static UpdateWatermarkRequest MakeUpdateRequest() =>
|
|
new(
|
|
"Corner Logo",
|
|
ChannelWatermarkMode.Permanent,
|
|
ChannelWatermarkImageSource.Custom,
|
|
"abc.png",
|
|
"image/png",
|
|
WatermarkLocation.BottomRight,
|
|
WatermarkSize.Scaled,
|
|
15,
|
|
5,
|
|
5,
|
|
15,
|
|
10,
|
|
80,
|
|
null,
|
|
0,
|
|
false);
|
|
}
|