Fix E2E test gaps and add comprehensive E2E + parity test suites
- Fix pull consumer fetch: send original stream subject in HMSG (not inbox) so NATS client distinguishes data messages from control messages - Fix MaxAge expiry: add background timer in StreamManager for periodic pruning - Fix JetStream wire format: Go-compatible anonymous objects with string enums, proper offset-based pagination for stream/consumer list APIs - Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream) - Add ~1000 parity tests across all subsystems (gaps closure) - Update gap inventory docs to reflect implementation status
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
using NATS.Server.Raft;
|
||||
|
||||
namespace NATS.Server.Tests.Raft;
|
||||
|
||||
public class RaftConfigAndStateParityBatch1Tests
|
||||
{
|
||||
[Fact]
|
||||
public void RaftState_string_matches_go_labels()
|
||||
{
|
||||
RaftState.Follower.String().ShouldBe("Follower");
|
||||
RaftState.Leader.String().ShouldBe("Leader");
|
||||
RaftState.Candidate.String().ShouldBe("Candidate");
|
||||
RaftState.Closed.String().ShouldBe("Closed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaftConfig_exposes_go_shape_fields()
|
||||
{
|
||||
var cfg = new RaftConfig
|
||||
{
|
||||
Name = "META",
|
||||
Store = new object(),
|
||||
Log = new object(),
|
||||
Track = true,
|
||||
Observer = true,
|
||||
Recovering = true,
|
||||
ScaleUp = true,
|
||||
};
|
||||
|
||||
cfg.Name.ShouldBe("META");
|
||||
cfg.Store.ShouldNotBeNull();
|
||||
cfg.Log.ShouldNotBeNull();
|
||||
cfg.Track.ShouldBeTrue();
|
||||
cfg.Observer.ShouldBeTrue();
|
||||
cfg.Recovering.ShouldBeTrue();
|
||||
cfg.ScaleUp.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaftNode_group_defaults_to_id_when_not_supplied()
|
||||
{
|
||||
using var node = new RaftNode("N1");
|
||||
node.GroupName.ShouldBe("N1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaftNode_group_uses_explicit_value_when_supplied()
|
||||
{
|
||||
using var node = new RaftNode("N1", group: "G1");
|
||||
node.GroupName.ShouldBe("G1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaftNode_created_utc_is_set_on_construction()
|
||||
{
|
||||
var before = DateTime.UtcNow;
|
||||
using var node = new RaftNode("N1");
|
||||
var after = DateTime.UtcNow;
|
||||
|
||||
node.CreatedUtc.ShouldBeGreaterThanOrEqualTo(before);
|
||||
node.CreatedUtc.ShouldBeLessThanOrEqualTo(after);
|
||||
}
|
||||
}
|
||||
149
tests/NATS.Server.Tests/Raft/RaftNodeParityBatch2Tests.cs
Normal file
149
tests/NATS.Server.Tests/Raft/RaftNodeParityBatch2Tests.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using NATS.Server.Raft;
|
||||
|
||||
namespace NATS.Server.Tests.Raft;
|
||||
|
||||
public class RaftNodeParityBatch2Tests
|
||||
{
|
||||
private static RaftNode ElectSingleNodeLeader()
|
||||
{
|
||||
var node = new RaftNode("n1");
|
||||
node.ConfigureCluster([node]);
|
||||
node.StartElection(1);
|
||||
node.IsLeader.ShouldBeTrue();
|
||||
return node;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Leader_tracking_flags_update_on_election_and_heartbeat()
|
||||
{
|
||||
var node1 = new RaftNode("n1");
|
||||
var node2 = new RaftNode("n2");
|
||||
var node3 = new RaftNode("n3");
|
||||
|
||||
node1.ConfigureCluster([node1, node2, node3]);
|
||||
node2.ConfigureCluster([node1, node2, node3]);
|
||||
node3.ConfigureCluster([node1, node2, node3]);
|
||||
|
||||
node1.StartElection(3);
|
||||
node1.ReceiveVote(node2.GrantVote(node1.Term, node1.Id), 3);
|
||||
|
||||
node1.IsLeader.ShouldBeTrue();
|
||||
node1.GroupLeader.ShouldBe("n1");
|
||||
node1.Leaderless.ShouldBeFalse();
|
||||
node1.HadPreviousLeader.ShouldBeTrue();
|
||||
node1.LeaderSince.ShouldNotBeNull();
|
||||
|
||||
node2.ReceiveHeartbeat(node1.Term, fromPeerId: "n1");
|
||||
node2.IsLeader.ShouldBeFalse();
|
||||
node2.GroupLeader.ShouldBe("n1");
|
||||
node2.Leaderless.ShouldBeFalse();
|
||||
node2.HadPreviousLeader.ShouldBeTrue();
|
||||
node2.LeaderSince.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stepdown_clears_group_leader_and_leader_since()
|
||||
{
|
||||
using var leader = ElectSingleNodeLeader();
|
||||
leader.GroupLeader.ShouldBe("n1");
|
||||
leader.LeaderSince.ShouldNotBeNull();
|
||||
|
||||
leader.RequestStepDown();
|
||||
|
||||
leader.Leaderless.ShouldBeTrue();
|
||||
leader.GroupLeader.ShouldBe(RaftNode.NoLeader);
|
||||
leader.LeaderSince.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Observer_mode_can_be_toggled()
|
||||
{
|
||||
using var node = new RaftNode("n1");
|
||||
node.IsObserver.ShouldBeFalse();
|
||||
|
||||
node.SetObserver(true);
|
||||
node.IsObserver.ShouldBeTrue();
|
||||
|
||||
node.SetObserver(false);
|
||||
node.IsObserver.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cluster_size_adjustments_enforce_boot_and_leader_rules()
|
||||
{
|
||||
using var node = new RaftNode("n1");
|
||||
node.ClusterSize().ShouldBe(1);
|
||||
|
||||
node.AdjustBootClusterSize(1).ShouldBeTrue();
|
||||
node.ClusterSize().ShouldBe(2); // floor is 2
|
||||
|
||||
node.ConfigureCluster([node]);
|
||||
node.StartElection(1);
|
||||
node.IsLeader.ShouldBeTrue();
|
||||
|
||||
node.AdjustClusterSize(5).ShouldBeTrue();
|
||||
node.ClusterSize().ShouldBe(5);
|
||||
node.AdjustBootClusterSize(7).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Progress_size_and_applied_accessors_report_expected_values()
|
||||
{
|
||||
using var leader = ElectSingleNodeLeader();
|
||||
await leader.ProposeAsync("abc", CancellationToken.None);
|
||||
await leader.ProposeAsync("de", CancellationToken.None);
|
||||
|
||||
var progress = leader.Progress();
|
||||
progress.Index.ShouldBe(2);
|
||||
progress.Commit.ShouldBe(2);
|
||||
progress.Applied.ShouldBe(2);
|
||||
|
||||
var size = leader.Size();
|
||||
size.Entries.ShouldBe(2);
|
||||
size.Bytes.ShouldBe(5);
|
||||
|
||||
var applied = leader.Applied(1);
|
||||
applied.Entries.ShouldBe(1);
|
||||
applied.Bytes.ShouldBe(3);
|
||||
leader.ProcessedIndex.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Campaign_timeout_randomization_and_defaults_match_go_constants()
|
||||
{
|
||||
using var node = new RaftNode("n1");
|
||||
for (var i = 0; i < 20; i++)
|
||||
{
|
||||
var timeout = node.RandomizedCampaignTimeout();
|
||||
timeout.ShouldBeGreaterThanOrEqualTo(RaftNode.MinCampaignTimeoutDefault);
|
||||
timeout.ShouldBeLessThan(RaftNode.MaxCampaignTimeoutDefault);
|
||||
}
|
||||
|
||||
RaftNode.HbIntervalDefault.ShouldBe(TimeSpan.FromSeconds(1));
|
||||
RaftNode.LostQuorumIntervalDefault.ShouldBe(TimeSpan.FromSeconds(10));
|
||||
RaftNode.ObserverModeIntervalDefault.ShouldBe(TimeSpan.FromHours(48));
|
||||
RaftNode.PeerRemoveTimeoutDefault.ShouldBe(TimeSpan.FromMinutes(5));
|
||||
RaftNode.NoLeader.ShouldBe(string.Empty);
|
||||
RaftNode.NoVote.ShouldBe(string.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stop_wait_for_stop_and_delete_set_lifecycle_state()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"raft-node-delete-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
File.WriteAllText(Path.Combine(path, "marker.txt"), "x");
|
||||
|
||||
using var node = new RaftNode("n1", persistDirectory: path);
|
||||
node.IsDeleted.ShouldBeFalse();
|
||||
|
||||
node.Stop();
|
||||
node.WaitForStop();
|
||||
node.IsDeleted.ShouldBeFalse();
|
||||
Directory.Exists(path).ShouldBeTrue();
|
||||
|
||||
node.Delete();
|
||||
node.IsDeleted.ShouldBeTrue();
|
||||
Directory.Exists(path).ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
79
tests/NATS.Server.Tests/Raft/RaftParityBatch3Tests.cs
Normal file
79
tests/NATS.Server.Tests/Raft/RaftParityBatch3Tests.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using NATS.Server.Raft;
|
||||
|
||||
namespace NATS.Server.Tests.Raft;
|
||||
|
||||
public class RaftParityBatch3Tests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ProposeMulti_proposes_entries_in_order()
|
||||
{
|
||||
using var leader = ElectSingleNodeLeader();
|
||||
|
||||
var indexes = await leader.ProposeMultiAsync(["cmd-1", "cmd-2", "cmd-3"], CancellationToken.None);
|
||||
|
||||
indexes.Count.ShouldBe(3);
|
||||
indexes[0].ShouldBe(1);
|
||||
indexes[1].ShouldBe(2);
|
||||
indexes[2].ShouldBe(3);
|
||||
leader.Log.Entries.Count.ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PeerState_tracks_lag_and_current_flags()
|
||||
{
|
||||
var peer = new RaftPeerState
|
||||
{
|
||||
PeerId = "n2",
|
||||
NextIndex = 10,
|
||||
MatchIndex = 7,
|
||||
LastContact = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
peer.RecalculateLag();
|
||||
peer.RefreshCurrent(TimeSpan.FromSeconds(1));
|
||||
|
||||
peer.Lag.ShouldBe(2);
|
||||
peer.Current.ShouldBeTrue();
|
||||
|
||||
peer.LastContact = DateTime.UtcNow - TimeSpan.FromSeconds(5);
|
||||
peer.RefreshCurrent(TimeSpan.FromSeconds(1));
|
||||
peer.Current.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommittedEntry_contains_index_and_entries()
|
||||
{
|
||||
var entries = new[]
|
||||
{
|
||||
new RaftLogEntry(42, 3, "set x"),
|
||||
new RaftLogEntry(43, 3, "set y"),
|
||||
};
|
||||
|
||||
var committed = new CommittedEntry(43, entries);
|
||||
|
||||
committed.Index.ShouldBe(43);
|
||||
committed.Entries.Count.ShouldBe(2);
|
||||
committed.Entries[0].Command.ShouldBe("set x");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaftEntry_roundtrips_to_wire_shape()
|
||||
{
|
||||
var entry = new RaftEntry(RaftEntryType.AddPeer, new byte[] { 1, 2, 3 });
|
||||
|
||||
var wire = entry.ToWire();
|
||||
var decoded = RaftEntry.FromWire(wire);
|
||||
|
||||
decoded.Type.ShouldBe(RaftEntryType.AddPeer);
|
||||
decoded.Data.ShouldBe(new byte[] { 1, 2, 3 });
|
||||
}
|
||||
|
||||
private static RaftNode ElectSingleNodeLeader()
|
||||
{
|
||||
var node = new RaftNode("n1");
|
||||
node.ConfigureCluster([node]);
|
||||
node.StartElection(1);
|
||||
node.IsLeader.ShouldBeTrue();
|
||||
return node;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user