wip: partial implementation salvaged from interrupted workflow run
Untrusted draft — no build/test had run yet. Review before building on it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
using ErsatzTV.Core.Api.Health;
|
||||||
|
using ErsatzTV.Core.Health;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Health;
|
||||||
|
|
||||||
|
internal static class Mapper
|
||||||
|
{
|
||||||
|
internal static HealthCheckResponseModel ProjectToResponseModel(HealthCheckResult result) =>
|
||||||
|
new(
|
||||||
|
result.Title,
|
||||||
|
GetStatus(result.Status),
|
||||||
|
result.Message,
|
||||||
|
result.Link.Match(l => l.Link, () => null));
|
||||||
|
|
||||||
|
private static string GetStatus(HealthCheckStatus status) =>
|
||||||
|
status switch
|
||||||
|
{
|
||||||
|
HealthCheckStatus.Pass => "pass",
|
||||||
|
HealthCheckStatus.Fail => "fail",
|
||||||
|
HealthCheckStatus.Warning => "warn",
|
||||||
|
HealthCheckStatus.Info => "info",
|
||||||
|
HealthCheckStatus.NotApplicable => "info",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(status))
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using ErsatzTV.Core.Api.Health;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Health;
|
||||||
|
|
||||||
|
public record GetAllHealthCheckResultsForApi : IRequest<List<HealthCheckResponseModel>>;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using ErsatzTV.Core.Api.Health;
|
||||||
|
using ErsatzTV.Core.Health;
|
||||||
|
using static ErsatzTV.Application.Health.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Health;
|
||||||
|
|
||||||
|
public class GetAllHealthCheckResultsForApiHandler
|
||||||
|
: IRequestHandler<GetAllHealthCheckResultsForApi, List<HealthCheckResponseModel>>
|
||||||
|
{
|
||||||
|
private readonly IHealthCheckService _healthCheckService;
|
||||||
|
|
||||||
|
public GetAllHealthCheckResultsForApiHandler(IHealthCheckService healthCheckService) =>
|
||||||
|
_healthCheckService = healthCheckService;
|
||||||
|
|
||||||
|
public async Task<List<HealthCheckResponseModel>> Handle(
|
||||||
|
GetAllHealthCheckResultsForApi request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<HealthCheckResult> results = await _healthCheckService.PerformHealthChecks(cancellationToken);
|
||||||
|
return results
|
||||||
|
.Filter(r => r.Status != HealthCheckStatus.NotApplicable)
|
||||||
|
.Map(ProjectToResponseModel)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ErsatzTV.Core.Api.Health;
|
||||||
|
|
||||||
|
public record HealthCheckResponseModel(
|
||||||
|
string Title,
|
||||||
|
string Status,
|
||||||
|
string Detail,
|
||||||
|
string Link);
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
using ErsatzTV.Application.Health;
|
||||||
|
using ErsatzTV.Core.Api.Health;
|
||||||
|
using ErsatzTV.Core.Health;
|
||||||
|
using LanguageExt;
|
||||||
|
using NSubstitute;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Tests.Application.Health;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class GetAllHealthCheckResultsForApiHandlerTests
|
||||||
|
{
|
||||||
|
private IHealthCheckService _healthCheckService = null!;
|
||||||
|
private GetAllHealthCheckResultsForApiHandler _handler = null!;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
_healthCheckService = Substitute.For<IHealthCheckService>();
|
||||||
|
_handler = new GetAllHealthCheckResultsForApiHandler(_healthCheckService);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Should_Map_Status_Codes_To_Lowercase_Strings()
|
||||||
|
{
|
||||||
|
var results = new List<HealthCheckResult>
|
||||||
|
{
|
||||||
|
new("Pass Check", HealthCheckStatus.Pass, "all good", "ok", Option<HealthCheckLink>.None),
|
||||||
|
new("Fail Check", HealthCheckStatus.Fail, "broken", "bad", Option<HealthCheckLink>.None),
|
||||||
|
new("Warn Check", HealthCheckStatus.Warning, "watch out", "warn", Option<HealthCheckLink>.None),
|
||||||
|
new("Info Check", HealthCheckStatus.Info, "fyi", "info", Option<HealthCheckLink>.None)
|
||||||
|
};
|
||||||
|
|
||||||
|
_healthCheckService.PerformHealthChecks(Arg.Any<CancellationToken>()).Returns(results);
|
||||||
|
|
||||||
|
List<HealthCheckResponseModel> response =
|
||||||
|
await _handler.Handle(new GetAllHealthCheckResultsForApi(), CancellationToken.None);
|
||||||
|
|
||||||
|
response.Count.ShouldBe(4);
|
||||||
|
response.Select(r => r.Status).ShouldBe(["pass", "fail", "warn", "info"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Should_Filter_Out_NotApplicable_Results()
|
||||||
|
{
|
||||||
|
var results = new List<HealthCheckResult>
|
||||||
|
{
|
||||||
|
new("Pass Check", HealthCheckStatus.Pass, "all good", "ok", Option<HealthCheckLink>.None),
|
||||||
|
new("NA Check", HealthCheckStatus.NotApplicable, "skip", "skip", Option<HealthCheckLink>.None)
|
||||||
|
};
|
||||||
|
|
||||||
|
_healthCheckService.PerformHealthChecks(Arg.Any<CancellationToken>()).Returns(results);
|
||||||
|
|
||||||
|
List<HealthCheckResponseModel> response =
|
||||||
|
await _handler.Handle(new GetAllHealthCheckResultsForApi(), CancellationToken.None);
|
||||||
|
|
||||||
|
response.Count.ShouldBe(1);
|
||||||
|
response[0].Title.ShouldBe("Pass Check");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Should_Include_Link_When_Present()
|
||||||
|
{
|
||||||
|
var results = new List<HealthCheckResult>
|
||||||
|
{
|
||||||
|
new(
|
||||||
|
"Linked Check",
|
||||||
|
HealthCheckStatus.Warning,
|
||||||
|
"detail message",
|
||||||
|
"brief",
|
||||||
|
Option<HealthCheckLink>.Some(new HealthCheckLink("https://example.com/docs")))
|
||||||
|
};
|
||||||
|
|
||||||
|
_healthCheckService.PerformHealthChecks(Arg.Any<CancellationToken>()).Returns(results);
|
||||||
|
|
||||||
|
List<HealthCheckResponseModel> response =
|
||||||
|
await _handler.Handle(new GetAllHealthCheckResultsForApi(), CancellationToken.None);
|
||||||
|
|
||||||
|
response[0].Link.ShouldBe("https://example.com/docs");
|
||||||
|
response[0].Detail.ShouldBe("detail message");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Should_Have_Null_Link_When_Absent()
|
||||||
|
{
|
||||||
|
var results = new List<HealthCheckResult>
|
||||||
|
{
|
||||||
|
new("No Link Check", HealthCheckStatus.Pass, "detail", "brief", Option<HealthCheckLink>.None)
|
||||||
|
};
|
||||||
|
|
||||||
|
_healthCheckService.PerformHealthChecks(Arg.Any<CancellationToken>()).Returns(results);
|
||||||
|
|
||||||
|
List<HealthCheckResponseModel> response =
|
||||||
|
await _handler.Handle(new GetAllHealthCheckResultsForApi(), CancellationToken.None);
|
||||||
|
|
||||||
|
response[0].Link.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Should_Return_Empty_List_On_Cancellation()
|
||||||
|
{
|
||||||
|
_healthCheckService.PerformHealthChecks(Arg.Any<CancellationToken>())
|
||||||
|
.Returns<Task<List<HealthCheckResult>>>(_ => throw new TaskCanceledException());
|
||||||
|
|
||||||
|
List<HealthCheckResponseModel> response =
|
||||||
|
await _handler.Handle(new GetAllHealthCheckResultsForApi(), CancellationToken.None);
|
||||||
|
|
||||||
|
response.ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using ErsatzTV.Application.Health;
|
||||||
|
using ErsatzTV.Controllers.Api;
|
||||||
|
using ErsatzTV.Core.Api.Health;
|
||||||
|
using MediatR;
|
||||||
|
using NSubstitute;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Tests.Controllers;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class HealthControllerTests
|
||||||
|
{
|
||||||
|
private IMediator _mediator = null!;
|
||||||
|
private HealthController _controller = null!;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
_mediator = Substitute.For<IMediator>();
|
||||||
|
_controller = new HealthController(_mediator);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetAll_Should_Return_Results_From_Mediator()
|
||||||
|
{
|
||||||
|
var expected = new List<HealthCheckResponseModel>
|
||||||
|
{
|
||||||
|
new("Check One", "pass", "all good", null),
|
||||||
|
new("Check Two", "fail", "broken", "https://example.com")
|
||||||
|
};
|
||||||
|
|
||||||
|
_mediator.Send(Arg.Any<GetAllHealthCheckResultsForApi>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(expected);
|
||||||
|
|
||||||
|
List<HealthCheckResponseModel> result = await _controller.GetAll(CancellationToken.None);
|
||||||
|
|
||||||
|
result.ShouldBe(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using ErsatzTV.Application.Health;
|
||||||
|
using ErsatzTV.Core.Api.Health;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Controllers.Api;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[EndpointGroupName("general")]
|
||||||
|
public class HealthController(IMediator mediator) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("/api/health")]
|
||||||
|
[Tags("Health")]
|
||||||
|
[EndpointSummary("Get health check results")]
|
||||||
|
[ProducesResponseType(typeof(List<HealthCheckResponseModel>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<List<HealthCheckResponseModel>> GetAll(CancellationToken cancellationToken) =>
|
||||||
|
await mediator.Send(new GetAllHealthCheckResultsForApi(), cancellationToken);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user