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

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);
}
}