using Akka.Actor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using ZB.MOM.WW.Health.Akka; namespace ZB.MOM.WW.Health.Akka.Tests; /// /// Table-driven tests for the pure helper covering both /// the role-less (ScadaBridge ActiveNode) and role-filtered (OtOpcUa AdminRoleLeader) matrices, /// plus the startup-safety null-guards on and /// when no is registered. /// public sealed class ActiveNodeDecisionTests { // Role-less: requiredRole == null. hasRole is irrelevant. Healthy iff (selfUp && isLeader), else Unhealthy. public static IEnumerable RoleLessCases() => new[] { new object?[] { true, true, false, (string?)null, HealthStatus.Healthy }, new object?[] { true, false, false, (string?)null, HealthStatus.Unhealthy }, new object?[] { false, true, false, (string?)null, HealthStatus.Unhealthy }, new object?[] { false, false, false, (string?)null, HealthStatus.Unhealthy }, }; [Theory] [MemberData(nameof(RoleLessCases))] public void Evaluate_RoleLess(bool selfUp, bool isLeader, bool hasRole, string? requiredRole, HealthStatus expected) { Assert.Equal(expected, ActiveNodeDecision.Evaluate(selfUp, isLeader, hasRole, requiredRole)); } // Role-filtered: requiredRole != null. // lacks role -> Healthy (probe irrelevant for this node) // has role & is leader -> Healthy (selfUp is ignored — role-filtered mode only cares about leadership) // has role & not leader -> Degraded public static IEnumerable RoleFilteredCases() => new[] { // node lacks the role -> Healthy regardless of selfUp / isLeader new object[] { true, true, false, "admin", HealthStatus.Healthy }, new object[] { true, false, false, "admin", HealthStatus.Healthy }, new object[] { false, false, false, "admin", HealthStatus.Healthy }, // node carries the role and is leader -> Healthy (selfUp=true) new object[] { true, true, true, "admin", HealthStatus.Healthy }, // node carries the role and is leader -> Healthy (selfUp=false: role-filtered mode ignores selfUp) new object[] { false, true, true, "admin", HealthStatus.Healthy }, // node carries the role but is not leader -> Degraded new object[] { true, false, true, "admin", HealthStatus.Degraded }, new object[] { false, false, true, "admin", HealthStatus.Degraded }, }; [Theory] [MemberData(nameof(RoleFilteredCases))] public void Evaluate_RoleFiltered(bool selfUp, bool isLeader, bool hasRole, string? requiredRole, HealthStatus expected) { Assert.Equal(expected, ActiveNodeDecision.Evaluate(selfUp, isLeader, hasRole, requiredRole)); } [Fact] public async Task HealthCheck_RoleLess_NoActorSystem_ReturnsDegraded() { var provider = new ServiceCollection().BuildServiceProvider(); var check = new ActiveNodeHealthCheck(provider); var result = await check.CheckHealthAsync(NewContext(check)); Assert.Equal(HealthStatus.Degraded, result.Status); } [Fact] public async Task HealthCheck_RoleFiltered_NoActorSystem_ReturnsDegraded() { var provider = new ServiceCollection().BuildServiceProvider(); var check = new ActiveNodeHealthCheck(provider, "admin"); var result = await check.CheckHealthAsync(NewContext(check)); Assert.Equal(HealthStatus.Degraded, result.Status); } [Fact] public void Gate_NoActorSystem_IsActiveNodeFalse() { var provider = new ServiceCollection().BuildServiceProvider(); var gate = new AkkaActiveNodeGate(provider); Assert.False(gate.IsActiveNode); } [Fact] public async Task HealthCheck_RoleLess_ClusterInaccessible_ReturnsDegraded() { // ActorSystem present but Akka.Cluster not configured → Cluster.Get throws. The check must // return Degraded (startup-safety rule), not let the exception escape (→ Unhealthy). using var system = ActorSystem.Create("plain-no-cluster-roleless"); try { var provider = new ServiceCollection() .AddSingleton(system) .BuildServiceProvider(); var check = new ActiveNodeHealthCheck(provider); var result = await check.CheckHealthAsync(NewContext(check)); Assert.Equal(HealthStatus.Degraded, result.Status); } finally { await system.Terminate(); } } [Fact] public async Task HealthCheck_RoleFiltered_ClusterInaccessible_ReturnsDegraded() { using var system = ActorSystem.Create("plain-no-cluster-rolefiltered"); try { var provider = new ServiceCollection() .AddSingleton(system) .BuildServiceProvider(); var check = new ActiveNodeHealthCheck(provider, "admin"); var result = await check.CheckHealthAsync(NewContext(check)); Assert.Equal(HealthStatus.Degraded, result.Status); } finally { await system.Terminate(); } } private static HealthCheckContext NewContext(IHealthCheck check) => new() { Registration = new HealthCheckRegistration("active-node", check, HealthStatus.Unhealthy, tags: null), }; }