using System.Net; using System.Net.Sockets; using System.Text; using NATS.Server.LeafNodes; using NATS.Server.Subscriptions; namespace NATS.Server.Tests.LeafNodes; public class LeafInterestIdempotencyTests { [Fact] public async Task Duplicate_RSplus_or_reconnect_replay_does_not_double_count_remote_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 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; using var subList = new SubList(); var remoteAdded = 0; subList.InterestChanged += change => { if (change.Kind == InterestChangeKind.RemoteAdded) remoteAdded++; }; leaf.RemoteSubscriptionReceived = sub => { subList.ApplyRemoteSub(sub); return Task.CompletedTask; }; leaf.StartLoop(timeout.Token); await WriteLineAsync(remoteSocket, "LS+ A orders.*", timeout.Token); await WaitForAsync(() => subList.HasRemoteInterest("A", "orders.created"), timeout.Token); await WriteLineAsync(remoteSocket, "LS+ A orders.*", timeout.Token); await Task.Delay(100, timeout.Token); subList.MatchRemote("A", "orders.created").Count.ShouldBe(1); remoteAdded.ShouldBe(1); } 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(); private static async Task WaitForAsync(Func predicate, CancellationToken ct) { while (!ct.IsCancellationRequested) { if (predicate()) return; await Task.Delay(20, ct); } throw new TimeoutException("Timed out waiting for condition."); } }