Files
natsdotnet/tests/NATS.Server.Clustering.Tests/Routes/RouteInterestIdempotencyTests.cs
Joseph Doherty 615752cdc2 refactor: extract NATS.Server.Clustering.Tests project
Move 29 clustering/routing test files from NATS.Server.Tests to a
dedicated NATS.Server.Clustering.Tests project. Update namespaces,
replace private GetFreePort/ReadUntilAsync helpers with TestUtilities
calls, and extract TestServerFactory/ClusterTestServer to TestUtilities
to fix cross-project reference from JetStreamStartupTests.
2026-03-12 15:31:58 -04:00

88 lines
3.0 KiB
C#

using System.Net;
using System.Net.Sockets;
using System.Text;
using NATS.Server.Routes;
using NATS.Server.Subscriptions;
namespace NATS.Server.Clustering.Tests.Routes;
public class RouteInterestIdempotencyTests
{
[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 routeSocket = await listener.AcceptSocketAsync();
await using var route = new RouteConnection(routeSocket);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var handshakeTask = route.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("ROUTE LOCAL");
await WriteLineAsync(remoteSocket, "ROUTE REMOTE", timeout.Token);
await handshakeTask;
using var subList = new SubList();
var remoteAdded = 0;
subList.InterestChanged += change =>
{
if (change.Kind == InterestChangeKind.RemoteAdded)
remoteAdded++;
};
route.RemoteSubscriptionReceived = sub =>
{
subList.ApplyRemoteSub(sub);
return Task.CompletedTask;
};
route.StartFrameLoop(timeout.Token);
await WriteLineAsync(remoteSocket, "RS+ A orders.*", timeout.Token);
await WaitForAsync(() => subList.HasRemoteInterest("A", "orders.created"), timeout.Token);
await WriteLineAsync(remoteSocket, "RS+ 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<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)
{
while (!ct.IsCancellationRequested)
{
if (predicate())
return;
await Task.Delay(20, ct);
}
throw new TimeoutException("Timed out waiting for condition.");
}
}