- ServerOptionTypes.cs: all supporting types — ClusterOpts, GatewayOpts, LeafNodeOpts, WebsocketOpts, MqttOpts, RemoteLeafOpts, RemoteGatewayOpts, CompressionOpts, TlsConfigOpts, JsLimitOpts, JsTpmOpts, AuthCalloutOpts, ProxiesConfig, IAuthentication, IAccountResolver, enums (WriteTimeoutPolicy, StoreCipher, OcspMode) - ServerOptions.cs: full Options struct with ~100 properties across 10 subsystems (general, logging, networking, TLS, cluster, gateway, leafnode, websocket, MQTT, JetStream) - ServerOptions.Methods.cs: Clone (deep copy), MergeOptions, SetBaselineOptions, RoutesFromStr, NormalizeBasePath, OverrideTls, OverrideCluster, ExpandPath, HomeDir, MaybeReadPidFile, GetDefaultAuthTimeout, ConfigFlags.NoErrOnUnknownFields - 17 tests covering defaults, random port, merge, clone, expand path, auth timeout, routes parsing, normalize path, cluster override, config flags - Config file parsing (processConfigFileLine 765-line function) deferred to follow-up - All 130 tests pass (129 unit + 1 integration) - DB: features 344/3673 complete, tests 148/3257 complete (9.1% overall)
334 lines
11 KiB
C#
334 lines
11 KiB
C#
// Copyright 2012-2025 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0
|
|
|
|
using Shouldly;
|
|
using ZB.MOM.NatsNet.Server;
|
|
|
|
namespace ZB.MOM.NatsNet.Server.Tests;
|
|
|
|
/// <summary>
|
|
/// Tests for ServerOptions — mirrors tests from server/opts_test.go.
|
|
/// </summary>
|
|
public class ServerOptionsTests
|
|
{
|
|
/// <summary>
|
|
/// Mirrors TestDefaultOptions — verifies baseline defaults are applied.
|
|
/// </summary>
|
|
[Fact] // T:2512
|
|
public void DefaultOptions_ShouldSetBaselineDefaults()
|
|
{
|
|
var opts = new ServerOptions();
|
|
opts.SetBaselineOptions();
|
|
|
|
opts.Host.ShouldBe(ServerConstants.DefaultHost);
|
|
opts.Port.ShouldBe(ServerConstants.DefaultPort);
|
|
opts.MaxConn.ShouldBe(ServerConstants.DefaultMaxConnections);
|
|
opts.HttpHost.ShouldBe(ServerConstants.DefaultHost);
|
|
opts.PingInterval.ShouldBe(ServerConstants.DefaultPingInterval);
|
|
opts.MaxPingsOut.ShouldBe(ServerConstants.DefaultPingMaxOut);
|
|
opts.TlsTimeout.ShouldBe(ServerConstants.TlsTimeout.TotalSeconds);
|
|
opts.AuthTimeout.ShouldBe(ServerConstants.AuthTimeout.TotalSeconds);
|
|
opts.MaxControlLine.ShouldBe(ServerConstants.MaxControlLineSize);
|
|
opts.MaxPayload.ShouldBe(ServerConstants.MaxPayloadSize);
|
|
opts.MaxPending.ShouldBe(ServerConstants.MaxPendingSize);
|
|
opts.WriteDeadline.ShouldBe(ServerConstants.DefaultFlushDeadline);
|
|
opts.MaxClosedClients.ShouldBe(ServerConstants.DefaultMaxClosedClients);
|
|
opts.LameDuckDuration.ShouldBe(ServerConstants.DefaultLameDuckDuration);
|
|
opts.LameDuckGracePeriod.ShouldBe(ServerConstants.DefaultLameDuckGracePeriod);
|
|
opts.LeafNode.ReconnectInterval.ShouldBe(ServerConstants.DefaultLeafNodeReconnect);
|
|
opts.ConnectErrorReports.ShouldBe(ServerConstants.DefaultConnectErrorReports);
|
|
opts.ReconnectErrorReports.ShouldBe(ServerConstants.DefaultReconnectErrorReports);
|
|
opts.JetStreamMaxMemory.ShouldBe(-1);
|
|
opts.JetStreamMaxStore.ShouldBe(-1);
|
|
opts.SyncInterval.ShouldBe(TimeSpan.FromMinutes(2));
|
|
opts.JetStreamRequestQueueLimit.ShouldBe(4096);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors TestOptions_RandomPort — RANDOM_PORT should become 0 after baseline.
|
|
/// </summary>
|
|
[Fact] // T:2513
|
|
public void RandomPort_ShouldResolveToZero()
|
|
{
|
|
var opts = new ServerOptions { Port = ServerConstants.RandomPort };
|
|
opts.SetBaselineOptions();
|
|
opts.Port.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors TestMergeOverrides — flag options override file options.
|
|
/// </summary>
|
|
[Fact] // T:2516
|
|
public void MergeOverrides_ShouldLetFlagsWin()
|
|
{
|
|
var fileOpts = new ServerOptions
|
|
{
|
|
Host = "0.0.0.0",
|
|
Port = 4222,
|
|
Username = "fileuser",
|
|
Password = "filepass",
|
|
};
|
|
|
|
var flagOpts = new ServerOptions
|
|
{
|
|
Port = 9999,
|
|
Username = "flaguser",
|
|
};
|
|
|
|
var merged = ServerOptions.MergeOptions(fileOpts, flagOpts);
|
|
|
|
merged.Port.ShouldBe(9999);
|
|
merged.Username.ShouldBe("flaguser");
|
|
// Host not overridden (empty in flags)
|
|
merged.Host.ShouldBe("0.0.0.0");
|
|
// Password not overridden (empty in flags)
|
|
merged.Password.ShouldBe("filepass");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors TestOptionsClone — deep copy should produce independent objects.
|
|
/// </summary>
|
|
[Fact] // T:2537
|
|
public void Clone_ShouldDeepCopyAllFields()
|
|
{
|
|
var opts = new ServerOptions
|
|
{
|
|
ConfigFile = "./configs/test.conf",
|
|
Host = "127.0.0.1",
|
|
Port = 2222,
|
|
Username = "derek",
|
|
Password = "porkchop",
|
|
AuthTimeout = 1.0,
|
|
Debug = true,
|
|
Trace = true,
|
|
Logtime = false,
|
|
HttpPort = ServerConstants.DefaultHttpPort,
|
|
HttpBasePath = ServerConstants.DefaultHttpBasePath,
|
|
PidFile = "/tmp/nats-server/nats-server.pid",
|
|
ProfPort = 6789,
|
|
Syslog = true,
|
|
RemoteSyslog = "udp://foo.com:33",
|
|
MaxControlLine = 2048,
|
|
MaxPayload = 65536,
|
|
MaxConn = 100,
|
|
PingInterval = TimeSpan.FromSeconds(60),
|
|
MaxPingsOut = 3,
|
|
Cluster = new ClusterOpts
|
|
{
|
|
NoAdvertise = true,
|
|
ConnectRetries = 2,
|
|
WriteDeadline = TimeSpan.FromSeconds(3),
|
|
},
|
|
Gateway = new GatewayOpts
|
|
{
|
|
Name = "A",
|
|
Gateways =
|
|
[
|
|
new RemoteGatewayOpts { Name = "B", Urls = [new Uri("nats://host:5222")] },
|
|
new RemoteGatewayOpts { Name = "C" },
|
|
],
|
|
},
|
|
WriteDeadline = TimeSpan.FromSeconds(3),
|
|
Routes = [new Uri("nats://localhost:4222")],
|
|
};
|
|
|
|
var clone = opts.Clone();
|
|
|
|
// Values should match.
|
|
clone.ConfigFile.ShouldBe(opts.ConfigFile);
|
|
clone.Host.ShouldBe(opts.Host);
|
|
clone.Port.ShouldBe(opts.Port);
|
|
clone.Username.ShouldBe(opts.Username);
|
|
clone.Gateway.Name.ShouldBe("A");
|
|
clone.Gateway.Gateways.Count.ShouldBe(2);
|
|
clone.Gateway.Gateways[0].Urls[0].Authority.ShouldBe("host:5222");
|
|
|
|
// Mutating clone should not affect original.
|
|
clone.Username = "changed";
|
|
opts.Username.ShouldBe("derek");
|
|
|
|
// Mutating original gateway URLs should not affect clone.
|
|
opts.Gateway.Gateways[0].Urls[0] = new Uri("nats://other:9999");
|
|
clone.Gateway.Gateways[0].Urls[0].Authority.ShouldBe("host:5222");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors TestOptionsCloneNilLists — clone of empty lists should produce empty lists.
|
|
/// </summary>
|
|
[Fact] // T:2538
|
|
public void CloneNilLists_ShouldProduceEmptyLists()
|
|
{
|
|
var opts = new ServerOptions();
|
|
var clone = opts.Clone();
|
|
clone.Routes.ShouldNotBeNull();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors TestExpandPath — tilde and env var expansion.
|
|
/// </summary>
|
|
[Fact] // T:2563
|
|
public void ExpandPath_ShouldExpandTildeAndEnvVars()
|
|
{
|
|
// Absolute path should not change.
|
|
var result = ServerOptions.ExpandPath("/foo/bar");
|
|
result.ShouldBe("/foo/bar");
|
|
|
|
// Tilde expansion.
|
|
var home = ServerOptions.HomeDir();
|
|
var expanded = ServerOptions.ExpandPath("~/test");
|
|
expanded.ShouldBe(Path.Combine(home, "test"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors TestDefaultAuthTimeout — verifies auth timeout logic.
|
|
/// </summary>
|
|
[Fact] // T:2572
|
|
public void DefaultAuthTimeout_ShouldBe2sWithoutTls()
|
|
{
|
|
var timeout = ServerOptions.GetDefaultAuthTimeout(null, 0);
|
|
timeout.ShouldBe(ServerConstants.AuthTimeout.TotalSeconds);
|
|
}
|
|
|
|
[Fact]
|
|
public void DefaultAuthTimeout_ShouldBeTlsTimeoutPlusOneWithTls()
|
|
{
|
|
// When TLS is configured, auth timeout = tls_timeout + 1
|
|
var timeout = ServerOptions.GetDefaultAuthTimeout(new object(), 4.0);
|
|
timeout.ShouldBe(5.0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests RoutesFromStr parsing.
|
|
/// </summary>
|
|
[Fact] // T:2576 (partial)
|
|
public void RoutesFromStr_ShouldParseCommaSeparatedUrls()
|
|
{
|
|
var routes = ServerOptions.RoutesFromStr("nats://localhost:4222,nats://localhost:4223");
|
|
routes.Count.ShouldBe(2);
|
|
routes[0].Authority.ShouldBe("localhost:4222");
|
|
routes[1].Authority.ShouldBe("localhost:4223");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests NormalizeBasePath.
|
|
/// </summary>
|
|
[Fact] // T:2581
|
|
public void NormalizeBasePath_ShouldCleanPaths()
|
|
{
|
|
ServerOptions.NormalizeBasePath("").ShouldBe("/");
|
|
ServerOptions.NormalizeBasePath("/").ShouldBe("/");
|
|
ServerOptions.NormalizeBasePath("foo").ShouldBe("/foo");
|
|
ServerOptions.NormalizeBasePath("/foo/").ShouldBe("/foo");
|
|
ServerOptions.NormalizeBasePath("//foo//bar//").ShouldBe("/foo/bar");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests ConfigFlags.NoErrOnUnknownFields.
|
|
/// </summary>
|
|
[Fact] // T:2502
|
|
public void NoErrOnUnknownFields_ShouldToggleFlag()
|
|
{
|
|
ConfigFlags.NoErrOnUnknownFields(true);
|
|
ConfigFlags.AllowUnknownTopLevelField.ShouldBeTrue();
|
|
|
|
ConfigFlags.NoErrOnUnknownFields(false);
|
|
ConfigFlags.AllowUnknownTopLevelField.ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests SetBaseline with cluster port set.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SetBaseline_WithClusterPort_ShouldApplyClusterDefaults()
|
|
{
|
|
var opts = new ServerOptions
|
|
{
|
|
Cluster = new ClusterOpts { Port = 6222 },
|
|
};
|
|
opts.SetBaselineOptions();
|
|
|
|
opts.Cluster.Host.ShouldBe(ServerConstants.DefaultHost);
|
|
opts.Cluster.TlsTimeout.ShouldBe(ServerConstants.TlsTimeout.TotalSeconds);
|
|
opts.Cluster.PoolSize.ShouldBe(ServerConstants.DefaultRoutePoolSize);
|
|
opts.Cluster.Compression.Mode.ShouldBe(CompressionModes.Accept);
|
|
// System account should be auto-pinned.
|
|
opts.Cluster.PinnedAccounts.ShouldContain(ServerConstants.DefaultSystemAccount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests SetBaseline with leaf node port set.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SetBaseline_WithLeafNodePort_ShouldApplyLeafDefaults()
|
|
{
|
|
var opts = new ServerOptions
|
|
{
|
|
LeafNode = new LeafNodeOpts { Port = 7422 },
|
|
};
|
|
opts.SetBaselineOptions();
|
|
|
|
opts.LeafNode.Host.ShouldBe(ServerConstants.DefaultHost);
|
|
opts.LeafNode.Compression.Mode.ShouldBe(CompressionModes.S2Auto);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests OverrideCluster with valid URL.
|
|
/// </summary>
|
|
[Fact] // T:2518
|
|
public void OverrideCluster_ShouldParseUrlAndSetHostPort()
|
|
{
|
|
var opts = new ServerOptions
|
|
{
|
|
Cluster = new ClusterOpts { ListenStr = "nats://user:pass@127.0.0.1:6222" },
|
|
};
|
|
var err = opts.OverrideCluster();
|
|
err.ShouldBeNull();
|
|
opts.Cluster.Host.ShouldBe("127.0.0.1");
|
|
opts.Cluster.Port.ShouldBe(6222);
|
|
opts.Cluster.Username.ShouldBe("user");
|
|
opts.Cluster.Password.ShouldBe("pass");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests OverrideCluster with empty string disables clustering.
|
|
/// </summary>
|
|
[Fact]
|
|
public void OverrideCluster_EmptyString_ShouldDisableClustering()
|
|
{
|
|
var opts = new ServerOptions
|
|
{
|
|
Cluster = new ClusterOpts { Port = 6222, ListenStr = "" },
|
|
};
|
|
var err = opts.OverrideCluster();
|
|
err.ShouldBeNull();
|
|
opts.Cluster.Port.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests MaybeReadPidFile returns original string when file doesn't exist.
|
|
/// </summary>
|
|
[Fact] // T:2585
|
|
public void MaybeReadPidFile_NonExistent_ShouldReturnOriginalString()
|
|
{
|
|
ServerOptions.MaybeReadPidFile("12345").ShouldBe("12345");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that MergeOptions handles null inputs.
|
|
/// </summary>
|
|
[Fact]
|
|
public void MergeOptions_NullInputs_ShouldReturnNonNull()
|
|
{
|
|
var result = ServerOptions.MergeOptions(null, null);
|
|
result.ShouldNotBeNull();
|
|
|
|
var fileOpts = new ServerOptions { Port = 1234 };
|
|
var r1 = ServerOptions.MergeOptions(fileOpts, null);
|
|
r1.Port.ShouldBe(1234);
|
|
|
|
var flagOpts = new ServerOptions { Port = 5678 };
|
|
var r2 = ServerOptions.MergeOptions(null, flagOpts);
|
|
r2.Port.ShouldBe(5678);
|
|
}
|
|
}
|