feat: add config processor mapping parsed config to NatsOptions
Port of Go server/opts.go processConfigFileLine switch. Maps parsed NATS config dictionaries to NatsOptions fields including: - Core options (port, host, server_name, limits, ping, write_deadline) - Logging (debug, trace, logfile, log rotation) - Authorization (single user, users array with permissions) - TLS (cert/key/ca, verify, pinned_certs, handshake_first) - Monitoring (http_port, https_port, http/https listen, base_path) - Lifecycle (lame_duck_duration/grace_period) - Server tags, file paths, system account options Includes error collection (not fail-fast), duration parsing (ms/s/m/h strings and numeric seconds), host:port listen parsing, and 56 tests covering all config sections plus validation edge cases.
This commit is contained in:
685
src/NATS.Server/Configuration/ConfigProcessor.cs
Normal file
685
src/NATS.Server/Configuration/ConfigProcessor.cs
Normal file
@@ -0,0 +1,685 @@
|
||||
// Port of Go server/opts.go processConfigFileLine — maps parsed config dictionaries
|
||||
// to NatsOptions. Reference: golang/nats-server/server/opts.go lines 1050-1400.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using NATS.Server.Auth;
|
||||
|
||||
namespace NATS.Server.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Maps a parsed NATS configuration dictionary (produced by <see cref="NatsConfParser"/>)
|
||||
/// into a fully populated <see cref="NatsOptions"/> instance. Collects all validation
|
||||
/// errors rather than failing on the first one.
|
||||
/// </summary>
|
||||
public static class ConfigProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a configuration file and returns the populated options.
|
||||
/// </summary>
|
||||
public static NatsOptions ProcessConfigFile(string filePath)
|
||||
{
|
||||
var config = NatsConfParser.ParseFile(filePath);
|
||||
var opts = new NatsOptions { ConfigFile = filePath };
|
||||
ApplyConfig(config, opts);
|
||||
return opts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses configuration text (not from a file) and returns the populated options.
|
||||
/// </summary>
|
||||
public static NatsOptions ProcessConfig(string configText)
|
||||
{
|
||||
var config = NatsConfParser.Parse(configText);
|
||||
var opts = new NatsOptions();
|
||||
ApplyConfig(config, opts);
|
||||
return opts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a parsed configuration dictionary to existing options.
|
||||
/// Throws <see cref="ConfigProcessorException"/> if any validation errors are collected.
|
||||
/// </summary>
|
||||
public static void ApplyConfig(Dictionary<string, object?> config, NatsOptions opts)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
foreach (var (key, value) in config)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessKey(key, value, opts, errors);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add($"Error processing '{key}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
throw new ConfigProcessorException("Configuration errors", errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessKey(string key, object? value, NatsOptions opts, List<string> errors)
|
||||
{
|
||||
// Keys are already case-insensitive from the parser (OrdinalIgnoreCase dictionaries),
|
||||
// but we normalize here for the switch statement.
|
||||
switch (key.ToLowerInvariant())
|
||||
{
|
||||
case "listen":
|
||||
ParseListen(value, opts);
|
||||
break;
|
||||
case "port":
|
||||
opts.Port = ToInt(value);
|
||||
break;
|
||||
case "host" or "net":
|
||||
opts.Host = ToString(value);
|
||||
break;
|
||||
case "server_name":
|
||||
var name = ToString(value);
|
||||
if (name.Contains(' '))
|
||||
errors.Add("server_name cannot contain spaces");
|
||||
else
|
||||
opts.ServerName = name;
|
||||
break;
|
||||
case "client_advertise":
|
||||
opts.ClientAdvertise = ToString(value);
|
||||
break;
|
||||
|
||||
// Logging
|
||||
case "debug":
|
||||
opts.Debug = ToBool(value);
|
||||
break;
|
||||
case "trace":
|
||||
opts.Trace = ToBool(value);
|
||||
break;
|
||||
case "trace_verbose":
|
||||
opts.TraceVerbose = ToBool(value);
|
||||
if (opts.TraceVerbose)
|
||||
opts.Trace = true;
|
||||
break;
|
||||
case "logtime":
|
||||
opts.Logtime = ToBool(value);
|
||||
break;
|
||||
case "logtime_utc":
|
||||
opts.LogtimeUTC = ToBool(value);
|
||||
break;
|
||||
case "logfile" or "log_file":
|
||||
opts.LogFile = ToString(value);
|
||||
break;
|
||||
case "log_size_limit":
|
||||
opts.LogSizeLimit = ToLong(value);
|
||||
break;
|
||||
case "log_max_num":
|
||||
opts.LogMaxFiles = ToInt(value);
|
||||
break;
|
||||
case "syslog":
|
||||
opts.Syslog = ToBool(value);
|
||||
break;
|
||||
case "remote_syslog":
|
||||
opts.RemoteSyslog = ToString(value);
|
||||
break;
|
||||
|
||||
// Limits
|
||||
case "max_payload":
|
||||
opts.MaxPayload = ToInt(value);
|
||||
break;
|
||||
case "max_control_line":
|
||||
opts.MaxControlLine = ToInt(value);
|
||||
break;
|
||||
case "max_connections" or "max_conn":
|
||||
opts.MaxConnections = ToInt(value);
|
||||
break;
|
||||
case "max_pending":
|
||||
opts.MaxPending = ToLong(value);
|
||||
break;
|
||||
case "max_subs" or "max_subscriptions":
|
||||
opts.MaxSubs = ToInt(value);
|
||||
break;
|
||||
case "max_sub_tokens" or "max_subscription_tokens":
|
||||
var tokens = ToInt(value);
|
||||
if (tokens > 256)
|
||||
errors.Add("max_sub_tokens cannot exceed 256");
|
||||
else
|
||||
opts.MaxSubTokens = tokens;
|
||||
break;
|
||||
case "max_traced_msg_len":
|
||||
opts.MaxTracedMsgLen = ToInt(value);
|
||||
break;
|
||||
case "max_closed_clients":
|
||||
opts.MaxClosedClients = ToInt(value);
|
||||
break;
|
||||
case "disable_sublist_cache" or "no_sublist_cache":
|
||||
opts.DisableSublistCache = ToBool(value);
|
||||
break;
|
||||
case "write_deadline":
|
||||
opts.WriteDeadline = ParseDuration(value);
|
||||
break;
|
||||
|
||||
// Ping
|
||||
case "ping_interval":
|
||||
opts.PingInterval = ParseDuration(value);
|
||||
break;
|
||||
case "ping_max" or "ping_max_out":
|
||||
opts.MaxPingsOut = ToInt(value);
|
||||
break;
|
||||
|
||||
// Monitoring
|
||||
case "http_port" or "monitor_port":
|
||||
opts.MonitorPort = ToInt(value);
|
||||
break;
|
||||
case "https_port":
|
||||
opts.MonitorHttpsPort = ToInt(value);
|
||||
break;
|
||||
case "http":
|
||||
ParseMonitorListen(value, opts, isHttps: false);
|
||||
break;
|
||||
case "https":
|
||||
ParseMonitorListen(value, opts, isHttps: true);
|
||||
break;
|
||||
case "http_base_path":
|
||||
opts.MonitorBasePath = ToString(value);
|
||||
break;
|
||||
|
||||
// Lifecycle
|
||||
case "lame_duck_duration":
|
||||
opts.LameDuckDuration = ParseDuration(value);
|
||||
break;
|
||||
case "lame_duck_grace_period":
|
||||
opts.LameDuckGracePeriod = ParseDuration(value);
|
||||
break;
|
||||
|
||||
// Files
|
||||
case "pidfile" or "pid_file":
|
||||
opts.PidFile = ToString(value);
|
||||
break;
|
||||
case "ports_file_dir":
|
||||
opts.PortsFileDir = ToString(value);
|
||||
break;
|
||||
|
||||
// Auth
|
||||
case "authorization":
|
||||
if (value is Dictionary<string, object?> authDict)
|
||||
ParseAuthorization(authDict, opts, errors);
|
||||
break;
|
||||
case "no_auth_user":
|
||||
opts.NoAuthUser = ToString(value);
|
||||
break;
|
||||
|
||||
// TLS
|
||||
case "tls":
|
||||
if (value is Dictionary<string, object?> tlsDict)
|
||||
ParseTls(tlsDict, opts, errors);
|
||||
break;
|
||||
case "allow_non_tls":
|
||||
opts.AllowNonTls = ToBool(value);
|
||||
break;
|
||||
|
||||
// Tags
|
||||
case "server_tags":
|
||||
if (value is Dictionary<string, object?> tagsDict)
|
||||
ParseTags(tagsDict, opts);
|
||||
break;
|
||||
|
||||
// Profiling
|
||||
case "prof_port":
|
||||
opts.ProfPort = ToInt(value);
|
||||
break;
|
||||
|
||||
// System account
|
||||
case "system_account":
|
||||
opts.SystemAccount = ToString(value);
|
||||
break;
|
||||
case "no_system_account":
|
||||
opts.NoSystemAccount = ToBool(value);
|
||||
break;
|
||||
case "no_header_support":
|
||||
opts.NoHeaderSupport = ToBool(value);
|
||||
break;
|
||||
case "connect_error_reports":
|
||||
opts.ConnectErrorReports = ToInt(value);
|
||||
break;
|
||||
case "reconnect_error_reports":
|
||||
opts.ReconnectErrorReports = ToInt(value);
|
||||
break;
|
||||
|
||||
// Unknown keys silently ignored (cluster, jetstream, gateway, leafnode, etc.)
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Listen parsing ────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Parses a "listen" value that can be:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>":4222"</c> — port only</item>
|
||||
/// <item><c>"0.0.0.0:4222"</c> — host + port</item>
|
||||
/// <item><c>"4222"</c> — bare number (port only)</item>
|
||||
/// <item><c>4222</c> — integer (port only)</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
private static void ParseListen(object? value, NatsOptions opts)
|
||||
{
|
||||
var (host, port) = ParseHostPort(value);
|
||||
if (host is not null)
|
||||
opts.Host = host;
|
||||
if (port is not null)
|
||||
opts.Port = port.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a monitor listen value. For "http" the port goes to MonitorPort;
|
||||
/// for "https" the port goes to MonitorHttpsPort.
|
||||
/// </summary>
|
||||
private static void ParseMonitorListen(object? value, NatsOptions opts, bool isHttps)
|
||||
{
|
||||
var (host, port) = ParseHostPort(value);
|
||||
if (host is not null)
|
||||
opts.MonitorHost = host;
|
||||
if (port is not null)
|
||||
{
|
||||
if (isHttps)
|
||||
opts.MonitorHttpsPort = port.Value;
|
||||
else
|
||||
opts.MonitorPort = port.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared host:port parsing logic.
|
||||
/// </summary>
|
||||
private static (string? Host, int? Port) ParseHostPort(object? value)
|
||||
{
|
||||
if (value is long l)
|
||||
return (null, (int)l);
|
||||
|
||||
var str = ToString(value);
|
||||
|
||||
// Try bare integer
|
||||
if (int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out var barePort))
|
||||
return (null, barePort);
|
||||
|
||||
// Check for host:port
|
||||
var colonIdx = str.LastIndexOf(':');
|
||||
if (colonIdx >= 0)
|
||||
{
|
||||
var hostPart = str[..colonIdx];
|
||||
var portPart = str[(colonIdx + 1)..];
|
||||
if (int.TryParse(portPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out var p))
|
||||
{
|
||||
var host = hostPart.Length > 0 ? hostPart : null;
|
||||
return (host, p);
|
||||
}
|
||||
}
|
||||
|
||||
throw new FormatException($"Cannot parse listen value: '{str}'");
|
||||
}
|
||||
|
||||
// ─── Duration parsing ──────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Parses a duration value. Accepts:
|
||||
/// <list type="bullet">
|
||||
/// <item>A string with unit suffix: "30s", "2m", "1h", "500ms"</item>
|
||||
/// <item>A number (long/double) treated as seconds</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
internal static TimeSpan ParseDuration(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
long seconds => TimeSpan.FromSeconds(seconds),
|
||||
double seconds => TimeSpan.FromSeconds(seconds),
|
||||
string s => ParseDurationString(s),
|
||||
_ => throw new FormatException($"Cannot parse duration from {value?.GetType().Name ?? "null"}"),
|
||||
};
|
||||
}
|
||||
|
||||
private static readonly Regex DurationPattern = new(
|
||||
@"^(-?\d+(?:\.\d+)?)\s*(ms|s|m|h)$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static TimeSpan ParseDurationString(string s)
|
||||
{
|
||||
var match = DurationPattern.Match(s);
|
||||
if (!match.Success)
|
||||
throw new FormatException($"Cannot parse duration: '{s}'");
|
||||
|
||||
var amount = double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
|
||||
var unit = match.Groups[2].Value.ToLowerInvariant();
|
||||
|
||||
return unit switch
|
||||
{
|
||||
"ms" => TimeSpan.FromMilliseconds(amount),
|
||||
"s" => TimeSpan.FromSeconds(amount),
|
||||
"m" => TimeSpan.FromMinutes(amount),
|
||||
"h" => TimeSpan.FromHours(amount),
|
||||
_ => throw new FormatException($"Unknown duration unit: '{unit}'"),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Authorization parsing ─────────────────────────────────────
|
||||
|
||||
private static void ParseAuthorization(Dictionary<string, object?> dict, NatsOptions opts, List<string> errors)
|
||||
{
|
||||
foreach (var (key, value) in dict)
|
||||
{
|
||||
switch (key.ToLowerInvariant())
|
||||
{
|
||||
case "user" or "username":
|
||||
opts.Username = ToString(value);
|
||||
break;
|
||||
case "pass" or "password":
|
||||
opts.Password = ToString(value);
|
||||
break;
|
||||
case "token":
|
||||
opts.Authorization = ToString(value);
|
||||
break;
|
||||
case "timeout":
|
||||
opts.AuthTimeout = value switch
|
||||
{
|
||||
long l => TimeSpan.FromSeconds(l),
|
||||
double d => TimeSpan.FromSeconds(d),
|
||||
string s => ParseDuration(s),
|
||||
_ => throw new FormatException($"Invalid auth timeout type: {value?.GetType().Name}"),
|
||||
};
|
||||
break;
|
||||
case "users":
|
||||
if (value is List<object?> userList)
|
||||
opts.Users = ParseUsers(userList, errors);
|
||||
break;
|
||||
default:
|
||||
// Unknown auth keys silently ignored
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<User> ParseUsers(List<object?> list, List<string> errors)
|
||||
{
|
||||
var users = new List<User>();
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (item is not Dictionary<string, object?> userDict)
|
||||
{
|
||||
errors.Add("Expected user entry to be a map");
|
||||
continue;
|
||||
}
|
||||
|
||||
string? username = null;
|
||||
string? password = null;
|
||||
string? account = null;
|
||||
Permissions? permissions = null;
|
||||
|
||||
foreach (var (key, value) in userDict)
|
||||
{
|
||||
switch (key.ToLowerInvariant())
|
||||
{
|
||||
case "user" or "username":
|
||||
username = ToString(value);
|
||||
break;
|
||||
case "pass" or "password":
|
||||
password = ToString(value);
|
||||
break;
|
||||
case "account":
|
||||
account = ToString(value);
|
||||
break;
|
||||
case "permissions" or "permission":
|
||||
if (value is Dictionary<string, object?> permDict)
|
||||
permissions = ParsePermissions(permDict, errors);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (username is null)
|
||||
{
|
||||
errors.Add("User entry missing 'user' field");
|
||||
continue;
|
||||
}
|
||||
|
||||
users.Add(new User
|
||||
{
|
||||
Username = username,
|
||||
Password = password ?? string.Empty,
|
||||
Account = account,
|
||||
Permissions = permissions,
|
||||
});
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
private static Permissions ParsePermissions(Dictionary<string, object?> dict, List<string> errors)
|
||||
{
|
||||
SubjectPermission? publish = null;
|
||||
SubjectPermission? subscribe = null;
|
||||
ResponsePermission? response = null;
|
||||
|
||||
foreach (var (key, value) in dict)
|
||||
{
|
||||
switch (key.ToLowerInvariant())
|
||||
{
|
||||
case "publish" or "pub":
|
||||
publish = ParseSubjectPermission(value, errors);
|
||||
break;
|
||||
case "subscribe" or "sub":
|
||||
subscribe = ParseSubjectPermission(value, errors);
|
||||
break;
|
||||
case "resp" or "response":
|
||||
if (value is Dictionary<string, object?> respDict)
|
||||
response = ParseResponsePermission(respDict);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new Permissions
|
||||
{
|
||||
Publish = publish,
|
||||
Subscribe = subscribe,
|
||||
Response = response,
|
||||
};
|
||||
}
|
||||
|
||||
private static SubjectPermission? ParseSubjectPermission(object? value, List<string> errors)
|
||||
{
|
||||
// Can be a simple list of strings (treated as allow) or a dict with allow/deny
|
||||
if (value is Dictionary<string, object?> dict)
|
||||
{
|
||||
IReadOnlyList<string>? allow = null;
|
||||
IReadOnlyList<string>? deny = null;
|
||||
|
||||
foreach (var (key, v) in dict)
|
||||
{
|
||||
switch (key.ToLowerInvariant())
|
||||
{
|
||||
case "allow":
|
||||
allow = ToStringList(v);
|
||||
break;
|
||||
case "deny":
|
||||
deny = ToStringList(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new SubjectPermission { Allow = allow, Deny = deny };
|
||||
}
|
||||
|
||||
if (value is List<object?> list)
|
||||
{
|
||||
return new SubjectPermission { Allow = ToStringList(list) };
|
||||
}
|
||||
|
||||
if (value is string s)
|
||||
{
|
||||
return new SubjectPermission { Allow = [s] };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ResponsePermission ParseResponsePermission(Dictionary<string, object?> dict)
|
||||
{
|
||||
var maxMsgs = 0;
|
||||
var expires = TimeSpan.Zero;
|
||||
|
||||
foreach (var (key, value) in dict)
|
||||
{
|
||||
switch (key.ToLowerInvariant())
|
||||
{
|
||||
case "max_msgs" or "max":
|
||||
maxMsgs = ToInt(value);
|
||||
break;
|
||||
case "expires" or "ttl":
|
||||
expires = ParseDuration(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new ResponsePermission { MaxMsgs = maxMsgs, Expires = expires };
|
||||
}
|
||||
|
||||
// ─── TLS parsing ───────────────────────────────────────────────
|
||||
|
||||
private static void ParseTls(Dictionary<string, object?> dict, NatsOptions opts, List<string> errors)
|
||||
{
|
||||
foreach (var (key, value) in dict)
|
||||
{
|
||||
switch (key.ToLowerInvariant())
|
||||
{
|
||||
case "cert_file":
|
||||
opts.TlsCert = ToString(value);
|
||||
break;
|
||||
case "key_file":
|
||||
opts.TlsKey = ToString(value);
|
||||
break;
|
||||
case "ca_file":
|
||||
opts.TlsCaCert = ToString(value);
|
||||
break;
|
||||
case "verify":
|
||||
opts.TlsVerify = ToBool(value);
|
||||
break;
|
||||
case "verify_and_map":
|
||||
var map = ToBool(value);
|
||||
opts.TlsMap = map;
|
||||
if (map)
|
||||
opts.TlsVerify = true;
|
||||
break;
|
||||
case "timeout":
|
||||
opts.TlsTimeout = value switch
|
||||
{
|
||||
long l => TimeSpan.FromSeconds(l),
|
||||
double d => TimeSpan.FromSeconds(d),
|
||||
string s => ParseDuration(s),
|
||||
_ => throw new FormatException($"Invalid TLS timeout type: {value?.GetType().Name}"),
|
||||
};
|
||||
break;
|
||||
case "connection_rate_limit":
|
||||
opts.TlsRateLimit = ToLong(value);
|
||||
break;
|
||||
case "pinned_certs":
|
||||
if (value is List<object?> pinnedList)
|
||||
{
|
||||
var certs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var item in pinnedList)
|
||||
{
|
||||
if (item is string s)
|
||||
certs.Add(s.ToLowerInvariant());
|
||||
}
|
||||
|
||||
opts.TlsPinnedCerts = certs;
|
||||
}
|
||||
|
||||
break;
|
||||
case "handshake_first" or "first" or "immediate":
|
||||
opts.TlsHandshakeFirst = ToBool(value);
|
||||
break;
|
||||
case "handshake_first_fallback":
|
||||
opts.TlsHandshakeFirstFallback = ParseDuration(value);
|
||||
break;
|
||||
default:
|
||||
// Unknown TLS keys silently ignored
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tags parsing ──────────────────────────────────────────────
|
||||
|
||||
private static void ParseTags(Dictionary<string, object?> dict, NatsOptions opts)
|
||||
{
|
||||
var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var (key, value) in dict)
|
||||
{
|
||||
tags[key] = ToString(value);
|
||||
}
|
||||
|
||||
opts.Tags = tags;
|
||||
}
|
||||
|
||||
// ─── Type conversion helpers ───────────────────────────────────
|
||||
|
||||
private static int ToInt(object? value) => value switch
|
||||
{
|
||||
long l => (int)l,
|
||||
int i => i,
|
||||
double d => (int)d,
|
||||
string s when int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) => i,
|
||||
_ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to int"),
|
||||
};
|
||||
|
||||
private static long ToLong(object? value) => value switch
|
||||
{
|
||||
long l => l,
|
||||
int i => i,
|
||||
double d => (long)d,
|
||||
string s when long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var l) => l,
|
||||
_ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to long"),
|
||||
};
|
||||
|
||||
private static bool ToBool(object? value) => value switch
|
||||
{
|
||||
bool b => b,
|
||||
_ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to bool"),
|
||||
};
|
||||
|
||||
private static string ToString(object? value) => value switch
|
||||
{
|
||||
string s => s,
|
||||
long l => l.ToString(CultureInfo.InvariantCulture),
|
||||
_ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to string"),
|
||||
};
|
||||
|
||||
private static IReadOnlyList<string> ToStringList(object? value)
|
||||
{
|
||||
if (value is List<object?> list)
|
||||
{
|
||||
var result = new List<string>(list.Count);
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (item is string s)
|
||||
result.Add(s);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (value is string str)
|
||||
return [str];
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when one or more configuration validation errors are detected.
|
||||
/// All errors are collected rather than failing on the first one.
|
||||
/// </summary>
|
||||
public sealed class ConfigProcessorException(string message, List<string> errors)
|
||||
: Exception(message)
|
||||
{
|
||||
public IReadOnlyList<string> Errors => errors;
|
||||
}
|
||||
504
tests/NATS.Server.Tests/ConfigProcessorTests.cs
Normal file
504
tests/NATS.Server.Tests/ConfigProcessorTests.cs
Normal file
@@ -0,0 +1,504 @@
|
||||
using NATS.Server;
|
||||
using NATS.Server.Configuration;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class ConfigProcessorTests
|
||||
{
|
||||
private static string TestDataPath(string fileName) =>
|
||||
Path.Combine(AppContext.BaseDirectory, "TestData", fileName);
|
||||
|
||||
// ─── Basic config ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_Port()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.Port.ShouldBe(4222);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_Host()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.Host.ShouldBe("0.0.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_ServerName()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.ServerName.ShouldBe("test-server");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_MaxPayload()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.MaxPayload.ShouldBe(2 * 1024 * 1024);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_MaxConnections()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.MaxConnections.ShouldBe(1000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_Debug()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.Debug.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_Trace()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.Trace.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_PingInterval()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.PingInterval.ShouldBe(TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_MaxPingsOut()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.MaxPingsOut.ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_WriteDeadline()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.WriteDeadline.ShouldBe(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_MaxSubs()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.MaxSubs.ShouldBe(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_MaxSubTokens()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.MaxSubTokens.ShouldBe(16);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_MaxControlLine()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.MaxControlLine.ShouldBe(2048);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_MaxPending()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.MaxPending.ShouldBe(32L * 1024 * 1024);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_LameDuckDuration()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.LameDuckDuration.ShouldBe(TimeSpan.FromSeconds(60));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_LameDuckGracePeriod()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.LameDuckGracePeriod.ShouldBe(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_MonitorPort()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.MonitorPort.ShouldBe(8222);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConf_Logtime()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("basic.conf"));
|
||||
opts.Logtime.ShouldBeTrue();
|
||||
opts.LogtimeUTC.ShouldBeFalse();
|
||||
}
|
||||
|
||||
// ─── Auth config ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AuthConf_SimpleUser()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("auth.conf"));
|
||||
opts.Username.ShouldBe("admin");
|
||||
opts.Password.ShouldBe("s3cret");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuthConf_AuthTimeout()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("auth.conf"));
|
||||
opts.AuthTimeout.ShouldBe(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuthConf_NoAuthUser()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("auth.conf"));
|
||||
opts.NoAuthUser.ShouldBe("guest");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuthConf_UsersArray()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("auth.conf"));
|
||||
opts.Users.ShouldNotBeNull();
|
||||
opts.Users.Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuthConf_AliceUser()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("auth.conf"));
|
||||
var alice = opts.Users!.First(u => u.Username == "alice");
|
||||
alice.Password.ShouldBe("pw1");
|
||||
alice.Permissions.ShouldNotBeNull();
|
||||
alice.Permissions!.Publish.ShouldNotBeNull();
|
||||
alice.Permissions.Publish!.Allow.ShouldNotBeNull();
|
||||
alice.Permissions.Publish.Allow!.ShouldContain("foo.>");
|
||||
alice.Permissions.Subscribe.ShouldNotBeNull();
|
||||
alice.Permissions.Subscribe!.Allow.ShouldNotBeNull();
|
||||
alice.Permissions.Subscribe.Allow!.ShouldContain(">");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuthConf_BobUser()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("auth.conf"));
|
||||
var bob = opts.Users!.First(u => u.Username == "bob");
|
||||
bob.Password.ShouldBe("pw2");
|
||||
bob.Permissions.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ─── TLS config ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TlsConf_CertFiles()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
||||
opts.TlsCert.ShouldBe("/path/to/cert.pem");
|
||||
opts.TlsKey.ShouldBe("/path/to/key.pem");
|
||||
opts.TlsCaCert.ShouldBe("/path/to/ca.pem");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TlsConf_Verify()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
||||
opts.TlsVerify.ShouldBeTrue();
|
||||
opts.TlsMap.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TlsConf_Timeout()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
||||
opts.TlsTimeout.ShouldBe(TimeSpan.FromSeconds(3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TlsConf_RateLimit()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
||||
opts.TlsRateLimit.ShouldBe(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TlsConf_PinnedCerts()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
||||
opts.TlsPinnedCerts.ShouldNotBeNull();
|
||||
opts.TlsPinnedCerts!.Count.ShouldBe(1);
|
||||
opts.TlsPinnedCerts.ShouldContain("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TlsConf_HandshakeFirst()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
||||
opts.TlsHandshakeFirst.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TlsConf_AllowNonTls()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
||||
opts.AllowNonTls.ShouldBeFalse();
|
||||
}
|
||||
|
||||
// ─── Full config ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void FullConf_CoreOptions()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
||||
opts.Port.ShouldBe(4222);
|
||||
opts.Host.ShouldBe("0.0.0.0");
|
||||
opts.ServerName.ShouldBe("full-test");
|
||||
opts.ClientAdvertise.ShouldBe("nats://public.example.com:4222");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullConf_Limits()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
||||
opts.MaxPayload.ShouldBe(1024 * 1024);
|
||||
opts.MaxControlLine.ShouldBe(4096);
|
||||
opts.MaxConnections.ShouldBe(65536);
|
||||
opts.MaxPending.ShouldBe(64L * 1024 * 1024);
|
||||
opts.MaxSubs.ShouldBe(0);
|
||||
opts.MaxSubTokens.ShouldBe(0);
|
||||
opts.MaxTracedMsgLen.ShouldBe(1024);
|
||||
opts.DisableSublistCache.ShouldBeFalse();
|
||||
opts.MaxClosedClients.ShouldBe(5000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullConf_Logging()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
||||
opts.Debug.ShouldBeFalse();
|
||||
opts.Trace.ShouldBeFalse();
|
||||
opts.TraceVerbose.ShouldBeFalse();
|
||||
opts.Logtime.ShouldBeTrue();
|
||||
opts.LogtimeUTC.ShouldBeFalse();
|
||||
opts.LogFile.ShouldBe("/var/log/nats.log");
|
||||
opts.LogSizeLimit.ShouldBe(100L * 1024 * 1024);
|
||||
opts.LogMaxFiles.ShouldBe(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullConf_Monitoring()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
||||
opts.MonitorPort.ShouldBe(8222);
|
||||
opts.MonitorBasePath.ShouldBe("/nats");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullConf_Files()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
||||
opts.PidFile.ShouldBe("/var/run/nats.pid");
|
||||
opts.PortsFileDir.ShouldBe("/var/run");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullConf_Lifecycle()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
||||
opts.LameDuckDuration.ShouldBe(TimeSpan.FromMinutes(2));
|
||||
opts.LameDuckGracePeriod.ShouldBe(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullConf_Tags()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
||||
opts.Tags.ShouldNotBeNull();
|
||||
opts.Tags!["region"].ShouldBe("us-east");
|
||||
opts.Tags["env"].ShouldBe("production");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullConf_Auth()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
||||
opts.Username.ShouldBe("admin");
|
||||
opts.Password.ShouldBe("secret");
|
||||
opts.AuthTimeout.ShouldBe(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullConf_Tls()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("full.conf"));
|
||||
opts.TlsCert.ShouldBe("/path/to/cert.pem");
|
||||
opts.TlsKey.ShouldBe("/path/to/key.pem");
|
||||
opts.TlsCaCert.ShouldBe("/path/to/ca.pem");
|
||||
opts.TlsVerify.ShouldBeTrue();
|
||||
opts.TlsTimeout.ShouldBe(TimeSpan.FromSeconds(2));
|
||||
opts.TlsHandshakeFirst.ShouldBeTrue();
|
||||
}
|
||||
|
||||
// ─── Listen combined format ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ListenCombined_HostAndPort()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfig("listen: \"10.0.0.1:5222\"");
|
||||
opts.Host.ShouldBe("10.0.0.1");
|
||||
opts.Port.ShouldBe(5222);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListenCombined_PortOnly()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfig("listen: \":5222\"");
|
||||
opts.Port.ShouldBe(5222);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListenCombined_BarePort()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfig("listen: 5222");
|
||||
opts.Port.ShouldBe(5222);
|
||||
}
|
||||
|
||||
// ─── HTTP combined format ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void HttpCombined_HostAndPort()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfig("http: \"10.0.0.1:8333\"");
|
||||
opts.MonitorHost.ShouldBe("10.0.0.1");
|
||||
opts.MonitorPort.ShouldBe(8333);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HttpsCombined_HostAndPort()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfig("https: \"10.0.0.1:8444\"");
|
||||
opts.MonitorHost.ShouldBe("10.0.0.1");
|
||||
opts.MonitorHttpsPort.ShouldBe(8444);
|
||||
}
|
||||
|
||||
// ─── Duration as number ────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void DurationAsNumber_TreatedAsSeconds()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfig("ping_interval: 60");
|
||||
opts.PingInterval.ShouldBe(TimeSpan.FromSeconds(60));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DurationAsString_Milliseconds()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfig("write_deadline: \"500ms\"");
|
||||
opts.WriteDeadline.ShouldBe(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DurationAsString_Hours()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfig("ping_interval: \"1h\"");
|
||||
opts.PingInterval.ShouldBe(TimeSpan.FromHours(1));
|
||||
}
|
||||
|
||||
// ─── Unknown keys ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void UnknownKeys_SilentlyIgnored()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfig("""
|
||||
port: 4222
|
||||
cluster { name: "my-cluster" }
|
||||
jetstream { store_dir: "/tmp/js" }
|
||||
unknown_key: "whatever"
|
||||
""");
|
||||
opts.Port.ShouldBe(4222);
|
||||
}
|
||||
|
||||
// ─── Server name validation ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ServerNameWithSpaces_ReportsError()
|
||||
{
|
||||
var ex = Should.Throw<ConfigProcessorException>(() =>
|
||||
ConfigProcessor.ProcessConfig("server_name: \"my server\""));
|
||||
ex.Errors.ShouldContain(e => e.Contains("server_name cannot contain spaces"));
|
||||
}
|
||||
|
||||
// ─── Max sub tokens validation ─────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void MaxSubTokens_ExceedsLimit_ReportsError()
|
||||
{
|
||||
var ex = Should.Throw<ConfigProcessorException>(() =>
|
||||
ConfigProcessor.ProcessConfig("max_sub_tokens: 300"));
|
||||
ex.Errors.ShouldContain(e => e.Contains("max_sub_tokens cannot exceed 256"));
|
||||
}
|
||||
|
||||
// ─── ProcessConfig from string ─────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ProcessConfig_FromString()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfig("""
|
||||
port: 9222
|
||||
host: "127.0.0.1"
|
||||
debug: true
|
||||
""");
|
||||
opts.Port.ShouldBe(9222);
|
||||
opts.Host.ShouldBe("127.0.0.1");
|
||||
opts.Debug.ShouldBeTrue();
|
||||
}
|
||||
|
||||
// ─── TraceVerbose sets Trace ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TraceVerbose_AlsoSetsTrace()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfig("trace_verbose: true");
|
||||
opts.TraceVerbose.ShouldBeTrue();
|
||||
opts.Trace.ShouldBeTrue();
|
||||
}
|
||||
|
||||
// ─── Error collection (not fail-fast) ──────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void MultipleErrors_AllCollected()
|
||||
{
|
||||
var ex = Should.Throw<ConfigProcessorException>(() =>
|
||||
ConfigProcessor.ProcessConfig("""
|
||||
server_name: "bad name"
|
||||
max_sub_tokens: 999
|
||||
"""));
|
||||
ex.Errors.Count.ShouldBe(2);
|
||||
ex.Errors.ShouldContain(e => e.Contains("server_name"));
|
||||
ex.Errors.ShouldContain(e => e.Contains("max_sub_tokens"));
|
||||
}
|
||||
|
||||
// ─── ConfigFile path tracking ──────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ProcessConfigFile_SetsConfigFilePath()
|
||||
{
|
||||
var path = TestDataPath("basic.conf");
|
||||
var opts = ConfigProcessor.ProcessConfigFile(path);
|
||||
opts.ConfigFile.ShouldBe(path);
|
||||
}
|
||||
|
||||
// ─── HasTls derived property ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void HasTls_TrueWhenCertAndKeySet()
|
||||
{
|
||||
var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("tls.conf"));
|
||||
opts.HasTls.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@
|
||||
<Using Include="Shouldly" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="TestData\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\NATS.Server\NATS.Server.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
11
tests/NATS.Server.Tests/TestData/auth.conf
Normal file
11
tests/NATS.Server.Tests/TestData/auth.conf
Normal file
@@ -0,0 +1,11 @@
|
||||
authorization {
|
||||
user: admin
|
||||
password: "s3cret"
|
||||
timeout: 5
|
||||
|
||||
users = [
|
||||
{ user: alice, password: "pw1", permissions: { publish: { allow: ["foo.>"] }, subscribe: { allow: [">"] } } }
|
||||
{ user: bob, password: "pw2" }
|
||||
]
|
||||
}
|
||||
no_auth_user: "guest"
|
||||
19
tests/NATS.Server.Tests/TestData/basic.conf
Normal file
19
tests/NATS.Server.Tests/TestData/basic.conf
Normal file
@@ -0,0 +1,19 @@
|
||||
port: 4222
|
||||
host: "0.0.0.0"
|
||||
server_name: "test-server"
|
||||
max_payload: 2mb
|
||||
max_connections: 1000
|
||||
debug: true
|
||||
trace: false
|
||||
logtime: true
|
||||
logtime_utc: false
|
||||
ping_interval: "30s"
|
||||
ping_max: 3
|
||||
write_deadline: "5s"
|
||||
max_subs: 100
|
||||
max_sub_tokens: 16
|
||||
max_control_line: 2048
|
||||
max_pending: 32mb
|
||||
lame_duck_duration: "60s"
|
||||
lame_duck_grace_period: "5s"
|
||||
http_port: 8222
|
||||
57
tests/NATS.Server.Tests/TestData/full.conf
Normal file
57
tests/NATS.Server.Tests/TestData/full.conf
Normal file
@@ -0,0 +1,57 @@
|
||||
# Full configuration with all supported options
|
||||
port: 4222
|
||||
host: "0.0.0.0"
|
||||
server_name: "full-test"
|
||||
client_advertise: "nats://public.example.com:4222"
|
||||
|
||||
max_payload: 1mb
|
||||
max_control_line: 4096
|
||||
max_connections: 65536
|
||||
max_pending: 64mb
|
||||
write_deadline: "10s"
|
||||
max_subs: 0
|
||||
max_sub_tokens: 0
|
||||
max_traced_msg_len: 1024
|
||||
disable_sublist_cache: false
|
||||
max_closed_clients: 5000
|
||||
|
||||
ping_interval: "2m"
|
||||
ping_max: 2
|
||||
|
||||
debug: false
|
||||
trace: false
|
||||
trace_verbose: false
|
||||
logtime: true
|
||||
logtime_utc: false
|
||||
logfile: "/var/log/nats.log"
|
||||
log_size_limit: 100mb
|
||||
log_max_num: 5
|
||||
|
||||
http_port: 8222
|
||||
http_base_path: "/nats"
|
||||
|
||||
pidfile: "/var/run/nats.pid"
|
||||
ports_file_dir: "/var/run"
|
||||
|
||||
lame_duck_duration: "2m"
|
||||
lame_duck_grace_period: "10s"
|
||||
|
||||
server_tags {
|
||||
region: "us-east"
|
||||
env: "production"
|
||||
}
|
||||
|
||||
authorization {
|
||||
user: admin
|
||||
password: "secret"
|
||||
timeout: 2
|
||||
}
|
||||
|
||||
tls {
|
||||
cert_file: "/path/to/cert.pem"
|
||||
key_file: "/path/to/key.pem"
|
||||
ca_file: "/path/to/ca.pem"
|
||||
verify: true
|
||||
timeout: 2
|
||||
handshake_first: true
|
||||
}
|
||||
12
tests/NATS.Server.Tests/TestData/tls.conf
Normal file
12
tests/NATS.Server.Tests/TestData/tls.conf
Normal file
@@ -0,0 +1,12 @@
|
||||
tls {
|
||||
cert_file: "/path/to/cert.pem"
|
||||
key_file: "/path/to/key.pem"
|
||||
ca_file: "/path/to/ca.pem"
|
||||
verify: true
|
||||
verify_and_map: true
|
||||
timeout: 3
|
||||
connection_rate_limit: 100
|
||||
pinned_certs: ["abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"]
|
||||
handshake_first: true
|
||||
}
|
||||
allow_non_tls: false
|
||||
Reference in New Issue
Block a user