Move 28 leaf node test files from NATS.Server.Tests into a dedicated NATS.Server.LeafNodes.Tests project. Update namespaces, add InternalsVisibleTo, register in solution file. Replace all Task.Delay polling loops with PollHelper.WaitUntilAsync/YieldForAsync from TestUtilities. Replace private ReadUntilAsync in LeafProtocolTests with SocketTestHelper.ReadUntilAsync. All 281 tests pass.
182 lines
7.1 KiB
C#
182 lines
7.1 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using NATS.Server.Configuration;
|
|
using NATS.Server.LeafNodes;
|
|
|
|
namespace NATS.Server.LeafNodes.Tests.LeafNodes;
|
|
|
|
public class LeafConnectionParityBatch3Tests
|
|
{
|
|
[Fact]
|
|
public async Task SendLeafConnect_writes_connect_json_payload_with_expected_fields()
|
|
{
|
|
using var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
listener.Start();
|
|
var endpoint = (IPEndPoint)listener.LocalEndpoint;
|
|
|
|
using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await remoteSocket.ConnectAsync(IPAddress.Loopback, endpoint.Port);
|
|
using var leafSocket = await listener.AcceptSocketAsync();
|
|
await using var leaf = new LeafConnection(leafSocket);
|
|
|
|
var info = new LeafConnectInfo
|
|
{
|
|
Jwt = "jwt-token",
|
|
Nkey = "nkey",
|
|
Sig = "sig",
|
|
Hub = true,
|
|
Cluster = "C1",
|
|
Headers = true,
|
|
JetStream = true,
|
|
Compression = "s2_auto",
|
|
RemoteAccount = "A",
|
|
Proto = 1,
|
|
};
|
|
|
|
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
await leaf.SendLeafConnectAsync(info, timeout.Token);
|
|
|
|
var line = await ReadLineAsync(remoteSocket, timeout.Token);
|
|
line.ShouldStartWith("CONNECT ");
|
|
|
|
var payload = line["CONNECT ".Length..];
|
|
using var json = JsonDocument.Parse(payload);
|
|
var root = json.RootElement;
|
|
|
|
root.GetProperty("jwt").GetString().ShouldBe("jwt-token");
|
|
root.GetProperty("nkey").GetString().ShouldBe("nkey");
|
|
root.GetProperty("sig").GetString().ShouldBe("sig");
|
|
root.GetProperty("hub").GetBoolean().ShouldBeTrue();
|
|
root.GetProperty("cluster").GetString().ShouldBe("C1");
|
|
root.GetProperty("headers").GetBoolean().ShouldBeTrue();
|
|
root.GetProperty("jetstream").GetBoolean().ShouldBeTrue();
|
|
root.GetProperty("compression").GetString().ShouldBe("s2_auto");
|
|
root.GetProperty("remote_account").GetString().ShouldBe("A");
|
|
root.GetProperty("proto").GetInt32().ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RemoteCluster_returns_cluster_from_leaf_handshake_attributes()
|
|
{
|
|
using var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
listener.Start();
|
|
var endpoint = (IPEndPoint)listener.LocalEndpoint;
|
|
|
|
using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await remoteSocket.ConnectAsync(IPAddress.Loopback, endpoint.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 cluster=HUB-A domain=JS-A", timeout.Token);
|
|
|
|
await handshakeTask;
|
|
|
|
leaf.RemoteId.ShouldBe("REMOTE");
|
|
leaf.RemoteCluster().ShouldBe("HUB-A");
|
|
leaf.RemoteJetStreamDomain.ShouldBe("JS-A");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetLeafConnectDelayIfSoliciting_sets_delay_only_for_solicited_connections()
|
|
{
|
|
await using var solicited = await CreateConnectionAsync();
|
|
solicited.IsSolicited = true;
|
|
solicited.SetLeafConnectDelayIfSoliciting(TimeSpan.FromSeconds(30));
|
|
solicited.GetConnectDelay().ShouldBe(TimeSpan.FromSeconds(30));
|
|
|
|
await using var inbound = await CreateConnectionAsync();
|
|
inbound.IsSolicited = false;
|
|
inbound.SetLeafConnectDelayIfSoliciting(TimeSpan.FromSeconds(30));
|
|
inbound.GetConnectDelay().ShouldBe(TimeSpan.Zero);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LeafProcessErr_maps_known_errors_to_reconnect_delays_on_solicited_connections()
|
|
{
|
|
await using var leaf = await CreateConnectionAsync();
|
|
leaf.IsSolicited = true;
|
|
|
|
leaf.LeafProcessErr("Permissions Violation for Subscription to foo");
|
|
leaf.GetConnectDelay().ShouldBe(LeafNodeManager.LeafNodeReconnectAfterPermViolation);
|
|
|
|
leaf.LeafProcessErr("Loop detected");
|
|
leaf.GetConnectDelay().ShouldBe(LeafNodeManager.LeafNodeReconnectDelayAfterLoopDetected);
|
|
|
|
leaf.LeafProcessErr("Cluster name is same");
|
|
leaf.GetConnectDelay().ShouldBe(LeafNodeManager.LeafNodeReconnectDelayAfterClusterNameSame);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LeafSubPermViolation_and_LeafPermViolation_set_permission_delay()
|
|
{
|
|
await using var leaf = await CreateConnectionAsync();
|
|
leaf.IsSolicited = true;
|
|
|
|
leaf.LeafSubPermViolation("subj.A");
|
|
leaf.GetConnectDelay().ShouldBe(LeafNodeManager.LeafNodeReconnectAfterPermViolation);
|
|
|
|
leaf.SetLeafConnectDelayIfSoliciting(TimeSpan.Zero);
|
|
leaf.LeafPermViolation(pub: true, subj: "subj.B");
|
|
leaf.GetConnectDelay().ShouldBe(LeafNodeManager.LeafNodeReconnectAfterPermViolation);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CancelMigrateTimer_stops_pending_timer_callback()
|
|
{
|
|
var remote = new RemoteLeafOptions();
|
|
using var signal = new SemaphoreSlim(0, 1);
|
|
|
|
remote.StartMigrateTimer(_ => signal.Release(), TimeSpan.FromMilliseconds(120));
|
|
remote.CancelMigrateTimer();
|
|
|
|
// The timer was disposed before its 120 ms deadline. We wait 300 ms via WaitAsync;
|
|
// if the callback somehow fires it will release the semaphore and WaitAsync returns
|
|
// true, which the assertion catches. No Task.Delay required.
|
|
var fired = await signal.WaitAsync(TimeSpan.FromMilliseconds(300));
|
|
fired.ShouldBeFalse();
|
|
}
|
|
|
|
private static async Task<LeafConnection> CreateConnectionAsync()
|
|
{
|
|
var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
listener.Start();
|
|
var endpoint = (IPEndPoint)listener.LocalEndpoint;
|
|
|
|
var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await client.ConnectAsync(IPAddress.Loopback, endpoint.Port);
|
|
|
|
var server = await listener.AcceptSocketAsync();
|
|
listener.Stop();
|
|
client.Dispose();
|
|
|
|
return new LeafConnection(server);
|
|
}
|
|
|
|
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)
|
|
throw new IOException("Connection closed while reading line");
|
|
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();
|
|
}
|