// Go parity: golang/nats-server/server/jetstream_cluster.go // Covers: RaftGroup quorum calculation, HasQuorum checks, StreamAssignment // and ConsumerAssignment creation, consumer dictionary operations, // Preferred peer tracking. using NATS.Server.JetStream.Cluster; namespace NATS.Server.JetStream.Tests.JetStream.Cluster; /// /// Tests for ClusterAssignmentTypes: RaftGroup quorum semantics, /// StreamAssignment lifecycle, and ConsumerAssignment defaults. /// Go reference: jetstream_cluster.go:154-266 (raftGroup, streamAssignment, consumerAssignment). /// public class AssignmentSerializationTests { // --------------------------------------------------------------- // RaftGroup quorum calculation // Go reference: jetstream_cluster.go:154-163 raftGroup.quorumNeeded() // --------------------------------------------------------------- [Fact] public void RaftGroup_quorum_size_for_single_node_is_one() { var group = new RaftGroup { Name = "test-r1", Peers = ["peer-1"] }; group.QuorumSize.ShouldBe(1); } [Fact] public void RaftGroup_quorum_size_for_three_nodes_is_two() { var group = new RaftGroup { Name = "test-r3", Peers = ["p1", "p2", "p3"] }; group.QuorumSize.ShouldBe(2); } [Fact] public void RaftGroup_quorum_size_for_five_nodes_is_three() { var group = new RaftGroup { Name = "test-r5", Peers = ["p1", "p2", "p3", "p4", "p5"] }; group.QuorumSize.ShouldBe(3); } [Fact] public void RaftGroup_quorum_size_for_empty_peers_is_one() { var group = new RaftGroup { Name = "test-empty", Peers = [] }; // (0 / 2) + 1 = 1 group.QuorumSize.ShouldBe(1); } // --------------------------------------------------------------- // HasQuorum checks // Go reference: jetstream_cluster.go raftGroup quorum check // --------------------------------------------------------------- [Fact] public void HasQuorum_returns_true_when_acks_meet_quorum() { var group = new RaftGroup { Name = "q-test", Peers = ["p1", "p2", "p3"] }; group.HasQuorum(2).ShouldBeTrue(); group.HasQuorum(3).ShouldBeTrue(); } [Fact] public void HasQuorum_returns_false_when_acks_below_quorum() { var group = new RaftGroup { Name = "q-test", Peers = ["p1", "p2", "p3"] }; group.HasQuorum(1).ShouldBeFalse(); group.HasQuorum(0).ShouldBeFalse(); } [Fact] public void HasQuorum_single_node_requires_one_ack() { var group = new RaftGroup { Name = "q-r1", Peers = ["p1"] }; group.HasQuorum(1).ShouldBeTrue(); group.HasQuorum(0).ShouldBeFalse(); } [Fact] public void HasQuorum_five_nodes_requires_three_acks() { var group = new RaftGroup { Name = "q-r5", Peers = ["p1", "p2", "p3", "p4", "p5"] }; group.HasQuorum(2).ShouldBeFalse(); group.HasQuorum(3).ShouldBeTrue(); group.HasQuorum(5).ShouldBeTrue(); } // --------------------------------------------------------------- // RaftGroup property defaults // Go reference: jetstream_cluster.go:154-163 // --------------------------------------------------------------- [Fact] public void RaftGroup_defaults_storage_to_file() { var group = new RaftGroup { Name = "defaults" }; group.StorageType.ShouldBe("file"); } [Fact] public void RaftGroup_defaults_cluster_to_empty() { var group = new RaftGroup { Name = "defaults" }; group.Cluster.ShouldBe(string.Empty); } [Fact] public void RaftGroup_preferred_peer_tracking() { var group = new RaftGroup { Name = "pref-test", Peers = ["p1", "p2", "p3"] }; group.Preferred.ShouldBe(string.Empty); group.Preferred = "p2"; group.Preferred.ShouldBe("p2"); } // --------------------------------------------------------------- // StreamAssignment creation // Go reference: jetstream_cluster.go:166-184 streamAssignment // --------------------------------------------------------------- [Fact] public void StreamAssignment_created_with_defaults() { var group = new RaftGroup { Name = "sa-group", Peers = ["p1"] }; var sa = new StreamAssignment { StreamName = "TEST-STREAM", Group = group, }; sa.StreamName.ShouldBe("TEST-STREAM"); sa.Group.ShouldBeSameAs(group); sa.ConfigJson.ShouldBe("{}"); sa.SyncSubject.ShouldBe(string.Empty); sa.Responded.ShouldBeFalse(); sa.Recovering.ShouldBeFalse(); sa.Reassigning.ShouldBeFalse(); sa.Consumers.ShouldBeEmpty(); sa.Created.ShouldBeGreaterThan(DateTime.MinValue); } [Fact] public void StreamAssignment_consumers_dictionary_operations() { var group = new RaftGroup { Name = "sa-cons", Peers = ["p1", "p2", "p3"] }; var sa = new StreamAssignment { StreamName = "MY-STREAM", Group = group, }; var consumerGroup = new RaftGroup { Name = "cons-group", Peers = ["p1"] }; var ca = new ConsumerAssignment { ConsumerName = "durable-1", StreamName = "MY-STREAM", Group = consumerGroup, }; sa.Consumers["durable-1"] = ca; sa.Consumers.Count.ShouldBe(1); sa.Consumers["durable-1"].ConsumerName.ShouldBe("durable-1"); sa.Consumers.Remove("durable-1"); sa.Consumers.ShouldBeEmpty(); } // --------------------------------------------------------------- // ConsumerAssignment creation // Go reference: jetstream_cluster.go:250-266 consumerAssignment // --------------------------------------------------------------- [Fact] public void ConsumerAssignment_created_with_defaults() { var group = new RaftGroup { Name = "ca-group", Peers = ["p1"] }; var ca = new ConsumerAssignment { ConsumerName = "my-consumer", StreamName = "MY-STREAM", Group = group, }; ca.ConsumerName.ShouldBe("my-consumer"); ca.StreamName.ShouldBe("MY-STREAM"); ca.Group.ShouldBeSameAs(group); ca.ConfigJson.ShouldBe("{}"); ca.Responded.ShouldBeFalse(); ca.Recovering.ShouldBeFalse(); ca.Created.ShouldBeGreaterThan(DateTime.MinValue); } [Fact] public void ConsumerAssignment_mutable_flags() { var group = new RaftGroup { Name = "ca-flags", Peers = ["p1"] }; var ca = new ConsumerAssignment { ConsumerName = "c1", StreamName = "S1", Group = group, }; ca.Responded = true; ca.Recovering = true; ca.Responded.ShouldBeTrue(); ca.Recovering.ShouldBeTrue(); } [Fact] public void StreamAssignment_mutable_flags() { var group = new RaftGroup { Name = "sa-flags", Peers = ["p1"] }; var sa = new StreamAssignment { StreamName = "S1", Group = group, }; sa.Responded = true; sa.Recovering = true; sa.Reassigning = true; sa.ConfigJson = """{"subjects":["test.>"]}"""; sa.SyncSubject = "$JS.SYNC.S1"; sa.Responded.ShouldBeTrue(); sa.Recovering.ShouldBeTrue(); sa.Reassigning.ShouldBeTrue(); sa.ConfigJson.ShouldBe("""{"subjects":["test.>"]}"""); sa.SyncSubject.ShouldBe("$JS.SYNC.S1"); } }