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:
Joseph Doherty
2026-02-26 14:18:18 -05:00
parent 11b387e442
commit 0df93c23b0
10 changed files with 2683 additions and 9 deletions

View 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 1023.
/// </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 29762982
// =========================================================================
[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();
}