// Reference: golang/nats-server/server/route.go — route hash map for O(1) server-ID lookup. // Tests for Gap 13.4: hash-based route storage added to RouteManager. using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging.Abstractions; using NATS.Server; using NATS.Server.Configuration; using NATS.Server.Routes; namespace NATS.Server.Tests.Routes; /// /// Tests for the FNV-1a hash-based route storage on . /// Covers ComputeRouteHash, RegisterRouteByHash, /// UnregisterRouteByHash, GetRouteByHash, /// GetRouteByServerId, and HashedRouteCount. /// Go reference: server/route.go — server-ID-keyed route hash map. /// public class RouteHashStorageTests { // Helper: build a RouteManager instance that is NOT started (no listener). // Only hash-map methods are exercised; StartAsync is never called. private static RouteManager CreateManager() => new( new ClusterOptions { Host = "127.0.0.1", Port = 0 }, new ServerStats(), serverId: Guid.NewGuid().ToString("N"), remoteSubSink: static _ => { }, routedMessageSink: static _ => { }, logger: NullLogger.Instance); // Helper: create a RouteConnection backed by a loopback-connected socket. // RouteConnection's constructor wraps the socket in NetworkStream, which // requires a connected socket, so we use a listener/accept pair. private static RouteConnection MakeRouteConnection() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); client.Connect(IPAddress.Loopback, ((IPEndPoint)listener.LocalEndpoint).Port); // Accept server side so the TCP handshake completes; we hand the client // socket to RouteConnection and let the server-side socket be GC'd. _ = listener.AcceptSocket(); return new RouteConnection(client); } // 1 ----------------------------------------------------------------------- [Fact] public void ComputeRouteHash_Deterministic() { // Go reference: server/route.go — hash derivation must be stable across calls. const string serverId = "server-abc-123"; var h1 = RouteManager.ComputeRouteHash(serverId); var h2 = RouteManager.ComputeRouteHash(serverId); var h3 = RouteManager.ComputeRouteHash(serverId); h1.ShouldBe(h2); h2.ShouldBe(h3); } // 2 ----------------------------------------------------------------------- [Fact] public void ComputeRouteHash_DifferentInputs_DifferentHashes() { // Go reference: server/route.go — distinct server IDs must not collide. var h1 = RouteManager.ComputeRouteHash("server-1"); var h2 = RouteManager.ComputeRouteHash("server-2"); h1.ShouldNotBe(h2); } // 3 ----------------------------------------------------------------------- [Fact] public void ComputeRouteHash_EmptyString_DoesNotThrow() { // Go reference: server/route.go — empty server ID is a degenerate but // valid input; the FNV offset basis is returned. var ex = Record.Exception(() => RouteManager.ComputeRouteHash(string.Empty)); ex.ShouldBeNull(); // The hash of an empty string equals the FNV-1a 64-bit offset basis. const ulong fnvOffsetBasis = 14695981039346656037UL; RouteManager.ComputeRouteHash(string.Empty).ShouldBe(fnvOffsetBasis); } // 4 ----------------------------------------------------------------------- [Fact] public async Task RegisterRouteByHash_CanRetrieve() { // Go reference: server/route.go — after registration the connection must // be retrievable by its hash key. var mgr = CreateManager(); await using var conn = MakeRouteConnection(); mgr.RegisterRouteByHash("srv-A", conn); var hash = RouteManager.ComputeRouteHash("srv-A"); mgr.GetRouteByHash(hash).ShouldBeSameAs(conn); } // 5 ----------------------------------------------------------------------- [Fact] public async Task UnregisterRouteByHash_RemovesEntry() { // Go reference: server/route.go — deregistration removes the hash entry. var mgr = CreateManager(); await using var conn = MakeRouteConnection(); mgr.RegisterRouteByHash("srv-B", conn); mgr.UnregisterRouteByHash("srv-B"); var hash = RouteManager.ComputeRouteHash("srv-B"); mgr.GetRouteByHash(hash).ShouldBeNull(); } // 6 ----------------------------------------------------------------------- [Fact] public async Task GetRouteByServerId_FindsRegistered() { // Go reference: server/route.go — string-based lookup computes hash internally. var mgr = CreateManager(); await using var conn = MakeRouteConnection(); mgr.RegisterRouteByHash("srv-C", conn); mgr.GetRouteByServerId("srv-C").ShouldBeSameAs(conn); } // 7 ----------------------------------------------------------------------- [Fact] public void GetRouteByServerId_NotRegistered_ReturnsNull() { // Go reference: server/route.go — unknown server ID yields null. var mgr = CreateManager(); mgr.GetRouteByServerId("unknown-server").ShouldBeNull(); } // 8 ----------------------------------------------------------------------- [Fact] public async Task HashedRouteCount_MatchesRegistrations() { // Go reference: server/route.go — count reflects registered entries. var mgr = CreateManager(); await using var c1 = MakeRouteConnection(); await using var c2 = MakeRouteConnection(); await using var c3 = MakeRouteConnection(); mgr.HashedRouteCount.ShouldBe(0); mgr.RegisterRouteByHash("srv-1", c1); mgr.HashedRouteCount.ShouldBe(1); mgr.RegisterRouteByHash("srv-2", c2); mgr.HashedRouteCount.ShouldBe(2); mgr.RegisterRouteByHash("srv-3", c3); mgr.HashedRouteCount.ShouldBe(3); mgr.UnregisterRouteByHash("srv-2"); mgr.HashedRouteCount.ShouldBe(2); } // 9 ----------------------------------------------------------------------- [Fact] public async Task RegisterRouteByHash_OverwritesPrevious() { // Go reference: server/route.go — re-registering the same server ID // replaces the stale connection with the new one. var mgr = CreateManager(); await using var old = MakeRouteConnection(); await using var replacement = MakeRouteConnection(); mgr.RegisterRouteByHash("srv-D", old); mgr.RegisterRouteByHash("srv-D", replacement); mgr.GetRouteByServerId("srv-D").ShouldBeSameAs(replacement); mgr.HashedRouteCount.ShouldBe(1); } // 10 ---------------------------------------------------------------------- [Fact] public async Task UnregisterRouteByHash_NonExistent_NoOp() { // Go reference: server/route.go — removing a hash key that was never // registered must not throw and must not alter the count. var mgr = CreateManager(); await using var conn = MakeRouteConnection(); mgr.RegisterRouteByHash("srv-E", conn); var ex = Record.Exception(() => mgr.UnregisterRouteByHash("does-not-exist")); ex.ShouldBeNull(); mgr.HashedRouteCount.ShouldBe(1); } }