using Akka.Actor; using Akka.Cluster; using Akka.TestKit.Xunit2; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using ZB.MOM.WW.Health.Akka; namespace ZB.MOM.WW.Health.Akka.Tests; /// /// Covers the cluster-view data dictionary that publishes /// from 0.2.0 on (consumed by the family overview dashboard to show each cluster's current leader). /// Uses a real single-node self-joined cluster via — the values come off /// , which cannot be faked, so a formed cluster is the only honest fixture. /// public sealed class AkkaClusterDataTests : TestKit { private const string ClusterConfig = """ akka { actor.provider = cluster remote.dot-netty.tcp { hostname = "127.0.0.1" port = 0 } cluster { roles = ["driver", "admin"] # Keep the single-node self-join deterministic and fast in-test. min-nr-of-members = 1 } } """; private static readonly TimeSpan FormationTimeout = TimeSpan.FromSeconds(30); public AkkaClusterDataTests() : base(ClusterConfig, nameof(AkkaClusterDataTests)) { } /// Self-joins the test node and waits until it is Up with a converged leader. private Cluster FormSingleNodeCluster() { var cluster = Cluster.Get(Sys); cluster.Join(cluster.SelfAddress); AwaitCondition( () => cluster.SelfMember.Status == MemberStatus.Up && cluster.State.Leader is not null, FormationTimeout); return cluster; } [Fact] public void BuildClusterData_SingleNodeCluster_ReportsSelfAsLeader() { var cluster = FormSingleNodeCluster(); var data = AkkaClusterHealthCheck.BuildClusterData(cluster); Assert.Equal(cluster.SelfAddress.ToString(), Assert.IsType(data["leader"])); Assert.Equal(cluster.SelfAddress.ToString(), Assert.IsType(data["selfAddress"])); Assert.Equal(1, Assert.IsType(data["memberCount"])); Assert.Equal(0, Assert.IsType(data["unreachableCount"])); } [Fact] public void BuildClusterData_RolesRoundTripSorted() { var cluster = FormSingleNodeCluster(); var roles = Assert.IsType(AkkaClusterHealthCheck.BuildClusterData(cluster)["selfRoles"]); // Sorted, so the payload is stable across restarts; the configured roles survive verbatim. Assert.Equal(new[] { "admin", "driver" }, roles); } [Fact] public async Task CheckHealth_FormedCluster_HealthyResultCarriesClusterData() { var cluster = FormSingleNodeCluster(); var provider = new ServiceCollection().AddSingleton(Sys).BuildServiceProvider(); var check = new AkkaClusterHealthCheck(provider, AkkaClusterStatusPolicy.Default); var result = await check.CheckHealthAsync(NewContext(check)); Assert.Equal(HealthStatus.Healthy, result.Status); Assert.Equal(cluster.SelfAddress.ToString(), result.Data["leader"]); Assert.Equal(1, result.Data["memberCount"]); } [Fact] public async Task CheckHealth_NoActorSystem_CarriesNoData() { // The startup-safety paths stay description-only: an empty data dictionary is what makes the // writer omit the "data" key entirely, so a not-yet-started node's payload is unchanged. var provider = new ServiceCollection().BuildServiceProvider(); var check = new AkkaClusterHealthCheck(provider, AkkaClusterStatusPolicy.Default); var result = await check.CheckHealthAsync(NewContext(check)); Assert.Equal(HealthStatus.Degraded, result.Status); Assert.Empty(result.Data); } private static HealthCheckContext NewContext(IHealthCheck check) => new() { Registration = new HealthCheckRegistration("akka-cluster", check, HealthStatus.Unhealthy, tags: null), }; }