Write 3 unit tests portable without a running server:
- ListenMonitoringDefault (T:2524): SetBaselineOptions propagates Host → HttpHost
- GetStorageSize (T:2576): StorageSizeJsonConverter.Parse K/M/G/T suffixes
- ClusterNameAndGatewayNameConflict (T:2571): ValidateOptions returns ErrClusterNameConfigConflict
Mark 74 opts_test.go stubs deferred: tests require either the NATS
conf-format parser (not yet ported), a running server (RunServer/NewServer),
or CLI flag-parsing infrastructure (ConfigureOptions).
Fix StorageSizeJsonConverter.Parse to return 0 for empty input,
matching Go getStorageSize("") == (0, nil).
Total unit tests: 638 passing.
385 lines
13 KiB
C#
385 lines
13 KiB
C#
// Copyright 2012-2025 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0
|
|
|
|
using Shouldly;
|
|
using ZB.MOM.NatsNet.Server;
|
|
using ZB.MOM.NatsNet.Server.Config;
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors TestListenMonitoringDefault — when Host is set without HTTPHost,
|
|
/// SetBaselineOptions should copy Host to HTTPHost.
|
|
/// </summary>
|
|
[Fact] // T:2524
|
|
public void ListenMonitoringDefault_ShouldSetHttpHostToHost()
|
|
{
|
|
var opts = new ServerOptions { Host = "10.0.1.22" };
|
|
opts.SetBaselineOptions();
|
|
|
|
opts.Host.ShouldBe("10.0.1.22");
|
|
opts.HttpHost.ShouldBe("10.0.1.22");
|
|
opts.Port.ShouldBe(ServerConstants.DefaultPort);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors TestGetStorageSize — StorageSizeJsonConverter.Parse converts K/M/G/T suffixes
|
|
/// and returns 0 for empty input; invalid suffixes throw.
|
|
/// </summary>
|
|
[Fact] // T:2576
|
|
public void GetStorageSize_ShouldParseSuffixes()
|
|
{
|
|
StorageSizeJsonConverter.Parse("1K").ShouldBe(1024L);
|
|
StorageSizeJsonConverter.Parse("1M").ShouldBe(1048576L);
|
|
StorageSizeJsonConverter.Parse("1G").ShouldBe(1073741824L);
|
|
StorageSizeJsonConverter.Parse("1T").ShouldBe(1099511627776L);
|
|
StorageSizeJsonConverter.Parse("").ShouldBe(0L);
|
|
|
|
Should.Throw<FormatException>(() => StorageSizeJsonConverter.Parse("1L"));
|
|
Should.Throw<FormatException>(() => StorageSizeJsonConverter.Parse("TT"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors TestClusterNameAndGatewayNameConflict — when Cluster.Name != Gateway.Name,
|
|
/// ValidateOptions should return ErrClusterNameConfigConflict.
|
|
/// </summary>
|
|
[Fact] // T:2571
|
|
public void ClusterNameAndGatewayNameConflict_ShouldReturnConflictError()
|
|
{
|
|
var opts = new ServerOptions
|
|
{
|
|
Cluster = new ClusterOpts { Name = "A", Port = -1 },
|
|
Gateway = new GatewayOpts { Name = "B", Port = -1 },
|
|
};
|
|
|
|
var err = NatsServer.ValidateOptions(opts);
|
|
err.ShouldNotBeNull();
|
|
err.ShouldBe(ServerErrors.ErrClusterNameConfigConflict);
|
|
}
|
|
}
|