using System.Net; using System.Net.Sockets; using System.Text; using Microsoft.Extensions.Logging.Abstractions; using NATS.Server; namespace NATS.Server.Tests; // Tests ported from Go server_test.go: // TestRandomPorts, TestInfoServerNameDefaultsToPK, TestInfoServerNameIsSettable, // TestLameDuckModeInfo (simplified — no cluster, just ldm property/state) public class ServerConfigTests { private static int GetFreePort() { using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); sock.Bind(new IPEndPoint(IPAddress.Loopback, 0)); return ((IPEndPoint)sock.LocalEndPoint!).Port; } private static async Task ReadUntilAsync(Socket sock, string expected, int timeoutMs = 5000) { using var cts = new CancellationTokenSource(timeoutMs); var sb = new StringBuilder(); var buf = new byte[4096]; while (!sb.ToString().Contains(expected)) { var n = await sock.ReceiveAsync(buf, SocketFlags.None, cts.Token); if (n == 0) break; sb.Append(Encoding.ASCII.GetString(buf, 0, n)); } return sb.ToString(); } // Ref: golang/nats-server/server/server_test.go TestRandomPorts // The Go test uses Port=-1 (their sentinel for "random"), we use Port=0 (.NET/BSD standard). // Verifies that after startup, server.Port is resolved to a non-zero ephemeral port. [Fact] public async Task Server_resolves_ephemeral_port_when_zero() { var opts = new NatsOptions { Port = 0 }; using var server = new NatsServer(opts, NullLoggerFactory.Instance); using var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); await server.WaitForReadyAsync(); try { server.Port.ShouldBeGreaterThan(0); server.Port.ShouldNotBe(4222); } finally { await cts.CancelAsync(); } } // Ref: golang/nats-server/server/server_test.go TestInfoServerNameIsSettable // Verifies that ServerName set in options is reflected in both the server property // and the INFO line sent to connecting clients. [Fact] public async Task Server_info_contains_server_name() { const string name = "my-test-server"; var port = GetFreePort(); var opts = new NatsOptions { Port = port, ServerName = name }; using var server = new NatsServer(opts, NullLoggerFactory.Instance); using var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); await server.WaitForReadyAsync(); try { // Property check server.ServerName.ShouldBe(name); // Wire check — INFO line sent on connect using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(IPAddress.Loopback, port); var infoLine = await ReadUntilAsync(sock, "INFO"); infoLine.ShouldContain("\"server_name\":\"my-test-server\""); } finally { await cts.CancelAsync(); } } // Ref: golang/nats-server/server/server_test.go TestInfoServerNameDefaultsToPK // Verifies that when no ServerName is configured, the server still populates both // server_id and server_name fields in the INFO line (name defaults to a generated value, // not null or empty). [Fact] public async Task Server_info_defaults_name_when_not_configured() { var port = GetFreePort(); var opts = new NatsOptions { Port = port }; // no ServerName set using var server = new NatsServer(opts, NullLoggerFactory.Instance); using var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); await server.WaitForReadyAsync(); try { // Both properties should be populated server.ServerId.ShouldNotBeNullOrWhiteSpace(); server.ServerName.ShouldNotBeNullOrWhiteSpace(); // Wire check — INFO line includes both fields using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(IPAddress.Loopback, port); var infoLine = await ReadUntilAsync(sock, "INFO"); infoLine.ShouldContain("\"server_id\":"); infoLine.ShouldContain("\"server_name\":"); } finally { await cts.CancelAsync(); } } // Ref: golang/nats-server/server/server_test.go TestLameDuckModeInfo // Simplified port: verifies that LameDuckShutdownAsync transitions the server into // lame duck mode (IsLameDuckMode becomes true) and that the server ultimately shuts // down. The full Go test requires a cluster to observe INFO updates with "ldm":true; // that aspect is not ported here because the .NET ServerInfo type does not include // an ldm/LameDuckMode field and cluster routing is out of scope for this test. [Fact] public async Task Lame_duck_mode_sets_is_lame_duck_mode_and_shuts_down() { var port = GetFreePort(); var opts = new NatsOptions { Port = port, LameDuckGracePeriod = TimeSpan.Zero, LameDuckDuration = TimeSpan.FromMilliseconds(50), }; using var server = new NatsServer(opts, NullLoggerFactory.Instance); using var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); await server.WaitForReadyAsync(); server.IsLameDuckMode.ShouldBeFalse(); // Trigger lame duck — no clients connected so it should proceed straight to shutdown. await server.LameDuckShutdownAsync(); server.IsLameDuckMode.ShouldBeTrue(); server.IsShuttingDown.ShouldBeTrue(); await cts.CancelAsync(); } }