using Akka.Actor;
using Akka.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests;
///
/// Guards / — the
/// node's OWN cluster-scoped role (per-cluster mesh, Phase 6), derived from the node's configured
/// at construction time, NOT from live cluster membership.
/// That distinction matters: this identity must be readable before the cluster forms, so later
/// Phase-6 work (singleton registration, a startup validator) can act on it at host-build time.
///
public sealed class ClusterRoleInfoTests
{
private static AkkaClusterOptions SampleOptions(params string[] roles) => new()
{
Port = 0,
Hostname = "127.0.0.1",
PublicHostname = "127.0.0.1",
SeedNodes = Array.Empty(),
Roles = roles,
};
///
/// Boots a real host through the production
/// entry point (mirrors SplitBrainResolverActivationTests) — 's
/// constructor needs a real clustered to subscribe to cluster events,
/// even though the properties under test never touch live membership — resolves
/// from DI, and returns its (ClusterRole, ClusterId) pair
/// after tearing the host down. Both start/stop live in this helper rather than the test methods
/// so the CancellationToken-less calls stay off xunit1051's radar (mirrors
/// SplitBrainResolverActivationTests.EffectiveConfigAsync).
///
private static async Task<(string? ClusterRole, string? ClusterId)> ResolveAsync(AkkaClusterOptions options)
{
var builder = Host.CreateDefaultBuilder();
builder.ConfigureServices(services =>
{
services.AddSingleton>(Options.Create(options));
services.AddAkka("otopcua-role-info-test", (ab, sp) => ab.WithOtOpcUaClusterBootstrap(sp));
services.AddSingleton();
});
using var host = builder.Build();
await host.StartAsync();
try
{
var roleInfo = host.Services.GetRequiredService();
return (roleInfo.ClusterRole, roleInfo.ClusterId);
}
finally
{
await host.StopAsync();
}
}
/// A node configured with a single cluster-scoped role exposes it and its ClusterId.
[Fact]
public async Task Single_cluster_role_exposes_role_and_id()
{
var (clusterRole, clusterId) = await ResolveAsync(SampleOptions("driver", "cluster-SITE-A"));
clusterRole.ShouldBe("cluster-SITE-A");
clusterId.ShouldBe("SITE-A");
}
/// A node configured with only fixed roles carries no cluster identity.
[Fact]
public async Task No_cluster_role_configured_yields_null()
{
var (clusterRole, clusterId) = await ResolveAsync(SampleOptions("admin", "driver"));
clusterRole.ShouldBeNull();
clusterId.ShouldBeNull();
}
///
/// More than one cluster-scoped role is a misconfiguration; the first configured wins rather
/// than throwing.
///
[Fact]
public async Task Multiple_cluster_roles_first_wins()
{
var (clusterRole, clusterId) = await ResolveAsync(SampleOptions("driver", "cluster-MAIN", "cluster-SITE-B"));
clusterRole.ShouldBe("cluster-MAIN");
clusterId.ShouldBe("MAIN");
}
/// An empty role list carries no cluster identity either.
[Fact]
public async Task Empty_roles_yields_null()
{
var (clusterRole, clusterId) = await ResolveAsync(SampleOptions());
clusterRole.ShouldBeNull();
clusterId.ShouldBeNull();
}
}