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);
}
}