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