Files
natsdotnet/tests/NATS.Server.Clustering.Tests/Routes/NoPoolFallbackTests.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

218 lines
7.8 KiB
C#

// Reference: golang/nats-server/server/route.go getRoutesExcludePool — no-pool fallback for
// backward compatibility with pre-pool peers (Gap 13.6).
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server.Configuration;
using NATS.Server.Routes;
namespace NATS.Server.Clustering.Tests.Routes;
/// <summary>
/// Tests for no-pool (legacy) route fallback — Gap 13.6.
/// Verifies that <see cref="RouteConnection.SupportsPooling"/>,
/// <see cref="RouteConnection.IsLegacyRoute"/>, and the new
/// <see cref="RouteManager"/> legacy-route helpers behave correctly, and that
/// <see cref="RouteManager.GetRouteForAccount"/> falls back to a legacy route
/// when neither a dedicated nor a pool route exists.
/// Go reference: server/route.go getRoutesExcludePool.
/// </summary>
public class NoPoolFallbackTests : IDisposable
{
private readonly List<TcpListener> _listeners = [];
private readonly List<Socket> _sockets = [];
public void Dispose()
{
foreach (var s in _sockets) s.Dispose();
foreach (var l in _listeners) l.Stop();
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
private static RouteManager CreateManager() =>
new(
new ClusterOptions { Host = "127.0.0.1", Port = 0, Routes = [] },
new ServerStats(),
Guid.NewGuid().ToString("N"),
_ => { },
_ => { },
NullLogger<RouteManager>.Instance);
/// <summary>
/// Creates a <see cref="RouteConnection"/> backed by a connected loopback
/// socket pair so that the internal <see cref="System.Net.Sockets.NetworkStream"/>
/// can be constructed without throwing. Both sockets are tracked for disposal.
/// </summary>
private RouteConnection MakeConnection()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
_listeners.Add(listener);
var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_sockets.Add(client);
client.Connect((IPEndPoint)listener.LocalEndpoint);
var server = listener.AcceptSocket();
_sockets.Add(server);
return new RouteConnection(server);
}
// -----------------------------------------------------------------------
// RouteConnection.SupportsPooling
// -----------------------------------------------------------------------
// Go reference: server/route.go — pool-capable route: NegotiatedPoolSize > 0.
[Fact]
public void SupportsPooling_WhenNegotiated_ReturnsTrue()
{
var conn = MakeConnection();
conn.SetNegotiatedPoolSize(3);
conn.SupportsPooling.ShouldBeTrue();
}
// Go reference: server/route.go — default state is pre-negotiation (no pooling).
[Fact]
public void SupportsPooling_Default_ReturnsFalse()
{
var conn = MakeConnection();
// NegotiatedPoolSize defaults to 0 — pooling not yet established.
conn.SupportsPooling.ShouldBeFalse();
}
// -----------------------------------------------------------------------
// RouteConnection.IsLegacyRoute
// -----------------------------------------------------------------------
// Go reference: server/route.go getRoutesExcludePool — NegotiatedPoolSize == 0 means legacy.
[Fact]
public void IsLegacyRoute_Default_ReturnsTrue()
{
var conn = MakeConnection();
// A freshly created connection has NegotiatedPoolSize == 0, making it legacy.
conn.IsLegacyRoute.ShouldBeTrue();
}
// Go reference: server/route.go — pool-negotiated connection is not legacy.
[Fact]
public void IsLegacyRoute_WhenNegotiated_ReturnsFalse()
{
var conn = MakeConnection();
conn.SetNegotiatedPoolSize(2);
conn.IsLegacyRoute.ShouldBeFalse();
}
// -----------------------------------------------------------------------
// RouteManager.GetLegacyRoute
// -----------------------------------------------------------------------
// Go reference: server/route.go getRoutesExcludePool — returns first legacy connection.
[Fact]
public void GetLegacyRoute_ReturnsLegacyConnection()
{
var manager = CreateManager();
var legacy = MakeConnection();
// NegotiatedPoolSize == 0 by default — this is a legacy route.
manager.RegisterRoute("server-legacy", legacy);
var result = manager.GetLegacyRoute();
result.ShouldBeSameAs(legacy);
}
// Go reference: server/route.go getRoutesExcludePool — null when all routes support pooling.
[Fact]
public void GetLegacyRoute_NoLegacy_ReturnsNull()
{
var manager = CreateManager();
var pooled = MakeConnection();
pooled.SetNegotiatedPoolSize(3);
manager.RegisterRoute("server-pooled", pooled);
var result = manager.GetLegacyRoute();
result.ShouldBeNull();
}
// -----------------------------------------------------------------------
// RouteManager.GetLegacyRoutes
// -----------------------------------------------------------------------
// Go reference: server/route.go getRoutesExcludePool — returns all legacy connections.
[Fact]
public void GetLegacyRoutes_ReturnsAllLegacy()
{
var manager = CreateManager();
var legacy1 = MakeConnection(); // NegotiatedPoolSize == 0 → legacy
var legacy2 = MakeConnection(); // NegotiatedPoolSize == 0 → legacy
var pooled = MakeConnection();
pooled.SetNegotiatedPoolSize(3);
manager.RegisterRoute("server-legacy-1", legacy1);
manager.RegisterRoute("server-legacy-2", legacy2);
manager.RegisterRoute("server-pooled", pooled);
var result = manager.GetLegacyRoutes();
result.Count.ShouldBe(2);
result.ShouldContain(legacy1);
result.ShouldContain(legacy2);
result.ShouldNotContain(pooled);
}
// -----------------------------------------------------------------------
// RouteManager.HasLegacyRoutes
// -----------------------------------------------------------------------
// Go reference: server/route.go — any legacy route present returns true.
[Fact]
public void HasLegacyRoutes_WhenPresent_ReturnsTrue()
{
var manager = CreateManager();
var legacy = MakeConnection(); // IsLegacyRoute == true (default)
manager.RegisterRoute("server-legacy", legacy);
manager.HasLegacyRoutes.ShouldBeTrue();
}
// Go reference: server/route.go — no legacy routes returns false.
[Fact]
public void HasLegacyRoutes_WhenAbsent_ReturnsFalse()
{
var manager = CreateManager();
var pooled = MakeConnection();
pooled.SetNegotiatedPoolSize(5);
manager.RegisterRoute("server-pooled", pooled);
manager.HasLegacyRoutes.ShouldBeFalse();
}
// -----------------------------------------------------------------------
// RouteManager.GetRouteForAccount — legacy fallback (Gap 13.6)
// -----------------------------------------------------------------------
// Go reference: server/route.go — when no dedicated or pool route exists,
// fall back to the first legacy route for the account.
[Fact]
public void GetRouteForAccount_FallsBackToLegacy()
{
var manager = CreateManager();
// Register only a legacy route (NegotiatedPoolSize == 0).
var legacy = MakeConnection();
manager.RegisterRoute("server-legacy", legacy);
// No dedicated account route and no pool-capable route registered.
// GetRouteForAccount must return the legacy connection.
var result = manager.GetRouteForAccount("$G");
result.ShouldBeSameAs(legacy);
}
}