Files
ersatztv/ErsatzTV.Tests/Controllers/TroubleshootControllerTests.cs
T
timothyandClaude Fable 5 7787b17495 test(api): controller + handler tests for block history and schedule validation
Route assertions, paging clamp, 404/422/400 paths and mediator interaction
for the new PlayoutController/TroubleshootController endpoints; handler tests
for ValidateSequentialSchedule (valid/invalid/throwing) and
GetPlayoutHistoryDetails (found/not-found/malformed JSON via in-memory SQLite).

Refs #145 #158

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:10:46 +02:00

140 lines
5.3 KiB
C#

using System.IO.Abstractions;
using System.Reflection;
using System.Text.Json;
using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.Troubleshooting;
using ErsatzTV.Application.Troubleshooting.Queries;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core.Api.Troubleshooting;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Troubleshooting;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Tests.Controllers;
[TestFixture]
public class TroubleshootControllerTests
{
private TroubleshootController _controller = null!;
private IMediator _mediator = null!;
[SetUp]
public void SetUp()
{
_mediator = Substitute.For<IMediator>();
_controller = new TroubleshootController(
Channel.CreateUnbounded<IFFmpegWorkerRequest>().Writer,
Substitute.For<IFileSystem>(),
Substitute.For<IConfigElementRepository>(),
Substitute.For<ITroubleshootingNotifier>(),
_mediator);
}
[Test]
public void Controller_Should_Expose_Idiomatic_Rest_Route_For_Info()
{
MethodInfo action = typeof(TroubleshootController).GetMethod(nameof(TroubleshootController.GetInfo))
?? throw new AssertionException("Missing action GetInfo");
var attribute = action.GetCustomAttributes<HttpGetAttribute>().Single();
attribute.Template.ShouldBe("api/troubleshoot/info");
attribute.Name.ShouldBe("GetTroubleshootingInfo");
}
[Test]
public async Task GetInfo_Should_Serialize_General_Section_And_Carry_Platform_Capabilities()
{
var info = new TroubleshootingInfo(
"1.2.3",
new Dictionary<string, string> { ["ETV_FOO"] = "bar" },
[],
[],
[],
new ErsatzTV.Application.FFmpegProfiles.FFmpegSettingsViewModel(),
[],
[],
[],
false,
false,
"nvidia output",
"qsv output",
"vaapi output",
"videotoolbox output");
_mediator.Send(Arg.Any<GetTroubleshootingInfo>(), Arg.Any<CancellationToken>())
.Returns(info);
TroubleshootingInfoResponseModel result = await _controller.GetInfo(CancellationToken.None);
result.NvidiaCapabilities.ShouldBe("nvidia output");
result.QsvCapabilities.ShouldBe("qsv output");
result.VaapiCapabilities.ShouldBe("vaapi output");
result.VideoToolboxCapabilities.ShouldBe("videotoolbox output");
using JsonDocument document = JsonDocument.Parse(result.GeneralJson);
document.RootElement.GetProperty("Version").GetString().ShouldBe("1.2.3");
document.RootElement.GetProperty("Environment").GetProperty("ETV_FOO").GetString().ShouldBe("bar");
document.RootElement.GetProperty("AviSynth").GetProperty("Demuxer").GetBoolean().ShouldBeFalse();
}
[Test]
public void Controller_Should_Expose_Idiomatic_Rest_Route_For_ValidateSchedule()
{
MethodInfo action = typeof(TroubleshootController).GetMethod(nameof(TroubleshootController.ValidateSchedule))
?? throw new AssertionException("Missing action ValidateSchedule");
var attribute = action.GetCustomAttributes<HttpPostAttribute>().Single();
attribute.Template.ShouldBe("api/troubleshoot/validate-schedule");
attribute.Name.ShouldBe("ValidateSequentialSchedule");
}
[Test]
public void ValidateSchedule_Should_Use_Stable_Request_Dto()
{
MethodInfo action = typeof(TroubleshootController).GetMethod(nameof(TroubleshootController.ValidateSchedule))
?? throw new AssertionException("Missing action ValidateSchedule");
action.GetParameters()[0].ParameterType.ShouldBe(typeof(ValidateSequentialScheduleRequest));
}
[TestCase("")]
[TestCase(" ")]
public async Task ValidateSchedule_Should_Return_400_For_Empty_Yaml(string yaml)
{
IActionResult result = await _controller.ValidateSchedule(
new ValidateSequentialScheduleRequest(yaml, false),
CancellationToken.None);
var badRequest = result.ShouldBeOfType<BadRequestObjectResult>();
badRequest.Value.ShouldBeOfType<ProblemDetails>().Status.ShouldBe(400);
await _mediator.DidNotReceive().Send(Arg.Any<ValidateSequentialSchedule>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task ValidateSchedule_Should_Map_Result_To_Response()
{
_mediator.Send(Arg.Any<ValidateSequentialSchedule>(), Arg.Any<CancellationToken>())
.Returns(new ValidateSequentialScheduleViewModel(false, ["boom"], "{}"));
IActionResult result = await _controller.ValidateSchedule(
new ValidateSequentialScheduleRequest("content: []", true),
CancellationToken.None);
var response = result.ShouldBeOfType<OkObjectResult>().Value
.ShouldBeOfType<ValidateSequentialScheduleResponseModel>();
response.IsValid.ShouldBeFalse();
response.Messages.ShouldBe(["boom"]);
response.Json.ShouldBe("{}");
await _mediator.Received(1).Send(
Arg.Is<ValidateSequentialSchedule>(q => q.Yaml == "content: []" && q.IsImport),
Arg.Any<CancellationToken>());
}
}