using System.Net; using System.Net.Sockets; using System.Text; using Microsoft.Extensions.Logging.Abstractions; using NATS.Server.Configuration; namespace NATS.Server.Tests; public class LeafProtocolTests { [Fact] public async Task Leaf_link_propagates_subscription_and_message_flow() { await using var fx = await LeafFixture.StartHubSpokeAsync(); await fx.SubscribeSpokeAsync("leaf.>"); await fx.PublishHubAsync("leaf.msg", "x"); (await fx.ReadSpokeMessageAsync()).ShouldContain("x"); } } internal sealed class LeafFixture : IAsyncDisposable { private readonly NatsServer _hub; private readonly NatsServer _spoke; private readonly CancellationTokenSource _hubCts; private readonly CancellationTokenSource _spokeCts; private Socket? _spokeSubscriber; private Socket? _hubPublisher; private LeafFixture(NatsServer hub, NatsServer spoke, CancellationTokenSource hubCts, CancellationTokenSource spokeCts) { _hub = hub; _spoke = spoke; _hubCts = hubCts; _spokeCts = spokeCts; } public static async Task StartHubSpokeAsync() { var hubOptions = new NatsOptions { Host = "127.0.0.1", Port = 0, 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); return new LeafFixture(hub, spoke, hubCts, spokeCts); } public async Task SubscribeSpokeAsync(string subject) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(IPAddress.Loopback, _spoke.Port); _spokeSubscriber = sock; _ = await ReadLineAsync(sock); // INFO await sock.SendAsync(Encoding.ASCII.GetBytes($"CONNECT {{}}\r\nSUB {subject} 1\r\nPING\r\n")); await ReadUntilAsync(sock, "PONG"); } public async Task PublishHubAsync(string subject, string payload) { var sock = _hubPublisher; if (sock == null) { sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(IPAddress.Loopback, _hub.Port); _hubPublisher = sock; _ = await ReadLineAsync(sock); // INFO await sock.SendAsync(Encoding.ASCII.GetBytes("CONNECT {}\r\nPING\r\n")); await ReadUntilAsync(sock, "PONG"); } await sock.SendAsync(Encoding.ASCII.GetBytes($"PUB {subject} {payload.Length}\r\n{payload}\r\nPING\r\n")); await ReadUntilAsync(sock, "PONG"); } public Task ReadSpokeMessageAsync() { if (_spokeSubscriber == null) throw new InvalidOperationException("Spoke subscriber was not initialized."); return ReadUntilAsync(_spokeSubscriber, "MSG "); } public async ValueTask DisposeAsync() { _spokeSubscriber?.Dispose(); _hubPublisher?.Dispose(); await _hubCts.CancelAsync(); await _spokeCts.CancelAsync(); _hub.Dispose(); _spoke.Dispose(); _hubCts.Dispose(); _spokeCts.Dispose(); } private static async Task ReadLineAsync(Socket sock) { var buf = new byte[4096]; var n = await sock.ReceiveAsync(buf, SocketFlags.None); return Encoding.ASCII.GetString(buf, 0, n); } private static async Task ReadUntilAsync(Socket sock, string expected) { var sb = new StringBuilder(); var buf = new byte[4096]; using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!sb.ToString().Contains(expected, StringComparison.Ordinal)) { var n = await sock.ReceiveAsync(buf, SocketFlags.None, cts.Token); if (n == 0) break; sb.Append(Encoding.ASCII.GetString(buf, 0, n)); } return sb.ToString(); } }