using ZB.MOM.WW.CBDDC.Core; using ZB.MOM.WW.CBDDC.Core.Network; using ZB.MOM.WW.CBDDC.Network.Leadership; namespace ZB.MOM.WW.CBDDC.Network.Tests; public class BullyLeaderElectionServiceTests { private static IDiscoveryService CreateDiscovery(IList peers) { var discovery = Substitute.For(); discovery.GetActivePeers().Returns(_ => peers); return discovery; } private static IPeerNodeConfigurationProvider CreateConfig(string nodeId) { var configProvider = Substitute.For(); configProvider.GetConfiguration().Returns(new PeerNodeConfiguration { NodeId = nodeId }); return configProvider; } /// /// Verifies that a single node elects itself as leader. /// [Fact] public async Task SingleNode_ShouldBecomeLeader() { var peers = new List(); var electionService = new BullyLeaderElectionService( CreateDiscovery(peers), CreateConfig("node-A"), electionInterval: TimeSpan.FromMilliseconds(100)); LeadershipChangedEventArgs? lastEvent = null; electionService.LeadershipChanged += (_, e) => lastEvent = e; await electionService.Start(); await Task.Delay(200); electionService.IsCloudGateway.ShouldBeTrue(); electionService.CurrentGatewayNodeId.ShouldBe("node-A"); lastEvent.ShouldNotBeNull(); lastEvent!.IsLocalNodeGateway.ShouldBeTrue(); lastEvent.CurrentGatewayNodeId.ShouldBe("node-A"); await electionService.Stop(); } /// /// Verifies that the smallest node ID is elected as leader among LAN peers. /// [Fact] public async Task MultipleNodes_SmallestNodeIdShouldBeLeader() { var peers = new List { new("node-B", "192.168.1.2:9000", DateTimeOffset.UtcNow, PeerType.LanDiscovered), new("node-C", "192.168.1.3:9000", DateTimeOffset.UtcNow, PeerType.LanDiscovered) }; var electionService = new BullyLeaderElectionService( CreateDiscovery(peers), CreateConfig("node-A"), electionInterval: TimeSpan.FromMilliseconds(100)); await electionService.Start(); await Task.Delay(200); electionService.IsCloudGateway.ShouldBeTrue(); electionService.CurrentGatewayNodeId.ShouldBe("node-A"); await electionService.Stop(); } /// /// Verifies that the local node is not elected when it is not the smallest node ID. /// [Fact] public async Task LocalNodeNotSmallest_ShouldNotBeLeader() { var peers = new List { new("node-A", "192.168.1.1:9000", DateTimeOffset.UtcNow, PeerType.LanDiscovered), new("node-B", "192.168.1.2:9000", DateTimeOffset.UtcNow, PeerType.LanDiscovered) }; var electionService = new BullyLeaderElectionService( CreateDiscovery(peers), CreateConfig("node-C"), electionInterval: TimeSpan.FromMilliseconds(100)); await electionService.Start(); await Task.Delay(200); electionService.IsCloudGateway.ShouldBeFalse(); electionService.CurrentGatewayNodeId.ShouldBe("node-A"); await electionService.Stop(); } /// /// Verifies that leadership is re-elected when the current leader fails. /// [Fact] public async Task LeaderFailure_ShouldReelect() { var peers = new List { new("node-A", "192.168.1.1:9000", DateTimeOffset.UtcNow, PeerType.LanDiscovered) }; var electionService = new BullyLeaderElectionService( CreateDiscovery(peers), CreateConfig("node-B"), electionInterval: TimeSpan.FromMilliseconds(100)); var leadershipChanges = new List(); electionService.LeadershipChanged += (_, e) => leadershipChanges.Add(e); await electionService.Start(); await Task.Delay(200); electionService.CurrentGatewayNodeId.ShouldBe("node-A"); peers.Clear(); await Task.Delay(200); electionService.IsCloudGateway.ShouldBeTrue(); electionService.CurrentGatewayNodeId.ShouldBe("node-B"); leadershipChanges.ShouldNotBeEmpty(); leadershipChanges.Last().IsLocalNodeGateway.ShouldBeTrue(); leadershipChanges.Last().CurrentGatewayNodeId.ShouldBe("node-B"); await electionService.Stop(); } /// /// Verifies that cloud peers are excluded from LAN gateway election. /// [Fact] public async Task CloudPeersExcludedFromElection() { var peers = new List { new("node-A", "192.168.1.1:9000", DateTimeOffset.UtcNow, PeerType.LanDiscovered), new("cloud-node-Z", "cloud.example.com:9000", DateTimeOffset.UtcNow, PeerType.CloudRemote) }; var electionService = new BullyLeaderElectionService( CreateDiscovery(peers), CreateConfig("node-B"), electionInterval: TimeSpan.FromMilliseconds(100)); await electionService.Start(); await Task.Delay(200); electionService.CurrentGatewayNodeId.ShouldBe("node-A"); await electionService.Stop(); } }