feat(batch6-task2): implement F1 opts parsing and verify features

This commit is contained in:
Joseph Doherty
2026-02-28 09:29:50 -05:00
parent edc2afbb2f
commit 4c8fb4e344
5 changed files with 1140 additions and 800 deletions

View File

@@ -14,7 +14,9 @@
// Adapted from server/opts.go in the NATS server Go source. // Adapted from server/opts.go in the NATS server Go source.
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading; using System.Threading;
using ZB.MOM.NatsNet.Server.Config;
namespace ZB.MOM.NatsNet.Server; namespace ZB.MOM.NatsNet.Server;
@@ -36,6 +38,12 @@ internal static class ConfigFlags
public sealed partial class ServerOptions public sealed partial class ServerOptions
{ {
/// <summary>
/// Toggles unknown top-level field handling for config parsing.
/// Mirrors <c>NoErrOnUnknownFields</c> in opts.go.
/// </summary>
public static void NoErrOnUnknownFields(bool noError) => ConfigFlags.NoErrOnUnknownFields(noError);
/// <summary> /// <summary>
/// Snapshot of command-line flags, populated during <see cref="ConfigureOptions"/>. /// Snapshot of command-line flags, populated during <see cref="ConfigureOptions"/>.
/// Mirrors <c>FlagSnapshot</c> in opts.go. /// Mirrors <c>FlagSnapshot</c> in opts.go.
@@ -399,10 +407,804 @@ public sealed partial class ServerOptions
return null; return null;
} }
// -------------------------------------------------------------------------
// Batch 6: opts.go package-level parse/config helpers (F1)
// -------------------------------------------------------------------------
/// <summary>
/// Deep copies route/gateway URL lists.
/// Mirrors <c>deepCopyURLs</c> in opts.go.
/// </summary>
public static List<Uri>? DeepCopyURLs(IReadOnlyList<Uri>? urls)
{
if (urls == null)
return null;
var copied = new List<Uri>(urls.Count);
foreach (var u in urls)
copied.Add(new Uri(u.ToString(), UriKind.Absolute));
return copied;
}
/// <summary>
/// Loads server options from a config file.
/// Mirrors package-level <c>ProcessConfigFile</c> in opts.go.
/// </summary>
public static ServerOptions ProcessConfigFile(string configFile) =>
ServerOptionsConfiguration.ProcessConfigFile(configFile);
/// <summary>
/// Normalizes token-like values to plain CLR values.
/// Mirrors <c>unwrapValue</c> intent from opts.go.
/// </summary>
public static object? UnwrapValue(object? value) => NormalizeConfigValue(value);
/// <summary>
/// Converts a recovered panic/exception to an error list entry.
/// Mirrors <c>convertPanicToErrorList</c> in opts.go.
/// </summary>
public static void ConvertPanicToErrorList(Exception? panic, ICollection<Exception>? 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));
}
/// <summary>
/// Converts a recovered panic/exception to a single error output.
/// Mirrors <c>convertPanicToError</c> in opts.go.
/// </summary>
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);
}
/// <summary>
/// Applies <c>system_account</c>/<c>system</c> config values.
/// Mirrors <c>configureSystemAccount</c> in opts.go.
/// </summary>
public static Exception? ConfigureSystemAccount(ServerOptions options, IReadOnlyDictionary<string, object?> 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;
}
/// <summary>
/// Builds a username/nkey identity map for duplicate detection.
/// Mirrors <c>setupUsersAndNKeysDuplicateCheckMap</c> in opts.go.
/// </summary>
public static HashSet<string> SetupUsersAndNKeysDuplicateCheckMap(ServerOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var identities = new HashSet<string>(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;
}
/// <summary>
/// Parses a duration from config value.
/// Mirrors <c>parseDuration</c> in opts.go.
/// </summary>
public static TimeSpan ParseDuration(
string field,
object? value,
ICollection<Exception>? errors = null,
ICollection<Exception>? 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;
}
/// <summary>
/// Parses write timeout policy value.
/// Mirrors <c>parseWriteDeadlinePolicy</c> in opts.go.
/// </summary>
public static WriteTimeoutPolicy ParseWriteDeadlinePolicy(string value, ICollection<Exception>? errors = null) =>
value.ToLowerInvariant() switch
{
"default" => WriteTimeoutPolicy.Default,
"close" => WriteTimeoutPolicy.Close,
"retry" => WriteTimeoutPolicy.Retry,
_ => ParseWriteDeadlinePolicyFallback(value, errors),
};
/// <summary>
/// Parses <c>listen</c> values (<c>port</c> or <c>host:port</c>).
/// Mirrors <c>parseListen</c> in opts.go.
/// </summary>
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);
}
/// <summary>
/// Parses cluster block config.
/// Mirrors <c>parseCluster</c> in opts.go.
/// </summary>
public static Exception? ParseCluster(
object? value,
ServerOptions options,
ICollection<Exception>? errors = null,
ICollection<Exception>? 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;
}
/// <summary>
/// Parses compression option values from bool/string/map forms.
/// Mirrors <c>parseCompression</c> in opts.go.
/// </summary>
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;
}
}
/// <summary>
/// Parses URL arrays with duplicate detection.
/// Mirrors <c>parseURLs</c> in opts.go.
/// </summary>
public static List<Uri> ParseURLs(
IEnumerable<object?> values,
string type,
ICollection<Exception>? warnings = null,
ICollection<Exception>? errors = null)
{
var urls = new List<Uri>();
var dedupe = new HashSet<string>(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;
}
/// <summary>
/// Parses a single URL entry.
/// Mirrors <c>parseURL</c> in opts.go.
/// </summary>
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;
}
/// <summary>
/// Parses gateway block config.
/// Mirrors <c>parseGateway</c> in opts.go.
/// </summary>
public static Exception? ParseGateway(
object? value,
ServerOptions options,
ICollection<Exception>? errors = null,
ICollection<Exception>? 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 helpers
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private static WriteTimeoutPolicy ParseWriteDeadlinePolicyFallback(string value, ICollection<Exception>? 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<Exception>? errors,
ICollection<Exception>? 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<string, object?> map,
IEnumerable<string> 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<string, object?> map)
{
var normalized = NormalizeConfigValue(value);
if (normalized is IReadOnlyDictionary<string, object?> readonlyMap)
{
map = readonlyMap;
return true;
}
if (normalized is Dictionary<string, object?> dict)
{
map = dict;
return true;
}
map = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
return false;
}
private static bool TryGetArray(object? value, out IReadOnlyList<object?> values)
{
var normalized = NormalizeConfigValue(value);
if (normalized is IReadOnlyList<object?> readonlyValues)
{
values = readonlyValues;
return true;
}
if (normalized is List<object?> 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) private static void MergeRoutes(ServerOptions opts, ServerOptions flagOpts)
{ {
var routeUrls = RoutesFromStr(flagOpts.RoutesStr); var routeUrls = RoutesFromStr(flagOpts.RoutesStr);

View File

@@ -1,731 +1,79 @@
using Shouldly; using Shouldly;
using ZB.MOM.NatsNet.Server; using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog; namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog;
public sealed class ConfigReloaderTests public sealed class ConfigReloaderTests
{ {
[Fact] // T:2748 [Fact] // T:2766
public void ConfigReloadClusterNoAdvertise_ShouldSucceed() public void ConfigReloadBoolFlags_ShouldSucceed()
{ {
var goFile = "server/reload_test.go"; var options = new ServerOptions();
var errors = new List<Exception>();
var warnings = new List<Exception>();
goFile.ShouldStartWith("server/"); ServerOptions.ParseCluster(
new Dictionary<string, object?>
{
["no_advertise"] = true,
["connect_backoff"] = true,
},
options,
errors,
warnings);
ServerConstants.DefaultPort.ShouldBe(4222); errors.ShouldBeEmpty();
options.Cluster.NoAdvertise.ShouldBeTrue();
ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); options.Cluster.ConnectBackoff.ShouldBeTrue();
options.InConfig.TryGetValue("Cluster.NoAdvertise", out var explicitValue).ShouldBeTrue();
if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) || explicitValue.ShouldBeTrue();
goFile.Contains("store", StringComparison.OrdinalIgnoreCase))
{
JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0);
JetStreamVersioning.GetRequiredApiLevel(new Dictionary<string, string>()).ShouldBe(string.Empty);
}
else
{
ServerUtilities.ParseSize("123"u8).ShouldBe(123);
ServerUtilities.ParseInt64("456"u8).ShouldBe(456);
}
"ConfigReloadClusterNoAdvertise_ShouldSucceed".ShouldContain("Should");
"TestConfigReloadClusterNoAdvertise".ShouldNotBeNullOrWhiteSpace();
} }
[Fact] // T:2749 [Fact]
public void ConfigReloadClusterName_ShouldSucceed() 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<Exception>();
ServerConstants.DefaultPort.ShouldBe(4222); ServerOptions.ParseCluster(
new Dictionary<string, object?>
{
["unknown_cluster_field"] = true,
},
options,
errors,
warnings: null);
ServerConstants.Version.ShouldNotBeNullOrWhiteSpace(); errors.Count.ShouldBe(1);
errors[0].Message.ShouldContain("unknown field");
if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) ||
goFile.Contains("store", StringComparison.OrdinalIgnoreCase))
{
JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0);
JetStreamVersioning.GetRequiredApiLevel(new Dictionary<string, string>()).ShouldBe(string.Empty);
}
else
{
ServerUtilities.ParseSize("123"u8).ShouldBe(123);
ServerUtilities.ParseInt64("456"u8).ShouldBe(456);
}
"ConfigReloadClusterName_ShouldSucceed".ShouldContain("Should");
"TestConfigReloadClusterName".ShouldNotBeNullOrWhiteSpace();
} }
[Fact] // T:2751 [Fact]
public void ConfigReloadClientAdvertise_ShouldSucceed() public void ParseCluster_WithUnknownFieldAndRelaxedMode_IgnoresUnknownField()
{ {
var goFile = "server/reload_test.go"; ServerOptions.NoErrOnUnknownFields(true);
goFile.ShouldStartWith("server/");
ServerConstants.DefaultPort.ShouldBe(4222);
ServerConstants.Version.ShouldNotBeNullOrWhiteSpace();
if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) ||
goFile.Contains("store", StringComparison.OrdinalIgnoreCase))
try
{ {
var options = new ServerOptions();
var errors = new List<Exception>();
JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); ServerOptions.ParseCluster(
new Dictionary<string, object?>
JetStreamVersioning.GetRequiredApiLevel(new Dictionary<string, string>()).ShouldBe(string.Empty); {
["unknown_cluster_field"] = true,
},
options,
errors,
warnings: null);
errors.ShouldBeEmpty();
} }
finally
else
{ {
ServerOptions.NoErrOnUnknownFields(false);
ServerUtilities.ParseSize("123"u8).ShouldBe(123);
ServerUtilities.ParseInt64("456"u8).ShouldBe(456);
} }
"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<string, string>()).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<string, string>()).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<string, string>()).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<string, string>()).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<string, string>()).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<string, string>()).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<string, string>()).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<string, string>()).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<string, string>()).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<string, string>()).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<string, string>()).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<string, string>()).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<string, string>()).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<string, string>()).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<string, string>()).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<string, string>()).ShouldBe(string.Empty);
}
else
{
ServerUtilities.ParseSize("123"u8).ShouldBe(123);
ServerUtilities.ParseInt64("456"u8).ShouldBe(456);
}
"ConfigReloadLeafNodeCompressionS2Auto_ShouldSucceed".ShouldContain("Should");
"TestConfigReloadLeafNodeCompressionS2Auto".ShouldNotBeNullOrWhiteSpace();
}
} }

View File

@@ -1,123 +1,313 @@
using Shouldly; using Shouldly;
using ZB.MOM.NatsNet.Server; using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Internal; using ZB.MOM.NatsNet.Server.Auth;
namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog; namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog;
public sealed class ServerOptionsTests public sealed class ServerOptionsTests
{ {
[Fact] // T:2552 [Fact]
public void AccountUsersLoadedProperly_ShouldSucceed() public void DeepCopyURLs_WithEntries_ReturnsIndependentCopy()
{ {
var goFile = "server/opts_test.go"; var source = new List<Uri>
goFile.ShouldStartWith("server/");
ServerConstants.DefaultPort.ShouldBe(4222);
ServerConstants.Version.ShouldNotBeNullOrWhiteSpace();
if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) ||
goFile.Contains("store", StringComparison.OrdinalIgnoreCase))
{ {
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<string, string>()).ShouldBe(string.Empty); copy.ShouldNotBeNull();
copy.Count.ShouldBe(2);
} ReferenceEquals(copy, source).ShouldBeFalse();
copy[0].ToString().ShouldBe(source[0].ToString());
else copy[1].ToString().ShouldBe(source[1].ToString());
{
ServerUtilities.ParseSize("123"u8).ShouldBe(123);
ServerUtilities.ParseInt64("456"u8).ShouldBe(456);
}
"AccountUsersLoadedProperly_ShouldSucceed".ShouldContain("Should");
"TestAccountUsersLoadedProperly".ShouldNotBeNullOrWhiteSpace();
} }
[Fact] // T:2561 [Fact]
public void SublistNoCacheConfigOnAccounts_ShouldSucceed() public void ProcessConfigFile_WithValidFile_ReturnsParsedOptions()
{ {
var goFile = "server/opts_test.go"; var tempFile = Path.GetTempFileName();
goFile.ShouldStartWith("server/");
ServerConstants.DefaultPort.ShouldBe(4222);
ServerConstants.Version.ShouldNotBeNullOrWhiteSpace();
if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) ||
goFile.Contains("store", StringComparison.OrdinalIgnoreCase))
try
{ {
File.WriteAllText(tempFile, """
{
"host": "127.0.0.1",
"port": 4444,
"system_account": "$SYS"
}
""");
JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); var options = ServerOptions.ProcessConfigFile(tempFile);
JetStreamVersioning.GetRequiredApiLevel(new Dictionary<string, string>()).ShouldBe(string.Empty);
options.Host.ShouldBe("127.0.0.1");
options.Port.ShouldBe(4444);
options.SystemAccount.ShouldBe("$SYS");
} }
finally
else
{ {
File.Delete(tempFile);
ServerUtilities.ParseSize("123"u8).ShouldBe(123);
ServerUtilities.ParseInt64("456"u8).ShouldBe(456);
} }
"SublistNoCacheConfigOnAccounts_ShouldSucceed".ShouldContain("Should");
"TestSublistNoCacheConfigOnAccounts".ShouldNotBeNullOrWhiteSpace();
} }
[Fact] // T:2585 [Fact]
public void NewServerFromConfigVsLoadConfig_ShouldSucceed() public void ConfigureSystemAccount_WithSystemAccountString_SetsValue()
{ {
var goFile = "server/opts_test.go"; var options = new ServerOptions();
var config = new Dictionary<string, object?>
goFile.ShouldStartWith("server/");
ServerConstants.DefaultPort.ShouldBe(4222);
ServerConstants.Version.ShouldNotBeNullOrWhiteSpace();
if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) ||
goFile.Contains("store", StringComparison.OrdinalIgnoreCase))
{ {
["system_account"] = "$SYSX",
};
JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0); var error = ServerOptions.ConfigureSystemAccount(options, config);
JetStreamVersioning.GetRequiredApiLevel(new Dictionary<string, string>()).ShouldBe(string.Empty); error.ShouldBeNull();
options.SystemAccount.ShouldBe("$SYSX");
}
else
{
ServerUtilities.ParseSize("123"u8).ShouldBe(123);
ServerUtilities.ParseInt64("456"u8).ShouldBe(456);
}
"NewServerFromConfigVsLoadConfig_ShouldSucceed".ShouldContain("Should");
"TestNewServerFromConfigVsLoadConfig".ShouldNotBeNullOrWhiteSpace();
} }
[Fact]
public void ConfigureSystemAccount_WithNonString_ReturnsError()
{
var options = new ServerOptions();
var config = new Dictionary<string, object?>
{
["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<Exception>();
var warnings = new List<Exception>();
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<Exception>();
var warnings = new List<Exception>();
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<Exception>();
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<string, object?>
{
["mode"] = CompressionModes.S2Best,
["rtt_thresholds"] = new List<object?> { "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<Exception>();
var errors = new List<Exception>();
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<Exception>();
var warnings = new List<Exception>();
var parseError = ServerOptions.ParseCluster(
new Dictionary<string, object?>
{
["name"] = "core",
["listen"] = "127.0.0.1:6222",
["connect_retries"] = 8L,
["connect_backoff"] = true,
["no_advertise"] = true,
["compression"] = "s2_fast",
["routes"] = new List<object?> { "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<Exception>();
var warnings = new List<Exception>();
var parseError = ServerOptions.ParseGateway(
new Dictionary<string, object?>
{
["name"] = "edge",
["listen"] = "127.0.0.1:7222",
["connect_retries"] = 4L,
["connect_backoff"] = true,
["advertise"] = "gw.local:7222",
["reject_unknown"] = true,
["authorization"] = new Dictionary<string, object?>
{
["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<Exception>();
var warnings = new List<Exception>();
ServerOptions.ParseCluster(
new Dictionary<string, object?>
{
["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<Exception>();
var warnings = new List<Exception>();
ServerOptions.ParseGateway(
new Dictionary<string, object?>
{
["write_timeout"] = "retry",
},
options,
errors,
warnings);
errors.ShouldBeEmpty();
options.Gateway.WriteTimeout.ShouldBe(WriteTimeoutPolicy.Retry);
}
} }

Binary file not shown.

View File

@@ -1,6 +1,6 @@
# NATS .NET Porting Status Report # NATS .NET Porting Status Report
Generated: 2026-02-28 14:23:21 UTC Generated: 2026-02-28 14:29:51 UTC
## Modules (12 total) ## Modules (12 total)
@@ -14,8 +14,8 @@ Generated: 2026-02-28 14:23:21 UTC
|--------|-------| |--------|-------|
| deferred | 2108 | | deferred | 2108 |
| n_a | 24 | | n_a | 24 |
| stub | 17 | | stub | 1 |
| verified | 1524 | | verified | 1540 |
## Unit Tests (3257 total) ## Unit Tests (3257 total)
@@ -34,4 +34,4 @@ Generated: 2026-02-28 14:23:21 UTC
## Overall Progress ## Overall Progress
**2769/6942 items complete (39.9%)** **2785/6942 items complete (40.1%)**