- Rename tests/NATS.Server.Tests -> tests/NATS.Server.Core.Tests - Update solution file, InternalsVisibleTo, and csproj references - Remove JETSTREAM_INTEGRATION_MATRIX and NATS.NKeys from csproj (moved to JetStream.Tests and Auth.Tests) - Update all namespaces from NATS.Server.Tests.* to NATS.Server.Core.Tests.* - Replace private GetFreePort/ReadUntilAsync helpers with TestUtilities calls - Fix stale namespace in Transport.Tests/NetworkingGoParityTests.cs
1019 lines
43 KiB
C#
1019 lines
43 KiB
C#
// Port of Go server/reload_test.go — Go-parity config reload tests.
|
|
// Covers: TestConfigReloadTLS, TestConfigReloadClusterAuthorization,
|
|
// TestConfigReloadClusterRoutes, TestConfigReloadAccountNKeyUsers,
|
|
// TestConfigReloadAccountExportImport, TestConfigReloadRoutePool,
|
|
// TestConfigReloadPerAccountRoutes, TestConfigReloadCompression,
|
|
// TestConfigReloadMaxControlLine, TestConfigReloadMaxPayload,
|
|
// TestConfigReloadWriteDeadline, TestConfigReloadPingInterval,
|
|
// TestConfigReloadMaxConnections, TestConfigReloadMaxSubscriptions,
|
|
// and additional reload_test.go coverage.
|
|
// 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;
|
|
using NATS.Server.TestUtilities;
|
|
|
|
namespace NATS.Server.Core.Tests.Configuration;
|
|
|
|
/// <summary>
|
|
/// Go-parity tests for config hot-reload behaviour ported from Go's reload_test.go.
|
|
/// Each test writes a temporary config file, starts the server with it, triggers a
|
|
/// config reload (optionally changing the file first), and asserts the correct
|
|
/// runtime behaviour after the reload.
|
|
/// </summary>
|
|
public class ReloadGoParityTests
|
|
{
|
|
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
|
|
private static async Task<Socket> 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 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 = TestPortAllocator.GetFreePort();
|
|
var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-reload-goparity-{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: Max Connections ──────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadMaxConnections (reload_test.go:1978)
|
|
/// Reducing max_connections below current client count causes the server
|
|
/// to reject new connections that would exceed the limit.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_MaxConnections_ReduceRejectsNew()
|
|
{
|
|
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);
|
|
server.ClientCount.ShouldBe(2);
|
|
|
|
// Reduce max_connections to 2 (equal to current count).
|
|
WriteConfigAndReload(server, configPath, $"port: {port}\nmax_connections: 2");
|
|
|
|
// A third connection should be rejected.
|
|
using var c3 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await c3.ConnectAsync(IPAddress.Loopback, port);
|
|
var response = await SocketTestHelper.ReadUntilAsync(c3, "-ERR", timeoutMs: 5000);
|
|
response.ShouldContain("maximum connections exceeded");
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadMaxConnections (reload_test.go:1978) — increase.
|
|
/// After increasing max_connections new connections should be accepted again.
|
|
/// Uses NatsConnection clients (which send CONNECT) so they are stably registered
|
|
/// in _clients and don't get cleaned up before the limit-test connections run.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_MaxConnections_IncreaseAllowsNew()
|
|
{
|
|
// Start with a high limit so c1 and c2 can connect, then reload down to 2.
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_connections: 65536");
|
|
try
|
|
{
|
|
// Use NatsConnection so they send CONNECT and are stably tracked in _clients.
|
|
await using var c1 = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
|
|
await c1.ConnectAsync();
|
|
await c1.PingAsync();
|
|
await using var c2 = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
|
|
await c2.ConnectAsync();
|
|
await c2.PingAsync();
|
|
server.ClientCount.ShouldBe(2);
|
|
|
|
// Reload to limit=2 (equal to current count); further connections should be rejected.
|
|
WriteConfigAndReload(server, configPath, $"port: {port}\nmax_connections: 2");
|
|
|
|
// A third connection should be rejected (limit=2, count=2).
|
|
using var c3reject = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await c3reject.ConnectAsync(IPAddress.Loopback, port);
|
|
var r1 = await SocketTestHelper.ReadUntilAsync(c3reject, "-ERR", timeoutMs: 5000);
|
|
r1.ShouldContain("maximum connections exceeded");
|
|
|
|
// Increase limit to 10.
|
|
WriteConfigAndReload(server, configPath, $"port: {port}\nmax_connections: 10");
|
|
|
|
// Now a new connection should succeed.
|
|
await using var c4 = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
|
|
await c4.ConnectAsync();
|
|
await c4.PingAsync();
|
|
server.ClientCount.ShouldBeGreaterThanOrEqualTo(3);
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
// ─── Tests: Max Payload ──────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadMaxPayload (reload_test.go:2032)
|
|
/// Reducing max_payload causes the server to reject oversized PUBs on new
|
|
/// connections. The INFO after reload advertises the new limit.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_MaxPayload_ReducedRejectsOversizedPub()
|
|
{
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_payload: 1048576");
|
|
try
|
|
{
|
|
// Publish a 5-byte message before reload — must succeed.
|
|
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 SocketTestHelper.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 beforeResponse = await SocketTestHelper.ReadUntilAsync(sock, "PONG");
|
|
beforeResponse.ShouldContain("MSG foo");
|
|
|
|
// Reduce max_payload to 2 bytes.
|
|
WriteConfigAndReload(server, configPath, $"port: {port}\nmax_payload: 2");
|
|
|
|
// On a NEW connection, a 5-byte publish must be rejected.
|
|
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 SocketTestHelper.ReadUntilAsync(sock2, "-ERR", timeoutMs: 5000);
|
|
errResponse.ShouldContain("-ERR");
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
// ─── Tests: Max Control Line ─────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadMaxControlLineWithClients (reload_test.go:3946)
|
|
/// Reload must update max_control_line without disconnecting existing clients.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_MaxControlLine_ReloadTakesEffect()
|
|
{
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_control_line: 4096");
|
|
try
|
|
{
|
|
await using var client1 = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
|
|
await client1.ConnectAsync();
|
|
await client1.PingAsync();
|
|
|
|
// Reduce max_control_line.
|
|
WriteConfigAndReload(server, configPath, $"port: {port}\nmax_control_line: 512");
|
|
|
|
// Existing connection must remain alive.
|
|
await client1.PingAsync();
|
|
|
|
// New connections must also work.
|
|
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);
|
|
}
|
|
}
|
|
|
|
// ─── Tests: Max Subscriptions ────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadMaxSubsUnsupported (reload_test.go:1917)
|
|
/// max_subs can be changed via reload; new value takes effect on new subscriptions.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_MaxSubs_ReloadTakesEffect()
|
|
{
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_subs: 0");
|
|
try
|
|
{
|
|
WriteConfigAndReload(server, configPath, $"port: {port}\nmax_subs: 20");
|
|
|
|
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: Write Deadline ───────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReload (reload_test.go:251) — write_deadline portion.
|
|
/// Changing write_deadline via reload must not disrupt existing connections.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_WriteDeadline_ReloadTakesEffect()
|
|
{
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nwrite_deadline: \"10s\"");
|
|
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}\nwrite_deadline: \"3s\"");
|
|
|
|
await client.PingAsync();
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
// ─── Tests: Ping Interval ────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReload (reload_test.go:251) — ping_interval portion.
|
|
/// Changing ping_interval via reload must not disrupt existing connections.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_PingInterval_ReloadTakesEffect()
|
|
{
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nping_interval: 120");
|
|
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}\nping_interval: 5");
|
|
|
|
await client.PingAsync();
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
// ─── Tests: TLS (simulated — no actual TLS certs in this test class) ────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadEnableTLS (reload_test.go:446) — basic scenario.
|
|
/// Enabling then disabling TLS-related flags via reload must not crash the server.
|
|
/// Full TLS cert rotation is covered in TlsReloadTests.cs.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_TlsFlags_ToggleViaReload()
|
|
{
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync(
|
|
"port: {PORT}\ntls_timeout: 2");
|
|
try
|
|
{
|
|
await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
|
|
await client.ConnectAsync();
|
|
await client.PingAsync();
|
|
|
|
// Increase TLS timeout via reload.
|
|
WriteConfigAndReload(server, configPath, $"port: {port}\ntls_timeout: 5");
|
|
|
|
await client.PingAsync();
|
|
|
|
// Revert.
|
|
WriteConfigAndReload(server, configPath, $"port: {port}\ntls_timeout: 2");
|
|
await client.PingAsync();
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
// ─── Tests: Cluster Authorization ────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadEnableClusterAuthorization (reload_test.go:1411) — stub.
|
|
/// Changing cluster authorization settings is non-reloadable (cluster section
|
|
/// is treated as immutable). Verify reload fails when cluster options change.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_ClusterAuthorization_ChangedClusterIsRejected()
|
|
{
|
|
// Go: TestConfigReloadEnableClusterAuthorization (reload_test.go:1411)
|
|
var clusterPort = TestPortAllocator.GetFreePort();
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync(
|
|
$"port: {{PORT}}\ncluster {{\n name: mycluster\n host: 127.0.0.1\n port: {clusterPort}\n}}");
|
|
try
|
|
{
|
|
var newClusterPort = TestPortAllocator.GetFreePort();
|
|
// Changing cluster host/port must be rejected.
|
|
File.WriteAllText(configPath,
|
|
$"port: {port}\ncluster {{\n name: mycluster\n host: 127.0.0.1\n port: {newClusterPort}\n}}");
|
|
Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow())
|
|
.Message.ShouldContain("Cluster");
|
|
|
|
// Server must still accept client connections after failed reload.
|
|
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: Cluster Routes ───────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadClusterRoutes (reload_test.go:1586) — simplified.
|
|
/// Changing cluster routes config is non-reloadable (entire cluster block
|
|
/// is immutable); the server must reject the reload with an appropriate error.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_ClusterRoutes_ChangeIsRejected()
|
|
{
|
|
// Go: TestConfigReloadClusterRoutes (reload_test.go:1586)
|
|
var clusterPort = TestPortAllocator.GetFreePort();
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync(
|
|
$"port: {{PORT}}\ncluster {{\n name: testcluster\n host: 127.0.0.1\n port: {clusterPort}\n}}");
|
|
try
|
|
{
|
|
var otherPort = TestPortAllocator.GetFreePort();
|
|
// Adding routes also changes the cluster block.
|
|
File.WriteAllText(configPath,
|
|
$"port: {port}\ncluster {{\n name: testcluster\n host: 127.0.0.1\n port: {clusterPort}\n routes: [\"nats://127.0.0.1:{otherPort}\"]\n}}");
|
|
Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow());
|
|
|
|
// Server must remain 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: Account NKey Users ───────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadAccountNKeyUsers (reload_test.go:2966) — simplified.
|
|
/// Adding/removing accounts and nkey users via reload must take effect.
|
|
/// NKey support is not yet in .NET; this test verifies password-based account users.
|
|
/// Adding a new user to an account must allow that user to connect after reload.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_AccountNKeyUsers_ReloadTakesEffect()
|
|
{
|
|
// Go: TestConfigReloadAccountNKeyUsers (reload_test.go:2966)
|
|
// Simplified: test user password-based accounts reload (nkey infra not yet in .NET).
|
|
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();
|
|
|
|
// Reload: add ivan to acctA (keep derek for compatibility with current .NET server).
|
|
WriteConfigAndReload(server, configPath,
|
|
$"port: {port}\naccounts {{\n acctA {{\n users = [\n {{user: derek, password: derek}}\n {{user: ivan, password: ivan}}\n ]\n }}\n}}");
|
|
|
|
// derek must still connect.
|
|
await derek.PingAsync();
|
|
|
|
// ivan (new user) must now connect.
|
|
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: Account Export/Import ────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadAccountStreamsImportExport (reload_test.go:3100) — simplified.
|
|
/// Adding a new account with separate users after reload must take effect.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_AccountExportImport_NewAccountAfterReload()
|
|
{
|
|
// Go: TestConfigReloadAccountStreamsImportExport (reload_test.go:3100)
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync(
|
|
"port: {PORT}\naccounts {\n alpha {\n users = [{user: alice, password: pass1}]\n }\n}");
|
|
try
|
|
{
|
|
await using var alice = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://alice:pass1@127.0.0.1:{port}",
|
|
});
|
|
await alice.ConnectAsync();
|
|
await alice.PingAsync();
|
|
|
|
// Add a new account via reload.
|
|
WriteConfigAndReload(server, configPath,
|
|
$"port: {port}\naccounts {{\n alpha {{\n users = [{{user: alice, password: pass1}}]\n }}\n beta {{\n users = [{{user: bob, password: pass2}}]\n }}\n}}");
|
|
|
|
// alice must still connect.
|
|
await alice.PingAsync();
|
|
|
|
// bob (new account) must now connect.
|
|
await using var bob = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://bob:pass2@127.0.0.1:{port}",
|
|
});
|
|
await bob.ConnectAsync();
|
|
await bob.PingAsync();
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadAccountServicesImportExport (reload_test.go:3294) — simplified.
|
|
/// Adding a user to an existing account via reload must take effect.
|
|
/// The test verifies the basic account user reload mechanism works.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_AccountExportImport_AccountUserReloadTakesEffect()
|
|
{
|
|
// Go: TestConfigReloadAccountServicesImportExport (reload_test.go:3294)
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync(
|
|
"port: {PORT}\naccounts {\n alpha {\n users = [{user: alice, password: pass1}]\n }\n beta {\n users = [{user: bob, password: pass2}]\n }\n}");
|
|
try
|
|
{
|
|
await using var alice = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://alice:pass1@127.0.0.1:{port}",
|
|
});
|
|
await alice.ConnectAsync();
|
|
await alice.PingAsync();
|
|
|
|
await using var bob = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://bob:pass2@127.0.0.1:{port}",
|
|
});
|
|
await bob.ConnectAsync();
|
|
await bob.PingAsync();
|
|
|
|
// Add charlie to alpha account.
|
|
WriteConfigAndReload(server, configPath,
|
|
$"port: {port}\naccounts {{\n alpha {{\n users = [{{user: alice, password: pass1}}, {{user: charlie, password: pass3}}]\n }}\n beta {{\n users = [{{user: bob, password: pass2}}]\n }}\n}}");
|
|
|
|
// alice still connects.
|
|
await alice.PingAsync();
|
|
|
|
// charlie (new) must now connect.
|
|
await using var charlie = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://charlie:pass3@127.0.0.1:{port}",
|
|
});
|
|
await charlie.ConnectAsync();
|
|
await charlie.PingAsync();
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
// ─── Tests: Route Pool (stub — no multi-server cluster in unit tests) ────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadRoutePoolAndPerAccount (reload_test.go:5148) — stub.
|
|
/// Route pool changes are only meaningful in multi-server clusters.
|
|
/// This test verifies that the config parser accepts pool_size in the
|
|
/// cluster section and that a reload with a changed pool_size is rejected
|
|
/// (cluster block is immutable at runtime).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_RoutePool_ClusterBlockIsImmutable()
|
|
{
|
|
// Go: TestConfigReloadRoutePoolAndPerAccount (reload_test.go:5148)
|
|
var clusterPort = TestPortAllocator.GetFreePort();
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync(
|
|
$"port: {{PORT}}\ncluster {{\n name: local\n host: 127.0.0.1\n port: {clusterPort}\n pool_size: 3\n}}");
|
|
try
|
|
{
|
|
await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
|
|
await client.ConnectAsync();
|
|
await client.PingAsync();
|
|
|
|
// Changing pool_size changes the cluster block — must be rejected.
|
|
File.WriteAllText(configPath,
|
|
$"port: {port}\ncluster {{\n name: local\n host: 127.0.0.1\n port: {clusterPort}\n pool_size: 5\n}}");
|
|
Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow())
|
|
.Message.ShouldContain("Cluster");
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadPerAccountRoutes (reload_test.go:5148) — stub.
|
|
/// Per-account routes are part of the cluster block; changing them is non-reloadable.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_PerAccountRoutes_ClusterBlockIsImmutable()
|
|
{
|
|
// Go: TestConfigReloadRoutePoolAndPerAccount (reload_test.go:5148)
|
|
var clusterPort = TestPortAllocator.GetFreePort();
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync(
|
|
$"port: {{PORT}}\naccounts {{\n A {{ users: [{{user: u1, password: pwd}}] }}\n}}\ncluster {{\n name: local\n host: 127.0.0.1\n port: {clusterPort}\n}}");
|
|
try
|
|
{
|
|
// Adding per-account routes is a cluster-block change — must be rejected.
|
|
File.WriteAllText(configPath,
|
|
$"port: {port}\naccounts {{\n A {{ users: [{{user: u1, password: pwd}}] }}\n}}\ncluster {{\n name: local\n host: 127.0.0.1\n port: {clusterPort}\n accounts: [\"A\"]\n}}");
|
|
Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow())
|
|
.Message.ShouldContain("Cluster");
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
// ─── Tests: Route Compression ────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadRouteCompression (reload_test.go:5877) — stub.
|
|
/// Route compression is part of the cluster block; changing it is non-reloadable.
|
|
/// (Full compression integration with actual routes requires multi-server setup.)
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_RouteCompression_ClusterBlockIsImmutable()
|
|
{
|
|
// Go: TestConfigReloadRouteCompression (reload_test.go:5877)
|
|
var clusterPort = TestPortAllocator.GetFreePort();
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync(
|
|
$"port: {{PORT}}\ncluster {{\n name: local\n host: 127.0.0.1\n port: {clusterPort}\n}}");
|
|
try
|
|
{
|
|
// Adding compression to the cluster block changes it — must be rejected.
|
|
File.WriteAllText(configPath,
|
|
$"port: {port}\ncluster {{\n name: local\n host: 127.0.0.1\n port: {clusterPort}\n compression: s2_better\n}}");
|
|
Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow())
|
|
.Message.ShouldContain("Cluster");
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
// ─── Tests: Misc reload scenarios from reload_test.go ───────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadChangePermissions (reload_test.go:1146) — simplified.
|
|
/// Users with different permissions can all connect after a permission reload.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_ChangePermissions_UsersStillConnect()
|
|
{
|
|
// Go: TestConfigReloadChangePermissions (reload_test.go:1146)
|
|
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();
|
|
|
|
// Reload with updated permissions (password change for alice).
|
|
WriteConfigAndReload(server, configPath,
|
|
$"port: {port}\nauthorization {{\n users = [\n {{user: alice, password: newpwd}}\n {{user: bob, password: bar}}\n ]\n}}");
|
|
|
|
// bob still connects with unchanged password.
|
|
await using var bob = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://bob:bar@127.0.0.1:{port}",
|
|
});
|
|
await bob.ConnectAsync();
|
|
await bob.PingAsync();
|
|
|
|
// alice with new password must connect.
|
|
await using var aliceNew = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://alice:newpwd@127.0.0.1:{port}",
|
|
});
|
|
await aliceNew.ConnectAsync();
|
|
await aliceNew.PingAsync();
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadAndVarz (reload_test.go:4144) — simplified.
|
|
/// Reload does not break message delivery after many connections have been served.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_PubSubAfterManyConnectionsAndReload()
|
|
{
|
|
// Go: TestConfigReloadAndVarz (reload_test.go:4144)
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nmax_connections: 65536");
|
|
try
|
|
{
|
|
// Serve several short-lived connections.
|
|
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();
|
|
}
|
|
|
|
// Reload with a different limit.
|
|
WriteConfigAndReload(server, configPath, $"port: {port}\nmax_connections: 100");
|
|
|
|
// Pub/sub must still work after reload.
|
|
await using var subConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
|
|
await subConn.ConnectAsync();
|
|
await using var subscription = await subConn.SubscribeCoreAsync<string>("test.goparity");
|
|
await subConn.PingAsync();
|
|
|
|
await using var pubConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
|
|
await pubConn.ConnectAsync();
|
|
await pubConn.PublishAsync("test.goparity", "after-reload");
|
|
|
|
using var msgCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
var msg = await subscription.Msgs.ReadAsync(msgCts.Token);
|
|
msg.Data.ShouldBe("after-reload");
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadLogging (reload_test.go:4377) — multiple sequential reloads.
|
|
/// Verifies that many repeated logging reloads leave the server stable.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_MultipleLoggingReloads_ServerStaysStable()
|
|
{
|
|
// Go: TestConfigReloadLogging (reload_test.go:4377)
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\ndebug: false\ntrace: false");
|
|
try
|
|
{
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
WriteConfigAndReload(server, configPath,
|
|
$"port: {port}\ndebug: {(i % 2 == 0 ? "true" : "false")}\ntrace: {(i % 3 == 0 ? "true" : "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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadNoPanicOnShutdown (reload_test.go:6358) — simplified.
|
|
/// Verifies that simultaneous reload and shutdown do not panic.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_NoPanicOnShutdown()
|
|
{
|
|
// Go: TestConfigReloadNoPanicOnShutdown (reload_test.go:6358)
|
|
var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-shutdown-{Guid.NewGuid():N}.conf");
|
|
var port = TestPortAllocator.GetFreePort();
|
|
|
|
try
|
|
{
|
|
for (int iter = 0; iter < 5; iter++)
|
|
{
|
|
File.WriteAllText(configPath, $"port: {port}\ndebug: false");
|
|
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();
|
|
|
|
// Trigger a reload and a cancel simultaneously.
|
|
var reloadTask = Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
File.WriteAllText(configPath, $"port: {port}\ndebug: true");
|
|
server.ReloadConfigOrThrow();
|
|
}
|
|
catch
|
|
{
|
|
// Ignored: reload may race with shutdown.
|
|
}
|
|
});
|
|
|
|
await Task.Delay(5);
|
|
await cts.CancelAsync();
|
|
server.Dispose();
|
|
await reloadTask;
|
|
|
|
// Reuse the same port for the next iteration.
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(configPath)) File.Delete(configPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadValidate (reload_test.go:4504) — simplified.
|
|
/// Reload of a config with a type error in a non-reloadable field must be rejected
|
|
/// and the server must remain fully operational.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_Validate_BadConfigRejected()
|
|
{
|
|
// Go: TestConfigReloadValidate (reload_test.go:4504)
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\ndebug: false");
|
|
try
|
|
{
|
|
// Write a config with a parse error.
|
|
File.WriteAllText(configPath, $"port: {port}\nauthorization {{\n user: test\n");
|
|
Should.Throw<Exception>(() => server.ReloadConfigOrThrow());
|
|
|
|
// Server must still accept connections.
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadAccountWithNoChanges (reload_test.go:2887) — reload no-op.
|
|
/// Reloading the same config must succeed as a no-op.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_AccountWithNoChanges_NoOpReload()
|
|
{
|
|
// Go: TestConfigReloadAccountWithNoChanges (reload_test.go:2887)
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync(
|
|
"port: {PORT}\naccounts {\n acctA {\n users = [\n {user: derek, password: pass}\n ]\n }\n}");
|
|
try
|
|
{
|
|
// Reload the SAME config — must succeed.
|
|
server.ReloadConfigOrThrow();
|
|
|
|
await using var derek = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://derek:pass@127.0.0.1:{port}",
|
|
});
|
|
await derek.ConnectAsync();
|
|
await derek.PingAsync();
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadAccounts (reload_test.go:4537) — adding/modifying accounts.
|
|
/// A user can be moved between accounts via reload.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_Accounts_UserMovedBetweenAccounts()
|
|
{
|
|
// Go: TestConfigReloadAccounts (reload_test.go:4537)
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync(
|
|
"port: {PORT}\naccounts {\n acctA {\n users = [{user: alice, password: pass}]\n }\n acctB {\n users = [{user: bob, password: pass}]\n }\n}");
|
|
try
|
|
{
|
|
await using var alice = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://alice:pass@127.0.0.1:{port}",
|
|
});
|
|
await alice.ConnectAsync();
|
|
await alice.PingAsync();
|
|
|
|
// Move alice to acctB.
|
|
WriteConfigAndReload(server, configPath,
|
|
$"port: {port}\naccounts {{\n acctA {{\n users = []\n }}\n acctB {{\n users = [{{user: alice, password: pass}}]\n }}\n}}");
|
|
|
|
// alice still connects (same credentials, now in acctB).
|
|
await using var aliceNew = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://alice:pass@127.0.0.1:{port}",
|
|
});
|
|
await aliceNew.ConnectAsync();
|
|
await aliceNew.PingAsync();
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadDefaultSystemAccount (reload_test.go:4694) — simplified.
|
|
/// The system_account field can be changed via reload (it's a reloadable option).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_DefaultSystemAccount_Reload()
|
|
{
|
|
// Go: TestConfigReloadDefaultSystemAccount (reload_test.go:4694)
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}\nno_sys_acc: true");
|
|
try
|
|
{
|
|
await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
|
|
await client.ConnectAsync();
|
|
await client.PingAsync();
|
|
|
|
// Toggle no_sys_acc (reloadable).
|
|
WriteConfigAndReload(server, configPath, $"port: {port}\nno_sys_acc: false");
|
|
|
|
await client.PingAsync();
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadAccountMappings (reload_test.go:4746) — simplified.
|
|
/// Accounts can be added and subject mappings take effect after reload.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_AccountMappings_ReloadTakesEffect()
|
|
{
|
|
// Go: TestConfigReloadAccountMappings (reload_test.go:4746)
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}");
|
|
try
|
|
{
|
|
await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
|
|
await client.ConnectAsync();
|
|
await client.PingAsync();
|
|
|
|
// Add an account with a mappings block via reload.
|
|
WriteConfigAndReload(server, configPath,
|
|
$"port: {port}\naccounts {{\n main {{\n users = [{{user: alice, password: pass}}]\n mappings = {{\n src: dest\n }}\n }}\n}}");
|
|
|
|
await using var alice = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://alice:pass@127.0.0.1:{port}",
|
|
});
|
|
await alice.ConnectAsync();
|
|
await alice.PingAsync();
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadNotPreventedByGateways (reload_test.go:3445) — simplified.
|
|
/// A reload that does not change the gateway config must succeed even when a
|
|
/// gateway block is present.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_GatewayPresent_ReloadSucceeds()
|
|
{
|
|
// Go: TestConfigReloadNotPreventedByGateways (reload_test.go:3445)
|
|
// Note: server_name is non-reloadable, so we do not include it to avoid
|
|
// conflicts between the parsed config value and the Options default.
|
|
var gatewayPort = TestPortAllocator.GetFreePort();
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync(
|
|
$"port: {{PORT}}\ngateway {{\n name: local\n port: {gatewayPort}\n}}");
|
|
try
|
|
{
|
|
// A reload that only changes debug (reloadable) while gateway block stays the same
|
|
// must succeed.
|
|
WriteConfigAndReload(server, configPath,
|
|
$"port: {port}\ndebug: true\ngateway {{\n name: local\n port: {gatewayPort}\n}}");
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadBoolFlags (reload_test.go:3480) — CLI override preservation.
|
|
/// When debug was set via CLI flag, a config reload that sets debug: false must not
|
|
/// override the CLI-set value.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ReloadGoParityTests_BoolFlags_CliOverridePreserved()
|
|
{
|
|
// Go: TestConfigReloadBoolFlags (reload_test.go:3480)
|
|
var port = TestPortAllocator.GetFreePort();
|
|
var configPath = Path.Combine(Path.GetTempPath(), $"natsdotnet-boolflags-{Guid.NewGuid():N}.conf");
|
|
File.WriteAllText(configPath, $"port: {port}\ndebug: false");
|
|
|
|
var options = new NatsOptions { ConfigFile = configPath, Port = port, Debug = true };
|
|
var server = new NatsServer(options, NullLoggerFactory.Instance);
|
|
var cliSnapshot = new NatsOptions { Debug = true };
|
|
server.SetCliSnapshot(cliSnapshot, new HashSet<string> { "Debug" });
|
|
|
|
var cts = new CancellationTokenSource();
|
|
_ = server.StartAsync(cts.Token);
|
|
await server.WaitForReadyAsync();
|
|
|
|
try
|
|
{
|
|
// Reload with debug: false in config, but CLI said true.
|
|
// The CLI override should win.
|
|
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);
|
|
}
|
|
}
|
|
}
|