using NATS.Server.JetStream.Cluster; namespace NATS.Server.Tests.JetStream.Cluster; /// /// Tests for validated stream/consumer assignment processing. /// Go reference: jetstream_cluster.go:4541-5925. /// public class JetStreamAssignmentProcessingTests { [Fact] public void ProcessStreamAssignment_validates_config() { var meta = new JetStreamMetaGroup(3); var sa = new StreamAssignment { StreamName = "valid-stream", Group = new RaftGroup { Name = "rg-1", Peers = ["n1", "n2", "n3"] }, ConfigJson = """{"subjects":["test.>"]}""", }; meta.ProcessStreamAssignment(sa).ShouldBeTrue(); meta.StreamCount.ShouldBe(1); } [Fact] public void ProcessStreamAssignment_rejects_empty_name() { var meta = new JetStreamMetaGroup(3); var sa = CreateStreamAssignment("", "rg-1"); meta.ProcessStreamAssignment(sa).ShouldBeFalse(); meta.StreamCount.ShouldBe(0); } [Fact] public void ProcessUpdateStreamAssignment_applies_config_change() { var meta = new JetStreamMetaGroup(3); meta.ProcessStreamAssignment(CreateStreamAssignment("updatable", "rg-u", """{"subjects":["old.>"]}""")); var updated = CreateStreamAssignment("updatable", "rg-u", """{"subjects":["new.>"]}"""); meta.ProcessUpdateStreamAssignment(updated).ShouldBeTrue(); var assignment = meta.GetStreamAssignment("updatable"); assignment!.ConfigJson.ShouldContain("new.>"); } [Fact] public void ProcessUpdateStreamAssignment_returns_false_for_nonexistent() { var meta = new JetStreamMetaGroup(3); var sa = CreateStreamAssignment("ghost", "rg-g"); meta.ProcessUpdateStreamAssignment(sa).ShouldBeFalse(); } [Fact] public void ProcessConsumerAssignment_requires_existing_stream() { var meta = new JetStreamMetaGroup(3); var ca = new ConsumerAssignment { ConsumerName = "orphan-consumer", StreamName = "nonexistent-stream", Group = new RaftGroup { Name = "rg-c", Peers = ["n1", "n2", "n3"] }, }; meta.ProcessConsumerAssignment(ca).ShouldBeFalse(); } [Fact] public void ProcessConsumerAssignment_succeeds_with_existing_stream() { var meta = new JetStreamMetaGroup(3); meta.ProcessStreamAssignment(CreateStreamAssignment("s1", "rg-s1")); var ca = new ConsumerAssignment { ConsumerName = "c1", StreamName = "s1", Group = new RaftGroup { Name = "rg-c1", Peers = ["n1", "n2", "n3"] }, }; meta.ProcessConsumerAssignment(ca).ShouldBeTrue(); meta.ConsumerCount.ShouldBe(1); } [Fact] public void ProcessStreamRemoval_cascades_to_consumers() { var meta = new JetStreamMetaGroup(3); meta.ProcessStreamAssignment(CreateStreamAssignment("cascade", "rg-cas")); meta.ProcessConsumerAssignment(new ConsumerAssignment { ConsumerName = "c1", StreamName = "cascade", Group = new RaftGroup { Name = "rg-c1", Peers = ["n1", "n2", "n3"] }, }); meta.ProcessStreamRemoval("cascade").ShouldBeTrue(); meta.StreamCount.ShouldBe(0); meta.ConsumerCount.ShouldBe(0); } [Fact] public void ProcessStreamRemoval_returns_false_for_nonexistent() { var meta = new JetStreamMetaGroup(3); meta.ProcessStreamRemoval("nope").ShouldBeFalse(); } [Fact] public void ProcessConsumerRemoval_returns_false_for_nonexistent_stream() { var meta = new JetStreamMetaGroup(3); meta.ProcessConsumerRemoval("ghost", "c1").ShouldBeFalse(); } [Fact] public void ProcessConsumerRemoval_returns_false_for_nonexistent_consumer() { var meta = new JetStreamMetaGroup(3); meta.ProcessStreamAssignment(CreateStreamAssignment("s1", "rg-s1")); meta.ProcessConsumerRemoval("s1", "nope").ShouldBeFalse(); } [Fact] public void ProcessConsumerRemoval_succeeds() { var meta = new JetStreamMetaGroup(3); meta.ProcessStreamAssignment(CreateStreamAssignment("s1", "rg-s1")); meta.ProcessConsumerAssignment(new ConsumerAssignment { ConsumerName = "c1", StreamName = "s1", Group = new RaftGroup { Name = "rg-c1", Peers = ["n1", "n2"] }, }); meta.ProcessConsumerRemoval("s1", "c1").ShouldBeTrue(); meta.ConsumerCount.ShouldBe(0); } [Fact] public void ProcessUpdateStreamAssignment_preserves_consumers() { var meta = new JetStreamMetaGroup(3); meta.ProcessStreamAssignment(CreateStreamAssignment("s1", "rg-s1", """{"subjects":["old"]}""")); meta.ProcessConsumerAssignment(new ConsumerAssignment { ConsumerName = "c1", StreamName = "s1", Group = new RaftGroup { Name = "rg-c1", Peers = ["n1", "n2"] }, }); var updated = CreateStreamAssignment("s1", "rg-s1", """{"subjects":["new"]}"""); meta.ProcessUpdateStreamAssignment(updated).ShouldBeTrue(); meta.ConsumerCount.ShouldBe(1); meta.GetConsumerAssignment("s1", "c1").ShouldNotBeNull(); } // Helper to create a StreamAssignment (StreamName is `required` so we must always provide it) private static StreamAssignment CreateStreamAssignment(string name, string groupName, string config = "{}") => new() { StreamName = name, Group = new RaftGroup { Name = groupName, Peers = ["n1", "n2", "n3"] }, ConfigJson = config, }; }