// Reference: golang/nats-server/server/route.go:533-545 — computeRoutePoolIdx // Tests for account-based route pool index computation and message routing. using System.Text; using Microsoft.Extensions.Logging.Abstractions; using NATS.Server.Configuration; using NATS.Server.Routes; namespace NATS.Server.Clustering.Tests.Routes; /// /// Tests for route pool accounting per account, matching Go's /// computeRoutePoolIdx behavior (route.go:533-545). /// public class RoutePoolAccountTests { [Fact] public void ComputeRoutePoolIdx_SinglePool_AlwaysReturnsZero() { RouteManager.ComputeRoutePoolIdx(1, "account-A").ShouldBe(0); RouteManager.ComputeRoutePoolIdx(1, "account-B").ShouldBe(0); RouteManager.ComputeRoutePoolIdx(1, "$G").ShouldBe(0); RouteManager.ComputeRoutePoolIdx(0, "anything").ShouldBe(0); } [Fact] public void ComputeRoutePoolIdx_DeterministicForSameAccount() { const int poolSize = 5; const string account = "my-test-account"; var first = RouteManager.ComputeRoutePoolIdx(poolSize, account); var second = RouteManager.ComputeRoutePoolIdx(poolSize, account); var third = RouteManager.ComputeRoutePoolIdx(poolSize, account); first.ShouldBe(second); second.ShouldBe(third); first.ShouldBeGreaterThanOrEqualTo(0); first.ShouldBeLessThan(poolSize); } [Fact] public void ComputeRoutePoolIdx_DistributesAcrossPool() { const int poolSize = 3; var usedIndices = new HashSet(); for (var i = 0; i < 100; i++) { var idx = RouteManager.ComputeRoutePoolIdx(poolSize, $"account-{i}"); idx.ShouldBeGreaterThanOrEqualTo(0); idx.ShouldBeLessThan(poolSize); usedIndices.Add(idx); } usedIndices.Count.ShouldBe(poolSize); } [Fact] public void ComputeRoutePoolIdx_EmptyAccount_ReturnsValid() { const int poolSize = 4; var idx = RouteManager.ComputeRoutePoolIdx(poolSize, string.Empty); idx.ShouldBeGreaterThanOrEqualTo(0); idx.ShouldBeLessThan(poolSize); } [Fact] public void ComputeRoutePoolIdx_DefaultGlobalAccount_ReturnsValid() { const int poolSize = 3; var idx = RouteManager.ComputeRoutePoolIdx(poolSize, "$G"); idx.ShouldBeGreaterThanOrEqualTo(0); idx.ShouldBeLessThan(poolSize); var idx2 = RouteManager.ComputeRoutePoolIdx(poolSize, "$G"); idx.ShouldBe(idx2); } [Fact] public async Task ForwardRoutedMessage_UsesCorrectPoolConnection() { var clusterName = Guid.NewGuid().ToString("N"); var optsA = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = clusterName, Host = "127.0.0.1", Port = 0, PoolSize = 1, Routes = [], }, }; var serverA = new NatsServer(optsA, NullLoggerFactory.Instance); var ctsA = new CancellationTokenSource(); _ = serverA.StartAsync(ctsA.Token); await serverA.WaitForReadyAsync(); var optsB = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = clusterName, Host = "127.0.0.1", Port = 0, PoolSize = 1, Routes = [$"127.0.0.1:{optsA.Cluster.Port}"], }, }; var serverB = new NatsServer(optsB, NullLoggerFactory.Instance); var ctsB = new CancellationTokenSource(); _ = serverB.StartAsync(ctsB.Token); await serverB.WaitForReadyAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested && (Interlocked.Read(ref serverA.Stats.Routes) == 0 || Interlocked.Read(ref serverB.Stats.Routes) == 0)) { await Task.Delay(50, timeout.Token); } Interlocked.Read(ref serverA.Stats.Routes).ShouldBeGreaterThan(0); var payload = Encoding.UTF8.GetBytes("hello"); await serverA.RouteManager!.ForwardRoutedMessageAsync( "$G", "test.subject", null, payload, CancellationToken.None); var poolIdx = RouteManager.ComputeRoutePoolIdx(1, "$G"); poolIdx.ShouldBe(0); await ctsA.CancelAsync(); await ctsB.CancelAsync(); serverA.Dispose(); serverB.Dispose(); ctsA.Dispose(); ctsB.Dispose(); } }