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