using Akka.Actor; using Akka.Cluster; using ZB.MOM.WW.ScadaBridge.Communication.ClusterState; namespace ZB.MOM.WW.ScadaBridge.IntegrationTests.Cluster; /// /// Real-cluster proof for the central→site failover relay (Task 10). The unit tests in /// SiteCommunicationActorTests pin the routing and guard logic with the failover action /// stubbed out; this pins the part they cannot — that the shared /// actually moves a SITE pair when scoped to the /// site-specific role. /// /// The site-specific role scope is the load-bearing detail: site singletons (the /// Deployment Manager) are placed on site-{SiteId}, not on the base Site role, so /// failing over the wrong scope would move the wrong node. /// public sealed class SiteFailoverRelayTests { [Fact] public async Task Failing_over_a_site_pair_moves_the_oldest_and_the_survivor_takes_over() { // A site pair, both nodes carrying the site-specific role. await using var f = await TwoNodeClusterFixture.StartAsync(role: "site-SiteA"); var oldest = Akka.Cluster.Cluster.Get(f.NodeA); Assert.True(ActiveNodeEvaluator.SelfIsOldestUp(oldest, "site-SiteA"), "precondition: NodeA must be the active (oldest Up) site node"); // Issued from the other node, as the relay does when contact rotation lands there. var target = ClusterFailoverCoordinator.FailOverOldest(f.NodeB, "site-SiteA"); Assert.Equal(oldest.SelfAddress, target); await f.NodeA.WhenTerminated.WaitAsync(TimeSpan.FromSeconds(30)); await TwoNodeClusterFixture.WaitForMemberRemoved(f.NodeB, oldest.SelfAddress, TimeSpan.FromSeconds(30)); Assert.True(ActiveNodeEvaluator.SelfIsOldestUp(Akka.Cluster.Cluster.Get(f.NodeB), "site-SiteA")); } [Fact] public async Task A_site_role_scope_that_matches_no_member_is_refused() { // Guards the role-scoping mistake directly: asking for a site that isn't this pair // must find no members and refuse, rather than falling back to some other node. await using var f = await TwoNodeClusterFixture.StartAsync(role: "site-SiteA"); var target = ClusterFailoverCoordinator.FailOverOldest(f.NodeB, "site-SiteB"); Assert.Null(target); // Positive control: the pair is untouched and still fully formed. await TwoNodeClusterFixture.WaitForMembersUp(f.NodeB, 2, TimeSpan.FromSeconds(10)); } }