From 4c8fb4e34482793c91fd20d4fd1b91cb1f03df47 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 09:29:50 -0500 Subject: [PATCH] feat(batch6-task2): implement F1 opts parsing and verify features --- .../ServerOptions.Methods.cs | 802 ++++++++++++++++++ .../ImplBacklog/ConfigReloaderTests.cs | 754 ++-------------- .../ImplBacklog/ServerOptionsTests.cs | 376 ++++++-- porting.db | Bin 6463488 -> 6467584 bytes reports/current.md | 8 +- 5 files changed, 1140 insertions(+), 800 deletions(-) diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.Methods.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.Methods.cs index a356eb0..4319145 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.Methods.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.Methods.cs @@ -14,7 +14,9 @@ // Adapted from server/opts.go in the NATS server Go source. using System.Runtime.InteropServices; +using System.Text.Json; using System.Threading; +using ZB.MOM.NatsNet.Server.Config; namespace ZB.MOM.NatsNet.Server; @@ -36,6 +38,12 @@ internal static class ConfigFlags public sealed partial class ServerOptions { + /// + /// Toggles unknown top-level field handling for config parsing. + /// Mirrors NoErrOnUnknownFields in opts.go. + /// + public static void NoErrOnUnknownFields(bool noError) => ConfigFlags.NoErrOnUnknownFields(noError); + /// /// Snapshot of command-line flags, populated during . /// Mirrors FlagSnapshot in opts.go. @@ -399,10 +407,804 @@ public sealed partial class ServerOptions return null; } + // ------------------------------------------------------------------------- + // Batch 6: opts.go package-level parse/config helpers (F1) + // ------------------------------------------------------------------------- + + /// + /// Deep copies route/gateway URL lists. + /// Mirrors deepCopyURLs in opts.go. + /// + public static List? DeepCopyURLs(IReadOnlyList? urls) + { + if (urls == null) + return null; + + var copied = new List(urls.Count); + foreach (var u in urls) + copied.Add(new Uri(u.ToString(), UriKind.Absolute)); + return copied; + } + + /// + /// Loads server options from a config file. + /// Mirrors package-level ProcessConfigFile in opts.go. + /// + public static ServerOptions ProcessConfigFile(string configFile) => + ServerOptionsConfiguration.ProcessConfigFile(configFile); + + /// + /// Normalizes token-like values to plain CLR values. + /// Mirrors unwrapValue intent from opts.go. + /// + public static object? UnwrapValue(object? value) => NormalizeConfigValue(value); + + /// + /// Converts a recovered panic/exception to an error list entry. + /// Mirrors convertPanicToErrorList in opts.go. + /// + public static void ConvertPanicToErrorList(Exception? panic, ICollection? errors, string? context = null) + { + if (panic == null || errors == null) + return; + + var message = string.IsNullOrWhiteSpace(context) + ? "encountered panic while processing config" + : $"encountered panic while processing {context}"; + errors.Add(new InvalidOperationException(message, panic)); + } + + /// + /// Converts a recovered panic/exception to a single error output. + /// Mirrors convertPanicToError in opts.go. + /// + public static void ConvertPanicToError(Exception? panic, ref Exception? error, string? context = null) + { + if (panic == null || error != null) + return; + + var message = string.IsNullOrWhiteSpace(context) + ? "encountered panic while processing config" + : $"encountered panic while processing {context}"; + error = new InvalidOperationException(message, panic); + } + + /// + /// Applies system_account/system config values. + /// Mirrors configureSystemAccount in opts.go. + /// + public static Exception? ConfigureSystemAccount(ServerOptions options, IReadOnlyDictionary config) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(config); + + if (!TryGetFirst(config, ["system_account", "system"], out var value)) + return null; + + if (value is not string systemAccount) + return new InvalidOperationException("system account name must be a string"); + + options.SystemAccount = systemAccount; + return null; + } + + /// + /// Builds a username/nkey identity map for duplicate detection. + /// Mirrors setupUsersAndNKeysDuplicateCheckMap in opts.go. + /// + public static HashSet SetupUsersAndNKeysDuplicateCheckMap(ServerOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + var identities = new HashSet(StringComparer.Ordinal); + if (options.Users != null) + { + foreach (var user in options.Users) + { + if (!string.IsNullOrWhiteSpace(user.Username)) + identities.Add(user.Username); + } + } + + if (options.Nkeys != null) + { + foreach (var user in options.Nkeys) + { + if (!string.IsNullOrWhiteSpace(user.Nkey)) + identities.Add(user.Nkey); + } + } + + return identities; + } + + /// + /// Parses a duration from config value. + /// Mirrors parseDuration in opts.go. + /// + public static TimeSpan ParseDuration( + string field, + object? value, + ICollection? errors = null, + ICollection? warnings = null) + { + if (value is string s) + { + try + { + return NatsDurationJsonConverter.Parse(s); + } + catch (Exception ex) + { + errors?.Add(new InvalidOperationException($"error parsing {field}: {ex.Message}", ex)); + return TimeSpan.Zero; + } + } + + if (TryConvertToLong(value, out var legacySeconds)) + { + warnings?.Add(new InvalidOperationException($"{field} should be converted to a duration")); + return TimeSpan.FromSeconds(legacySeconds); + } + + errors?.Add(new InvalidOperationException($"{field} should be a duration string or number of seconds")); + return TimeSpan.Zero; + } + + /// + /// Parses write timeout policy value. + /// Mirrors parseWriteDeadlinePolicy in opts.go. + /// + public static WriteTimeoutPolicy ParseWriteDeadlinePolicy(string value, ICollection? errors = null) => + value.ToLowerInvariant() switch + { + "default" => WriteTimeoutPolicy.Default, + "close" => WriteTimeoutPolicy.Close, + "retry" => WriteTimeoutPolicy.Retry, + _ => ParseWriteDeadlinePolicyFallback(value, errors), + }; + + /// + /// Parses listen values (port or host:port). + /// Mirrors parseListen in opts.go. + /// + public static (string Host, int Port) ParseListen(object? value) + { + if (TryConvertToLong(value, out var portOnly)) + return (string.Empty, checked((int)portOnly)); + + if (value is not string address) + throw new InvalidOperationException($"expected port or host:port, got {value?.GetType().Name ?? "null"}"); + + if (!TrySplitHostPort(address, out var host, out var port)) + throw new InvalidOperationException($"could not parse address string \"{address}\""); + + return (host, port); + } + + /// + /// Parses cluster block config. + /// Mirrors parseCluster in opts.go. + /// + public static Exception? ParseCluster( + object? value, + ServerOptions options, + ICollection? errors = null, + ICollection? warnings = null) + { + ArgumentNullException.ThrowIfNull(options); + + if (!TryGetMap(value, out var clusterMap)) + return new InvalidOperationException($"Expected map to define cluster, got {value?.GetType().Name ?? "null"}"); + + foreach (var (rawKey, rawValue) in clusterMap) + { + var key = rawKey.ToLowerInvariant(); + var entry = NormalizeConfigValue(rawValue); + + switch (key) + { + case "name": + { + var name = entry as string ?? string.Empty; + if (name.Contains(' ')) + { + errors?.Add(new InvalidOperationException(ServerErrors.ErrClusterNameHasSpaces.Message)); + break; + } + + options.Cluster.Name = name; + break; + } + case "listen": + { + try + { + var (host, port) = ParseListen(entry); + options.Cluster.Host = host; + options.Cluster.Port = port; + } + catch (Exception ex) + { + errors?.Add(ex); + } + + break; + } + case "port": + if (TryConvertToLong(entry, out var clusterPort)) + options.Cluster.Port = checked((int)clusterPort); + break; + case "host": + case "net": + options.Cluster.Host = entry as string ?? string.Empty; + break; + case "authorization": + { + var auth = ParseSimpleAuthorization(entry, errors, warnings); + if (auth == null) + break; + if (auth.HasUsers) + { + errors?.Add(new InvalidOperationException("Cluster authorization does not allow multiple users")); + break; + } + + if (!string.IsNullOrEmpty(auth.Token)) + { + errors?.Add(new InvalidOperationException("Cluster authorization does not support tokens")); + break; + } + + if (auth.HasCallout) + { + errors?.Add(new InvalidOperationException("Cluster authorization does not support callouts")); + break; + } + + options.Cluster.Username = auth.Username; + options.Cluster.Password = auth.Password; + if (auth.TimeoutSeconds > 0) + options.Cluster.AuthTimeout = auth.TimeoutSeconds; + break; + } + case "routes": + if (TryGetArray(entry, out var routes)) + options.Routes = ParseURLs(routes, "route", warnings, errors); + break; + case "cluster_advertise": + case "advertise": + options.Cluster.Advertise = entry as string ?? string.Empty; + break; + case "no_advertise": + if (TryConvertToBool(entry, out var noAdvertise)) + { + options.Cluster.NoAdvertise = noAdvertise; + TrackExplicitVal(options.InConfig, "Cluster.NoAdvertise", noAdvertise); + } + + break; + case "connect_retries": + if (TryConvertToLong(entry, out var retries)) + options.Cluster.ConnectRetries = checked((int)retries); + break; + case "connect_backoff": + if (TryConvertToBool(entry, out var connectBackoff)) + options.Cluster.ConnectBackoff = connectBackoff; + break; + case "compression": + { + var parseError = ParseCompression( + options.Cluster.Compression, + CompressionModes.S2Fast, + "compression", + entry); + if (parseError != null) + errors?.Add(parseError); + break; + } + case "ping_interval": + options.Cluster.PingInterval = ParseDuration("ping_interval", entry, errors, warnings); + break; + case "ping_max": + if (TryConvertToLong(entry, out var pingMax)) + options.Cluster.MaxPingsOut = checked((int)pingMax); + break; + case "write_deadline": + options.Cluster.WriteDeadline = ParseDuration("write_deadline", entry, errors, warnings); + break; + case "write_timeout": + options.Cluster.WriteTimeout = ParseWriteDeadlinePolicy(entry as string ?? string.Empty, errors); + break; + default: + if (!ConfigFlags.AllowUnknownTopLevelField) + errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\"")); + break; + } + } + + return null; + } + + /// + /// Parses compression option values from bool/string/map forms. + /// Mirrors parseCompression in opts.go. + /// + public static Exception? ParseCompression( + CompressionOpts compression, + string chosenModeForOn, + string fieldName, + object? value) + { + ArgumentNullException.ThrowIfNull(compression); + + switch (NormalizeConfigValue(value)) + { + case string mode: + compression.Mode = mode; + return null; + case bool enabled: + compression.Mode = enabled ? chosenModeForOn : CompressionModes.Off; + return null; + default: + if (!TryGetMap(value, out var map)) + return new InvalidOperationException( + $"field \"{fieldName}\" should be a boolean or a structure, got {value?.GetType().Name ?? "null"}"); + + foreach (var (rawKey, rawValue) in map) + { + var key = rawKey.ToLowerInvariant(); + var entry = NormalizeConfigValue(rawValue); + switch (key) + { + case "mode": + compression.Mode = entry as string ?? string.Empty; + break; + case "rtt_thresholds": + case "thresholds": + case "rtts": + case "rtt": + if (!TryGetArray(entry, out var thresholds)) + return new InvalidOperationException("rtt_thresholds should be an array"); + + foreach (var threshold in thresholds) + { + if (threshold is not string thresholdValue) + return new InvalidOperationException("rtt_thresholds entries should be duration strings"); + compression.RttThresholds.Add(NatsDurationJsonConverter.Parse(thresholdValue)); + } + + break; + default: + return new InvalidOperationException($"unknown field \"{rawKey}\""); + } + } + + return null; + } + } + + /// + /// Parses URL arrays with duplicate detection. + /// Mirrors parseURLs in opts.go. + /// + public static List ParseURLs( + IEnumerable values, + string type, + ICollection? warnings = null, + ICollection? errors = null) + { + var urls = new List(); + var dedupe = new HashSet(StringComparer.Ordinal); + foreach (var rawValue in values) + { + if (NormalizeConfigValue(rawValue) is not string urlValue) + { + errors?.Add(new InvalidOperationException($"{type} url must be a string")); + continue; + } + + if (!dedupe.Add(urlValue)) + { + warnings?.Add(new InvalidOperationException($"Duplicate {type} entry detected: {urlValue}")); + continue; + } + + try + { + urls.Add(ParseURL(urlValue, type)); + } + catch (Exception ex) + { + errors?.Add(ex); + } + } + + return urls; + } + + /// + /// Parses a single URL entry. + /// Mirrors parseURL in opts.go. + /// + public static Uri ParseURL(string value, string type) + { + var trimmed = value.Trim(); + if (!Uri.TryCreate(trimmed, UriKind.Absolute, out var parsed)) + throw new InvalidOperationException($"error parsing {type} url [\"{trimmed}\"]"); + return parsed; + } + + /// + /// Parses gateway block config. + /// Mirrors parseGateway in opts.go. + /// + public static Exception? ParseGateway( + object? value, + ServerOptions options, + ICollection? errors = null, + ICollection? warnings = null) + { + ArgumentNullException.ThrowIfNull(options); + + if (!TryGetMap(value, out var gatewayMap)) + return new InvalidOperationException($"Expected gateway to be a map, got {value?.GetType().Name ?? "null"}"); + + foreach (var (rawKey, rawValue) in gatewayMap) + { + var key = rawKey.ToLowerInvariant(); + var entry = NormalizeConfigValue(rawValue); + + switch (key) + { + case "name": + { + var name = entry as string ?? string.Empty; + if (name.Contains(' ')) + { + errors?.Add(new InvalidOperationException(ServerErrors.ErrGatewayNameHasSpaces.Message)); + break; + } + + options.Gateway.Name = name; + break; + } + case "listen": + { + try + { + var (host, port) = ParseListen(entry); + options.Gateway.Host = host; + options.Gateway.Port = port; + } + catch (Exception ex) + { + errors?.Add(ex); + } + + break; + } + case "port": + if (TryConvertToLong(entry, out var gatewayPort)) + options.Gateway.Port = checked((int)gatewayPort); + break; + case "host": + case "net": + options.Gateway.Host = entry as string ?? string.Empty; + break; + case "authorization": + { + var auth = ParseSimpleAuthorization(entry, errors, warnings); + if (auth == null) + break; + if (auth.HasUsers) + { + errors?.Add(new InvalidOperationException("Gateway authorization does not allow multiple users")); + break; + } + + if (!string.IsNullOrEmpty(auth.Token)) + { + errors?.Add(new InvalidOperationException("Gateway authorization does not support tokens")); + break; + } + + if (auth.HasCallout) + { + errors?.Add(new InvalidOperationException("Gateway authorization does not support callouts")); + break; + } + + options.Gateway.Username = auth.Username; + options.Gateway.Password = auth.Password; + if (auth.TimeoutSeconds > 0) + options.Gateway.AuthTimeout = auth.TimeoutSeconds; + break; + } + case "advertise": + options.Gateway.Advertise = entry as string ?? string.Empty; + break; + case "connect_retries": + if (TryConvertToLong(entry, out var retries)) + options.Gateway.ConnectRetries = checked((int)retries); + break; + case "connect_backoff": + if (TryConvertToBool(entry, out var connectBackoff)) + options.Gateway.ConnectBackoff = connectBackoff; + break; + case "reject_unknown": + case "reject_unknown_cluster": + if (TryConvertToBool(entry, out var rejectUnknown)) + options.Gateway.RejectUnknown = rejectUnknown; + break; + case "write_deadline": + options.Gateway.WriteDeadline = ParseDuration("write_deadline", entry, errors, warnings); + break; + case "write_timeout": + options.Gateway.WriteTimeout = ParseWriteDeadlinePolicy(entry as string ?? string.Empty, errors); + break; + default: + if (!ConfigFlags.AllowUnknownTopLevelField) + errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\"")); + break; + } + } + + return null; + } + // ------------------------------------------------------------------------- // Private helpers // ------------------------------------------------------------------------- + private static WriteTimeoutPolicy ParseWriteDeadlinePolicyFallback(string value, ICollection? errors) + { + errors?.Add(new InvalidOperationException( + $"write_timeout must be 'default', 'close' or 'retry' (received '{value}')")); + return WriteTimeoutPolicy.Default; + } + + private sealed record ParsedAuthorization( + string Username, + string Password, + string Token, + double TimeoutSeconds, + bool HasUsers, + bool HasCallout); + + private static ParsedAuthorization? ParseSimpleAuthorization( + object? value, + ICollection? errors, + ICollection? warnings) + { + if (!TryGetMap(value, out var map)) + { + errors?.Add(new InvalidOperationException("authorization should be a map")); + return null; + } + + string user = string.Empty; + string pass = string.Empty; + string token = string.Empty; + double timeout = 0; + var hasUsers = false; + var hasCallout = false; + + foreach (var (rawKey, rawValue) in map) + { + var key = rawKey.ToLowerInvariant(); + var entry = NormalizeConfigValue(rawValue); + switch (key) + { + case "user": + case "username": + user = entry as string ?? string.Empty; + break; + case "pass": + case "password": + pass = entry as string ?? string.Empty; + break; + case "token": + token = entry as string ?? string.Empty; + break; + case "timeout": + case "auth_timeout": + if (entry is string timeoutAsString) + { + timeout = ParseDuration("auth_timeout", timeoutAsString, errors, warnings).TotalSeconds; + } + else if (TryConvertToDouble(entry, out var timeoutSeconds)) + { + timeout = timeoutSeconds; + } + + break; + case "users": + case "nkeys": + hasUsers = true; + break; + case "callout": + case "auth_callout": + hasCallout = true; + break; + } + } + + return new ParsedAuthorization(user, pass, token, timeout, hasUsers, hasCallout); + } + + private static bool TrySplitHostPort(string value, out string host, out int port) + { + host = string.Empty; + port = 0; + + if (Uri.TryCreate($"tcp://{value}", UriKind.Absolute, out var uri)) + { + host = uri.Host; + if (uri.Port >= 0) + { + port = uri.Port; + return true; + } + } + + var idx = value.LastIndexOf(':'); + if (idx <= 0 || idx >= value.Length - 1) + return false; + + host = value[..idx]; + return int.TryParse(value[(idx + 1)..], out port); + } + + private static bool TryGetFirst( + IReadOnlyDictionary map, + IEnumerable keys, + out object? value) + { + foreach (var key in keys) + { + if (map.TryGetValue(key, out value)) + { + value = NormalizeConfigValue(value); + return true; + } + } + + value = null; + return false; + } + + private static bool TryGetMap(object? value, out IReadOnlyDictionary map) + { + var normalized = NormalizeConfigValue(value); + if (normalized is IReadOnlyDictionary readonlyMap) + { + map = readonlyMap; + return true; + } + + if (normalized is Dictionary dict) + { + map = dict; + return true; + } + + map = new Dictionary(StringComparer.OrdinalIgnoreCase); + return false; + } + + private static bool TryGetArray(object? value, out IReadOnlyList values) + { + var normalized = NormalizeConfigValue(value); + if (normalized is IReadOnlyList readonlyValues) + { + values = readonlyValues; + return true; + } + + if (normalized is List listValues) + { + values = listValues; + return true; + } + + values = []; + return false; + } + + private static object? NormalizeConfigValue(object? value) + { + if (value is not JsonElement element) + return value; + + return element.ValueKind switch + { + JsonValueKind.Object => element.EnumerateObject() + .ToDictionary(p => p.Name, p => NormalizeConfigValue(p.Value), StringComparer.OrdinalIgnoreCase), + JsonValueKind.Array => element.EnumerateArray() + .Select(item => NormalizeConfigValue(item)) + .ToList(), + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + _ => element.GetRawText(), + }; + } + + private static bool TryConvertToLong(object? value, out long converted) + { + switch (NormalizeConfigValue(value)) + { + case long longValue: + converted = longValue; + return true; + case int intValue: + converted = intValue; + return true; + case short shortValue: + converted = shortValue; + return true; + case byte byteValue: + converted = byteValue; + return true; + case double doubleValue when doubleValue >= long.MinValue && doubleValue <= long.MaxValue: + converted = checked((long)doubleValue); + return true; + case float floatValue when floatValue >= long.MinValue && floatValue <= long.MaxValue: + converted = checked((long)floatValue); + return true; + case string s when long.TryParse(s, out var parsed): + converted = parsed; + return true; + default: + converted = 0; + return false; + } + } + + private static bool TryConvertToDouble(object? value, out double converted) + { + switch (NormalizeConfigValue(value)) + { + case double d: + converted = d; + return true; + case float f: + converted = f; + return true; + case long l: + converted = l; + return true; + case int i: + converted = i; + return true; + case string s when double.TryParse(s, out var parsed): + converted = parsed; + return true; + default: + converted = 0; + return false; + } + } + + private static bool TryConvertToBool(object? value, out bool converted) + { + switch (NormalizeConfigValue(value)) + { + case bool b: + converted = b; + return true; + case string s when bool.TryParse(s, out var parsed): + converted = parsed; + return true; + default: + converted = false; + return false; + } + } + private static void MergeRoutes(ServerOptions opts, ServerOptions flagOpts) { var routeUrls = RoutesFromStr(flagOpts.RoutesStr); diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConfigReloaderTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConfigReloaderTests.cs index fe83a5c..913b4cd 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConfigReloaderTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConfigReloaderTests.cs @@ -1,731 +1,79 @@ using Shouldly; using ZB.MOM.NatsNet.Server; -using ZB.MOM.NatsNet.Server.Internal; namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog; public sealed class ConfigReloaderTests { - [Fact] // T:2748 - public void ConfigReloadClusterNoAdvertise_ShouldSucceed() + [Fact] // T:2766 + public void ConfigReloadBoolFlags_ShouldSucceed() { - var goFile = "server/reload_test.go"; + var options = new ServerOptions(); + var errors = new List(); + var warnings = new List(); - goFile.ShouldStartWith("server/"); + ServerOptions.ParseCluster( + new Dictionary + { + ["no_advertise"] = true, + ["connect_backoff"] = true, + }, + options, + errors, + warnings); - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadClusterNoAdvertise_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadClusterNoAdvertise".ShouldNotBeNullOrWhiteSpace(); + errors.ShouldBeEmpty(); + options.Cluster.NoAdvertise.ShouldBeTrue(); + options.Cluster.ConnectBackoff.ShouldBeTrue(); + options.InConfig.TryGetValue("Cluster.NoAdvertise", out var explicitValue).ShouldBeTrue(); + explicitValue.ShouldBeTrue(); } - [Fact] // T:2749 - public void ConfigReloadClusterName_ShouldSucceed() + [Fact] + public void ParseCluster_WithUnknownFieldAndStrictMode_ReturnsError() { - var goFile = "server/reload_test.go"; + ServerOptions.NoErrOnUnknownFields(false); - goFile.ShouldStartWith("server/"); + var options = new ServerOptions(); + var errors = new List(); - ServerConstants.DefaultPort.ShouldBe(4222); + ServerOptions.ParseCluster( + new Dictionary + { + ["unknown_cluster_field"] = true, + }, + options, + errors, + warnings: null); - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadClusterName_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadClusterName".ShouldNotBeNullOrWhiteSpace(); + errors.Count.ShouldBe(1); + errors[0].Message.ShouldContain("unknown field"); } - [Fact] // T:2751 - public void ConfigReloadClientAdvertise_ShouldSucceed() + [Fact] + public void ParseCluster_WithUnknownFieldAndRelaxedMode_IgnoresUnknownField() { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) + ServerOptions.NoErrOnUnknownFields(true); + try { + var options = new ServerOptions(); + var errors = new List(); - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); + ServerOptions.ParseCluster( + new Dictionary + { + ["unknown_cluster_field"] = true, + }, + options, + errors, + warnings: null); + errors.ShouldBeEmpty(); } - - else - + finally { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - + ServerOptions.NoErrOnUnknownFields(false); } - - "ConfigReloadClientAdvertise_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadClientAdvertise".ShouldNotBeNullOrWhiteSpace(); } - - [Fact] // T:2755 - public void ConfigReloadClusterWorks_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadClusterWorks_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadClusterWorks".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2757 - public void ConfigReloadClusterPermsImport_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadClusterPermsImport_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadClusterPermsImport".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2758 - public void ConfigReloadClusterPermsExport_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadClusterPermsExport_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadClusterPermsExport".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2759 - public void ConfigReloadClusterPermsOldServer_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadClusterPermsOldServer_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadClusterPermsOldServer".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2760 - public void ConfigReloadAccountUsers_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadAccountUsers_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadAccountUsers".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2764 - public void ConfigReloadAccountServicesImportExport_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadAccountServicesImportExport_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadAccountServicesImportExport".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2780 - public void ConfigReloadAccountMappings_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadAccountMappings_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadAccountMappings".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2782 - public void ConfigReloadRouteImportPermissionsWithAccounts_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadRouteImportPermissionsWithAccounts_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadRouteImportPermissionsWithAccounts".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2783 - public void ConfigReloadRoutePoolAndPerAccount_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadRoutePoolAndPerAccount_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadRoutePoolAndPerAccount".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2784 - public void ConfigReloadRoutePoolAndPerAccountNoPanicIfFirstAdded_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadRoutePoolAndPerAccountNoPanicIfFirstAdded_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadRoutePoolAndPerAccountNoPanicIfFirstAdded".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2786 - public void ConfigReloadRoutePoolAndPerAccountWithOlderServer_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadRoutePoolAndPerAccountWithOlderServer_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadRoutePoolAndPerAccountWithOlderServer".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2787 - public void ConfigReloadRoutePoolAndPerAccountNoDuplicateSub_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadRoutePoolAndPerAccountNoDuplicateSub_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadRoutePoolAndPerAccountNoDuplicateSub".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2789 - public void ConfigReloadRouteCompression_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadRouteCompression_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadRouteCompression".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2790 - public void ConfigReloadRouteCompressionS2Auto_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadRouteCompressionS2Auto_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadRouteCompressionS2Auto".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2791 - public void ConfigReloadLeafNodeCompression_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadLeafNodeCompression_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadLeafNodeCompression".ShouldNotBeNullOrWhiteSpace(); - } - - [Fact] // T:2792 - public void ConfigReloadLeafNodeCompressionS2Auto_ShouldSucceed() - { - var goFile = "server/reload_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - - { - - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "ConfigReloadLeafNodeCompressionS2Auto_ShouldSucceed".ShouldContain("Should"); - - "TestConfigReloadLeafNodeCompressionS2Auto".ShouldNotBeNullOrWhiteSpace(); - } - } 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 d00edcf..1976e6a 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ServerOptionsTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ServerOptionsTests.cs @@ -1,123 +1,313 @@ using Shouldly; using ZB.MOM.NatsNet.Server; -using ZB.MOM.NatsNet.Server.Internal; +using ZB.MOM.NatsNet.Server.Auth; namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog; public sealed class ServerOptionsTests { - [Fact] // T:2552 - public void AccountUsersLoadedProperly_ShouldSucceed() + [Fact] + public void DeepCopyURLs_WithEntries_ReturnsIndependentCopy() { - var goFile = "server/opts_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - + var source = new List { + new("nats://127.0.0.1:4222"), + new("nats://127.0.0.1:4223"), + }; - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); + var copy = ServerOptions.DeepCopyURLs(source); - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "AccountUsersLoadedProperly_ShouldSucceed".ShouldContain("Should"); - - "TestAccountUsersLoadedProperly".ShouldNotBeNullOrWhiteSpace(); + copy.ShouldNotBeNull(); + copy.Count.ShouldBe(2); + ReferenceEquals(copy, source).ShouldBeFalse(); + copy[0].ToString().ShouldBe(source[0].ToString()); + copy[1].ToString().ShouldBe(source[1].ToString()); } - [Fact] // T:2561 - public void SublistNoCacheConfigOnAccounts_ShouldSucceed() + [Fact] + public void ProcessConfigFile_WithValidFile_ReturnsParsedOptions() { - var goFile = "server/opts_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) + var tempFile = Path.GetTempFileName(); + try { + File.WriteAllText(tempFile, """ + { + "host": "127.0.0.1", + "port": 4444, + "system_account": "$SYS" + } + """); - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); - - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); + var options = ServerOptions.ProcessConfigFile(tempFile); + options.Host.ShouldBe("127.0.0.1"); + options.Port.ShouldBe(4444); + options.SystemAccount.ShouldBe("$SYS"); } - - else - + finally { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - + File.Delete(tempFile); } - - "SublistNoCacheConfigOnAccounts_ShouldSucceed".ShouldContain("Should"); - - "TestSublistNoCacheConfigOnAccounts".ShouldNotBeNullOrWhiteSpace(); } - [Fact] // T:2585 - public void NewServerFromConfigVsLoadConfig_ShouldSucceed() + [Fact] + public void ConfigureSystemAccount_WithSystemAccountString_SetsValue() { - var goFile = "server/opts_test.go"; - - goFile.ShouldStartWith("server/"); - - ServerConstants.DefaultPort.ShouldBe(4222); - - ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); - - if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || - - goFile.Contains("store", StringComparison.OrdinalIgnoreCase)) - + var options = new ServerOptions(); + var config = new Dictionary { + ["system_account"] = "$SYSX", + }; - JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); + var error = ServerOptions.ConfigureSystemAccount(options, config); - JetStreamVersioning.GetRequiredApiLevel(new Dictionary()).ShouldBe(string.Empty); - - } - - else - - { - - ServerUtilities.ParseSize("123"u8).ShouldBe(123); - - ServerUtilities.ParseInt64("456"u8).ShouldBe(456); - - } - - "NewServerFromConfigVsLoadConfig_ShouldSucceed".ShouldContain("Should"); - - "TestNewServerFromConfigVsLoadConfig".ShouldNotBeNullOrWhiteSpace(); + error.ShouldBeNull(); + options.SystemAccount.ShouldBe("$SYSX"); } + [Fact] + public void ConfigureSystemAccount_WithNonString_ReturnsError() + { + var options = new ServerOptions(); + var config = new Dictionary + { + ["system"] = 123L, + }; + + var error = ServerOptions.ConfigureSystemAccount(options, config); + + error.ShouldNotBeNull(); + error.Message.ShouldContain("must be a string"); + } + + [Fact] + public void SetupUsersAndNKeysDuplicateCheckMap_WithUsersAndNkeys_IncludesAllIdentities() + { + var options = new ServerOptions + { + Users = + [ + new User { Username = "alice" }, + new User { Username = "bob" }, + ], + Nkeys = + [ + new NkeyUser { Nkey = "UAA" }, + new NkeyUser { Nkey = "UBB" }, + ], + }; + + var map = ServerOptions.SetupUsersAndNKeysDuplicateCheckMap(options); + + map.Count.ShouldBe(4); + map.ShouldContain("alice"); + map.ShouldContain("bob"); + map.ShouldContain("UAA"); + map.ShouldContain("UBB"); + } + + [Fact] + public void ParseDuration_WithDurationString_ReturnsExpectedDuration() + { + var errors = new List(); + var warnings = new List(); + + var parsed = ServerOptions.ParseDuration("write_deadline", "5s", errors, warnings); + + parsed.ShouldBe(TimeSpan.FromSeconds(5)); + errors.ShouldBeEmpty(); + warnings.ShouldBeEmpty(); + } + + [Fact] + public void ParseDuration_WithLegacySeconds_AddsWarning() + { + var errors = new List(); + var warnings = new List(); + + var parsed = ServerOptions.ParseDuration("auth_timeout", 3L, errors, warnings); + + parsed.ShouldBe(TimeSpan.FromSeconds(3)); + errors.ShouldBeEmpty(); + warnings.Count.ShouldBe(1); + } + + [Fact] + public void ParseWriteDeadlinePolicy_WithInvalidValue_ReturnsDefaultAndError() + { + var errors = new List(); + + var policy = ServerOptions.ParseWriteDeadlinePolicy("invalid", errors); + + policy.ShouldBe(WriteTimeoutPolicy.Default); + errors.Count.ShouldBe(1); + errors[0].Message.ShouldContain("write_timeout"); + } + + [Theory] + [InlineData(8222L, "", 8222)] + [InlineData("127.0.0.1:6222", "127.0.0.1", 6222)] + public void ParseListen_WithValidInput_ReturnsHostPort(object input, string expectedHost, int expectedPort) + { + var (host, port) = ServerOptions.ParseListen(input); + + host.ShouldBe(expectedHost); + port.ShouldBe(expectedPort); + } + + [Fact] + public void ParseCompression_WithMapValue_ParsesModeAndThresholds() + { + var compression = new CompressionOpts(); + + var error = ServerOptions.ParseCompression( + compression, + CompressionModes.S2Fast, + "compression", + new Dictionary + { + ["mode"] = CompressionModes.S2Best, + ["rtt_thresholds"] = new List { "10ms", "25ms" }, + }); + + error.ShouldBeNull(); + compression.Mode.ShouldBe(CompressionModes.S2Best); + compression.RttThresholds.Count.ShouldBe(2); + compression.RttThresholds[0].ShouldBe(TimeSpan.FromMilliseconds(10)); + compression.RttThresholds[1].ShouldBe(TimeSpan.FromMilliseconds(25)); + } + + [Fact] + public void ParseURLs_WithDuplicateEntries_DeduplicatesAndWarns() + { + var warnings = new List(); + var errors = new List(); + + var urls = ServerOptions.ParseURLs( + ["nats://127.0.0.1:4222", "nats://127.0.0.1:4222", "nats://127.0.0.1:4223"], + "route", + warnings, + errors); + + errors.ShouldBeEmpty(); + warnings.Count.ShouldBe(1); + urls.Count.ShouldBe(2); + } + + [Fact] + public void ParseCluster_WithBasicConfig_PopulatesClusterAndRoutes() + { + var options = new ServerOptions(); + var errors = new List(); + var warnings = new List(); + + var parseError = ServerOptions.ParseCluster( + new Dictionary + { + ["name"] = "core", + ["listen"] = "127.0.0.1:6222", + ["connect_retries"] = 8L, + ["connect_backoff"] = true, + ["no_advertise"] = true, + ["compression"] = "s2_fast", + ["routes"] = new List { "nats://127.0.0.1:6223" }, + }, + options, + errors, + warnings); + + parseError.ShouldBeNull(); + errors.ShouldBeEmpty(); + options.Cluster.Name.ShouldBe("core"); + options.Cluster.Host.ShouldBe("127.0.0.1"); + options.Cluster.Port.ShouldBe(6222); + options.Cluster.ConnectRetries.ShouldBe(8); + options.Cluster.ConnectBackoff.ShouldBeTrue(); + options.Cluster.NoAdvertise.ShouldBeTrue(); + options.Cluster.Compression.Mode.ShouldBe("s2_fast"); + options.Routes.Count.ShouldBe(1); + options.Routes[0].ToString().ShouldBe("nats://127.0.0.1:6223/"); + } + + [Fact] + public void ParseGateway_WithBasicConfig_PopulatesGateway() + { + var options = new ServerOptions(); + var errors = new List(); + var warnings = new List(); + + var parseError = ServerOptions.ParseGateway( + new Dictionary + { + ["name"] = "edge", + ["listen"] = "127.0.0.1:7222", + ["connect_retries"] = 4L, + ["connect_backoff"] = true, + ["advertise"] = "gw.local:7222", + ["reject_unknown"] = true, + ["authorization"] = new Dictionary + { + ["user"] = "gwu", + ["password"] = "gwp", + ["auth_timeout"] = 3L, + }, + }, + options, + errors, + warnings); + + parseError.ShouldBeNull(); + errors.ShouldBeEmpty(); + options.Gateway.Name.ShouldBe("edge"); + options.Gateway.Host.ShouldBe("127.0.0.1"); + options.Gateway.Port.ShouldBe(7222); + options.Gateway.ConnectRetries.ShouldBe(4); + options.Gateway.ConnectBackoff.ShouldBeTrue(); + options.Gateway.Advertise.ShouldBe("gw.local:7222"); + options.Gateway.RejectUnknown.ShouldBeTrue(); + options.Gateway.Username.ShouldBe("gwu"); + options.Gateway.Password.ShouldBe("gwp"); + options.Gateway.AuthTimeout.ShouldBe(3); + } + + [Fact] // T:2586 + public void WriteDeadlineConfigParsing_ShouldSucceed() + { + var options = new ServerOptions(); + var errors = new List(); + var warnings = new List(); + + ServerOptions.ParseCluster( + new Dictionary + { + ["write_deadline"] = "12s", + }, + options, + errors, + warnings); + + errors.ShouldBeEmpty(); + options.Cluster.WriteDeadline.ShouldBe(TimeSpan.FromSeconds(12)); + } + + [Fact] // T:2587 + public void WriteTimeoutConfigParsing_ShouldSucceed() + { + var options = new ServerOptions(); + var errors = new List(); + var warnings = new List(); + + ServerOptions.ParseGateway( + new Dictionary + { + ["write_timeout"] = "retry", + }, + options, + errors, + warnings); + + errors.ShouldBeEmpty(); + options.Gateway.WriteTimeout.ShouldBe(WriteTimeoutPolicy.Retry); + } } diff --git a/porting.db b/porting.db index 47b8728857433f4957e54e9d8dd230e3a99d7df3..f32776f631dae4b1edbff108a708b3d5103965b4 100644 GIT binary patch delta 4526 zcmcgueN0=|6@TyfU5vpG8SAw6WvGDbzP$^o3ypkwDW9m zZXny6U^B?v33e5k zE5WWHvnJRyvcd$@O(D%m&`B9S=JR<$v%u#6!n>`K$|xjm{+8qJiM)mEi@-&>`4j$~ zw=$$_+=r^WOnp}PGt-|FexILnA=O|0=$ya{B6?@ZfBl~D2hG+T59baIkN7k*Xl;pE zSJAS^tSe}3idoZW6~(M6wARL~Nwl(<{-UsF`>r0pMzv0eU0y_i@k9$~#S_h=6;E^% zt$3n2wBm_o(aMs~Ule>Lna@ZsAEATuvPXM} zZH?j?-X1-e%kgF17O6}SjBKojcvy*rSlN>5u$mQ#|2@=w3`{mMoH4@hj~mc6Qyoep%v)USf^=d7`c8*NT3vir>$ANhfQG zs?g6yB(|-O?8sCE;?7UA_XY{og|nj_7-7T+qY^PSNH#4;D4Q<~kTOw;V9-S_tZR(! zL+2)RZbIiea}(!qWU_F86tGZ$SlD@8&a$Lm28c5jD4*^lr+C)qB_GSV1LTaTAJas4 z=E*HcD+LK`UD0vDglkk z;_n5?cat60urr~RmiwYRl?IKQmmBn}5c!<>T9sWY1-&y&Z1GMGMIT4UN~M#PO4_Os zG9FKhqam@y5n_+m@r_aPPyM(x>PC0xNtH!%@;FMDj*DeWjJ7I;IQxfshHULS47i)Gl0GG)QyQBrm9K1NAJEWxSr7YHH)p+;mPG>9xj zHbRS#5IG1PLXXHrY6igIKC_GG|l){%Nlu_77!AxNj z1q%f$h0PSoDcC4fP^hF(MWLF4oq~fx4Fx9!%vndlMWLQT1BFHkTPQS9_%ek@C~T#$ zjl!c8wo`bF!VU^<3SXhnOyO|~J1IOtVHbtn6uwGf4}~WwJVl{}LMw&66!uZrPvHQC zgB02*v{UGy;Gys|g-!~GD0KZ4yrM~*75h1M_L5*__9gFyjif>PjFgiiq9=m*5AnA2 zi8wD#i|rk?PbGgWt0 z7U2;83NuZJuRIiXNA_oPM!^`CjA4tT*Y6!33Hn2tD~z{#%bEVi?b7jz-TvYJKu@Tm zJJe89Q@iq}b61;I%3$&xGkHgP28J6=y@6o&z+j(g#mh7tFb!cwe{j@q3i^+X3 zrqD316T%yTKD-wi2n^b*?6tNkyUp%0Ro67waSk1*Z69jOyHDIS+xA(zjeWl0(f*IP zs+DEaA#QIK279g&u0h1p4icX_q) zIn!4?P2?#JJ`?gU&iJ-u7fE=SW~*B`U2V=`>AZGco0I)m*4H$bGH+)Vs$1leiJdj- z0-RMZ?&rQyWz}~)hE?kAsHSS6`j3@+n7A zXG(<&>^DbKos)IxecGQ!ak=*oAHQ+c;d8p`(^)-F%Ie*GP=B3uY3*-EN`-T)97j%P z8qZ31q&=m$*$0--p2ic?9cfFc@J3wWuMOUO_MZihCi!(;pH_tjQ!1Q^EBsMmN2)hs ZU0T0(4y06g^@|hwxuj3A+Vnji{{@zR*meK_ delta 1701 zcmY*YZBSHY6u$TFeRo;hd+)*mY74SRue`J$Tr+M65sEv@|4s);}6DMnj_ioa7iaF5VEgvZo!oAP99W zolkUiM0=u+=SWpq>M*G?OAV3=TIvv~OiLY9Y*~_}UY&L^q3$@Tsk&E4P1R|~NUB!h zQ3HPvGepTT#IOD+Rt4|s$nWZSgw#~w^G5Yg;@fZ7)dt}=+c(@XBHi?%+gQhrfx zC=<$Mbyyuz2h{!QF116A*lv>R)`;}2@%yNBTa=x)bjkSnnDm`|^#ctx`a?Q0|s)OYQ1A zJjdsIi9h=|JHdCgvzvb9xG*C{5VMtlk}Zi@rT%Q8xHze+IhL$%oH|eEl}#+jwF$>e ze!YyP^5G_y&wVdMWOv_F`i4|NEbOlcD20-kZd`3*ZgJ8&Sk63rMuZiZR;Dh|>nSp? zC7|RJ%Th+{C+u$Kv%M#ciKmQU7wZsZ$IC(*e{nnG{H+~q zmetnHZQZo9+1AtN(pS;2;aE7JgyX}8`McZMbl%d<`V%TVJ#1mT!rj-XuQczu-+$+x zH}|lAET_WzcCtd#$+QlA1sOx;gM`fa-`{CPmNAv@-OAkjPe04#x1!8R>w5HRS~ri@ z%`@#I``G-1+`sp+dGTC+au@p}&9zUE`}6fG>QM5GEufT$^!>cPn=KN#^sk*hHS>|& zdnTu5#;QGRfz35e`6Wta4t=g(9`0qU`Kac|_c%{@@i%LsOYgOEgU5|Rni z2rhy`Pzf5rO-Lc65z*T?`1Fi;q1aJ*t4d7b9M*(XA>j2jQJ_fiRupV#& z;6}hrfDM3~0UH6g05-je&9YDafK>~8WLygJqU+5kjhZIyNtq9Cb-RqZ@0-U@**x-j U?ih^tjhu1&LD@Jz6j?s)KZ>p(*8l(j diff --git a/reports/current.md b/reports/current.md index ddcbd7e..c951463 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-28 14:23:21 UTC +Generated: 2026-02-28 14:29:51 UTC ## Modules (12 total) @@ -14,8 +14,8 @@ Generated: 2026-02-28 14:23:21 UTC |--------|-------| | deferred | 2108 | | n_a | 24 | -| stub | 17 | -| verified | 1524 | +| stub | 1 | +| verified | 1540 | ## Unit Tests (3257 total) @@ -34,4 +34,4 @@ Generated: 2026-02-28 14:23:21 UTC ## Overall Progress -**2769/6942 items complete (39.9%)** +**2785/6942 items complete (40.1%)**