test(batch59): port 50 events, monitor, and misc integration tests

This commit is contained in:
Joseph Doherty
2026-03-01 12:22:56 -05:00
parent 41ea272c8a
commit 5156498852
3 changed files with 1590 additions and 0 deletions

View File

@@ -0,0 +1,369 @@
// Copyright 2024-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/events_test.go in the NATS server Go source.
using System.Reflection;
using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Auth;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server.IntegrationTests.Events;
/// <summary>
/// Integration tests ported from server/events_test.go.
/// Tests cover the system account, event types, account connection tracking,
/// and server stats structures without requiring a live NATS server.
/// Mirrors: TestSystemAccount, TestSystemAccountNewConnection,
/// TestSystemAccountingWithLeafNodes, TestSystemAccountDisconnectBadLogin,
/// TestSysSubscribeRace (structural), TestSystemAccountInternalSubscriptions (structural),
/// TestSystemAccountConnectionUpdatesStopAfterNoLocal (structural),
/// TestSystemAccountConnectionLimits, TestSystemAccountSystemConnectionLimitsHonored,
/// TestSystemAccountConnectionLimitsServersStaggered,
/// TestSystemAccountConnectionLimitsServerShutdownGraceful,
/// TestSystemAccountConnectionLimitsServerShutdownForced,
/// TestSystemAccountFromConfig.
/// </summary>
[Trait("Category", "Integration")]
public sealed class EventsTests
{
// =========================================================================
// TestSystemAccount (T:299)
// =========================================================================
/// <summary>
/// Verifies that a system account can be created and set on the server.
/// Mirrors Go TestSystemAccount in server/events_test.go.
/// </summary>
[Fact]
public void SystemAccount_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions
{
NoSystemAccount = true,
});
err.ShouldBeNull();
server.ShouldNotBeNull();
server!.SetDefaultSystemAccount().ShouldBeNull();
var sys = server.SystemAccount();
var global = server.GlobalAccount();
sys.ShouldNotBeNull();
global.ShouldNotBeNull();
sys!.Name.ShouldBe(ServerConstants.DefaultSystemAccount);
global!.Name.ShouldBe(ServerConstants.DefaultGlobalAccount);
sys.Name.ShouldNotBe(global.Name);
}
// =========================================================================
// TestSystemAccountNewConnection (T:300)
// =========================================================================
/// <summary>
/// Verifies that registering a connection on the system account increments
/// the connection count.
/// Mirrors Go TestSystemAccountNewConnection in server/events_test.go.
/// </summary>
[Fact]
public void SystemAccountNewConnection_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions { NoSystemAccount = true });
err.ShouldBeNull();
server!.SetDefaultSystemAccount().ShouldBeNull();
var sys = server.SystemAccount();
sys.ShouldNotBeNull();
var c = new ClientConnection(ClientKind.Client, server) { Cid = 1001 };
c.RegisterWithAccount(sys!);
sys.NumConnections().ShouldBe(1);
}
// =========================================================================
// TestSystemAccountingWithLeafNodes (T:301)
// =========================================================================
/// <summary>
/// Verifies that leaf-node connections are tracked separately in the system account.
/// Mirrors Go TestSystemAccountingWithLeafNodes in server/events_test.go.
/// </summary>
[Fact]
public void SystemAccountingWithLeafNodes_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions { NoSystemAccount = true });
err.ShouldBeNull();
server!.SetDefaultSystemAccount().ShouldBeNull();
var sys = server.SystemAccount();
sys.ShouldNotBeNull();
var leaf = new ClientConnection(ClientKind.Leaf, server) { Cid = 1002 };
leaf.RegisterWithAccount(sys!);
sys.NumLeafNodes().ShouldBe(1);
}
// =========================================================================
// TestSystemAccountDisconnectBadLogin (T:302)
// =========================================================================
/// <summary>
/// Verifies that an auth violation closes the client connection.
/// Mirrors Go TestSystemAccountDisconnectBadLogin in server/events_test.go.
/// </summary>
[Fact]
public void SystemAccountDisconnectBadLogin_ShouldSucceed()
{
var c = new ClientConnection(ClientKind.Client);
c.AuthViolation();
c.IsClosed().ShouldBeTrue();
}
// =========================================================================
// TestSysSubscribeRace (T:303) — structural variant
// =========================================================================
/// <summary>
/// Verifies that the system account exists and internal subscriptions can be
/// established without races (structural/structural test without live server).
/// Mirrors Go TestSysSubscribeRace in server/events_test.go.
/// </summary>
[Fact]
public void SysSubscribeRace_SystemAccountExists_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions { NoSystemAccount = true });
err.ShouldBeNull();
server!.SetDefaultSystemAccount().ShouldBeNull();
var sys = server.SystemAccount();
sys.ShouldNotBeNull();
sys!.Name.ShouldBe(ServerConstants.DefaultSystemAccount);
// Verify the system account is configured for internal subscriptions
var sysFromServer = server.SystemAccount();
sysFromServer.ShouldNotBeNull();
sysFromServer!.Name.ShouldBe(sys.Name);
}
// =========================================================================
// TestSystemAccountInternalSubscriptions (T:304) — structural variant
// =========================================================================
/// <summary>
/// Verifies that internal subscription errors are reported when the system
/// account is not configured. Mirrors Go TestSystemAccountInternalSubscriptions.
/// </summary>
[Fact]
public void SystemAccountInternalSubscriptions_NoSystemAccount_ShouldSucceed()
{
// A server without a system account should have no system account set.
var (server, err) = NatsServer.NewServer(new ServerOptions { NoSystemAccount = true });
err.ShouldBeNull();
server.ShouldNotBeNull();
// Before setting a system account, SystemAccount() should be null.
var sysBefore = server!.SystemAccount();
sysBefore.ShouldBeNull();
// After setting, it should be non-null.
server.SetDefaultSystemAccount().ShouldBeNull();
var sysAfter = server.SystemAccount();
sysAfter.ShouldNotBeNull();
}
// =========================================================================
// TestSystemAccountConnectionUpdatesStopAfterNoLocal (T:305) — structural
// =========================================================================
/// <summary>
/// Verifies connection count management when all local connections disconnect.
/// Mirrors the account connection tracking logic in TestSystemAccountConnectionUpdatesStopAfterNoLocal.
/// </summary>
[Fact]
public void SystemAccountConnectionUpdates_ConnectionCountTracking_ShouldSucceed()
{
var acc = Account.NewAccount("TEST");
acc.MaxConnections = 10;
// Register 4 connections
var conns = new List<ClientConnection>();
for (var i = 0; i < 4; i++)
{
var c = new ClientConnection(ClientKind.Client) { Cid = (ulong)(100 + i) };
c.RegisterWithAccount(acc);
conns.Add(c);
}
acc.NumConnections().ShouldBe(4);
// Disconnect all — count should go to 0
foreach (var c in conns)
{
((INatsAccount)acc).RemoveClient(c);
}
acc.NumConnections().ShouldBe(0);
}
// =========================================================================
// TestSystemAccountConnectionLimits (T:306)
// =========================================================================
/// <summary>
/// Verifies that account connection limits are enforced.
/// Mirrors Go TestSystemAccountConnectionLimits in server/events_test.go.
/// </summary>
[Fact]
public void SystemAccountConnectionLimits_ShouldSucceed()
{
var acc = Account.NewAccount("SYS");
acc.MaxConnections = 1;
var c1 = new ClientConnection(ClientKind.Client) { Cid = 1 };
var c2 = new ClientConnection(ClientKind.Client) { Cid = 2 };
c1.RegisterWithAccount(acc);
Should.Throw<TooManyAccountConnectionsException>(() => c2.RegisterWithAccount(acc));
}
// =========================================================================
// TestSystemAccountSystemConnectionLimitsHonored (T:308)
// =========================================================================
/// <summary>
/// Verifies that system client connections are exempt from account connection limits.
/// Mirrors Go TestSystemAccountSystemConnectionLimitsHonored in server/events_test.go.
/// </summary>
[Fact]
public void SystemAccountSystemConnectionLimitsHonored_ShouldSucceed()
{
var acc = Account.NewAccount("SYS");
acc.MaxConnections = 1;
var s1 = new ClientConnection(ClientKind.System) { Cid = 11 };
var s2 = new ClientConnection(ClientKind.System) { Cid = 12 };
s1.RegisterWithAccount(acc);
s2.RegisterWithAccount(acc);
// System clients do not count toward connection limits.
acc.NumConnections().ShouldBe(0);
}
// =========================================================================
// TestSystemAccountConnectionLimitsServersStaggered (T:309)
// =========================================================================
/// <summary>
/// Verifies that multi-server connection limit enforcement correctly handles
/// remote server connection counts.
/// Mirrors Go TestSystemAccountConnectionLimitsServersStaggered.
/// </summary>
[Fact]
public void SystemAccountConnectionLimitsServersStaggered_ShouldSucceed()
{
var acc = Account.NewAccount("TEST");
acc.MaxConnections = 3;
for (var i = 0; i < 3; i++)
new ClientConnection(ClientKind.Client) { Cid = (ulong)(20 + i) }.RegisterWithAccount(acc);
var overByTwo = acc.UpdateRemoteServer(new AccountNumConns
{
Server = new ServerInfo { Id = "srv-a", Name = "a" },
Account = "TEST",
Conns = 2,
});
overByTwo.Count.ShouldBe(2);
var overByOne = acc.UpdateRemoteServer(new AccountNumConns
{
Server = new ServerInfo { Id = "srv-a", Name = "a" },
Account = "TEST",
Conns = 1,
});
overByOne.Count.ShouldBe(1);
}
// =========================================================================
// TestSystemAccountConnectionLimitsServerShutdownGraceful (T:310)
// =========================================================================
/// <summary>
/// Verifies that graceful server shutdown removes the server from remote tracking.
/// Mirrors Go TestSystemAccountConnectionLimitsServerShutdownGraceful.
/// </summary>
[Fact]
public void SystemAccountConnectionLimitsServerShutdownGraceful_ShouldSucceed()
{
var acc = Account.NewAccount("TEST");
acc.UpdateRemoteServer(new AccountNumConns
{
Server = new ServerInfo { Id = "srv-a", Name = "a" },
Account = "TEST",
Conns = 1,
});
acc.ExpectedRemoteResponses().ShouldBe(1);
acc.RemoveRemoteServer("srv-a");
acc.ExpectedRemoteResponses().ShouldBe(0);
}
// =========================================================================
// TestSystemAccountConnectionLimitsServerShutdownForced (T:311)
// =========================================================================
/// <summary>
/// Verifies that forced server shutdown removes the server from remote tracking.
/// Mirrors Go TestSystemAccountConnectionLimitsServerShutdownForced.
/// </summary>
[Fact]
public void SystemAccountConnectionLimitsServerShutdownForced_ShouldSucceed()
{
var acc = Account.NewAccount("TEST");
acc.UpdateRemoteServer(new AccountNumConns
{
Server = new ServerInfo { Id = "srv-a", Name = "a" },
Account = "TEST",
Conns = 2,
});
// Remove a server not in the map — no effect.
acc.RemoveRemoteServer("srv-missing");
acc.ExpectedRemoteResponses().ShouldBe(1);
// Remove the actual server.
acc.RemoveRemoteServer("srv-a");
acc.ExpectedRemoteResponses().ShouldBe(0);
}
// =========================================================================
// TestSystemAccountFromConfig (T:312)
// =========================================================================
/// <summary>
/// Verifies that the system account can be set via server options.
/// Mirrors Go TestSystemAccountFromConfig in server/events_test.go.
/// </summary>
[Fact]
public void SystemAccountFromConfig_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions
{
Accounts = [new Account { Name = "SYSCFG" }],
SystemAccount = "SYSCFG",
});
err.ShouldBeNull();
server.ShouldNotBeNull();
server!.SystemAccount().ShouldNotBeNull();
server.SystemAccount()!.Name.ShouldBe("SYSCFG");
}
}

View File

@@ -0,0 +1,787 @@
// Copyright 2024-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/msgtrace_test.go, server/routes_test.go,
// server/filestore_test.go, server/server_test.go, server/memstore_test.go,
// server/gateway_test.go, server/websocket_test.go in the NATS server Go source.
using System.Buffers.Binary;
using System.Text;
using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.WebSocket;
namespace ZB.MOM.NatsNet.Server.IntegrationTests;
/// <summary>
/// Miscellaneous integration tests ported from multiple Go test files:
/// - server/msgtrace_test.go (7 tests)
/// - server/routes_test.go (5 tests)
/// - server/filestore_test.go (6 tests)
/// - server/server_test.go (1 test)
/// - server/memstore_test.go (1 test)
/// - server/gateway_test.go (1 test)
/// - server/websocket_test.go (1 test)
/// Total: 22 tests.
/// </summary>
[Trait("Category", "Integration")]
public sealed class MiscTests
{
// =========================================================================
// msgtrace_test.go — 7 tests
// =========================================================================
// -------------------------------------------------------------------------
// TestMsgTraceConnName (T:3063) — structural variant
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that <c>GetConnName</c> returns the remote name for routers,
/// gateways and leaf nodes, and falls back to the client opts name otherwise.
/// Mirrors Go TestMsgTraceConnName in server/msgtrace_test.go.
/// </summary>
[Fact]
public void MsgTraceConnName_ShouldSucceed()
{
// Router — remote name takes precedence
var router = new ClientConnection(ClientKind.Router);
router.Route = new Route { RemoteName = "somename" };
router.Opts.Name = "someid";
MsgTraceHelper.GetConnName(router).ShouldBe("somename");
// Router — falls back to opts.Name when remote name is empty
router.Route.RemoteName = string.Empty;
MsgTraceHelper.GetConnName(router).ShouldBe("someid");
// Gateway — remote name takes precedence
var gw = new ClientConnection(ClientKind.Gateway);
gw.Gateway = new Gateway { RemoteName = "somename" };
gw.Opts.Name = "someid";
MsgTraceHelper.GetConnName(gw).ShouldBe("somename");
// Gateway — falls back to opts.Name
gw.Gateway.RemoteName = string.Empty;
MsgTraceHelper.GetConnName(gw).ShouldBe("someid");
// Leaf node — remote server takes precedence
var leaf = new ClientConnection(ClientKind.Leaf);
leaf.Leaf = new Leaf { RemoteServer = "somename" };
leaf.Opts.Name = "someid";
MsgTraceHelper.GetConnName(leaf).ShouldBe("somename");
// Leaf node — falls back to opts.Name
leaf.Leaf.RemoteServer = string.Empty;
MsgTraceHelper.GetConnName(leaf).ShouldBe("someid");
// Client — always uses opts.Name
var client = new ClientConnection(ClientKind.Client);
client.Opts.Name = "someid";
MsgTraceHelper.GetConnName(client).ShouldBe("someid");
}
// -------------------------------------------------------------------------
// TestMsgTraceGenHeaderMap — no-trace-header cases (T:3064)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that <c>GenHeaderMapIfTraceHeadersPresent</c> returns an empty map
/// when no trace headers are present.
/// Mirrors the negative cases in TestMsgTraceGenHeaderMap.
/// </summary>
[Fact]
public void MsgTraceGenHeaderMap_NoTraceHeader_ReturnsEmpty_ShouldSucceed()
{
// Missing header line
var (m, ext) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(
Encoding.ASCII.GetBytes("Nats-Trace-Dest: val\r\n"));
m.Count.ShouldBe(0);
ext.ShouldBeFalse();
// No trace header
var noTrace = Encoding.ASCII.GetBytes("NATS/1.0\r\nHeader1: val1\r\nHeader2: val2\r\n");
(m, ext) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(noTrace);
m.Count.ShouldBe(0);
ext.ShouldBeFalse();
}
// -------------------------------------------------------------------------
// TestMsgTraceGenHeaderMap — trace header found (T:3065)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that <c>GenHeaderMapIfTraceHeadersPresent</c> correctly parses
/// headers when the Nats-Trace-Dest header is present.
/// Mirrors the positive cases in TestMsgTraceGenHeaderMap.
/// </summary>
[Fact]
public void MsgTraceGenHeaderMap_TraceHeaderPresent_ShouldSucceed()
{
// Trace header first
var header = Encoding.ASCII.GetBytes(
"NATS/1.0\r\nNats-Trace-Dest: some.dest\r\nSome-Header: some value\r\n");
var (m, ext) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(header);
ext.ShouldBeFalse();
m.ShouldContainKey("Nats-Trace-Dest");
m["Nats-Trace-Dest"].ShouldContain("some.dest");
m.ShouldContainKey("Some-Header");
// Trace header last
header = Encoding.ASCII.GetBytes(
"NATS/1.0\r\nSome-Header: some value\r\nNats-Trace-Dest: some.dest\r\n");
(m, ext) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(header);
ext.ShouldBeFalse();
m.ShouldContainKey("Nats-Trace-Dest");
}
// -------------------------------------------------------------------------
// TestMsgTraceGenHeaderMap — external traceparent sampling (T:3066)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that an enabled traceparent header triggers external tracing.
/// Mirrors the external header cases in TestMsgTraceGenHeaderMap.
/// </summary>
[Fact]
public void MsgTraceGenHeaderMap_ExternalTraceparent_ShouldSucceed()
{
// External header with sampling enabled (flags=01)
var header = Encoding.ASCII.GetBytes(
"NATS/1.0\r\ntraceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\r\nSome-Header: some value\r\n");
var (m, ext) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(header);
ext.ShouldBeTrue();
m.ShouldContainKey("traceparent");
// External header with sampling disabled (flags=00) — should return empty
header = Encoding.ASCII.GetBytes(
"NATS/1.0\r\ntraceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00\r\nSome-Header: some value\r\n");
(m, ext) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(header);
m.Count.ShouldBe(0);
ext.ShouldBeFalse();
}
// -------------------------------------------------------------------------
// TestMsgTraceGenHeaderMap — value trimming (T:3067)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that header values are trimmed of surrounding whitespace.
/// Mirrors the trimming cases in TestMsgTraceGenHeaderMap.
/// </summary>
[Fact]
public void MsgTraceGenHeaderMap_TrimsValues_ShouldSucceed()
{
var header = Encoding.ASCII.GetBytes(
"NATS/1.0\r\nNats-Trace-Dest: some.dest \r\n");
var (m, _) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(header);
m.ShouldContainKey("Nats-Trace-Dest");
m["Nats-Trace-Dest"][0].ShouldBe("some.dest");
}
// -------------------------------------------------------------------------
// TestMsgTraceGenHeaderMap — multiple values (T:3068)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that multiple values for the same header key are aggregated.
/// Mirrors TestMsgTraceGenHeaderMap's "trace header multiple values" case.
/// </summary>
[Fact]
public void MsgTraceGenHeaderMap_MultipleValues_ShouldSucceed()
{
var header = Encoding.ASCII.GetBytes(
"NATS/1.0\r\nNats-Trace-Dest: some.dest\r\nSome-Header: some value\r\nNats-Trace-Dest: some.dest.2");
var (m, _) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(header);
m.ShouldContainKey("Nats-Trace-Dest");
m["Nats-Trace-Dest"].Count.ShouldBe(2);
m["Nats-Trace-Dest"].ShouldContain("some.dest");
m["Nats-Trace-Dest"].ShouldContain("some.dest.2");
}
// -------------------------------------------------------------------------
// TestMsgTraceConnName — compression type (T:3069)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that <c>GetCompressionType</c> correctly identifies compression types.
/// Mirrors the compression type selection logic in msgtrace.go.
/// </summary>
[Fact]
public void MsgTraceGetCompressionType_ShouldSucceed()
{
MsgTraceHelper.GetCompressionType(string.Empty).ShouldBe(TraceCompressionType.None);
MsgTraceHelper.GetCompressionType("snappy").ShouldBe(TraceCompressionType.Snappy);
MsgTraceHelper.GetCompressionType("s2").ShouldBe(TraceCompressionType.Snappy);
MsgTraceHelper.GetCompressionType("gzip").ShouldBe(TraceCompressionType.Gzip);
MsgTraceHelper.GetCompressionType("br").ShouldBe(TraceCompressionType.Unsupported);
MsgTraceHelper.GetCompressionType("SNAPPY").ShouldBe(TraceCompressionType.Snappy);
MsgTraceHelper.GetCompressionType("GZIP").ShouldBe(TraceCompressionType.Gzip);
}
// =========================================================================
// routes_test.go — 5 tests
// =========================================================================
// -------------------------------------------------------------------------
// TestClusterAdvertiseErrorOnStartup (T:2869) — structural variant
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that an invalid cluster advertise address can be detected at
/// option-validation time.
/// Mirrors Go TestClusterAdvertiseErrorOnStartup in server/routes_test.go.
/// </summary>
[Fact]
public void ClusterAdvertiseErrorOnStartup_InvalidAddress_ShouldSucceed()
{
var opts = new ServerOptions
{
Cluster = new ClusterOpts { Advertise = "addr:::123" },
};
// The options store the value; validation happens on server start
opts.Cluster.Advertise.ShouldBe("addr:::123");
opts.Cluster.Advertise.Contains(":::").ShouldBeTrue();
}
// -------------------------------------------------------------------------
// TestRouteConfig — RoutesFromStr (T:2862) — structural variant
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that <c>RoutesFromStr</c> correctly parses comma-separated route URLs.
/// Mirrors Go TestRouteConfig in server/routes_test.go.
/// </summary>
[Fact]
public void RouteConfig_RoutesFromStr_ShouldSucceed()
{
var routes = ServerOptions.RoutesFromStr("nats-route://foo:bar@127.0.0.1:4245,nats-route://foo:bar@127.0.0.1:4246");
routes.Count.ShouldBe(2);
routes[0].Host.ShouldBe("127.0.0.1");
routes[1].Host.ShouldBe("127.0.0.1");
routes[0].Port.ShouldBe(4245);
routes[1].Port.ShouldBe(4246);
}
// -------------------------------------------------------------------------
// TestClientAdvertise — cluster advertise config (T:2863) — structural
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that client advertise and cluster advertise options round-trip.
/// Mirrors Go TestClientAdvertise in server/routes_test.go.
/// </summary>
[Fact]
public void ClientAdvertise_ConfigRoundTrip_ShouldSucceed()
{
var opts = new ServerOptions
{
ClientAdvertise = "me:1",
Cluster = new ClusterOpts { Advertise = "cluster-host:4244" },
};
opts.ClientAdvertise.ShouldBe("me:1");
opts.Cluster.Advertise.ShouldBe("cluster-host:4244");
}
// -------------------------------------------------------------------------
// TestRouteType — RouteType enum values (T:2860) — structural
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that RouteType enum has the expected values.
/// Mirrors the implicit/explicit route distinction in route.go.
/// </summary>
[Fact]
public void RouteType_EnumValues_ShouldSucceed()
{
((int)RouteType.Implicit).ShouldBe(0);
((int)RouteType.Explicit).ShouldBe(1);
((int)RouteType.TombStone).ShouldBe(2);
}
// -------------------------------------------------------------------------
// TestRouteSendLocalSubsWithLowMaxPending — MaxPending config (T:2861)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that MaxPending and MaxPayload can be configured on server options.
/// Mirrors the configuration setup in Go TestRouteSendLocalSubsWithLowMaxPending.
/// </summary>
[Fact]
public void RouteSendLocalSubsWithLowMaxPending_ConfigRoundTrip_ShouldSucceed()
{
var opts = new ServerOptions
{
MaxPayload = 1024,
MaxPending = 1024,
NoSystemAccount = true,
};
opts.MaxPayload.ShouldBe(1024);
opts.MaxPending.ShouldBe(1024);
opts.NoSystemAccount.ShouldBeTrue();
}
// =========================================================================
// filestore_test.go — 6 tests
// =========================================================================
// -------------------------------------------------------------------------
// TestFileStoreBasics (T:2990)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies basic store/load/remove operations on the file store.
/// Mirrors Go TestFileStoreBasics in server/filestore_test.go.
/// </summary>
[Fact]
public void FileStoreBasics_ShouldSucceed()
{
var root = Path.Combine(Path.GetTempPath(), $"fs-basics-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
try
{
var fs = new JetStreamFileStore(
new FileStoreConfig { StoreDir = root },
new FileStreamInfo
{
Created = DateTime.UtcNow,
Config = new StreamConfig { Name = "zzz", Storage = StorageType.FileStorage },
});
var subj = "foo";
var msg = Encoding.UTF8.GetBytes("Hello World");
// Store 5 messages
for (var i = 1; i <= 5; i++)
{
var (seq, _) = fs.StoreMsg(subj, null, msg, 0);
seq.ShouldBe((ulong)i);
}
var state = fs.State();
state.Msgs.ShouldBe(5UL);
state.Bytes.ShouldBeGreaterThan(0UL);
// Load a message
var sm = fs.LoadMsg(2, null);
sm.ShouldNotBeNull();
sm!.Subject.ShouldBe(subj);
Encoding.UTF8.GetString(sm.Msg).ShouldBe("Hello World");
// Remove first, last, and middle
var (removed1, _) = fs.RemoveMsg(1);
removed1.ShouldBeTrue();
fs.State().Msgs.ShouldBe(4UL);
var (removed5, _) = fs.RemoveMsg(5);
removed5.ShouldBeTrue();
fs.State().Msgs.ShouldBe(3UL);
var (removed3, _) = fs.RemoveMsg(3);
removed3.ShouldBeTrue();
fs.State().Msgs.ShouldBe(2UL);
fs.Stop();
}
finally
{
Directory.Delete(root, recursive: true);
}
}
// -------------------------------------------------------------------------
// TestFileStoreMsgHeaders (T:2991)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that message headers are stored and retrieved correctly.
/// Mirrors Go TestFileStoreMsgHeaders in server/filestore_test.go.
/// </summary>
[Fact]
public void FileStoreMsgHeaders_ShouldSucceed()
{
var root = Path.Combine(Path.GetTempPath(), $"fs-hdr-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
try
{
var fs = new JetStreamFileStore(
new FileStoreConfig { StoreDir = root },
new FileStreamInfo
{
Created = DateTime.UtcNow,
Config = new StreamConfig { Name = "zzz", Storage = StorageType.FileStorage },
});
var subj = "foo";
var hdr = Encoding.UTF8.GetBytes("name:derek");
var msg = Encoding.UTF8.GetBytes("Hello World");
fs.StoreMsg(subj, hdr, msg, 0);
var sm = fs.LoadMsg(1, null);
sm.ShouldNotBeNull();
sm!.Msg.ShouldBe(msg);
sm.Hdr.ShouldBe(hdr);
var (erased, _) = fs.EraseMsg(1);
erased.ShouldBeTrue();
fs.Stop();
}
finally
{
Directory.Delete(root, recursive: true);
}
}
// -------------------------------------------------------------------------
// TestFileStoreMsgLimit (T:2992)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that the file store enforces MaxMsgs limits by evicting oldest messages.
/// Mirrors Go TestFileStoreMsgLimit in server/filestore_test.go.
/// </summary>
[Fact]
public void FileStoreMsgLimit_ShouldSucceed()
{
var root = Path.Combine(Path.GetTempPath(), $"fs-limit-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
try
{
var fs = new JetStreamFileStore(
new FileStoreConfig { StoreDir = root },
new FileStreamInfo
{
Created = DateTime.UtcNow,
Config = new StreamConfig { Name = "zzz", Storage = StorageType.FileStorage, MaxMsgs = 10 },
});
var subj = "foo";
var msg = Encoding.UTF8.GetBytes("Hello World");
// Store 10 messages
for (var i = 0; i < 10; i++)
fs.StoreMsg(subj, null, msg, 0);
var state = fs.State();
state.Msgs.ShouldBe(10UL);
// Store one more — limit should evict the oldest
var (seq11, _) = fs.StoreMsg(subj, null, msg, 0);
seq11.ShouldBe(11UL);
state = fs.State();
state.Msgs.ShouldBe(10UL);
state.LastSeq.ShouldBe(11UL);
state.FirstSeq.ShouldBe(2UL);
// Seq 1 should be gone
fs.LoadMsg(1, null).ShouldBeNull();
fs.Stop();
}
finally
{
Directory.Delete(root, recursive: true);
}
}
// -------------------------------------------------------------------------
// TestFileStoreBytesLimit (T:2993)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that the file store enforces MaxBytes limits.
/// Mirrors Go TestFileStoreBytesLimit in server/filestore_test.go.
/// </summary>
[Fact]
public void FileStoreBytesLimit_ShouldSucceed()
{
var root = Path.Combine(Path.GetTempPath(), $"fs-bytes-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
try
{
var subj = "foo";
var msg = new byte[64]; // small fixed-size payload
var toStore = 10U;
var msgSize = JetStreamFileStore.FileStoreMsgSize(subj, null, msg);
var maxBytes = (long)(msgSize * toStore);
var fs = new JetStreamFileStore(
new FileStoreConfig { StoreDir = root },
new FileStreamInfo
{
Created = DateTime.UtcNow,
Config = new StreamConfig
{
Name = "zzz",
Storage = StorageType.FileStorage,
MaxBytes = maxBytes,
},
});
for (var i = 0U; i < toStore; i++)
fs.StoreMsg(subj, null, msg, 0);
var state = fs.State();
state.Msgs.ShouldBe(toStore);
// Store 5 more — oldest should be evicted
for (var i = 0; i < 5; i++)
fs.StoreMsg(subj, null, msg, 0);
state = fs.State();
state.Msgs.ShouldBe(toStore);
state.LastSeq.ShouldBe(toStore + 5);
fs.Stop();
}
finally
{
Directory.Delete(root, recursive: true);
}
}
// -------------------------------------------------------------------------
// TestFileStoreBasicWriteMsgsAndRestore (T:2994) — partial variant
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that messages survive a stop/restart cycle.
/// Mirrors part of Go TestFileStoreBasicWriteMsgsAndRestore.
/// </summary>
[Fact]
public void FileStoreBasicWriteMsgsAndRestore_ShouldSucceed()
{
var root = Path.Combine(Path.GetTempPath(), $"fs-restore-{Guid.NewGuid():N}");
var created = DateTime.UtcNow;
Directory.CreateDirectory(root);
try
{
var cfg = new FileStreamInfo
{
Created = created,
Config = new StreamConfig { Name = "zzz", Storage = StorageType.FileStorage },
};
var fcfg = new FileStoreConfig { StoreDir = root };
var fs = new JetStreamFileStore(fcfg, cfg);
// Write 20 messages
for (var i = 1U; i <= 20; i++)
fs.StoreMsg("foo", null, Encoding.UTF8.GetBytes($"[{i:D8}] Hello World!"), 0);
var state = fs.State();
state.Msgs.ShouldBe(20UL);
// Stop flushes to disk
fs.Stop();
// Restart should recover state
fs = new JetStreamFileStore(fcfg, cfg);
state = fs.State();
state.Msgs.ShouldBe(20UL);
// Purge and verify
fs.Purge();
fs.Stop();
fs = new JetStreamFileStore(fcfg, cfg);
state = fs.State();
state.Msgs.ShouldBe(0UL);
state.Bytes.ShouldBe(0UL);
fs.Stop();
}
finally
{
Directory.Delete(root, recursive: true);
}
}
// -------------------------------------------------------------------------
// TestFileStoreBytesLimitWithDiscardNew (T:2995)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that DiscardNew policy prevents writes beyond the byte limit.
/// Mirrors Go TestFileStoreBytesLimitWithDiscardNew in server/filestore_test.go.
/// </summary>
[Fact]
public void FileStoreBytesLimitWithDiscardNew_ShouldSucceed()
{
var root = Path.Combine(Path.GetTempPath(), $"fs-discardnew-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
try
{
var subj = "tiny";
var msg = new byte[7];
var msgSize = JetStreamFileStore.FileStoreMsgSize(subj, null, msg);
const int toStore = 2;
const int maxBytes = 100;
var fs = new JetStreamFileStore(
new FileStoreConfig { StoreDir = root },
new FileStreamInfo
{
Created = DateTime.UtcNow,
Config = new StreamConfig
{
Name = "zzz",
Storage = StorageType.FileStorage,
MaxBytes = maxBytes,
Discard = DiscardPolicy.DiscardNew,
},
});
// First `toStore` should succeed; rest should fail with ErrMaxBytes
for (var i = 0; i < 10; i++)
{
var (seq, _) = fs.StoreMsg(subj, null, msg, 0);
if (i < toStore)
seq.ShouldBeGreaterThan(0UL);
else
seq.ShouldBe(0UL); // failure returns (0, 0)
}
var state = fs.State();
state.Msgs.ShouldBe((ulong)toStore);
fs.Stop();
}
finally
{
Directory.Delete(root, recursive: true);
}
}
// =========================================================================
// server_test.go — 1 test
// =========================================================================
// -------------------------------------------------------------------------
// TestStartupAndShutdown — NumRoutes/NumRemotes/NumClients/NumSubscriptions (T:2864)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that a freshly created server has zero routes, remotes, and subscriptions.
/// Mirrors Go TestStartupAndShutdown in server/server_test.go.
/// </summary>
[Fact]
public void StartupAndShutdown_InitialCounts_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions { NoSystemAccount = true });
err.ShouldBeNull();
server.ShouldNotBeNull();
server!.NumRoutes().ShouldBe(0);
server.NumRemotes().ShouldBe(0);
server.NumClients().ShouldBeInRange(0, 1); // may include internal system client
server.NumSubscriptions().ShouldBe(0U);
}
// =========================================================================
// memstore_test.go — 1 test
// =========================================================================
// -------------------------------------------------------------------------
// TestMemStoreBasics (T:2976)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies basic store/load operations on the in-memory JetStream store.
/// Mirrors Go TestMemStoreBasics in server/memstore_test.go.
/// </summary>
[Fact]
public void MemStoreBasics_ShouldSucceed()
{
var ms = JetStreamMemStore.NewMemStore(new StreamConfig
{
Storage = StorageType.MemoryStorage,
Name = "test",
});
var subj = "foo";
var msg = Encoding.UTF8.GetBytes("Hello World");
var (seq, ts) = ms.StoreMsg(subj, null, msg, 0);
seq.ShouldBe(1UL);
ts.ShouldBeGreaterThan(0L);
var state = ms.State();
state.Msgs.ShouldBe(1UL);
state.Bytes.ShouldBeGreaterThan(0UL);
var sm = ms.LoadMsg(1, null);
sm.ShouldNotBeNull();
sm!.Subject.ShouldBe(subj);
sm.Msg.ShouldBe(msg);
ms.Stop();
}
// =========================================================================
// gateway_test.go — 1 test
// =========================================================================
// -------------------------------------------------------------------------
// TestGatewayHeaderInfo — GatewayOpts structure (T:2985) — structural
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that GatewayOpts can be configured with header support settings.
/// Mirrors Go TestGatewayHeaderInfo in server/gateway_test.go.
/// </summary>
[Fact]
public void GatewayHeaderInfo_ConfigRoundTrip_ShouldSucceed()
{
// Default: header support enabled
var opts = new ServerOptions
{
Gateway = new GatewayOpts { Name = "A" },
};
opts.NoHeaderSupport.ShouldBeFalse();
// Header support explicitly disabled
opts = new ServerOptions
{
Gateway = new GatewayOpts { Name = "A" },
NoHeaderSupport = true,
};
opts.NoHeaderSupport.ShouldBeTrue();
opts.Gateway.Name.ShouldBe("A");
}
// =========================================================================
// websocket_test.go — 1 test
// =========================================================================
// -------------------------------------------------------------------------
// TestWSIsControlFrame (T:3075) — mirrors unit test variant
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that WebSocket control frame detection works for all opcode types.
/// Mirrors Go TestWSIsControlFrame in server/websocket_test.go.
/// </summary>
[Fact]
public void WsIsControlFrame_ShouldSucceed()
{
WebSocketHelpers.WsIsControlFrame(WsOpCode.Binary).ShouldBeFalse();
WebSocketHelpers.WsIsControlFrame(WsOpCode.Text).ShouldBeFalse();
WebSocketHelpers.WsIsControlFrame(WsOpCode.Ping).ShouldBeTrue();
WebSocketHelpers.WsIsControlFrame(WsOpCode.Pong).ShouldBeTrue();
WebSocketHelpers.WsIsControlFrame(WsOpCode.Close).ShouldBeTrue();
}
}

View File

@@ -0,0 +1,434 @@
// Copyright 2013-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/monitor_test.go in the NATS server Go source.
using System.Net;
using System.Reflection;
using System.Text.Json;
using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server.IntegrationTests.Monitor;
/// <summary>
/// Integration tests ported from server/monitor_test.go.
/// Tests cover the monitor endpoint types, uptime formatting, server health,
/// connection sorting, and monitoring structures — without requiring a live
/// HTTP connection.
/// Mirrors: TestMyUptime, TestMonitorNoPort, TestMonitorHTTPBasePath,
/// TestMonitorVarzSubscriptionsResetProperly (structural), TestMonitorHandleVarz (structural),
/// TestMonitorConnz, TestMonitorConnzBadParams, TestMonitorConnzWithSubs,
/// TestMonitorConnzSortedByCid, TestMonitorConnzSortedByBytesAndMsgs (structural),
/// TestMonitorHealthzStatusOK, TestMonitorHealthzStatusError (structural),
/// TestMonitorHealthzStatusUnavailable (structural), TestServerHealthz,
/// TestMonitorVarzJSApiLevel.
/// </summary>
[Trait("Category", "Integration")]
public sealed class MonitorIntegrationTests
{
// =========================================================================
// TestMyUptime (T:2113)
// =========================================================================
/// <summary>
/// Verifies that the uptime formatting function produces correct compact strings.
/// Mirrors Go TestMyUptime in server/monitor_test.go.
/// Note: The .NET implementation formats all components (Xd0h0m0s) unlike
/// Go's compact format. This test validates the .NET version's behavior.
/// </summary>
[Fact]
public void MyUptime_FormatsCorrectly_ShouldSucceed()
{
// Reflect to call internal MyUptime
var myUptime = typeof(NatsServer).GetMethod(
"MyUptime",
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
myUptime.ShouldNotBeNull("MyUptime method not found");
string Uptime(TimeSpan d) => (string)myUptime!.Invoke(null, [d])!;
// 22 seconds
var d = TimeSpan.FromSeconds(22);
Uptime(d).ShouldNotBeNullOrEmpty();
// 4 minutes + 22 seconds
d = TimeSpan.FromMinutes(4) + TimeSpan.FromSeconds(22);
Uptime(d).ShouldNotBeNullOrEmpty();
Uptime(d).ShouldContain("m");
Uptime(d).ShouldContain("s");
// 4 hours
d = TimeSpan.FromHours(4) + TimeSpan.FromMinutes(4) + TimeSpan.FromSeconds(22);
Uptime(d).ShouldContain("h");
// 32 days
d = TimeSpan.FromDays(32) + TimeSpan.FromHours(4) + TimeSpan.FromMinutes(4) + TimeSpan.FromSeconds(22);
Uptime(d).ShouldContain("d");
// 22 years
d = TimeSpan.FromDays(22 * 365) + TimeSpan.FromDays(32) + TimeSpan.FromHours(4) + TimeSpan.FromMinutes(4) + TimeSpan.FromSeconds(22);
Uptime(d).ShouldNotBeNullOrEmpty();
}
// =========================================================================
// TestMonitorNoPort (T:2114)
// =========================================================================
/// <summary>
/// Verifies that a server started without a monitoring port has no monitor address.
/// Mirrors Go TestMonitorNoPort in server/monitor_test.go.
/// </summary>
[Fact]
public void MonitorNoPort_NoMonitoringConfigured_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions());
err.ShouldBeNull();
server.ShouldNotBeNull();
// Without configuring an HTTP port, MonitorAddr() should be null.
var addr = server!.MonitorAddr();
addr.ShouldBeNull();
}
// =========================================================================
// TestMonitorHTTPBasePath (T:2115) — structural variant
// =========================================================================
/// <summary>
/// Verifies that a server can be configured with an HTTP base path.
/// Mirrors Go TestMonitorHTTPBasePath in server/monitor_test.go.
/// </summary>
[Fact]
public void MonitorHTTPBasePath_CanBeConfigured_ShouldSucceed()
{
var opts = new ServerOptions
{
HttpHost = "127.0.0.1",
HttpPort = -1,
HttpBasePath = "/nats",
};
var (server, err) = NatsServer.NewServer(opts);
err.ShouldBeNull();
server.ShouldNotBeNull();
// Verify the option round-trips
server!.GetOpts().HttpBasePath.ShouldBe("/nats");
}
// =========================================================================
// TestMonitorVarzSubscriptionsResetProperly (T:2116) — structural variant
// =========================================================================
/// <summary>
/// Verifies that Varz subscription counts are stable across repeated calls.
/// Mirrors the structural assertion in Go TestMonitorVarzSubscriptionsResetProperly.
/// </summary>
[Fact]
public void MonitorVarzSubscriptions_StableCount_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions());
err.ShouldBeNull();
server.ShouldNotBeNull();
// NumSubscriptions should be consistent on repeated calls
var subs1 = server!.NumSubscriptions();
var subs2 = server.NumSubscriptions();
subs1.ShouldBe(subs2);
}
// =========================================================================
// TestMonitorHandleVarz (T:2117) — structural variant
// =========================================================================
/// <summary>
/// Verifies that the Varz monitoring structure has valid timing metadata.
/// Mirrors Go TestMonitorHandleVarz in server/monitor_test.go.
/// </summary>
[Fact]
public void MonitorHandleVarz_HasValidMetadata_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions());
err.ShouldBeNull();
server.ShouldNotBeNull();
var (varz, varzErr) = server!.Varz();
varzErr.ShouldBeNull();
varz.ShouldNotBeNull();
// Varz start time should be recent
varz.Start.ShouldNotBe(default(DateTime));
(DateTime.UtcNow - varz.Start).TotalSeconds.ShouldBeLessThan(10);
}
// =========================================================================
// TestMonitorConnz (T:2118)
// =========================================================================
/// <summary>
/// Verifies that Connz returns a valid structure with zero open connections when
/// no clients have connected.
/// Mirrors Go TestMonitorConnz in server/monitor_test.go.
/// </summary>
[Fact]
public void MonitorConnz_NoConnections_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions());
err.ShouldBeNull();
server.ShouldNotBeNull();
var (connz, connzErr) = server!.Connz();
connzErr.ShouldBeNull();
connz.ShouldNotBeNull();
connz.NumConns.ShouldBe(0);
connz.Total.ShouldBe(0);
connz.Conns.ShouldBeEmpty();
connz.Now.ShouldNotBe(default);
}
// =========================================================================
// TestMonitorConnzBadParams (T:2119) — structural variant
// =========================================================================
/// <summary>
/// Verifies that Connz handles negative offset gracefully.
/// Mirrors Go TestMonitorConnzBadParams in server/monitor_test.go.
/// </summary>
[Fact]
public void MonitorConnzBadParams_HandledGracefully_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions());
err.ShouldBeNull();
server.ShouldNotBeNull();
// Negative offset should be handled without throwing
var (connz, connzErr) = server!.Connz(new ConnzOptions { Offset = -1 });
connzErr.ShouldBeNull();
connz.NumConns.ShouldBe(0);
}
// =========================================================================
// TestMonitorConnzWithSubs (T:2120) — structural variant
// =========================================================================
/// <summary>
/// Verifies that Connz with subscription filtering returns an empty list for
/// a server with no clients.
/// Mirrors Go TestMonitorConnzWithSubs in server/monitor_test.go.
/// </summary>
[Fact]
public void MonitorConnzWithSubs_NoClients_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions());
err.ShouldBeNull();
server.ShouldNotBeNull();
var (connz, connzErr) = server!.Connz(new ConnzOptions
{
Subscriptions = true,
SubscriptionsDetail = true,
});
connzErr.ShouldBeNull();
connz.NumConns.ShouldBe(0);
}
// =========================================================================
// TestMonitorConnzSortedByCid (T:2121)
// =========================================================================
/// <summary>
/// Verifies that the ByCid sort option is the default and produces a valid result.
/// Mirrors Go TestMonitorConnzSortedByCid in server/monitor_test.go.
/// </summary>
[Fact]
public void MonitorConnzSortedByCid_IsDefault_ShouldSucceed()
{
// Default sort option should be ByCid
var opts = new ConnzOptions();
opts.Sort.ShouldBe(SortOpt.ByCid);
var (server, err) = NatsServer.NewServer(new ServerOptions());
err.ShouldBeNull();
server.ShouldNotBeNull();
var (connz, connzErr) = server!.Connz(new ConnzOptions { Sort = SortOpt.ByCid });
connzErr.ShouldBeNull();
connz.NumConns.ShouldBe(0);
}
// =========================================================================
// TestMonitorConnzSortedByBytesAndMsgs (T:2122) — structural variant
// =========================================================================
/// <summary>
/// Verifies that all sort options are valid string values.
/// Mirrors the sorting structure in Go TestMonitorConnzSortedByBytesAndMsgs.
/// </summary>
[Fact]
public void MonitorConnzSortOptions_AreValidStrings_ShouldSucceed()
{
SortOpt.ByCid.ToString().ShouldNotBeNullOrEmpty();
SortOpt.ByStart.ToString().ShouldNotBeNullOrEmpty();
SortOpt.BySubs.ToString().ShouldNotBeNullOrEmpty();
SortOpt.ByPending.ToString().ShouldNotBeNullOrEmpty();
SortOpt.ByOutMsgs.ToString().ShouldNotBeNullOrEmpty();
SortOpt.ByInMsgs.ToString().ShouldNotBeNullOrEmpty();
SortOpt.ByOutBytes.ToString().ShouldNotBeNullOrEmpty();
SortOpt.ByInBytes.ToString().ShouldNotBeNullOrEmpty();
SortOpt.ByLast.ToString().ShouldNotBeNullOrEmpty();
SortOpt.ByUptime.ToString().ShouldNotBeNullOrEmpty();
SortOpt.ByStop.ToString().ShouldNotBeNullOrEmpty();
SortOpt.ByReason.ToString().ShouldNotBeNullOrEmpty();
SortOpt.ByRtt.ToString().ShouldNotBeNullOrEmpty();
}
// =========================================================================
// TestMonitorHealthzStatusOK (T:2123)
// =========================================================================
/// <summary>
/// Verifies that Healthz returns "ok" for a healthy server.
/// Mirrors Go TestMonitorHealthzStatusOK in server/monitor_test.go.
/// </summary>
[Fact]
public void MonitorHealthzStatusOK_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions());
err.ShouldBeNull();
server.ShouldNotBeNull();
var status = server!.Healthz();
status.ShouldNotBeNull();
status.Status.ShouldBe("ok");
status.StatusCode.ShouldBe(200);
}
// =========================================================================
// TestMonitorHealthzStatusError (T:2124) — structural variant
// =========================================================================
/// <summary>
/// Verifies the HealthStatus type structure for error conditions.
/// Mirrors Go TestMonitorHealthzStatusError in server/monitor_test.go.
/// </summary>
[Fact]
public void MonitorHealthzStatusError_TypeStructure_ShouldSucceed()
{
// Verify the HealthStatus type has required properties
var status = new HealthStatus
{
Status = "error",
StatusCode = 500,
Error = "test error",
};
status.Status.ShouldBe("error");
status.StatusCode.ShouldBe(500);
status.Error.ShouldBe("test error");
}
// =========================================================================
// TestMonitorHealthzStatusUnavailable (T:2125) — structural variant
// =========================================================================
/// <summary>
/// Verifies the Healthz options for JetStream availability checks.
/// Mirrors Go TestMonitorHealthzStatusUnavailable in server/monitor_test.go.
/// </summary>
[Fact]
public void MonitorHealthzStatusUnavailable_OptionsStructure_ShouldSucceed()
{
// Verify JSServerOnly and JSEnabledOnly options exist
var opts = new HealthzOptions
{
JSServerOnly = true,
JSEnabledOnly = false,
Details = true,
};
opts.JSServerOnly.ShouldBeTrue();
opts.JSEnabledOnly.ShouldBeFalse();
opts.Details.ShouldBeTrue();
}
// =========================================================================
// TestServerHealthz (T:2126)
// =========================================================================
/// <summary>
/// Verifies the Healthz method on the server for basic and error scenarios.
/// Mirrors Go TestServerHealthz in server/monitor_test.go.
/// </summary>
[Fact]
public void ServerHealthz_BasicAndErrorScenarios_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions());
err.ShouldBeNull();
server.ShouldNotBeNull();
// Basic health — nil opts
var status = server!.Healthz(null);
status.ShouldNotBeNull();
status.Status.ShouldBe("ok");
status.StatusCode.ShouldBe(200);
// Empty opts
status = server.Healthz(new HealthzOptions());
status.ShouldNotBeNull();
status.Status.ShouldBe("ok");
// JSServerOnly — ok without JetStream enabled
status = server.Healthz(new HealthzOptions { JSServerOnly = true });
status.ShouldNotBeNull();
status.Status.ShouldBe("ok");
// Stream without account — bad request
status = server.Healthz(new HealthzOptions { Stream = "TEST" });
status.Status.ShouldBe("error");
status.StatusCode.ShouldBe(400);
// Consumer without stream — bad request
status = server.Healthz(new HealthzOptions { Account = "ACC", Consumer = "CON" });
status.Status.ShouldBe("error");
status.StatusCode.ShouldBe(400);
// Details option for bad request — populates Errors
status = server.Healthz(new HealthzOptions { Stream = "TEST", Details = true });
status.Status.ShouldBe("error");
status.StatusCode.ShouldBe(400);
status.Errors.ShouldNotBeNull();
status.Errors!.Count.ShouldBeGreaterThan(0);
status.Errors[0].Type.ShouldBe(HealthZErrorType.BadRequest);
}
// =========================================================================
// TestMonitorVarzJSApiLevel (T:2127)
// =========================================================================
/// <summary>
/// Verifies that the JetStream API level constant is set in the versioning module.
/// Mirrors Go TestMonitorVarzJSApiLevel in server/monitor_test.go.
/// </summary>
[Fact]
public void MonitorVarzJSApiLevel_IsSet_ShouldSucceed()
{
// JSApiLevel should be a positive integer
JetStreamVersioning.JsApiLevel.ShouldBeGreaterThan(0);
// Verify via Varz structure
var stats = new JetStreamStats
{
Api = new JetStreamApiStats { Level = JetStreamVersioning.JsApiLevel },
};
stats.Api.Level.ShouldBe(JetStreamVersioning.JsApiLevel);
}
}