refactor: extract NATS.Server.LeafNodes.Tests project

Move 28 leaf node test files from NATS.Server.Tests into a dedicated
NATS.Server.LeafNodes.Tests project. Update namespaces, add
InternalsVisibleTo, register in solution file. Replace all Task.Delay
polling loops with PollHelper.WaitUntilAsync/YieldForAsync from
TestUtilities. Replace private ReadUntilAsync in LeafProtocolTests
with SocketTestHelper.ReadUntilAsync.

All 281 tests pass.
This commit is contained in:
Joseph Doherty
2026-03-12 15:23:33 -04:00
parent 9972b74bc3
commit 3f7d896a34
31 changed files with 132 additions and 243 deletions

View File

@@ -0,0 +1,179 @@
using NATS.Server.LeafNodes;
namespace NATS.Server.LeafNodes.Tests.LeafNodes;
/// <summary>
/// Tests for leaf node loop detection via $LDS. prefix.
/// Reference: golang/nats-server/server/leafnode_test.go
/// </summary>
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();
}
}