using System.Linq; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; using ZB.MOM.WW.Health; namespace ZB.MOM.WW.ScadaBridge.Host.Tests; /// /// WP-12: Tests for the three-tier health endpoints after adopting the shared /// ZB.MOM.WW.Health probes. Verifies that /health/ready, /health/active and the new /// /healthz tier are mapped, and that the readiness/active tier split is now carried by /// the canonical (Ready for database + akka-cluster, Active for /// active-node) rather than by check-name predicates. These are pure route/tag assertions /// — they require no database, LDAP, or formed Akka cluster. /// public class HealthCheckTests : IDisposable { private readonly List _disposables = new(); public HealthCheckTests() { // Host-003: connection strings are externalised; supply them via env vars. _disposables.Add(new CentralDbTestEnvironment()); } public void Dispose() { foreach (var d in _disposables) { try { d.Dispose(); } catch { /* best effort */ } } } private WebApplicationFactory CreateCentralFactory() { var factory = new WebApplicationFactory() .WithWebHostBuilder(builder => { builder.ConfigureAppConfiguration((context, config) => { config.AddInMemoryCollection(new Dictionary { ["ScadaBridge:Node:NodeHostname"] = "localhost", ["ScadaBridge:Node:RemotingPort"] = "0", ["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@localhost:2551", ["ScadaBridge:Cluster:SeedNodes:1"] = "akka.tcp://scadabridge@localhost:2552", ["ScadaBridge:Database:SkipMigrations"] = "true", }); }); builder.UseSetting("ScadaBridge:Node:Role", "Central"); builder.UseSetting("ScadaBridge:Database:SkipMigrations", "true"); }); _disposables.Add(factory); return factory; } private static IEnumerable Registrations(WebApplicationFactory factory) => factory.Services.GetRequiredService>().Value.Registrations; [Fact] public async Task HealthReady_Endpoint_IsMapped() { var previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); try { Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central"); var factory = CreateCentralFactory(); var client = factory.CreateClient(); _disposables.Add(client); var response = await client.GetAsync("/health/ready"); // The endpoint exists and returns a status code. With test infrastructure // (no real DB / cluster) the readiness checks may report Unhealthy, so we // accept either 200 (Healthy/Degraded) or 503 (Unhealthy) — never 404. Assert.NotEqual(System.Net.HttpStatusCode.NotFound, response.StatusCode); Assert.True( response.StatusCode == System.Net.HttpStatusCode.OK || response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable, $"Expected 200 or 503, got {(int)response.StatusCode}"); } finally { Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", previousEnv); } } [Fact] public async Task HealthActive_Endpoint_IsMapped() { var previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); try { Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central"); var factory = CreateCentralFactory(); var client = factory.CreateClient(); _disposables.Add(client); var response = await client.GetAsync("/health/active"); Assert.NotEqual(System.Net.HttpStatusCode.NotFound, response.StatusCode); Assert.True( response.StatusCode == System.Net.HttpStatusCode.OK || response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable, $"Expected 200 or 503, got {(int)response.StatusCode}"); } finally { Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", previousEnv); } } [Fact] public async Task Healthz_LivenessEndpoint_IsMappedAndReturns200() { // New tier added by adopting the shared library: /healthz runs no checks, so it // returns 200 as long as the process is up — independent of DB / cluster state. var previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); try { Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central"); var factory = CreateCentralFactory(); var client = factory.CreateClient(); _disposables.Add(client); var response = await client.GetAsync("/healthz"); Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); } finally { Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", previousEnv); } } [Fact] public void ReadyTier_Carries_Database_And_AkkaCluster() { // Host-001 regression guard: readiness reflects cluster membership + DB connectivity // only (REQ-HOST-4a), NOT cluster leadership. The split is now carried by the Ready tag // rather than a check-name predicate: database + akka-cluster are Ready-tagged, and the // leader-only active-node check is NOT — so a fully operational standby central node // still reports ready on /health/ready. var previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); try { Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central"); var factory = CreateCentralFactory(); var registrations = Registrations(factory).ToDictionary(r => r.Name); Assert.True(registrations.ContainsKey("database"), "Expected a 'database' health check."); Assert.True(registrations.ContainsKey("akka-cluster"), "Expected an 'akka-cluster' health check."); Assert.Contains(ZbHealthTags.Ready, registrations["database"].Tags); Assert.Contains(ZbHealthTags.Ready, registrations["akka-cluster"].Tags); // The leader-only active-node check must NOT be on the readiness tier. Assert.DoesNotContain(ZbHealthTags.Ready, registrations["active-node"].Tags); } finally { Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", previousEnv); } } [Fact] public void ActiveTier_Carries_Only_ActiveNode() { // The active-node leader check carries the Active tag (→ /health/active); the readiness // checks do not, so /health/active reports leadership alone. var previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); try { Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central"); var factory = CreateCentralFactory(); var registrations = Registrations(factory).ToDictionary(r => r.Name); Assert.True(registrations.ContainsKey("active-node"), "Expected an 'active-node' health check."); Assert.Contains(ZbHealthTags.Active, registrations["active-node"].Tags); Assert.DoesNotContain(ZbHealthTags.Active, registrations["database"].Tags); Assert.DoesNotContain(ZbHealthTags.Active, registrations["akka-cluster"].Tags); } finally { Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", previousEnv); } } }