435 lines
17 KiB
C#
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);
|
|
}
|
|
}
|