Files
natsnet/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerOptionsTests.cs
Joseph Doherty f08fc5d6a7 feat: port session 03 — Configuration & Options types, Clone, MergeOptions, SetBaseline
- 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)
2026-02-26 11:51:01 -05:00

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);
}
}