using NATS.Server.LeafNodes; namespace NATS.Server.LeafNodes.Tests.LeafNodes; /// /// Tests for leaf node loop detection via $LDS. prefix. /// Reference: golang/nats-server/server/leafnode_test.go /// public class LeafNodeLoopDetectionTests { // Go: TestLeafNodeLoop server/leafnode_test.go:837 [Fact] public void HasLoopMarker_returns_true_for_marked_subject() { var marked = LeafLoopDetector.Mark("orders.created", "SERVER1"); LeafLoopDetector.HasLoopMarker(marked).ShouldBeTrue(); } [Fact] public void HasLoopMarker_returns_false_for_plain_subject() { LeafLoopDetector.HasLoopMarker("orders.created").ShouldBeFalse(); } [Fact] public void Mark_prepends_LDS_prefix_with_server_id() { LeafLoopDetector.Mark("foo.bar", "ABC123").ShouldBe("$LDS.ABC123.foo.bar"); } [Fact] public void IsLooped_returns_true_when_subject_contains_own_server_id() { var marked = LeafLoopDetector.Mark("foo.bar", "MYSERVER"); LeafLoopDetector.IsLooped(marked, "MYSERVER").ShouldBeTrue(); } [Fact] public void IsLooped_returns_false_when_subject_contains_different_server_id() { var marked = LeafLoopDetector.Mark("foo.bar", "OTHER"); LeafLoopDetector.IsLooped(marked, "MYSERVER").ShouldBeFalse(); } // Go: TestLeafNodeLoopDetectionOnActualLoop server/leafnode_test.go:9410 [Fact] public void TryUnmark_extracts_original_subject_from_single_mark() { var marked = LeafLoopDetector.Mark("orders.created", "S1"); LeafLoopDetector.TryUnmark(marked, out var unmarked).ShouldBeTrue(); unmarked.ShouldBe("orders.created"); } [Fact] public void TryUnmark_extracts_original_subject_from_nested_marks() { var nested = LeafLoopDetector.Mark(LeafLoopDetector.Mark("data.stream", "S1"), "S2"); LeafLoopDetector.TryUnmark(nested, out var unmarked).ShouldBeTrue(); unmarked.ShouldBe("data.stream"); } [Fact] public void TryUnmark_extracts_original_from_triple_nested_marks() { var tripleNested = LeafLoopDetector.Mark( LeafLoopDetector.Mark(LeafLoopDetector.Mark("test.subject", "S1"), "S2"), "S3"); LeafLoopDetector.TryUnmark(tripleNested, out var unmarked).ShouldBeTrue(); unmarked.ShouldBe("test.subject"); } [Fact] public void TryUnmark_returns_false_for_unmarked_subject() { LeafLoopDetector.TryUnmark("orders.created", out var unmarked).ShouldBeFalse(); unmarked.ShouldBe("orders.created"); } [Fact] public void Mark_preserves_dot_separated_structure() { var marked = LeafLoopDetector.Mark("a.b.c.d", "SRV"); marked.ShouldStartWith("$LDS.SRV."); marked.ShouldEndWith("a.b.c.d"); } // Go: TestLeafNodeLoopDetectionWithMultipleClusters server/leafnode_test.go:3546 [Fact] public void IsLooped_detects_loop_in_nested_marks() { var marked = LeafLoopDetector.Mark(LeafLoopDetector.Mark("test", "REMOTE"), "LOCAL"); LeafLoopDetector.IsLooped(marked, "LOCAL").ShouldBeTrue(); LeafLoopDetector.IsLooped(marked, "REMOTE").ShouldBeFalse(); } [Fact] public void HasLoopMarker_works_with_prefix_only() { LeafLoopDetector.HasLoopMarker("$LDS.").ShouldBeTrue(); } [Fact] public void IsLooped_returns_false_for_plain_subject() { LeafLoopDetector.IsLooped("plain.subject", "MYSERVER").ShouldBeFalse(); } [Fact] public void Mark_with_single_token_subject() { var marked = LeafLoopDetector.Mark("simple", "S1"); marked.ShouldBe("$LDS.S1.simple"); LeafLoopDetector.TryUnmark(marked, out var unmarked).ShouldBeTrue(); unmarked.ShouldBe("simple"); } // Go: TestLeafNodeLoopFromDAG server/leafnode_test.go:899 [Fact] public void Multiple_servers_in_chain_each_add_their_mark() { var original = "data.stream"; var fromS1 = LeafLoopDetector.Mark(original, "S1"); fromS1.ShouldBe("$LDS.S1.data.stream"); var fromS2 = LeafLoopDetector.Mark(fromS1, "S2"); fromS2.ShouldBe("$LDS.S2.$LDS.S1.data.stream"); LeafLoopDetector.IsLooped(fromS2, "S2").ShouldBeTrue(); LeafLoopDetector.IsLooped(fromS2, "S1").ShouldBeFalse(); LeafLoopDetector.TryUnmark(fromS2, out var unmarked).ShouldBeTrue(); unmarked.ShouldBe("data.stream"); } [Fact] public void Roundtrip_mark_unmark_preserves_original() { var subjects = new[] { "foo", "foo.bar", "foo.bar.baz", "a.b.c.d.e", "single", "with.*.wildcard", "with.>" }; foreach (var subject in subjects) { var marked = LeafLoopDetector.Mark(subject, "TESTSRV"); LeafLoopDetector.TryUnmark(marked, out var unmarked).ShouldBeTrue(); unmarked.ShouldBe(subject, $"Failed roundtrip for: {subject}"); } } [Fact] public void Four_server_chain_marks_and_unmarks_correctly() { var step1 = LeafLoopDetector.Mark("test", "A"); var step2 = LeafLoopDetector.Mark(step1, "B"); var step3 = LeafLoopDetector.Mark(step2, "C"); var step4 = LeafLoopDetector.Mark(step3, "D"); LeafLoopDetector.IsLooped(step4, "D").ShouldBeTrue(); LeafLoopDetector.IsLooped(step4, "C").ShouldBeFalse(); LeafLoopDetector.IsLooped(step4, "B").ShouldBeFalse(); LeafLoopDetector.IsLooped(step4, "A").ShouldBeFalse(); LeafLoopDetector.TryUnmark(step4, out var unmarked).ShouldBeTrue(); unmarked.ShouldBe("test"); } [Fact] public void HasLoopMarker_is_case_sensitive() { LeafLoopDetector.HasLoopMarker("$LDS.SRV.foo").ShouldBeTrue(); LeafLoopDetector.HasLoopMarker("$lds.SRV.foo").ShouldBeFalse(); } // Go: TestLeafNodeLoopDetectedOnAcceptSide server/leafnode_test.go:1522 [Fact] public void IsLooped_is_case_sensitive_for_server_id() { var marked = LeafLoopDetector.Mark("foo", "MYSERVER"); LeafLoopDetector.IsLooped(marked, "MYSERVER").ShouldBeTrue(); LeafLoopDetector.IsLooped(marked, "myserver").ShouldBeFalse(); } }