Backend: 401 documented on all mutating settings/resolution routes; resolution delete distinguishes 404 (unknown) from 422 (not custom); XMLTV enum bridging via exhaustive switch instead of int casts; field-level Arg.Is assertions incl. non-null watermark/filler flow. Frontend: resolution add/delete failures surfaced inline (were silent); partial saves merge succeeded groups via allSettled; empty numeric fields invalid + tunerCount min 1; HLS Direct select shows out-of-list wire values; media-source rows show derived last-scan. ErsatzTV.Tests 495; web suite 145. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
113 lines
4.0 KiB
C#
113 lines
4.0 KiB
C#
using ErsatzTV.Application.Resolutions;
|
|
using ErsatzTV.Controllers.Api;
|
|
using ErsatzTV.Controllers.Api.Requests;
|
|
using ErsatzTV.Core;
|
|
using ErsatzTV.Core.Api.Settings;
|
|
using ErsatzTV.Core.Errors;
|
|
using LanguageExt;
|
|
using MediatR;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using NSubstitute;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
using static LanguageExt.Prelude;
|
|
|
|
namespace ErsatzTV.Tests.Controllers;
|
|
|
|
[TestFixture]
|
|
public class ResolutionControllerTests
|
|
{
|
|
private ResolutionController _controller = null!;
|
|
private IMediator _mediator = null!;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
_mediator = Substitute.For<IMediator>();
|
|
_controller = new ResolutionController(_mediator);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetAll_Should_Map_View_Models_To_Response_Models()
|
|
{
|
|
_mediator.Send(Arg.Any<GetAllResolutions>(), Arg.Any<CancellationToken>())
|
|
.Returns(
|
|
new List<ResolutionViewModel>
|
|
{
|
|
new(1, "1920x1080", 1920, 1080, false),
|
|
new(2, "1280x720", 1280, 720, true)
|
|
});
|
|
|
|
List<ResolutionResponseModel> result = await _controller.GetAll(CancellationToken.None);
|
|
|
|
result.ShouldBe(
|
|
[
|
|
new ResolutionResponseModel(1, "1920x1080", 1920, 1080, false),
|
|
new ResolutionResponseModel(2, "1280x720", 1280, 720, true)
|
|
]);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Create_Should_Return_201_With_Location_And_Body()
|
|
{
|
|
_mediator.Send(Arg.Any<CreateCustomResolution>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<BaseError>.None);
|
|
_mediator.Send(Arg.Any<GetResolutionByName>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<ResolutionViewModel>.Some(new ResolutionViewModel(9, "640x480", 640, 480, true)));
|
|
|
|
IActionResult result = await _controller.Create(new CreateResolutionRequest(640, 480), CancellationToken.None);
|
|
|
|
var created = result.ShouldBeOfType<CreatedResult>();
|
|
created.StatusCode.ShouldBe(201);
|
|
created.Location.ShouldBe("/api/settings/resolutions/9");
|
|
created.Value.ShouldBe(new ResolutionResponseModel(9, "640x480", 640, 480, true));
|
|
await _mediator.Received(1).Send(
|
|
Arg.Is<CreateCustomResolution>(c => c.Width == 640 && c.Height == 480),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Test]
|
|
public async Task Create_Should_Return_422_When_Resolution_Is_Not_Unique()
|
|
{
|
|
_mediator.Send(Arg.Any<CreateCustomResolution>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<BaseError>.Some(BaseError.New("Resolution width and height must be unique")));
|
|
|
|
IActionResult result = await _controller.Create(new CreateResolutionRequest(1920, 1080), CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Delete_Should_Return_204_On_Success()
|
|
{
|
|
_mediator.Send(Arg.Any<DeleteCustomResolution>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<BaseError>.None);
|
|
|
|
IActionResult result = await _controller.Delete(9, CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<NoContentResult>();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Delete_Should_Return_404_For_Unknown_Resolution()
|
|
{
|
|
_mediator.Send(Arg.Any<DeleteCustomResolution>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<BaseError>.Some(new NotFoundError("Resolution 42 does not exist.")));
|
|
|
|
IActionResult result = await _controller.Delete(42, CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<NotFoundObjectResult>();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Delete_Should_Return_422_For_NonCustom_Resolution()
|
|
{
|
|
_mediator.Send(Arg.Any<DeleteCustomResolution>(), Arg.Any<CancellationToken>())
|
|
.Returns(Option<BaseError>.Some(BaseError.New("Resolution 3 is not a custom resolution.")));
|
|
|
|
IActionResult result = await _controller.Delete(3, CancellationToken.None);
|
|
|
|
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
|
|
}
|
|
}
|