// Port of Go server/reload_test.go — extended config reload parity tests. // Covers: no-config-file reload, unsupported option changes, invalid config, // auth rotation, token auth, multiple users, max payload, max control line, // ping interval, max pings out, write deadline, max pending, debug/trace toggles, // authorization timeout, client advertise, PID file changes, log file rotation, // connect error reports, max subscriptions, cluster config changes, and more. // Reference: golang/nats-server/server/reload_test.go using System.Net; using System.Net.Sockets; using System.Text; using Microsoft.Extensions.Logging.Abstractions; using NATS.Client.Core; using NATS.Server.Configuration; namespace NATS.Server.Tests.Configuration; /// /// Extended parity tests for config hot reload behaviour ported from Go's /// reload_test.go. Each test writes a config file, starts the server, /// changes the config, triggers a reload, and verifies the change took effect. /// public class ConfigReloadExtendedParityTests { // ─── Helpers ──────────────────────────────────────────────────────────── 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 RawConnectAsync(int port) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(IPAddress.Loopback, port); var buf = new byte[4096]; await sock.ReceiveAsync(buf, SocketFlags.None); return sock; } 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, StringComparison.Ordinal)) { int n; try { n = await sock.ReceiveAsync(buf, SocketFlags.None, cts.Token); } catch (OperationCanceledException) { break; } if (n == 0) break; sb.Append(Encoding.ASCII.GetString(buf, 0, n)); } return sb.ToString(); } private static void WriteConfigAndReload(NatsServer server, string configPath, string configText) { File.WriteAllText(configPath, configText); server.ReloadConfigOrThrow(); } private static async Task<(NatsServer server, int port, CancellationTokenSource cts, string configPath)> StartServerWithConfigAsync(string configContent) { var port = GetFreePort(); var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-reload-{Guid.NewGuid():N}.conf"); var finalContent = configContent.Replace("{PORT}", port.ToString()); File.WriteAllText(configPath, finalContent); var options = new NatsOptions { ConfigFile = configPath, Port = port }; var server = new NatsServer(options, NullLoggerFactory.Instance); var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); await server.WaitForReadyAsync(); return (server, port, cts, configPath); } private static async Task CleanupAsync(NatsServer server, CancellationTokenSource cts, string configPath) { await cts.CancelAsync(); server.Dispose(); if (File.Exists(configPath)) File.Delete(configPath); } private static bool ContainsInChain(Exception ex, string substring) { Exception? current = ex; while (current != null) { if (current.Message.Contains(substring, StringComparison.OrdinalIgnoreCase)) return true; current = current.InnerException; } return false; } // ─── Tests: No Config File ────────────────────────────────────────────── /// /// Go: TestConfigReloadNoConfigFile server/reload_test.go:116 /// Reload must fail when the server was started without a config file. /// [Fact] public async Task Reload_without_config_file_throws() { var port = GetFreePort(); var options = new NatsOptions { Port = port }; var server = new NatsServer(options, NullLoggerFactory.Instance); var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); await server.WaitForReadyAsync(); try { Should.Throw(() => server.ReloadConfigOrThrow()); } finally { await cts.CancelAsync(); server.Dispose(); } } // ─── Tests: Unsupported Changes ───────────────────────────────────────── /// /// Go: TestConfigReloadUnsupportedHotSwapping server/reload_test.go:180 /// Changing the listen port must be rejected (non-reloadable). /// [Fact] public async Task Reload_port_change_rejected() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { var newPort = GetFreePort(); File.WriteAllText(configPath, $"port: {newPort}"); Should.Throw(() => server.ReloadConfigOrThrow()) .Message.ShouldContain("Port"); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReloadInvalidConfig server/reload_test.go:202 /// Reload with an invalid config file must fail without changing the running config. /// [Fact] public async Task Reload_invalid_config_rejected() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\ndebug: false"); try { // Write invalid config (missing closing brace). File.WriteAllText(configPath, $"port: {port}\nauthorization {{\n user: test\n"); Should.Throw(() => server.ReloadConfigOrThrow()); // Server should still be operational. await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Debug / Trace Toggle ──────────────────────────────────────── /// /// Go: TestConfigReload server/reload_test.go:251 (partial — debug/trace portion). /// Verifies that debug and trace can be toggled via config reload. /// [Fact] public async Task Reload_debug_toggle() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\ndebug: false"); try { WriteConfigAndReload(server, configPath, $"port: {port}\ndebug: true"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); WriteConfigAndReload(server, configPath, $"port: {port}\ndebug: false"); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReload server/reload_test.go:251 (partial — trace portion). /// [Fact] public async Task Reload_trace_toggle() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\ntrace: false"); try { WriteConfigAndReload(server, configPath, $"port: {port}\ntrace: true"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); WriteConfigAndReload(server, configPath, $"port: {port}\ntrace: false"); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReload server/reload_test.go:251 (partial — logtime portion). /// [Fact] public async Task Reload_logtime_toggle() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nlogtime: false"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nlogtime: true\nlogtime_utc: true"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReloadLogging server/reload_test.go:4377 (partial — trace_verbose). /// [Fact] public async Task Reload_trace_verbose_toggle() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\ntrace_verbose: false"); try { WriteConfigAndReload(server, configPath, $"port: {port}\ntrace_verbose: true"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); WriteConfigAndReload(server, configPath, $"port: {port}\ntrace_verbose: false"); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: User Authentication ───────────────────────────────────────── /// /// Go: TestConfigReloadRotateUserAuthentication server/reload_test.go:658 /// Changing username/password must reject old credentials and accept new ones. /// [Fact] public async Task Reload_rotate_user_authentication() { var (server, port, cts, configPath) = await StartServerWithConfigAsync( "port: {PORT}\nauthorization {\n user: tyler\n password: T0pS3cr3t\n}"); try { await using var nc = new NatsConnection(new NatsOpts { Url = $"nats://tyler:T0pS3cr3t@127.0.0.1:{port}", }); await nc.ConnectAsync(); await nc.PingAsync(); WriteConfigAndReload(server, configPath, $"port: {port}\nauthorization {{\n user: derek\n password: passw0rd\n}}"); await using var oldCreds = new NatsConnection(new NatsOpts { Url = $"nats://tyler:T0pS3cr3t@127.0.0.1:{port}", MaxReconnectRetry = 0, }); var ex = await Should.ThrowAsync(async () => { await oldCreds.ConnectAsync(); await oldCreds.PingAsync(); }); ContainsInChain(ex, "Authorization Violation").ShouldBeTrue( $"Expected 'Authorization Violation' in exception chain, but got: {ex}"); await using var newCreds = new NatsConnection(new NatsOpts { Url = $"nats://derek:passw0rd@127.0.0.1:{port}", }); await newCreds.ConnectAsync(); await newCreds.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReloadDisableUserAuthentication server/reload_test.go:781 /// [Fact] public async Task Reload_disable_user_authentication() { var (server, port, cts, configPath) = await StartServerWithConfigAsync( "port: {PORT}\nauthorization {\n user: tyler\n password: T0pS3cr3t\n}"); try { await using var authConn = new NatsConnection(new NatsOpts { Url = $"nats://tyler:T0pS3cr3t@127.0.0.1:{port}", }); await authConn.ConnectAsync(); await authConn.PingAsync(); WriteConfigAndReload(server, configPath, $"port: {port}"); await using var noAuthConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", }); await noAuthConn.ConnectAsync(); await noAuthConn.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Token Authentication ──────────────────────────────────────── /// /// Go: TestConfigReloadEnableTokenAuthentication server/reload_test.go:871 /// [Fact] public async Task Reload_enable_token_authentication() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { await using var noAuth = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", }); await noAuth.ConnectAsync(); await noAuth.PingAsync(); WriteConfigAndReload(server, configPath, $"port: {port}\nauthorization {{\n token: T0pS3cr3t\n}}"); await using var noTokenConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", MaxReconnectRetry = 0, }); var ex = await Should.ThrowAsync(async () => { await noTokenConn.ConnectAsync(); await noTokenConn.PingAsync(); }); ContainsInChain(ex, "Authorization Violation").ShouldBeTrue( $"Expected 'Authorization Violation' but got: {ex}"); await using var tokenConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", AuthOpts = NatsAuthOpts.Default with { Token = "T0pS3cr3t" }, }); await tokenConn.ConnectAsync(); await tokenConn.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReloadRotateTokenAuthentication server/reload_test.go:814 /// [Fact] public async Task Reload_rotate_token_authentication() { var (server, port, cts, configPath) = await StartServerWithConfigAsync( "port: {PORT}\nauthorization {\n token: T0pS3cr3t\n}"); try { await using var nc = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", AuthOpts = NatsAuthOpts.Default with { Token = "T0pS3cr3t" }, }); await nc.ConnectAsync(); await nc.PingAsync(); WriteConfigAndReload(server, configPath, $"port: {port}\nauthorization {{\n token: passw0rd\n}}"); await using var oldToken = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", AuthOpts = NatsAuthOpts.Default with { Token = "T0pS3cr3t" }, MaxReconnectRetry = 0, }); var ex = await Should.ThrowAsync(async () => { await oldToken.ConnectAsync(); await oldToken.PingAsync(); }); ContainsInChain(ex, "Authorization Violation").ShouldBeTrue( $"Expected 'Authorization Violation' but got: {ex}"); await using var newToken = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", AuthOpts = NatsAuthOpts.Default with { Token = "passw0rd" }, }); await newToken.ConnectAsync(); await newToken.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReloadDisableTokenAuthentication server/reload_test.go:932 /// [Fact] public async Task Reload_disable_token_authentication() { var (server, port, cts, configPath) = await StartServerWithConfigAsync( "port: {PORT}\nauthorization {\n token: T0pS3cr3t\n}"); try { await using var tokenConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", AuthOpts = NatsAuthOpts.Default with { Token = "T0pS3cr3t" }, }); await tokenConn.ConnectAsync(); await tokenConn.PingAsync(); WriteConfigAndReload(server, configPath, $"port: {port}"); await using var noAuth = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", }); await noAuth.ConnectAsync(); await noAuth.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Multiple Users Authentication ─────────────────────────────── /// /// Go: TestConfigReloadEnableUsersAuthentication server/reload_test.go:1052 /// [Fact] public async Task Reload_enable_users_authentication() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { await using var noAuth = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", }); await noAuth.ConnectAsync(); await noAuth.PingAsync(); WriteConfigAndReload(server, configPath, $"port: {port}\nauthorization {{\n users = [\n {{user: alice, password: foo}}\n {{user: bob, password: bar}}\n ]\n}}"); await using var noCredConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", MaxReconnectRetry = 0, }); var ex = await Should.ThrowAsync(async () => { await noCredConn.ConnectAsync(); await noCredConn.PingAsync(); }); ContainsInChain(ex, "Authorization Violation").ShouldBeTrue( $"Expected 'Authorization Violation' but got: {ex}"); await using var aliceConn = new NatsConnection(new NatsOpts { Url = $"nats://alice:foo@127.0.0.1:{port}", }); await aliceConn.ConnectAsync(); await aliceConn.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReloadRotateUsersAuthentication server/reload_test.go:965 /// [Fact] public async Task Reload_rotate_users_authentication() { var (server, port, cts, configPath) = await StartServerWithConfigAsync( "port: {PORT}\nauthorization {\n users = [\n {user: alice, password: foo}\n {user: bob, password: bar}\n ]\n}"); try { await using var alice = new NatsConnection(new NatsOpts { Url = $"nats://alice:foo@127.0.0.1:{port}", }); await alice.ConnectAsync(); await alice.PingAsync(); WriteConfigAndReload(server, configPath, $"port: {port}\nauthorization {{\n users = [\n {{user: alice, password: baz}}\n {{user: bob, password: bar}}\n ]\n}}"); await using var oldAlice = new NatsConnection(new NatsOpts { Url = $"nats://alice:foo@127.0.0.1:{port}", MaxReconnectRetry = 0, }); var ex = await Should.ThrowAsync(async () => { await oldAlice.ConnectAsync(); await oldAlice.PingAsync(); }); ContainsInChain(ex, "Authorization Violation").ShouldBeTrue( $"Expected 'Authorization Violation' but got: {ex}"); await using var newAlice = new NatsConnection(new NatsOpts { Url = $"nats://alice:baz@127.0.0.1:{port}", }); await newAlice.ConnectAsync(); await newAlice.PingAsync(); await using var bob = new NatsConnection(new NatsOpts { Url = $"nats://bob:bar@127.0.0.1:{port}", }); await bob.ConnectAsync(); await bob.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReloadDisableUsersAuthentication server/reload_test.go:1113 /// [Fact] public async Task Reload_disable_users_authentication() { var (server, port, cts, configPath) = await StartServerWithConfigAsync( "port: {PORT}\nauthorization {\n users = [\n {user: alice, password: foo}\n ]\n}"); try { await using var authConn = new NatsConnection(new NatsOpts { Url = $"nats://alice:foo@127.0.0.1:{port}", }); await authConn.ConnectAsync(); await authConn.PingAsync(); WriteConfigAndReload(server, configPath, $"port: {port}"); await using var noAuth = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", }); await noAuth.ConnectAsync(); await noAuth.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Max Payload ───────────────────────────────────────────────── /// /// Go: TestConfigReloadMaxPayload server/reload_test.go:2032 /// Reducing max_payload must cause oversized publishes on new connections to be rejected. /// [Fact] public async Task Reload_max_payload_takes_effect() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_payload: 1048576"); try { using var sock = await RawConnectAsync(port); await sock.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false,\"pedantic\":false}\r\n"), SocketFlags.None); await sock.SendAsync(Encoding.ASCII.GetBytes("SUB foo 1\r\n"), SocketFlags.None); await sock.SendAsync(Encoding.ASCII.GetBytes("PING\r\n"), SocketFlags.None); await ReadUntilAsync(sock, "PONG"); await sock.SendAsync(Encoding.ASCII.GetBytes("PUB foo 5\r\nhello\r\n"), SocketFlags.None); await sock.SendAsync(Encoding.ASCII.GetBytes("PING\r\n"), SocketFlags.None); var response = await ReadUntilAsync(sock, "PONG"); response.ShouldContain("MSG foo"); WriteConfigAndReload(server, configPath, $"port: {port}\nmax_payload: 2"); using var sock2 = await RawConnectAsync(port); await sock2.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false,\"pedantic\":false}\r\n"), SocketFlags.None); await sock2.SendAsync(Encoding.ASCII.GetBytes("PUB foo 5\r\nhello\r\n"), SocketFlags.None); var errResponse = await ReadUntilAsync(sock2, "-ERR", timeoutMs: 5000); errResponse.ShouldContain("-ERR"); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Limits ────────────────────────────────────────────────────── /// /// Go: TestConfigReloadMaxControlLineWithClients server/reload_test.go:3946 /// [Fact] public async Task Reload_max_control_line() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_control_line: 4096"); try { await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); WriteConfigAndReload(server, configPath, $"port: {port}\nmax_control_line: 256"); await using var client2 = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client2.ConnectAsync(); await client2.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReload server/reload_test.go:251 (partial — ping_interval portion). /// [Fact] public async Task Reload_ping_interval() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nping_interval: 120"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nping_interval: 5"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReload server/reload_test.go:251 (partial — max_pings_out portion). /// [Fact] public async Task Reload_max_pings_out() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_pings_out: 2"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nmax_pings_out: 5"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReload server/reload_test.go:251 (partial — write_deadline portion). /// [Fact] public async Task Reload_write_deadline() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nwrite_deadline: \"10s\""); try { WriteConfigAndReload(server, configPath, $"port: {port}\nwrite_deadline: \"3s\""); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReload server/reload_test.go:251 (partial — max_pending). /// [Fact] public async Task Reload_max_pending() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nmax_pending: 1024"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReload server/reload_test.go:251 (partial — auth_timeout portion). /// [Fact] public async Task Reload_auth_timeout() { var (server, port, cts, configPath) = await StartServerWithConfigAsync( "port: {PORT}\nauthorization {\n user: tyler\n password: T0pS3cr3t\n timeout: 1\n}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nauthorization {{\n user: tyler\n password: T0pS3cr3t\n timeout: 5\n}}"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://tyler:T0pS3cr3t@127.0.0.1:{port}", }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReloadClientAdvertise server/reload_test.go:1932 /// [Fact] public async Task Reload_client_advertise() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nclient_advertise: \"me:1\""); WriteConfigAndReload(server, configPath, $"port: {port}"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: File Paths ────────────────────────────────────────────────── /// /// Go: TestConfigReloadRotateFiles server/reload_test.go:2095 (partial — pid_file). /// [Fact] public async Task Reload_pid_file_change() { var pidFile1 = Path.Combine(Path.GetTempPath(), $"natsdotnet-pid1-{Guid.NewGuid():N}.pid"); var pidFile2 = Path.Combine(Path.GetTempPath(), $"natsdotnet-pid2-{Guid.NewGuid():N}.pid"); var (server, port, cts, configPath) = await StartServerWithConfigAsync( $"port: {{PORT}}\npid_file: \"{pidFile1.Replace("\\", "\\\\")}\""); try { WriteConfigAndReload(server, configPath, $"port: {port}\npid_file: \"{pidFile2.Replace("\\", "\\\\")}\""); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); if (File.Exists(pidFile1)) File.Delete(pidFile1); if (File.Exists(pidFile2)) File.Delete(pidFile2); } } /// /// Go: TestConfigReloadRotateFiles server/reload_test.go:2095 (partial — log_file). /// [Fact] public async Task Reload_log_file_change() { var logFile1 = Path.Combine(Path.GetTempPath(), $"natsdotnet-log1-{Guid.NewGuid():N}.log"); var logFile2 = Path.Combine(Path.GetTempPath(), $"natsdotnet-log2-{Guid.NewGuid():N}.log"); var (server, port, cts, configPath) = await StartServerWithConfigAsync( $"port: {{PORT}}\nlog_file: \"{logFile1.Replace("\\", "\\\\")}\""); try { WriteConfigAndReload(server, configPath, $"port: {port}\nlog_file: \"{logFile2.Replace("\\", "\\\\")}\""); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); if (File.Exists(logFile1)) File.Delete(logFile1); if (File.Exists(logFile2)) File.Delete(logFile2); } } /// /// Changing log_size_limit via reload must take effect. /// [Fact] public async Task Reload_log_size_limit() { var logFile = Path.Combine(Path.GetTempPath(), $"natsdotnet-logsize-{Guid.NewGuid():N}.log"); var (server, port, cts, configPath) = await StartServerWithConfigAsync( $"port: {{PORT}}\nlog_file: \"{logFile.Replace("\\", "\\\\")}\""); try { WriteConfigAndReload(server, configPath, $"port: {port}\nlog_file: \"{logFile.Replace("\\", "\\\\")}\"\nlog_size_limit: 1048576"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); if (File.Exists(logFile)) File.Delete(logFile); } } /// /// Changing log_max_files via reload must take effect. /// [Fact] public async Task Reload_log_max_files() { var logFile = Path.Combine(Path.GetTempPath(), $"natsdotnet-logmax-{Guid.NewGuid():N}.log"); var (server, port, cts, configPath) = await StartServerWithConfigAsync( $"port: {{PORT}}\nlog_file: \"{logFile.Replace("\\", "\\\\")}\""); try { WriteConfigAndReload(server, configPath, $"port: {port}\nlog_file: \"{logFile.Replace("\\", "\\\\")}\"\nlog_max_files: 5"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); if (File.Exists(logFile)) File.Delete(logFile); } } // ─── Tests: Connect Error Reports ─────────────────────────────────────── /// /// Go: TestConfigReloadConnectErrReports server/reload_test.go:4193 /// [Fact] public async Task Reload_connect_error_reports() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nconnect_error_reports: 2\nreconnect_error_reports: 3"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReloadConnectErrReports server/reload_test.go:4193 (reconnect_error_reports). /// [Fact] public async Task Reload_reconnect_error_reports() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nreconnect_error_reports: 5"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Max Connections ───────────────────────────────────────────── /// /// Go: TestConfigReloadMaxConnections server/reload_test.go:1978 (extended). /// Increasing max_connections after reducing it should allow new connections. /// [Fact] public async Task Reload_max_connections_increase_allows_new_connections() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_connections: 65536"); try { using var c1 = await RawConnectAsync(port); server.ClientCount.ShouldBe(1); WriteConfigAndReload(server, configPath, $"port: {port}\nmax_connections: 1"); using var c2 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await c2.ConnectAsync(IPAddress.Loopback, port); var response = await ReadUntilAsync(c2, "-ERR", timeoutMs: 5000); response.ShouldContain("maximum connections exceeded"); WriteConfigAndReload(server, configPath, $"port: {port}\nmax_connections: 10"); using var c3 = await RawConnectAsync(port); server.ClientCount.ShouldBeGreaterThanOrEqualTo(2); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReloadMaxConnections server/reload_test.go:1978 /// Reducing max_connections below the current client count must reject new connections. /// [Fact] public async Task Reload_max_connections_below_current_rejects_new() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_connections: 65536"); try { using var c1 = await RawConnectAsync(port); using var c2 = await RawConnectAsync(port); using var c3 = await RawConnectAsync(port); server.ClientCount.ShouldBe(3); WriteConfigAndReload(server, configPath, $"port: {port}\nmax_connections: 2"); using var c4 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await c4.ConnectAsync(IPAddress.Loopback, port); var response = await ReadUntilAsync(c4, "-ERR", timeoutMs: 5000); response.ShouldContain("maximum connections exceeded"); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Unchanged Config ──────────────────────────────────────────── /// /// Go: TestConfigReloadAccountWithNoChanges server/reload_test.go:2887 /// Reloading an identical config must be a no-op. /// [Fact] public async Task Reload_unchanged_config_is_noop() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\ndebug: false"); try { server.ReloadConfigOrThrow(); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Multiple Sequential Reloads ───────────────────────────────── /// /// Go: TestConfigReloadLogging server/reload_test.go:4377 (simplified). /// Multiple sequential reloads with different logging settings must all succeed. /// [Fact] public async Task Reload_multiple_sequential_logging_reloads() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\ndebug: false\ntrace: false"); try { WriteConfigAndReload(server, configPath, $"port: {port}\ndebug: true\ntrace: false"); WriteConfigAndReload(server, configPath, $"port: {port}\ndebug: false\ntrace: true"); WriteConfigAndReload(server, configPath, $"port: {port}\ndebug: false\ntrace: false"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReload server/reload_test.go:251 (combined — auth + max payload). /// [Fact] public async Task Reload_combined_auth_and_limits() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_payload: 1048576"); try { await using var noAuth = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await noAuth.ConnectAsync(); await noAuth.PingAsync(); WriteConfigAndReload(server, configPath, $"port: {port}\nmax_payload: 1024\nauthorization {{\n user: tyler\n password: T0pS3cr3t\n}}"); await using var noAuthPost = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", MaxReconnectRetry = 0, }); await Should.ThrowAsync(async () => { await noAuthPost.ConnectAsync(); await noAuthPost.PingAsync(); }); await using var authConn = new NatsConnection(new NatsOpts { Url = $"nats://tyler:T0pS3cr3t@127.0.0.1:{port}", }); await authConn.ConnectAsync(); await authConn.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Max Subs ──────────────────────────────────────────────────── /// /// Go: TestConfigReloadMaxSubsUnsupported server/reload_test.go:1917 /// [Fact] public async Task Reload_max_subs() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_subs: 0"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nmax_subs: 10"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Changing max_sub_tokens via reload must take effect. /// [Fact] public async Task Reload_max_sub_tokens() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nmax_sub_tokens: 16"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Server Name ───────────────────────────────────────────────── /// /// Go: TestConfigReloadUnsupported server/reload_test.go:129 (server_name). /// [Fact] public async Task Reload_server_name_change_rejected() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nserver_name: alpha"); try { File.WriteAllText(configPath, $"port: {port}\nserver_name: beta"); Should.Throw(() => server.ReloadConfigOrThrow()) .Message.ShouldContain("ServerName"); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Lame Duck ─────────────────────────────────────────────────── /// /// Changing lame_duck_duration via reload. /// [Fact] public async Task Reload_lame_duck_duration() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nlame_duck_duration: \"30s\""); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Changing lame_duck_grace_period via reload. /// [Fact] public async Task Reload_lame_duck_grace_period() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nlame_duck_grace_period: \"5s\""); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Pub/Sub After Reload ──────────────────────────────────────── /// /// Go: TestConfigReload server/reload_test.go:251 (validation that pub/sub works post-reload). /// [Fact] public async Task Reload_pubsub_still_works_after_reload() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\ndebug: false"); try { await using var sub = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await sub.ConnectAsync(); await using var subscription = await sub.SubscribeCoreAsync("test.subject"); await sub.PingAsync(); await using var pub = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await pub.ConnectAsync(); await pub.PublishAsync("test.subject", "before-reload"); using var cts1 = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var msg = await subscription.Msgs.ReadAsync(cts1.Token); msg.Data.ShouldBe("before-reload"); WriteConfigAndReload(server, configPath, $"port: {port}\ndebug: true"); await Task.Delay(100); await pub.PublishAsync("test.subject", "after-reload"); using var cts2 = new CancellationTokenSource(TimeSpan.FromSeconds(5)); msg = await subscription.Msgs.ReadAsync(cts2.Token); msg.Data.ShouldBe("after-reload"); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Account Users ─────────────────────────────────────────────── /// /// Go: TestConfigReloadAccountUsers server/reload_test.go:2670 (simplified). /// [Fact] public async Task Reload_account_user_changes() { var (server, port, cts, configPath) = await StartServerWithConfigAsync( "port: {PORT}\naccounts {\n acctA {\n users = [\n {user: derek, password: derek}\n ]\n }\n}"); try { await using var derek = new NatsConnection(new NatsOpts { Url = $"nats://derek:derek@127.0.0.1:{port}", }); await derek.ConnectAsync(); await derek.PingAsync(); WriteConfigAndReload(server, configPath, $"port: {port}\naccounts {{\n acctA {{\n users = [\n {{user: derek, password: derek}}\n {{user: ivan, password: ivan}}\n ]\n }}\n}}"); await derek.PingAsync(); await using var ivan = new NatsConnection(new NatsOpts { Url = $"nats://ivan:ivan@127.0.0.1:{port}", }); await ivan.ConnectAsync(); await ivan.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Cluster Config Changes ────────────────────────────────────── /// /// Go: TestConfigReloadClusterPortUnsupported server/reload_test.go:1394 /// [Fact] public async Task Reload_cluster_port_change_rejected() { var clusterPort = GetFreePort(); var (server, port, cts, configPath) = await StartServerWithConfigAsync( $"port: {{PORT}}\ncluster {{\n host: 127.0.0.1\n port: {clusterPort}\n}}"); try { var newClusterPort = GetFreePort(); File.WriteAllText(configPath, $"port: {port}\ncluster {{\n host: 127.0.0.1\n port: {newClusterPort}\n}}"); Should.Throw(() => server.ReloadConfigOrThrow()) .Message.ShouldContain("Cluster"); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReloadClusterName server/reload_test.go:1893 /// [Fact] public async Task Reload_cluster_name_change_rejected() { var (server, port, cts, configPath) = await StartServerWithConfigAsync( "port: {PORT}\ncluster {\n name: abc\n host: 127.0.0.1\n port: -1\n}"); try { File.WriteAllText(configPath, $"port: {port}\ncluster {{\n name: xyz\n host: 127.0.0.1\n port: -1\n}}"); Should.Throw(() => server.ReloadConfigOrThrow()) .Message.ShouldContain("Cluster"); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: JetStream StoreDir ────────────────────────────────────────── /// /// JetStream.StoreDir is non-reloadable. /// [Fact] public async Task Reload_jetstream_store_dir_change_rejected() { var storeDir1 = Path.Combine(Path.GetTempPath(), $"nats-js-1-{Guid.NewGuid():N}"); var storeDir2 = Path.Combine(Path.GetTempPath(), $"nats-js-2-{Guid.NewGuid():N}"); Directory.CreateDirectory(storeDir1); var (server, port, cts, configPath) = await StartServerWithConfigAsync( $"port: {{PORT}}\njetstream {{\n store_dir: \"{storeDir1.Replace("\\", "\\\\")}\"\n}}"); try { File.WriteAllText(configPath, $"port: {port}\njetstream {{\n store_dir: \"{storeDir2.Replace("\\", "\\\\")}\"\n}}"); Should.Throw(() => server.ReloadConfigOrThrow()) .Message.ShouldContain("JetStream.StoreDir"); } finally { await CleanupAsync(server, cts, configPath); if (Directory.Exists(storeDir1)) Directory.Delete(storeDir1, true); if (Directory.Exists(storeDir2)) Directory.Delete(storeDir2, true); } } // ─── Tests: CLI Override Preservation ──────────────────────────────────── /// /// Go: TestConfigReloadBoolFlags server/reload_test.go:3480 (simplified). /// [Fact] public async Task Reload_cli_overrides_preserved() { var port = GetFreePort(); var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-cli-{Guid.NewGuid():N}.conf"); File.WriteAllText(configPath, $"port: {port}\ndebug: false"); var options = new NatsOptions { ConfigFile = configPath, Port = port, Debug = true }; options.InCmdLine.Add("Debug"); var server = new NatsServer(options, NullLoggerFactory.Instance); var cliSnapshot = new NatsOptions { Debug = true }; server.SetCliSnapshot(cliSnapshot, new HashSet { "Debug" }); var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); await server.WaitForReadyAsync(); try { WriteConfigAndReload(server, configPath, $"port: {port}\ndebug: false"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await cts.CancelAsync(); server.Dispose(); if (File.Exists(configPath)) File.Delete(configPath); } } // ─── Tests: Misc Reloadable Options ───────────────────────────────────── /// /// Changing syslog settings via reload. /// Go: TestConfigReload server/reload_test.go:251 (partial — syslog portion). /// [Fact] public async Task Reload_syslog_settings() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nsyslog: false"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nsyslog: true"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Go: TestConfigReload server/reload_test.go:251 (partial — remote_syslog). /// [Fact] public async Task Reload_remote_syslog() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nremote_syslog: \"udp://127.0.0.1:514\""); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Changing no_header_support via reload. /// [Fact] public async Task Reload_no_header_support() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nno_header_support: true"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Changing disable_sublist_cache via reload. /// [Fact] public async Task Reload_disable_sublist_cache() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\ndisable_sublist_cache: true"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Changing no_sys_acc via reload. /// [Fact] public async Task Reload_no_system_account() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nno_sys_acc: true"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Changing max_closed_clients via reload. /// [Fact] public async Task Reload_max_closed_clients() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nmax_closed_clients: 500"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Changing max_traced_msg_len via reload. /// [Fact] public async Task Reload_max_traced_msg_len() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\nmax_traced_msg_len: 1024"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Changing tags via reload. /// [Fact] public async Task Reload_tags_change() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { WriteConfigAndReload(server, configPath, $"port: {port}\ntags: {{ region: \"us-east-1\" }}"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Rapid Reload Cycles ───────────────────────────────────────── /// /// Verifies that the server handles many rapid sequential reloads without /// errors or instability. /// [Fact] public async Task Reload_rapid_sequential_reloads() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\ndebug: false"); try { for (int i = 0; i < 20; i++) { WriteConfigAndReload(server, configPath, $"port: {port}\ndebug: {(i % 2 == 0).ToString().ToLowerInvariant()}"); } await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Auth + Existing Connections ───────────────────────────────── /// /// Go: TestConfigReloadEnableUserAuthentication server/reload_test.go:720 /// Enabling auth with existing connections. /// [Fact] public async Task Reload_enable_auth_with_existing_connections() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { using var rawConn1 = await RawConnectAsync(port); using var rawConn2 = await RawConnectAsync(port); server.ClientCount.ShouldBe(2); WriteConfigAndReload(server, configPath, $"port: {port}\nauthorization {{\n user: test\n password: secret\n}}"); await using var authConn = new NatsConnection(new NatsOpts { Url = $"nats://test:secret@127.0.0.1:{port}", }); await authConn.ConnectAsync(); await authConn.PingAsync(); await using var noAuth = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}", MaxReconnectRetry = 0, }); await Should.ThrowAsync(async () => { await noAuth.ConnectAsync(); await noAuth.PingAsync(); }); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Concurrent Connections During Reload ──────────────────────── /// /// Verifies that connections established during a reload cycle are handled gracefully. /// [Fact] public async Task Reload_concurrent_connections_during_reload() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\ndebug: false"); try { var tasks = new List(); for (int i = 0; i < 5; i++) { tasks.Add(Task.Run(async () => { await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); })); } WriteConfigAndReload(server, configPath, $"port: {port}\ndebug: true"); await Task.WhenAll(tasks); await using var postReload = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await postReload.ConnectAsync(); await postReload.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Reload After Connections Served ───────────────────────────── /// /// Go: TestConfigReloadAndVarz server/reload_test.go:4144 (simplified). /// [Fact] public async Task Reload_after_connections_served() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_connections: 65536"); try { for (int i = 0; i < 5; i++) { await using var conn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await conn.ConnectAsync(); await conn.PingAsync(); } WriteConfigAndReload(server, configPath, $"port: {port}\nmax_connections: 100"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } // ─── Tests: Monitor Port ──────────────────────────────────────────────── /// /// Changing monitor_port (http_port) via reload. /// [Fact] public async Task Reload_monitor_port() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { var monPort = GetFreePort(); WriteConfigAndReload(server, configPath, $"port: {port}\nhttp_port: {monPort}"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } /// /// Changing prof_port via reload. /// [Fact] public async Task Reload_prof_port() { var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}"); try { var profPort = GetFreePort(); WriteConfigAndReload(server, configPath, $"port: {port}\nprof_port: {profPort}"); await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); await client.ConnectAsync(); await client.PingAsync(); } finally { await CleanupAsync(server, cts, configPath); } } }