using System.Net; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.DependencyInjection; using ZB.MOM.WW.Health; namespace ZB.MOM.WW.Health.Tests; /// /// Verifies the three-tier convention: /// each endpoint runs only the checks tagged for its tier, /healthz runs nothing, and the /// standard ASP.NET HealthChecks status-to-HTTP mapping (Healthy/Degraded → 200, Unhealthy → 503) /// holds per tier. /// public sealed class TierMappingTests { /// /// An test double that records each invocation and returns a /// configurable result, so tests can assert which checks actually ran per tier. /// private sealed class RecordingHealthCheck : IHealthCheck { private readonly HealthStatus _status; private int _invocations; public RecordingHealthCheck(HealthStatus status) => _status = status; public int Invocations => Volatile.Read(ref _invocations); public Task CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { Interlocked.Increment(ref _invocations); return Task.FromResult(new HealthCheckResult(_status)); } } private static async Task<(HttpResponseMessage Response, RecordingHealthCheck Ready, RecordingHealthCheck Active)> RunAsync(string path, HealthStatus readyStatus = HealthStatus.Healthy, HealthStatus activeStatus = HealthStatus.Healthy) { var ready = new RecordingHealthCheck(readyStatus); var active = new RecordingHealthCheck(activeStatus); var builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); builder.Services.AddHealthChecks() .AddCheck("ready-check", ready, tags: new[] { ZbHealthTags.Ready }) .AddCheck("active-check", active, tags: new[] { ZbHealthTags.Active }); await using var app = builder.Build(); app.MapZbHealth(); await app.StartAsync(); var client = app.GetTestClient(); var response = await client.GetAsync(path); return (response, ready, active); } [Fact] public async Task ReadyEndpoint_RunsOnlyReadyCheck() { var (response, ready, active) = await RunAsync("/health/ready"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(1, ready.Invocations); Assert.Equal(0, active.Invocations); } [Fact] public async Task ActiveEndpoint_RunsOnlyActiveCheck() { var (response, ready, active) = await RunAsync("/health/active"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(0, ready.Invocations); Assert.Equal(1, active.Invocations); } [Fact] public async Task LivenessEndpoint_RunsNoChecks_AndReturns200() { var (response, ready, active) = await RunAsync("/healthz"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(0, ready.Invocations); Assert.Equal(0, active.Invocations); } [Fact] public async Task ReadyEndpoint_Healthy_Returns200() { var (response, _, _) = await RunAsync("/health/ready", readyStatus: HealthStatus.Healthy); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task ReadyEndpoint_Unhealthy_Returns503() { var (response, _, _) = await RunAsync("/health/ready", readyStatus: HealthStatus.Unhealthy); Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); } [Fact] public async Task ActiveEndpoint_Unhealthy_Returns503() { var (response, _, _) = await RunAsync("/health/active", activeStatus: HealthStatus.Unhealthy); Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); } [Fact] public async Task LivenessEndpoint_UnaffectedByUnhealthyChecks() { // Even though every registered check is Unhealthy, /healthz runs none of them // (predicate _ => false) and stays 200 as long as the process is up. var (response, ready, active) = await RunAsync( "/healthz", readyStatus: HealthStatus.Unhealthy, activeStatus: HealthStatus.Unhealthy); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(0, ready.Invocations); Assert.Equal(0, active.Invocations); } [Fact] public async Task Options_OverrideRoutePaths() { var ready = new RecordingHealthCheck(HealthStatus.Healthy); var active = new RecordingHealthCheck(HealthStatus.Healthy); var builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); builder.Services.AddHealthChecks() .AddCheck("ready-check", ready, tags: new[] { ZbHealthTags.Ready }) .AddCheck("active-check", active, tags: new[] { ZbHealthTags.Active }); await using var app = builder.Build(); app.MapZbHealth(new ZbHealthEndpointOptions { ReadyPath = "/custom/ready", ActivePath = "/custom/active", LivePath = "/custom/live", }); await app.StartAsync(); var client = app.GetTestClient(); var readyResponse = await client.GetAsync("/custom/ready"); Assert.Equal(HttpStatusCode.OK, readyResponse.StatusCode); Assert.Equal(1, ready.Invocations); Assert.Equal(0, active.Invocations); var liveResponse = await client.GetAsync("/custom/live"); Assert.Equal(HttpStatusCode.OK, liveResponse.StatusCode); // The default paths must no longer be mapped when overridden. var defaultReady = await client.GetAsync("/health/ready"); Assert.Equal(HttpStatusCode.NotFound, defaultReady.StatusCode); } }