// 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);
}
}
}