diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ServerOptionsTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ServerOptionsTests.cs index c94586c..dabe2ee 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ServerOptionsTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ServerOptionsTests.cs @@ -1,11 +1,38 @@ using Shouldly; using ZB.MOM.NatsNet.Server; using ZB.MOM.NatsNet.Server.Auth; +using System.Linq; namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog; public sealed class ServerOptionsTests { + private static Dictionary Map(params (string Key, object? Value)[] entries) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var (key, value) in entries) + map[key] = value; + return map; + } + + private static List Arr(params object?[] entries) => [.. entries]; + + private static string CreateJsonConfig(string json) + { + var path = Path.GetTempFileName(); + File.WriteAllText(path, json); + return path; + } + + private static T ReadProperty(object target, string propertyName) + { + var property = target.GetType().GetProperty(propertyName); + property.ShouldNotBeNull(); + var value = property.GetValue(target); + value.ShouldNotBeNull(); + return (T)value; + } + [Fact] public void DeepCopyURLs_WithEntries_ReturnsIndependentCopy() { @@ -271,6 +298,515 @@ public sealed class ServerOptionsTests options.Gateway.AuthTimeout.ShouldBe(3); } + [Fact] // T:2514 + public void ConfigFile_ShouldSucceed() + { + var path = CreateJsonConfig(""" + { + "Host": "127.0.0.1", + "Port": 4242, + "SystemAccount": "$SYS" + } + """); + + try + { + var opts = ServerOptions.ProcessConfigFile(path); + opts.Host.ShouldBe("127.0.0.1"); + opts.Port.ShouldBe(4242); + opts.SystemAccount.ShouldBe("$SYS"); + } + finally + { + File.Delete(path); + } + } + + [Fact] // T:2517 + public void RouteFlagOverride_ShouldSucceed() + { + var merged = ServerOptions.MergeOptions( + new ServerOptions(), + new ServerOptions { RoutesStr = "nats-route://ruser:top_secret@127.0.0.1:8246" }); + + merged.RoutesStr.ShouldBe("nats-route://ruser:top_secret@127.0.0.1:8246"); + merged.Routes.Count.ShouldBe(1); + merged.Routes[0].ToString().ShouldBe("nats-route://ruser:top_secret@127.0.0.1:8246/"); + } + + [Fact] // T:2519 + public void RouteFlagOverrideWithMultiple_ShouldSucceed() + { + var routes = "nats-route://ruser:top_secret@127.0.0.1:8246, nats-route://ruser:top_secret@127.0.0.1:8266"; + var merged = ServerOptions.MergeOptions(new ServerOptions(), new ServerOptions { RoutesStr = routes }); + + merged.RoutesStr.ShouldBe(routes); + merged.Routes.Count.ShouldBe(2); + } + + [Fact] // T:2520 + public void DynamicPortOnListen_ShouldSucceed() + { + var (host, port) = ServerOptions.ParseListen("127.0.0.1:-1"); + host.ShouldBe("127.0.0.1"); + port.ShouldBe(-1); + } + + [Fact] // T:2521 + public void ListenConfig_ShouldSucceed() + { + var opts = new ServerOptions(); + var error = opts.ProcessConfigString(""" + { + "listen": "10.0.1.22:4422", + "cluster": { + "listen": "127.0.0.1:4244" + } + } + """); + + error.ShouldBeNull(); + opts.SetBaselineOptions(); + opts.Host.ShouldBe("10.0.1.22"); + opts.Port.ShouldBe(4422); + opts.Cluster.Host.ShouldBe("127.0.0.1"); + opts.Cluster.Port.ShouldBe(4244); + } + + [Fact] // T:2522 + public void ListenPortOnlyConfig_ShouldSucceed() + { + var opts = new ServerOptions(); + var error = opts.ProcessConfigString(""" + { + "listen": 8922 + } + """); + + error.ShouldBeNull(); + opts.SetBaselineOptions(); + opts.Host.ShouldBe(ServerConstants.DefaultHost); + opts.Port.ShouldBe(8922); + } + + [Fact] // T:2523 + public void ListenPortWithColonConfig_ShouldSucceed() + { + var (host, port) = ServerOptions.ParseListen("127.0.0.1:8922"); + host.ShouldBe("127.0.0.1"); + port.ShouldBe(8922); + } + + [Fact] // T:2525 + public void MultipleUsersConfig_ShouldSucceed() + { + var (nkeys, users, error) = ServerOptions.ParseUsers( + Arr( + Map(("user", "alice"), ("password", "foo")), + Map(("user", "bob"), ("password", "bar")))); + + error.ShouldBeNull(); + users.Count.ShouldBe(2); + nkeys.ShouldBeEmpty(); + } + + [Fact] // T:2526 + public void AuthorizationConfig_ShouldSucceed() + { + var (auth, error) = ServerOptions.ParseAuthorization( + Map(("users", Arr( + Map( + ("user", "alice"), + ("password", "pwd"), + ("permissions", Map(("publish", "*"), ("subscribe", ">")))), + Map(("user", "bob"), ("password", "pwd")))))); + + error.ShouldBeNull(); + auth.ShouldNotBeNull(); + auth.Users.Count.ShouldBe(2); + + var alice = auth.Users.Single(u => u.Username == "alice"); + alice.Permissions.ShouldNotBeNull(); + alice.Permissions.Publish.ShouldNotBeNull(); + alice.Permissions.Publish.Allow.ShouldContain("*"); + alice.Permissions.Subscribe.ShouldNotBeNull(); + alice.Permissions.Subscribe.Allow.ShouldContain(">"); + } + + [Fact] // T:2527 + public void NewStyleAuthorizationConfig_ShouldSucceed() + { + var (auth, error) = ServerOptions.ParseAuthorization( + Map(("users", Arr( + Map( + ("user", "alice"), + ("password", "pwd"), + ("permissions", Map( + ("publish", Map(("allow", Arr("foo", "bar", "baz")))), + ("subscribe", Map(("deny", Arr("$SYS.>"))))))))))); + + error.ShouldBeNull(); + auth.ShouldNotBeNull(); + var alice = auth.Users.Single(); + alice.Permissions.ShouldNotBeNull(); + alice.Permissions.Publish.Allow.Count.ShouldBe(3); + alice.Permissions.Subscribe.Deny.ShouldContain("$SYS.>"); + } + + [Fact] // T:2528 + public void NkeyUsersConfig_ShouldSucceed() + { + var (nkeys, users, error) = ServerOptions.ParseUsers( + Arr( + Map(("nkey", "UDKTV7HZVYJFJN64LLMYQBUR6MTNNYCDC3LAZH4VHURW3GZLL3FULBXV")), + Map(("nkey", "UA3C5TBZYK5GJQJRWPMU6NFY5JNAEVQB2V2TUZFZDHFJFUYVKTTUOFKZ")))); + + error.ShouldBeNull(); + nkeys.Count.ShouldBe(2); + users.ShouldBeEmpty(); + } + + [Fact] // T:2529 + public void TlsPinnedCertificates_ShouldSucceed() + { + var (tlsOptions, error) = ServerOptions.ParseTLS( + Map(("pinned_certs", Arr( + "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069", + "a8f407340dcc719864214b85ed96f98d16cbffa8f509d9fa4ca237b7bb3f9c32"))), + isClientCtx: false); + + error.ShouldBeNull(); + tlsOptions.ShouldNotBeNull(); + tlsOptions.PinnedCerts.ShouldNotBeNull(); + tlsOptions.PinnedCerts.Count.ShouldBe(2); + } + + [Fact] // T:2530 + public void NkeyUsersDefaultPermissionsConfig_ShouldSucceed() + { + var (auth, error) = ServerOptions.ParseAuthorization( + Map( + ("default_permissions", Map(("publish", "foo"))), + ("users", Arr( + Map(("user", "user"), ("password", "pwd")), + Map(("user", "other"), ("password", "pwd"), ("permissions", Map(("subscribe", "bar")))), + Map(("nkey", "UDKTV7HZVYJFJN64LLMYQBUR6MTNNYCDC3LAZH4VHURW3GZLL3FULBXV")), + Map(("nkey", "UA3C5TBZYK5GJQJRWPMU6NFY5JNAEVQB2V2TUZFZDHFJFUYVKTTUOFKZ"), ("permissions", Map(("subscribe", "bar")))))))); + + error.ShouldBeNull(); + auth.ShouldNotBeNull(); + + var defaultUser = auth.Users.Single(u => u.Username == "user"); + defaultUser.Permissions.ShouldNotBeNull(); + defaultUser.Permissions.Publish.ShouldNotBeNull(); + defaultUser.Permissions.Publish.Allow.ShouldContain("foo"); + + var defaultNkey = auth.Nkeys.Single(n => n.Nkey.StartsWith("UDK", StringComparison.Ordinal)); + defaultNkey.Permissions.ShouldNotBeNull(); + defaultNkey.Permissions.Publish.ShouldNotBeNull(); + defaultNkey.Permissions.Publish.Allow.ShouldContain("foo"); + } + + [Fact] // T:2531 + public void NkeyUsersWithPermsConfig_ShouldSucceed() + { + var (nkeys, users, error) = ServerOptions.ParseUsers( + Arr(Map( + ("nkey", "UDKTV7HZVYJFJN64LLMYQBUR6MTNNYCDC3LAZH4VHURW3GZLL3FULBXV"), + ("permissions", Map( + ("publish", "$SYS.>"), + ("subscribe", Map(("deny", Arr("foo", "bar", "baz"))))))))); + + error.ShouldBeNull(); + users.ShouldBeEmpty(); + nkeys.Count.ShouldBe(1); + nkeys[0].Permissions.ShouldNotBeNull(); + nkeys[0].Permissions.Publish.Allow.ShouldContain("$SYS.>"); + nkeys[0].Permissions.Subscribe.Deny.Count.ShouldBe(3); + } + + [Fact] // T:2532 + public void BadNkeyConfig_ShouldSucceed() + { + var (_, _, error) = ServerOptions.ParseUsers(Arr(Map(("nkey", "Ufoo")))); + error.ShouldNotBeNull(); + error.Message.ShouldContain("Not a valid public nkey"); + } + + [Fact] // T:2533 + public void NkeyWithPassConfig_ShouldSucceed() + { + var (_, _, error) = ServerOptions.ParseUsers( + Arr(Map(("nkey", "UDKTV7HZVYJFJN64LLMYQBUR6MTNNYCDC3LAZH4VHURW3GZLL3FULBXV"), ("pass", "foo")))); + + error.ShouldNotBeNull(); + error.Message.ShouldContain("Nkey users do not take usernames or passwords"); + } + + [Fact] // T:2540 + public void EmptyConfig_ShouldSucceed() + { + var opts = new ServerOptions(); + var error = opts.ProcessConfigFileOverload2510(string.Empty); + error.ShouldBeNull(); + opts.ConfigFile.ShouldBe(string.Empty); + } + + [Fact] // T:2541 + public void MalformedListenAddress_ShouldSucceed() + { + Should.Throw(() => ServerOptions.ParseListen("bad::address")); + } + + [Fact] // T:2542 + public void MalformedClusterAddress_ShouldSucceed() + { + var opts = new ServerOptions(); + var errors = new List(); + var warnings = new List(); + + var parseError = ServerOptions.ParseCluster(Map(("listen", "bad::address")), opts, errors, warnings); + + parseError.ShouldBeNull(); + errors.Count.ShouldBeGreaterThan(0); + } + + [Fact] // T:2545 + public void PingIntervalOld_ShouldSucceed() + { + var errors = new List(); + var warnings = new List(); + + var parsed = ServerOptions.ParseDuration("ping_interval", 5L, errors, warnings); + + parsed.ShouldBe(TimeSpan.FromSeconds(5)); + errors.ShouldBeEmpty(); + warnings.Count.ShouldBe(1); + } + + [Fact] // T:2546 + public void PingIntervalNew_ShouldSucceed() + { + var errors = new List(); + var warnings = new List(); + + var parsed = ServerOptions.ParseDuration("ping_interval", "5m", errors, warnings); + + parsed.ShouldBe(TimeSpan.FromMinutes(5)); + errors.ShouldBeEmpty(); + warnings.ShouldBeEmpty(); + } + + [Fact] // T:2547 + public void OptionsProcessConfigFile_ShouldSucceed() + { + var path = CreateJsonConfig(""" + { + "debug": false, + "trace": true + } + """); + + try + { + var opts = new ServerOptions + { + Debug = true, + Trace = false, + LogFile = "test.log", + }; + + var error = opts.ProcessConfigFileOverload2510(path); + + error.ShouldBeNull(); + opts.ConfigFile.ShouldBe(path); + opts.Debug.ShouldBeFalse(); + opts.Trace.ShouldBeTrue(); + opts.LogFile.ShouldBe("test.log"); + } + finally + { + File.Delete(path); + } + } + + [Fact] // T:2549 + public void ClusterPermissionsConfig_ShouldSucceed() + { + var cluster = new ClusterOpts(); + var permissions = new Permissions + { + Publish = new SubjectPermission { Allow = ["foo"] }, + Subscribe = new SubjectPermission { Allow = ["bar"] }, + }; + + ServerOptions.SetClusterPermissions(cluster, permissions); + + cluster.Permissions.ShouldNotBeNull(); + cluster.Permissions.Import.ShouldNotBeNull(); + cluster.Permissions.Import.Allow.ShouldContain("foo"); + cluster.Permissions.Export.ShouldNotBeNull(); + cluster.Permissions.Export.Allow.ShouldContain("bar"); + } + + [Fact] // T:2550 + public void ParseServiceLatency_ShouldSucceed() + { + var (latency, error) = ServerOptions.ParseServiceLatency( + "latency", + Map(("sampling", "33%"), ("subject", "latency.tracking.add"))); + + error.ShouldBeNull(); + latency.ShouldNotBeNull(); + ReadProperty(latency, "Sampling").ShouldBe(33); + ReadProperty(latency, "Subject").ShouldBe("latency.tracking.add"); + } + + [Fact] // T:2553 + public void ParsingGateways_ShouldSucceed() + { + var opts = new ServerOptions(); + var errors = new List(); + var warnings = new List(); + + var parseError = ServerOptions.ParseGateway( + Map( + ("name", "A"), + ("listen", "127.0.0.1:4444"), + ("authorization", Map(("user", "ivan"), ("password", "pwd"), ("timeout", 2L))), + ("advertise", "me:1"), + ("connect_retries", 10L), + ("connect_backoff", true), + ("reject_unknown_cluster", true)), + opts, + errors, + warnings); + + parseError.ShouldBeNull(); + errors.ShouldBeEmpty(); + opts.Gateway.Name.ShouldBe("A"); + opts.Gateway.Host.ShouldBe("127.0.0.1"); + opts.Gateway.Port.ShouldBe(4444); + opts.Gateway.Username.ShouldBe("ivan"); + opts.Gateway.Password.ShouldBe("pwd"); + opts.Gateway.AuthTimeout.ShouldBe(2); + opts.Gateway.ConnectRetries.ShouldBe(10); + opts.Gateway.ConnectBackoff.ShouldBeTrue(); + opts.Gateway.RejectUnknown.ShouldBeTrue(); + } + + [Fact] // T:2555 + public void ParsingLeafNodesListener_ShouldSucceed() + { + var opts = new ServerOptions(); + var errors = new List(); + var warnings = new List(); + + var parseError = ServerOptions.ParseLeafNodes( + Map( + ("listen", "127.0.0.1:3333"), + ("authorization", Map(("user", "derek"), ("password", "s3cr3t!"), ("timeout", 2.2))), + ("advertise", "me:22")), + opts, + errors, + warnings); + + parseError.ShouldBeNull(); + errors.ShouldBeEmpty(); + opts.LeafNode.Host.ShouldBe("127.0.0.1"); + opts.LeafNode.Port.ShouldBe(3333); + opts.LeafNode.Username.ShouldBe("derek"); + opts.LeafNode.Password.ShouldBe("s3cr3t!"); + opts.LeafNode.AuthTimeout.ShouldBe(2.2); + opts.LeafNode.Advertise.ShouldBe("me:22"); + } + + [Fact] // T:2556 + public void ParsingLeafNodeRemotes_ShouldSucceed() + { + var remotes = ServerOptions.ParseRemoteLeafNodes( + Arr( + Map( + ("url", "nats-leaf://127.0.0.1:2222"), + ("account", "foobar"), + ("credentials", "./my.creds")))); + + remotes.Count.ShouldBe(1); + remotes[0].Urls.Count.ShouldBe(1); + remotes[0].Urls[0].ToString().ShouldBe("nats-leaf://127.0.0.1:2222/"); + remotes[0].LocalAccount.ShouldBe("foobar"); + remotes[0].Credentials.ShouldContain("my.creds"); + } + + [Fact] // T:2560 + public void SublistNoCacheConfig_ShouldSucceed() + { + var opts = new ServerOptions(); + var error = opts.ProcessConfigString(""" + { + "disable_sublist_cache": true + } + """); + + error.ShouldBeNull(); + opts.NoSublistCache.ShouldBeTrue(); + } + + [Fact] // T:2583 + public void OptionsProxyRequired_ShouldSucceed() + { + var (authSingle, singleError) = ServerOptions.ParseAuthorization( + Map( + ("user", "user"), + ("password", "pwd"), + ("proxy_required", true))); + + singleError.ShouldBeNull(); + authSingle.ShouldNotBeNull(); + authSingle.ProxyRequired.ShouldBeTrue(); + + var (authUsers, usersError) = ServerOptions.ParseAuthorization( + Map( + ("users", Arr( + Map(("user", "user1"), ("password", "pwd1")), + Map(("user", "user2"), ("password", "pwd2"), ("proxy_required", true)), + Map(("nkey", "UCARKS2E3KVB7YORL2DG34XLT7PUCOL2SVM7YXV6ETHLW6Z46UUJ2VZ3"), ("proxy_required", true)), + Map(("nkey", "UD6AYQSOIN2IN5OGC6VQZCR4H3UFMIOXSW6NNS6N53CLJA4PB56CEJJI"), ("proxy_required", false)))))); + + usersError.ShouldBeNull(); + authUsers.ShouldNotBeNull(); + authUsers.Users.Single(u => u.Username == "user2").ProxyRequired.ShouldBeTrue(); + authUsers.Users.Single(u => u.Username == "user1").ProxyRequired.ShouldBeFalse(); + authUsers.Nkeys.Single(n => n.Nkey.StartsWith("UCAR", StringComparison.Ordinal)).ProxyRequired.ShouldBeTrue(); + } + + [Fact] // T:2588 + public void WebsocketPingIntervalConfig_ShouldSucceed() + { + var opts = new ServerOptions(); + var errors = new List(); + var warnings = new List(); + + var parseError = ServerOptions.ParseWebsocket( + Map(("port", 8080L), ("ping_interval", "30s")), + opts, + errors, + warnings); + + parseError.ShouldBeNull(); + errors.ShouldBeEmpty(); + opts.Websocket.PingInterval.ShouldBe(TimeSpan.FromSeconds(30)); + + parseError = ServerOptions.ParseWebsocket( + Map(("port", 8080L), ("ping_interval", 45L)), + opts, + errors, + warnings); + + parseError.ShouldBeNull(); + opts.Websocket.PingInterval.ShouldBe(TimeSpan.FromSeconds(45)); + } + [Fact] // T:2586 public void WriteDeadlineConfigParsing_ShouldSucceed() { diff --git a/porting.db b/porting.db index 4d69ce4..3c8775e 100644 Binary files a/porting.db and b/porting.db differ