feat(batch6-task2): implement F1 opts parsing and verify features
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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>();
|
||||||
goFile.ShouldStartWith("server/");
|
var warnings = new List<Exception>();
|
||||||
|
|
||||||
ServerConstants.DefaultPort.ShouldBe(4222);
|
|
||||||
|
|
||||||
ServerConstants.Version.ShouldNotBeNullOrWhiteSpace();
|
|
||||||
|
|
||||||
if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
|
|
||||||
goFile.Contains("store", StringComparison.OrdinalIgnoreCase))
|
|
||||||
|
|
||||||
|
ServerOptions.ParseCluster(
|
||||||
|
new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
|
["no_advertise"] = true,
|
||||||
|
["connect_backoff"] = true,
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
errors,
|
||||||
|
warnings);
|
||||||
|
|
||||||
JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0);
|
errors.ShouldBeEmpty();
|
||||||
|
options.Cluster.NoAdvertise.ShouldBeTrue();
|
||||||
JetStreamVersioning.GetRequiredApiLevel(new Dictionary<string, string>()).ShouldBe(string.Empty);
|
options.Cluster.ConnectBackoff.ShouldBeTrue();
|
||||||
|
options.InConfig.TryGetValue("Cluster.NoAdvertise", out var explicitValue).ShouldBeTrue();
|
||||||
|
explicitValue.ShouldBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
[Fact]
|
||||||
|
public void ParseCluster_WithUnknownFieldAndStrictMode_ReturnsError()
|
||||||
{
|
{
|
||||||
|
ServerOptions.NoErrOnUnknownFields(false);
|
||||||
|
|
||||||
ServerUtilities.ParseSize("123"u8).ShouldBe(123);
|
var options = new ServerOptions();
|
||||||
|
var errors = new List<Exception>();
|
||||||
|
|
||||||
ServerUtilities.ParseInt64("456"u8).ShouldBe(456);
|
ServerOptions.ParseCluster(
|
||||||
|
new Dictionary<string, object?>
|
||||||
}
|
|
||||||
|
|
||||||
"ConfigReloadClusterNoAdvertise_ShouldSucceed".ShouldContain("Should");
|
|
||||||
|
|
||||||
"TestConfigReloadClusterNoAdvertise".ShouldNotBeNullOrWhiteSpace();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact] // T:2749
|
|
||||||
public void ConfigReloadClusterName_ShouldSucceed()
|
|
||||||
{
|
{
|
||||||
var goFile = "server/reload_test.go";
|
["unknown_cluster_field"] = true,
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
errors,
|
||||||
|
warnings: null);
|
||||||
|
|
||||||
goFile.ShouldStartWith("server/");
|
errors.Count.ShouldBe(1);
|
||||||
|
errors[0].Message.ShouldContain("unknown field");
|
||||||
ServerConstants.DefaultPort.ShouldBe(4222);
|
}
|
||||||
|
|
||||||
ServerConstants.Version.ShouldNotBeNullOrWhiteSpace();
|
|
||||||
|
|
||||||
if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
|
|
||||||
goFile.Contains("store", StringComparison.OrdinalIgnoreCase))
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseCluster_WithUnknownFieldAndRelaxedMode_IgnoresUnknownField()
|
||||||
{
|
{
|
||||||
|
ServerOptions.NoErrOnUnknownFields(true);
|
||||||
|
|
||||||
JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0);
|
try
|
||||||
|
|
||||||
JetStreamVersioning.GetRequiredApiLevel(new Dictionary<string, string>()).ShouldBe(string.Empty);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
|
|
||||||
{
|
{
|
||||||
|
var options = new ServerOptions();
|
||||||
|
var errors = new List<Exception>();
|
||||||
|
|
||||||
ServerUtilities.ParseSize("123"u8).ShouldBe(123);
|
ServerOptions.ParseCluster(
|
||||||
|
new Dictionary<string, object?>
|
||||||
ServerUtilities.ParseInt64("456"u8).ShouldBe(456);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
"ConfigReloadClusterName_ShouldSucceed".ShouldContain("Should");
|
|
||||||
|
|
||||||
"TestConfigReloadClusterName".ShouldNotBeNullOrWhiteSpace();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact] // T:2751
|
|
||||||
public void ConfigReloadClientAdvertise_ShouldSucceed()
|
|
||||||
{
|
{
|
||||||
var goFile = "server/reload_test.go";
|
["unknown_cluster_field"] = true,
|
||||||
|
},
|
||||||
goFile.ShouldStartWith("server/");
|
options,
|
||||||
|
errors,
|
||||||
ServerConstants.DefaultPort.ShouldBe(4222);
|
warnings: null);
|
||||||
|
|
||||||
ServerConstants.Version.ShouldNotBeNullOrWhiteSpace();
|
|
||||||
|
|
||||||
if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
|
|
||||||
goFile.Contains("store", StringComparison.OrdinalIgnoreCase))
|
|
||||||
|
|
||||||
|
errors.ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
finally
|
||||||
{
|
{
|
||||||
|
ServerOptions.NoErrOnUnknownFields(false);
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
copy[1].ToString().ShouldBe(source[1].ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
[Fact]
|
||||||
|
public void ProcessConfigFile_WithValidFile_ReturnsParsedOptions()
|
||||||
{
|
{
|
||||||
|
var tempFile = Path.GetTempFileName();
|
||||||
|
|
||||||
ServerUtilities.ParseSize("123"u8).ShouldBe(123);
|
try
|
||||||
|
|
||||||
ServerUtilities.ParseInt64("456"u8).ShouldBe(456);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
"AccountUsersLoadedProperly_ShouldSucceed".ShouldContain("Should");
|
|
||||||
|
|
||||||
"TestAccountUsersLoadedProperly".ShouldNotBeNullOrWhiteSpace();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact] // T:2561
|
|
||||||
public void SublistNoCacheConfigOnAccounts_ShouldSucceed()
|
|
||||||
{
|
{
|
||||||
var goFile = "server/opts_test.go";
|
File.WriteAllText(tempFile, """
|
||||||
|
|
||||||
goFile.ShouldStartWith("server/");
|
|
||||||
|
|
||||||
ServerConstants.DefaultPort.ShouldBe(4222);
|
|
||||||
|
|
||||||
ServerConstants.Version.ShouldNotBeNullOrWhiteSpace();
|
|
||||||
|
|
||||||
if (goFile.Contains("jetstream", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
|
|
||||||
goFile.Contains("store", StringComparison.OrdinalIgnoreCase))
|
|
||||||
|
|
||||||
{
|
{
|
||||||
|
"host": "127.0.0.1",
|
||||||
JetStreamVersioning.JsApiLevel.ShouldBeGreaterThanOrEqualTo(0);
|
"port": 4444,
|
||||||
|
"system_account": "$SYS"
|
||||||
JetStreamVersioning.GetRequiredApiLevel(new Dictionary<string, string>()).ShouldBe(string.Empty);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
else
|
var options = ServerOptions.ProcessConfigFile(tempFile);
|
||||||
|
|
||||||
|
options.Host.ShouldBe("127.0.0.1");
|
||||||
|
options.Port.ShouldBe(4444);
|
||||||
|
options.SystemAccount.ShouldBe("$SYS");
|
||||||
|
}
|
||||||
|
finally
|
||||||
{
|
{
|
||||||
|
File.Delete(tempFile);
|
||||||
ServerUtilities.ParseSize("123"u8).ShouldBe(123);
|
}
|
||||||
|
|
||||||
ServerUtilities.ParseInt64("456"u8).ShouldBe(456);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"SublistNoCacheConfigOnAccounts_ShouldSucceed".ShouldContain("Should");
|
[Fact]
|
||||||
|
public void ConfigureSystemAccount_WithSystemAccountString_SetsValue()
|
||||||
"TestSublistNoCacheConfigOnAccounts".ShouldNotBeNullOrWhiteSpace();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact] // T:2585
|
|
||||||
public void NewServerFromConfigVsLoadConfig_ShouldSucceed()
|
|
||||||
{
|
{
|
||||||
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
|
[Fact]
|
||||||
|
public void ConfigureSystemAccount_WithNonString_ReturnsError()
|
||||||
{
|
{
|
||||||
|
var options = new ServerOptions();
|
||||||
|
var config = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["system"] = 123L,
|
||||||
|
};
|
||||||
|
|
||||||
ServerUtilities.ParseSize("123"u8).ShouldBe(123);
|
var error = ServerOptions.ConfigureSystemAccount(options, config);
|
||||||
|
|
||||||
ServerUtilities.ParseInt64("456"u8).ShouldBe(456);
|
|
||||||
|
|
||||||
|
error.ShouldNotBeNull();
|
||||||
|
error.Message.ShouldContain("must be a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
"NewServerFromConfigVsLoadConfig_ShouldSucceed".ShouldContain("Should");
|
[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" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
"TestNewServerFromConfigVsLoadConfig".ShouldNotBeNullOrWhiteSpace();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
@@ -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%)**
|
||||||
|
|||||||
Reference in New Issue
Block a user