using System.Net; using System.Net.Sockets; using System.Text; using Microsoft.Extensions.Logging.Abstractions; using NATS.Server.Configuration; using NATS.Server.LeafNodes; namespace NATS.Server.LeafNodes.Tests.LeafNodes; public class LeafNodeManagerParityBatch5Tests { [Fact] [SlopwatchSuppress("SW004", "Delay verifies a blocked subject is NOT forwarded; absence of a frame cannot be observed via synchronization primitives")] public async Task PropagateLocalSubscription_enforces_spoke_subscribe_permissions_and_keeps_queue_weight() { await using var ctx = await CreateManagerWithInboundConnectionAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var conn = ctx.Manager.GetConnectionByRemoteId("SPOKE1"); conn.ShouldNotBeNull(); conn!.IsSpoke = true; var sync = ctx.Manager.SendPermsAndAccountInfo( ctx.ConnectionId, "$G", pubAllow: null, subAllow: ["allowed.>"]); sync.Found.ShouldBeTrue(); sync.PermsSynced.ShouldBeTrue(); ctx.Manager.PropagateLocalSubscription("$G", "blocked.data", null); ctx.Manager.PropagateLocalSubscription("$G", "allowed.data", "workers", queueWeight: 4); // Only the allowed subject should appear on the wire; the blocked one is filtered synchronously. var line = await ReadLineAsync(ctx.RemoteSocket, timeout.Token); line.ShouldBe("LS+ $G allowed.data workers 4"); } [Fact] public async Task PropagateLocalSubscription_allows_loop_and_gateway_reply_prefixes_for_spoke() { await using var ctx = await CreateManagerWithInboundConnectionAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var conn = ctx.Manager.GetConnectionByRemoteId("SPOKE1"); conn.ShouldNotBeNull(); conn!.IsSpoke = true; ctx.Manager.SendPermsAndAccountInfo( ctx.ConnectionId, "$G", pubAllow: null, subAllow: ["allowed.>"]); ctx.Manager.PropagateLocalSubscription("$G", "$LDS.HUB.loop", null); (await ReadLineAsync(ctx.RemoteSocket, timeout.Token)).ShouldBe("LS+ $G $LDS.HUB.loop"); ctx.Manager.PropagateLocalSubscription("$G", "_GR_.A.reply", null); (await ReadLineAsync(ctx.RemoteSocket, timeout.Token)).ShouldBe("LS+ $G _GR_.A.reply"); } private sealed class ManagerContext : IAsyncDisposable { private readonly CancellationTokenSource _cts; public ManagerContext(LeafNodeManager manager, string connectionId, Socket remoteSocket, CancellationTokenSource cts) { Manager = manager; ConnectionId = connectionId; RemoteSocket = remoteSocket; _cts = cts; } public LeafNodeManager Manager { get; } public string ConnectionId { get; } public Socket RemoteSocket { get; } public async ValueTask DisposeAsync() { RemoteSocket.Close(); await Manager.DisposeAsync(); _cts.Dispose(); } } private static async Task CreateManagerWithInboundConnectionAsync() { var options = new LeafNodeOptions { Host = "127.0.0.1", Port = 0 }; var manager = new LeafNodeManager( options, new ServerStats(), "HUB", _ => { }, _ => { }, NullLogger.Instance); var cts = new CancellationTokenSource(); await manager.StartAsync(cts.Token); var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await remoteSocket.ConnectAsync(IPAddress.Loopback, options.Port); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var registered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); manager.OnConnectionRegistered = id => registered.TrySetResult(id); timeout.Token.Register(() => registered.TrySetCanceled(timeout.Token)); await WriteLineAsync(remoteSocket, "LEAF SPOKE1", timeout.Token); (await ReadLineAsync(remoteSocket, timeout.Token)).ShouldStartWith("LEAF "); var connectionId = await registered.Task; return new ManagerContext(manager, connectionId, remoteSocket, cts); } 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(); }