using Akka.Actor; using Akka.Configuration; using Akka.TestKit.Xunit; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; using Shouldly; using Xunit; using ZB.MOM.WW.Health; using ZB.MOM.WW.Health.Akka; using ZB.MOM.WW.OtOpcUa.Host.Health; namespace ZB.MOM.WW.OtOpcUa.Host.Tests.Health; /// /// Pins OtOpcUa's active-tier wiring. The rule (oldest Up member of a role) now lives in the /// shared ClusterActiveNode and is tested there against a real two-node cluster; what stays /// OtOpcUa's own decision — and what regressed in lmxopcua#494 — is which check is /// registered on the active tag, and with which roles. /// public sealed class ActiveTierRegistrationTests { private static HealthCheckRegistration ClusterPrimaryRegistration(bool hasAdmin) => new ServiceCollection() .AddOtOpcUaHealth(hasAdmin) .BuildServiceProvider() .GetRequiredService>() .Value.Registrations .Where(r => r.Name == "cluster-primary") .ShouldHaveSingleItem(); [Fact] public void Active_tier_uses_the_shared_ActiveNodeHealthCheck() { // Not a bespoke copy. Two apps in the family independently hand-rolled this check because the // shared one selected by RoleLeader; since Health 0.3.0 the shared one is the correct rule, so // a private reimplementation reappearing here would be the regression. var registration = ClusterPrimaryRegistration(hasAdmin: true); registration.Factory(new ServiceCollection().BuildServiceProvider()) .ShouldBeOfType(); } [Theory] [InlineData(true)] [InlineData(false)] public void Active_tier_check_is_registered_on_every_node_role(bool hasAdmin) { // Registered on driver-only nodes too, unlike the admin-only configdb probe. A site pair needs // an active tier just as much as the central pair does — under the old admin-scoped check all // four driver-only site nodes answered 200 and no consumer could find a site's Primary. ClusterPrimaryRegistration(hasAdmin).Tags.ShouldContain(ZbHealthTags.Active); } /// /// R03:2.1 + fix round 1 / I6 (coordinator ruling) — the OPC UA endpoint probe's tiers by role: /// /// driver-only: Ready + Active (it cannot serve and must not be routed to); /// fused admin+driver: Ready ONLY, until the ServiceLevel calculator consumes the fact /// (F-C1) — Traefik routes the AdminUI on /health/active, so a failed SDK on the active /// central node must not take the AdminUI out of rotation; /// admin-only: absent (it starts no endpoint to be degraded by). /// /// [Theory] [InlineData(false, true, true, true)] [InlineData(true, true, true, false)] [InlineData(true, false, false, false)] public void Opcua_endpoint_probe_tiers_follow_the_node_roles(bool hasAdmin, bool hasDriver, bool ready, bool active) { var registrations = new ServiceCollection() .AddOtOpcUaHealth(hasAdmin: hasAdmin, hasDriver: hasDriver) .BuildServiceProvider() .GetRequiredService>() .Value.Registrations .Where(r => r.Name == OpcUaEndpointHealthCheck.Name) .ToList(); if (!ready && !active) { registrations.ShouldBeEmpty(); return; } var registration = registrations.ShouldHaveSingleItem(); registration.Tags.Contains(ZbHealthTags.Ready).ShouldBe(ready); registration.Tags.Contains(ZbHealthTags.Active).ShouldBe(active); } [Fact] public void Active_tier_check_is_not_on_the_ready_tier() { // Readiness must not depend on being the active node: a healthy standby is ready to serve, and // tagging it ready would take the whole pair out of rotation whenever one node is standby. ClusterPrimaryRegistration(hasAdmin: true).Tags.ShouldNotContain(ZbHealthTags.Ready); } } /// lmxopcua#771 — the akka check must use the shared SPEC Default policy so a node that is not a live /// cluster member makes /health/ready fail (the retired OtOpcUaCompat policy could only say Degraded). public sealed class AkkaReadyPolicyTests : TestKit { public AkkaReadyPolicyTests() : base(ConfigurationFactory.ParseString( "akka.actor.provider = cluster\nakka.remote.dot-netty.tcp.port = 0\nakka.remote.dot-netty.tcp.hostname = 127.0.0.1")) { } [Fact] public async Task Akka_check_is_Unhealthy_when_self_has_not_joined_a_cluster() { var services = new ServiceCollection(); services.AddSingleton(Sys); services.AddOtOpcUaHealth(hasAdmin: true); var sp = services.BuildServiceProvider(); var registration = sp.GetRequiredService>() .Value.Registrations.Single(r => r.Name == "akka"); registration.Tags.ShouldContain(ZbHealthTags.Ready); var result = await registration.Factory(sp).CheckHealthAsync( new HealthCheckContext { Registration = registration }, CancellationToken.None); result.Status.ShouldBe(HealthStatus.Unhealthy); } }