80668a07bd
Phase 0 of docs/plans/2026-07-22-overview-dashboard-impl-plan.md: give the
canonical health JSON a structured channel so the family overview dashboard can
read each Akka cluster's current leader.
- ZbHealthWriter: optional `"data": {...}` per entry, sourced from
HealthReportEntry.Data, emitted only when non-empty. Per-property JsonIgnore
(NOT a global DefaultIgnoreCondition) so `"description": null` still renders —
payloads from data-less checks stay byte-identical to 0.1.0.
- AkkaClusterHealthCheck: BuildClusterData publishes this node's own view —
leader (omitted while unknown), selfAddress, selfRoles (sorted), memberCount,
unreachableCount — on every result path. The startup-safety paths (no
ActorSystem / cluster inaccessible) stay description-only.
- Tests: writer data emit/omit (raw-JSON assert on the omit case), and a real
single-node self-joined cluster via Akka.TestKit.Xunit2 for the data values.
70 tests green (25/39/6).
- Version 0.1.0 -> 0.2.0; 3 packages published to the Gitea feed and
restore-verified from a scratch consumer, which serves data.leader live.
105 lines
4.4 KiB
C#
105 lines
4.4 KiB
C#
using Akka.Actor;
|
|
using Akka.Cluster;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
|
using ZB.MOM.WW.Health.Akka;
|
|
|
|
namespace ZB.MOM.WW.Health.Akka.Tests;
|
|
|
|
/// <summary>
|
|
/// Table-driven tests for the pure status-mapping function inside <see cref="AkkaClusterStatusPolicy"/>.
|
|
/// The two presets (<see cref="AkkaClusterStatusPolicy.Default"/> and
|
|
/// <see cref="AkkaClusterStatusPolicy.OtOpcUaCompat"/>) are the convergence targets for ScadaBridge
|
|
/// and OtOpcUa respectively; every <see cref="MemberStatus"/> is exercised so a drift in either
|
|
/// preset fails loudly. Also covers the startup-safety null-guard on <see cref="AkkaClusterHealthCheck"/>.
|
|
/// </summary>
|
|
public sealed class AkkaClusterStatusPolicyTests
|
|
{
|
|
public static IEnumerable<object[]> DefaultCases() => new[]
|
|
{
|
|
new object[] { MemberStatus.Up, HealthStatus.Healthy },
|
|
new object[] { MemberStatus.Joining, HealthStatus.Healthy },
|
|
new object[] { MemberStatus.Leaving, HealthStatus.Degraded },
|
|
new object[] { MemberStatus.Exiting, HealthStatus.Degraded },
|
|
new object[] { MemberStatus.WeaklyUp, HealthStatus.Unhealthy },
|
|
new object[] { MemberStatus.Down, HealthStatus.Unhealthy },
|
|
new object[] { MemberStatus.Removed, HealthStatus.Unhealthy },
|
|
new object[] { (MemberStatus)99, HealthStatus.Unhealthy }, // unknown / future status
|
|
};
|
|
|
|
[Theory]
|
|
[MemberData(nameof(DefaultCases))]
|
|
public void Default_MapsEveryStatus(MemberStatus status, HealthStatus expected)
|
|
{
|
|
Assert.Equal(expected, AkkaClusterStatusPolicy.Default.Evaluate(status));
|
|
}
|
|
|
|
public static IEnumerable<object[]> OtOpcUaCompatCases() => new[]
|
|
{
|
|
new object[] { MemberStatus.Up, HealthStatus.Healthy },
|
|
new object[] { MemberStatus.Joining, HealthStatus.Degraded },
|
|
new object[] { MemberStatus.Leaving, HealthStatus.Degraded },
|
|
new object[] { MemberStatus.Exiting, HealthStatus.Degraded },
|
|
new object[] { MemberStatus.WeaklyUp, HealthStatus.Degraded },
|
|
new object[] { MemberStatus.Down, HealthStatus.Degraded },
|
|
new object[] { MemberStatus.Removed, HealthStatus.Degraded },
|
|
new object[] { (MemberStatus)99, HealthStatus.Degraded }, // unknown / future status
|
|
};
|
|
|
|
[Theory]
|
|
[MemberData(nameof(OtOpcUaCompatCases))]
|
|
public void OtOpcUaCompat_OnlyUpIsHealthy(MemberStatus status, HealthStatus expected)
|
|
{
|
|
Assert.Equal(expected, AkkaClusterStatusPolicy.OtOpcUaCompat.Evaluate(status));
|
|
}
|
|
|
|
[Fact]
|
|
public void CustomPolicy_UsesSuppliedFunc()
|
|
{
|
|
var policy = new AkkaClusterStatusPolicy(_ => HealthStatus.Unhealthy);
|
|
Assert.Equal(HealthStatus.Unhealthy, policy.Evaluate(MemberStatus.Up));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task HealthCheck_NoActorSystem_ReturnsDegraded()
|
|
{
|
|
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);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task HealthCheck_ActorSystemPresentButClusterInaccessible_ReturnsDegraded()
|
|
{
|
|
// A plain (non-clustered) ActorSystem exists in DI, but Akka.Cluster is not configured,
|
|
// so Cluster.Get(system) throws a ConfigurationException — the startup race the spec calls
|
|
// out. The check must return Degraded, not let the exception escape (→ Unhealthy via the host).
|
|
using var system = ActorSystem.Create("plain-no-cluster");
|
|
try
|
|
{
|
|
var provider = new ServiceCollection()
|
|
.AddSingleton(system)
|
|
.BuildServiceProvider();
|
|
var check = new AkkaClusterHealthCheck(provider, AkkaClusterStatusPolicy.Default);
|
|
|
|
var result = await check.CheckHealthAsync(NewContext(check));
|
|
|
|
Assert.Equal(HealthStatus.Degraded, result.Status);
|
|
// No cluster to describe → no data, so the emitted payload stays the pre-0.2.0 shape.
|
|
Assert.Empty(result.Data);
|
|
}
|
|
finally
|
|
{
|
|
await system.Terminate();
|
|
}
|
|
}
|
|
|
|
private static HealthCheckContext NewContext(IHealthCheck check) => new()
|
|
{
|
|
Registration = new HealthCheckRegistration("akka-cluster", check, HealthStatus.Unhealthy, tags: null),
|
|
};
|
|
}
|