using NATS.Server.Raft; namespace NATS.Server.Raft.Tests.Raft; /// /// Ported from Go: TestNRGSimple in golang/nats-server/server/raft_test.go /// Validates basic RAFT election mechanics and state convergence after proposals. /// public class RaftElectionBasicTests { [Fact] public async Task Three_node_group_elects_leader() { // Reference: TestNRGSimple — create 3-node RAFT group, wait for leader election. var cluster = RaftTestCluster.Create(3); var leader = await cluster.ElectLeaderAsync(); // Verify exactly 1 leader among the 3 nodes. leader.IsLeader.ShouldBeTrue(); leader.Role.ShouldBe(RaftRole.Leader); leader.Term.ShouldBe(1); // The other 2 nodes should not be leaders. var followers = cluster.Nodes.Where(n => n.Id != leader.Id).ToList(); followers.Count.ShouldBe(2); foreach (var follower in followers) { follower.IsLeader.ShouldBeFalse(); } // Verify the cluster has exactly 1 leader total. cluster.Nodes.Count(n => n.IsLeader).ShouldBe(1); cluster.Nodes.Count(n => !n.IsLeader).ShouldBe(2); } [Fact] public async Task State_converges_after_proposals() { // Reference: TestNRGSimple — propose entries and verify all nodes converge. var cluster = RaftTestCluster.Create(3); var leader = await cluster.ElectLeaderAsync(); // Propose multiple entries like the Go test does with proposeDelta. var index1 = await leader.ProposeAsync("delta-22", default); var index2 = await leader.ProposeAsync("delta-minus-11", default); var index3 = await leader.ProposeAsync("delta-minus-10", default); // Wait for all members to have applied the entries. await cluster.WaitForAppliedAsync(index3); // All nodes should have converged to the same applied index. cluster.Nodes.All(n => n.AppliedIndex >= index3).ShouldBeTrue(); // The leader's log should contain all 3 entries. leader.Log.Entries.Count.ShouldBe(3); leader.Log.Entries[0].Command.ShouldBe("delta-22"); leader.Log.Entries[1].Command.ShouldBe("delta-minus-11"); leader.Log.Entries[2].Command.ShouldBe("delta-minus-10"); // Verify log indices are sequential. leader.Log.Entries[0].Index.ShouldBe(1); leader.Log.Entries[1].Index.ShouldBe(2); leader.Log.Entries[2].Index.ShouldBe(3); // All entries should carry the current term. foreach (var entry in leader.Log.Entries) { entry.Term.ShouldBe(leader.Term); } } [Fact] public async Task Candidate_receives_majority_to_become_leader() { // Validates the vote-counting mechanics in detail. var node1 = new RaftNode("n1"); var node2 = new RaftNode("n2"); var node3 = new RaftNode("n3"); var allNodes = new[] { node1, node2, node3 }; foreach (var n in allNodes) n.ConfigureCluster(allNodes); // n1 starts an election. node1.StartElection(clusterSize: 3); node1.Role.ShouldBe(RaftRole.Candidate); node1.Term.ShouldBe(1); node1.TermState.VotedFor.ShouldBe("n1"); // With only 1 vote (self), not yet leader. node1.IsLeader.ShouldBeFalse(); // n2 grants vote. var voteFromN2 = node2.GrantVote(node1.Term, "n1"); voteFromN2.Granted.ShouldBeTrue(); node1.ReceiveVote(voteFromN2, clusterSize: 3); // With 2 out of 3 votes (majority), should now be leader. node1.IsLeader.ShouldBeTrue(); node1.Role.ShouldBe(RaftRole.Leader); } [Fact] public async Task Leader_steps_down_on_request() { var cluster = RaftTestCluster.Create(3); var leader = await cluster.ElectLeaderAsync(); leader.IsLeader.ShouldBeTrue(); leader.RequestStepDown(); leader.IsLeader.ShouldBeFalse(); leader.Role.ShouldBe(RaftRole.Follower); } [Fact] public void Follower_steps_down_to_higher_term_on_heartbeat() { // When a follower receives a heartbeat with a higher term, it updates its term. var node = new RaftNode("n1"); node.StartElection(clusterSize: 1); node.IsLeader.ShouldBeTrue(); node.Term.ShouldBe(1); // Receiving heartbeat with higher term causes step-down. node.ReceiveHeartbeat(term: 5); node.Role.ShouldBe(RaftRole.Follower); node.Term.ShouldBe(5); } [Fact] public async Task Five_node_group_elects_leader_with_quorum() { var cluster = RaftTestCluster.Create(5); var leader = await cluster.ElectLeaderAsync(); leader.IsLeader.ShouldBeTrue(); cluster.Nodes.Count(n => n.IsLeader).ShouldBe(1); cluster.Nodes.Count(n => !n.IsLeader).ShouldBe(4); } }