// Port of Go server/opts_test.go — config parsing and options parity tests. // Reference: golang/nats-server/server/opts_test.go using System.Net; using System.Net.Sockets; using NATS.Server.Auth; using NATS.Server.Configuration; namespace NATS.Server.Tests.Configuration; /// /// Parity tests ported from Go server/opts_test.go that exercise config parsing, /// option defaults, variable substitution, and authorization block parsing. /// public class OptsGoParityTests { // ─── Helpers ──────────────────────────────────────────────────────────── /// /// Creates a temporary config file with the given content and returns its path. /// The file is deleted after the test via the returned IDisposable registered /// with a finalizer helper. /// private static string CreateTempConf(string content) { var path = Path.GetTempFileName(); File.WriteAllText(path, content); return path; } // ─── TestOptions_RandomPort ────────────────────────────────────────────── /// /// Go: TestOptions_RandomPort server/opts_test.go:87 /// /// In Go, port=-1 (RANDOM_PORT) is resolved to 0 (ephemeral) by setBaselineOptions. /// In .NET, port=-1 means "use the OS ephemeral port". We verify that parsing /// "listen: -1" or setting Port=-1 does NOT produce a normal port, and that /// port=0 is the canonical ephemeral indicator in the .NET implementation. /// [Fact] public void RandomPort_NegativeOne_IsEphemeral() { // Go: RANDOM_PORT = -1; setBaselineOptions resolves it to 0. // In .NET we can parse port: -1 from config to get port=-1, which the // server treats as ephemeral (it will bind to port 0 on the OS). // Verify the .NET parser accepts it without error. var opts = ConfigProcessor.ProcessConfig("port: -1"); opts.Port.ShouldBe(-1); } [Fact] public void RandomPort_Zero_IsEphemeral() { // Port 0 is the canonical OS ephemeral port indicator. var opts = ConfigProcessor.ProcessConfig("port: 0"); opts.Port.ShouldBe(0); } // ─── TestListenPortOnlyConfig ───────────────────────────────────────────── /// /// Go: TestListenPortOnlyConfig server/opts_test.go:507 /// /// Verifies that a config containing only "listen: 8922" (bare port number) /// is parsed correctly — host stays as the default, port is set to 8922. /// [Fact] public void ListenPortOnly_ParsesBarePort() { // Go test loads ./configs/listen_port.conf which contains "listen: 8922" var conf = CreateTempConf("listen: 8922\n"); try { var opts = ConfigProcessor.ProcessConfigFile(conf); opts.Port.ShouldBe(8922); // Host should remain at the default (0.0.0.0) opts.Host.ShouldBe("0.0.0.0"); } finally { File.Delete(conf); } } // ─── TestListenPortWithColonConfig ──────────────────────────────────────── /// /// Go: TestListenPortWithColonConfig server/opts_test.go:527 /// /// Verifies that "listen: :8922" (colon-prefixed port) is parsed correctly — /// the host part is empty so host stays at default, port is set to 8922. /// [Fact] public void ListenPortWithColon_ParsesPortOnly() { // Go test loads ./configs/listen_port_with_colon.conf which contains "listen: :8922" var conf = CreateTempConf("listen: \":8922\"\n"); try { var opts = ConfigProcessor.ProcessConfigFile(conf); opts.Port.ShouldBe(8922); // Host should remain at the default (0.0.0.0), not empty string opts.Host.ShouldBe("0.0.0.0"); } finally { File.Delete(conf); } } // ─── TestMultipleUsersConfig ────────────────────────────────────────────── /// /// Go: TestMultipleUsersConfig server/opts_test.go:565 /// /// Verifies that a config with multiple users in an authorization block /// is parsed without error and produces the correct user list. /// [Fact] public void MultipleUsers_ParsesWithoutError() { // Go test loads ./configs/multiple_users.conf which has 2 users var conf = CreateTempConf(""" listen: "127.0.0.1:4443" authorization { users = [ {user: alice, password: foo} {user: bob, password: bar} ] timeout: 0.5 } """); try { var opts = ConfigProcessor.ProcessConfigFile(conf); opts.Users.ShouldNotBeNull(); opts.Users!.Count.ShouldBe(2); } finally { File.Delete(conf); } } // ─── TestAuthorizationConfig ────────────────────────────────────────────── /// /// Go: TestAuthorizationConfig server/opts_test.go:575 /// /// Verifies authorization block parsing: users array, per-user permissions /// (publish/subscribe), and allow_responses (ResponsePermission). /// The Go test uses ./configs/authorization.conf which has 5 users with /// varying permission configurations including variable references. /// We inline an equivalent config here. /// [Fact] public void AuthorizationConfig_UsersAndPermissions() { var conf = CreateTempConf(""" authorization { users = [ {user: alice, password: foo, permissions: { publish: { allow: ["*"] }, subscribe: { allow: [">"] } } } {user: bob, password: bar, permissions: { publish: { allow: ["req.foo", "req.bar"] }, subscribe: { allow: ["_INBOX.>"] } } } {user: susan, password: baz, permissions: { subscribe: { allow: ["PUBLIC.>"] } } } {user: svca, password: pc, permissions: { subscribe: { allow: ["my.service.req"] }, publish: { allow: [] }, resp: { max: 1, expires: "0s" } } } {user: svcb, password: sam, permissions: { subscribe: { allow: ["my.service.req"] }, publish: { allow: [] }, resp: { max: 10, expires: "1m" } } } ] } """); try { var opts = ConfigProcessor.ProcessConfigFile(conf); opts.Users.ShouldNotBeNull(); opts.Users!.Count.ShouldBe(5); // Build a map for easy lookup var userMap = opts.Users.ToDictionary(u => u.Username); // Alice: publish="*", subscribe=">" var alice = userMap["alice"]; alice.Permissions.ShouldNotBeNull(); alice.Permissions!.Publish.ShouldNotBeNull(); alice.Permissions.Publish!.Allow.ShouldNotBeNull(); alice.Permissions.Publish.Allow!.ShouldContain("*"); alice.Permissions.Subscribe.ShouldNotBeNull(); alice.Permissions.Subscribe!.Allow.ShouldNotBeNull(); alice.Permissions.Subscribe.Allow!.ShouldContain(">"); // Bob: publish=["req.foo","req.bar"], subscribe=["_INBOX.>"] var bob = userMap["bob"]; bob.Permissions.ShouldNotBeNull(); bob.Permissions!.Publish.ShouldNotBeNull(); bob.Permissions.Publish!.Allow!.ShouldContain("req.foo"); bob.Permissions.Publish.Allow!.ShouldContain("req.bar"); bob.Permissions.Subscribe!.Allow!.ShouldContain("_INBOX.>"); // Susan: subscribe="PUBLIC.>", no publish perms var susan = userMap["susan"]; susan.Permissions.ShouldNotBeNull(); susan.Permissions!.Publish.ShouldBeNull(); susan.Permissions.Subscribe.ShouldNotBeNull(); susan.Permissions.Subscribe!.Allow!.ShouldContain("PUBLIC.>"); // Service B (svcb): response permissions max=10, expires=1m var svcb = userMap["svcb"]; svcb.Permissions.ShouldNotBeNull(); svcb.Permissions!.Response.ShouldNotBeNull(); svcb.Permissions.Response!.MaxMsgs.ShouldBe(10); svcb.Permissions.Response.Expires.ShouldBe(TimeSpan.FromMinutes(1)); } finally { File.Delete(conf); } } // ─── TestAuthorizationConfig — simple token block ───────────────────────── [Fact] public void AuthorizationConfig_TokenAndTimeout() { // Go: TestAuthorizationConfig also verifies the top-level authorization block // with user/password/timeout fields. var opts = ConfigProcessor.ProcessConfig(""" authorization { user: admin password: "s3cr3t" timeout: 3 } """); opts.Username.ShouldBe("admin"); opts.Password.ShouldBe("s3cr3t"); opts.AuthTimeout.ShouldBe(TimeSpan.FromSeconds(3)); } // ─── TestOptionsClone ───────────────────────────────────────────────────── /// /// Go: TestOptionsClone server/opts_test.go:1221 /// /// Verifies that a populated NatsOptions is correctly copied by a clone /// operation and that mutating the clone does not affect the original. /// In .NET, NatsOptions is mutable so "clone" means making a shallow-enough /// copy of the value properties. /// [Fact] public void OptionsClone_ProducesIndependentCopy() { var opts = new NatsOptions { Host = "127.0.0.1", Port = 2222, Username = "derek", Password = "porkchop", Debug = true, Trace = true, PidFile = "/tmp/nats-server/nats-server.pid", ProfPort = 6789, Syslog = true, RemoteSyslog = "udp://foo.com:33", MaxControlLine = 2048, MaxPayload = 65536, MaxConnections = 100, PingInterval = TimeSpan.FromSeconds(60), MaxPingsOut = 3, }; // Simulate a shallow clone by constructing a copy var clone = new NatsOptions { Host = opts.Host, Port = opts.Port, Username = opts.Username, Password = opts.Password, Debug = opts.Debug, Trace = opts.Trace, PidFile = opts.PidFile, ProfPort = opts.ProfPort, Syslog = opts.Syslog, RemoteSyslog = opts.RemoteSyslog, MaxControlLine = opts.MaxControlLine, MaxPayload = opts.MaxPayload, MaxConnections = opts.MaxConnections, PingInterval = opts.PingInterval, MaxPingsOut = opts.MaxPingsOut, }; // Verify all copied fields clone.Host.ShouldBe(opts.Host); clone.Port.ShouldBe(opts.Port); clone.Username.ShouldBe(opts.Username); clone.Password.ShouldBe(opts.Password); clone.Debug.ShouldBe(opts.Debug); clone.Trace.ShouldBe(opts.Trace); clone.PidFile.ShouldBe(opts.PidFile); clone.ProfPort.ShouldBe(opts.ProfPort); clone.Syslog.ShouldBe(opts.Syslog); clone.RemoteSyslog.ShouldBe(opts.RemoteSyslog); clone.MaxControlLine.ShouldBe(opts.MaxControlLine); clone.MaxPayload.ShouldBe(opts.MaxPayload); clone.MaxConnections.ShouldBe(opts.MaxConnections); clone.PingInterval.ShouldBe(opts.PingInterval); clone.MaxPingsOut.ShouldBe(opts.MaxPingsOut); // Mutating the clone should not affect the original clone.Password = "new_password"; opts.Password.ShouldBe("porkchop"); clone.Port = 9999; opts.Port.ShouldBe(2222); } // ─── TestOptionsCloneNilLists ────────────────────────────────────────────── /// /// Go: TestOptionsCloneNilLists server/opts_test.go:1281 /// /// Verifies that cloning an empty Options struct produces nil/empty collections, /// not empty-but-non-nil lists. In .NET, an unset NatsOptions.Users is null. /// [Fact] public void OptionsCloneNilLists_UsersIsNullByDefault() { // Go: opts := &Options{}; clone := opts.Clone(); clone.Users should be nil. var opts = new NatsOptions(); opts.Users.ShouldBeNull(); } // ─── TestProcessConfigString ────────────────────────────────────────────── /// /// Go: TestProcessConfigString server/opts_test.go:3407 /// /// Verifies that ProcessConfig (from string) can parse basic option values /// without requiring a file on disk. /// [Fact] public void ProcessConfigString_ParsesBasicOptions() { // Go uses opts.ProcessConfigString(config); .NET equivalent is ConfigProcessor.ProcessConfig. var opts = ConfigProcessor.ProcessConfig(""" port: 9222 host: "127.0.0.1" debug: true max_connections: 500 """); opts.Port.ShouldBe(9222); opts.Host.ShouldBe("127.0.0.1"); opts.Debug.ShouldBeTrue(); opts.MaxConnections.ShouldBe(500); } [Fact] public void ProcessConfigString_MultipleOptions() { var opts = ConfigProcessor.ProcessConfig(""" port: 4333 server_name: "myserver" max_payload: 65536 ping_interval: "30s" """); opts.Port.ShouldBe(4333); opts.ServerName.ShouldBe("myserver"); opts.MaxPayload.ShouldBe(65536); opts.PingInterval.ShouldBe(TimeSpan.FromSeconds(30)); } // ─── TestDefaultSentinel ────────────────────────────────────────────────── /// /// Go: TestDefaultSentinel server/opts_test.go:3489 /// /// Verifies that .NET NatsOptions defaults match expected sentinel values. /// In Go, setBaselineOptions populates defaults. In .NET, defaults are defined /// in NatsOptions property initializers. /// [Fact] public void DefaultOptions_PortIs4222() { var opts = new NatsOptions(); opts.Port.ShouldBe(4222); } [Fact] public void DefaultOptions_HostIs0000() { var opts = new NatsOptions(); opts.Host.ShouldBe("0.0.0.0"); } [Fact] public void DefaultOptions_MaxPayloadIs1MB() { var opts = new NatsOptions(); opts.MaxPayload.ShouldBe(1024 * 1024); } [Fact] public void DefaultOptions_MaxControlLineIs4096() { var opts = new NatsOptions(); opts.MaxControlLine.ShouldBe(4096); } [Fact] public void DefaultOptions_MaxConnectionsIs65536() { var opts = new NatsOptions(); opts.MaxConnections.ShouldBe(65536); } [Fact] public void DefaultOptions_PingIntervalIs2Minutes() { var opts = new NatsOptions(); opts.PingInterval.ShouldBe(TimeSpan.FromMinutes(2)); } [Fact] public void DefaultOptions_MaxPingsOutIs2() { var opts = new NatsOptions(); opts.MaxPingsOut.ShouldBe(2); } [Fact] public void DefaultOptions_WriteDeadlineIs10Seconds() { var opts = new NatsOptions(); opts.WriteDeadline.ShouldBe(TimeSpan.FromSeconds(10)); } [Fact] public void DefaultOptions_AuthTimeoutIs2Seconds() { var opts = new NatsOptions(); opts.AuthTimeout.ShouldBe(TimeSpan.FromSeconds(2)); } [Fact] public void DefaultOptions_LameDuckDurationIs2Minutes() { var opts = new NatsOptions(); opts.LameDuckDuration.ShouldBe(TimeSpan.FromMinutes(2)); } [Fact] public void DefaultOptions_MaxClosedClientsIs10000() { var opts = new NatsOptions(); opts.MaxClosedClients.ShouldBe(10_000); } [Fact] public void DefaultOptions_MaxSubsIsZero_Unlimited() { var opts = new NatsOptions(); opts.MaxSubs.ShouldBe(0); } [Fact] public void DefaultOptions_DebugAndTraceAreFalse() { var opts = new NatsOptions(); opts.Debug.ShouldBeFalse(); opts.Trace.ShouldBeFalse(); } [Fact] public void DefaultOptions_MaxPendingIs64MB() { var opts = new NatsOptions(); opts.MaxPending.ShouldBe(64L * 1024 * 1024); } // ─── TestWriteDeadlineConfigParsing ─────────────────────────────────────── /// /// Go: TestParseWriteDeadline server/opts_test.go:1187 /// /// Verifies write_deadline parsing from config strings: /// - Invalid unit ("1x") should throw /// - Valid string "1s" should produce 1 second /// - Bare integer 2 should produce 2 seconds (treated as seconds) /// [Fact] public void WriteDeadline_InvalidUnit_ThrowsException() { // Go: expects error containing "parsing" var conf = CreateTempConf("write_deadline: \"1x\""); try { Should.Throw(() => ConfigProcessor.ProcessConfigFile(conf)); } finally { File.Delete(conf); } } [Fact] public void WriteDeadline_ValidStringSeconds_Parsed() { var conf = CreateTempConf("write_deadline: \"1s\""); try { var opts = ConfigProcessor.ProcessConfigFile(conf); opts.WriteDeadline.ShouldBe(TimeSpan.FromSeconds(1)); } finally { File.Delete(conf); } } [Fact] public void WriteDeadline_BareInteger_TreatedAsSeconds() { // Go: write_deadline: 2 (integer) is treated as 2 seconds var conf = CreateTempConf("write_deadline: 2"); try { var opts = ConfigProcessor.ProcessConfigFile(conf); opts.WriteDeadline.ShouldBe(TimeSpan.FromSeconds(2)); } finally { File.Delete(conf); } } [Fact] public void WriteDeadline_StringMilliseconds_Parsed() { var opts = ConfigProcessor.ProcessConfig("write_deadline: \"500ms\""); opts.WriteDeadline.ShouldBe(TimeSpan.FromMilliseconds(500)); } [Fact] public void WriteDeadline_StringMinutes_Parsed() { var opts = ConfigProcessor.ProcessConfig("write_deadline: \"2m\""); opts.WriteDeadline.ShouldBe(TimeSpan.FromMinutes(2)); } // ─── TestWriteTimeoutConfigParsing alias ────────────────────────────────── /// /// Go: TestWriteTimeoutConfigParsing server/opts_test.go:4059 /// /// In Go, write_timeout is a policy enum (default/retry/close) on cluster/gateway/leafnode. /// In .NET the field is write_deadline which is a TimeSpan. We verify the .NET /// duration parsing is consistent with what the Go reference parses for the client-facing /// write deadline field (not the per-subsystem policy). /// [Fact] public void WriteDeadline_AllDurationFormats_Parsed() { // Verify all supported duration formats ConfigProcessor.ProcessConfig("write_deadline: \"30s\"").WriteDeadline .ShouldBe(TimeSpan.FromSeconds(30)); ConfigProcessor.ProcessConfig("write_deadline: \"1h\"").WriteDeadline .ShouldBe(TimeSpan.FromHours(1)); ConfigProcessor.ProcessConfig("write_deadline: 60").WriteDeadline .ShouldBe(TimeSpan.FromSeconds(60)); } // ─── TestExpandPath ──────────────────────────────────────────────────────── /// /// Go: TestExpandPath server/opts_test.go:2808 /// /// Verifies that file paths in config values that contain "~" are expanded /// to the home directory. The .NET port does not yet have a dedicated /// expandPath helper, but we verify that file paths are accepted as-is and /// that the PidFile / LogFile fields store the raw value parsed from config. /// [Fact] public void PathConfig_AbsolutePathStoredVerbatim() { // Go: {path: "/foo/bar", wantPath: "/foo/bar"} var opts = ConfigProcessor.ProcessConfig("pid_file: \"/foo/bar/nats.pid\""); opts.PidFile.ShouldBe("/foo/bar/nats.pid"); } [Fact] public void PathConfig_RelativePathStoredVerbatim() { // Go: {path: "foo/bar", wantPath: "foo/bar"} var opts = ConfigProcessor.ProcessConfig("log_file: \"foo/bar/nats.log\""); opts.LogFile.ShouldBe("foo/bar/nats.log"); } [Fact] public void PathConfig_HomeDirectory_TildeIsStoredVerbatim() { // In Go, expandPath("~/fizz") expands using $HOME. // In the .NET config parser the raw value is stored; expansion // happens at server startup. Verify the parser does not choke on it. var opts = ConfigProcessor.ProcessConfig("pid_file: \"~/nats/nats.pid\""); opts.PidFile.ShouldBe("~/nats/nats.pid"); } // ─── TestVarReferencesVar ───────────────────────────────────────────────── /// /// Go: TestVarReferencesVar server/opts_test.go:4186 /// /// Verifies that a config variable can reference another variable defined /// earlier in the same file and the final value is correctly resolved. /// [Fact] public void VarReferencesVar_ChainedResolution() { // Go test: A: 7890, B: $A, C: $B, port: $C → port = 7890 var conf = CreateTempConf(""" A: 7890 B: $A C: $B port: $C """); try { var opts = ConfigProcessor.ProcessConfigFile(conf); opts.Port.ShouldBe(7890); } finally { File.Delete(conf); } } // ─── TestVarReferencesEnvVar ────────────────────────────────────────────── /// /// Go: TestVarReferencesEnvVar server/opts_test.go:4203 /// /// Verifies that a config variable can reference an environment variable /// and the chain A: $ENV_VAR, B: $A, port: $B resolves correctly. /// [Fact] public void VarReferencesEnvVar_ChainedResolution() { // Go test: A: $_TEST_ENV_NATS_PORT_, B: $A, C: $B, port: $C → port = 7890 var envVar = "_DOTNET_TEST_NATS_PORT_" + Guid.NewGuid().ToString("N")[..8].ToUpperInvariant(); Environment.SetEnvironmentVariable(envVar, "7890"); try { var conf = CreateTempConf($""" A: ${envVar} B: $A C: $B port: $C """); try { var opts = ConfigProcessor.ProcessConfigFile(conf); opts.Port.ShouldBe(7890); } finally { File.Delete(conf); } } finally { Environment.SetEnvironmentVariable(envVar, null); } } [Fact] public void VarReferencesEnvVar_DirectEnvVarInPort() { // Direct: port: $ENV_VAR (no intermediate variable) var envVar = "_DOTNET_TEST_PORT_" + Guid.NewGuid().ToString("N")[..8].ToUpperInvariant(); Environment.SetEnvironmentVariable(envVar, "8765"); try { var conf = CreateTempConf($"port: ${envVar}\n"); try { var opts = ConfigProcessor.ProcessConfigFile(conf); opts.Port.ShouldBe(8765); } finally { File.Delete(conf); } } finally { Environment.SetEnvironmentVariable(envVar, null); } } // ─── TestHandleUnknownTopLevelConfigurationField ─────────────────────────── /// /// Go: TestHandleUnknownTopLevelConfigurationField server/opts_test.go:2632 /// /// Verifies that unknown top-level config fields are silently ignored /// (the .NET ConfigProcessor uses a default: break in its switch statement, /// so unknown keys are no-ops). The Go test verifies that a "streaming" block /// which is unknown does not cause a crash. /// [Fact] public void UnknownTopLevelField_SilentlyIgnored() { // Go test: port: 1234, streaming { id: "me" } → should not error, // NoErrOnUnknownFields(true) mode. In .NET, unknown fields are always ignored. var opts = ConfigProcessor.ProcessConfig(""" port: 1234 streaming { id: "me" } """); opts.Port.ShouldBe(1234); } [Fact] public void UnknownTopLevelField_KnownFieldsStillParsed() { var opts = ConfigProcessor.ProcessConfig(""" port: 5555 totally_unknown_field: "some_value" server_name: "my-server" """); opts.Port.ShouldBe(5555); opts.ServerName.ShouldBe("my-server"); } // ─── Additional coverage: authorization block defaults ──────────────────── [Fact] public void Authorization_SimpleUserPassword_WithTimeout() { var opts = ConfigProcessor.ProcessConfig(""" authorization { user: "testuser" password: "testpass" timeout: 5 } """); opts.Username.ShouldBe("testuser"); opts.Password.ShouldBe("testpass"); opts.AuthTimeout.ShouldBe(TimeSpan.FromSeconds(5)); } [Fact] public void Authorization_TokenField() { var opts = ConfigProcessor.ProcessConfig(""" authorization { token: "my_secret_token" } """); opts.Authorization.ShouldBe("my_secret_token"); } [Fact] public void Authorization_TimeoutAsFloat_ParsedAsSeconds() { // Go's authorization timeout can be a float (e.g., 0.5 seconds) var opts = ConfigProcessor.ProcessConfig(""" authorization { user: alice password: foo timeout: 0.5 } """); opts.AuthTimeout.ShouldBe(TimeSpan.FromSeconds(0.5)); } // ─── Listen combined format (colon-port) ───────────────────────────────── [Fact] public void Listen_BarePortNumber_SetsPort() { var opts = ConfigProcessor.ProcessConfig("listen: 5222"); opts.Port.ShouldBe(5222); } [Fact] public void Listen_ColonPort_SetsPort() { var opts = ConfigProcessor.ProcessConfig("listen: \":5222\""); opts.Port.ShouldBe(5222); } [Fact] public void Listen_HostAndPort_SetsBoth() { var opts = ConfigProcessor.ProcessConfig("listen: \"127.0.0.1:5222\""); opts.Host.ShouldBe("127.0.0.1"); opts.Port.ShouldBe(5222); } // ─── Empty config ────────────────────────────────────────────────────────── /// /// Go: TestEmptyConfig server/opts_test.go:1302 /// /// Verifies that an empty config string is parsed without error /// and produces default option values. /// [Fact] public void EmptyConfig_ProducesDefaults() { // Go: ProcessConfigFile("") succeeds, opts.ConfigFile == "" var opts = ConfigProcessor.ProcessConfig(""); opts.Port.ShouldBe(4222); opts.Host.ShouldBe("0.0.0.0"); } // ─── MaxClosedClients ────────────────────────────────────────────────────── /// /// Go: TestMaxClosedClients server/opts_test.go:1340 /// /// Verifies that max_closed_clients is parsed correctly. /// [Fact] public void MaxClosedClients_Parsed() { // Go: max_closed_clients: 5 → opts.MaxClosedClients == 5 var opts = ConfigProcessor.ProcessConfig("max_closed_clients: 5"); opts.MaxClosedClients.ShouldBe(5); } // ─── PingInterval ───────────────────────────────────────────────────────── /// /// Go: TestPingIntervalNew server/opts_test.go:1369 /// /// Verifies that a quoted duration string for ping_interval parses correctly. /// [Fact] public void PingInterval_QuotedDurationString_Parsed() { // Go: ping_interval: "5m" → opts.PingInterval = 5 minutes var opts = ConfigProcessor.ProcessConfig("ping_interval: \"5m\""); opts.PingInterval.ShouldBe(TimeSpan.FromMinutes(5)); } [Fact] public void PingInterval_BareIntegerSeconds_Parsed() { // Go: TestPingIntervalOld — ping_interval: 5 (bare integer treated as seconds) var opts = ConfigProcessor.ProcessConfig("ping_interval: 5"); opts.PingInterval.ShouldBe(TimeSpan.FromSeconds(5)); } }