Ports 40 tests from Go's opts_test.go and reload_test.go covering: NatsOptions defaults, ConfigProcessor parsing round-trips, ConfigReloader diff/validate semantics, CLI override precedence, and runtime reload rejection of host/cluster/JetStream changes.
631 lines
23 KiB
C#
631 lines
23 KiB
C#
// Advanced configuration and reload tests for full Go parity.
|
|
// Covers: CLI override precedence (opts_test.go TestMergeOverrides, TestConfigureOptions),
|
|
// configuration defaults (opts_test.go TestDefaultOptions), configuration validation
|
|
// (opts_test.go TestMalformedListenAddress, TestMaxClosedClients), NatsOptions model
|
|
// defaults, ConfigProcessor parsing, ConfigReloader diff/validate semantics, and
|
|
// reload scenarios not covered by ConfigReloadExtendedParityTests.
|
|
// Reference: golang/nats-server/server/opts_test.go, 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;
|
|
|
|
/// <summary>
|
|
/// Advanced configuration model and hot-reload tests ported from Go's opts_test.go
|
|
/// and reload_test.go. Focuses on: NatsOptions defaults, ConfigProcessor parsing,
|
|
/// ConfigReloader diff/validate, CLI-override precedence, and reload-time validation
|
|
/// paths not exercised by the basic and extended parity suites.
|
|
/// </summary>
|
|
public class ConfigReloadAdvancedTests
|
|
{
|
|
// ─── 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<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 async Task<string> 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-adv-{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);
|
|
}
|
|
|
|
// ─── Tests: NatsOptions Default Values ──────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestDefaultOptions opts_test.go:52
|
|
/// NatsOptions must be constructed with the correct NATS protocol defaults.
|
|
/// </summary>
|
|
[Fact]
|
|
public void NatsOptions_default_port_is_4222()
|
|
{
|
|
var opts = new NatsOptions();
|
|
opts.Port.ShouldBe(4222);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestDefaultOptions opts_test.go:52
|
|
/// Default host must be the wildcard address to listen on all interfaces.
|
|
/// </summary>
|
|
[Fact]
|
|
public void NatsOptions_default_host_is_wildcard()
|
|
{
|
|
var opts = new NatsOptions();
|
|
opts.Host.ShouldBe("0.0.0.0");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestDefaultOptions opts_test.go:52 (MaxConn = DEFAULT_MAX_CONNECTIONS = 65536)
|
|
/// </summary>
|
|
[Fact]
|
|
public void NatsOptions_default_max_connections_is_65536()
|
|
{
|
|
var opts = new NatsOptions();
|
|
opts.MaxConnections.ShouldBe(65536);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestDefaultOptions opts_test.go:52 (MaxPayload = MAX_PAYLOAD_SIZE = 1MB)
|
|
/// </summary>
|
|
[Fact]
|
|
public void NatsOptions_default_max_payload_is_1_megabyte()
|
|
{
|
|
var opts = new NatsOptions();
|
|
opts.MaxPayload.ShouldBe(1024 * 1024);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestDefaultOptions opts_test.go:52 (MaxControlLine = MAX_CONTROL_LINE_SIZE = 4096)
|
|
/// </summary>
|
|
[Fact]
|
|
public void NatsOptions_default_max_control_line_is_4096()
|
|
{
|
|
var opts = new NatsOptions();
|
|
opts.MaxControlLine.ShouldBe(4096);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestDefaultOptions opts_test.go:52 (PingInterval = DEFAULT_PING_INTERVAL = 2m)
|
|
/// </summary>
|
|
[Fact]
|
|
public void NatsOptions_default_ping_interval_is_two_minutes()
|
|
{
|
|
var opts = new NatsOptions();
|
|
opts.PingInterval.ShouldBe(TimeSpan.FromMinutes(2));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestDefaultOptions opts_test.go:52 (MaxPingsOut = DEFAULT_PING_MAX_OUT = 2)
|
|
/// </summary>
|
|
[Fact]
|
|
public void NatsOptions_default_max_pings_out_is_2()
|
|
{
|
|
var opts = new NatsOptions();
|
|
opts.MaxPingsOut.ShouldBe(2);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestDefaultOptions opts_test.go:52 (AuthTimeout = AUTH_TIMEOUT = 2s)
|
|
/// </summary>
|
|
[Fact]
|
|
public void NatsOptions_default_auth_timeout_is_two_seconds()
|
|
{
|
|
var opts = new NatsOptions();
|
|
opts.AuthTimeout.ShouldBe(TimeSpan.FromSeconds(2));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestDefaultOptions opts_test.go:52 (WriteDeadline = DEFAULT_FLUSH_DEADLINE = 10s)
|
|
/// </summary>
|
|
[Fact]
|
|
public void NatsOptions_default_write_deadline_is_ten_seconds()
|
|
{
|
|
var opts = new NatsOptions();
|
|
opts.WriteDeadline.ShouldBe(TimeSpan.FromSeconds(10));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestDefaultOptions opts_test.go:52 (ConnectErrorReports = 3600)
|
|
/// </summary>
|
|
[Fact]
|
|
public void NatsOptions_default_connect_error_reports()
|
|
{
|
|
var opts = new NatsOptions();
|
|
opts.ConnectErrorReports.ShouldBe(3600);
|
|
}
|
|
|
|
// ─── Tests: ConfigProcessor Parsing ────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigFile opts_test.go:97 — parsed config overrides default port.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_parses_port()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("port: 14222");
|
|
opts.Port.ShouldBe(14222);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigFile opts_test.go:97 — parsed config sets host.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_parses_host()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("host: 127.0.0.1");
|
|
opts.Host.ShouldBe("127.0.0.1");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigFile opts_test.go:97 — parsed config sets server_name.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_parses_server_name()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("server_name: my-server");
|
|
opts.ServerName.ShouldBe("my-server");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigFile opts_test.go:97 — debug/trace flags parsed from config.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_parses_debug_and_trace()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("debug: true\ntrace: true");
|
|
opts.Debug.ShouldBeTrue();
|
|
opts.Trace.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigFile opts_test.go:97 — max_payload parsed from config.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_parses_max_payload()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("max_payload: 65536");
|
|
opts.MaxPayload.ShouldBe(65536);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestPingIntervalNew opts_test.go:1369 — ping_interval parsed as duration string.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_parses_ping_interval_duration_string()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("ping_interval: \"60s\"");
|
|
opts.PingInterval.ShouldBe(TimeSpan.FromSeconds(60));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestParseWriteDeadline opts_test.go:1187 — write_deadline as "Xs" duration string.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_parses_write_deadline_duration_string()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("write_deadline: \"3s\"");
|
|
opts.WriteDeadline.ShouldBe(TimeSpan.FromSeconds(3));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMalformedListenAddress opts_test.go:1314
|
|
/// A malformed listen address must produce a parsing exception.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_rejects_malformed_listen_address()
|
|
{
|
|
Should.Throw<Exception>(() => ConfigProcessor.ProcessConfig("listen: \":not-a-port\""));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestEmptyConfig opts_test.go:1302
|
|
/// An empty config file must produce options with all default values.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_empty_config_produces_defaults()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("");
|
|
opts.Port.ShouldBe(4222);
|
|
opts.Host.ShouldBe("0.0.0.0");
|
|
opts.MaxPayload.ShouldBe(1024 * 1024);
|
|
opts.MaxConnections.ShouldBe(65536);
|
|
}
|
|
|
|
// ─── Tests: ConfigReloader Diff / Validate ──────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadUnsupportedHotSwapping reload_test.go:180
|
|
/// ConfigReloader.Diff must detect port change as non-reloadable.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigReloader_diff_detects_port_change_as_non_reloadable()
|
|
{
|
|
var oldOpts = new NatsOptions { Port = 4222 };
|
|
var newOpts = new NatsOptions { Port = 5555 };
|
|
|
|
var changes = ConfigReloader.Diff(oldOpts, newOpts);
|
|
var portChange = changes.FirstOrDefault(c => c.Name == "Port");
|
|
|
|
portChange.ShouldNotBeNull();
|
|
portChange!.IsNonReloadable.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReload reload_test.go:251 — debug flag diff correctly categorised.
|
|
/// ConfigReloader.Diff must categorise debug change as a logging change.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigReloader_diff_categorises_debug_as_logging_change()
|
|
{
|
|
var oldOpts = new NatsOptions { Debug = false };
|
|
var newOpts = new NatsOptions { Debug = true };
|
|
|
|
var changes = ConfigReloader.Diff(oldOpts, newOpts);
|
|
var debugChange = changes.FirstOrDefault(c => c.Name == "Debug");
|
|
|
|
debugChange.ShouldNotBeNull();
|
|
debugChange!.IsLoggingChange.ShouldBeTrue();
|
|
debugChange.IsNonReloadable.ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadRotateUserAuthentication reload_test.go:658
|
|
/// ConfigReloader.Diff must categorise username/password change as an auth change.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigReloader_diff_categorises_username_as_auth_change()
|
|
{
|
|
var oldOpts = new NatsOptions { Username = "alice" };
|
|
var newOpts = new NatsOptions { Username = "bob" };
|
|
|
|
var changes = ConfigReloader.Diff(oldOpts, newOpts);
|
|
var usernameChange = changes.FirstOrDefault(c => c.Name == "Username");
|
|
|
|
usernameChange.ShouldNotBeNull();
|
|
usernameChange!.IsAuthChange.ShouldBeTrue();
|
|
usernameChange.IsNonReloadable.ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReload reload_test.go:251
|
|
/// ConfigReloader.Diff on identical options must return an empty change list.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigReloader_diff_on_identical_options_returns_empty()
|
|
{
|
|
var opts = new NatsOptions { Port = 4222, Debug = false, MaxPayload = 1024 * 1024 };
|
|
var same = new NatsOptions { Port = 4222, Debug = false, MaxPayload = 1024 * 1024 };
|
|
|
|
var changes = ConfigReloader.Diff(opts, same);
|
|
changes.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadClusterPortUnsupported reload_test.go:1394
|
|
/// ConfigReloader.Diff must detect cluster port change as non-reloadable.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigReloader_diff_detects_cluster_port_change_as_non_reloadable()
|
|
{
|
|
var oldOpts = new NatsOptions { Cluster = new ClusterOptions { Host = "127.0.0.1", Port = 6222 } };
|
|
var newOpts = new NatsOptions { Cluster = new ClusterOptions { Host = "127.0.0.1", Port = 7777 } };
|
|
|
|
var changes = ConfigReloader.Diff(oldOpts, newOpts);
|
|
var clusterChange = changes.FirstOrDefault(c => c.Name == "Cluster");
|
|
|
|
clusterChange.ShouldNotBeNull();
|
|
clusterChange!.IsNonReloadable.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: reload_test.go — JetStream.StoreDir change must be non-reloadable.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigReloader_diff_detects_jetstream_store_dir_change_as_non_reloadable()
|
|
{
|
|
var oldOpts = new NatsOptions { JetStream = new JetStreamOptions { StoreDir = "/tmp/js1" } };
|
|
var newOpts = new NatsOptions { JetStream = new JetStreamOptions { StoreDir = "/tmp/js2" } };
|
|
|
|
var changes = ConfigReloader.Diff(oldOpts, newOpts);
|
|
var jsDirChange = changes.FirstOrDefault(c => c.Name == "JetStream.StoreDir");
|
|
|
|
jsDirChange.ShouldNotBeNull();
|
|
jsDirChange!.IsNonReloadable.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// ConfigReloader.Validate must return errors for all non-reloadable changes.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigReloader_validate_returns_errors_for_non_reloadable_changes()
|
|
{
|
|
var oldOpts = new NatsOptions { Port = 4222 };
|
|
var newOpts = new NatsOptions { Port = 9999 };
|
|
|
|
var changes = ConfigReloader.Diff(oldOpts, newOpts);
|
|
var errors = ConfigReloader.Validate(changes);
|
|
|
|
errors.ShouldNotBeEmpty();
|
|
errors.ShouldContain(e => e.Contains("Port", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
// ─── Tests: CLI Override Precedence ────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestMergeOverrides opts_test.go:264
|
|
/// ConfigReloader.MergeCliOverrides must restore the CLI port value after a
|
|
/// config reload that tries to set a different port.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigReloader_merge_cli_overrides_restores_port()
|
|
{
|
|
// Simulate: CLI sets port=14222; config file says port=9999.
|
|
var cliValues = new NatsOptions { Port = 14222 };
|
|
var cliFlags = new HashSet<string> { "Port" };
|
|
var fromConfig = new NatsOptions { Port = 9999 };
|
|
|
|
ConfigReloader.MergeCliOverrides(fromConfig, cliValues, cliFlags);
|
|
|
|
fromConfig.Port.ShouldBe(14222);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMergeOverrides opts_test.go:264
|
|
/// CLI debug=true must override config debug=false after merge.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigReloader_merge_cli_overrides_restores_debug_flag()
|
|
{
|
|
var cliValues = new NatsOptions { Debug = true };
|
|
var cliFlags = new HashSet<string> { "Debug" };
|
|
var fromConfig = new NatsOptions { Debug = false };
|
|
|
|
ConfigReloader.MergeCliOverrides(fromConfig, cliValues, cliFlags);
|
|
|
|
fromConfig.Debug.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMergeOverrides opts_test.go:264
|
|
/// A flag not present in cliFlags must not override the config value.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigReloader_merge_cli_overrides_ignores_non_cli_fields()
|
|
{
|
|
var cliValues = new NatsOptions { MaxPayload = 512 };
|
|
// MaxPayload is NOT in cliFlags — it came from config, not CLI.
|
|
var cliFlags = new HashSet<string> { "Port" };
|
|
var fromConfig = new NatsOptions { MaxPayload = 1024 * 1024 };
|
|
|
|
ConfigReloader.MergeCliOverrides(fromConfig, cliValues, cliFlags);
|
|
|
|
// MaxPayload should remain the config-file value, not the CLI stub value.
|
|
fromConfig.MaxPayload.ShouldBe(1024 * 1024);
|
|
}
|
|
|
|
// ─── Tests: Config File Parsing Round-Trip ──────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigFile opts_test.go:97 — max_connections parsed and accessible.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_parses_max_connections()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("max_connections: 100");
|
|
opts.MaxConnections.ShouldBe(100);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigFile opts_test.go:97 — lame_duck_duration parsed from config.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_parses_lame_duck_duration()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("lame_duck_duration: \"4m\"");
|
|
opts.LameDuckDuration.ShouldBe(TimeSpan.FromMinutes(4));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMaxClosedClients opts_test.go:1340 — max_closed_clients parsed.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_parses_max_closed_clients()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("max_closed_clients: 500");
|
|
opts.MaxClosedClients.ShouldBe(500);
|
|
}
|
|
|
|
// ─── Tests: Reload Host Change Rejected ────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadUnsupportedHotSwapping reload_test.go:180
|
|
/// Changing the listen host must be rejected at reload time.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Reload_host_change_rejected()
|
|
{
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}");
|
|
try
|
|
{
|
|
File.WriteAllText(configPath, $"port: {port}\nhost: 127.0.0.1");
|
|
Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow())
|
|
.Message.ShouldContain("Host");
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
// ─── Tests: Reload TLS Settings ────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Reloading with allow_non_tls must succeed and not disconnect existing clients.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Reload_allow_non_tls_setting()
|
|
{
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}");
|
|
try
|
|
{
|
|
WriteConfigAndReload(server, configPath, $"port: {port}\nallow_non_tls: 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);
|
|
}
|
|
}
|
|
|
|
// ─── Tests: Reload Cluster Name Change ─────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: TestConfigReloadClusterName reload_test.go:1893
|
|
/// Adding a cluster block for the first time is a non-reloadable change.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Reload_adding_cluster_block_rejected()
|
|
{
|
|
var clusterPort = GetFreePort();
|
|
var (server, port, cts, configPath) = await StartServerWithConfigAsync("port: {PORT}");
|
|
try
|
|
{
|
|
File.WriteAllText(configPath,
|
|
$"port: {port}\ncluster {{\n name: new-cluster\n host: 127.0.0.1\n port: {clusterPort}\n}}");
|
|
Should.Throw<InvalidOperationException>(() => server.ReloadConfigOrThrow())
|
|
.Message.ShouldContain("Cluster");
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(server, cts, configPath);
|
|
}
|
|
}
|
|
|
|
// ─── Tests: JetStream Options Model ────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// JetStreamOptions must have sensible defaults (StoreDir empty, all limits 0).
|
|
/// Go: server/opts.go JetStreamConfig defaults.
|
|
/// </summary>
|
|
[Fact]
|
|
public void JetStreamOptions_defaults_are_empty_and_unlimited()
|
|
{
|
|
var jsOpts = new JetStreamOptions();
|
|
jsOpts.StoreDir.ShouldBe(string.Empty);
|
|
jsOpts.MaxMemoryStore.ShouldBe(0L);
|
|
jsOpts.MaxFileStore.ShouldBe(0L);
|
|
jsOpts.MaxStreams.ShouldBe(0);
|
|
jsOpts.MaxConsumers.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// ConfigProcessor must correctly parse a jetstream block with store_dir.
|
|
/// Go: server/opts.go parseJetStream.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_parses_jetstream_store_dir()
|
|
{
|
|
var storeDir = Path.Combine(Path.GetTempPath(), $"nats-js-parse-{Guid.NewGuid():N}");
|
|
var opts = ConfigProcessor.ProcessConfig(
|
|
$"jetstream {{\n store_dir: \"{storeDir.Replace("\\", "\\\\")}\"\n}}");
|
|
|
|
opts.JetStream.ShouldNotBeNull();
|
|
opts.JetStream!.StoreDir.ShouldBe(storeDir);
|
|
}
|
|
|
|
// ─── Tests: Reload max_sub_tokens Validation ────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: opts_test.go (max_sub_tokens validation) — ConfigProcessor must reject
|
|
/// max_sub_tokens values that exceed 256.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_rejects_max_sub_tokens_above_256()
|
|
{
|
|
Should.Throw<ConfigProcessorException>(() =>
|
|
ConfigProcessor.ProcessConfig("max_sub_tokens: 300"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// ConfigProcessor must accept max_sub_tokens values of exactly 256.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_accepts_max_sub_tokens_at_boundary_256()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("max_sub_tokens: 256");
|
|
opts.MaxSubTokens.ShouldBe(256);
|
|
}
|
|
|
|
// ─── Tests: server_name with spaces ────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Go: opts_test.go server_name validation — server names containing spaces
|
|
/// must be rejected by the config processor.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConfigProcessor_rejects_server_name_with_spaces()
|
|
{
|
|
Should.Throw<ConfigProcessorException>(() =>
|
|
ConfigProcessor.ProcessConfig("server_name: \"my server\""));
|
|
}
|
|
}
|