// Copyright 2012-2026 The NATS Authors // Licensed under the Apache License, Version 2.0 using Shouldly; namespace ZB.MOM.NatsNet.Server.Tests.JetStream; public sealed class RaftNodeCoreTests { [Fact] public void SnapshotHelpers_WhenEncodedAndLoaded_ShouldRoundTrip() { var storeDir = Path.Combine(Path.GetTempPath(), $"raft-node-core-{Guid.NewGuid():N}"); var raft = new Raft { StoreDir = storeDir, Term_ = 3, Applied_ = 9, PApplied = 7, Wps = [1, 2, 3], }; try { var checkpoint = raft.CreateSnapshotCheckpointLocked(force: true); checkpoint.ShouldNotBeNull(); var snapshot = new Snapshot { LastTerm = 3, LastIndex = 9, PeerState = [7, 8], Data = [9, 10, 11], }; raft.InstallSnapshotInternal(snapshot).ShouldBeNull(); var (loaded, error) = raft.LoadLastSnapshot(); error.ShouldBeNull(); loaded.ShouldNotBeNull(); loaded!.LastTerm.ShouldBe(3UL); loaded.LastIndex.ShouldBe(9UL); loaded.PeerState.ShouldBe([7, 8]); loaded.Data.ShouldBe([9, 10, 11]); raft.SetupLastSnapshot().ShouldBeNull(); raft.PIndex.ShouldBe(9UL); raft.PTerm.ShouldBe(3UL); } finally { if (Directory.Exists(storeDir)) { Directory.Delete(storeDir, recursive: true); } } } [Fact] public void LeadershipHelpers_WhenSteppingDownAndSelectingLeader_ShouldUpdateState() { var raft = new Raft { Id = "N1", StateValue = (int)RaftState.Candidate, Peers_ = new Dictionary { ["N2"] = new() { Li = 3 }, ["N3"] = new() { Li = 7 }, }, }; raft.StepdownLocked("N3"); raft.State().ShouldBe(RaftState.Follower); raft.LeaderId.ShouldBe("N3"); raft.SelectNextLeader().ShouldBe("N3"); } [Fact] public void CampaignHelpers_WhenLeaderOrFollower_ShouldReturnExpectedOutcome() { var raft = new Raft { StateValue = (int)RaftState.Leader, }; raft.CampaignInternal(TimeSpan.FromMilliseconds(200)).ShouldNotBeNull(); raft.XferCampaign().ShouldNotBeNull(); raft.StateValue = (int)RaftState.Follower; raft.CampaignInternal(TimeSpan.FromMilliseconds(200)).ShouldBeNull(); raft.State().ShouldBe(RaftState.Candidate); } [Fact] public void ProgressHelpers_WhenCatchupAndKnownPeersChange_ShouldTrackFlags() { var raft = new Raft { Commit = 10, Applied_ = 8, Catchup = new CatchupState(), }; raft.IsCatchingUp().ShouldBeTrue(); raft.IsCurrent(includeForwardProgress: true).ShouldBeFalse(); raft.Catchup = null; raft.UpdateKnownPeersLocked(["N2", "N3"]); raft.Peers_.Count.ShouldBe(2); raft.RandCampaignTimeout().ShouldBeGreaterThan(TimeSpan.Zero); } [Fact] public void RunLoopHelpers_WhenInvoked_ShouldManageSubscriptionAndTimers() { var raft = new Raft { Id = "N1", GroupName = "RG", Active = DateTime.UtcNow, }; var inbox = raft.NewInbox(); var catchupInbox = raft.NewCatchupInbox(); inbox.ShouldContain("_INBOX."); catchupInbox.ShouldContain("_INBOX.CATCHUP."); var sub = raft.Subscribe("raft.append"); raft.AeSub.ShouldNotBeNull(); raft.Unsubscribe(sub); raft.AeSub.ShouldBeNull(); raft.CreateInternalSubs().ShouldBeNull(); raft.ResetElectionTimeout(); raft.Elect.ShouldNotBeNull(); raft.ResetElect(TimeSpan.FromMilliseconds(10)); raft.ElectTimer().ShouldBeGreaterThan(DateTime.MinValue); } [Fact] public void CodecHelpers_WhenRoundTrippingEntriesAndVotes_ShouldPreserveFields() { var raft = new Raft { GroupName = "RG", }; var entry = raft.NewEntry(EntryType.EntryNormal, [1, 2, 3]); var proposed = raft.NewProposedEntry(entry, "reply"); proposed.Reply.ShouldBe("reply"); var appendEntry = raft.NewAppendEntry("L1", 2, 1, 1, 0, [entry]); appendEntry.String().ShouldContain("leader=L1"); appendEntry.ShouldStore().ShouldBeTrue(); var decodedAppend = raft.DecodeAppendEntry(appendEntry.Encode()); decodedAppend.Leader.ShouldBe("L1"); var appendResponse = raft.NewAppendEntryResponse(2, 1, "N2", "_R_", success: true); var decodedResponse = raft.DecodeAppendEntryResponse(appendResponse.Encode()); decodedResponse.Success.ShouldBeTrue(); decodedResponse.Peer.ShouldBe("N2"); var voteRequest = new VoteRequest { TermV = 4, Candidate = "N3", LastIndex = 9, LastTerm = 3, Reply = "_R_" }; var decodedVoteRequest = raft.DecodeVoteRequest(voteRequest.Encode()); decodedVoteRequest.Candidate.ShouldBe("N3"); var voteResponse = new VoteResponse { TermV = 4, Peer = "N2", Granted = true, Empty = false }; var decodedVoteResponse = raft.DecodeVoteResponse(voteResponse.Encode()); decodedVoteResponse.Granted.ShouldBeTrue(); decodedVoteResponse.Peer.ShouldBe("N2"); } [Fact] public void PeerStatePersistence_WhenWrittenAndRead_ShouldRoundTrip() { var raft = new Raft(); var storeDir = Path.Combine(Path.GetTempPath(), $"raft-peer-state-{Guid.NewGuid():N}"); try { var state = new PeerState { KnownPeers = ["A1", "A2"], ClusterSize = 2, DomainExt = 7, }; raft.PeerStateBufSize(state).ShouldBeGreaterThan(0); raft.WritePeerStateStatic(storeDir, state).ShouldBeNull(); var (readState, readError) = raft.ReadPeerState(storeDir); readError.ShouldBeNull(); readState.ShouldNotBeNull(); readState!.KnownPeers.Count.ShouldBe(2); raft.WriteTermVoteStatic(storeDir, 6, "A1").ShouldBeNull(); raft.Term_.ShouldBe(6UL); raft.Vote.ShouldBe("A1"); } finally { if (Directory.Exists(storeDir)) { Directory.Delete(storeDir, recursive: true); } } } }