Port 405 new test methods across 5 subsystems for Go parity: - Monitoring: 102 tests (varz, connz, routez, subsz, stacksz) - Leaf Nodes: 85 tests (connection, forwarding, loop detection, subject filter, JetStream) - MQTT Bridge: 86 tests (advanced, auth, retained messages, topic mapping, will messages) - Client Protocol: 73 tests (connection handling, protocol violations, limits) - Config Reload: 59 tests (hot reload, option changes, permission updates) Total: 1,678 tests passing, 0 failures, 3 skipped
538 lines
23 KiB
C#
538 lines
23 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NATS.Client.Core;
|
|
using NATS.Server.Auth;
|
|
using NATS.Server.Configuration;
|
|
using NATS.Server.LeafNodes;
|
|
using NATS.Server.Subscriptions;
|
|
|
|
namespace NATS.Server.Tests.LeafNodes;
|
|
|
|
/// <summary>
|
|
/// Tests for leaf node connection establishment, authentication, and lifecycle.
|
|
/// Reference: golang/nats-server/server/leafnode_test.go
|
|
/// </summary>
|
|
public class LeafNodeConnectionTests
|
|
{
|
|
// Go: TestLeafNodeBasicAuthSingleton server/leafnode_test.go:602
|
|
[Fact]
|
|
public async Task Leaf_node_connects_with_basic_hub_spoke_setup()
|
|
{
|
|
await using var fixture = await LeafFixture.StartAsync();
|
|
fixture.Hub.Stats.Leafs.ShouldBeGreaterThan(0);
|
|
fixture.Spoke.Stats.Leafs.ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
// Go: TestLeafNodesBasicTokenAuth server/leafnode_test.go:10862
|
|
[Fact]
|
|
public async Task Leaf_node_connects_with_token_auth_on_hub()
|
|
{
|
|
var hubOptions = new NatsOptions
|
|
{
|
|
Host = "127.0.0.1",
|
|
Port = 0,
|
|
Authorization = "secret-token",
|
|
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);
|
|
|
|
hub.Stats.Leafs.ShouldBeGreaterThan(0);
|
|
spoke.Stats.Leafs.ShouldBeGreaterThan(0);
|
|
|
|
await spokeCts.CancelAsync();
|
|
await hubCts.CancelAsync();
|
|
spoke.Dispose();
|
|
hub.Dispose();
|
|
spokeCts.Dispose();
|
|
hubCts.Dispose();
|
|
}
|
|
|
|
// Go: TestLeafNodeBasicAuthSingleton server/leafnode_test.go:602
|
|
[Fact]
|
|
public async Task Leaf_node_connects_with_user_password_auth()
|
|
{
|
|
var users = new User[] { new() { Username = "leafuser", Password = "leafpass" } };
|
|
|
|
var hubOptions = new NatsOptions
|
|
{
|
|
Host = "127.0.0.1", Port = 0, Users = users,
|
|
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);
|
|
|
|
hub.Stats.Leafs.ShouldBeGreaterThan(0);
|
|
|
|
await spokeCts.CancelAsync();
|
|
await hubCts.CancelAsync();
|
|
spoke.Dispose();
|
|
hub.Dispose();
|
|
spokeCts.Dispose();
|
|
hubCts.Dispose();
|
|
}
|
|
|
|
// Go: TestLeafNodeRTT server/leafnode_test.go:488
|
|
[Fact]
|
|
public async Task Hub_and_spoke_both_report_leaf_connection_count()
|
|
{
|
|
await using var fixture = await LeafFixture.StartAsync();
|
|
Interlocked.Read(ref fixture.Hub.Stats.Leafs).ShouldBe(1);
|
|
Interlocked.Read(ref fixture.Spoke.Stats.Leafs).ShouldBe(1);
|
|
}
|
|
|
|
// Go: TestLeafNodeTwoRemotesToSameHubAccount server/leafnode_test.go:8758
|
|
[Fact]
|
|
public async Task Two_spoke_servers_can_connect_to_same_hub()
|
|
{
|
|
await using var fixture = await TwoSpokeFixture.StartAsync();
|
|
Interlocked.Read(ref fixture.Hub.Stats.Leafs).ShouldBeGreaterThanOrEqualTo(2);
|
|
Interlocked.Read(ref fixture.Spoke1.Stats.Leafs).ShouldBeGreaterThan(0);
|
|
Interlocked.Read(ref fixture.Spoke2.Stats.Leafs).ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
// Go: TestLeafNodeRemoteWrongPort server/leafnode_test.go:1095
|
|
[Fact]
|
|
public async Task Outbound_handshake_completes_between_raw_sockets()
|
|
{
|
|
using var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
listener.Start();
|
|
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
|
using var clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await clientSocket.ConnectAsync(IPAddress.Loopback, port);
|
|
using var acceptedSocket = await listener.AcceptSocketAsync();
|
|
|
|
await using var leaf = new LeafConnection(acceptedSocket);
|
|
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
|
|
(await ReadLineAsync(clientSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
|
|
await WriteLineAsync(clientSocket, "LEAF REMOTE", timeout.Token);
|
|
await handshakeTask;
|
|
|
|
leaf.RemoteId.ShouldBe("REMOTE");
|
|
}
|
|
|
|
// Go: TestLeafNodeCloseTLSConnection server/leafnode_test.go:968
|
|
[Fact]
|
|
public async Task Inbound_handshake_completes_between_raw_sockets()
|
|
{
|
|
using var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
listener.Start();
|
|
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
|
using var clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await clientSocket.ConnectAsync(IPAddress.Loopback, port);
|
|
using var acceptedSocket = await listener.AcceptSocketAsync();
|
|
|
|
await using var leaf = new LeafConnection(acceptedSocket);
|
|
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var handshakeTask = leaf.PerformInboundHandshakeAsync("SERVER", timeout.Token);
|
|
await WriteLineAsync(clientSocket, "LEAF REMOTE_CLIENT", timeout.Token);
|
|
(await ReadLineAsync(clientSocket, timeout.Token)).ShouldBe("LEAF SERVER");
|
|
await handshakeTask;
|
|
|
|
leaf.RemoteId.ShouldBe("REMOTE_CLIENT");
|
|
}
|
|
|
|
// Go: TestLeafNodeNoPingBeforeConnect server/leafnode_test.go:3713
|
|
[Fact]
|
|
public async Task Leaf_connection_disposes_cleanly_without_starting_loop()
|
|
{
|
|
using var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
listener.Start();
|
|
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
|
using var clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await clientSocket.ConnectAsync(IPAddress.Loopback, port);
|
|
using var acceptedSocket = await listener.AcceptSocketAsync();
|
|
|
|
var leaf = new LeafConnection(acceptedSocket);
|
|
await leaf.DisposeAsync();
|
|
|
|
var buffer = new byte[1];
|
|
var read = await clientSocket.ReceiveAsync(buffer, SocketFlags.None);
|
|
read.ShouldBe(0);
|
|
}
|
|
|
|
// Go: TestLeafNodeBannerNoClusterNameIfNoCluster server/leafnode_test.go:9803
|
|
[Fact]
|
|
public async Task Leaf_connection_sends_LS_plus_and_LS_minus()
|
|
{
|
|
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 timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
|
|
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
|
|
await WriteLineAsync(remoteSocket, "LEAF REMOTE", timeout.Token);
|
|
await handshakeTask;
|
|
|
|
await leaf.SendLsPlusAsync("$G", "foo.bar", null, timeout.Token);
|
|
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LS+ $G foo.bar");
|
|
|
|
await leaf.SendLsPlusAsync("$G", "foo.baz", "queue1", timeout.Token);
|
|
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LS+ $G foo.baz queue1");
|
|
|
|
await leaf.SendLsMinusAsync("$G", "foo.bar", null, timeout.Token);
|
|
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LS- $G foo.bar");
|
|
}
|
|
|
|
// Go: TestLeafNodeLMsgSplit server/leafnode_test.go:2387
|
|
[Fact]
|
|
public async Task Leaf_connection_sends_LMSG()
|
|
{
|
|
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 timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
|
|
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
|
|
await WriteLineAsync(remoteSocket, "LEAF REMOTE", timeout.Token);
|
|
await handshakeTask;
|
|
|
|
var payload = "hello world"u8.ToArray();
|
|
await leaf.SendMessageAsync("$G", "test.subject", "reply-to", payload, timeout.Token);
|
|
|
|
var controlLine = await ReadLineAsync(remoteSocket, timeout.Token);
|
|
controlLine.ShouldBe($"LMSG $G test.subject reply-to {payload.Length}");
|
|
}
|
|
|
|
// Go: TestLeafNodeLMsgSplit server/leafnode_test.go:2387
|
|
[Fact]
|
|
public async Task Leaf_connection_sends_LMSG_with_no_reply()
|
|
{
|
|
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 timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
|
|
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
|
|
await WriteLineAsync(remoteSocket, "LEAF REMOTE", timeout.Token);
|
|
await handshakeTask;
|
|
|
|
var payload = "test"u8.ToArray();
|
|
await leaf.SendMessageAsync("ACCT", "subject", null, payload, timeout.Token);
|
|
|
|
var controlLine = await ReadLineAsync(remoteSocket, timeout.Token);
|
|
controlLine.ShouldBe($"LMSG ACCT subject - {payload.Length}");
|
|
}
|
|
|
|
// Go: TestLeafNodeLMsgSplit server/leafnode_test.go:2387
|
|
[Fact]
|
|
public async Task Leaf_connection_sends_LMSG_with_empty_payload()
|
|
{
|
|
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 timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
|
|
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
|
|
await WriteLineAsync(remoteSocket, "LEAF REMOTE", timeout.Token);
|
|
await handshakeTask;
|
|
|
|
await leaf.SendMessageAsync("$G", "empty.msg", null, ReadOnlyMemory<byte>.Empty, timeout.Token);
|
|
var controlLine = await ReadLineAsync(remoteSocket, timeout.Token);
|
|
controlLine.ShouldBe("LMSG $G empty.msg - 0");
|
|
}
|
|
|
|
// Go: TestLeafNodeTmpClients server/leafnode_test.go:1663
|
|
[Fact]
|
|
public async Task Leaf_connection_receives_LS_plus_and_triggers_callback()
|
|
{
|
|
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 timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
|
|
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
|
|
await WriteLineAsync(remoteSocket, "LEAF REMOTE", timeout.Token);
|
|
await handshakeTask;
|
|
|
|
var received = new List<RemoteSubscription>();
|
|
leaf.RemoteSubscriptionReceived = sub => { received.Add(sub); return Task.CompletedTask; };
|
|
leaf.StartLoop(timeout.Token);
|
|
|
|
await WriteLineAsync(remoteSocket, "LS+ $G orders.>", timeout.Token);
|
|
await WaitForAsync(() => received.Count >= 1, timeout.Token);
|
|
|
|
received[0].Subject.ShouldBe("orders.>");
|
|
received[0].Account.ShouldBe("$G");
|
|
received[0].IsRemoval.ShouldBeFalse();
|
|
}
|
|
|
|
// Go: TestLeafNodeRouteParseLSUnsub server/leafnode_test.go:2486
|
|
[Fact]
|
|
public async Task Leaf_connection_receives_LS_minus_and_triggers_removal()
|
|
{
|
|
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 timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
|
|
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
|
|
await WriteLineAsync(remoteSocket, "LEAF REMOTE", timeout.Token);
|
|
await handshakeTask;
|
|
|
|
var received = new List<RemoteSubscription>();
|
|
leaf.RemoteSubscriptionReceived = sub => { received.Add(sub); return Task.CompletedTask; };
|
|
leaf.StartLoop(timeout.Token);
|
|
|
|
await WriteLineAsync(remoteSocket, "LS+ $G foo.bar", timeout.Token);
|
|
await WaitForAsync(() => received.Count >= 1, timeout.Token);
|
|
|
|
await WriteLineAsync(remoteSocket, "LS- $G foo.bar", timeout.Token);
|
|
await WaitForAsync(() => received.Count >= 2, timeout.Token);
|
|
|
|
received[1].Subject.ShouldBe("foo.bar");
|
|
received[1].IsRemoval.ShouldBeTrue();
|
|
}
|
|
|
|
// Go: TestLeafNodeLMsgSplit server/leafnode_test.go:2387
|
|
[Fact]
|
|
public async Task Leaf_connection_receives_LMSG_and_triggers_message_callback()
|
|
{
|
|
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 timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
|
|
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
|
|
await WriteLineAsync(remoteSocket, "LEAF REMOTE", timeout.Token);
|
|
await handshakeTask;
|
|
|
|
var messages = new List<LeafMessage>();
|
|
leaf.MessageReceived = msg => { messages.Add(msg); return Task.CompletedTask; };
|
|
leaf.StartLoop(timeout.Token);
|
|
|
|
var payload = "hello from remote"u8.ToArray();
|
|
await WriteLineAsync(remoteSocket, $"LMSG $G test.subject reply-to {payload.Length}", timeout.Token);
|
|
await remoteSocket.SendAsync(payload, SocketFlags.None, timeout.Token);
|
|
await remoteSocket.SendAsync("\r\n"u8.ToArray(), SocketFlags.None, timeout.Token);
|
|
|
|
await WaitForAsync(() => messages.Count >= 1, timeout.Token);
|
|
|
|
messages[0].Subject.ShouldBe("test.subject");
|
|
messages[0].ReplyTo.ShouldBe("reply-to");
|
|
messages[0].Account.ShouldBe("$G");
|
|
Encoding.ASCII.GetString(messages[0].Payload.Span).ShouldBe("hello from remote");
|
|
}
|
|
|
|
// Go: TestLeafNodeLMsgSplit server/leafnode_test.go:2387
|
|
[Fact]
|
|
public async Task Leaf_connection_receives_LMSG_with_account_scoped_format()
|
|
{
|
|
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 timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
|
|
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
|
|
await WriteLineAsync(remoteSocket, "LEAF REMOTE", timeout.Token);
|
|
await handshakeTask;
|
|
|
|
var messages = new List<LeafMessage>();
|
|
leaf.MessageReceived = msg => { messages.Add(msg); return Task.CompletedTask; };
|
|
leaf.StartLoop(timeout.Token);
|
|
|
|
var payload = "acct"u8.ToArray();
|
|
await WriteLineAsync(remoteSocket, $"LMSG MYACCT test.subject - {payload.Length}", timeout.Token);
|
|
await remoteSocket.SendAsync(payload, SocketFlags.None, timeout.Token);
|
|
await remoteSocket.SendAsync("\r\n"u8.ToArray(), SocketFlags.None, timeout.Token);
|
|
|
|
await WaitForAsync(() => messages.Count >= 1, timeout.Token);
|
|
|
|
messages[0].Account.ShouldBe("MYACCT");
|
|
messages[0].Subject.ShouldBe("test.subject");
|
|
messages[0].ReplyTo.ShouldBeNull();
|
|
}
|
|
|
|
// Go: TestLeafNodeTwoRemotesToSameHubAccount server/leafnode_test.go:2210
|
|
[Fact]
|
|
public async Task Leaf_connection_receives_LS_plus_with_queue()
|
|
{
|
|
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 timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
|
|
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
|
|
await WriteLineAsync(remoteSocket, "LEAF REMOTE", timeout.Token);
|
|
await handshakeTask;
|
|
|
|
var received = new List<RemoteSubscription>();
|
|
leaf.RemoteSubscriptionReceived = sub => { received.Add(sub); return Task.CompletedTask; };
|
|
leaf.StartLoop(timeout.Token);
|
|
|
|
await WriteLineAsync(remoteSocket, "LS+ $G work.> workers", timeout.Token);
|
|
await WaitForAsync(() => received.Count >= 1, timeout.Token);
|
|
|
|
received[0].Subject.ShouldBe("work.>");
|
|
received[0].Queue.ShouldBe("workers");
|
|
received[0].Account.ShouldBe("$G");
|
|
}
|
|
|
|
// Go: TestLeafNodeSlowConsumer server/leafnode_test.go:9103
|
|
[Fact]
|
|
public async Task Leaf_connection_handles_multiple_rapid_LMSG_messages()
|
|
{
|
|
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 timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
|
|
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
|
|
await WriteLineAsync(remoteSocket, "LEAF REMOTE", timeout.Token);
|
|
await handshakeTask;
|
|
|
|
var messageCount = 0;
|
|
leaf.MessageReceived = _ => { Interlocked.Increment(ref messageCount); return Task.CompletedTask; };
|
|
leaf.StartLoop(timeout.Token);
|
|
|
|
const int numMessages = 20;
|
|
for (var i = 0; i < numMessages; i++)
|
|
{
|
|
var payload = Encoding.ASCII.GetBytes($"msg-{i}");
|
|
var line = $"LMSG $G test.multi - {payload.Length}\r\n";
|
|
await remoteSocket.SendAsync(Encoding.ASCII.GetBytes(line), SocketFlags.None, timeout.Token);
|
|
await remoteSocket.SendAsync(payload, SocketFlags.None, timeout.Token);
|
|
await remoteSocket.SendAsync("\r\n"u8.ToArray(), SocketFlags.None, timeout.Token);
|
|
}
|
|
|
|
await WaitForAsync(() => Volatile.Read(ref messageCount) >= numMessages, timeout.Token);
|
|
Volatile.Read(ref messageCount).ShouldBe(numMessages);
|
|
}
|
|
|
|
private static async Task<string> ReadLineAsync(Socket socket, CancellationToken ct)
|
|
{
|
|
var bytes = new List<byte>(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();
|
|
|
|
private static async Task WaitForAsync(Func<bool> predicate, CancellationToken ct)
|
|
{
|
|
while (!ct.IsCancellationRequested)
|
|
{
|
|
if (predicate()) return;
|
|
await Task.Delay(20, ct);
|
|
}
|
|
|
|
throw new TimeoutException("Timed out waiting for condition.");
|
|
}
|
|
}
|