Port of Go server/opts.go processConfigFileLine switch. Maps parsed NATS config dictionaries to NatsOptions fields including: - Core options (port, host, server_name, limits, ping, write_deadline) - Logging (debug, trace, logfile, log rotation) - Authorization (single user, users array with permissions) - TLS (cert/key/ca, verify, pinned_certs, handshake_first) - Monitoring (http_port, https_port, http/https listen, base_path) - Lifecycle (lame_duck_duration/grace_period) - Server tags, file paths, system account options Includes error collection (not fail-fast), duration parsing (ms/s/m/h strings and numeric seconds), host:port listen parsing, and 56 tests covering all config sections plus validation edge cases.
505 lines
16 KiB
C#
505 lines
16 KiB
C#
using NATS.Server;
|
|
using NATS.Server.Configuration;
|
|
|
|
namespace NATS.Server.Tests;
|
|
|
|
public class ConfigProcessorTests
|
|
{
|
|
private static string TestDataPath(string fileName) =>
|
|
Path.Combine(AppContext.BaseDirectory, "TestData", fileName);
|
|
|
|
// ─── Basic config ──────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void BasicConf_Port()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.Port.ShouldBe(4222);
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_Host()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.Host.ShouldBe("0.0.0.0");
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_ServerName()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.ServerName.ShouldBe("test-server");
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_MaxPayload()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.MaxPayload.ShouldBe(2 * 1024 * 1024);
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_MaxConnections()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.MaxConnections.ShouldBe(1000);
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_Debug()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.Debug.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_Trace()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.Trace.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_PingInterval()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.PingInterval.ShouldBe(TimeSpan.FromSeconds(30));
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_MaxPingsOut()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.MaxPingsOut.ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_WriteDeadline()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.WriteDeadline.ShouldBe(TimeSpan.FromSeconds(5));
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_MaxSubs()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.MaxSubs.ShouldBe(100);
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_MaxSubTokens()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.MaxSubTokens.ShouldBe(16);
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_MaxControlLine()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.MaxControlLine.ShouldBe(2048);
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_MaxPending()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.MaxPending.ShouldBe(32L * 1024 * 1024);
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_LameDuckDuration()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.LameDuckDuration.ShouldBe(TimeSpan.FromSeconds(60));
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_LameDuckGracePeriod()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.LameDuckGracePeriod.ShouldBe(TimeSpan.FromSeconds(5));
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_MonitorPort()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.MonitorPort.ShouldBe(8222);
|
|
}
|
|
|
|
[Fact]
|
|
public void BasicConf_Logtime()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
|
opts.Logtime.ShouldBeTrue();
|
|
opts.LogtimeUTC.ShouldBeFalse();
|
|
}
|
|
|
|
// ─── Auth config ───────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void AuthConf_SimpleUser()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("auth.conf"));
|
|
opts.Username.ShouldBe("admin");
|
|
opts.Password.ShouldBe("s3cret");
|
|
}
|
|
|
|
[Fact]
|
|
public void AuthConf_AuthTimeout()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("auth.conf"));
|
|
opts.AuthTimeout.ShouldBe(TimeSpan.FromSeconds(5));
|
|
}
|
|
|
|
[Fact]
|
|
public void AuthConf_NoAuthUser()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("auth.conf"));
|
|
opts.NoAuthUser.ShouldBe("guest");
|
|
}
|
|
|
|
[Fact]
|
|
public void AuthConf_UsersArray()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("auth.conf"));
|
|
opts.Users.ShouldNotBeNull();
|
|
opts.Users.Count.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void AuthConf_AliceUser()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("auth.conf"));
|
|
var alice = opts.Users!.First(u => u.Username == "alice");
|
|
alice.Password.ShouldBe("pw1");
|
|
alice.Permissions.ShouldNotBeNull();
|
|
alice.Permissions!.Publish.ShouldNotBeNull();
|
|
alice.Permissions.Publish!.Allow.ShouldNotBeNull();
|
|
alice.Permissions.Publish.Allow!.ShouldContain("foo.>");
|
|
alice.Permissions.Subscribe.ShouldNotBeNull();
|
|
alice.Permissions.Subscribe!.Allow.ShouldNotBeNull();
|
|
alice.Permissions.Subscribe.Allow!.ShouldContain(">");
|
|
}
|
|
|
|
[Fact]
|
|
public void AuthConf_BobUser()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("auth.conf"));
|
|
var bob = opts.Users!.First(u => u.Username == "bob");
|
|
bob.Password.ShouldBe("pw2");
|
|
bob.Permissions.ShouldBeNull();
|
|
}
|
|
|
|
// ─── TLS config ────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void TlsConf_CertFiles()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
|
opts.TlsCert.ShouldBe("/path/to/cert.pem");
|
|
opts.TlsKey.ShouldBe("/path/to/key.pem");
|
|
opts.TlsCaCert.ShouldBe("/path/to/ca.pem");
|
|
}
|
|
|
|
[Fact]
|
|
public void TlsConf_Verify()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
|
opts.TlsVerify.ShouldBeTrue();
|
|
opts.TlsMap.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void TlsConf_Timeout()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
|
opts.TlsTimeout.ShouldBe(TimeSpan.FromSeconds(3));
|
|
}
|
|
|
|
[Fact]
|
|
public void TlsConf_RateLimit()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
|
opts.TlsRateLimit.ShouldBe(100);
|
|
}
|
|
|
|
[Fact]
|
|
public void TlsConf_PinnedCerts()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
|
opts.TlsPinnedCerts.ShouldNotBeNull();
|
|
opts.TlsPinnedCerts!.Count.ShouldBe(1);
|
|
opts.TlsPinnedCerts.ShouldContain("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
|
|
}
|
|
|
|
[Fact]
|
|
public void TlsConf_HandshakeFirst()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
|
opts.TlsHandshakeFirst.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void TlsConf_AllowNonTls()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
|
opts.AllowNonTls.ShouldBeFalse();
|
|
}
|
|
|
|
// ─── Full config ───────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void FullConf_CoreOptions()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
|
opts.Port.ShouldBe(4222);
|
|
opts.Host.ShouldBe("0.0.0.0");
|
|
opts.ServerName.ShouldBe("full-test");
|
|
opts.ClientAdvertise.ShouldBe("nats://public.example.com:4222");
|
|
}
|
|
|
|
[Fact]
|
|
public void FullConf_Limits()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
|
opts.MaxPayload.ShouldBe(1024 * 1024);
|
|
opts.MaxControlLine.ShouldBe(4096);
|
|
opts.MaxConnections.ShouldBe(65536);
|
|
opts.MaxPending.ShouldBe(64L * 1024 * 1024);
|
|
opts.MaxSubs.ShouldBe(0);
|
|
opts.MaxSubTokens.ShouldBe(0);
|
|
opts.MaxTracedMsgLen.ShouldBe(1024);
|
|
opts.DisableSublistCache.ShouldBeFalse();
|
|
opts.MaxClosedClients.ShouldBe(5000);
|
|
}
|
|
|
|
[Fact]
|
|
public void FullConf_Logging()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
|
opts.Debug.ShouldBeFalse();
|
|
opts.Trace.ShouldBeFalse();
|
|
opts.TraceVerbose.ShouldBeFalse();
|
|
opts.Logtime.ShouldBeTrue();
|
|
opts.LogtimeUTC.ShouldBeFalse();
|
|
opts.LogFile.ShouldBe("/var/log/nats.log");
|
|
opts.LogSizeLimit.ShouldBe(100L * 1024 * 1024);
|
|
opts.LogMaxFiles.ShouldBe(5);
|
|
}
|
|
|
|
[Fact]
|
|
public void FullConf_Monitoring()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
|
opts.MonitorPort.ShouldBe(8222);
|
|
opts.MonitorBasePath.ShouldBe("/nats");
|
|
}
|
|
|
|
[Fact]
|
|
public void FullConf_Files()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
|
opts.PidFile.ShouldBe("/var/run/nats.pid");
|
|
opts.PortsFileDir.ShouldBe("/var/run");
|
|
}
|
|
|
|
[Fact]
|
|
public void FullConf_Lifecycle()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
|
opts.LameDuckDuration.ShouldBe(TimeSpan.FromMinutes(2));
|
|
opts.LameDuckGracePeriod.ShouldBe(TimeSpan.FromSeconds(10));
|
|
}
|
|
|
|
[Fact]
|
|
public void FullConf_Tags()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
|
opts.Tags.ShouldNotBeNull();
|
|
opts.Tags!["region"].ShouldBe("us-east");
|
|
opts.Tags["env"].ShouldBe("production");
|
|
}
|
|
|
|
[Fact]
|
|
public void FullConf_Auth()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
|
opts.Username.ShouldBe("admin");
|
|
opts.Password.ShouldBe("secret");
|
|
opts.AuthTimeout.ShouldBe(TimeSpan.FromSeconds(2));
|
|
}
|
|
|
|
[Fact]
|
|
public void FullConf_Tls()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
|
opts.TlsCert.ShouldBe("/path/to/cert.pem");
|
|
opts.TlsKey.ShouldBe("/path/to/key.pem");
|
|
opts.TlsCaCert.ShouldBe("/path/to/ca.pem");
|
|
opts.TlsVerify.ShouldBeTrue();
|
|
opts.TlsTimeout.ShouldBe(TimeSpan.FromSeconds(2));
|
|
opts.TlsHandshakeFirst.ShouldBeTrue();
|
|
}
|
|
|
|
// ─── Listen combined format ────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ListenCombined_HostAndPort()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("listen: \"10.0.0.1:5222\"");
|
|
opts.Host.ShouldBe("10.0.0.1");
|
|
opts.Port.ShouldBe(5222);
|
|
}
|
|
|
|
[Fact]
|
|
public void ListenCombined_PortOnly()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("listen: \":5222\"");
|
|
opts.Port.ShouldBe(5222);
|
|
}
|
|
|
|
[Fact]
|
|
public void ListenCombined_BarePort()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("listen: 5222");
|
|
opts.Port.ShouldBe(5222);
|
|
}
|
|
|
|
// ─── HTTP combined format ──────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void HttpCombined_HostAndPort()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("http: \"10.0.0.1:8333\"");
|
|
opts.MonitorHost.ShouldBe("10.0.0.1");
|
|
opts.MonitorPort.ShouldBe(8333);
|
|
}
|
|
|
|
[Fact]
|
|
public void HttpsCombined_HostAndPort()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("https: \"10.0.0.1:8444\"");
|
|
opts.MonitorHost.ShouldBe("10.0.0.1");
|
|
opts.MonitorHttpsPort.ShouldBe(8444);
|
|
}
|
|
|
|
// ─── Duration as number ────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void DurationAsNumber_TreatedAsSeconds()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("ping_interval: 60");
|
|
opts.PingInterval.ShouldBe(TimeSpan.FromSeconds(60));
|
|
}
|
|
|
|
[Fact]
|
|
public void DurationAsString_Milliseconds()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("write_deadline: \"500ms\"");
|
|
opts.WriteDeadline.ShouldBe(TimeSpan.FromMilliseconds(500));
|
|
}
|
|
|
|
[Fact]
|
|
public void DurationAsString_Hours()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("ping_interval: \"1h\"");
|
|
opts.PingInterval.ShouldBe(TimeSpan.FromHours(1));
|
|
}
|
|
|
|
// ─── Unknown keys ──────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void UnknownKeys_SilentlyIgnored()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("""
|
|
port: 4222
|
|
cluster { name: "my-cluster" }
|
|
jetstream { store_dir: "/tmp/js" }
|
|
unknown_key: "whatever"
|
|
""");
|
|
opts.Port.ShouldBe(4222);
|
|
}
|
|
|
|
// ─── Server name validation ────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ServerNameWithSpaces_ReportsError()
|
|
{
|
|
var ex = Should.Throw<ConfigProcessorException>(() =>
|
|
ConfigProcessor.ProcessConfig("server_name: \"my server\""));
|
|
ex.Errors.ShouldContain(e => e.Contains("server_name cannot contain spaces"));
|
|
}
|
|
|
|
// ─── Max sub tokens validation ─────────────────────────────────
|
|
|
|
[Fact]
|
|
public void MaxSubTokens_ExceedsLimit_ReportsError()
|
|
{
|
|
var ex = Should.Throw<ConfigProcessorException>(() =>
|
|
ConfigProcessor.ProcessConfig("max_sub_tokens: 300"));
|
|
ex.Errors.ShouldContain(e => e.Contains("max_sub_tokens cannot exceed 256"));
|
|
}
|
|
|
|
// ─── ProcessConfig from string ─────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ProcessConfig_FromString()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("""
|
|
port: 9222
|
|
host: "127.0.0.1"
|
|
debug: true
|
|
""");
|
|
opts.Port.ShouldBe(9222);
|
|
opts.Host.ShouldBe("127.0.0.1");
|
|
opts.Debug.ShouldBeTrue();
|
|
}
|
|
|
|
// ─── TraceVerbose sets Trace ────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void TraceVerbose_AlsoSetsTrace()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfig("trace_verbose: true");
|
|
opts.TraceVerbose.ShouldBeTrue();
|
|
opts.Trace.ShouldBeTrue();
|
|
}
|
|
|
|
// ─── Error collection (not fail-fast) ──────────────────────────
|
|
|
|
[Fact]
|
|
public void MultipleErrors_AllCollected()
|
|
{
|
|
var ex = Should.Throw<ConfigProcessorException>(() =>
|
|
ConfigProcessor.ProcessConfig("""
|
|
server_name: "bad name"
|
|
max_sub_tokens: 999
|
|
"""));
|
|
ex.Errors.Count.ShouldBe(2);
|
|
ex.Errors.ShouldContain(e => e.Contains("server_name"));
|
|
ex.Errors.ShouldContain(e => e.Contains("max_sub_tokens"));
|
|
}
|
|
|
|
// ─── ConfigFile path tracking ──────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ProcessConfigFile_SetsConfigFilePath()
|
|
{
|
|
var path = TestDataPath("basic.conf");
|
|
var opts = ConfigProcessor.ProcessConfigFile(path);
|
|
opts.ConfigFile.ShouldBe(path);
|
|
}
|
|
|
|
// ─── HasTls derived property ───────────────────────────────────
|
|
|
|
[Fact]
|
|
public void HasTls_TrueWhenCertAndKeySet()
|
|
{
|
|
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
|
opts.HasTls.ShouldBeTrue();
|
|
}
|
|
}
|