Files
natsdotnet/tests/NATS.Server.Tests/Raft/RaftStrictConsensusRuntimeTests.cs
Joseph Doherty 7e0bed2447 feat: add chunk-based snapshot streaming with CRC32 validation (Gap 8.3)
- Add SnapshotChunkEnumerator: IEnumerable<byte[]> that splits snapshot data
  into fixed-size chunks (default 65536 bytes) and computes CRC32 over the
  full payload for integrity validation during streaming transfer
- Add RaftInstallSnapshotChunkWire: 24-byte header + variable data wire type
  encoding [snapshotIndex:8][snapshotTerm:4][chunkIndex:4][totalChunks:4][crc32:4][data:N]
- Extend InstallSnapshotFromChunksAsync with optional expectedCrc32 parameter;
  validates assembled data against CRC32 before applying snapshot state, throwing
  InvalidDataException on mismatch to prevent corrupt state installation
- Fix stub IRaftTransport implementations in test files missing SendTimeoutNowAsync
- Fix incorrect role assertion in RaftLeadershipTransferTests (single-node quorum = 1)
- 15 new tests in RaftSnapshotStreamingTests covering enumeration, reassembly,
  CRC correctness, validation success/failure, and wire format roundtrip
2026-02-25 08:21:36 -05:00

48 lines
2.0 KiB
C#

using NATS.Server.Raft;
namespace NATS.Server.Tests.Raft;
public class RaftStrictConsensusRuntimeTests
{
[Fact]
public async Task Quorum_and_nextindex_rules_gate_commit_visibility_and_snapshot_catchup_convergence()
{
var voter = new RaftNode("v1");
voter.GrantVote(2, "cand-a").Granted.ShouldBeTrue();
voter.GrantVote(2, "cand-b").Granted.ShouldBeFalse();
var transport = new RejectingRaftTransport();
var leader = new RaftNode("n1", transport);
var followerA = new RaftNode("n2", transport);
var followerB = new RaftNode("n3", transport);
var cluster = new[] { leader, followerA, followerB };
foreach (var node in cluster)
node.ConfigureCluster(cluster);
leader.StartElection(cluster.Length);
leader.ReceiveVote(new VoteResponse { Granted = true }, cluster.Length);
leader.IsLeader.ShouldBeTrue();
_ = await leader.ProposeAsync("cmd-1", default);
leader.AppliedIndex.ShouldBe(0);
followerA.AppliedIndex.ShouldBe(0);
followerB.AppliedIndex.ShouldBe(0);
}
private sealed class RejectingRaftTransport : IRaftTransport
{
public Task<IReadOnlyList<AppendResult>> AppendEntriesAsync(string leaderId, IReadOnlyList<string> followerIds, RaftLogEntry entry, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<AppendResult>>(
followerIds.Select(id => new AppendResult { FollowerId = id, Success = false }).ToArray());
public Task<VoteResponse> RequestVoteAsync(string candidateId, string voterId, VoteRequest request, CancellationToken ct)
=> Task.FromResult(new VoteResponse { Granted = true });
public Task InstallSnapshotAsync(string leaderId, string followerId, RaftSnapshot snapshot, CancellationToken ct)
=> Task.CompletedTask;
public Task SendTimeoutNowAsync(string leaderId, string targetId, ulong term, CancellationToken ct)
=> Task.CompletedTask;
}
}