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.
81 lines
2.9 KiB
C#
81 lines
2.9 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using NATS.Server.LeafNodes;
|
|
using NATS.Server.Subscriptions;
|
|
using NATS.Server.TestUtilities;
|
|
|
|
namespace NATS.Server.LeafNodes.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 PollHelper.YieldForAsync(100);
|
|
|
|
subList.MatchRemote("A", "orders.created").Count.ShouldBe(1);
|
|
remoteAdded.ShouldBe(1);
|
|
}
|
|
|
|
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)
|
|
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<bool> predicate, CancellationToken ct)
|
|
{
|
|
await PollHelper.WaitOrThrowAsync(predicate, "Timed out waiting for condition.");
|
|
}
|
|
}
|