// 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; /// /// 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. /// [Trait("Category", "Integration")] public sealed class MonitorIntegrationTests { // ========================================================================= // TestMyUptime (T:2113) // ========================================================================= /// /// 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. /// [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) // ========================================================================= /// /// Verifies that a server started without a monitoring port has no monitor address. /// Mirrors Go TestMonitorNoPort in server/monitor_test.go. /// [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 // ========================================================================= /// /// Verifies that a server can be configured with an HTTP base path. /// Mirrors Go TestMonitorHTTPBasePath in server/monitor_test.go. /// [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 // ========================================================================= /// /// Verifies that Varz subscription counts are stable across repeated calls. /// Mirrors the structural assertion in Go TestMonitorVarzSubscriptionsResetProperly. /// [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 // ========================================================================= /// /// Verifies that the Varz monitoring structure has valid timing metadata. /// Mirrors Go TestMonitorHandleVarz in server/monitor_test.go. /// [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) // ========================================================================= /// /// Verifies that Connz returns a valid structure with zero open connections when /// no clients have connected. /// Mirrors Go TestMonitorConnz in server/monitor_test.go. /// [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 // ========================================================================= /// /// Verifies that Connz handles negative offset gracefully. /// Mirrors Go TestMonitorConnzBadParams in server/monitor_test.go. /// [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 // ========================================================================= /// /// Verifies that Connz with subscription filtering returns an empty list for /// a server with no clients. /// Mirrors Go TestMonitorConnzWithSubs in server/monitor_test.go. /// [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) // ========================================================================= /// /// Verifies that the ByCid sort option is the default and produces a valid result. /// Mirrors Go TestMonitorConnzSortedByCid in server/monitor_test.go. /// [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 // ========================================================================= /// /// Verifies that all sort options are valid string values. /// Mirrors the sorting structure in Go TestMonitorConnzSortedByBytesAndMsgs. /// [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) // ========================================================================= /// /// Verifies that Healthz returns "ok" for a healthy server. /// Mirrors Go TestMonitorHealthzStatusOK in server/monitor_test.go. /// [Fact(Skip = "deferred: Monitor health check implementation incomplete")] 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 // ========================================================================= /// /// Verifies the HealthStatus type structure for error conditions. /// Mirrors Go TestMonitorHealthzStatusError in server/monitor_test.go. /// [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 // ========================================================================= /// /// Verifies the Healthz options for JetStream availability checks. /// Mirrors Go TestMonitorHealthzStatusUnavailable in server/monitor_test.go. /// [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) // ========================================================================= /// /// Verifies the Healthz method on the server for basic and error scenarios. /// Mirrors Go TestServerHealthz in server/monitor_test.go. /// [Fact(Skip = "deferred: Monitor health check implementation incomplete")] 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) // ========================================================================= /// /// Verifies that the JetStream API level constant is set in the versioning module. /// Mirrors Go TestMonitorVarzJSApiLevel in server/monitor_test.go. /// [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); } }