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;
using NATS.Server.TestUtilities;
namespace NATS.Server.Transport.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();
await PollHelper.WaitUntilAsync(() => fixture.Local.HasRemoteInterest("gw.interest.test"));
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();
await PollHelper.WaitUntilAsync(() => fixture.Local.HasRemoteInterest("gw.fwd.test"));
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();
await PollHelper.WaitUntilAsync(() => fixture.Local.HasRemoteInterest("gw.unsub.test"));
fixture.Local.HasRemoteInterest("gw.unsub.test").ShouldBeTrue();
await sub.DisposeAsync();
await conn.PingAsync();
await PollHelper.WaitUntilAsync(() => !(fixture.Local.HasRemoteInterest("gw.unsub.test")));
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();
await PollHelper.WaitUntilAsync(() => fixture.Local.HasRemoteInterest("gw.wild.test"));
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
{
await PollHelper.WaitUntilAsync(() => !(serverA.Stats.Routes < 3));
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
{
await PollHelper.WaitUntilAsync(() => !(serverA.Stats.Routes < 3));
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();
await PollHelper.WaitUntilAsync(() => serverA.HasRemoteInterest("route.sub.test"));
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
{
await PollHelper.WaitUntilAsync(() => !(serverA.Stats.Routes < 3));
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();
await PollHelper.WaitUntilAsync(() => serverA.HasRemoteInterest("route.fwd.test"));
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 PollHelper.YieldForAsync(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();
await PollHelper.WaitUntilAsync(() => !((local.Stats.Gateways == 0 || remote.Stats.Gateways == 0)));
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();
await PollHelper.WaitUntilAsync(() => !((hub.Stats.Leafs == 0 || spoke.Stats.Leafs == 0)));
return new LeafFixture(hub, spoke, hubCts, spokeCts);
}
public async Task WaitForRemoteInterestOnHubAsync(string subject)
{
await PollHelper.WaitOrThrowAsync(
() => Hub.HasRemoteInterest(subject),
$"Timed out waiting for remote interest on hub for '{subject}'.");
}
public async Task WaitForRemoteInterestOnSpokeAsync(string subject)
{
await PollHelper.WaitOrThrowAsync(
() => Spoke.HasRemoteInterest(subject),
$"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();
}
}