Files
ersatztv/ErsatzTV.Tests/Controllers/MaintenanceControllerTests.cs
T
timothyandClaude Opus 4.8 5e5f0af684 fix(235-A): normalize async-op error contracts on Maintenance + Troubleshoot controllers (#235)
Slice A of the async-op API contract normalization.

MaintenanceController:
- EmptyTrash error path: was 500 text/plain (error.ToString()); now maps the
  BaseError Left through ApiResults.ToErrorResult() -> 404 (NotFoundError) / 422
  ProblemDetails. Success stays 200 OkResult. Added ProducesResponseType 200 + 422.
- CleanArtwork: fire-and-forget enqueue of DeleteOrphanedArtwork was a silent 200;
  now returns 202 Accepted (AcceptedResult) since it queues background work.
  Added ProducesResponseType 202. (Controller does not derive from ControllerBase,
  so results are built directly as before.)

TroubleshootController.TroubleshootPlayback (GET|HEAD /api/troubleshoot/playback.m3u8):
- Two bare body-less NotFound() call sites conflated "not found" with "prepare/
  playback failure". Both now return a ProblemDetails body:
  * prepare-failure (result.IsLeft): mapped through error.ToErrorResult() -> 404 for
    NotFoundError (unknown media item/channel) else 422 for a validation BaseError.
  * terminal fall-through (prepare ok but no playable output): kept 404 with a
    distinguishing ApiResults.NotFoundProblem(...) detail.
- Added ProducesResponseType 404 + 422 (409 already present).

Consumer check: the SPA (PlaybackTroubleshootingScreen) feeds the playback.m3u8 URL
straight to hls.js via HlsPlayer, which never inspects the HTTP status code — playback
state is surfaced via the separate /api/troubleshoot/playback/status poll. So the
404->422 split for the validation subcase is safe; no player code branches on the
status code.

Tests: MaintenanceControllerTests (200/422/202 + enqueue assertion),
TroubleshootControllerTests (prepare 404 NotFoundError, 422 validation). All green;
Api error-metadata/contract/security scans still pass.

Note: OpenAPI artifacts (v1.json / v1.d.ts) intentionally NOT regenerated here — the
orchestrator regenerates once after all #235 slices merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:54:21 +02:00

68 lines
2.1 KiB
C#

using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.Maintenance;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Core;
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 MaintenanceControllerTests
{
private MaintenanceController _controller = null!;
private IMediator _mediator = null!;
private Channel<IBackgroundServiceRequest> _workerChannel = null!;
[SetUp]
public void SetUp()
{
_mediator = Substitute.For<IMediator>();
_workerChannel = Channel.CreateUnbounded<IBackgroundServiceRequest>();
_controller = new MaintenanceController(_mediator, _workerChannel.Writer);
}
[Test]
public async Task EmptyTrash_Should_Return_200_On_Success()
{
_mediator.Send(Arg.Any<EmptyTrash>())
.Returns(Right<BaseError, LanguageExt.Unit>(unit));
IActionResult result = await _controller.EmptyTrash();
result.ShouldBeOfType<OkResult>();
await _mediator.Received(1).Send(Arg.Any<EmptyTrash>());
}
[Test]
public async Task EmptyTrash_Should_Return_422_ProblemDetails_On_Error()
{
_mediator.Send(Arg.Any<EmptyTrash>())
.Returns(Left<BaseError, LanguageExt.Unit>(BaseError.New("Failed to empty trash")));
IActionResult result = await _controller.EmptyTrash();
var unprocessable = result.ShouldBeOfType<UnprocessableEntityObjectResult>();
var problem = unprocessable.Value.ShouldBeOfType<ProblemDetails>();
problem.Status.ShouldBe(422);
problem.Detail.ShouldBe("Failed to empty trash");
}
[Test]
public async Task CleanArtwork_Should_Return_202_And_Enqueue_DeleteOrphanedArtwork()
{
IActionResult result = await _controller.CleanArtwork(CancellationToken.None);
result.ShouldBeOfType<AcceptedResult>();
_workerChannel.Reader.TryRead(out IBackgroundServiceRequest? enqueued).ShouldBeTrue();
enqueued.ShouldBeOfType<DeleteOrphanedArtwork>();
}
}