using Microsoft.Extensions.Diagnostics.HealthChecks; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Server.Diagnostics; using ZB.MOM.WW.MxGateway.Server.Sessions; namespace ZB.MOM.WW.MxGateway.Tests.Diagnostics; public sealed class SessionHealthCheckTests { /// /// An idle gateway is healthy. This is the load-bearing case: a gateway holding no sessions is /// the normal steady state on a host nothing dials yet, and a probe that reports red there is /// one operators learn to ignore. /// /// A task that represents the asynchronous operation. [Fact] public async Task Healthy_WhenNoSessionsAreOpen() { var check = new SessionHealthCheck(new SessionRegistry()); HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext()); Assert.Equal(HealthStatus.Healthy, result.Status); Assert.Equal(0, result.Data["total"]); Assert.Equal("No MXAccess sessions are open.", result.Description); } /// Every session ready reports healthy, with the counts carried as entry data. /// A task that represents the asynchronous operation. [Fact] public async Task Healthy_WhenAllSessionsReady() { var check = new SessionHealthCheck(RegistryWith(SessionState.Ready, SessionState.Ready)); HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext()); Assert.Equal(HealthStatus.Healthy, result.Status); Assert.Equal(2, result.Data["total"]); Assert.Equal(2, result.Data["ready"]); Assert.Equal(0, result.Data["faulted"]); } /// /// A faulted session alongside a usable one is degraded, not unhealthy — the gateway is still /// serving the sessions that work. /// /// A task that represents the asynchronous operation. [Fact] public async Task Degraded_WhenSomeFaultedAndSomeReady() { var check = new SessionHealthCheck(RegistryWith(SessionState.Ready, SessionState.Faulted)); HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext()); Assert.Equal(HealthStatus.Degraded, result.Status); Assert.Equal(1, result.Data["ready"]); Assert.Equal(1, result.Data["faulted"]); Assert.Contains("1 faulted", result.Description, StringComparison.Ordinal); } /// /// A session still starting counts as usable for grading, so a fault beside it is degraded /// rather than unhealthy — the startup has not failed yet. /// /// A task that represents the asynchronous operation. [Fact] public async Task Degraded_WhenFaultedBesideAStartingSession() { var check = new SessionHealthCheck( RegistryWith(SessionState.Faulted, SessionState.StartingWorker)); HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext()); Assert.Equal(HealthStatus.Degraded, result.Status); Assert.Equal(1, result.Data["starting"]); } /// Every session faulted is the genuinely bad condition, and the only unhealthy one. /// A task that represents the asynchronous operation. [Fact] public async Task Unhealthy_WhenEverySessionIsFaulted() { var check = new SessionHealthCheck(RegistryWith(SessionState.Faulted, SessionState.Faulted)); HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext()); Assert.Equal(HealthStatus.Unhealthy, result.Status); Assert.Equal(2, result.Data["faulted"]); Assert.Equal(0, result.Data["ready"]); } /// /// Closed sessions linger in the registry until they are removed. They are counted separately /// and excluded from the verdict, so a gateway whose sessions all closed cleanly is healthy — /// not unhealthy for having zero ready ones. /// /// A task that represents the asynchronous operation. [Fact] public async Task Healthy_WhenOnlyClosedSessionsRemain() { var check = new SessionHealthCheck(RegistryWith(SessionState.Closed, SessionState.Closed)); HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext()); Assert.Equal(HealthStatus.Healthy, result.Status); Assert.Equal(2, result.Data["closing"]); Assert.Equal(0, result.Data["ready"]); } private static SessionRegistry RegistryWith(params SessionState[] states) { var registry = new SessionRegistry(); for (int i = 0; i < states.Length; i++) { GatewaySession session = CreateSession($"session-{i}"); session.TransitionTo(states[i]); Assert.True(registry.TryAdd(session)); } return registry; } private static GatewaySession CreateSession(string sessionId) { return new GatewaySession( sessionId, "mxaccess", $"mxaccess-gateway-1-{sessionId}", "nonce", clientIdentity: null, clientSessionName: "test-session", clientCorrelationId: "client-correlation", TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5), DateTimeOffset.UnixEpoch); } }