// Copyright 2012-2025 The NATS Authors // Licensed under the Apache License, Version 2.0 using Shouldly; using ZB.MOM.NatsNet.Server; using ZB.MOM.NatsNet.Server.Config; namespace ZB.MOM.NatsNet.Server.Tests; /// /// Tests for ServerOptions — mirrors tests from server/opts_test.go. /// public class ServerOptionsTests { /// /// Mirrors TestDefaultOptions — verifies baseline defaults are applied. /// [Fact] // T:2512 public void DefaultOptions_ShouldSetBaselineDefaults() { var opts = new ServerOptions(); opts.SetBaselineOptions(); opts.Host.ShouldBe(ServerConstants.DefaultHost); opts.Port.ShouldBe(ServerConstants.DefaultPort); opts.MaxConn.ShouldBe(ServerConstants.DefaultMaxConnections); opts.HttpHost.ShouldBe(ServerConstants.DefaultHost); opts.PingInterval.ShouldBe(ServerConstants.DefaultPingInterval); opts.MaxPingsOut.ShouldBe(ServerConstants.DefaultPingMaxOut); opts.TlsTimeout.ShouldBe(ServerConstants.TlsTimeout.TotalSeconds); opts.AuthTimeout.ShouldBe(ServerConstants.AuthTimeout.TotalSeconds); opts.MaxControlLine.ShouldBe(ServerConstants.MaxControlLineSize); opts.MaxPayload.ShouldBe(ServerConstants.MaxPayloadSize); opts.MaxPending.ShouldBe(ServerConstants.MaxPendingSize); opts.WriteDeadline.ShouldBe(ServerConstants.DefaultFlushDeadline); opts.MaxClosedClients.ShouldBe(ServerConstants.DefaultMaxClosedClients); opts.LameDuckDuration.ShouldBe(ServerConstants.DefaultLameDuckDuration); opts.LameDuckGracePeriod.ShouldBe(ServerConstants.DefaultLameDuckGracePeriod); opts.LeafNode.ReconnectInterval.ShouldBe(ServerConstants.DefaultLeafNodeReconnect); opts.ConnectErrorReports.ShouldBe(ServerConstants.DefaultConnectErrorReports); opts.ReconnectErrorReports.ShouldBe(ServerConstants.DefaultReconnectErrorReports); opts.JetStreamMaxMemory.ShouldBe(-1); opts.JetStreamMaxStore.ShouldBe(-1); opts.SyncInterval.ShouldBe(TimeSpan.FromMinutes(2)); opts.JetStreamRequestQueueLimit.ShouldBe(4096); } /// /// Mirrors TestOptions_RandomPort — RANDOM_PORT should become 0 after baseline. /// [Fact] // T:2513 public void RandomPort_ShouldResolveToZero() { var opts = new ServerOptions { Port = ServerConstants.RandomPort }; opts.SetBaselineOptions(); opts.Port.ShouldBe(0); } /// /// Mirrors TestMergeOverrides — flag options override file options. /// [Fact] // T:2516 public void MergeOverrides_ShouldLetFlagsWin() { var fileOpts = new ServerOptions { Host = "0.0.0.0", Port = 4222, Username = "fileuser", Password = "filepass", }; var flagOpts = new ServerOptions { Port = 9999, Username = "flaguser", }; var merged = ServerOptions.MergeOptions(fileOpts, flagOpts); merged.Port.ShouldBe(9999); merged.Username.ShouldBe("flaguser"); // Host not overridden (empty in flags) merged.Host.ShouldBe("0.0.0.0"); // Password not overridden (empty in flags) merged.Password.ShouldBe("filepass"); } /// /// Mirrors TestOptionsClone — deep copy should produce independent objects. /// [Fact] // T:2537 public void Clone_ShouldDeepCopyAllFields() { var opts = new ServerOptions { ConfigFile = "./configs/test.conf", Host = "127.0.0.1", Port = 2222, Username = "derek", Password = "porkchop", AuthTimeout = 1.0, Debug = true, Trace = true, Logtime = false, HttpPort = ServerConstants.DefaultHttpPort, HttpBasePath = ServerConstants.DefaultHttpBasePath, PidFile = "/tmp/nats-server/nats-server.pid", ProfPort = 6789, Syslog = true, RemoteSyslog = "udp://foo.com:33", MaxControlLine = 2048, MaxPayload = 65536, MaxConn = 100, PingInterval = TimeSpan.FromSeconds(60), MaxPingsOut = 3, Cluster = new ClusterOpts { NoAdvertise = true, ConnectRetries = 2, WriteDeadline = TimeSpan.FromSeconds(3), }, Gateway = new GatewayOpts { Name = "A", Gateways = [ new RemoteGatewayOpts { Name = "B", Urls = [new Uri("nats://host:5222")] }, new RemoteGatewayOpts { Name = "C" }, ], }, WriteDeadline = TimeSpan.FromSeconds(3), Routes = [new Uri("nats://localhost:4222")], }; var clone = opts.Clone(); // Values should match. clone.ConfigFile.ShouldBe(opts.ConfigFile); clone.Host.ShouldBe(opts.Host); clone.Port.ShouldBe(opts.Port); clone.Username.ShouldBe(opts.Username); clone.Gateway.Name.ShouldBe("A"); clone.Gateway.Gateways.Count.ShouldBe(2); clone.Gateway.Gateways[0].Urls[0].Authority.ShouldBe("host:5222"); // Mutating clone should not affect original. clone.Username = "changed"; opts.Username.ShouldBe("derek"); // Mutating original gateway URLs should not affect clone. opts.Gateway.Gateways[0].Urls[0] = new Uri("nats://other:9999"); clone.Gateway.Gateways[0].Urls[0].Authority.ShouldBe("host:5222"); } /// /// Mirrors TestOptionsCloneNilLists — clone of empty lists should produce empty lists. /// [Fact] // T:2538 public void CloneNilLists_ShouldProduceEmptyLists() { var opts = new ServerOptions(); var clone = opts.Clone(); clone.Routes.ShouldNotBeNull(); } /// /// Mirrors TestExpandPath — tilde and env var expansion. /// [Fact] // T:2563 public void ExpandPath_ShouldExpandTildeAndEnvVars() { // Absolute path should not change. var result = ServerOptions.ExpandPath("/foo/bar"); result.ShouldBe("/foo/bar"); // Tilde expansion. var home = ServerOptions.HomeDir(); var expanded = ServerOptions.ExpandPath("~/test"); expanded.ShouldBe(Path.Combine(home, "test")); } /// /// Mirrors TestDefaultAuthTimeout — verifies auth timeout logic. /// [Fact] // T:2572 public void DefaultAuthTimeout_ShouldBe2sWithoutTls() { var timeout = ServerOptions.GetDefaultAuthTimeout(null, 0); timeout.ShouldBe(ServerConstants.AuthTimeout.TotalSeconds); } [Fact] public void DefaultAuthTimeout_ShouldBeTlsTimeoutPlusOneWithTls() { // When TLS is configured, auth timeout = tls_timeout + 1 var timeout = ServerOptions.GetDefaultAuthTimeout(new object(), 4.0); timeout.ShouldBe(5.0); } /// /// Tests RoutesFromStr parsing. /// [Fact] // T:2576 (partial) public void RoutesFromStr_ShouldParseCommaSeparatedUrls() { var routes = ServerOptions.RoutesFromStr("nats://localhost:4222,nats://localhost:4223"); routes.Count.ShouldBe(2); routes[0].Authority.ShouldBe("localhost:4222"); routes[1].Authority.ShouldBe("localhost:4223"); } /// /// Tests NormalizeBasePath. /// [Fact] // T:2581 public void NormalizeBasePath_ShouldCleanPaths() { ServerOptions.NormalizeBasePath("").ShouldBe("/"); ServerOptions.NormalizeBasePath("/").ShouldBe("/"); ServerOptions.NormalizeBasePath("foo").ShouldBe("/foo"); ServerOptions.NormalizeBasePath("/foo/").ShouldBe("/foo"); ServerOptions.NormalizeBasePath("//foo//bar//").ShouldBe("/foo/bar"); } /// /// Tests ConfigFlags.NoErrOnUnknownFields. /// [Fact] // T:2502 public void NoErrOnUnknownFields_ShouldToggleFlag() { ConfigFlags.NoErrOnUnknownFields(true); ConfigFlags.AllowUnknownTopLevelField.ShouldBeTrue(); ConfigFlags.NoErrOnUnknownFields(false); ConfigFlags.AllowUnknownTopLevelField.ShouldBeFalse(); } /// /// Tests SetBaseline with cluster port set. /// [Fact] public void SetBaseline_WithClusterPort_ShouldApplyClusterDefaults() { var opts = new ServerOptions { Cluster = new ClusterOpts { Port = 6222 }, }; opts.SetBaselineOptions(); opts.Cluster.Host.ShouldBe(ServerConstants.DefaultHost); opts.Cluster.TlsTimeout.ShouldBe(ServerConstants.TlsTimeout.TotalSeconds); opts.Cluster.PoolSize.ShouldBe(ServerConstants.DefaultRoutePoolSize); opts.Cluster.Compression.Mode.ShouldBe(CompressionModes.Accept); // System account should be auto-pinned. opts.Cluster.PinnedAccounts.ShouldContain(ServerConstants.DefaultSystemAccount); } /// /// Tests SetBaseline with leaf node port set. /// [Fact] public void SetBaseline_WithLeafNodePort_ShouldApplyLeafDefaults() { var opts = new ServerOptions { LeafNode = new LeafNodeOpts { Port = 7422 }, }; opts.SetBaselineOptions(); opts.LeafNode.Host.ShouldBe(ServerConstants.DefaultHost); opts.LeafNode.Compression.Mode.ShouldBe(CompressionModes.S2Auto); } /// /// Tests OverrideCluster with valid URL. /// [Fact] // T:2518 public void OverrideCluster_ShouldParseUrlAndSetHostPort() { var opts = new ServerOptions { Cluster = new ClusterOpts { ListenStr = "nats://user:pass@127.0.0.1:6222" }, }; var err = opts.OverrideCluster(); err.ShouldBeNull(); opts.Cluster.Host.ShouldBe("127.0.0.1"); opts.Cluster.Port.ShouldBe(6222); opts.Cluster.Username.ShouldBe("user"); opts.Cluster.Password.ShouldBe("pass"); } /// /// Tests OverrideCluster with empty string disables clustering. /// [Fact] public void OverrideCluster_EmptyString_ShouldDisableClustering() { var opts = new ServerOptions { Cluster = new ClusterOpts { Port = 6222, ListenStr = "" }, }; var err = opts.OverrideCluster(); err.ShouldBeNull(); opts.Cluster.Port.ShouldBe(0); } /// /// Tests MaybeReadPidFile returns original string when file doesn't exist. /// [Fact] // T:2585 public void MaybeReadPidFile_NonExistent_ShouldReturnOriginalString() { ServerOptions.MaybeReadPidFile("12345").ShouldBe("12345"); } /// /// Tests that MergeOptions handles null inputs. /// [Fact] public void MergeOptions_NullInputs_ShouldReturnNonNull() { var result = ServerOptions.MergeOptions(null, null); result.ShouldNotBeNull(); var fileOpts = new ServerOptions { Port = 1234 }; var r1 = ServerOptions.MergeOptions(fileOpts, null); r1.Port.ShouldBe(1234); var flagOpts = new ServerOptions { Port = 5678 }; var r2 = ServerOptions.MergeOptions(null, flagOpts); r2.Port.ShouldBe(5678); } /// /// Mirrors TestListenMonitoringDefault — when Host is set without HTTPHost, /// SetBaselineOptions should copy Host to HTTPHost. /// [Fact] // T:2524 public void ListenMonitoringDefault_ShouldSetHttpHostToHost() { var opts = new ServerOptions { Host = "10.0.1.22" }; opts.SetBaselineOptions(); opts.Host.ShouldBe("10.0.1.22"); opts.HttpHost.ShouldBe("10.0.1.22"); opts.Port.ShouldBe(ServerConstants.DefaultPort); } /// /// Mirrors TestGetStorageSize — StorageSizeJsonConverter.Parse converts K/M/G/T suffixes /// and returns 0 for empty input; invalid suffixes throw. /// [Fact] // T:2576 public void GetStorageSize_ShouldParseSuffixes() { StorageSizeJsonConverter.Parse("1K").ShouldBe(1024L); StorageSizeJsonConverter.Parse("1M").ShouldBe(1048576L); StorageSizeJsonConverter.Parse("1G").ShouldBe(1073741824L); StorageSizeJsonConverter.Parse("1T").ShouldBe(1099511627776L); StorageSizeJsonConverter.Parse("").ShouldBe(0L); Should.Throw(() => StorageSizeJsonConverter.Parse("1L")); Should.Throw(() => StorageSizeJsonConverter.Parse("TT")); } /// /// Mirrors TestClusterNameAndGatewayNameConflict — when Cluster.Name != Gateway.Name, /// ValidateOptions should return ErrClusterNameConfigConflict. /// [Fact] // T:2571 public void ClusterNameAndGatewayNameConflict_ShouldReturnConflictError() { var opts = new ServerOptions { Cluster = new ClusterOpts { Name = "A", Port = -1 }, Gateway = new GatewayOpts { Name = "B", Port = -1 }, }; var err = NatsServer.ValidateOptions(opts); err.ShouldNotBeNull(); err.ShouldBe(ServerErrors.ErrClusterNameConfigConflict); } }