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(() => 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(() => 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(() => 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(); } }