Move 43 Raft consensus test files (8 root-level + 35 in Raft/ subfolder) from NATS.Server.Tests into a dedicated NATS.Server.Raft.Tests project. Update namespaces, add InternalsVisibleTo, and fix timing/exception handling issues in moved test files.
167 lines
5.5 KiB
C#
167 lines
5.5 KiB
C#
using System.Text.Json;
|
|
using NATS.Server.Raft;
|
|
|
|
namespace NATS.Server.Raft.Tests.Raft;
|
|
|
|
/// <summary>
|
|
/// Wire format encoding/decoding tests for RAFT RPC contracts.
|
|
/// Go: TestNRGAppendEntryEncode, TestNRGAppendEntryDecode in server/raft_test.go:82-152.
|
|
/// The .NET implementation uses JSON serialization rather than binary encoding,
|
|
/// so these tests validate JSON round-trip fidelity for all RPC types.
|
|
/// </summary>
|
|
public class RaftWireFormatTests
|
|
{
|
|
// Go: TestNRGAppendEntryEncode server/raft_test.go:82
|
|
[Fact]
|
|
public void VoteRequest_json_round_trip()
|
|
{
|
|
var original = new VoteRequest { Term = 5, CandidateId = "node-alpha" };
|
|
var json = JsonSerializer.Serialize(original);
|
|
json.ShouldNotBeNullOrWhiteSpace();
|
|
|
|
var decoded = JsonSerializer.Deserialize<VoteRequest>(json);
|
|
decoded.ShouldNotBeNull();
|
|
decoded.Term.ShouldBe(5);
|
|
decoded.CandidateId.ShouldBe("node-alpha");
|
|
}
|
|
|
|
// Go: TestNRGAppendEntryEncode server/raft_test.go:82
|
|
[Fact]
|
|
public void VoteResponse_json_round_trip()
|
|
{
|
|
var granted = new VoteResponse { Granted = true };
|
|
var json = JsonSerializer.Serialize(granted);
|
|
var decoded = JsonSerializer.Deserialize<VoteResponse>(json);
|
|
decoded.ShouldNotBeNull();
|
|
decoded.Granted.ShouldBeTrue();
|
|
|
|
var denied = new VoteResponse { Granted = false };
|
|
var json2 = JsonSerializer.Serialize(denied);
|
|
var decoded2 = JsonSerializer.Deserialize<VoteResponse>(json2);
|
|
decoded2.ShouldNotBeNull();
|
|
decoded2.Granted.ShouldBeFalse();
|
|
}
|
|
|
|
// Go: TestNRGAppendEntryEncode server/raft_test.go:82
|
|
[Fact]
|
|
public void AppendResult_json_round_trip()
|
|
{
|
|
var original = new AppendResult { FollowerId = "f1", Success = true };
|
|
var json = JsonSerializer.Serialize(original);
|
|
var decoded = JsonSerializer.Deserialize<AppendResult>(json);
|
|
decoded.ShouldNotBeNull();
|
|
decoded.FollowerId.ShouldBe("f1");
|
|
decoded.Success.ShouldBeTrue();
|
|
}
|
|
|
|
// Go: TestNRGAppendEntryEncode server/raft_test.go:82 — multiple entries
|
|
[Fact]
|
|
public void RaftLogEntry_batch_json_round_trip_preserves_order()
|
|
{
|
|
var entries = Enumerable.Range(1, 50)
|
|
.Select(i => new RaftLogEntry(Index: i, Term: (i % 3) + 1, Command: $"op-{i}"))
|
|
.ToList();
|
|
|
|
var json = JsonSerializer.Serialize(entries);
|
|
var decoded = JsonSerializer.Deserialize<List<RaftLogEntry>>(json);
|
|
|
|
decoded.ShouldNotBeNull();
|
|
decoded.Count.ShouldBe(50);
|
|
|
|
for (var i = 0; i < 50; i++)
|
|
{
|
|
decoded[i].Index.ShouldBe(i + 1);
|
|
decoded[i].Term.ShouldBe((i + 1) % 3 + 1);
|
|
decoded[i].Command.ShouldBe($"op-{i + 1}");
|
|
}
|
|
}
|
|
|
|
// Go: TestNRGAppendEntryEncode server/raft_test.go:82 — large payload
|
|
[Fact]
|
|
public void RaftLogEntry_large_command_round_trips()
|
|
{
|
|
var largeCommand = new string('x', 65536);
|
|
var entry = new RaftLogEntry(Index: 1, Term: 1, Command: largeCommand);
|
|
|
|
var json = JsonSerializer.Serialize(entry);
|
|
var decoded = JsonSerializer.Deserialize<RaftLogEntry>(json);
|
|
|
|
decoded.ShouldNotBeNull();
|
|
decoded.Command.Length.ShouldBe(65536);
|
|
decoded.Command.ShouldBe(largeCommand);
|
|
}
|
|
|
|
// Go: TestNRGAppendEntryEncode server/raft_test.go:82 — snapshot marker
|
|
[Fact]
|
|
public void RaftSnapshot_json_round_trip()
|
|
{
|
|
var data = new byte[256];
|
|
Random.Shared.NextBytes(data);
|
|
|
|
var snapshot = new RaftSnapshot
|
|
{
|
|
LastIncludedIndex = 999,
|
|
LastIncludedTerm = 42,
|
|
Data = data,
|
|
};
|
|
|
|
var json = JsonSerializer.Serialize(snapshot);
|
|
var decoded = JsonSerializer.Deserialize<RaftSnapshot>(json);
|
|
|
|
decoded.ShouldNotBeNull();
|
|
decoded.LastIncludedIndex.ShouldBe(999);
|
|
decoded.LastIncludedTerm.ShouldBe(42);
|
|
decoded.Data.ShouldBe(data);
|
|
}
|
|
|
|
// Go: TestNRGAppendEntryEncode server/raft_test.go:82 — empty snapshot data
|
|
[Fact]
|
|
public void RaftSnapshot_empty_data_round_trips()
|
|
{
|
|
var snapshot = new RaftSnapshot
|
|
{
|
|
LastIncludedIndex = 10,
|
|
LastIncludedTerm = 2,
|
|
Data = [],
|
|
};
|
|
|
|
var json = JsonSerializer.Serialize(snapshot);
|
|
var decoded = JsonSerializer.Deserialize<RaftSnapshot>(json);
|
|
|
|
decoded.ShouldNotBeNull();
|
|
decoded.Data.ShouldBeEmpty();
|
|
}
|
|
|
|
// Go: TestNRGAppendEntryEncode server/raft_test.go:82 — special characters
|
|
[Fact]
|
|
public void RaftLogEntry_special_characters_in_command_round_trips()
|
|
{
|
|
var commands = new[]
|
|
{
|
|
"hello\nworld",
|
|
"tab\there",
|
|
"quote\"inside",
|
|
"backslash\\path",
|
|
"unicode-\u00e9\u00e0\u00fc",
|
|
"{\"nested\":\"json\"}",
|
|
};
|
|
|
|
foreach (var cmd in commands)
|
|
{
|
|
var entry = new RaftLogEntry(Index: 1, Term: 1, Command: cmd);
|
|
var json = JsonSerializer.Serialize(entry);
|
|
var decoded = JsonSerializer.Deserialize<RaftLogEntry>(json);
|
|
decoded.ShouldNotBeNull();
|
|
decoded.Command.ShouldBe(cmd);
|
|
}
|
|
}
|
|
|
|
// Go: TestNRGAppendEntryDecode server/raft_test.go:125 — deserialization of malformed input
|
|
[Fact]
|
|
public void Malformed_json_returns_null_or_throws()
|
|
{
|
|
var badJson = "not-json-at-all";
|
|
Should.Throw<JsonException>(() => JsonSerializer.Deserialize<RaftLogEntry>(badJson));
|
|
}
|
|
}
|