using System.Net; using System.Net.Sockets; using System.Text; using Microsoft.Extensions.Logging.Abstractions; using NATS.Server; using NATS.Server.Configuration; using NATS.Server.TestUtilities; namespace NATS.Server.Core.Tests.Server; public class CoreServerGapParityTests { [Fact] public void ClientURL_uses_advertise_when_present() { using var server = new NatsServer( new NatsOptions { Host = "0.0.0.0", Port = 4222, ClientAdvertise = "demo.example.net:4333" }, NullLoggerFactory.Instance); server.ClientURL().ShouldBe("nats://demo.example.net:4333"); } [Fact] public void ClientURL_uses_loopback_for_wildcard_host() { using var server = new NatsServer( new NatsOptions { Host = "0.0.0.0", Port = 4222 }, NullLoggerFactory.Instance); server.ClientURL().ShouldBe("nats://127.0.0.1:4222"); } [Fact] public void WebsocketURL_uses_default_host_port_when_enabled() { using var server = new NatsServer( new NatsOptions { WebSocket = new WebSocketOptions { Host = "0.0.0.0", Port = 8080, NoTls = true, }, }, NullLoggerFactory.Instance); server.WebsocketURL().ShouldBe("ws://127.0.0.1:8080"); } [Fact] public void WebsocketURL_returns_null_when_disabled() { using var server = new NatsServer(new NatsOptions(), NullLoggerFactory.Instance); server.WebsocketURL().ShouldBeNull(); } [Fact] public void Account_count_methods_reflect_loaded_and_active_accounts() { using var server = new NatsServer(new NatsOptions(), NullLoggerFactory.Instance); server.NumLoadedAccounts().ShouldBe(2); // $G + $SYS server.NumActiveAccounts().ShouldBe(0); var app = server.GetOrCreateAccount("APP"); server.NumLoadedAccounts().ShouldBe(3); app.AddClient(42); server.NumActiveAccounts().ShouldBe(1); } [Fact] public void Address_and_counter_methods_are_derived_from_options_and_stats() { using var server = new NatsServer( new NatsOptions { Host = "127.0.0.1", Port = 4222, MonitorHost = "127.0.0.1", MonitorPort = 8222, ProfPort = 6060, }, NullLoggerFactory.Instance); server.Stats.Routes = 2; server.Stats.Gateways = 1; server.Stats.Leafs = 3; server.Addr().ShouldBe("127.0.0.1:4222"); server.MonitorAddr().ShouldBe("127.0.0.1:8222"); server.ProfilerAddr().ShouldBe("127.0.0.1:6060"); server.NumRoutes().ShouldBe(2); server.NumLeafNodes().ShouldBe(3); server.NumRemotes().ShouldBe(6); } [Fact] public void ToString_includes_identity_and_address() { using var server = new NatsServer( new NatsOptions { ServerName = "test-node", Host = "127.0.0.1", Port = 4222 }, NullLoggerFactory.Instance); var value = server.ToString(); value.ShouldContain("NatsServer("); value.ShouldContain("Name=test-node"); value.ShouldContain("Addr=127.0.0.1:4222"); } [Fact] public void PortsInfo_returns_configured_listen_endpoints() { using var server = new NatsServer( new NatsOptions { Host = "127.0.0.1", Port = 4222, MonitorHost = "127.0.0.1", MonitorPort = 8222, ProfPort = 6060, WebSocket = new WebSocketOptions { Host = "127.0.0.1", Port = 8443 }, Cluster = new ClusterOptions { Host = "127.0.0.1", Port = 6222 }, LeafNode = new LeafNodeOptions { Host = "127.0.0.1", Port = 7422 }, }, NullLoggerFactory.Instance); var ports = server.PortsInfo(); ports.Nats.ShouldContain("127.0.0.1:4222"); ports.Monitoring.ShouldContain("127.0.0.1:8222"); ports.Profile.ShouldContain("127.0.0.1:6060"); ports.WebSocket.ShouldContain("127.0.0.1:8443"); ports.Cluster.ShouldContain("127.0.0.1:6222"); ports.LeafNodes.ShouldContain("127.0.0.1:7422"); } [Fact] public void Profiler_and_peer_accessors_have_parity_surface() { using var server = new NatsServer( new NatsOptions { Port = 4222, ProfPort = 6060 }, NullLoggerFactory.Instance); server.StartProfiler().ShouldBeTrue(); server.ActivePeers().ShouldBeEmpty(); } [Fact] public void Connect_urls_helpers_include_non_wildcard_and_cache_refresh() { using var server = new NatsServer( new NatsOptions { Host = "127.0.0.1", Port = 4222 }, NullLoggerFactory.Instance); var urls = server.GetConnectURLs(); urls.ShouldContain("nats://127.0.0.1:4222"); server.UpdateServerINFOAndSendINFOToClients(); var info = Encoding.ASCII.GetString(server.CachedInfoLine); info.ShouldContain("\"connect_urls\":[\"nats://127.0.0.1:4222\"]"); } [Fact] public async Task DisconnectClientByID_closes_connected_client() { var port = TestPortAllocator.GetFreePort(); using var server = new NatsServer(new NatsOptions { Host = "127.0.0.1", Port = port }, NullLoggerFactory.Instance); using var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); await server.WaitForReadyAsync(); using var socket = await ConnectAndHandshakeAsync(port); await WaitUntilAsync(() => server.ClientCount == 1); var clientId = server.GetClients().Single().Id; server.DisconnectClientByID(clientId).ShouldBeTrue(); await WaitUntilAsync(() => server.ClientCount == 0); await server.ShutdownAsync(); } [Fact] public async Task LDMClientByID_closes_connected_client() { var port = TestPortAllocator.GetFreePort(); using var server = new NatsServer(new NatsOptions { Host = "127.0.0.1", Port = port }, NullLoggerFactory.Instance); using var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); await server.WaitForReadyAsync(); using var socket = await ConnectAndHandshakeAsync(port); await WaitUntilAsync(() => server.ClientCount == 1); var clientId = server.GetClients().Single().Id; server.LDMClientByID(clientId).ShouldBeTrue(); await WaitUntilAsync(() => server.ClientCount == 0); await server.ShutdownAsync(); } private static async Task ConnectAndHandshakeAsync(int port) { var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await socket.ConnectAsync(IPAddress.Loopback, port); _ = await ReadLineAsync(socket, CancellationToken.None); // INFO await socket.SendAsync(Encoding.ASCII.GetBytes("CONNECT {}\r\nPING\r\n"), SocketFlags.None); var pong = await ReadUntilContainsAsync(socket, "PONG", CancellationToken.None); pong.ShouldContain("PONG"); return socket; } private static async Task ReadLineAsync(Socket socket, CancellationToken ct) { var buffer = new List(256); var single = new byte[1]; while (true) { var n = await socket.ReceiveAsync(single.AsMemory(0, 1), SocketFlags.None, ct); if (n == 0) break; if (single[0] == '\n') break; if (single[0] != '\r') buffer.Add(single[0]); } return Encoding.ASCII.GetString([.. buffer]); } private static async Task ReadUntilContainsAsync(Socket socket, string token, CancellationToken ct) { var end = DateTime.UtcNow.AddSeconds(3); var builder = new StringBuilder(); while (DateTime.UtcNow < end) { var line = await ReadLineAsync(socket, ct); if (line.Length == 0) continue; builder.AppendLine(line); if (builder.ToString().Contains(token, StringComparison.Ordinal)) return builder.ToString(); } return builder.ToString(); } private static async Task WaitUntilAsync(Func predicate) { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); while (!cts.IsCancellationRequested) { if (predicate()) return; await Task.Yield(); } throw new TimeoutException("Condition was not met in time."); } }