// 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.Tests.Routes; /// /// 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. /// public class AccountRouteTests : IDisposable { // -- Helpers -- // Track listeners and sockets for cleanup after each test. private readonly List _listeners = []; private readonly List _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.Instance); /// /// 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. /// 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); } }