feat: port session 09 — Server Core Init & Config
Port server/server.go account management and initialization (~1950 LOC): - NatsServer.cs: full server struct fields (atomic counters, locks, maps, stubs for gateway/websocket/mqtt/ocsp/leafnode) - NatsServer.Init.cs: factory methods (New/NewServer/NewServerFromConfig), compression helpers (ValidateAndNormalizeCompressionOption, SelectCompressionMode, SelectS2AutoModeBasedOnRtt, CompressOptsEqual), cluster-name management, validation (ValidateCluster, ValidatePinnedCerts, ValidateOptions), trusted-key processing, CLI helpers, running-state checks, and Start() stub - NatsServer.Accounts.cs: account management (ConfigureAccounts, LookupOrRegisterAccount, RegisterAccount, SetSystemAccount, SetDefaultSystemAccount, SetSystemAccountInternal, CreateInternalClient*, ShouldTrackSubscriptions, RegisterAccountNoLock, SetAccountSublist, SetRouteInfo, LookupAccount, LookupOrFetchAccount, UpdateAccount, UpdateAccountWithClaimJwt, FetchRawAccountClaims, FetchAccountClaims, VerifyAccountClaims, FetchAccountFromResolver, GlobalAccountOnly, StandAloneMode, ConfiguredRoutes, ActivePeers, ComputeRoutePoolIdx) - NatsServerTypes.cs: ServerInfo, ServerStats, NodeInfo, ServerProtocol, CompressionMode constants, AccountClaims stub, InternalState stub, and cross-session stubs for JetStream/gateway/websocket/mqtt/ocsp - AuthTypes.cs: extend Account stub with Issuer, ClaimJwt, RoutePoolIdx, Incomplete, Updated, Sublist, Server fields, and IsExpired() - ServerOptions.cs: add Accounts property (List<Account>) - ServerTests.cs: 38 standalone tests (IDs 2866, 2882, plus compression and validation helpers); server-dependent tests marked n/a Features: 77 complete (IDs 2974–3050) Tests: 2 complete (2866, 2882); 18 n/a (server-dependent) All tests: 545 unit + 1 integration pass
This commit is contained in:
@@ -167,10 +167,23 @@ public class RoutePermissions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stub for Account type. Full implementation in later sessions.
|
||||
/// Stub for Account type. Full implementation in session 11.
|
||||
/// Mirrors Go <c>Account</c> struct.
|
||||
/// </summary>
|
||||
public class Account
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
// Fields required by session 09 account management (NatsServer.Accounts.cs).
|
||||
// Full implementation in session 11.
|
||||
public string Issuer { get; set; } = string.Empty;
|
||||
internal string ClaimJwt { get; set; } = string.Empty;
|
||||
internal int RoutePoolIdx { get; set; }
|
||||
internal bool Incomplete { get; set; }
|
||||
internal DateTime Updated { get; set; }
|
||||
internal ZB.MOM.NatsNet.Server.Internal.DataStructures.SubscriptionIndex? Sublist { get; set; }
|
||||
internal object? Server { get; set; } // INatsServer — avoids circular reference
|
||||
|
||||
/// <summary>Returns true if this account's JWT has expired. Stub — full impl in session 11.</summary>
|
||||
public bool IsExpired() => false;
|
||||
}
|
||||
|
||||
713
dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Accounts.cs
Normal file
713
dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Accounts.cs
Normal file
@@ -0,0 +1,713 @@
|
||||
// Copyright 2012-2026 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Adapted from server/server.go (account methods) in the NATS server Go source.
|
||||
// Session 09: account management — configure, register, lookup, fetch.
|
||||
|
||||
using ZB.MOM.NatsNet.Server.Auth;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
public sealed partial class NatsServer
|
||||
{
|
||||
// =========================================================================
|
||||
// Account-mode helpers (features 3004–3007)
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when only the global ($G) account is defined (pre-NATS 2.0 mode).
|
||||
/// Mirrors Go <c>Server.globalAccountOnly</c>.
|
||||
/// </summary>
|
||||
public bool GlobalAccountOnly()
|
||||
{
|
||||
if (_trustedKeys is not null) return false;
|
||||
|
||||
bool hasOthers = false;
|
||||
_mu.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var kvp in _accounts)
|
||||
{
|
||||
var acc = kvp.Value;
|
||||
// Ignore global and system accounts.
|
||||
if (acc == _gacc) continue;
|
||||
var sysAcc = _sysAccAtomic;
|
||||
if (sysAcc != null && acc == sysAcc) continue;
|
||||
hasOthers = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally { _mu.ExitReadLock(); }
|
||||
|
||||
return !hasOthers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when this server has no routes or gateways configured.
|
||||
/// Mirrors Go <c>Server.standAloneMode</c>.
|
||||
/// </summary>
|
||||
public bool StandAloneMode()
|
||||
{
|
||||
var opts = GetOpts();
|
||||
return opts.Cluster.Port == 0 && opts.Gateway.Port == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of configured routes.
|
||||
/// Mirrors Go <c>Server.configuredRoutes</c>.
|
||||
/// </summary>
|
||||
public int ConfiguredRoutes() => GetOpts().Routes.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Returns online JetStream peer node names from the node-info map.
|
||||
/// Mirrors Go <c>Server.ActivePeers</c>.
|
||||
/// </summary>
|
||||
public List<string> ActivePeers()
|
||||
{
|
||||
var peers = new List<string>();
|
||||
foreach (var kvp in _nodeToInfo)
|
||||
{
|
||||
if (kvp.Value is NodeInfo ni && !ni.Offline)
|
||||
peers.Add(kvp.Key);
|
||||
}
|
||||
return peers;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ConfigureAccounts (feature 3001)
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Reads accounts from options and registers/updates them.
|
||||
/// Returns a set of account names whose stream imports changed (for reload)
|
||||
/// and any error.
|
||||
/// Mirrors Go <c>Server.configureAccounts</c>.
|
||||
/// Server lock must be held on entry.
|
||||
/// </summary>
|
||||
public (HashSet<string> ChangedStreamImports, Exception? Error) ConfigureAccounts(bool reloading)
|
||||
{
|
||||
var awcsti = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
// Create the global ($G) account if not yet present.
|
||||
if (_gacc == null)
|
||||
{
|
||||
_gacc = new Account { Name = ServerConstants.DefaultGlobalAccount };
|
||||
RegisterAccountNoLock(_gacc);
|
||||
}
|
||||
|
||||
var opts = GetOpts();
|
||||
|
||||
// Walk accounts from options.
|
||||
foreach (var optAcc in opts.Accounts)
|
||||
{
|
||||
Account a;
|
||||
bool create = true;
|
||||
|
||||
if (reloading && optAcc.Name != ServerConstants.DefaultGlobalAccount)
|
||||
{
|
||||
if (_accounts.TryGetValue(optAcc.Name, out var existing))
|
||||
{
|
||||
a = existing;
|
||||
// Full import/export diffing deferred to session 11 (accounts.go).
|
||||
create = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
a = new Account { Name = optAcc.Name };
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
a = optAcc.Name == ServerConstants.DefaultGlobalAccount ? _gacc! : new Account { Name = optAcc.Name };
|
||||
}
|
||||
|
||||
if (create)
|
||||
{
|
||||
// Will be a no-op for the global account (already registered).
|
||||
RegisterAccountNoLock(a);
|
||||
}
|
||||
|
||||
// If an account named $SYS is found, make it the system account.
|
||||
if (optAcc.Name == ServerConstants.DefaultSystemAccount &&
|
||||
string.IsNullOrEmpty(opts.SystemAccount))
|
||||
{
|
||||
opts.SystemAccount = ServerConstants.DefaultSystemAccount;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve system account if configured.
|
||||
if (!string.IsNullOrEmpty(opts.SystemAccount))
|
||||
{
|
||||
// Release server lock for lookupAccount (lock ordering: account → server).
|
||||
_mu.ExitWriteLock();
|
||||
var (acc, err) = LookupAccountInternal(opts.SystemAccount);
|
||||
_mu.EnterWriteLock();
|
||||
|
||||
if (err != null)
|
||||
return (awcsti, new InvalidOperationException($"error resolving system account: {err.Message}", err));
|
||||
|
||||
if (acc != null && _sys != null && acc != _sys.Account)
|
||||
_sys.Account = acc;
|
||||
|
||||
if (acc != null)
|
||||
_sysAccAtomic = acc;
|
||||
}
|
||||
|
||||
return (awcsti, null);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Account counts (features 3022–3023)
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Returns the total number of registered accounts (slow, test only).
|
||||
/// Mirrors Go <c>Server.numAccounts</c>.
|
||||
/// </summary>
|
||||
public int NumAccounts()
|
||||
{
|
||||
_mu.EnterReadLock();
|
||||
try { return _accounts.Count; }
|
||||
finally { _mu.ExitReadLock(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of loaded accounts.
|
||||
/// Mirrors Go <c>Server.NumLoadedAccounts</c>.
|
||||
/// </summary>
|
||||
public int NumLoadedAccounts() => NumAccounts();
|
||||
|
||||
// =========================================================================
|
||||
// Account registration (features 3024–3025)
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Returns the named account if known, or creates and registers a new one.
|
||||
/// Mirrors Go <c>Server.LookupOrRegisterAccount</c>.
|
||||
/// </summary>
|
||||
public (Account Account, bool IsNew) LookupOrRegisterAccount(string name)
|
||||
{
|
||||
_mu.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (_accounts.TryGetValue(name, out var existing))
|
||||
return (existing, false);
|
||||
|
||||
var acc = new Account { Name = name };
|
||||
RegisterAccountNoLock(acc);
|
||||
return (acc, true);
|
||||
}
|
||||
finally { _mu.ExitWriteLock(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new account. Returns error if the account already exists.
|
||||
/// Mirrors Go <c>Server.RegisterAccount</c>.
|
||||
/// </summary>
|
||||
public (Account? Account, Exception? Error) RegisterAccount(string name)
|
||||
{
|
||||
_mu.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (_accounts.ContainsKey(name))
|
||||
return (null, ServerErrors.ErrAccountExists);
|
||||
|
||||
var acc = new Account { Name = name };
|
||||
RegisterAccountNoLock(acc);
|
||||
return (acc, null);
|
||||
}
|
||||
finally { _mu.ExitWriteLock(); }
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// System account (features 3026–3030)
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Sets the named account as the server's system account.
|
||||
/// Mirrors Go <c>Server.SetSystemAccount</c>.
|
||||
/// </summary>
|
||||
public Exception? SetSystemAccount(string accName)
|
||||
{
|
||||
if (_accounts.TryGetValue(accName, out var acc))
|
||||
return SetSystemAccountInternal(acc);
|
||||
|
||||
// Not locally known — try resolver.
|
||||
var (ac, _, fetchErr) = FetchAccountClaims(accName);
|
||||
if (fetchErr != null) return fetchErr;
|
||||
|
||||
var newAcc = BuildInternalAccount(ac);
|
||||
// Due to race, registerAccount returns the existing one if already registered.
|
||||
var racc = RegisterAccountInternal(newAcc);
|
||||
return SetSystemAccountInternal(racc ?? newAcc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the system account, or null if none is set.
|
||||
/// Mirrors Go <c>Server.SystemAccount</c>.
|
||||
/// </summary>
|
||||
public Account? SystemAccount() => _sysAccAtomic;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the global ($G) account.
|
||||
/// Mirrors Go <c>Server.GlobalAccount</c>.
|
||||
/// </summary>
|
||||
public Account? GlobalAccount()
|
||||
{
|
||||
_mu.EnterReadLock();
|
||||
try { return _gacc; }
|
||||
finally { _mu.ExitReadLock(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a default system account ($SYS) if one does not already exist.
|
||||
/// Mirrors Go <c>Server.SetDefaultSystemAccount</c>.
|
||||
/// </summary>
|
||||
public Exception? SetDefaultSystemAccount()
|
||||
{
|
||||
var (_, isNew) = LookupOrRegisterAccount(ServerConstants.DefaultSystemAccount);
|
||||
if (!isNew) return null;
|
||||
Debugf("Created system account: \"{0}\"", ServerConstants.DefaultSystemAccount);
|
||||
return SetSystemAccount(ServerConstants.DefaultSystemAccount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns <paramref name="acc"/> as the system account and starts internal
|
||||
/// messaging goroutines.
|
||||
/// Mirrors Go <c>Server.setSystemAccount</c>.
|
||||
/// Server lock must NOT be held on entry; this method acquires/releases it.
|
||||
/// </summary>
|
||||
public Exception? SetSystemAccountInternal(Account acc)
|
||||
{
|
||||
if (acc == null)
|
||||
return ServerErrors.ErrMissingAccount;
|
||||
if (acc.IsExpired())
|
||||
return ServerErrors.ErrAccountExpired;
|
||||
if (!IsTrustedIssuer(acc.Issuer))
|
||||
return ServerErrors.ErrAccountValidation;
|
||||
|
||||
_mu.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (_sys != null)
|
||||
return ServerErrors.ErrAccountExists;
|
||||
|
||||
_sys = new InternalState { Account = acc };
|
||||
}
|
||||
finally { _mu.ExitWriteLock(); }
|
||||
|
||||
// Store atomically for fast lookup on hot paths.
|
||||
_sysAccAtomic = acc;
|
||||
|
||||
// Full internal-messaging bootstrap (initEventTracking, sendLoop, etc.)
|
||||
// is deferred to session 12 (events.go).
|
||||
AddSystemAccountExports(acc);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Internal client factories (features 3031–3034)
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>Creates an internal system client.</summary>
|
||||
public ClientConnection CreateInternalSystemClient() =>
|
||||
CreateInternalClient(ClientKind.System);
|
||||
|
||||
/// <summary>Creates an internal JetStream client.</summary>
|
||||
public ClientConnection CreateInternalJetStreamClient() =>
|
||||
CreateInternalClient(ClientKind.JetStream);
|
||||
|
||||
/// <summary>Creates an internal account client.</summary>
|
||||
public ClientConnection CreateInternalAccountClient() =>
|
||||
CreateInternalClient(ClientKind.Account);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an internal client of the given <paramref name="kind"/>.
|
||||
/// Mirrors Go <c>Server.createInternalClient</c>.
|
||||
/// </summary>
|
||||
public ClientConnection CreateInternalClient(ClientKind kind)
|
||||
{
|
||||
if (kind != ClientKind.System && kind != ClientKind.JetStream && kind != ClientKind.Account)
|
||||
throw new InvalidOperationException($"createInternalClient: unsupported kind {kind}");
|
||||
|
||||
var c = new ClientConnection(kind, this);
|
||||
// Mirrors: c.echo = false; c.headers = true; flags.set(noReconnect)
|
||||
// Full client initialisation deferred to session 10 (client.go).
|
||||
return c;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Subscription tracking / account sublist (features 3035–3038)
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if accounts should track subscriptions for route/gateway propagation.
|
||||
/// Server lock must be held on entry.
|
||||
/// Mirrors Go <c>Server.shouldTrackSubscriptions</c>.
|
||||
/// </summary>
|
||||
public bool ShouldTrackSubscriptions()
|
||||
{
|
||||
var opts = GetOpts();
|
||||
return opts.Cluster.Port != 0 || opts.Gateway.Port != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes <see cref="RegisterAccountNoLock"/> under the server write lock.
|
||||
/// Returns the already-registered account if a duplicate is detected, or null.
|
||||
/// Mirrors Go <c>Server.registerAccount</c>.
|
||||
/// </summary>
|
||||
public Account? RegisterAccountInternal(Account acc)
|
||||
{
|
||||
_mu.EnterWriteLock();
|
||||
try { return RegisterAccountNoLock(acc); }
|
||||
finally { _mu.ExitWriteLock(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the account's subscription index based on the NoSublistCache option.
|
||||
/// Mirrors Go <c>Server.setAccountSublist</c>.
|
||||
/// Server lock must be held on entry.
|
||||
/// </summary>
|
||||
public void SetAccountSublist(Account acc)
|
||||
{
|
||||
if (acc?.Sublist != null) return;
|
||||
if (acc == null) return;
|
||||
var opts = GetOpts();
|
||||
acc.Sublist = opts?.NoSublistCache == true
|
||||
? SubscriptionIndex.NewSublist(false)
|
||||
: SubscriptionIndex.NewSublistWithCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an account in the server's account map.
|
||||
/// If the account is already registered (race), returns the existing one.
|
||||
/// Server lock must be held on entry.
|
||||
/// Mirrors Go <c>Server.registerAccountNoLock</c>.
|
||||
/// </summary>
|
||||
public Account? RegisterAccountNoLock(Account acc)
|
||||
{
|
||||
// If already registered, return existing.
|
||||
if (_accounts.TryGetValue(acc.Name, out var existing))
|
||||
{
|
||||
_tmpAccounts.TryRemove(acc.Name, out _);
|
||||
return existing;
|
||||
}
|
||||
|
||||
SetAccountSublist(acc);
|
||||
SetRouteInfo(acc);
|
||||
acc.Server = this;
|
||||
acc.Updated = DateTime.UtcNow;
|
||||
|
||||
_accounts[acc.Name] = acc;
|
||||
_tmpAccounts.TryRemove(acc.Name, out _);
|
||||
|
||||
// enableAccountTracking and registerSystemImports deferred to session 12.
|
||||
EnableAccountTracking(acc);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Route info for accounts (feature 3039)
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Sets the account's route-pool index based on cluster configuration.
|
||||
/// Mirrors Go <c>Server.setRouteInfo</c>.
|
||||
/// Both server and account locks must be held on entry.
|
||||
/// </summary>
|
||||
public void SetRouteInfo(Account acc)
|
||||
{
|
||||
const int accDedicatedRoute = -1;
|
||||
|
||||
if (_accRoutes != null && _accRoutes.ContainsKey(acc.Name))
|
||||
{
|
||||
// Dedicated route: store name in hash map; use index -1.
|
||||
_accRouteByHash.TryAdd(acc.Name, null);
|
||||
acc.RoutePoolIdx = accDedicatedRoute;
|
||||
}
|
||||
else
|
||||
{
|
||||
acc.RoutePoolIdx = ComputeRoutePoolIdx(_routesPoolSize, acc.Name);
|
||||
if (_routesPoolSize > 1)
|
||||
_accRouteByHash.TryAdd(acc.Name, acc.RoutePoolIdx);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Account lookup (features 3040–3042)
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Returns the account for <paramref name="name"/> if locally known, without
|
||||
/// fetching from the resolver.
|
||||
/// Mirrors Go <c>Server.lookupAccountInternal</c> (private helper).
|
||||
/// Lock must NOT be held on entry.
|
||||
/// </summary>
|
||||
public (Account? Account, Exception? Error) LookupAccountInternal(string name)
|
||||
=> LookupOrFetchAccount(name, fetch: false);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the account for <paramref name="name"/>, optionally fetching from
|
||||
/// the resolver if not locally known or if expired.
|
||||
/// Mirrors Go <c>Server.lookupOrFetchAccount</c>.
|
||||
/// Lock must NOT be held on entry.
|
||||
/// </summary>
|
||||
public (Account? Account, Exception? Error) LookupOrFetchAccount(string name, bool fetch)
|
||||
{
|
||||
_accounts.TryGetValue(name, out var acc);
|
||||
|
||||
if (acc != null)
|
||||
{
|
||||
if (acc.IsExpired())
|
||||
{
|
||||
Debugf("Requested account [{0}] has expired", name);
|
||||
if (_accResolver != null && fetch)
|
||||
{
|
||||
var updateErr = UpdateAccount(acc);
|
||||
if (updateErr != null)
|
||||
return (null, ServerErrors.ErrAccountExpired);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (null, ServerErrors.ErrAccountExpired);
|
||||
}
|
||||
}
|
||||
return (acc, null);
|
||||
}
|
||||
|
||||
if (_accResolver == null || !fetch)
|
||||
return (null, ServerErrors.ErrMissingAccount);
|
||||
|
||||
return FetchAccountFromResolver(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public account lookup — always fetches from resolver if needed.
|
||||
/// Mirrors Go <c>Server.LookupAccount</c>.
|
||||
/// </summary>
|
||||
public (Account? Account, Exception? Error) LookupAccount(string name)
|
||||
=> LookupOrFetchAccount(name, fetch: true);
|
||||
|
||||
// =========================================================================
|
||||
// Account update (features 3043–3044)
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Fetches fresh claims and updates the account if the claims have changed.
|
||||
/// Mirrors Go <c>Server.updateAccount</c>.
|
||||
/// Lock must NOT be held on entry.
|
||||
/// </summary>
|
||||
public Exception? UpdateAccount(Account acc)
|
||||
{
|
||||
// Don't update more than once per second unless the account is incomplete.
|
||||
if (!acc.Incomplete && (DateTime.UtcNow - acc.Updated) < TimeSpan.FromSeconds(1))
|
||||
{
|
||||
Debugf("Requested account update for [{0}] ignored, too soon", acc.Name);
|
||||
return ServerErrors.ErrAccountResolverUpdateTooSoon;
|
||||
}
|
||||
|
||||
var (claimJwt, err) = FetchRawAccountClaims(acc.Name);
|
||||
if (err != null) return err;
|
||||
|
||||
return UpdateAccountWithClaimJwt(acc, claimJwt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies updated JWT claims to the account if they differ from what is stored.
|
||||
/// Mirrors Go <c>Server.updateAccountWithClaimJWT</c>.
|
||||
/// Lock must NOT be held on entry.
|
||||
/// </summary>
|
||||
public Exception? UpdateAccountWithClaimJwt(Account acc, string claimJwt)
|
||||
{
|
||||
if (acc == null) return ServerErrors.ErrMissingAccount;
|
||||
|
||||
// If JWT hasn't changed and account is not incomplete, skip.
|
||||
if (!string.IsNullOrEmpty(acc.ClaimJwt) && acc.ClaimJwt == claimJwt && !acc.Incomplete)
|
||||
{
|
||||
Debugf("Requested account update for [{0}], same claims detected", acc.Name);
|
||||
return null;
|
||||
}
|
||||
|
||||
var (accClaims, _, verifyErr) = VerifyAccountClaims(claimJwt);
|
||||
if (verifyErr != null) return verifyErr;
|
||||
if (accClaims == null) return null;
|
||||
|
||||
if (acc.Name != accClaims.Subject)
|
||||
return ServerErrors.ErrAccountValidation;
|
||||
|
||||
acc.Issuer = accClaims.Issuer;
|
||||
// Full UpdateAccountClaims() deferred to session 11.
|
||||
acc.ClaimJwt = claimJwt;
|
||||
return null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Account claims fetch / verify (features 3045–3048)
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the raw JWT string for an account from the resolver.
|
||||
/// Mirrors Go <c>Server.fetchRawAccountClaims</c>.
|
||||
/// Lock must NOT be held on entry.
|
||||
/// </summary>
|
||||
public (string Jwt, Exception? Error) FetchRawAccountClaims(string name)
|
||||
{
|
||||
if (_accResolver == null)
|
||||
return (string.Empty, ServerErrors.ErrNoAccountResolver);
|
||||
|
||||
var start = DateTime.UtcNow;
|
||||
var (jwt, err) = FetchAccountFromResolverRaw(name);
|
||||
var elapsed = DateTime.UtcNow - start;
|
||||
|
||||
if (elapsed > TimeSpan.FromSeconds(1))
|
||||
Warnf("Account [{0}] fetch took {1}", name, elapsed);
|
||||
else
|
||||
Debugf("Account [{0}] fetch took {1}", name, elapsed);
|
||||
|
||||
if (err != null)
|
||||
{
|
||||
Warnf("Account fetch failed: {0}", err);
|
||||
return (string.Empty, err);
|
||||
}
|
||||
return (jwt, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches and decodes account JWT claims from the resolver.
|
||||
/// Mirrors Go <c>Server.fetchAccountClaims</c>.
|
||||
/// Lock must NOT be held on entry.
|
||||
/// </summary>
|
||||
public (AccountClaims? Claims, string Jwt, Exception? Error) FetchAccountClaims(string name)
|
||||
{
|
||||
var (claimJwt, err) = FetchRawAccountClaims(name);
|
||||
if (err != null) return (null, string.Empty, err);
|
||||
|
||||
var (claims, verifiedJwt, verifyErr) = VerifyAccountClaims(claimJwt);
|
||||
if (claims != null && claims.Subject != name)
|
||||
return (null, string.Empty, ServerErrors.ErrAccountValidation);
|
||||
|
||||
return (claims, verifiedJwt, verifyErr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes and validates an account JWT string.
|
||||
/// Mirrors Go <c>Server.verifyAccountClaims</c>.
|
||||
/// </summary>
|
||||
public (AccountClaims? Claims, string Jwt, Exception? Error) VerifyAccountClaims(string claimJwt)
|
||||
{
|
||||
// Full JWT decoding deferred to session 06 JWT integration.
|
||||
// Stub: create a minimal claims object from the raw JWT.
|
||||
var claims = AccountClaims.TryDecode(claimJwt);
|
||||
if (claims == null)
|
||||
return (null, string.Empty, ServerErrors.ErrAccountValidation);
|
||||
|
||||
if (!IsTrustedIssuer(claims.Issuer))
|
||||
return (null, string.Empty, ServerErrors.ErrAccountValidation);
|
||||
|
||||
return (claims, claimJwt, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches an account from the resolver, registers it, and returns it.
|
||||
/// Mirrors Go <c>Server.fetchAccount</c>.
|
||||
/// Lock must NOT be held on entry.
|
||||
/// </summary>
|
||||
public (Account? Account, Exception? Error) FetchAccountFromResolver(string name)
|
||||
{
|
||||
var (accClaims, claimJwt, err) = FetchAccountClaims(name);
|
||||
if (accClaims == null) return (null, err);
|
||||
|
||||
var acc = BuildInternalAccount(accClaims);
|
||||
// Due to possible race, registerAccount may return an already-registered account.
|
||||
var racc = RegisterAccountInternal(acc);
|
||||
if (racc != null)
|
||||
{
|
||||
// Update with new claims if changed.
|
||||
var updateErr = UpdateAccountWithClaimJwt(racc, claimJwt);
|
||||
return updateErr != null ? (null, updateErr) : (racc, null);
|
||||
}
|
||||
|
||||
acc.ClaimJwt = claimJwt;
|
||||
return (acc, null);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Account helpers
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Builds an Account from decoded claims.
|
||||
/// Mirrors Go <c>Server.buildInternalAccount</c>.
|
||||
/// Full JetStream limits / import / export wiring deferred to session 11.
|
||||
/// </summary>
|
||||
internal Account BuildInternalAccount(AccountClaims? claims)
|
||||
{
|
||||
var acc = new Account
|
||||
{
|
||||
Name = claims?.Subject ?? string.Empty,
|
||||
Issuer = claims?.Issuer ?? string.Empty,
|
||||
};
|
||||
return acc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the raw JWT directly from the resolver without timing/logging.
|
||||
/// </summary>
|
||||
private (string Jwt, Exception? Error) FetchAccountFromResolverRaw(string name)
|
||||
{
|
||||
if (_accResolver == null)
|
||||
return (string.Empty, ServerErrors.ErrNoAccountResolver);
|
||||
var (jwt, err) = _accResolver.Fetch(name);
|
||||
return (jwt, err);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the route-pool index for an account name using FNV-32a.
|
||||
/// Mirrors Go <c>computeRoutePoolIdx</c> (route.go).
|
||||
/// </summary>
|
||||
internal static int ComputeRoutePoolIdx(int poolSize, string accountName)
|
||||
{
|
||||
if (poolSize <= 1) return 0;
|
||||
// FNV-32a hash (Go uses fnv.New32a)
|
||||
uint hash = 2166136261u;
|
||||
foreach (var b in System.Text.Encoding.UTF8.GetBytes(accountName))
|
||||
{
|
||||
hash ^= b;
|
||||
hash *= 16777619u;
|
||||
}
|
||||
return (int)(hash % (uint)poolSize);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Stubs for subsystems implemented in later sessions
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Stub: enables account tracking (session 12 — events.go).
|
||||
/// </summary>
|
||||
internal void EnableAccountTracking(Account acc) { /* session 12 */ }
|
||||
|
||||
/// <summary>
|
||||
/// Stub: registers system imports on an account (session 12).
|
||||
/// </summary>
|
||||
internal void RegisterSystemImports(Account acc) { /* session 12 */ }
|
||||
|
||||
/// <summary>
|
||||
/// Stub: adds system-account exports (session 12).
|
||||
/// </summary>
|
||||
internal void AddSystemAccountExports(Account acc) { /* session 12 */ }
|
||||
}
|
||||
1058
dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Init.cs
Normal file
1058
dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Init.cs
Normal file
File diff suppressed because it is too large
Load Diff
305
dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.cs
Normal file
305
dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.cs
Normal file
@@ -0,0 +1,305 @@
|
||||
// Copyright 2012-2026 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Adapted from server/server.go in the NATS server Go source.
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Channels;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.NatsNet.Server.Auth;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
/// <summary>
|
||||
/// The core NATS server class.
|
||||
/// Mirrors Go <c>Server</c> struct in server/server.go.
|
||||
/// Session 09: initialization, configuration, and account management.
|
||||
/// Sessions 10-23 add further capabilities as partial class files.
|
||||
/// </summary>
|
||||
public sealed partial class NatsServer : INatsServer
|
||||
{
|
||||
// =========================================================================
|
||||
// Build-time stamps (mirrors package-level vars in server.go)
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Binary-stamped trusted operator keys (space-separated NKey public keys).
|
||||
/// In Go this is a package-level var that can be overridden at build time.
|
||||
/// In .NET it can be set before constructing any server instance.
|
||||
/// Mirrors Go package-level <c>trustedKeys</c> var.
|
||||
/// </summary>
|
||||
public static string StampedTrustedKeys { get; set; } = string.Empty;
|
||||
|
||||
// =========================================================================
|
||||
// Atomic counters (mirrors fields accessed with atomic operations)
|
||||
// =========================================================================
|
||||
|
||||
private ulong _gcid; // global client id counter
|
||||
private long _pinnedAccFail; // pinned-account auth failures
|
||||
private int _activeAccounts; // number of active accounts
|
||||
|
||||
// =========================================================================
|
||||
// Stats (embedded Go structs: stats, scStats, staleStats)
|
||||
// =========================================================================
|
||||
|
||||
private readonly ServerStats _stats = new();
|
||||
private readonly SlowConsumerStats _scStats = new();
|
||||
private readonly StaleConnectionStats _staleStats = new();
|
||||
|
||||
// =========================================================================
|
||||
// Core identity
|
||||
// =========================================================================
|
||||
|
||||
// kp / xkp are NKey keypairs — represented as byte arrays here.
|
||||
// Full crypto operations deferred to auth session.
|
||||
private byte[]? _kpSeed; // server NKey seed
|
||||
private string _pub = string.Empty; // server public key (server ID)
|
||||
private byte[]? _xkpSeed; // x25519 key seed
|
||||
private string _xpub = string.Empty; // x25519 public key
|
||||
|
||||
// =========================================================================
|
||||
// Server info (wire protocol)
|
||||
// =========================================================================
|
||||
|
||||
private readonly ReaderWriterLockSlim _mu = new(LockRecursionPolicy.SupportsRecursion);
|
||||
private readonly ReaderWriterLockSlim _reloadMu = new(LockRecursionPolicy.SupportsRecursion);
|
||||
private ServerInfo _info = new();
|
||||
private string _configFile = string.Empty;
|
||||
|
||||
// =========================================================================
|
||||
// Options (protected by _optsMu)
|
||||
// =========================================================================
|
||||
|
||||
private readonly ReaderWriterLockSlim _optsMu = new(LockRecursionPolicy.NoRecursion);
|
||||
private ServerOptions _opts;
|
||||
|
||||
// =========================================================================
|
||||
// Running / shutdown state
|
||||
// =========================================================================
|
||||
|
||||
private int _running; // 1 = running, 0 = not (Interlocked)
|
||||
private int _shutdown; // 1 = shutting down
|
||||
private readonly CancellationTokenSource _quitCts = new();
|
||||
private readonly TaskCompletionSource _startupComplete = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private readonly TaskCompletionSource _shutdownComplete = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private Task? _quitTask;
|
||||
|
||||
// =========================================================================
|
||||
// Listeners (forward-declared stubs — fully wired in session 10)
|
||||
// =========================================================================
|
||||
|
||||
private System.Net.Sockets.TcpListener? _listener;
|
||||
private Exception? _listenerErr;
|
||||
|
||||
// =========================================================================
|
||||
// Accounts
|
||||
// =========================================================================
|
||||
|
||||
private Account? _gacc; // global account
|
||||
private Account? _sysAccAtomic; // system account (atomic)
|
||||
private readonly ConcurrentDictionary<string, Account> _accounts = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, Account> _tmpAccounts = new(StringComparer.Ordinal);
|
||||
private IAccountResolver? _accResolver;
|
||||
private InternalState? _sys; // system messaging state
|
||||
|
||||
// =========================================================================
|
||||
// Client/route/leaf tracking
|
||||
// =========================================================================
|
||||
|
||||
private readonly Dictionary<ulong, ClientConnection> _clients = [];
|
||||
private readonly Dictionary<ulong, ClientConnection> _leafs = [];
|
||||
private Dictionary<string, List<ClientConnection>> _routes = [];
|
||||
private int _routesPoolSize = 1;
|
||||
private bool _routesReject;
|
||||
private int _routesNoPool;
|
||||
private Dictionary<string, Dictionary<string, ClientConnection>>? _accRoutes;
|
||||
private readonly ConcurrentDictionary<string, object?> _accRouteByHash = new(StringComparer.Ordinal);
|
||||
private Channel<struct_>? _accAddedCh; // stub
|
||||
private string _accAddedReqId = string.Empty;
|
||||
|
||||
// =========================================================================
|
||||
// User / nkey maps
|
||||
// =========================================================================
|
||||
|
||||
private Dictionary<string, Auth.User>? _users;
|
||||
private Dictionary<string, Auth.NkeyUser>? _nkeys;
|
||||
|
||||
// =========================================================================
|
||||
// Connection tracking
|
||||
// =========================================================================
|
||||
|
||||
private ulong _totalClients;
|
||||
private ClosedRingBuffer _closed = new(0);
|
||||
private DateTime _start;
|
||||
private DateTime _configTime;
|
||||
|
||||
// =========================================================================
|
||||
// Goroutine / WaitGroup tracking
|
||||
// =========================================================================
|
||||
|
||||
private readonly object _grMu = new();
|
||||
private bool _grRunning;
|
||||
private readonly Dictionary<ulong, ClientConnection> _grTmpClients = [];
|
||||
private readonly SemaphoreSlim _grWg = new(1, 1); // simplified wg
|
||||
|
||||
// =========================================================================
|
||||
// Cluster name (separate lock)
|
||||
// =========================================================================
|
||||
|
||||
private readonly ReaderWriterLockSlim _cnMu = new(LockRecursionPolicy.NoRecursion);
|
||||
private string _cn = string.Empty;
|
||||
private ServerInfo _routeInfo = new();
|
||||
private bool _leafNoCluster;
|
||||
private bool _leafNodeEnabled;
|
||||
private bool _leafDisableConnect;
|
||||
private bool _ldm;
|
||||
|
||||
// =========================================================================
|
||||
// Trusted keys
|
||||
// =========================================================================
|
||||
|
||||
private List<string>? _trustedKeys;
|
||||
private HashSet<string> _strictSigningKeyUsage = [];
|
||||
|
||||
// =========================================================================
|
||||
// Monitoring / stats endpoint
|
||||
// =========================================================================
|
||||
|
||||
private string _httpBasePath = string.Empty;
|
||||
private readonly Dictionary<string, ulong> _httpReqStats = [];
|
||||
|
||||
// =========================================================================
|
||||
// Client connect URLs
|
||||
// =========================================================================
|
||||
|
||||
private readonly List<string> _clientConnectUrls = [];
|
||||
private readonly RefCountedUrlSet _clientConnectUrlsMap = new();
|
||||
|
||||
// =========================================================================
|
||||
// Gateway / Websocket / MQTT / OCSP stubs
|
||||
// =========================================================================
|
||||
|
||||
private readonly SrvGateway _gateway = new();
|
||||
private readonly SrvWebsocket _websocket = new();
|
||||
private readonly SrvMqtt _mqtt = new();
|
||||
private OcspMonitor[]? _ocsps;
|
||||
private bool _ocspPeerVerify;
|
||||
private IOcspResponseCache? _ocsprc;
|
||||
|
||||
// =========================================================================
|
||||
// Gateway reply map (stub — session 16)
|
||||
// =========================================================================
|
||||
|
||||
private readonly SubscriptionIndex _gwLeafSubs;
|
||||
|
||||
// =========================================================================
|
||||
// NUID event ID generator
|
||||
// =========================================================================
|
||||
|
||||
// Replaced by actual NUID in session 10. Use Guid for now.
|
||||
private string NextEventId() => Guid.NewGuid().ToString("N");
|
||||
|
||||
// =========================================================================
|
||||
// Various stubs
|
||||
// =========================================================================
|
||||
|
||||
private readonly List<string> _leafRemoteCfgs = []; // stub — session 15
|
||||
private readonly List<object> _proxiesKeyPairs = []; // stub — session 09 (proxies)
|
||||
private readonly Dictionary<string, Dictionary<ulong, ClientConnection>> _proxiedConns = [];
|
||||
private long _cproto; // count of INFO-capable clients
|
||||
private readonly ConcurrentDictionary<string, object?> _nodeToInfo = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, object?> _raftNodes = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, string> _routesToSelf = [];
|
||||
private INetResolver? _routeResolver;
|
||||
private readonly ConcurrentDictionary<string, object?> _rateLimitLogging = new();
|
||||
private readonly Channel<TimeSpan> _rateLimitLoggingCh;
|
||||
private RateCounter? _connRateCounter;
|
||||
|
||||
// GW reply map expiration
|
||||
private readonly ConcurrentDictionary<string, object?> _gwrm = new();
|
||||
|
||||
// Catchup bytes
|
||||
private readonly ReaderWriterLockSlim _gcbMu = new(LockRecursionPolicy.NoRecursion);
|
||||
private long _gcbOut;
|
||||
private long _gcbOutMax;
|
||||
private readonly Channel<struct_>? _gcbKick; // stub
|
||||
|
||||
// Sync-out semaphore
|
||||
private readonly SemaphoreSlim _syncOutSem;
|
||||
private const int MaxConcurrentSyncRequests = 16;
|
||||
|
||||
// =========================================================================
|
||||
// Logging
|
||||
// =========================================================================
|
||||
|
||||
private ILogger _logger = NullLogger.Instance;
|
||||
private int _traceEnabled;
|
||||
private int _debugEnabled;
|
||||
private int _traceSysAcc;
|
||||
|
||||
// =========================================================================
|
||||
// INatsServer implementation
|
||||
// =========================================================================
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ulong NextClientId() => Interlocked.Increment(ref _gcid);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ServerOptions Options => GetOpts();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TraceEnabled => Interlocked.CompareExchange(ref _traceEnabled, 0, 0) != 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TraceSysAcc => Interlocked.CompareExchange(ref _traceSysAcc, 0, 0) != 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILogger Logger => _logger;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DecActiveAccounts() => Interlocked.Decrement(ref _activeAccounts);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void IncActiveAccounts() => Interlocked.Increment(ref _activeAccounts);
|
||||
|
||||
// =========================================================================
|
||||
// Logging helpers (mirrors Go s.Debugf / s.Noticef / s.Warnf / s.Errorf)
|
||||
// =========================================================================
|
||||
|
||||
internal void Debugf(string fmt, params object?[] args) => _logger.LogDebug(fmt, args);
|
||||
internal void Noticef(string fmt, params object?[] args) => _logger.LogInformation(fmt, args);
|
||||
internal void Warnf(string fmt, params object?[] args) => _logger.LogWarning(fmt, args);
|
||||
internal void Errorf(string fmt, params object?[] args) => _logger.LogError(fmt, args);
|
||||
internal void Fatalf(string fmt, params object?[] args) => _logger.LogCritical(fmt, args);
|
||||
|
||||
// =========================================================================
|
||||
// Constructor
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Direct constructor — do not call directly; use <see cref="NewServer(ServerOptions)"/>.
|
||||
/// </summary>
|
||||
private NatsServer(ServerOptions opts)
|
||||
{
|
||||
_opts = opts;
|
||||
_gwLeafSubs = SubscriptionIndex.NewSublistWithCache();
|
||||
_rateLimitLoggingCh = Channel.CreateBounded<TimeSpan>(1);
|
||||
_syncOutSem = new SemaphoreSlim(MaxConcurrentSyncRequests, MaxConcurrentSyncRequests);
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder struct for stub channel types
|
||||
internal readonly struct struct_ { }
|
||||
293
dotnet/src/ZB.MOM.NatsNet.Server/NatsServerTypes.cs
Normal file
293
dotnet/src/ZB.MOM.NatsNet.Server/NatsServerTypes.cs
Normal file
@@ -0,0 +1,293 @@
|
||||
// Copyright 2012-2026 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Adapted from server/server.go in the NATS server Go source.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using ZB.MOM.NatsNet.Server.Auth;
|
||||
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
// ============================================================================
|
||||
// Wire-protocol Info payload
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// The INFO payload sent to clients, routes, gateways and leaf nodes.
|
||||
/// Mirrors Go <c>Info</c> struct in server.go.
|
||||
/// </summary>
|
||||
public sealed class ServerInfo
|
||||
{
|
||||
[JsonPropertyName("server_id")] public string Id { get; set; } = string.Empty;
|
||||
[JsonPropertyName("server_name")] public string Name { get; set; } = string.Empty;
|
||||
[JsonPropertyName("version")] public string Version { get; set; } = string.Empty;
|
||||
[JsonPropertyName("proto")] public int Proto { get; set; }
|
||||
[JsonPropertyName("git_commit")] public string? GitCommit { get; set; }
|
||||
[JsonPropertyName("go")] public string GoVersion { get; set; } = string.Empty;
|
||||
[JsonPropertyName("host")] public string Host { get; set; } = string.Empty;
|
||||
[JsonPropertyName("port")] public int Port { get; set; }
|
||||
[JsonPropertyName("headers")] public bool Headers { get; set; }
|
||||
[JsonPropertyName("auth_required")] public bool AuthRequired { get; set; }
|
||||
[JsonPropertyName("tls_required")] public bool TlsRequired { get; set; }
|
||||
[JsonPropertyName("tls_verify")] public bool TlsVerify { get; set; }
|
||||
[JsonPropertyName("tls_available")] public bool TlsAvailable { get; set; }
|
||||
[JsonPropertyName("max_payload")] public int MaxPayload { get; set; }
|
||||
[JsonPropertyName("jetstream")] public bool JetStream { get; set; }
|
||||
[JsonPropertyName("ip")] public string? Ip { get; set; }
|
||||
[JsonPropertyName("client_id")] public ulong Cid { get; set; }
|
||||
[JsonPropertyName("client_ip")] public string? ClientIp { get; set; }
|
||||
[JsonPropertyName("nonce")] public string? Nonce { get; set; }
|
||||
[JsonPropertyName("cluster")] public string? Cluster { get; set; }
|
||||
[JsonPropertyName("cluster_dynamic")] public bool Dynamic { get; set; }
|
||||
[JsonPropertyName("domain")] public string? Domain { get; set; }
|
||||
[JsonPropertyName("connect_urls")] public string[]? ClientConnectUrls { get; set; }
|
||||
[JsonPropertyName("ws_connect_urls")] public string[]? WsConnectUrls { get; set; }
|
||||
[JsonPropertyName("ldm")] public bool LameDuckMode { get; set; }
|
||||
[JsonPropertyName("compression")] public string? Compression { get; set; }
|
||||
[JsonPropertyName("connect_info")] public bool ConnectInfo { get; set; }
|
||||
[JsonPropertyName("remote_account")] public string? RemoteAccount { get; set; }
|
||||
[JsonPropertyName("acc_is_sys")] public bool IsSystemAccount { get; set; }
|
||||
[JsonPropertyName("api_lvl")] public int JsApiLevel { get; set; }
|
||||
[JsonPropertyName("xkey")] public string? XKey { get; set; }
|
||||
|
||||
// Route-specific
|
||||
[JsonPropertyName("import")] public SubjectPermission? Import { get; set; }
|
||||
[JsonPropertyName("export")] public SubjectPermission? Export { get; set; }
|
||||
[JsonPropertyName("lnoc")] public bool Lnoc { get; set; }
|
||||
[JsonPropertyName("lnocu")] public bool Lnocu { get; set; }
|
||||
[JsonPropertyName("info_on_connect")] public bool InfoOnConnect { get; set; }
|
||||
[JsonPropertyName("route_pool_size")] public int RoutePoolSize { get; set; }
|
||||
[JsonPropertyName("route_pool_idx")] public int RoutePoolIdx { get; set; }
|
||||
[JsonPropertyName("route_account")] public string? RouteAccount { get; set; }
|
||||
[JsonPropertyName("route_acc_add_reqid")] public string? RouteAccReqId { get; set; }
|
||||
[JsonPropertyName("gossip_mode")] public byte GossipMode { get; set; }
|
||||
|
||||
// Gateway-specific
|
||||
[JsonPropertyName("gateway")] public string? Gateway { get; set; }
|
||||
[JsonPropertyName("gateway_urls")] public string[]? GatewayUrls { get; set; }
|
||||
[JsonPropertyName("gateway_url")] public string? GatewayUrl { get; set; }
|
||||
[JsonPropertyName("gateway_cmd")] public byte GatewayCmd { get; set; }
|
||||
[JsonPropertyName("gateway_cmd_payload")] public byte[]? GatewayCmdPayload { get; set; }
|
||||
[JsonPropertyName("gateway_nrp")] public bool GatewayNrp { get; set; }
|
||||
[JsonPropertyName("gateway_iom")] public bool GatewayIom { get; set; }
|
||||
|
||||
// LeafNode-specific
|
||||
[JsonPropertyName("leafnode_urls")] public string[]? LeafNodeUrls { get; set; }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Server stats structures
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Aggregate message/byte counters for the server.
|
||||
/// Mirrors Go embedded <c>stats</c> struct in server.go.
|
||||
/// </summary>
|
||||
internal sealed class ServerStats
|
||||
{
|
||||
public long InMsgs;
|
||||
public long OutMsgs;
|
||||
public long InBytes;
|
||||
public long OutBytes;
|
||||
public long SlowConsumers;
|
||||
public long StaleConnections;
|
||||
public long Stalls;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-kind slow-consumer counters (atomic).
|
||||
/// Mirrors Go embedded <c>scStats</c> in server.go.
|
||||
/// </summary>
|
||||
internal sealed class SlowConsumerStats
|
||||
{
|
||||
public long Clients;
|
||||
public long Routes;
|
||||
public long Leafs;
|
||||
public long Gateways;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-kind stale-connection counters (atomic).
|
||||
/// Mirrors Go embedded <c>staleStats</c> in server.go.
|
||||
/// </summary>
|
||||
internal sealed class StaleConnectionStats
|
||||
{
|
||||
public long Clients;
|
||||
public long Routes;
|
||||
public long Leafs;
|
||||
public long Gateways;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// nodeInfo — JetStream node metadata
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Per-node JetStream metadata stored in the server's node-info map.
|
||||
/// Mirrors Go <c>nodeInfo</c> struct in server.go.
|
||||
/// </summary>
|
||||
internal sealed class NodeInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Version { get; set; } = string.Empty;
|
||||
public string Cluster { get; set; } = string.Empty;
|
||||
public string Domain { get; set; } = string.Empty;
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string[] Tags { get; set; } = [];
|
||||
public object? Cfg { get; set; } // JetStreamConfig — session 19
|
||||
public object? Stats { get; set; } // JetStreamStats — session 19
|
||||
public bool Offline { get; set; }
|
||||
public bool Js { get; set; }
|
||||
public bool BinarySnapshots { get; set; }
|
||||
public bool AccountNrg { get; set; }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Server protocol version constants
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Server-to-server (route/leaf/gateway) protocol versions.
|
||||
/// Mirrors the iota block at the top of server.go.
|
||||
/// </summary>
|
||||
public static class ServerProtocol
|
||||
{
|
||||
/// <summary>Original route protocol (2009).</summary>
|
||||
public const int RouteProtoZero = 0;
|
||||
/// <summary>Route protocol that supports INFO updates.</summary>
|
||||
public const int RouteProtoInfo = 1;
|
||||
/// <summary>Route/cluster protocol with account support.</summary>
|
||||
public const int RouteProtoV2 = 2;
|
||||
/// <summary>Protocol with distributed message tracing.</summary>
|
||||
public const int MsgTraceProto = 3;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Compression mode constants
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Compression mode string constants.
|
||||
/// Mirrors the <c>const</c> block in server.go.
|
||||
/// </summary>
|
||||
public static class CompressionMode
|
||||
{
|
||||
public const string NotSupported = "not supported";
|
||||
public const string Off = "off";
|
||||
public const string Accept = "accept";
|
||||
public const string S2Auto = "s2_auto";
|
||||
public const string S2Uncompressed = "s2_uncompressed";
|
||||
public const string S2Fast = "s2_fast";
|
||||
public const string S2Better = "s2_better";
|
||||
public const string S2Best = "s2_best";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Stub types for cross-session dependencies
|
||||
// ============================================================================
|
||||
|
||||
// These stubs will be replaced with full implementations in later sessions.
|
||||
// They are declared here to allow the NatsServer class to compile.
|
||||
|
||||
/// <summary>Stub for reference-counted URL set (session 09/12).</summary>
|
||||
internal sealed class RefCountedUrlSet : Dictionary<string, int> { }
|
||||
|
||||
/// <summary>Stub for the system/internal messaging state (session 12).</summary>
|
||||
internal sealed class InternalState
|
||||
{
|
||||
public Account? Account { get; set; }
|
||||
// Full implementation in session 12 (events.go)
|
||||
}
|
||||
|
||||
/// <summary>Stub for JetStream state pointer (session 19).</summary>
|
||||
internal sealed class JetStreamState { }
|
||||
|
||||
/// <summary>Stub for JetStream config (session 19).</summary>
|
||||
public sealed class JetStreamConfig
|
||||
{
|
||||
public string StoreDir { get; set; } = string.Empty;
|
||||
public TimeSpan SyncInterval { get; set; }
|
||||
public bool SyncAlways { get; set; }
|
||||
public bool Strict { get; set; }
|
||||
public long MaxMemory { get; set; }
|
||||
public long MaxStore { get; set; }
|
||||
public string Domain { get; set; } = string.Empty;
|
||||
public bool CompressOK { get; set; }
|
||||
public string UniqueTag { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Stub for server gateway state (session 16).</summary>
|
||||
internal sealed class SrvGateway
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Stub for server websocket state (session 23).</summary>
|
||||
internal sealed class SrvWebsocket
|
||||
{
|
||||
public RefCountedUrlSet ConnectUrlsMap { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>Stub for server MQTT state (session 22).</summary>
|
||||
internal sealed class SrvMqtt { }
|
||||
|
||||
/// <summary>Stub for OCSP monitor (session 23).</summary>
|
||||
internal sealed class OcspMonitor { }
|
||||
|
||||
/// <summary>Stub for OCSP response cache (session 23).</summary>
|
||||
internal interface IOcspResponseCache { }
|
||||
|
||||
/// <summary>Stub for IP queue (session 02 — already ported as IpQueue).</summary>
|
||||
// IpQueue is already in session 02 internals — used here via object.
|
||||
|
||||
/// <summary>Stub for leaf node config (session 15).</summary>
|
||||
internal sealed class LeafNodeCfg { }
|
||||
|
||||
/// <summary>Stub for network resolver (session 09).</summary>
|
||||
internal interface INetResolver { }
|
||||
|
||||
/// <summary>Factory for rate counters.</summary>
|
||||
internal static class RateCounterFactory
|
||||
{
|
||||
public static ZB.MOM.NatsNet.Server.Internal.RateCounter Create(long rateLimit)
|
||||
=> new(rateLimit);
|
||||
}
|
||||
|
||||
/// <summary>Stub for RaftNode (session 20).</summary>
|
||||
public interface IRaftNode { }
|
||||
|
||||
/// <summary>
|
||||
/// Stub for JWT account claims (session 06/11).
|
||||
/// Mirrors Go <c>jwt.AccountClaims</c> from nats.io/jwt/v2.
|
||||
/// Full implementation will decode a signed JWT and expose limits/imports/exports.
|
||||
/// </summary>
|
||||
public sealed class AccountClaims
|
||||
{
|
||||
/// <summary>Account public NKey (subject of the JWT).</summary>
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Operator or signing-key that issued this JWT.</summary>
|
||||
public string Issuer { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal stub decoder — returns null until session 11 provides full JWT parsing.
|
||||
/// In Go: <c>jwt.DecodeAccountClaims(claimJWT)</c>.
|
||||
/// </summary>
|
||||
public static AccountClaims? TryDecode(string claimJwt)
|
||||
{
|
||||
if (string.IsNullOrEmpty(claimJwt)) return null;
|
||||
// TODO: implement proper JWT decoding in session 11.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,8 @@ public sealed partial class ServerOptions
|
||||
public string DefaultSentinel { get; set; } = string.Empty;
|
||||
public string SystemAccount { get; set; } = string.Empty;
|
||||
public bool NoSystemAccount { get; set; }
|
||||
/// <summary>Parsed account objects from config. Mirrors Go opts.Accounts.</summary>
|
||||
public List<Auth.Account> Accounts { get; set; } = [];
|
||||
public AuthCalloutOpts? AuthCallout { get; set; }
|
||||
public bool AlwaysEnableNonce { get; set; }
|
||||
public List<User>? Users { get; set; }
|
||||
|
||||
251
dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerTests.cs
Normal file
251
dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerTests.cs
Normal file
@@ -0,0 +1,251 @@
|
||||
// Copyright 2012-2026 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Adapted from server/server_test.go in the NATS server Go source.
|
||||
// Session 09: standalone unit tests for NatsServer helpers.
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Standalone unit tests for <see cref="NatsServer"/> helpers.
|
||||
/// Tests that require a running server (listener, TLS, cluster) are marked n/a
|
||||
/// and will be ported in sessions 10–23.
|
||||
/// </summary>
|
||||
public sealed class ServerTests
|
||||
{
|
||||
// =========================================================================
|
||||
// TestSemanticVersion — Test ID 2866
|
||||
// Validates that ServerConstants.Version matches semver format.
|
||||
// Mirrors Go TestSemanticVersion in server/server_test.go.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void Version_IsValidSemVer()
|
||||
{
|
||||
// SemVer regex: major.minor.patch with optional pre-release / build meta.
|
||||
var semVerRe = new Regex(@"^\d+\.\d+\.\d+(-\S+)?(\+\S+)?$", RegexOptions.Compiled);
|
||||
semVerRe.IsMatch(ServerConstants.Version).ShouldBeTrue(
|
||||
$"Version ({ServerConstants.Version}) is not a valid SemVer string");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestProcessCommandLineArgs — Test ID 2882
|
||||
// Tests the ProcessCommandLineArgs helper.
|
||||
// The Go version uses flag.FlagSet; our C# port takes string[].
|
||||
// Mirrors Go TestProcessCommandLineArgs in server/server_test.go.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ProcessCommandLineArgs_NoArgs_ReturnsFalse()
|
||||
{
|
||||
var (showVersion, showHelp, err) = NatsServer.ProcessCommandLineArgs([]);
|
||||
err.ShouldBeNull();
|
||||
showVersion.ShouldBeFalse();
|
||||
showHelp.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("version", true, false)]
|
||||
[InlineData("VERSION", true, false)]
|
||||
[InlineData("help", false, true)]
|
||||
[InlineData("HELP", false, true)]
|
||||
public void ProcessCommandLineArgs_KnownSubcommand_ReturnsCorrectFlags(
|
||||
string arg, bool wantVersion, bool wantHelp)
|
||||
{
|
||||
var (showVersion, showHelp, err) = NatsServer.ProcessCommandLineArgs([arg]);
|
||||
err.ShouldBeNull();
|
||||
showVersion.ShouldBe(wantVersion);
|
||||
showHelp.ShouldBe(wantHelp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessCommandLineArgs_UnknownSubcommand_ReturnsError()
|
||||
{
|
||||
var (_, _, err) = NatsServer.ProcessCommandLineArgs(["foo"]);
|
||||
err.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CompressionMode helpers — standalone tests for features 2976–2982
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData("off", CompressionMode.Off)]
|
||||
[InlineData("false", CompressionMode.Off)]
|
||||
[InlineData("accept", CompressionMode.Accept)]
|
||||
[InlineData("s2_fast", CompressionMode.S2Fast)]
|
||||
[InlineData("fast", CompressionMode.S2Fast)]
|
||||
[InlineData("better", CompressionMode.S2Better)]
|
||||
[InlineData("best", CompressionMode.S2Best)]
|
||||
public void ValidateAndNormalizeCompressionOption_KnownModes_NormalizesCorrectly(
|
||||
string input, string expected)
|
||||
{
|
||||
var co = new CompressionOpts { Mode = input };
|
||||
NatsServer.ValidateAndNormalizeCompressionOption(co, CompressionMode.S2Fast);
|
||||
co.Mode.ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndNormalizeCompressionOption_OnAlias_MapsToChosenMode()
|
||||
{
|
||||
var co = new CompressionOpts { Mode = "on" };
|
||||
NatsServer.ValidateAndNormalizeCompressionOption(co, CompressionMode.S2Better);
|
||||
co.Mode.ShouldBe(CompressionMode.S2Better);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndNormalizeCompressionOption_S2Auto_UsesDefaults_WhenNoThresholds()
|
||||
{
|
||||
var co = new CompressionOpts { Mode = "s2_auto" };
|
||||
NatsServer.ValidateAndNormalizeCompressionOption(co, CompressionMode.S2Fast);
|
||||
co.Mode.ShouldBe(CompressionMode.S2Auto);
|
||||
co.RttThresholds.ShouldBe(NatsServer.DefaultCompressionS2AutoRttThresholds.ToList());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndNormalizeCompressionOption_UnsupportedMode_Throws()
|
||||
{
|
||||
var co = new CompressionOpts { Mode = "bogus" };
|
||||
Should.Throw<InvalidOperationException>(
|
||||
() => NatsServer.ValidateAndNormalizeCompressionOption(co, CompressionMode.S2Fast));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(5, CompressionMode.S2Uncompressed)] // <= 10 ms threshold
|
||||
[InlineData(25, CompressionMode.S2Fast)] // 10 < rtt <= 50 ms
|
||||
[InlineData(75, CompressionMode.S2Better)] // 50 < rtt <= 100 ms
|
||||
[InlineData(150, CompressionMode.S2Best)] // > 100 ms
|
||||
public void SelectS2AutoModeBasedOnRtt_DefaultThresholds_CorrectMode(int rttMs, string expected)
|
||||
{
|
||||
var result = NatsServer.SelectS2AutoModeBasedOnRtt(
|
||||
TimeSpan.FromMilliseconds(rttMs),
|
||||
NatsServer.DefaultCompressionS2AutoRttThresholds);
|
||||
result.ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(CompressionMode.Off, CompressionMode.S2Fast, CompressionMode.Off)]
|
||||
[InlineData(CompressionMode.Accept, CompressionMode.Accept, CompressionMode.Off)]
|
||||
[InlineData(CompressionMode.S2Fast, CompressionMode.Accept, CompressionMode.S2Fast)]
|
||||
[InlineData(CompressionMode.Accept, CompressionMode.S2Fast, CompressionMode.S2Fast)]
|
||||
[InlineData(CompressionMode.Accept, CompressionMode.S2Auto, CompressionMode.S2Fast)]
|
||||
public void SelectCompressionMode_TableDriven(string local, string remote, string expected)
|
||||
{
|
||||
NatsServer.SelectCompressionMode(local, remote).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectCompressionMode_RemoteNotSupported_ReturnsNotSupported()
|
||||
{
|
||||
NatsServer.SelectCompressionMode(CompressionMode.S2Fast, CompressionMode.NotSupported)
|
||||
.ShouldBe(CompressionMode.NotSupported);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompressOptsEqual_SameMode_ReturnsTrue()
|
||||
{
|
||||
var c1 = new CompressionOpts { Mode = CompressionMode.S2Fast };
|
||||
var c2 = new CompressionOpts { Mode = CompressionMode.S2Fast };
|
||||
NatsServer.CompressOptsEqual(c1, c2).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompressOptsEqual_DifferentModes_ReturnsFalse()
|
||||
{
|
||||
var c1 = new CompressionOpts { Mode = CompressionMode.S2Fast };
|
||||
var c2 = new CompressionOpts { Mode = CompressionMode.S2Best };
|
||||
NatsServer.CompressOptsEqual(c1, c2).ShouldBeFalse();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Validation helpers
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ValidateCluster_ClusterNameWithSpaces_ReturnsError()
|
||||
{
|
||||
var opts = new ServerOptions();
|
||||
opts.Cluster.Name = "bad name";
|
||||
var err = NatsServer.ValidateCluster(opts);
|
||||
err.ShouldNotBeNull();
|
||||
err.ShouldBeSameAs(ServerErrors.ErrClusterNameHasSpaces);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePinnedCerts_ValidSha256_ReturnsNull()
|
||||
{
|
||||
var pinned = new PinnedCertSet(
|
||||
[new string('a', 64)]); // 64 hex chars
|
||||
var err = NatsServer.ValidatePinnedCerts(pinned);
|
||||
err.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePinnedCerts_InvalidSha256_ReturnsError()
|
||||
{
|
||||
var pinned = new PinnedCertSet(["not_a_sha256"]);
|
||||
var err = NatsServer.ValidatePinnedCerts(pinned);
|
||||
err.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// GetServerProto
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void GetServerProto_DefaultOpts_ReturnsMsgTraceProto()
|
||||
{
|
||||
var opts = new ServerOptions();
|
||||
// SetBaselineOptions so OverrideProto gets default 0.
|
||||
opts.SetBaselineOptions();
|
||||
var (s, err) = NatsServer.NewServer(opts);
|
||||
err.ShouldBeNull();
|
||||
s.ShouldNotBeNull();
|
||||
s!.GetServerProto().ShouldBe(ServerProtocol.MsgTraceProto);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Account helpers
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ComputeRoutePoolIdx_PoolSizeOne_AlwaysReturnsZero()
|
||||
{
|
||||
NatsServer.ComputeRoutePoolIdx(1, "any-account").ShouldBe(0);
|
||||
NatsServer.ComputeRoutePoolIdx(0, "any-account").ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeRoutePoolIdx_PoolSizeN_ReturnsIndexInRange()
|
||||
{
|
||||
const int poolSize = 5;
|
||||
var idx = NatsServer.ComputeRoutePoolIdx(poolSize, "my-account");
|
||||
idx.ShouldBeInRange(0, poolSize - 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NeedsCompression_Empty_ReturnsFalse()
|
||||
=> NatsServer.NeedsCompression(string.Empty).ShouldBeFalse();
|
||||
|
||||
[Fact]
|
||||
public void NeedsCompression_Off_ReturnsFalse()
|
||||
=> NatsServer.NeedsCompression(CompressionMode.Off).ShouldBeFalse();
|
||||
|
||||
[Fact]
|
||||
public void NeedsCompression_S2Fast_ReturnsTrue()
|
||||
=> NatsServer.NeedsCompression(CompressionMode.S2Fast).ShouldBeTrue();
|
||||
}
|
||||
Reference in New Issue
Block a user