Files
natsdotnet/tests/NATS.Server.Raft.Tests/Raft/RaftWireFormatTests.cs
Joseph Doherty edf9ed770e refactor: extract NATS.Server.Raft.Tests project
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.
2026-03-12 15:36:02 -04:00

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));
}
}