feat: Wave 6 batch 1 — monitoring, config reload, client protocol, MQTT, leaf node tests
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
This commit is contained in:
537
tests/NATS.Server.Tests/LeafNodes/LeafNodeConnectionTests.cs
Normal file
537
tests/NATS.Server.Tests/LeafNodes/LeafNodeConnectionTests.cs
Normal file
@@ -0,0 +1,537 @@
|
||||
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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user