Files
natsnet/dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/Monitor/MonitorIntegrationTests.cs

435 lines
17 KiB
C#

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