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
252 lines
9.7 KiB
C#
252 lines
9.7 KiB
C#
// 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();
|
||
}
|