using System.Net; using System.Net.Sockets; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NATS.Client.Core; using NATS.Server.Auth; using NATS.Server.Configuration; using NATS.Server.Gateways; using NATS.Server.LeafNodes; using NATS.Server.Routes; using NATS.Server.Subscriptions; namespace NATS.Server.Tests.Networking; /// /// Ported Go networking tests for gateway interest mode, route pool accounting, /// and leaf node connections. Each test references the Go function name and file. /// public class NetworkingGoParityTests { // ════════════════════════════════════════════════════════════════════ // GATEWAY INTEREST MODE (~20 tests from gateway_test.go) // ════════════════════════════════════════════════════════════════════ // Go: TestGatewayDontSendSubInterest server/gateway_test.go:1755 [Fact] public void Tracker_starts_in_optimistic_mode() { var tracker = new GatewayInterestTracker(); tracker.GetMode("$G").ShouldBe(GatewayInterestMode.Optimistic); } // Go: TestGatewayAccountInterest server/gateway_test.go:1794 [Fact] public void Tracker_no_interest_accumulates_in_optimistic_mode() { var tracker = new GatewayInterestTracker(noInterestThreshold: 5); for (var i = 0; i < 4; i++) tracker.TrackNoInterest("$G", $"subj.{i}"); tracker.GetMode("$G").ShouldBe(GatewayInterestMode.Optimistic); tracker.ShouldForward("$G", "subj.0").ShouldBeFalse(); tracker.ShouldForward("$G", "other").ShouldBeTrue(); } // Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934 [Fact] public void Tracker_switches_to_interest_only_at_threshold() { var tracker = new GatewayInterestTracker(noInterestThreshold: 3); tracker.TrackNoInterest("$G", "a"); tracker.TrackNoInterest("$G", "b"); tracker.TrackNoInterest("$G", "c"); tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly); } // Go: TestGatewayAccountInterest server/gateway_test.go:1794 [Fact] public void Tracker_interest_only_blocks_unknown_subjects() { var tracker = new GatewayInterestTracker(noInterestThreshold: 1); tracker.TrackNoInterest("$G", "trigger"); tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly); tracker.ShouldForward("$G", "unknown.subject").ShouldBeFalse(); } // Go: TestGatewayAccountInterest server/gateway_test.go:1794 [Fact] public void Tracker_interest_only_forwards_tracked_subjects() { var tracker = new GatewayInterestTracker(noInterestThreshold: 1); tracker.TrackNoInterest("$G", "trigger"); tracker.TrackInterest("$G", "orders.>"); tracker.ShouldForward("$G", "orders.created").ShouldBeTrue(); tracker.ShouldForward("$G", "users.created").ShouldBeFalse(); } // Go: TestGatewayAccountUnsub server/gateway_test.go:1912 [Fact] public void Tracker_removing_interest_in_io_mode_stops_forwarding() { var tracker = new GatewayInterestTracker(noInterestThreshold: 1); tracker.TrackNoInterest("$G", "trigger"); tracker.TrackInterest("$G", "foo"); tracker.ShouldForward("$G", "foo").ShouldBeTrue(); tracker.TrackNoInterest("$G", "foo"); tracker.ShouldForward("$G", "foo").ShouldBeFalse(); } // Go: TestGatewayAccountInterest server/gateway_test.go:1794 [Fact] public void Tracker_accounts_are_independent() { var tracker = new GatewayInterestTracker(noInterestThreshold: 1); tracker.TrackNoInterest("ACCT_A", "trigger"); tracker.GetMode("ACCT_A").ShouldBe(GatewayInterestMode.InterestOnly); tracker.GetMode("ACCT_B").ShouldBe(GatewayInterestMode.Optimistic); tracker.ShouldForward("ACCT_B", "any.subject").ShouldBeTrue(); } // Go: TestGatewaySwitchToInterestOnlyModeImmediately server/gateway_test.go:6934 [Fact] public void Tracker_explicit_switch_to_interest_only() { var tracker = new GatewayInterestTracker(); tracker.SwitchToInterestOnly("$G"); tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly); tracker.ShouldForward("$G", "anything").ShouldBeFalse(); } // Go: TestGatewayAccountInterest server/gateway_test.go:1794 [Fact] public void Tracker_optimistic_mode_interest_add_removes_from_no_interest() { var tracker = new GatewayInterestTracker(); tracker.TrackNoInterest("$G", "foo"); tracker.ShouldForward("$G", "foo").ShouldBeFalse(); tracker.TrackInterest("$G", "foo"); tracker.ShouldForward("$G", "foo").ShouldBeTrue(); } // Go: TestGatewaySubjectInterest server/gateway_test.go:1972 [Fact] public void Tracker_wildcard_interest_matches_in_io_mode() { var tracker = new GatewayInterestTracker(noInterestThreshold: 1); tracker.TrackNoInterest("$G", "trigger"); tracker.TrackInterest("$G", "events.>"); tracker.ShouldForward("$G", "events.created").ShouldBeTrue(); tracker.ShouldForward("$G", "events.a.b.c").ShouldBeTrue(); tracker.ShouldForward("$G", "other").ShouldBeFalse(); } // Go: TestGatewayAccountInterest server/gateway_test.go:1794 [Fact] public void ShouldForwardInterestOnly_uses_SubList_remote_interest() { using var subList = new SubList(); subList.ApplyRemoteSub(new RemoteSubscription("orders.*", null, "gw1", "$G")); GatewayManager.ShouldForwardInterestOnly(subList, "$G", "orders.created").ShouldBeTrue(); GatewayManager.ShouldForwardInterestOnly(subList, "$G", "users.created").ShouldBeFalse(); } // Go: TestGatewayAccountUnsub server/gateway_test.go:1912 [Fact] public void ShouldForwardInterestOnly_respects_removal() { using var subList = new SubList(); subList.ApplyRemoteSub(new RemoteSubscription("orders.*", null, "gw1", "$G")); GatewayManager.ShouldForwardInterestOnly(subList, "$G", "orders.created").ShouldBeTrue(); subList.ApplyRemoteSub(RemoteSubscription.Removal("orders.*", null, "gw1", "$G")); GatewayManager.ShouldForwardInterestOnly(subList, "$G", "orders.created").ShouldBeFalse(); } // Go: TestGatewaySubjectInterest server/gateway_test.go:1972 [Fact] public async Task Gateway_propagates_subject_interest_end_to_end() { await using var fixture = await TwoGatewayFixture.StartAsync(); await using var conn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Remote.Port}", }); await conn.ConnectAsync(); await using var sub = await conn.SubscribeCoreAsync("gw.interest.test"); await conn.PingAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested && !fixture.Local.HasRemoteInterest("gw.interest.test")) await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); fixture.Local.HasRemoteInterest("gw.interest.test").ShouldBeTrue(); } // Go: TestGatewayDontSendSubInterest server/gateway_test.go:1755 [Fact] public async Task Gateway_message_forwarded_to_remote_subscriber() { await using var fixture = await TwoGatewayFixture.StartAsync(); await using var remoteConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Remote.Port}", }); await remoteConn.ConnectAsync(); await using var localConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Local.Port}", }); await localConn.ConnectAsync(); await using var sub = await remoteConn.SubscribeCoreAsync("gw.fwd.test"); await remoteConn.PingAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested && !fixture.Local.HasRemoteInterest("gw.fwd.test")) await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); await localConn.PublishAsync("gw.fwd.test", "gateway-msg"); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); (await sub.Msgs.ReadAsync(cts.Token)).Data.ShouldBe("gateway-msg"); } // Go: TestGatewayAccountUnsub server/gateway_test.go:1912 [Fact] public async Task Gateway_unsubscribe_removes_remote_interest() { await using var fixture = await TwoGatewayFixture.StartAsync(); await using var conn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Remote.Port}", }); await conn.ConnectAsync(); var sub = await conn.SubscribeCoreAsync("gw.unsub.test"); await conn.PingAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested && !fixture.Local.HasRemoteInterest("gw.unsub.test")) await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); fixture.Local.HasRemoteInterest("gw.unsub.test").ShouldBeTrue(); await sub.DisposeAsync(); await conn.PingAsync(); using var unsTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!unsTimeout.IsCancellationRequested && fixture.Local.HasRemoteInterest("gw.unsub.test")) await Task.Delay(50, unsTimeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); fixture.Local.HasRemoteInterest("gw.unsub.test").ShouldBeFalse(); } // Go: TestGatewayNoAccInterestThenQSubThenRegularSub server/gateway_test.go:5643 [Fact] public async Task Gateway_wildcard_interest_propagates() { await using var fixture = await TwoGatewayFixture.StartAsync(); await using var conn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Remote.Port}", }); await conn.ConnectAsync(); await using var sub = await conn.SubscribeCoreAsync("gw.wild.>"); await conn.PingAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested && !fixture.Local.HasRemoteInterest("gw.wild.test")) await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); fixture.Local.HasRemoteInterest("gw.wild.test").ShouldBeTrue(); fixture.Local.HasRemoteInterest("gw.wild.deep.nested").ShouldBeTrue(); } // Go: TestGatewayNoCrashOnInvalidSubject server/gateway_test.go:6279 [Fact] public void Invalid_subject_does_not_crash_SubList() { using var subList = new SubList(); // Should handle gracefully, not throw subList.HasRemoteInterest("$G", "valid.subject").ShouldBeFalse(); subList.HasRemoteInterest("$G", "").ShouldBeFalse(); } // Go: TestGatewayLogAccountInterestModeSwitch server/gateway_test.go:5843 [Fact] public void Tracker_default_threshold_is_1000() { GatewayInterestTracker.DefaultNoInterestThreshold.ShouldBe(1000); } // Go: TestGatewayAccountInterestModeSwitchOnlyOncePerAccount server/gateway_test.go:5932 [Fact] public void Tracker_switch_is_idempotent() { var tracker = new GatewayInterestTracker(noInterestThreshold: 1); tracker.TrackNoInterest("$G", "a"); tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly); // Switching again should not change state tracker.SwitchToInterestOnly("$G"); tracker.GetMode("$G").ShouldBe(GatewayInterestMode.InterestOnly); } // Go: TestGatewayReplyMappingBasic server/gateway_test.go:3200 [Fact] public void Reply_mapper_round_trips() { var mapped = ReplyMapper.ToGatewayReply("INBOX.abc123", "SERVERID1"); mapped.ShouldNotBeNull(); mapped!.ShouldStartWith("_GR_."); ReplyMapper.HasGatewayReplyPrefix(mapped).ShouldBeTrue(); ReplyMapper.TryRestoreGatewayReply(mapped, out var restored).ShouldBeTrue(); restored.ShouldBe("INBOX.abc123"); } // Go: TestGatewayReplyMappingBasic server/gateway_test.go:3200 [Fact] public void Reply_mapper_null_input_returns_null() { var result = ReplyMapper.ToGatewayReply(null, "S1"); result.ShouldBeNull(); } // ════════════════════════════════════════════════════════════════════ // ROUTE POOL ACCOUNTING (~15 tests from routes_test.go) // ════════════════════════════════════════════════════════════════════ // Go: TestRoutePool server/routes_test.go:1966 [Fact] public void Route_pool_idx_deterministic_for_same_account() { var idx1 = RouteManager.ComputeRoutePoolIdx(3, "$G"); var idx2 = RouteManager.ComputeRoutePoolIdx(3, "$G"); idx1.ShouldBe(idx2); } // Go: TestRoutePool server/routes_test.go:1966 [Fact] public void Route_pool_idx_in_range() { for (var poolSize = 1; poolSize <= 10; poolSize++) { var idx = RouteManager.ComputeRoutePoolIdx(poolSize, "$G"); idx.ShouldBeGreaterThanOrEqualTo(0); idx.ShouldBeLessThan(poolSize); } } // Go: TestRoutePool server/routes_test.go:1966 [Fact] public void Route_pool_idx_distributes_accounts() { var accounts = new[] { "$G", "ACCT_A", "ACCT_B", "ACCT_C", "ACCT_D" }; var poolSize = 3; var indices = new HashSet(); foreach (var account in accounts) indices.Add(RouteManager.ComputeRoutePoolIdx(poolSize, account)); // With 5 accounts and pool of 3, we should use at least 2 different indices indices.Count.ShouldBeGreaterThanOrEqualTo(2); } // Go: TestRoutePool server/routes_test.go:1966 [Fact] public void Route_pool_idx_single_pool_always_zero() { RouteManager.ComputeRoutePoolIdx(1, "$G").ShouldBe(0); RouteManager.ComputeRoutePoolIdx(1, "ACCT_A").ShouldBe(0); RouteManager.ComputeRoutePoolIdx(1, "ACCT_B").ShouldBe(0); } // Go: TestRoutePoolConnectRace server/routes_test.go:2100 [Fact] public async Task Route_pool_default_three_connections_per_peer() { var optsA = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = Guid.NewGuid().ToString("N"), Host = "127.0.0.1", Port = 0, }, }; var serverA = new NatsServer(optsA, NullLoggerFactory.Instance); var ctsA = new CancellationTokenSource(); _ = serverA.StartAsync(ctsA.Token); await serverA.WaitForReadyAsync(); try { var optsB = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = Guid.NewGuid().ToString("N"), Host = "127.0.0.1", Port = 0, Routes = [serverA.ClusterListen!], }, }; var serverB = new NatsServer(optsB, NullLoggerFactory.Instance); var ctsB = new CancellationTokenSource(); _ = serverB.StartAsync(ctsB.Token); await serverB.WaitForReadyAsync(); try { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested && serverA.Stats.Routes < 3) await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); serverA.Stats.Routes.ShouldBeGreaterThanOrEqualTo(3); } finally { await ctsB.CancelAsync(); serverB.Dispose(); ctsB.Dispose(); } } finally { await ctsA.CancelAsync(); serverA.Dispose(); ctsA.Dispose(); } } // Go: TestRoutePoolRouteStoredSameIndexBothSides server/routes_test.go:2180 [Fact] public void Route_pool_idx_uses_FNV1a_hash() { // Go uses fnv.New32a() — FNV-1a 32-bit // Verify we produce the same hash for known inputs var idx = RouteManager.ComputeRoutePoolIdx(10, "$G"); idx.ShouldBeGreaterThanOrEqualTo(0); idx.ShouldBeLessThan(10); // Same input always produces same output RouteManager.ComputeRoutePoolIdx(10, "$G").ShouldBe(idx); } // Go: TestRoutePoolPerAccountSubUnsubProtoParsing server/routes_test.go:3104 [Fact] public async Task Route_subscription_propagation_between_peers() { var optsA = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = Guid.NewGuid().ToString("N"), Host = "127.0.0.1", Port = 0, }, }; var serverA = new NatsServer(optsA, NullLoggerFactory.Instance); var ctsA = new CancellationTokenSource(); _ = serverA.StartAsync(ctsA.Token); await serverA.WaitForReadyAsync(); try { var optsB = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = Guid.NewGuid().ToString("N"), Host = "127.0.0.1", Port = 0, Routes = [serverA.ClusterListen!], }, }; var serverB = new NatsServer(optsB, NullLoggerFactory.Instance); var ctsB = new CancellationTokenSource(); _ = serverB.StartAsync(ctsB.Token); await serverB.WaitForReadyAsync(); try { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested && serverA.Stats.Routes < 3) await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); await using var conn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{serverB.Port}", }); await conn.ConnectAsync(); await using var sub = await conn.SubscribeCoreAsync("route.sub.test"); await conn.PingAsync(); using var interest = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!interest.IsCancellationRequested && !serverA.HasRemoteInterest("route.sub.test")) await Task.Delay(50, interest.Token).ContinueWith(_ => { }, TaskScheduler.Default); serverA.HasRemoteInterest("route.sub.test").ShouldBeTrue(); } finally { await ctsB.CancelAsync(); serverB.Dispose(); ctsB.Dispose(); } } finally { await ctsA.CancelAsync(); serverA.Dispose(); ctsA.Dispose(); } } // Go: TestRoutePerAccount server/routes_test.go:2539 [Fact] public void Route_pool_different_accounts_can_get_different_indices() { // With a large pool, different accounts should hash to different slots var indices = new Dictionary(); for (var i = 0; i < 100; i++) { var acct = $"account_{i}"; indices[acct] = RouteManager.ComputeRoutePoolIdx(100, acct); } // With 100 accounts and pool size 100, we should have decent distribution var uniqueIndices = indices.Values.Distinct().Count(); uniqueIndices.ShouldBeGreaterThan(20); } // Go: TestRouteSendLocalSubsWithLowMaxPending server/routes_test.go:1098 [Fact] public async Task Route_message_forwarded_to_subscriber_on_peer() { var optsA = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = Guid.NewGuid().ToString("N"), Host = "127.0.0.1", Port = 0, }, }; var serverA = new NatsServer(optsA, NullLoggerFactory.Instance); var ctsA = new CancellationTokenSource(); _ = serverA.StartAsync(ctsA.Token); await serverA.WaitForReadyAsync(); try { var optsB = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = Guid.NewGuid().ToString("N"), Host = "127.0.0.1", Port = 0, Routes = [serverA.ClusterListen!], }, }; var serverB = new NatsServer(optsB, NullLoggerFactory.Instance); var ctsB = new CancellationTokenSource(); _ = serverB.StartAsync(ctsB.Token); await serverB.WaitForReadyAsync(); try { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested && serverA.Stats.Routes < 3) await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); await using var subConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{serverB.Port}", }); await subConn.ConnectAsync(); await using var pubConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{serverA.Port}", }); await pubConn.ConnectAsync(); await using var sub = await subConn.SubscribeCoreAsync("route.fwd.test"); await subConn.PingAsync(); using var interest = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!interest.IsCancellationRequested && !serverA.HasRemoteInterest("route.fwd.test")) await Task.Delay(50, interest.Token).ContinueWith(_ => { }, TaskScheduler.Default); await pubConn.PublishAsync("route.fwd.test", "routed-msg"); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); (await sub.Msgs.ReadAsync(cts.Token)).Data.ShouldBe("routed-msg"); } finally { await ctsB.CancelAsync(); serverB.Dispose(); ctsB.Dispose(); } } finally { await ctsA.CancelAsync(); serverA.Dispose(); ctsA.Dispose(); } } // Go: TestRoutePoolAndPerAccountErrors server/routes_test.go:1906 [Fact] public void Route_pool_idx_zero_pool_returns_zero() { RouteManager.ComputeRoutePoolIdx(0, "$G").ShouldBe(0); } // Go: TestRoutePoolSizeDifferentOnEachServer server/routes_test.go:2254 [Fact] public void Route_pool_idx_consistent_across_sizes() { // The hash should be deterministic regardless of pool size var hashSmall = RouteManager.ComputeRoutePoolIdx(3, "test"); var hashLarge = RouteManager.ComputeRoutePoolIdx(100, "test"); hashSmall.ShouldBeGreaterThanOrEqualTo(0); hashLarge.ShouldBeGreaterThanOrEqualTo(0); } // ════════════════════════════════════════════════════════════════════ // LEAF NODE CONNECTIONS (~20 tests from leafnode_test.go) // ════════════════════════════════════════════════════════════════════ // Go: TestLeafNodeLoop server/leafnode_test.go:837 [Fact] public void Leaf_loop_detector_marks_and_detects() { var marked = LeafLoopDetector.Mark("test.subject", "S1"); LeafLoopDetector.HasLoopMarker(marked).ShouldBeTrue(); LeafLoopDetector.IsLooped(marked, "S1").ShouldBeTrue(); LeafLoopDetector.IsLooped(marked, "S2").ShouldBeFalse(); } // Go: TestLeafNodeLoop server/leafnode_test.go:837 [Fact] public void Leaf_loop_detector_unmarks() { var marked = LeafLoopDetector.Mark("orders.created", "SERVER1"); LeafLoopDetector.TryUnmark(marked, out var unmarked).ShouldBeTrue(); unmarked.ShouldBe("orders.created"); } // Go: TestLeafNodeLoop server/leafnode_test.go:837 [Fact] public void Leaf_loop_detector_non_marked_returns_false() { LeafLoopDetector.HasLoopMarker("plain.subject").ShouldBeFalse(); LeafLoopDetector.IsLooped("plain.subject", "S1").ShouldBeFalse(); LeafLoopDetector.TryUnmark("plain.subject", out _).ShouldBeFalse(); } // Go: TestLeafNodeBasicAuthSingleton server/leafnode_test.go:602 [Fact] public async Task Leaf_connection_handshake_succeeds() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await remoteSocket.ConnectAsync(IPAddress.Loopback, port); using var leafSocket = await listener.AcceptSocketAsync(); await using var leaf = new LeafConnection(leafSocket); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL1", cts.Token); (await ReadLineAsync(remoteSocket, cts.Token)).ShouldBe("LEAF LOCAL1"); await WriteLineAsync(remoteSocket, "LEAF REMOTE1", cts.Token); await handshakeTask; leaf.RemoteId.ShouldBe("REMOTE1"); } // Go: TestLeafNodeRTT server/leafnode_test.go:488 [Fact] public async Task Leaf_connection_inbound_handshake() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await remoteSocket.ConnectAsync(IPAddress.Loopback, port); using var leafSocket = await listener.AcceptSocketAsync(); await using var leaf = new LeafConnection(leafSocket); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var handshakeTask = leaf.PerformInboundHandshakeAsync("SERVER1", cts.Token); await WriteLineAsync(remoteSocket, "LEAF REMOTE2", cts.Token); (await ReadLineAsync(remoteSocket, cts.Token)).ShouldBe("LEAF SERVER1"); await handshakeTask; leaf.RemoteId.ShouldBe("REMOTE2"); } // Go: TestLeafNodePermissions server/leafnode_test.go:1267 [Fact] public async Task Leaf_LS_plus_sends_subscription_interest() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await remoteSocket.ConnectAsync(IPAddress.Loopback, port); using var leafSocket = await listener.AcceptSocketAsync(); await using var leaf = new LeafConnection(leafSocket); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token); (await ReadLineAsync(remoteSocket, cts.Token)).ShouldStartWith("LEAF "); await WriteLineAsync(remoteSocket, "LEAF SPOKE", cts.Token); await handshakeTask; await leaf.SendLsPlusAsync("$G", "test.subject", null, cts.Token); (await ReadLineAsync(remoteSocket, cts.Token)).ShouldBe("LS+ $G test.subject"); } // Go: TestLeafNodePermissions server/leafnode_test.go:1267 [Fact] public async Task Leaf_LS_minus_sends_unsubscription() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await remoteSocket.ConnectAsync(IPAddress.Loopback, port); using var leafSocket = await listener.AcceptSocketAsync(); await using var leaf = new LeafConnection(leafSocket); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token); (await ReadLineAsync(remoteSocket, cts.Token)).ShouldStartWith("LEAF "); await WriteLineAsync(remoteSocket, "LEAF SPOKE", cts.Token); await handshakeTask; await leaf.SendLsMinusAsync("$G", "test.subject", null, cts.Token); (await ReadLineAsync(remoteSocket, cts.Token)).ShouldBe("LS- $G test.subject"); } // Go: TestLeafNodePermissions server/leafnode_test.go:1267 [Fact] public async Task Leaf_LS_plus_with_queue_group() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await remoteSocket.ConnectAsync(IPAddress.Loopback, port); using var leafSocket = await listener.AcceptSocketAsync(); await using var leaf = new LeafConnection(leafSocket); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token); (await ReadLineAsync(remoteSocket, cts.Token)).ShouldStartWith("LEAF "); await WriteLineAsync(remoteSocket, "LEAF SPOKE", cts.Token); await handshakeTask; await leaf.SendLsPlusAsync("$G", "queue.subject", "workers", cts.Token); (await ReadLineAsync(remoteSocket, cts.Token)).ShouldBe("LS+ $G queue.subject workers"); } // Go: TestLeafNodeInterestPropagationDaisychain server/leafnode_test.go:3953 [Fact] public async Task Leaf_receives_remote_subscription() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await remoteSocket.ConnectAsync(IPAddress.Loopback, port); using var leafSocket = await listener.AcceptSocketAsync(); await using var leaf = new LeafConnection(leafSocket); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token); (await ReadLineAsync(remoteSocket, cts.Token)).ShouldStartWith("LEAF "); await WriteLineAsync(remoteSocket, "LEAF SPOKE", cts.Token); await handshakeTask; var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); leaf.RemoteSubscriptionReceived = sub => { received.TrySetResult(sub); return Task.CompletedTask; }; leaf.StartLoop(cts.Token); await WriteLineAsync(remoteSocket, "LS+ $G events.>", cts.Token); var result = await received.Task.WaitAsync(cts.Token); result.Account.ShouldBe("$G"); result.Subject.ShouldBe("events.>"); result.IsRemoval.ShouldBeFalse(); } // Go: TestLeafNodeInterestPropagationDaisychain server/leafnode_test.go:3953 [Fact] public async Task Leaf_receives_remote_unsubscription() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await remoteSocket.ConnectAsync(IPAddress.Loopback, port); using var leafSocket = await listener.AcceptSocketAsync(); await using var leaf = new LeafConnection(leafSocket); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token); (await ReadLineAsync(remoteSocket, cts.Token)).ShouldStartWith("LEAF "); await WriteLineAsync(remoteSocket, "LEAF SPOKE", cts.Token); await handshakeTask; var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); leaf.RemoteSubscriptionReceived = sub => { if (sub.IsRemoval) received.TrySetResult(sub); return Task.CompletedTask; }; leaf.StartLoop(cts.Token); await WriteLineAsync(remoteSocket, "LS+ $G events.>", cts.Token); await Task.Delay(100); await WriteLineAsync(remoteSocket, "LS- $G events.>", cts.Token); var result = await received.Task.WaitAsync(cts.Token); result.IsRemoval.ShouldBeTrue(); result.Subject.ShouldBe("events.>"); } // Go: TestLeafNodeOriginClusterInfo server/leafnode_test.go:1942 [Fact] public async Task Leaf_handshake_propagates_JetStream_domain() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await remoteSocket.ConnectAsync(IPAddress.Loopback, port); using var leafSocket = await listener.AcceptSocketAsync(); await using var leaf = new LeafConnection(leafSocket) { JetStreamDomain = "hub-domain" }; using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token); var line = await ReadLineAsync(remoteSocket, cts.Token); line.ShouldBe("LEAF HUB domain=hub-domain"); await WriteLineAsync(remoteSocket, "LEAF SPOKE domain=spoke-domain", cts.Token); await handshakeTask; leaf.RemoteJetStreamDomain.ShouldBe("spoke-domain"); } // Go: TestLeafNodeRemoteIsHub server/leafnode_test.go:1177 [Fact] public async Task Leaf_manager_solicited_connection_backoff() { // Verify the exponential backoff computation LeafNodeManager.ComputeBackoff(0).ShouldBe(TimeSpan.FromSeconds(1)); LeafNodeManager.ComputeBackoff(1).ShouldBe(TimeSpan.FromSeconds(2)); LeafNodeManager.ComputeBackoff(2).ShouldBe(TimeSpan.FromSeconds(4)); LeafNodeManager.ComputeBackoff(3).ShouldBe(TimeSpan.FromSeconds(8)); LeafNodeManager.ComputeBackoff(4).ShouldBe(TimeSpan.FromSeconds(16)); LeafNodeManager.ComputeBackoff(5).ShouldBe(TimeSpan.FromSeconds(32)); LeafNodeManager.ComputeBackoff(6).ShouldBe(TimeSpan.FromSeconds(60)); LeafNodeManager.ComputeBackoff(7).ShouldBe(TimeSpan.FromSeconds(60)); LeafNodeManager.ComputeBackoff(-1).ShouldBe(TimeSpan.FromSeconds(1)); } // Go: TestLeafNodeHubWithGateways server/leafnode_test.go:1584 [Fact] public async Task Leaf_hub_spoke_message_round_trip() { await using var fixture = await LeafFixture.StartAsync(); await using var hubConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Hub.Port}", }); await hubConn.ConnectAsync(); await using var spokeConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Spoke.Port}", }); await spokeConn.ConnectAsync(); await using var sub = await spokeConn.SubscribeCoreAsync("leaf.roundtrip"); await spokeConn.PingAsync(); await fixture.WaitForRemoteInterestOnHubAsync("leaf.roundtrip"); await hubConn.PublishAsync("leaf.roundtrip", "round-trip-msg"); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); (await sub.Msgs.ReadAsync(cts.Token)).Data.ShouldBe("round-trip-msg"); } // Go: TestLeafNodeStreamAndShadowSubs server/leafnode_test.go:6176 [Fact] public async Task Leaf_spoke_to_hub_message_delivery() { await using var fixture = await LeafFixture.StartAsync(); await using var hubConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Hub.Port}", }); await hubConn.ConnectAsync(); await using var spokeConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Spoke.Port}", }); await spokeConn.ConnectAsync(); await using var sub = await hubConn.SubscribeCoreAsync("leaf.reverse"); await hubConn.PingAsync(); await fixture.WaitForRemoteInterestOnSpokeAsync("leaf.reverse"); await spokeConn.PublishAsync("leaf.reverse", "reverse-msg"); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); (await sub.Msgs.ReadAsync(cts.Token)).Data.ShouldBe("reverse-msg"); } // Go: TestLeafNodeQueueGroupDistribution server/leafnode_test.go:4021 [Fact] public async Task Leaf_queue_subscription_delivery() { await using var fixture = await LeafFixture.StartAsync(); await using var hubConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Hub.Port}", }); await hubConn.ConnectAsync(); await using var spokeConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{fixture.Spoke.Port}", }); await spokeConn.ConnectAsync(); await using var sub = await spokeConn.SubscribeCoreAsync("leaf.queue", queueGroup: "workers"); await spokeConn.PingAsync(); await fixture.WaitForRemoteInterestOnHubAsync("leaf.queue"); await hubConn.PublishAsync("leaf.queue", "queue-msg"); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); (await sub.Msgs.ReadAsync(cts.Token)).Data.ShouldBe("queue-msg"); } // Go: TestLeafNodeDuplicateMsg server/leafnode_test.go:6513 [Fact] public async Task Leaf_no_remote_interest_for_unsubscribed_subject() { await using var fixture = await LeafFixture.StartAsync(); fixture.Hub.HasRemoteInterest("nonexistent.leaf.subject").ShouldBeFalse(); fixture.Spoke.HasRemoteInterest("nonexistent.leaf.subject").ShouldBeFalse(); } // Go: TestLeafNodePermissions server/leafnode_test.go:1267 [Fact] public async Task Leaf_connection_LMSG_sends_message() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await remoteSocket.ConnectAsync(IPAddress.Loopback, port); using var leafSocket = await listener.AcceptSocketAsync(); await using var leaf = new LeafConnection(leafSocket); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token); (await ReadLineAsync(remoteSocket, cts.Token)).ShouldStartWith("LEAF "); await WriteLineAsync(remoteSocket, "LEAF SPOKE", cts.Token); await handshakeTask; var payload = Encoding.UTF8.GetBytes("hello-leaf"); await leaf.SendMessageAsync("$G", "test.msg", "reply.to", payload, cts.Token); var line = await ReadLineAsync(remoteSocket, cts.Token); line.ShouldBe("LMSG $G test.msg reply.to 10"); // Read payload + CRLF var buf = new byte[12]; // 10 payload + 2 CRLF var offset = 0; while (offset < 12) { var n = await remoteSocket.ReceiveAsync(buf.AsMemory(offset), SocketFlags.None, cts.Token); offset += n; } Encoding.UTF8.GetString(buf, 0, 10).ShouldBe("hello-leaf"); } // Go: TestLeafNodeIsolatedLeafSubjectPropagationGlobal server/leafnode_test.go:10280 [Fact] public async Task Leaf_LMSG_with_no_reply() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await remoteSocket.ConnectAsync(IPAddress.Loopback, ((IPEndPoint)listener.LocalEndpoint).Port); using var leafSocket = await listener.AcceptSocketAsync(); await using var leaf = new LeafConnection(leafSocket); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var handshakeTask = leaf.PerformOutboundHandshakeAsync("HUB", cts.Token); (await ReadLineAsync(remoteSocket, cts.Token)).ShouldStartWith("LEAF "); await WriteLineAsync(remoteSocket, "LEAF SPOKE", cts.Token); await handshakeTask; await leaf.SendMessageAsync("$G", "no.reply", null, "data"u8.ToArray(), cts.Token); var line = await ReadLineAsync(remoteSocket, cts.Token); line.ShouldBe("LMSG $G no.reply - 4"); } // ════════════════════════════════════════════════════════════════════ // Helpers // ════════════════════════════════════════════════════════════════════ private static async Task ReadLineAsync(Socket socket, CancellationToken ct) { var bytes = new List(64); var single = new byte[1]; while (true) { var read = await socket.ReceiveAsync(single, SocketFlags.None, ct); if (read == 0) break; if (single[0] == (byte)'\n') break; if (single[0] != (byte)'\r') bytes.Add(single[0]); } return Encoding.ASCII.GetString([.. bytes]); } private static Task WriteLineAsync(Socket socket, string line, CancellationToken ct) => socket.SendAsync(Encoding.ASCII.GetBytes($"{line}\r\n"), SocketFlags.None, ct).AsTask(); } // ════════════════════════════════════════════════════════════════════════ // Shared Fixtures // ════════════════════════════════════════════════════════════════════════ internal sealed class TwoGatewayFixture : IAsyncDisposable { private readonly CancellationTokenSource _localCts; private readonly CancellationTokenSource _remoteCts; private TwoGatewayFixture(NatsServer local, NatsServer remote, CancellationTokenSource localCts, CancellationTokenSource remoteCts) { Local = local; Remote = remote; _localCts = localCts; _remoteCts = remoteCts; } public NatsServer Local { get; } public NatsServer Remote { get; } public static async Task StartAsync() { var localOptions = new NatsOptions { Host = "127.0.0.1", Port = 0, Gateway = new GatewayOptions { Name = "LOCAL", Host = "127.0.0.1", Port = 0, }, }; var local = new NatsServer(localOptions, NullLoggerFactory.Instance); var localCts = new CancellationTokenSource(); _ = local.StartAsync(localCts.Token); await local.WaitForReadyAsync(); var remoteOptions = new NatsOptions { Host = "127.0.0.1", Port = 0, Gateway = new GatewayOptions { Name = "REMOTE", Host = "127.0.0.1", Port = 0, Remotes = [local.GatewayListen!], }, }; var remote = new NatsServer(remoteOptions, NullLoggerFactory.Instance); var remoteCts = new CancellationTokenSource(); _ = remote.StartAsync(remoteCts.Token); await remote.WaitForReadyAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested && (local.Stats.Gateways == 0 || remote.Stats.Gateways == 0)) await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); return new TwoGatewayFixture(local, remote, localCts, remoteCts); } public async ValueTask DisposeAsync() { await _localCts.CancelAsync(); await _remoteCts.CancelAsync(); Local.Dispose(); Remote.Dispose(); _localCts.Dispose(); _remoteCts.Dispose(); } } /// /// Leaf fixture duplicated here to avoid cross-namespace dependencies. /// Uses hub and spoke servers connected via leaf node protocol. /// internal sealed class LeafFixture : IAsyncDisposable { private readonly CancellationTokenSource _hubCts; private readonly CancellationTokenSource _spokeCts; private LeafFixture(NatsServer hub, NatsServer spoke, CancellationTokenSource hubCts, CancellationTokenSource spokeCts) { Hub = hub; Spoke = spoke; _hubCts = hubCts; _spokeCts = spokeCts; } public NatsServer Hub { get; } public NatsServer Spoke { get; } public static async Task StartAsync() { var hubOptions = new NatsOptions { Host = "127.0.0.1", Port = 0, LeafNode = new LeafNodeOptions { Host = "127.0.0.1", Port = 0, }, }; var hub = new NatsServer(hubOptions, NullLoggerFactory.Instance); var hubCts = new CancellationTokenSource(); _ = hub.StartAsync(hubCts.Token); await hub.WaitForReadyAsync(); var spokeOptions = new NatsOptions { Host = "127.0.0.1", Port = 0, LeafNode = new LeafNodeOptions { Host = "127.0.0.1", Port = 0, Remotes = [hub.LeafListen!], }, }; var spoke = new NatsServer(spokeOptions, NullLoggerFactory.Instance); var spokeCts = new CancellationTokenSource(); _ = spoke.StartAsync(spokeCts.Token); await spoke.WaitForReadyAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested && (hub.Stats.Leafs == 0 || spoke.Stats.Leafs == 0)) await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); return new LeafFixture(hub, spoke, hubCts, spokeCts); } public async Task WaitForRemoteInterestOnHubAsync(string subject) { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested) { if (Hub.HasRemoteInterest(subject)) return; await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); } throw new TimeoutException($"Timed out waiting for remote interest on hub for '{subject}'."); } public async Task WaitForRemoteInterestOnSpokeAsync(string subject) { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested) { if (Spoke.HasRemoteInterest(subject)) return; await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); } throw new TimeoutException($"Timed out waiting for remote interest on spoke for '{subject}'."); } public async ValueTask DisposeAsync() { await _spokeCts.CancelAsync(); await _hubCts.CancelAsync(); Spoke.Dispose(); Hub.Dispose(); _spokeCts.Dispose(); _hubCts.Dispose(); } }