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.
198 lines
6.4 KiB
C#
198 lines
6.4 KiB
C#
// Reference: golang/nats-server/server/route.go — per-account dedicated route registration (Gap 13.2).
|
|
// Tests for account-specific dedicated route connections in RouteManager.
|
|
|
|
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 per-account dedicated route connections (Gap 13.2).
|
|
/// Verifies that RouteManager correctly stores, retrieves, and prioritises
|
|
/// dedicated routes over pool-based routes for specific accounts.
|
|
/// Go reference: server/route.go — per-account dedicated route handling.
|
|
/// </summary>
|
|
public class AccountRouteTests : IDisposable
|
|
{
|
|
// -- Helpers --
|
|
|
|
// Track listeners and sockets for cleanup after each test.
|
|
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();
|
|
}
|
|
|
|
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 RouteConnection backed by a connected loopback socket pair so
|
|
/// that RouteConnection can construct its internal NetworkStream without
|
|
/// throwing. Both sockets and the listener 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);
|
|
}
|
|
|
|
// -- Tests --
|
|
|
|
// Go: server/route.go — per-account dedicated route registration.
|
|
[Fact]
|
|
public void RegisterAccountRoute_AddsRoute()
|
|
{
|
|
var manager = CreateManager();
|
|
var connection = MakeConnection();
|
|
|
|
manager.RegisterAccountRoute("ACCT-A", connection);
|
|
|
|
manager.GetDedicatedAccountRoute("ACCT-A").ShouldBeSameAs(connection);
|
|
}
|
|
|
|
// Go: server/route.go — overwrite existing dedicated route for same account.
|
|
[Fact]
|
|
public void RegisterAccountRoute_OverwritesPrevious()
|
|
{
|
|
var manager = CreateManager();
|
|
var first = MakeConnection();
|
|
var second = MakeConnection();
|
|
|
|
manager.RegisterAccountRoute("ACCT-A", first);
|
|
manager.RegisterAccountRoute("ACCT-A", second);
|
|
|
|
manager.GetDedicatedAccountRoute("ACCT-A").ShouldBeSameAs(second);
|
|
}
|
|
|
|
// Go: server/route.go — removing a dedicated route cleans up the entry.
|
|
[Fact]
|
|
public void UnregisterAccountRoute_RemovesRoute()
|
|
{
|
|
var manager = CreateManager();
|
|
var connection = MakeConnection();
|
|
|
|
manager.RegisterAccountRoute("ACCT-A", connection);
|
|
manager.UnregisterAccountRoute("ACCT-A");
|
|
|
|
manager.GetDedicatedAccountRoute("ACCT-A").ShouldBeNull();
|
|
}
|
|
|
|
// Go: server/route.go — unregistering a never-registered account is safe.
|
|
[Fact]
|
|
public void UnregisterAccountRoute_NonExistent_NoOp()
|
|
{
|
|
var manager = CreateManager();
|
|
|
|
// Must not throw.
|
|
var ex = Record.Exception(() => manager.UnregisterAccountRoute("NONEXISTENT"));
|
|
ex.ShouldBeNull();
|
|
}
|
|
|
|
// Go: server/route.go — lookup on unregistered account returns null.
|
|
[Fact]
|
|
public void GetDedicatedAccountRoute_NotRegistered_ReturnsNull()
|
|
{
|
|
var manager = CreateManager();
|
|
|
|
manager.GetDedicatedAccountRoute("UNKNOWN").ShouldBeNull();
|
|
}
|
|
|
|
// Go: server/route.go — HasDedicatedRoute returns true for registered account.
|
|
[Fact]
|
|
public void HasDedicatedRoute_RegisteredReturnsTrue()
|
|
{
|
|
var manager = CreateManager();
|
|
var connection = MakeConnection();
|
|
|
|
manager.RegisterAccountRoute("ACCT-B", connection);
|
|
|
|
manager.HasDedicatedRoute("ACCT-B").ShouldBeTrue();
|
|
}
|
|
|
|
// Go: server/route.go — HasDedicatedRoute returns false for unknown account.
|
|
[Fact]
|
|
public void HasDedicatedRoute_NotRegisteredReturnsFalse()
|
|
{
|
|
var manager = CreateManager();
|
|
|
|
manager.HasDedicatedRoute("ACCT-B").ShouldBeFalse();
|
|
}
|
|
|
|
// Go: server/route.go — listing accounts with dedicated routes.
|
|
[Fact]
|
|
public void GetAccountsWithDedicatedRoutes_ReturnsAllRegistered()
|
|
{
|
|
var manager = CreateManager();
|
|
|
|
manager.RegisterAccountRoute("ACCT-1", MakeConnection());
|
|
manager.RegisterAccountRoute("ACCT-2", MakeConnection());
|
|
manager.RegisterAccountRoute("ACCT-3", MakeConnection());
|
|
|
|
var accounts = manager.GetAccountsWithDedicatedRoutes();
|
|
|
|
accounts.Count.ShouldBe(3);
|
|
accounts.ShouldContain("ACCT-1");
|
|
accounts.ShouldContain("ACCT-2");
|
|
accounts.ShouldContain("ACCT-3");
|
|
}
|
|
|
|
// Go: server/route.go — DedicatedRouteCount tracks registered entries.
|
|
[Fact]
|
|
public void DedicatedRouteCount_MatchesRegistrations()
|
|
{
|
|
var manager = CreateManager();
|
|
|
|
manager.DedicatedRouteCount.ShouldBe(0);
|
|
|
|
manager.RegisterAccountRoute("ACCT-X", MakeConnection());
|
|
manager.DedicatedRouteCount.ShouldBe(1);
|
|
|
|
manager.RegisterAccountRoute("ACCT-Y", MakeConnection());
|
|
manager.DedicatedRouteCount.ShouldBe(2);
|
|
|
|
manager.UnregisterAccountRoute("ACCT-X");
|
|
manager.DedicatedRouteCount.ShouldBe(1);
|
|
}
|
|
|
|
// Go: server/route.go — dedicated route takes priority over pool-based route
|
|
// for the account it is registered against.
|
|
[Fact]
|
|
public void GetRouteForAccount_PrefersDedicatedRoute()
|
|
{
|
|
var manager = CreateManager();
|
|
|
|
// Register a dedicated route for "ACCT-PREF".
|
|
var dedicated = MakeConnection();
|
|
manager.RegisterAccountRoute("ACCT-PREF", dedicated);
|
|
|
|
// GetRouteForAccount must return the dedicated connection even though
|
|
// no pool routes exist (the dedicated path short-circuits pool lookup).
|
|
var result = manager.GetRouteForAccount("ACCT-PREF");
|
|
|
|
result.ShouldBeSameAs(dedicated);
|
|
}
|
|
}
|