Merge branch 'worktree-agent-a3c77b78'
This commit is contained in:
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
787
dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/MiscTests.cs
Normal file
787
dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/MiscTests.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user