using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging.Abstractions; using NATS.Server.Configuration; using NATS.Server.LeafNodes; namespace NATS.Server.LeafNodes.Tests.LeafNodes; /// /// Unit tests for leaf node reconnect state validation (Gap 12.3). /// Verifies that , /// , , /// and enforce self-connect detection, /// duplicate-connection rejection, and JetStream domain conflict detection. /// Go reference: leafnode.go addLeafNodeConnection — duplicate and domain checks. /// public class LeafValidationTests { private static LeafNodeManager CreateManager(string serverId = "server-A") => new( options: new LeafNodeOptions { Host = "127.0.0.1", Port = 0 }, stats: new ServerStats(), serverId: serverId, remoteSubSink: _ => { }, messageSink: _ => { }, logger: NullLogger.Instance); /// /// Creates a connected socket pair and returns the server-side socket. /// The caller is responsible for disposing both sockets. /// private static async Task<(Socket serverSide, Socket clientSide, TcpListener listener)> CreateSocketPairAsync() { var tcpListener = new TcpListener(IPAddress.Loopback, 0); tcpListener.Start(); var clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await clientSocket.ConnectAsync(IPAddress.Loopback, ((IPEndPoint)tcpListener.LocalEndpoint).Port); var serverSocket = await tcpListener.AcceptSocketAsync(); tcpListener.Stop(); return (serverSocket, clientSocket, tcpListener); } private static async Task CreateConnectionWithRemoteIdAsync(string remoteId, string? jsDomain = null) { var (serverSide, clientSide, _) = await CreateSocketPairAsync(); clientSide.Dispose(); // only need the server side for the LeafConnection var conn = new LeafConnection(serverSide) { RemoteId = remoteId, JetStreamDomain = jsDomain, }; return conn; } // Go: leafnode.go addLeafNodeConnection — happy path with distinct server IDs [Fact] public async Task ValidateRemoteLeafNode_Valid_ReturnsValid() { var manager = CreateManager("server-A"); await using var conn = await CreateConnectionWithRemoteIdAsync("server-B"); manager.InjectConnectionForTesting(conn); var result = manager.ValidateRemoteLeafNode("server-C", "$G", null); result.Valid.ShouldBeTrue(); result.Error.ShouldBeNull(); result.ErrorCode.ShouldBe(LeafValidationError.None); } // Go: leafnode.go — loop detection: reject when remote ID matches own server ID [Fact] public void ValidateRemoteLeafNode_SelfConnect_ReturnsError() { var manager = CreateManager("server-A"); var result = manager.ValidateRemoteLeafNode("server-A", "$G", null); result.Valid.ShouldBeFalse(); result.ErrorCode.ShouldBe(LeafValidationError.SelfConnect); result.Error.ShouldNotBeNull(); } // Go: leafnode.go addLeafNodeConnection checkForDup — existing connection from same remote [Fact] public async Task ValidateRemoteLeafNode_DuplicateConnection_ReturnsError() { var manager = CreateManager("server-A"); await using var conn = await CreateConnectionWithRemoteIdAsync("server-B"); manager.InjectConnectionForTesting(conn); var result = manager.ValidateRemoteLeafNode("server-B", "$G", null); result.Valid.ShouldBeFalse(); result.ErrorCode.ShouldBe(LeafValidationError.DuplicateConnection); result.Error.ShouldNotBeNull(); } // Go: leafnode.go addLeafNodeConnection — JetStream domain conflict between existing and incoming [Fact] public async Task ValidateRemoteLeafNode_JsDomainConflict_ReturnsError() { var manager = CreateManager("server-A"); await using var conn = await CreateConnectionWithRemoteIdAsync("server-B", jsDomain: "domain-hub"); manager.InjectConnectionForTesting(conn); // server-C tries to connect with a different JS domain var result = manager.ValidateRemoteLeafNode("server-C", "$G", "domain-spoke"); result.Valid.ShouldBeFalse(); result.ErrorCode.ShouldBe(LeafValidationError.JetStreamDomainConflict); result.Error.ShouldNotBeNull(); } // Go: leafnode.go — null JS domain is never a conflict [Fact] public async Task ValidateRemoteLeafNode_NullJsDomain_Valid() { var manager = CreateManager("server-A"); await using var conn = await CreateConnectionWithRemoteIdAsync("server-B", jsDomain: "domain-hub"); manager.InjectConnectionForTesting(conn); // Incoming with null domain — no domain conflict check performed var result = manager.ValidateRemoteLeafNode("server-C", "$G", null); result.Valid.ShouldBeTrue(); result.ErrorCode.ShouldBe(LeafValidationError.None); } // Go: leafnode.go — IsSelfConnect true when IDs match [Fact] public void IsSelfConnect_MatchingId_ReturnsTrue() { var manager = CreateManager("server-A"); manager.IsSelfConnect("server-A").ShouldBeTrue(); } // Go: leafnode.go — IsSelfConnect false when IDs differ [Fact] public void IsSelfConnect_DifferentId_ReturnsFalse() { var manager = CreateManager("server-A"); manager.IsSelfConnect("server-B").ShouldBeFalse(); } // Go: leafnode.go — HasConnection true when remote ID is registered [Fact] public async Task HasConnection_Existing_ReturnsTrue() { var manager = CreateManager("server-A"); await using var conn = await CreateConnectionWithRemoteIdAsync("server-B"); manager.InjectConnectionForTesting(conn); manager.HasConnection("server-B").ShouldBeTrue(); } // Go: leafnode.go — HasConnection false when remote ID is not registered [Fact] public void HasConnection_Missing_ReturnsFalse() { var manager = CreateManager("server-A"); manager.HasConnection("server-B").ShouldBeFalse(); } // Go: leafnode.go — GetConnectionByRemoteId returns the matching connection [Fact] public async Task GetConnectionByRemoteId_Found_ReturnsConnection() { var manager = CreateManager("server-A"); await using var conn = await CreateConnectionWithRemoteIdAsync("server-B"); manager.InjectConnectionForTesting(conn); var found = manager.GetConnectionByRemoteId("server-B"); found.ShouldNotBeNull(); found.RemoteId.ShouldBe("server-B"); } }