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.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 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 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) 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(); }