refactor: extract NATS.Server.LeafNodes.Tests project
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.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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<ManagerContext> CreateManagerWithInboundConnectionAsync()
|
||||
{
|
||||
var options = new LeafNodeOptions { Host = "127.0.0.1", Port = 0 };
|
||||
var manager = new LeafNodeManager(
|
||||
options,
|
||||
new ServerStats(),
|
||||
"HUB",
|
||||
_ => { },
|
||||
_ => { },
|
||||
NullLogger<LeafNodeManager>.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<string>(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<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();
|
||||
}
|
||||
Reference in New Issue
Block a user