Files
ersatztv/ErsatzTV.Tests/Controllers/ResolutionControllerTests.cs
T
timothyandClaude Fable 5 540def7f17
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m51s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m21s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
fix: address #93 adversarial review findings
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>
2026-07-07 09:22:51 +02:00

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>();
}
}