using System.Net.Http; using System.Text.Json; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Hosting; using ZB.MOM.WW.OtOpcUa.Server.Observability; namespace ZB.MOM.WW.OtOpcUa.Server.Tests; [Trait("Category", "Integration")] public sealed class HealthEndpointsHostTests : IAsyncLifetime { private static int _portCounter = 48500 + Random.Shared.Next(0, 99); private readonly int _port = Interlocked.Increment(ref _portCounter); private string Prefix => $"http://localhost:{_port}/"; private readonly DriverHost _driverHost = new(); private HealthEndpointsHost _host = null!; private HttpClient _client = null!; public ValueTask InitializeAsync() { _client = new HttpClient { BaseAddress = new Uri(Prefix) }; return ValueTask.CompletedTask; } public async ValueTask DisposeAsync() { _client.Dispose(); if (_host is not null) await _host.DisposeAsync(); } private HealthEndpointsHost Start(Func? configDbHealthy = null, Func? usingStaleConfig = null) { _host = new HealthEndpointsHost( _driverHost, NullLogger.Instance, configDbHealthy, usingStaleConfig, prefix: Prefix); _host.Start(); return _host; } [Fact] public async Task Healthz_ReturnsHealthy_EmptyFleet() { Start(); var response = await _client.GetAsync("/healthz"); response.IsSuccessStatusCode.ShouldBeTrue(); var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; body.GetProperty("status").GetString().ShouldBe("healthy"); body.GetProperty("configDbReachable").GetBoolean().ShouldBeTrue(); body.GetProperty("usingStaleConfig").GetBoolean().ShouldBeFalse(); } [Fact] public async Task Healthz_StaleConfig_Returns200_WithFlag() { Start(configDbHealthy: () => false, usingStaleConfig: () => true); var response = await _client.GetAsync("/healthz"); response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; body.GetProperty("configDbReachable").GetBoolean().ShouldBeFalse(); body.GetProperty("usingStaleConfig").GetBoolean().ShouldBeTrue(); } [Fact] public async Task Healthz_UnreachableConfig_And_NoCache_Returns503() { Start(configDbHealthy: () => false, usingStaleConfig: () => false); var response = await _client.GetAsync("/healthz"); response.StatusCode.ShouldBe(System.Net.HttpStatusCode.ServiceUnavailable); } [Fact] public async Task Readyz_EmptyFleet_Is200_Healthy() { Start(); var response = await _client.GetAsync("/readyz"); response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; body.GetProperty("verdict").GetString().ShouldBe("Healthy"); } [Fact] public async Task Readyz_WithHealthyDriver_Is200() { await _driverHost.RegisterAsync(new StubDriver("drv-1", DriverState.Healthy), "{}", CancellationToken.None); Start(); var response = await _client.GetAsync("/readyz"); response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; body.GetProperty("verdict").GetString().ShouldBe("Healthy"); body.GetProperty("drivers").GetArrayLength().ShouldBe(1); } [Fact] public async Task Readyz_WithFaultedDriver_Is503() { await _driverHost.RegisterAsync(new StubDriver("dead", DriverState.Faulted), "{}", CancellationToken.None); await _driverHost.RegisterAsync(new StubDriver("alive", DriverState.Healthy), "{}", CancellationToken.None); Start(); var response = await _client.GetAsync("/readyz"); response.StatusCode.ShouldBe(System.Net.HttpStatusCode.ServiceUnavailable); var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; body.GetProperty("verdict").GetString().ShouldBe("Faulted"); } [Fact] public async Task Readyz_WithDegradedDriver_Is200_WithDegradedList() { await _driverHost.RegisterAsync(new StubDriver("drv-ok", DriverState.Healthy), "{}", CancellationToken.None); await _driverHost.RegisterAsync(new StubDriver("drv-deg", DriverState.Degraded), "{}", CancellationToken.None); Start(); var response = await _client.GetAsync("/readyz"); response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; body.GetProperty("verdict").GetString().ShouldBe("Degraded"); body.GetProperty("degradedDrivers").GetArrayLength().ShouldBe(1); body.GetProperty("degradedDrivers")[0].GetString().ShouldBe("drv-deg"); } [Fact] public async Task Readyz_WithInitializingDriver_Is503() { await _driverHost.RegisterAsync(new StubDriver("init", DriverState.Initializing), "{}", CancellationToken.None); Start(); var response = await _client.GetAsync("/readyz"); response.StatusCode.ShouldBe(System.Net.HttpStatusCode.ServiceUnavailable); } [Fact] public async Task Unknown_Path_Returns404() { Start(); var response = await _client.GetAsync("/foo"); response.StatusCode.ShouldBe(System.Net.HttpStatusCode.NotFound); } private sealed class StubDriver : IDriver { private readonly DriverState _state; public StubDriver(string id, DriverState state) { DriverInstanceId = id; _state = state; } public string DriverInstanceId { get; } public string DriverType => "Stub"; public Task InitializeAsync(string _, CancellationToken ct) => Task.CompletedTask; public Task ReinitializeAsync(string _, CancellationToken ct) => Task.CompletedTask; public Task ShutdownAsync(CancellationToken ct) => Task.CompletedTask; public DriverHealth GetHealth() => new(_state, null, null); public long GetMemoryFootprint() => 0; public Task FlushOptionalCachesAsync(CancellationToken ct) => Task.CompletedTask; } }