feat: add config processor mapping parsed config to NatsOptions

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.
This commit is contained in:
Joseph Doherty
2026-02-23 04:47:54 -05:00
parent 5219f77f9b
commit 8a2ded8e48
7 changed files with 1292 additions and 0 deletions

View File

@@ -0,0 +1,504 @@
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();
}
}