docs: add design doc for SYSTEM and ACCOUNT connection types
Covers 6 implementation layers: ClientKind enum + INatsClient interface, event infrastructure with Channel<T>, system event publishing, request-reply monitoring services, import/export model with ACCOUNT client, and response routing with latency tracking.
This commit is contained in:
76
tests/NATS.Server.Tests/ConfigIntegrationTests.cs
Normal file
76
tests/NATS.Server.Tests/ConfigIntegrationTests.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using NATS.Server.Configuration;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class ConfigIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Server_WithConfigFile_LoadsOptionsFromFile()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), $"nats_test_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(dir);
|
||||
try
|
||||
{
|
||||
var confPath = Path.Combine(dir, "test.conf");
|
||||
File.WriteAllText(confPath, "port: 14222\nmax_payload: 2mb\ndebug: true");
|
||||
|
||||
var opts = ConfigProcessor.ProcessConfigFile(confPath);
|
||||
opts.Port.ShouldBe(14222);
|
||||
opts.MaxPayload.ShouldBe(2 * 1024 * 1024);
|
||||
opts.Debug.ShouldBeTrue();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(dir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Server_CliOverridesConfig()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), $"nats_test_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(dir);
|
||||
try
|
||||
{
|
||||
var confPath = Path.Combine(dir, "test.conf");
|
||||
File.WriteAllText(confPath, "port: 14222\ndebug: true");
|
||||
|
||||
var opts = ConfigProcessor.ProcessConfigFile(confPath);
|
||||
opts.Port.ShouldBe(14222);
|
||||
|
||||
// Simulate CLI override: user passed -p 5222 on command line
|
||||
var cliSnapshot = new NatsOptions { Port = 5222 };
|
||||
var cliFlags = new HashSet<string> { "Port" };
|
||||
ConfigReloader.MergeCliOverrides(opts, cliSnapshot, cliFlags);
|
||||
|
||||
opts.Port.ShouldBe(5222);
|
||||
opts.Debug.ShouldBeTrue(); // Config file value preserved
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(dir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reload_ChangingPort_ReturnsError()
|
||||
{
|
||||
var oldOpts = new NatsOptions { Port = 4222 };
|
||||
var newOpts = new NatsOptions { Port = 5222 };
|
||||
var changes = ConfigReloader.Diff(oldOpts, newOpts);
|
||||
var errors = ConfigReloader.Validate(changes);
|
||||
errors.Count.ShouldBeGreaterThan(0);
|
||||
errors[0].ShouldContain("Port");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reload_ChangingDebug_IsValid()
|
||||
{
|
||||
var oldOpts = new NatsOptions { Debug = false };
|
||||
var newOpts = new NatsOptions { Debug = true };
|
||||
var changes = ConfigReloader.Diff(oldOpts, newOpts);
|
||||
var errors = ConfigReloader.Validate(changes);
|
||||
errors.ShouldBeEmpty();
|
||||
changes.ShouldContain(c => c.IsLoggingChange);
|
||||
}
|
||||
}
|
||||
504
tests/NATS.Server.Tests/ConfigProcessorTests.cs
Normal file
504
tests/NATS.Server.Tests/ConfigProcessorTests.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
89
tests/NATS.Server.Tests/ConfigReloadTests.cs
Normal file
89
tests/NATS.Server.Tests/ConfigReloadTests.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using NATS.Server;
|
||||
using NATS.Server.Auth;
|
||||
using NATS.Server.Configuration;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class ConfigReloadTests
|
||||
{
|
||||
[Fact]
|
||||
public void Diff_NoChanges_ReturnsEmpty()
|
||||
{
|
||||
var old = new NatsOptions { Port = 4222, Debug = true };
|
||||
var @new = new NatsOptions { Port = 4222, Debug = true };
|
||||
var changes = ConfigReloader.Diff(old, @new);
|
||||
changes.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_ReloadableChange_ReturnsChange()
|
||||
{
|
||||
var old = new NatsOptions { Debug = false };
|
||||
var @new = new NatsOptions { Debug = true };
|
||||
var changes = ConfigReloader.Diff(old, @new);
|
||||
changes.Count.ShouldBe(1);
|
||||
changes[0].Name.ShouldBe("Debug");
|
||||
changes[0].IsLoggingChange.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_NonReloadableChange_ReturnsNonReloadableChange()
|
||||
{
|
||||
var old = new NatsOptions { Port = 4222 };
|
||||
var @new = new NatsOptions { Port = 5222 };
|
||||
var changes = ConfigReloader.Diff(old, @new);
|
||||
changes.Count.ShouldBe(1);
|
||||
changes[0].IsNonReloadable.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_MultipleChanges_ReturnsAll()
|
||||
{
|
||||
var old = new NatsOptions { Debug = false, MaxPayload = 1024 };
|
||||
var @new = new NatsOptions { Debug = true, MaxPayload = 2048 };
|
||||
var changes = ConfigReloader.Diff(old, @new);
|
||||
changes.Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_AuthChange_MarkedCorrectly()
|
||||
{
|
||||
var old = new NatsOptions { Username = "alice" };
|
||||
var @new = new NatsOptions { Username = "bob" };
|
||||
var changes = ConfigReloader.Diff(old, @new);
|
||||
changes[0].IsAuthChange.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_TlsChange_MarkedCorrectly()
|
||||
{
|
||||
var old = new NatsOptions { TlsCert = "/old/cert.pem" };
|
||||
var @new = new NatsOptions { TlsCert = "/new/cert.pem" };
|
||||
var changes = ConfigReloader.Diff(old, @new);
|
||||
changes[0].IsTlsChange.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_NonReloadableChanges_ReturnsErrors()
|
||||
{
|
||||
var changes = new List<IConfigChange>
|
||||
{
|
||||
new ConfigChange("Port", isNonReloadable: true),
|
||||
};
|
||||
var errors = ConfigReloader.Validate(changes);
|
||||
errors.Count.ShouldBe(1);
|
||||
errors[0].ShouldContain("Port");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeWithCli_CliOverridesConfig()
|
||||
{
|
||||
var fromConfig = new NatsOptions { Port = 5222, Debug = true };
|
||||
var cliFlags = new HashSet<string> { "Port" };
|
||||
var cliValues = new NatsOptions { Port = 4222 };
|
||||
|
||||
ConfigReloader.MergeCliOverrides(fromConfig, cliValues, cliFlags);
|
||||
fromConfig.Port.ShouldBe(4222); // CLI wins
|
||||
fromConfig.Debug.ShouldBeTrue(); // config value kept (not in CLI)
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@
|
||||
<Using Include="Shouldly" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="TestData\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\NATS.Server\NATS.Server.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
221
tests/NATS.Server.Tests/NatsConfLexerTests.cs
Normal file
221
tests/NATS.Server.Tests/NatsConfLexerTests.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
using NATS.Server.Configuration;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class NatsConfLexerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Lex_SimpleKeyStringValue_ReturnsKeyAndString()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("foo = \"bar\"").ToList();
|
||||
tokens[0].Type.ShouldBe(TokenType.Key);
|
||||
tokens[0].Value.ShouldBe("foo");
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
tokens[1].Value.ShouldBe("bar");
|
||||
tokens[2].Type.ShouldBe(TokenType.Eof);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_SingleQuotedString_ReturnsString()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("foo = 'bar'").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
tokens[1].Value.ShouldBe("bar");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_IntegerValue_ReturnsInteger()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("port = 4222").ToList();
|
||||
tokens[0].Type.ShouldBe(TokenType.Key);
|
||||
tokens[0].Value.ShouldBe("port");
|
||||
tokens[1].Type.ShouldBe(TokenType.Integer);
|
||||
tokens[1].Value.ShouldBe("4222");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_IntegerWithSuffix_ReturnsInteger()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("size = 64mb").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.Integer);
|
||||
tokens[1].Value.ShouldBe("64mb");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_BooleanValues_ReturnsBool()
|
||||
{
|
||||
foreach (var val in new[] { "true", "false", "yes", "no", "on", "off" })
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize($"flag = {val}").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.Bool);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_FloatValue_ReturnsFloat()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("rate = 2.5").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.Float);
|
||||
tokens[1].Value.ShouldBe("2.5");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_NegativeNumber_ReturnsInteger()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("offset = -10").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.Integer);
|
||||
tokens[1].Value.ShouldBe("-10");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_DatetimeValue_ReturnsDatetime()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("ts = 2024-01-15T10:30:00Z").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.DateTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_HashComment_IsIgnored()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("# this is a comment\nfoo = 1").ToList();
|
||||
var keys = tokens.Where(t => t.Type == TokenType.Key).ToList();
|
||||
keys.Count.ShouldBe(1);
|
||||
keys[0].Value.ShouldBe("foo");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_SlashComment_IsIgnored()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("// comment\nfoo = 1").ToList();
|
||||
var keys = tokens.Where(t => t.Type == TokenType.Key).ToList();
|
||||
keys.Count.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_MapBlock_ReturnsMapStartEnd()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("auth { user: admin }").ToList();
|
||||
tokens[0].Type.ShouldBe(TokenType.Key);
|
||||
tokens[0].Value.ShouldBe("auth");
|
||||
tokens[1].Type.ShouldBe(TokenType.MapStart);
|
||||
tokens[2].Type.ShouldBe(TokenType.Key);
|
||||
tokens[2].Value.ShouldBe("user");
|
||||
tokens[3].Type.ShouldBe(TokenType.String);
|
||||
tokens[3].Value.ShouldBe("admin");
|
||||
tokens[4].Type.ShouldBe(TokenType.MapEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_Array_ReturnsArrayStartEnd()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("items = [1, 2, 3]").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.ArrayStart);
|
||||
tokens[2].Type.ShouldBe(TokenType.Integer);
|
||||
tokens[2].Value.ShouldBe("1");
|
||||
tokens[5].Type.ShouldBe(TokenType.ArrayEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_Variable_ReturnsVariable()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("secret = $MY_VAR").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.Variable);
|
||||
tokens[1].Value.ShouldBe("MY_VAR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_Include_ReturnsInclude()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("include \"auth.conf\"").ToList();
|
||||
tokens[0].Type.ShouldBe(TokenType.Include);
|
||||
tokens[0].Value.ShouldBe("auth.conf");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_EscapeSequences_AreProcessed()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("msg = \"hello\\tworld\\n\"").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
tokens[1].Value.ShouldBe("hello\tworld\n");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_HexEscape_IsProcessed()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("val = \"\\x41\\x42\"").ToList();
|
||||
tokens[1].Value.ShouldBe("AB");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_ColonSeparator_Works()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("foo: bar").ToList();
|
||||
tokens[0].Type.ShouldBe(TokenType.Key);
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_WhitespaceSeparator_Works()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("foo bar").ToList();
|
||||
tokens[0].Type.ShouldBe(TokenType.Key);
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_SemicolonTerminator_IsHandled()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("foo = 1; bar = 2").ToList();
|
||||
var keys = tokens.Where(t => t.Type == TokenType.Key).ToList();
|
||||
keys.Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_EmptyInput_ReturnsEof()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("").ToList();
|
||||
tokens.Count.ShouldBe(1);
|
||||
tokens[0].Type.ShouldBe(TokenType.Eof);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_BlockString_ReturnsString()
|
||||
{
|
||||
var input = "desc (\nthis is\na block\n)\n";
|
||||
var tokens = NatsConfLexer.Tokenize(input).ToList();
|
||||
tokens[0].Type.ShouldBe(TokenType.Key);
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_IPAddress_ReturnsString()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("host = 127.0.0.1").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
tokens[1].Value.ShouldBe("127.0.0.1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_TrackLineNumbers()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("a = 1\nb = 2\nc = 3").ToList();
|
||||
tokens[0].Line.ShouldBe(1); // a
|
||||
tokens[2].Line.ShouldBe(2); // b
|
||||
tokens[4].Line.ShouldBe(3); // c
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_UnterminatedString_ReturnsError()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("foo = \"unterminated").ToList();
|
||||
tokens.ShouldContain(t => t.Type == TokenType.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_StringStartingWithDigit_TreatedAsString()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("foo = 3xyz").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
tokens[1].Value.ShouldBe("3xyz");
|
||||
}
|
||||
}
|
||||
184
tests/NATS.Server.Tests/NatsConfParserTests.cs
Normal file
184
tests/NATS.Server.Tests/NatsConfParserTests.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using NATS.Server.Configuration;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class NatsConfParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_SimpleTopLevel_ReturnsCorrectTypes()
|
||||
{
|
||||
var result = NatsConfParser.Parse("foo = '1'; bar = 2.2; baz = true; boo = 22");
|
||||
result["foo"].ShouldBe("1");
|
||||
result["bar"].ShouldBe(2.2);
|
||||
result["baz"].ShouldBe(true);
|
||||
result["boo"].ShouldBe(22L);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Booleans_AllVariants()
|
||||
{
|
||||
foreach (var (input, expected) in new[] {
|
||||
("true", true), ("TRUE", true), ("yes", true), ("on", true),
|
||||
("false", false), ("FALSE", false), ("no", false), ("off", false)
|
||||
})
|
||||
{
|
||||
var result = NatsConfParser.Parse($"flag = {input}");
|
||||
result["flag"].ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_IntegerWithSuffix_AppliesMultiplier()
|
||||
{
|
||||
var result = NatsConfParser.Parse("a = 1k; b = 2mb; c = 3gb; d = 4kb");
|
||||
result["a"].ShouldBe(1000L);
|
||||
result["b"].ShouldBe(2L * 1024 * 1024);
|
||||
result["c"].ShouldBe(3L * 1024 * 1024 * 1024);
|
||||
result["d"].ShouldBe(4L * 1024);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_NestedMap_ReturnsDictionary()
|
||||
{
|
||||
var result = NatsConfParser.Parse("auth { user: admin, pass: secret }");
|
||||
var auth = result["auth"].ShouldBeOfType<Dictionary<string, object?>>();
|
||||
auth["user"].ShouldBe("admin");
|
||||
auth["pass"].ShouldBe("secret");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Array_ReturnsList()
|
||||
{
|
||||
var result = NatsConfParser.Parse("items = [1, 2, 3]");
|
||||
var items = result["items"].ShouldBeOfType<List<object?>>();
|
||||
items.Count.ShouldBe(3);
|
||||
items[0].ShouldBe(1L);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Variable_ResolvesFromContext()
|
||||
{
|
||||
var result = NatsConfParser.Parse("index = 22\nfoo = $index");
|
||||
result["foo"].ShouldBe(22L);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_NestedVariable_UsesBlockScope()
|
||||
{
|
||||
var input = "index = 22\nnest {\n index = 11\n foo = $index\n}\nbar = $index";
|
||||
var result = NatsConfParser.Parse(input);
|
||||
var nest = result["nest"].ShouldBeOfType<Dictionary<string, object?>>();
|
||||
nest["foo"].ShouldBe(11L);
|
||||
result["bar"].ShouldBe(22L);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_EnvironmentVariable_ResolvesFromEnv()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("NATS_TEST_VAR_12345", "hello");
|
||||
try
|
||||
{
|
||||
var result = NatsConfParser.Parse("val = $NATS_TEST_VAR_12345");
|
||||
result["val"].ShouldBe("hello");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("NATS_TEST_VAR_12345", null);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_UndefinedVariable_Throws()
|
||||
{
|
||||
Should.Throw<FormatException>(() =>
|
||||
NatsConfParser.Parse("val = $UNDEFINED_VAR_XYZZY_99999"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_IncludeDirective_MergesFile()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), $"nats_test_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(dir);
|
||||
try
|
||||
{
|
||||
File.WriteAllText(Path.Combine(dir, "main.conf"), "port = 4222\ninclude \"sub.conf\"");
|
||||
File.WriteAllText(Path.Combine(dir, "sub.conf"), "host = \"localhost\"");
|
||||
var result = NatsConfParser.ParseFile(Path.Combine(dir, "main.conf"));
|
||||
result["port"].ShouldBe(4222L);
|
||||
result["host"].ShouldBe("localhost");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(dir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_MultipleKeySeparators_AllWork()
|
||||
{
|
||||
var r1 = NatsConfParser.Parse("a = 1");
|
||||
var r2 = NatsConfParser.Parse("a : 1");
|
||||
var r3 = NatsConfParser.Parse("a 1");
|
||||
r1["a"].ShouldBe(1L);
|
||||
r2["a"].ShouldBe(1L);
|
||||
r3["a"].ShouldBe(1L);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ErrorOnInvalidInput_Throws()
|
||||
{
|
||||
Should.Throw<FormatException>(() => NatsConfParser.Parse("= invalid"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_CommentsInsideBlocks_AreIgnored()
|
||||
{
|
||||
var input = "auth {\n # comment\n user: admin\n // another comment\n pass: secret\n}";
|
||||
var result = NatsConfParser.Parse(input);
|
||||
var auth = result["auth"].ShouldBeOfType<Dictionary<string, object?>>();
|
||||
auth["user"].ShouldBe("admin");
|
||||
auth["pass"].ShouldBe("secret");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ArrayOfMaps_Works()
|
||||
{
|
||||
var input = "users = [\n { user: alice, pass: pw1 }\n { user: bob, pass: pw2 }\n]";
|
||||
var result = NatsConfParser.Parse(input);
|
||||
var users = result["users"].ShouldBeOfType<List<object?>>();
|
||||
users.Count.ShouldBe(2);
|
||||
var first = users[0].ShouldBeOfType<Dictionary<string, object?>>();
|
||||
first["user"].ShouldBe("alice");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_BcryptPassword_HandledAsString()
|
||||
{
|
||||
var input = "pass = $2a$04$P/.bd.7unw9Ew7yWJqXsl.f4oNRLQGvadEL2YnqQXbbb.IVQajRdK";
|
||||
var result = NatsConfParser.Parse(input);
|
||||
((string)result["pass"]!).ShouldStartWith("$2a$");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseFile_WithDigest_ReturnsStableHash()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), $"nats_test_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(dir);
|
||||
try
|
||||
{
|
||||
var conf = Path.Combine(dir, "test.conf");
|
||||
File.WriteAllText(conf, "port = 4222\nhost = \"localhost\"");
|
||||
var (result, digest) = NatsConfParser.ParseFileWithDigest(conf);
|
||||
result["port"].ShouldBe(4222L);
|
||||
digest.ShouldStartWith("sha256:");
|
||||
digest.Length.ShouldBeGreaterThan(10);
|
||||
|
||||
var (_, digest2) = NatsConfParser.ParseFileWithDigest(conf);
|
||||
digest2.ShouldBe(digest);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(dir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,22 @@ public class NatsOptionsTests
|
||||
opts.LogSizeLimit.ShouldBe(0L);
|
||||
opts.Tags.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void New_fields_have_correct_defaults()
|
||||
{
|
||||
var opts = new NatsOptions();
|
||||
opts.ClientAdvertise.ShouldBeNull();
|
||||
opts.TraceVerbose.ShouldBeFalse();
|
||||
opts.MaxTracedMsgLen.ShouldBe(0);
|
||||
opts.DisableSublistCache.ShouldBeFalse();
|
||||
opts.ConnectErrorReports.ShouldBe(3600);
|
||||
opts.ReconnectErrorReports.ShouldBe(1);
|
||||
opts.NoHeaderSupport.ShouldBeFalse();
|
||||
opts.MaxClosedClients.ShouldBe(10_000);
|
||||
opts.NoSystemAccount.ShouldBeFalse();
|
||||
opts.SystemAccount.ShouldBeNull();
|
||||
}
|
||||
}
|
||||
|
||||
public class LogOverrideTests
|
||||
|
||||
11
tests/NATS.Server.Tests/TestData/auth.conf
Normal file
11
tests/NATS.Server.Tests/TestData/auth.conf
Normal file
@@ -0,0 +1,11 @@
|
||||
authorization {
|
||||
user: admin
|
||||
password: "s3cret"
|
||||
timeout: 5
|
||||
|
||||
users = [
|
||||
{ user: alice, password: "pw1", permissions: { publish: { allow: ["foo.>"] }, subscribe: { allow: [">"] } } }
|
||||
{ user: bob, password: "pw2" }
|
||||
]
|
||||
}
|
||||
no_auth_user: "guest"
|
||||
19
tests/NATS.Server.Tests/TestData/basic.conf
Normal file
19
tests/NATS.Server.Tests/TestData/basic.conf
Normal file
@@ -0,0 +1,19 @@
|
||||
port: 4222
|
||||
host: "0.0.0.0"
|
||||
server_name: "test-server"
|
||||
max_payload: 2mb
|
||||
max_connections: 1000
|
||||
debug: true
|
||||
trace: false
|
||||
logtime: true
|
||||
logtime_utc: false
|
||||
ping_interval: "30s"
|
||||
ping_max: 3
|
||||
write_deadline: "5s"
|
||||
max_subs: 100
|
||||
max_sub_tokens: 16
|
||||
max_control_line: 2048
|
||||
max_pending: 32mb
|
||||
lame_duck_duration: "60s"
|
||||
lame_duck_grace_period: "5s"
|
||||
http_port: 8222
|
||||
57
tests/NATS.Server.Tests/TestData/full.conf
Normal file
57
tests/NATS.Server.Tests/TestData/full.conf
Normal file
@@ -0,0 +1,57 @@
|
||||
# Full configuration with all supported options
|
||||
port: 4222
|
||||
host: "0.0.0.0"
|
||||
server_name: "full-test"
|
||||
client_advertise: "nats://public.example.com:4222"
|
||||
|
||||
max_payload: 1mb
|
||||
max_control_line: 4096
|
||||
max_connections: 65536
|
||||
max_pending: 64mb
|
||||
write_deadline: "10s"
|
||||
max_subs: 0
|
||||
max_sub_tokens: 0
|
||||
max_traced_msg_len: 1024
|
||||
disable_sublist_cache: false
|
||||
max_closed_clients: 5000
|
||||
|
||||
ping_interval: "2m"
|
||||
ping_max: 2
|
||||
|
||||
debug: false
|
||||
trace: false
|
||||
trace_verbose: false
|
||||
logtime: true
|
||||
logtime_utc: false
|
||||
logfile: "/var/log/nats.log"
|
||||
log_size_limit: 100mb
|
||||
log_max_num: 5
|
||||
|
||||
http_port: 8222
|
||||
http_base_path: "/nats"
|
||||
|
||||
pidfile: "/var/run/nats.pid"
|
||||
ports_file_dir: "/var/run"
|
||||
|
||||
lame_duck_duration: "2m"
|
||||
lame_duck_grace_period: "10s"
|
||||
|
||||
server_tags {
|
||||
region: "us-east"
|
||||
env: "production"
|
||||
}
|
||||
|
||||
authorization {
|
||||
user: admin
|
||||
password: "secret"
|
||||
timeout: 2
|
||||
}
|
||||
|
||||
tls {
|
||||
cert_file: "/path/to/cert.pem"
|
||||
key_file: "/path/to/key.pem"
|
||||
ca_file: "/path/to/ca.pem"
|
||||
verify: true
|
||||
timeout: 2
|
||||
handshake_first: true
|
||||
}
|
||||
12
tests/NATS.Server.Tests/TestData/tls.conf
Normal file
12
tests/NATS.Server.Tests/TestData/tls.conf
Normal file
@@ -0,0 +1,12 @@
|
||||
tls {
|
||||
cert_file: "/path/to/cert.pem"
|
||||
key_file: "/path/to/key.pem"
|
||||
ca_file: "/path/to/ca.pem"
|
||||
verify: true
|
||||
verify_and_map: true
|
||||
timeout: 3
|
||||
connection_rate_limit: 100
|
||||
pinned_certs: ["abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"]
|
||||
handshake_first: true
|
||||
}
|
||||
allow_non_tls: false
|
||||
Reference in New Issue
Block a user