From 390758e318fc11ccf962eb04d81e685590ba4bb2 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 11:18:06 -0500 Subject: [PATCH] test(batch7): implement t1 server options mapped tests --- .../ImplBacklog/ServerOptionsTests.cs | 536 ++++++++++++++++++ porting.db | Bin 6496256 -> 6500352 bytes 2 files changed, 536 insertions(+) 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 4d69ce474f7d71b4f47f7a3a2d2d3ac167e77b88..3c8775ed4e84fe0687142ae9a4ae9fc30bb39e8f 100644 GIT binary patch delta 6508 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