refactor: extract NATS.Server.Monitoring.Tests project
Move 39 monitoring, events, and system endpoint test files from NATS.Server.Tests into a dedicated NATS.Server.Monitoring.Tests project. Update namespaces, replace private GetFreePort/ReadUntilAsync with TestUtilities shared helpers, add InternalsVisibleTo, and register in the solution file. All 439 tests pass.
This commit is contained in:
286
tests/NATS.Server.Monitoring.Tests/Events/AuthErrorEventTests.cs
Normal file
286
tests/NATS.Server.Monitoring.Tests/Events/AuthErrorEventTests.cs
Normal file
@@ -0,0 +1,286 @@
|
||||
// Port of Go server/events_test.go — auth error advisory publication tests.
|
||||
// Go reference: golang/nats-server/server/events.go:2631 sendAuthErrorEvent.
|
||||
//
|
||||
// Tests cover: SendAuthErrorEvent counter, enqueue behaviour, record field
|
||||
// preservation, SendConnectEvent, SendDisconnectEvent, and the supporting
|
||||
// detail record types AuthErrorDetail, ConnectEventDetail, DisconnectEventDetail.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server;
|
||||
using NATS.Server.Events;
|
||||
using NATS.Server.TestUtilities;
|
||||
|
||||
namespace NATS.Server.Monitoring.Tests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="InternalEventSystem.SendAuthErrorEvent"/>,
|
||||
/// <see cref="InternalEventSystem.SendConnectEvent"/>,
|
||||
/// <see cref="InternalEventSystem.SendDisconnectEvent"/>, and the three
|
||||
/// companion detail record types.
|
||||
/// Go reference: events_test.go TestSystemAccountDisconnectBadLogin,
|
||||
/// TestSystemAccountNewConnection.
|
||||
/// </summary>
|
||||
public class AuthErrorEventTests : IAsyncLifetime
|
||||
{
|
||||
private NatsServer _server = null!;
|
||||
private int _port;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_port = TestPortAllocator.GetFreePort();
|
||||
_server = new NatsServer(new NatsOptions { Port = _port }, NullLoggerFactory.Instance);
|
||||
_ = _server.StartAsync(CancellationToken.None);
|
||||
await _server.WaitForReadyAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _server.ShutdownAsync();
|
||||
_server.Dispose();
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// AuthErrorEventCount
|
||||
// Go reference: events.go:2631 sendAuthErrorEvent — counter per advisory
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// AuthErrorEventCount starts at zero before any advisories are sent.
|
||||
/// Go reference: events_test.go TestSystemAccountDisconnectBadLogin.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AuthErrorEventCount_StartsAtZero()
|
||||
{
|
||||
// Go reference: events_test.go TestSystemAccountDisconnectBadLogin — no events at startup.
|
||||
var es = _server.EventSystem!;
|
||||
es.AuthErrorEventCount.ShouldBe(0L);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calling SendAuthErrorEvent once increments the counter to 1.
|
||||
/// Go reference: events.go:2631 sendAuthErrorEvent — each call is one advisory.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SendAuthErrorEvent_IncrementsCounter()
|
||||
{
|
||||
// Go reference: events_test.go TestSystemAccountDisconnectBadLogin.
|
||||
var es = _server.EventSystem!;
|
||||
var detail = new AuthErrorDetail(
|
||||
ClientId: 42,
|
||||
RemoteAddress: "127.0.0.1:5000",
|
||||
AccountName: "$G",
|
||||
UserName: "alice",
|
||||
Reason: "Authorization Violation",
|
||||
OccurredAt: DateTime.UtcNow);
|
||||
|
||||
es.SendAuthErrorEvent(_server.ServerId, detail);
|
||||
|
||||
es.AuthErrorEventCount.ShouldBe(1L);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Each call to SendAuthErrorEvent enqueues a message (counter grows by one per call).
|
||||
/// Go reference: events.go:2687 sendInternalMsg — advisory is always enqueued.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SendAuthErrorEvent_EnqueuesMessage()
|
||||
{
|
||||
// Go reference: events.go sendAuthErrorEvent publishes via sendInternalMsg.
|
||||
var es = _server.EventSystem!;
|
||||
var detail = new AuthErrorDetail(
|
||||
ClientId: 7,
|
||||
RemoteAddress: "10.0.0.1:4222",
|
||||
AccountName: null,
|
||||
UserName: null,
|
||||
Reason: "Authentication Timeout",
|
||||
OccurredAt: DateTime.UtcNow);
|
||||
|
||||
var before = es.AuthErrorEventCount;
|
||||
es.SendAuthErrorEvent(_server.ServerId, detail);
|
||||
var after = es.AuthErrorEventCount;
|
||||
|
||||
// The counter increment is the observable side-effect of the enqueue path.
|
||||
(after - before).ShouldBe(1L);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sending multiple auth error events increments the counter for each.
|
||||
/// Go reference: events.go:2631 sendAuthErrorEvent — cumulative count.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AuthErrorEventCount_MultipleSends_Incremented()
|
||||
{
|
||||
// Go reference: events_test.go TestSystemAccountDisconnectBadLogin.
|
||||
var es = _server.EventSystem!;
|
||||
var detail = new AuthErrorDetail(
|
||||
ClientId: 1,
|
||||
RemoteAddress: "192.168.1.1:9999",
|
||||
AccountName: "myacc",
|
||||
UserName: "bob",
|
||||
Reason: "Bad credentials",
|
||||
OccurredAt: DateTime.UtcNow);
|
||||
|
||||
var before = es.AuthErrorEventCount;
|
||||
|
||||
const int count = 5;
|
||||
for (var i = 0; i < count; i++)
|
||||
es.SendAuthErrorEvent(_server.ServerId, detail);
|
||||
|
||||
(es.AuthErrorEventCount - before).ShouldBe(count);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// SendConnectEvent
|
||||
// Go reference: events.go postConnectEvent / sendConnect
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// SendConnectEvent enqueues a message without throwing.
|
||||
/// Go reference: events.go postConnectEvent — advisory fired on client connect.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SendConnectEvent_EnqueuesMessage()
|
||||
{
|
||||
// Go reference: events_test.go TestSystemAccountNewConnection.
|
||||
var es = _server.EventSystem!;
|
||||
var detail = new ConnectEventDetail(
|
||||
ClientId: 10,
|
||||
RemoteAddress: "127.0.0.1:6000",
|
||||
AccountName: "$G",
|
||||
UserName: "user1",
|
||||
ConnectedAt: DateTime.UtcNow);
|
||||
|
||||
var ex = Record.Exception(() => es.SendConnectEvent(_server.ServerId, detail));
|
||||
ex.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// SendDisconnectEvent
|
||||
// Go reference: events.go postDisconnectEvent / sendDisconnect
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// SendDisconnectEvent enqueues a message without throwing.
|
||||
/// Go reference: events.go postDisconnectEvent — advisory fired on client disconnect.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SendDisconnectEvent_EnqueuesMessage()
|
||||
{
|
||||
// Go reference: events_test.go TestSystemAccountNewConnection (disconnect part).
|
||||
var es = _server.EventSystem!;
|
||||
var detail = new DisconnectEventDetail(
|
||||
ClientId: 20,
|
||||
RemoteAddress: "127.0.0.1:7000",
|
||||
AccountName: "$G",
|
||||
Reason: "Client Closed",
|
||||
DisconnectedAt: DateTime.UtcNow);
|
||||
|
||||
var ex = Record.Exception(() => es.SendDisconnectEvent(_server.ServerId, detail));
|
||||
ex.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// AuthErrorDetail record
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// AuthErrorDetail preserves all fields passed to its constructor.
|
||||
/// Go reference: events.go:2631 — all client fields captured in the advisory.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AuthErrorDetail_PreservesAllFields()
|
||||
{
|
||||
// Go reference: events_test.go TestSystemAccountDisconnectBadLogin.
|
||||
var now = DateTime.UtcNow;
|
||||
var detail = new AuthErrorDetail(
|
||||
ClientId: 99,
|
||||
RemoteAddress: "10.0.0.2:1234",
|
||||
AccountName: "test-account",
|
||||
UserName: "testuser",
|
||||
Reason: "Authorization Violation",
|
||||
OccurredAt: now);
|
||||
|
||||
detail.ClientId.ShouldBe(99UL);
|
||||
detail.RemoteAddress.ShouldBe("10.0.0.2:1234");
|
||||
detail.AccountName.ShouldBe("test-account");
|
||||
detail.UserName.ShouldBe("testuser");
|
||||
detail.Reason.ShouldBe("Authorization Violation");
|
||||
detail.OccurredAt.ShouldBe(now);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AuthErrorDetail accepts a non-empty Reason (the key advisory field).
|
||||
/// Go reference: events.go:2631 sendAuthErrorEvent — reason is always set.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AuthErrorDetail_ReasonRequired()
|
||||
{
|
||||
// Go reference: events_test.go TestSystemAccountDisconnectBadLogin — reason distinguishes error types.
|
||||
var detail = new AuthErrorDetail(
|
||||
ClientId: 1,
|
||||
RemoteAddress: "127.0.0.1:0",
|
||||
AccountName: null,
|
||||
UserName: null,
|
||||
Reason: "Authentication Timeout",
|
||||
OccurredAt: DateTime.UtcNow);
|
||||
|
||||
detail.Reason.ShouldNotBeNullOrEmpty();
|
||||
detail.Reason.ShouldBe("Authentication Timeout");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// ConnectEventDetail record
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// ConnectEventDetail preserves all constructor fields.
|
||||
/// Go reference: events.go postConnectEvent — all fields captured on connect.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ConnectEventDetail_PreservesFields()
|
||||
{
|
||||
// Go reference: events_test.go TestSystemAccountNewConnection.
|
||||
var connectedAt = new DateTime(2026, 2, 25, 10, 0, 0, DateTimeKind.Utc);
|
||||
var detail = new ConnectEventDetail(
|
||||
ClientId: 55,
|
||||
RemoteAddress: "192.168.0.5:8080",
|
||||
AccountName: "prod-account",
|
||||
UserName: "svc-user",
|
||||
ConnectedAt: connectedAt);
|
||||
|
||||
detail.ClientId.ShouldBe(55UL);
|
||||
detail.RemoteAddress.ShouldBe("192.168.0.5:8080");
|
||||
detail.AccountName.ShouldBe("prod-account");
|
||||
detail.UserName.ShouldBe("svc-user");
|
||||
detail.ConnectedAt.ShouldBe(connectedAt);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// DisconnectEventDetail record
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// DisconnectEventDetail preserves all constructor fields.
|
||||
/// Go reference: events.go postDisconnectEvent — all fields captured on disconnect.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DisconnectEventDetail_PreservesFields()
|
||||
{
|
||||
// Go reference: events_test.go TestSystemAccountNewConnection (disconnect part).
|
||||
var disconnectedAt = new DateTime(2026, 2, 25, 11, 0, 0, DateTimeKind.Utc);
|
||||
var detail = new DisconnectEventDetail(
|
||||
ClientId: 77,
|
||||
RemoteAddress: "172.16.0.3:3000",
|
||||
AccountName: "staging-account",
|
||||
Reason: "Slow Consumer",
|
||||
DisconnectedAt: disconnectedAt);
|
||||
|
||||
detail.ClientId.ShouldBe(77UL);
|
||||
detail.RemoteAddress.ShouldBe("172.16.0.3:3000");
|
||||
detail.AccountName.ShouldBe("staging-account");
|
||||
detail.Reason.ShouldBe("Slow Consumer");
|
||||
detail.DisconnectedAt.ShouldBe(disconnectedAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
using System.Text.Json;
|
||||
using NATS.Server.Events;
|
||||
|
||||
namespace NATS.Server.Monitoring.Tests.Events;
|
||||
|
||||
public class EventApiAndSubjectsParityBatch2Tests
|
||||
{
|
||||
[Fact]
|
||||
public void EventSubjects_DefineMissingServerRequestSubjects()
|
||||
{
|
||||
EventSubjects.RemoteLatency.ShouldBe("$SYS.SERVER.{0}.ACC.{1}.LATENCY.M2");
|
||||
EventSubjects.UserDirectInfo.ShouldBe("$SYS.REQ.USER.INFO");
|
||||
EventSubjects.UserDirectReq.ShouldBe("$SYS.REQ.USER.{0}.INFO");
|
||||
EventSubjects.AccountNumSubsReq.ShouldBe("$SYS.REQ.ACCOUNT.NSUBS");
|
||||
EventSubjects.AccountSubs.ShouldBe("$SYS._INBOX_.{0}.NSUBS");
|
||||
EventSubjects.ClientKickReq.ShouldBe("$SYS.REQ.SERVER.{0}.KICK");
|
||||
EventSubjects.ClientLdmReq.ShouldBe("$SYS.REQ.SERVER.{0}.LDM");
|
||||
EventSubjects.ServerStatsPingReq.ShouldBe("$SYS.REQ.SERVER.PING.STATSZ");
|
||||
EventSubjects.ServerReloadReq.ShouldBe("$SYS.REQ.SERVER.{0}.RELOAD");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OcspSubjects_MatchGoPatterns()
|
||||
{
|
||||
EventSubjects.OcspPeerReject.ShouldBe("$SYS.SERVER.{0}.OCSP.PEER.CONN.REJECT");
|
||||
EventSubjects.OcspPeerChainlinkInvalid.ShouldBe("$SYS.SERVER.{0}.OCSP.PEER.LINK.INVALID");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OcspPeerRejectEvent_IncludesPeerCertInfo()
|
||||
{
|
||||
var evt = new OcspPeerRejectEventMsg
|
||||
{
|
||||
Id = "id",
|
||||
Kind = "client",
|
||||
Reason = "revoked",
|
||||
Peer = new EventCertInfo
|
||||
{
|
||||
Subject = "CN=client",
|
||||
Issuer = "CN=issuer",
|
||||
Fingerprint = "fingerprint",
|
||||
Raw = "raw",
|
||||
},
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
json.ShouldContain("\"peer\":");
|
||||
json.ShouldContain("\"subject\":\"CN=client\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OcspPeerChainlinkInvalidEvent_SerializesExpectedShape()
|
||||
{
|
||||
var evt = new OcspPeerChainlinkInvalidEventMsg
|
||||
{
|
||||
Id = "id",
|
||||
Link = new EventCertInfo { Subject = "CN=link" },
|
||||
Peer = new EventCertInfo { Subject = "CN=peer" },
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
json.ShouldContain("\"type\":\"io.nats.server.advisory.v1.ocsp_peer_link_invalid\"");
|
||||
json.ShouldContain("\"link\":");
|
||||
json.ShouldContain("\"peer\":");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventFilterOptions_HasCoreGoFields()
|
||||
{
|
||||
var opts = new EventFilterOptions
|
||||
{
|
||||
Name = "srv-a",
|
||||
Cluster = "cluster-a",
|
||||
Host = "127.0.0.1",
|
||||
Tags = ["a", "b"],
|
||||
Domain = "domain-a",
|
||||
};
|
||||
|
||||
opts.Name.ShouldBe("srv-a");
|
||||
opts.Cluster.ShouldBe("cluster-a");
|
||||
opts.Host.ShouldBe("127.0.0.1");
|
||||
opts.Tags.ShouldBe(["a", "b"]);
|
||||
opts.Domain.ShouldBe("domain-a");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionRequestTypes_IncludeBaseFilterFields()
|
||||
{
|
||||
new StatszEventOptions { Name = "n" }.Name.ShouldBe("n");
|
||||
new ConnzEventOptions { Cluster = "c" }.Cluster.ShouldBe("c");
|
||||
new RoutezEventOptions { Host = "h" }.Host.ShouldBe("h");
|
||||
new HealthzEventOptions { Domain = "d" }.Domain.ShouldBe("d");
|
||||
new JszEventOptions { Tags = ["t"] }.Tags.ShouldBe(["t"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerApiResponses_ExposeDataAndError()
|
||||
{
|
||||
var response = new ServerAPIResponse
|
||||
{
|
||||
Server = new EventServerInfo { Id = "S1" },
|
||||
Data = new { ok = true },
|
||||
Error = new ServerAPIError { Code = 500, Description = "err" },
|
||||
};
|
||||
|
||||
response.Server.Id.ShouldBe("S1");
|
||||
response.Error?.Code.ShouldBe(500);
|
||||
response.Error?.Description.ShouldBe("err");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypedServerApiWrappers_CarryResponsePayload()
|
||||
{
|
||||
new ServerAPIConnzResponse { Data = new object() }.Data.ShouldNotBeNull();
|
||||
new ServerAPIRoutezResponse { Data = new object() }.Data.ShouldNotBeNull();
|
||||
new ServerAPIGatewayzResponse { Data = new object() }.Data.ShouldNotBeNull();
|
||||
new ServerAPIJszResponse { Data = new object() }.Data.ShouldNotBeNull();
|
||||
new ServerAPIHealthzResponse { Data = new object() }.Data.ShouldNotBeNull();
|
||||
new ServerAPIVarzResponse { Data = new object() }.Data.ShouldNotBeNull();
|
||||
new ServerAPISubszResponse { Data = new object() }.Data.ShouldNotBeNull();
|
||||
new ServerAPILeafzResponse { Data = new object() }.Data.ShouldNotBeNull();
|
||||
new ServerAPIAccountzResponse { Data = new object() }.Data.ShouldNotBeNull();
|
||||
new ServerAPIExpvarzResponse { Data = new object() }.Data.ShouldNotBeNull();
|
||||
new ServerAPIpqueueszResponse { Data = new object() }.Data.ShouldNotBeNull();
|
||||
new ServerAPIRaftzResponse { Data = new object() }.Data.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequestPayloadTypes_KickAndLdm()
|
||||
{
|
||||
var kick = new KickClientReq { ClientId = 22 };
|
||||
var ldm = new LDMClientReq { ClientId = 33 };
|
||||
|
||||
kick.ClientId.ShouldBe(22UL);
|
||||
ldm.ClientId.ShouldBe(33UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserInfo_IncludesExpectedIdentityFields()
|
||||
{
|
||||
var info = new UserInfo
|
||||
{
|
||||
User = "alice",
|
||||
Account = "A",
|
||||
Permissions = "pubsub",
|
||||
};
|
||||
|
||||
info.User.ShouldBe("alice");
|
||||
info.Account.ShouldBe("A");
|
||||
info.Permissions.ShouldBe("pubsub");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
// Go reference: server/events.go:2082-2090 — compressionType / snappyCompression,
|
||||
// and events.go:578-598 — internalSendLoop optional compression branch.
|
||||
|
||||
using System.Text;
|
||||
using NATS.Server.Events;
|
||||
|
||||
namespace NATS.Server.Monitoring.Tests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="EventCompressor"/> — S2/Snappy compression for system event payloads.
|
||||
/// Go reference: server/events.go — compressed system events via snappyCompression.
|
||||
/// </summary>
|
||||
public class EventCompressionTests : IDisposable
|
||||
{
|
||||
public EventCompressionTests()
|
||||
{
|
||||
// Ensure a clean statistics baseline for every test.
|
||||
EventCompressor.ResetStats();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
EventCompressor.ResetStats();
|
||||
}
|
||||
|
||||
// ── 1 ──────────────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void Compress_ValidPayload_ReturnsCompressed()
|
||||
{
|
||||
// Arrange
|
||||
var json = """{"server":{"name":"s1","id":"ABCDEF"},"data":{"conns":42,"bytes":1024}}""";
|
||||
var payload = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
// Act
|
||||
var compressed = EventCompressor.Compress(payload);
|
||||
|
||||
// Assert
|
||||
compressed.ShouldNotBeNull();
|
||||
compressed.Length.ShouldBeGreaterThan(0);
|
||||
// Snappy output begins with a varint for the original length — not the same raw bytes.
|
||||
compressed.ShouldNotBe(payload);
|
||||
}
|
||||
|
||||
// ── 2 ──────────────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void Decompress_RoundTrip_MatchesOriginal()
|
||||
{
|
||||
// Arrange
|
||||
var original = Encoding.UTF8.GetBytes(
|
||||
"""{"server":{"name":"test","id":"XYZ"},"stats":{"cpu":0.5,"mem":1048576}}""");
|
||||
|
||||
// Act
|
||||
var compressed = EventCompressor.Compress(original);
|
||||
var decompressed = EventCompressor.Decompress(compressed);
|
||||
|
||||
// Assert
|
||||
decompressed.ShouldBe(original);
|
||||
}
|
||||
|
||||
// ── 3 ──────────────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void ShouldCompress_BelowThreshold_ReturnsFalse()
|
||||
{
|
||||
// 100 bytes is well below the default 256-byte threshold.
|
||||
EventCompressor.ShouldCompress(100).ShouldBeFalse();
|
||||
}
|
||||
|
||||
// ── 4 ──────────────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void ShouldCompress_AboveThreshold_ReturnsTrue()
|
||||
{
|
||||
// 500 bytes exceeds the default 256-byte threshold.
|
||||
EventCompressor.ShouldCompress(500).ShouldBeTrue();
|
||||
}
|
||||
|
||||
// ── 5 ──────────────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void CompressIfBeneficial_SmallPayload_NotCompressed()
|
||||
{
|
||||
// Arrange — 50 bytes is below the 256-byte threshold.
|
||||
var payload = Encoding.UTF8.GetBytes("small");
|
||||
|
||||
// Act
|
||||
var (data, compressed) = EventCompressor.CompressIfBeneficial(payload);
|
||||
|
||||
// Assert
|
||||
compressed.ShouldBeFalse();
|
||||
data.ShouldBe(payload);
|
||||
}
|
||||
|
||||
// ── 6 ──────────────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void CompressIfBeneficial_LargePayload_Compressed()
|
||||
{
|
||||
// Arrange — build a payload well above the 256-byte threshold.
|
||||
var largeJson = """{"server":{"name":"s1"},"data":""" + new string('x', 500) + "}";
|
||||
var payload = Encoding.UTF8.GetBytes(largeJson);
|
||||
payload.Length.ShouldBeGreaterThan(256);
|
||||
|
||||
// Act
|
||||
var (data, isCompressed) = EventCompressor.CompressIfBeneficial(payload);
|
||||
|
||||
// Assert
|
||||
isCompressed.ShouldBeTrue();
|
||||
// The returned bytes should decompress back to the original.
|
||||
var restored = EventCompressor.Decompress(data);
|
||||
restored.ShouldBe(payload);
|
||||
}
|
||||
|
||||
// ── 7 ──────────────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void GetCompressionRatio_Calculates()
|
||||
{
|
||||
// 100 / 200 = 0.5
|
||||
var ratio = EventCompressor.GetCompressionRatio(originalSize: 200, compressedSize: 100);
|
||||
|
||||
ratio.ShouldBe(0.5, tolerance: 0.001);
|
||||
}
|
||||
|
||||
// ── 8 ──────────────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void TotalCompressed_IncrementedOnCompress()
|
||||
{
|
||||
// Arrange — stats were reset in constructor.
|
||||
EventCompressor.TotalCompressed.ShouldBe(0L);
|
||||
|
||||
var largePayload = Encoding.UTF8.GetBytes(new string('a', 512));
|
||||
|
||||
// Act — two calls that exceed threshold.
|
||||
EventCompressor.CompressIfBeneficial(largePayload);
|
||||
EventCompressor.CompressIfBeneficial(largePayload);
|
||||
|
||||
// Assert
|
||||
EventCompressor.TotalCompressed.ShouldBe(2L);
|
||||
}
|
||||
|
||||
// ── 9 ──────────────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void BytesSaved_TracksCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
// Use a highly-compressible payload so savings are guaranteed.
|
||||
var payload = Encoding.UTF8.GetBytes(new string('z', 1024));
|
||||
|
||||
// Act
|
||||
EventCompressor.CompressIfBeneficial(payload);
|
||||
|
||||
// Assert — compressed version of 1 024 repeated bytes should be much smaller.
|
||||
EventCompressor.BytesSaved.ShouldBeGreaterThan(0L);
|
||||
// BytesSaved = original - compressed; should be less than original size.
|
||||
EventCompressor.BytesSaved.ShouldBeLessThan(payload.Length);
|
||||
}
|
||||
|
||||
// ── 10 ─────────────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void ResetStats_ClearsAll()
|
||||
{
|
||||
// Arrange — produce some stats first.
|
||||
var largePayload = Encoding.UTF8.GetBytes(new string('b', 512));
|
||||
EventCompressor.CompressIfBeneficial(largePayload);
|
||||
EventCompressor.TotalCompressed.ShouldBeGreaterThan(0L);
|
||||
|
||||
// Act
|
||||
EventCompressor.ResetStats();
|
||||
|
||||
// Assert
|
||||
EventCompressor.TotalCompressed.ShouldBe(0L);
|
||||
EventCompressor.TotalUncompressed.ShouldBe(0L);
|
||||
EventCompressor.BytesSaved.ShouldBe(0L);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAcceptEncoding_ParsesSnappyAndGzip()
|
||||
{
|
||||
EventCompressor.GetAcceptEncoding("gzip, snappy").ShouldBe(EventCompressionType.Snappy);
|
||||
EventCompressor.GetAcceptEncoding("gzip").ShouldBe(EventCompressionType.Gzip);
|
||||
EventCompressor.GetAcceptEncoding("br").ShouldBe(EventCompressionType.Unsupported);
|
||||
EventCompressor.GetAcceptEncoding(null).ShouldBe(EventCompressionType.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompressionHeaderConstants_MatchGo()
|
||||
{
|
||||
EventCompressor.AcceptEncodingHeader.ShouldBe("Accept-Encoding");
|
||||
EventCompressor.ContentEncodingHeader.ShouldBe("Content-Encoding");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompressAndDecompress_Gzip_RoundTrip_MatchesOriginal()
|
||||
{
|
||||
var payload = Encoding.UTF8.GetBytes("""{"server":"s1","data":"gzip-payload"}""");
|
||||
|
||||
var compressed = EventCompressor.Compress(payload, EventCompressionType.Gzip);
|
||||
var restored = EventCompressor.Decompress(compressed, EventCompressionType.Gzip);
|
||||
|
||||
restored.ShouldBe(payload);
|
||||
}
|
||||
}
|
||||
943
tests/NATS.Server.Monitoring.Tests/Events/EventGoParityTests.cs
Normal file
943
tests/NATS.Server.Monitoring.Tests/Events/EventGoParityTests.cs
Normal file
@@ -0,0 +1,943 @@
|
||||
// Port of Go server/events_test.go — system event DTO and subject parity tests.
|
||||
// Reference: golang/nats-server/server/events_test.go
|
||||
//
|
||||
// Tests cover: ConnectEventMsg, DisconnectEventMsg, ServerStatsMsg,
|
||||
// AccountNumConns, AuthErrorEventMsg, ShutdownEventMsg serialization,
|
||||
// event subject pattern formatting, event filtering by tag/server ID,
|
||||
// and HealthZ status code mapping.
|
||||
|
||||
using System.Text.Json;
|
||||
using NATS.Server.Events;
|
||||
|
||||
namespace NATS.Server.Monitoring.Tests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Parity tests ported from Go server/events_test.go exercising
|
||||
/// system event DTOs, JSON serialization shapes, event subjects,
|
||||
/// and event filtering logic.
|
||||
/// </summary>
|
||||
public class EventGoParityTests
|
||||
{
|
||||
// ========================================================================
|
||||
// ConnectEventMsg serialization
|
||||
// Go reference: events_test.go TestSystemAccountNewConnection
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ConnectEventMsg_JsonShape_MatchesGo()
|
||||
{
|
||||
// Go: TestSystemAccountNewConnection — verifies connect event JSON shape.
|
||||
var evt = new ConnectEventMsg
|
||||
{
|
||||
Id = "evt-001",
|
||||
Time = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
Server = new EventServerInfo
|
||||
{
|
||||
Name = "test-server",
|
||||
Id = "NSVR001",
|
||||
Cluster = "test-cluster",
|
||||
Version = "2.10.0",
|
||||
},
|
||||
Client = new EventClientInfo
|
||||
{
|
||||
Id = 42,
|
||||
Account = "$G",
|
||||
User = "alice",
|
||||
Name = "test-client",
|
||||
Lang = "csharp",
|
||||
Version = "1.0",
|
||||
},
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
|
||||
json.ShouldContain("\"type\":");
|
||||
json.ShouldContain(ConnectEventMsg.EventType);
|
||||
json.ShouldContain("\"server\":");
|
||||
json.ShouldContain("\"client\":");
|
||||
json.ShouldContain("\"id\":\"evt-001\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConnectEventMsg_EventType_Constant()
|
||||
{
|
||||
// Go: connect event type string.
|
||||
ConnectEventMsg.EventType.ShouldBe("io.nats.server.advisory.v1.client_connect");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConnectEventMsg_DefaultType_MatchesConstant()
|
||||
{
|
||||
var evt = new ConnectEventMsg();
|
||||
evt.Type.ShouldBe(ConnectEventMsg.EventType);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// DisconnectEventMsg serialization
|
||||
// Go reference: events_test.go TestSystemAccountNewConnection (disconnect part)
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void DisconnectEventMsg_JsonShape_MatchesGo()
|
||||
{
|
||||
// Go: TestSystemAccountNewConnection — verifies disconnect event includes
|
||||
// sent/received stats and reason.
|
||||
var evt = new DisconnectEventMsg
|
||||
{
|
||||
Id = "evt-002",
|
||||
Time = DateTime.UtcNow,
|
||||
Server = new EventServerInfo { Name = "test-server", Id = "NSVR001" },
|
||||
Client = new EventClientInfo { Id = 42, Account = "$G" },
|
||||
Sent = new DataStats { Msgs = 100, Bytes = 10240 },
|
||||
Received = new DataStats { Msgs = 50, Bytes = 5120 },
|
||||
Reason = "Client Closed",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
|
||||
json.ShouldContain("\"type\":");
|
||||
json.ShouldContain(DisconnectEventMsg.EventType);
|
||||
json.ShouldContain("\"sent\":");
|
||||
json.ShouldContain("\"received\":");
|
||||
json.ShouldContain("\"reason\":");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisconnectEventMsg_EventType_Constant()
|
||||
{
|
||||
DisconnectEventMsg.EventType.ShouldBe("io.nats.server.advisory.v1.client_disconnect");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisconnectEventMsg_Reason_ClientClosed()
|
||||
{
|
||||
// Go: TestSystemAccountDisconnectBadLogin — reason is captured on disconnect.
|
||||
var evt = new DisconnectEventMsg { Reason = "Client Closed" };
|
||||
evt.Reason.ShouldBe("Client Closed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisconnectEventMsg_Reason_AuthViolation()
|
||||
{
|
||||
// Go: TestSystemAccountDisconnectBadLogin — bad login reason.
|
||||
var evt = new DisconnectEventMsg { Reason = "Authentication Violation" };
|
||||
evt.Reason.ShouldBe("Authentication Violation");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// DataStats
|
||||
// Go reference: events_test.go TestSystemAccountingWithLeafNodes
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void DataStats_JsonSerialization()
|
||||
{
|
||||
// Go: TestSystemAccountingWithLeafNodes — verifies sent/received stats structure.
|
||||
var stats = new DataStats
|
||||
{
|
||||
Msgs = 1000,
|
||||
Bytes = 65536,
|
||||
Routes = new MsgBytesStats { Msgs = 200, Bytes = 10240 },
|
||||
Gateways = new MsgBytesStats { Msgs = 50, Bytes = 2048 },
|
||||
Leafs = new MsgBytesStats { Msgs = 100, Bytes = 5120 },
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(stats);
|
||||
|
||||
json.ShouldContain("\"msgs\":");
|
||||
json.ShouldContain("\"bytes\":");
|
||||
json.ShouldContain("\"routes\":");
|
||||
json.ShouldContain("\"gateways\":");
|
||||
json.ShouldContain("\"leafs\":");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataStats_NullSubStats_OmittedFromJson()
|
||||
{
|
||||
// Go: When no routes/gateways/leafs, those fields are omitted (omitempty).
|
||||
var stats = new DataStats { Msgs = 100, Bytes = 1024 };
|
||||
|
||||
var json = JsonSerializer.Serialize(stats);
|
||||
|
||||
json.ShouldNotContain("\"routes\":");
|
||||
json.ShouldNotContain("\"gateways\":");
|
||||
json.ShouldNotContain("\"leafs\":");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// AccountNumConns
|
||||
// Go reference: events_test.go TestAccountReqMonitoring
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void AccountNumConns_JsonShape_MatchesGo()
|
||||
{
|
||||
// Go: TestAccountReqMonitoring — verifies account connection count event shape.
|
||||
var evt = new AccountNumConns
|
||||
{
|
||||
Id = "evt-003",
|
||||
Time = DateTime.UtcNow,
|
||||
Server = new EventServerInfo { Name = "test-server", Id = "NSVR001" },
|
||||
AccountName = "MYACCOUNT",
|
||||
Connections = 5,
|
||||
LeafNodes = 2,
|
||||
TotalConnections = 10,
|
||||
NumSubscriptions = 42,
|
||||
Sent = new DataStats { Msgs = 500, Bytes = 25600 },
|
||||
Received = new DataStats { Msgs = 250, Bytes = 12800 },
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
|
||||
json.ShouldContain("\"type\":");
|
||||
json.ShouldContain(AccountNumConns.EventType);
|
||||
json.ShouldContain("\"acc\":");
|
||||
json.ShouldContain("\"conns\":");
|
||||
json.ShouldContain("\"leafnodes\":");
|
||||
json.ShouldContain("\"total_conns\":");
|
||||
json.ShouldContain("\"num_subscriptions\":");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AccountNumConns_EventType_Constant()
|
||||
{
|
||||
AccountNumConns.EventType.ShouldBe("io.nats.server.advisory.v1.account_connections");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AccountNumConns_SlowConsumers_IncludedWhenNonZero()
|
||||
{
|
||||
var evt = new AccountNumConns { SlowConsumers = 3 };
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
json.ShouldContain("\"slow_consumers\":3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AccountNumConns_SlowConsumers_OmittedWhenZero()
|
||||
{
|
||||
// Go: omitempty behavior — zero slow_consumers omitted.
|
||||
var evt = new AccountNumConns { SlowConsumers = 0 };
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
json.ShouldNotContain("\"slow_consumers\":");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// ServerStatsMsg
|
||||
// Go reference: events_test.go TestServerEventsPingStatsZDedicatedRecvQ
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ServerStatsMsg_JsonShape_MatchesGo()
|
||||
{
|
||||
// Go: TestServerEventsPingStatsZDedicatedRecvQ — verifies server stats shape.
|
||||
var msg = new ServerStatsMsg
|
||||
{
|
||||
Server = new EventServerInfo
|
||||
{
|
||||
Name = "test-server",
|
||||
Id = "NSVR001",
|
||||
Version = "2.10.0",
|
||||
JetStream = true,
|
||||
},
|
||||
Stats = new ServerStatsData
|
||||
{
|
||||
Start = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
Mem = 134217728,
|
||||
Cores = 8,
|
||||
Cpu = 12.5,
|
||||
Connections = 10,
|
||||
TotalConnections = 100,
|
||||
ActiveAccounts = 5,
|
||||
Subscriptions = 42,
|
||||
Sent = new DataStats { Msgs = 1000, Bytes = 65536 },
|
||||
Received = new DataStats { Msgs = 500, Bytes = 32768 },
|
||||
InMsgs = 500,
|
||||
OutMsgs = 1000,
|
||||
InBytes = 32768,
|
||||
OutBytes = 65536,
|
||||
},
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(msg);
|
||||
|
||||
json.ShouldContain("\"server\":");
|
||||
json.ShouldContain("\"statsz\":");
|
||||
json.ShouldContain("\"mem\":");
|
||||
json.ShouldContain("\"cores\":");
|
||||
json.ShouldContain("\"connections\":");
|
||||
json.ShouldContain("\"total_connections\":");
|
||||
json.ShouldContain("\"subscriptions\":");
|
||||
json.ShouldContain("\"in_msgs\":");
|
||||
json.ShouldContain("\"out_msgs\":");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerStatsData_SlowConsumerStats_JsonShape()
|
||||
{
|
||||
// Go: TestServerEventsPingStatsSlowConsumersStats — breakdown by type.
|
||||
var data = new ServerStatsData
|
||||
{
|
||||
SlowConsumers = 10,
|
||||
SlowConsumerStats = new NATS.Server.Events.SlowConsumersStats
|
||||
{
|
||||
Clients = 5,
|
||||
Routes = 2,
|
||||
Gateways = 1,
|
||||
Leafs = 2,
|
||||
},
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
|
||||
json.ShouldContain("\"slow_consumers\":10");
|
||||
json.ShouldContain("\"slow_consumer_stats\":");
|
||||
json.ShouldContain("\"clients\":5");
|
||||
json.ShouldContain("\"routes\":2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerStatsData_StaleConnectionStats_JsonShape()
|
||||
{
|
||||
// Go: TestServerEventsPingStatsStaleConnectionStats — stale conn breakdown.
|
||||
var data = new ServerStatsData
|
||||
{
|
||||
StaleConnections = 7,
|
||||
StaleConnectionStats = new NATS.Server.Events.StaleConnectionStats
|
||||
{
|
||||
Clients = 3,
|
||||
Routes = 1,
|
||||
Gateways = 2,
|
||||
Leafs = 1,
|
||||
},
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
|
||||
json.ShouldContain("\"stale_connections\":7");
|
||||
json.ShouldContain("\"stale_connection_stats\":");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerStatsData_RouteStats_JsonShape()
|
||||
{
|
||||
// Go: TestServerEventsPingStatsZDedicatedRecvQ — route stats in statsz.
|
||||
var data = new ServerStatsData
|
||||
{
|
||||
Routes =
|
||||
[
|
||||
new RouteStat
|
||||
{
|
||||
Id = 100,
|
||||
Name = "route-1",
|
||||
Sent = new DataStats { Msgs = 200, Bytes = 10240 },
|
||||
Received = new DataStats { Msgs = 150, Bytes = 7680 },
|
||||
Pending = 5,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
|
||||
json.ShouldContain("\"routes\":");
|
||||
json.ShouldContain("\"rid\":100");
|
||||
json.ShouldContain("\"pending\":5");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerStatsData_GatewayStats_JsonShape()
|
||||
{
|
||||
// Go: TestGatewayNameClientInfo — gateway stats in statsz.
|
||||
var data = new ServerStatsData
|
||||
{
|
||||
Gateways =
|
||||
[
|
||||
new GatewayStat
|
||||
{
|
||||
Id = 200,
|
||||
Name = "gw-east",
|
||||
Sent = new DataStats { Msgs = 500, Bytes = 25600 },
|
||||
Received = new DataStats { Msgs = 300, Bytes = 15360 },
|
||||
InboundConnections = 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
|
||||
json.ShouldContain("\"gateways\":");
|
||||
json.ShouldContain("\"gwid\":200");
|
||||
json.ShouldContain("\"inbound_connections\":3");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// ShutdownEventMsg
|
||||
// Go reference: events_test.go TestServerEventsLDMKick
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ShutdownEventMsg_JsonShape_MatchesGo()
|
||||
{
|
||||
// Go: ShutdownEventMsg includes server info and reason.
|
||||
var evt = new ShutdownEventMsg
|
||||
{
|
||||
Server = new EventServerInfo { Name = "test-server", Id = "NSVR001" },
|
||||
Reason = "process exit",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
|
||||
json.ShouldContain("\"server\":");
|
||||
json.ShouldContain("\"reason\":");
|
||||
json.ShouldContain("\"process exit\"");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// LameDuckEventMsg
|
||||
// Go reference: events_test.go TestServerEventsLDMKick
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void LameDuckEventMsg_JsonShape_MatchesGo()
|
||||
{
|
||||
// Go: TestServerEventsLDMKick — lame duck event emitted before shutdown.
|
||||
var evt = new LameDuckEventMsg
|
||||
{
|
||||
Server = new EventServerInfo { Name = "test-server", Id = "NSVR001" },
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
|
||||
json.ShouldContain("\"server\":");
|
||||
json.ShouldContain("\"name\":\"test-server\"");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// AuthErrorEventMsg
|
||||
// Go reference: events_test.go TestSystemAccountDisconnectBadLogin
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void AuthErrorEventMsg_JsonShape_MatchesGo()
|
||||
{
|
||||
// Go: TestSystemAccountDisconnectBadLogin — auth error advisory.
|
||||
var evt = new AuthErrorEventMsg
|
||||
{
|
||||
Id = "evt-004",
|
||||
Time = DateTime.UtcNow,
|
||||
Server = new EventServerInfo { Name = "test-server", Id = "NSVR001" },
|
||||
Client = new EventClientInfo { Id = 99, Host = "192.168.1.100" },
|
||||
Reason = "Authorization Violation",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
|
||||
json.ShouldContain("\"type\":");
|
||||
json.ShouldContain(AuthErrorEventMsg.EventType);
|
||||
json.ShouldContain("\"reason\":");
|
||||
json.ShouldContain("\"Authorization Violation\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuthErrorEventMsg_EventType_Constant()
|
||||
{
|
||||
AuthErrorEventMsg.EventType.ShouldBe("io.nats.server.advisory.v1.client_auth");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// OcspPeerRejectEventMsg
|
||||
// Go reference: events.go OCSPPeerRejectEventMsg struct
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void OcspPeerRejectEventMsg_JsonShape_MatchesGo()
|
||||
{
|
||||
var evt = new OcspPeerRejectEventMsg
|
||||
{
|
||||
Id = "evt-005",
|
||||
Time = DateTime.UtcNow,
|
||||
Kind = "client",
|
||||
Server = new EventServerInfo { Name = "test-server", Id = "NSVR001" },
|
||||
Reason = "OCSP certificate revoked",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
|
||||
json.ShouldContain("\"type\":");
|
||||
json.ShouldContain(OcspPeerRejectEventMsg.EventType);
|
||||
json.ShouldContain("\"kind\":\"client\"");
|
||||
json.ShouldContain("\"reason\":");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OcspPeerRejectEventMsg_EventType_Constant()
|
||||
{
|
||||
OcspPeerRejectEventMsg.EventType.ShouldBe("io.nats.server.advisory.v1.ocsp_peer_reject");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// AccNumConnsReq
|
||||
// Go reference: events.go accNumConnsReq
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void AccNumConnsReq_JsonShape_MatchesGo()
|
||||
{
|
||||
var req = new AccNumConnsReq
|
||||
{
|
||||
Server = new EventServerInfo { Name = "test-server", Id = "NSVR001" },
|
||||
Account = "$G",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(req);
|
||||
|
||||
json.ShouldContain("\"server\":");
|
||||
json.ShouldContain("\"acc\":\"$G\"");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// EventServerInfo
|
||||
// Go reference: events_test.go TestServerEventsFilteredByTag
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void EventServerInfo_Tags_Serialized()
|
||||
{
|
||||
// Go: TestServerEventsFilteredByTag — server info includes tags for filtering.
|
||||
var info = new EventServerInfo
|
||||
{
|
||||
Name = "test-server",
|
||||
Id = "NSVR001",
|
||||
Tags = ["region:us-east-1", "env:production"],
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(info);
|
||||
|
||||
json.ShouldContain("\"tags\":");
|
||||
json.ShouldContain("\"region:us-east-1\"");
|
||||
json.ShouldContain("\"env:production\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventServerInfo_NullTags_OmittedFromJson()
|
||||
{
|
||||
// Go: omitempty — nil tags are not serialized.
|
||||
var info = new EventServerInfo { Name = "test-server", Id = "NSVR001" };
|
||||
var json = JsonSerializer.Serialize(info);
|
||||
json.ShouldNotContain("\"tags\":");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventServerInfo_Metadata_Serialized()
|
||||
{
|
||||
var info = new EventServerInfo
|
||||
{
|
||||
Name = "test-server",
|
||||
Id = "NSVR001",
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["cloud"] = "aws",
|
||||
["zone"] = "us-east-1a",
|
||||
},
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(info);
|
||||
|
||||
json.ShouldContain("\"metadata\":");
|
||||
json.ShouldContain("\"cloud\":");
|
||||
json.ShouldContain("\"aws\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventServerInfo_NullMetadata_OmittedFromJson()
|
||||
{
|
||||
var info = new EventServerInfo { Name = "test-server", Id = "NSVR001" };
|
||||
var json = JsonSerializer.Serialize(info);
|
||||
json.ShouldNotContain("\"metadata\":");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventServerInfo_JetStream_IncludedWhenTrue()
|
||||
{
|
||||
var info = new EventServerInfo { Name = "s1", Id = "N1", JetStream = true };
|
||||
var json = JsonSerializer.Serialize(info);
|
||||
json.ShouldContain("\"jetstream\":true");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventServerInfo_JetStream_OmittedWhenFalse()
|
||||
{
|
||||
// Go: omitempty — JetStream false is not serialized.
|
||||
var info = new EventServerInfo { Name = "s1", Id = "N1", JetStream = false };
|
||||
var json = JsonSerializer.Serialize(info);
|
||||
json.ShouldNotContain("\"jetstream\":");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// EventClientInfo
|
||||
// Go reference: events_test.go TestGatewayNameClientInfo
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void EventClientInfo_AllFields_Serialized()
|
||||
{
|
||||
// Go: TestGatewayNameClientInfo — client info includes all connection metadata.
|
||||
var info = new EventClientInfo
|
||||
{
|
||||
Id = 42,
|
||||
Account = "MYACCOUNT",
|
||||
User = "alice",
|
||||
Name = "test-client",
|
||||
Lang = "go",
|
||||
Version = "1.30.0",
|
||||
RttNanos = 1_500_000, // 1.5ms
|
||||
Host = "192.168.1.100",
|
||||
Kind = "Client",
|
||||
ClientType = "nats",
|
||||
Tags = ["role:publisher"],
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(info);
|
||||
|
||||
json.ShouldContain("\"id\":42");
|
||||
json.ShouldContain("\"acc\":\"MYACCOUNT\"");
|
||||
json.ShouldContain("\"user\":\"alice\"");
|
||||
json.ShouldContain("\"name\":\"test-client\"");
|
||||
json.ShouldContain("\"lang\":\"go\"");
|
||||
json.ShouldContain("\"rtt\":");
|
||||
json.ShouldContain("\"kind\":\"Client\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventClientInfo_MqttClient_Serialized()
|
||||
{
|
||||
// Go: MQTT client ID is included in client info when present.
|
||||
var info = new EventClientInfo
|
||||
{
|
||||
Id = 10,
|
||||
MqttClient = "mqtt-device-42",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(info);
|
||||
|
||||
json.ShouldContain("\"client_id\":\"mqtt-device-42\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventClientInfo_NullOptionalFields_OmittedFromJson()
|
||||
{
|
||||
// Go: omitempty — null optional fields are not serialized.
|
||||
var info = new EventClientInfo { Id = 1 };
|
||||
|
||||
var json = JsonSerializer.Serialize(info);
|
||||
|
||||
json.ShouldNotContain("\"acc\":");
|
||||
json.ShouldNotContain("\"user\":");
|
||||
json.ShouldNotContain("\"name\":");
|
||||
json.ShouldNotContain("\"lang\":");
|
||||
json.ShouldNotContain("\"kind\":");
|
||||
json.ShouldNotContain("\"tags\":");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Event Subject Patterns
|
||||
// Go reference: events.go subject constants
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void EventSubjects_ConnectEvent_Format()
|
||||
{
|
||||
// Go: $SYS.ACCOUNT.%s.CONNECT
|
||||
var subject = string.Format(EventSubjects.ConnectEvent, "$G");
|
||||
subject.ShouldBe("$SYS.ACCOUNT.$G.CONNECT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventSubjects_DisconnectEvent_Format()
|
||||
{
|
||||
// Go: $SYS.ACCOUNT.%s.DISCONNECT
|
||||
var subject = string.Format(EventSubjects.DisconnectEvent, "$G");
|
||||
subject.ShouldBe("$SYS.ACCOUNT.$G.DISCONNECT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventSubjects_AccountConns_Format()
|
||||
{
|
||||
// Go: $SYS.ACCOUNT.%s.SERVER.CONNS (new format)
|
||||
var subject = string.Format(EventSubjects.AccountConnsNew, "MYACCOUNT");
|
||||
subject.ShouldBe("$SYS.ACCOUNT.MYACCOUNT.SERVER.CONNS");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventSubjects_AccountConnsOld_Format()
|
||||
{
|
||||
// Go: $SYS.SERVER.ACCOUNT.%s.CONNS (old format for backward compat)
|
||||
var subject = string.Format(EventSubjects.AccountConnsOld, "MYACCOUNT");
|
||||
subject.ShouldBe("$SYS.SERVER.ACCOUNT.MYACCOUNT.CONNS");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventSubjects_ServerStats_Format()
|
||||
{
|
||||
// Go: $SYS.SERVER.%s.STATSZ
|
||||
var subject = string.Format(EventSubjects.ServerStats, "NSVR001");
|
||||
subject.ShouldBe("$SYS.SERVER.NSVR001.STATSZ");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventSubjects_ServerShutdown_Format()
|
||||
{
|
||||
// Go: $SYS.SERVER.%s.SHUTDOWN
|
||||
var subject = string.Format(EventSubjects.ServerShutdown, "NSVR001");
|
||||
subject.ShouldBe("$SYS.SERVER.NSVR001.SHUTDOWN");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventSubjects_ServerLameDuck_Format()
|
||||
{
|
||||
// Go: $SYS.SERVER.%s.LAMEDUCK
|
||||
var subject = string.Format(EventSubjects.ServerLameDuck, "NSVR001");
|
||||
subject.ShouldBe("$SYS.SERVER.NSVR001.LAMEDUCK");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventSubjects_AuthError_Format()
|
||||
{
|
||||
// Go: $SYS.SERVER.%s.CLIENT.AUTH.ERR
|
||||
var subject = string.Format(EventSubjects.AuthError, "NSVR001");
|
||||
subject.ShouldBe("$SYS.SERVER.NSVR001.CLIENT.AUTH.ERR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventSubjects_AuthErrorAccount_IsConstant()
|
||||
{
|
||||
// Go: $SYS.ACCOUNT.CLIENT.AUTH.ERR (no server ID interpolation)
|
||||
EventSubjects.AuthErrorAccount.ShouldBe("$SYS.ACCOUNT.CLIENT.AUTH.ERR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventSubjects_ServerPing_Format()
|
||||
{
|
||||
// Go: $SYS.REQ.SERVER.PING.%s (e.g., STATSZ, VARZ)
|
||||
var subject = string.Format(EventSubjects.ServerPing, "STATSZ");
|
||||
subject.ShouldBe("$SYS.REQ.SERVER.PING.STATSZ");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventSubjects_ServerReq_Format()
|
||||
{
|
||||
// Go: $SYS.REQ.SERVER.%s.%s (server ID + request type)
|
||||
var subject = string.Format(EventSubjects.ServerReq, "NSVR001", "VARZ");
|
||||
subject.ShouldBe("$SYS.REQ.SERVER.NSVR001.VARZ");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventSubjects_AccountReq_Format()
|
||||
{
|
||||
// Go: $SYS.REQ.ACCOUNT.%s.%s (account + request type)
|
||||
var subject = string.Format(EventSubjects.AccountReq, "MYACCOUNT", "CONNZ");
|
||||
subject.ShouldBe("$SYS.REQ.ACCOUNT.MYACCOUNT.CONNZ");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Event filtering by tag
|
||||
// Go reference: events_test.go TestServerEventsFilteredByTag
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void EventServerInfo_TagFiltering_MatchesTag()
|
||||
{
|
||||
// Go: TestServerEventsFilteredByTag — servers can be filtered by tag value.
|
||||
var server = new EventServerInfo
|
||||
{
|
||||
Name = "s1",
|
||||
Id = "NSVR001",
|
||||
Tags = ["region:us-east-1", "env:prod"],
|
||||
};
|
||||
|
||||
// Simulate filtering: check if server has a specific tag.
|
||||
server.Tags.ShouldContain("region:us-east-1");
|
||||
server.Tags.ShouldContain("env:prod");
|
||||
server.Tags.ShouldNotContain("region:eu-west-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventServerInfo_TagFiltering_EmptyTags_NoMatch()
|
||||
{
|
||||
// Go: TestServerEventsFilteredByTag — server with no tags does not match any filter.
|
||||
var server = new EventServerInfo { Name = "s1", Id = "NSVR001" };
|
||||
server.Tags.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventServerInfo_FilterByServerId()
|
||||
{
|
||||
// Go: TestServerEventsPingStatsZFilter — filter stats events by server ID.
|
||||
var servers = new[]
|
||||
{
|
||||
new EventServerInfo { Name = "s1", Id = "NSVR001" },
|
||||
new EventServerInfo { Name = "s2", Id = "NSVR002" },
|
||||
new EventServerInfo { Name = "s3", Id = "NSVR003" },
|
||||
};
|
||||
|
||||
var filtered = servers.Where(s => s.Id == "NSVR002").ToArray();
|
||||
filtered.Length.ShouldBe(1);
|
||||
filtered[0].Name.ShouldBe("s2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventServerInfo_FilterByServerId_NoMatch()
|
||||
{
|
||||
// Go: TestServerEventsPingStatsZFailFilter — non-existent server ID returns nothing.
|
||||
var servers = new[]
|
||||
{
|
||||
new EventServerInfo { Name = "s1", Id = "NSVR001" },
|
||||
};
|
||||
|
||||
var filtered = servers.Where(s => s.Id == "NONEXISTENT").ToArray();
|
||||
filtered.Length.ShouldBe(0);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Event JSON roundtrip via source-generated context
|
||||
// Go reference: events_test.go TestServerEventsReceivedByQSubs
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ConnectEventMsg_RoundTrip_ViaContext()
|
||||
{
|
||||
// Go: TestServerEventsReceivedByQSubs — events received and parsed correctly.
|
||||
var original = new ConnectEventMsg
|
||||
{
|
||||
Id = "roundtrip-001",
|
||||
Time = new DateTime(2024, 6, 15, 12, 0, 0, DateTimeKind.Utc),
|
||||
Server = new EventServerInfo { Name = "s1", Id = "NSVR001" },
|
||||
Client = new EventClientInfo { Id = 42, Account = "$G", User = "alice" },
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(original, EventJsonContext.Default.ConnectEventMsg);
|
||||
var deserialized = JsonSerializer.Deserialize(json, EventJsonContext.Default.ConnectEventMsg);
|
||||
|
||||
deserialized.ShouldNotBeNull();
|
||||
deserialized!.Id.ShouldBe("roundtrip-001");
|
||||
deserialized.Type.ShouldBe(ConnectEventMsg.EventType);
|
||||
deserialized.Server.Name.ShouldBe("s1");
|
||||
deserialized.Client.Id.ShouldBe(42UL);
|
||||
deserialized.Client.Account.ShouldBe("$G");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisconnectEventMsg_RoundTrip_ViaContext()
|
||||
{
|
||||
var original = new DisconnectEventMsg
|
||||
{
|
||||
Id = "roundtrip-002",
|
||||
Time = DateTime.UtcNow,
|
||||
Server = new EventServerInfo { Name = "s1", Id = "NSVR001" },
|
||||
Client = new EventClientInfo { Id = 99 },
|
||||
Sent = new DataStats { Msgs = 100, Bytes = 1024 },
|
||||
Received = new DataStats { Msgs = 50, Bytes = 512 },
|
||||
Reason = "Client Closed",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(original, EventJsonContext.Default.DisconnectEventMsg);
|
||||
var deserialized = JsonSerializer.Deserialize(json, EventJsonContext.Default.DisconnectEventMsg);
|
||||
|
||||
deserialized.ShouldNotBeNull();
|
||||
deserialized!.Reason.ShouldBe("Client Closed");
|
||||
deserialized.Sent.Msgs.ShouldBe(100);
|
||||
deserialized.Received.Bytes.ShouldBe(512);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerStatsMsg_RoundTrip_ViaContext()
|
||||
{
|
||||
var original = new ServerStatsMsg
|
||||
{
|
||||
Server = new EventServerInfo { Name = "s1", Id = "NSVR001", JetStream = true },
|
||||
Stats = new ServerStatsData
|
||||
{
|
||||
Mem = 134217728,
|
||||
Cores = 8,
|
||||
Connections = 10,
|
||||
Subscriptions = 42,
|
||||
Sent = new DataStats { Msgs = 1000, Bytes = 65536 },
|
||||
Received = new DataStats { Msgs = 500, Bytes = 32768 },
|
||||
},
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(original, EventJsonContext.Default.ServerStatsMsg);
|
||||
var deserialized = JsonSerializer.Deserialize(json, EventJsonContext.Default.ServerStatsMsg);
|
||||
|
||||
deserialized.ShouldNotBeNull();
|
||||
deserialized!.Server.JetStream.ShouldBeTrue();
|
||||
deserialized.Stats.Mem.ShouldBe(134217728);
|
||||
deserialized.Stats.Connections.ShouldBe(10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AccountNumConns_RoundTrip_ViaContext()
|
||||
{
|
||||
var original = new AccountNumConns
|
||||
{
|
||||
Id = "roundtrip-004",
|
||||
Time = DateTime.UtcNow,
|
||||
Server = new EventServerInfo { Name = "s1", Id = "NSVR001" },
|
||||
AccountName = "$G",
|
||||
Connections = 5,
|
||||
TotalConnections = 20,
|
||||
NumSubscriptions = 15,
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(original, EventJsonContext.Default.AccountNumConns);
|
||||
var deserialized = JsonSerializer.Deserialize(json, EventJsonContext.Default.AccountNumConns);
|
||||
|
||||
deserialized.ShouldNotBeNull();
|
||||
deserialized!.AccountName.ShouldBe("$G");
|
||||
deserialized.Connections.ShouldBe(5);
|
||||
deserialized.TotalConnections.ShouldBe(20);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuthErrorEventMsg_RoundTrip_ViaContext()
|
||||
{
|
||||
var original = new AuthErrorEventMsg
|
||||
{
|
||||
Id = "roundtrip-005",
|
||||
Time = DateTime.UtcNow,
|
||||
Server = new EventServerInfo { Name = "s1", Id = "NSVR001" },
|
||||
Client = new EventClientInfo { Id = 99, Host = "10.0.0.1" },
|
||||
Reason = "Authorization Violation",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(original, EventJsonContext.Default.AuthErrorEventMsg);
|
||||
var deserialized = JsonSerializer.Deserialize(json, EventJsonContext.Default.AuthErrorEventMsg);
|
||||
|
||||
deserialized.ShouldNotBeNull();
|
||||
deserialized!.Reason.ShouldBe("Authorization Violation");
|
||||
deserialized.Type.ShouldBe(AuthErrorEventMsg.EventType);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Event subject $SYS prefix validation
|
||||
// Go reference: events.go — all system subjects start with $SYS
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void AllEventSubjects_StartWithSysDollarPrefix()
|
||||
{
|
||||
// Go: All system event subjects must start with $SYS.
|
||||
EventSubjects.ConnectEvent.ShouldStartWith("$SYS.");
|
||||
EventSubjects.DisconnectEvent.ShouldStartWith("$SYS.");
|
||||
EventSubjects.AccountConnsNew.ShouldStartWith("$SYS.");
|
||||
EventSubjects.AccountConnsOld.ShouldStartWith("$SYS.");
|
||||
EventSubjects.ServerStats.ShouldStartWith("$SYS.");
|
||||
EventSubjects.ServerShutdown.ShouldStartWith("$SYS.");
|
||||
EventSubjects.ServerLameDuck.ShouldStartWith("$SYS.");
|
||||
EventSubjects.AuthError.ShouldStartWith("$SYS.");
|
||||
EventSubjects.AuthErrorAccount.ShouldStartWith("$SYS.");
|
||||
EventSubjects.ServerPing.ShouldStartWith("$SYS.");
|
||||
EventSubjects.ServerReq.ShouldStartWith("$SYS.");
|
||||
EventSubjects.AccountReq.ShouldStartWith("$SYS.");
|
||||
EventSubjects.InboxResponse.ShouldStartWith("$SYS.");
|
||||
}
|
||||
}
|
||||
469
tests/NATS.Server.Monitoring.Tests/Events/EventPayloadTests.cs
Normal file
469
tests/NATS.Server.Monitoring.Tests/Events/EventPayloadTests.cs
Normal file
@@ -0,0 +1,469 @@
|
||||
using System.Text.Json;
|
||||
using NATS.Server.Events;
|
||||
|
||||
namespace NATS.Server.Monitoring.Tests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Tests that all event DTOs have complete JSON fields matching Go's output.
|
||||
/// Go reference: events.go:100-300 — TypedEvent, ServerInfo, ClientInfo,
|
||||
/// DataStats, ServerStats, ConnectEventMsg, DisconnectEventMsg, AccountNumConns.
|
||||
/// </summary>
|
||||
public class EventPayloadTests
|
||||
{
|
||||
// --- EventServerInfo ---
|
||||
|
||||
[Fact]
|
||||
public void EventServerInfo_serializes_all_fields_matching_Go()
|
||||
{
|
||||
var info = new EventServerInfo
|
||||
{
|
||||
Name = "test-server",
|
||||
Host = "127.0.0.1",
|
||||
Id = "ABCDEF123456",
|
||||
Cluster = "test-cluster",
|
||||
Domain = "test-domain",
|
||||
Version = "2.10.0",
|
||||
Tags = ["tag1", "tag2"],
|
||||
Metadata = new Dictionary<string, string> { ["env"] = "test" },
|
||||
JetStream = true,
|
||||
Flags = 1,
|
||||
Seq = 42,
|
||||
Time = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(info);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
root.GetProperty("name").GetString().ShouldBe("test-server");
|
||||
root.GetProperty("host").GetString().ShouldBe("127.0.0.1");
|
||||
root.GetProperty("id").GetString().ShouldBe("ABCDEF123456");
|
||||
root.GetProperty("cluster").GetString().ShouldBe("test-cluster");
|
||||
root.GetProperty("domain").GetString().ShouldBe("test-domain");
|
||||
root.GetProperty("ver").GetString().ShouldBe("2.10.0");
|
||||
root.GetProperty("tags").GetArrayLength().ShouldBe(2);
|
||||
root.GetProperty("metadata").GetProperty("env").GetString().ShouldBe("test");
|
||||
root.GetProperty("jetstream").GetBoolean().ShouldBeTrue();
|
||||
root.GetProperty("flags").GetUInt64().ShouldBe(1UL);
|
||||
root.GetProperty("seq").GetUInt64().ShouldBe(42UL);
|
||||
root.GetProperty("time").GetDateTime().Year.ShouldBe(2025);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventServerInfo_omits_null_optional_fields()
|
||||
{
|
||||
var info = new EventServerInfo
|
||||
{
|
||||
Name = "s",
|
||||
Id = "ID",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(info);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
root.TryGetProperty("cluster", out _).ShouldBeFalse();
|
||||
root.TryGetProperty("domain", out _).ShouldBeFalse();
|
||||
root.TryGetProperty("tags", out _).ShouldBeFalse();
|
||||
root.TryGetProperty("metadata", out _).ShouldBeFalse();
|
||||
}
|
||||
|
||||
// --- EventClientInfo ---
|
||||
|
||||
[Fact]
|
||||
public void EventClientInfo_serializes_all_fields_matching_Go()
|
||||
{
|
||||
var ci = new EventClientInfo
|
||||
{
|
||||
Start = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
Stop = new DateTime(2025, 1, 1, 1, 0, 0, DateTimeKind.Utc),
|
||||
Host = "10.0.0.1",
|
||||
Id = 99,
|
||||
Account = "$G",
|
||||
Service = "orders",
|
||||
User = "admin",
|
||||
Name = "my-client",
|
||||
Lang = "go",
|
||||
Version = "1.30.0",
|
||||
RttNanos = 5_000_000, // 5ms
|
||||
Server = "srv-1",
|
||||
Cluster = "cluster-east",
|
||||
Alternates = ["alt1", "alt2"],
|
||||
Jwt = "eyJ...",
|
||||
IssuerKey = "OABC...",
|
||||
NameTag = "test-tag",
|
||||
Tags = ["dev"],
|
||||
Kind = "Client",
|
||||
ClientType = "nats",
|
||||
MqttClient = "mqtt-abc",
|
||||
Nonce = "nonce123",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(ci);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
root.GetProperty("host").GetString().ShouldBe("10.0.0.1");
|
||||
root.GetProperty("id").GetUInt64().ShouldBe(99UL);
|
||||
root.GetProperty("acc").GetString().ShouldBe("$G");
|
||||
root.GetProperty("svc").GetString().ShouldBe("orders");
|
||||
root.GetProperty("user").GetString().ShouldBe("admin");
|
||||
root.GetProperty("name").GetString().ShouldBe("my-client");
|
||||
root.GetProperty("lang").GetString().ShouldBe("go");
|
||||
root.GetProperty("ver").GetString().ShouldBe("1.30.0");
|
||||
root.GetProperty("rtt").GetInt64().ShouldBe(5_000_000);
|
||||
root.GetProperty("server").GetString().ShouldBe("srv-1");
|
||||
root.GetProperty("cluster").GetString().ShouldBe("cluster-east");
|
||||
root.GetProperty("alts").GetArrayLength().ShouldBe(2);
|
||||
root.GetProperty("jwt").GetString().ShouldBe("eyJ...");
|
||||
root.GetProperty("issuer_key").GetString().ShouldBe("OABC...");
|
||||
root.GetProperty("name_tag").GetString().ShouldBe("test-tag");
|
||||
root.GetProperty("tags").GetArrayLength().ShouldBe(1);
|
||||
root.GetProperty("kind").GetString().ShouldBe("Client");
|
||||
root.GetProperty("client_type").GetString().ShouldBe("nats");
|
||||
root.GetProperty("client_id").GetString().ShouldBe("mqtt-abc");
|
||||
root.GetProperty("nonce").GetString().ShouldBe("nonce123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventClientInfo_omits_null_optional_fields()
|
||||
{
|
||||
var ci = new EventClientInfo { Id = 1 };
|
||||
var json = JsonSerializer.Serialize(ci);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
root.TryGetProperty("svc", out _).ShouldBeFalse();
|
||||
root.TryGetProperty("user", out _).ShouldBeFalse();
|
||||
root.TryGetProperty("server", out _).ShouldBeFalse();
|
||||
root.TryGetProperty("cluster", out _).ShouldBeFalse();
|
||||
root.TryGetProperty("alts", out _).ShouldBeFalse();
|
||||
root.TryGetProperty("jwt", out _).ShouldBeFalse();
|
||||
root.TryGetProperty("issuer_key", out _).ShouldBeFalse();
|
||||
root.TryGetProperty("nonce", out _).ShouldBeFalse();
|
||||
}
|
||||
|
||||
// --- DataStats ---
|
||||
|
||||
[Fact]
|
||||
public void DataStats_serializes_with_optional_sub_stats()
|
||||
{
|
||||
var ds = new DataStats
|
||||
{
|
||||
Msgs = 100,
|
||||
Bytes = 2048,
|
||||
Gateways = new MsgBytesStats { Msgs = 10, Bytes = 256 },
|
||||
Routes = new MsgBytesStats { Msgs = 50, Bytes = 1024 },
|
||||
Leafs = new MsgBytesStats { Msgs = 40, Bytes = 768 },
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(ds);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
root.GetProperty("msgs").GetInt64().ShouldBe(100);
|
||||
root.GetProperty("bytes").GetInt64().ShouldBe(2048);
|
||||
root.GetProperty("gateways").GetProperty("msgs").GetInt64().ShouldBe(10);
|
||||
root.GetProperty("routes").GetProperty("bytes").GetInt64().ShouldBe(1024);
|
||||
root.GetProperty("leafs").GetProperty("msgs").GetInt64().ShouldBe(40);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataStats_omits_null_sub_stats()
|
||||
{
|
||||
var ds = new DataStats { Msgs = 5, Bytes = 50 };
|
||||
var json = JsonSerializer.Serialize(ds);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
root.TryGetProperty("gateways", out _).ShouldBeFalse();
|
||||
root.TryGetProperty("routes", out _).ShouldBeFalse();
|
||||
root.TryGetProperty("leafs", out _).ShouldBeFalse();
|
||||
}
|
||||
|
||||
// --- ConnectEventMsg ---
|
||||
|
||||
[Fact]
|
||||
public void ConnectEventMsg_has_correct_type_and_required_fields()
|
||||
{
|
||||
var evt = new ConnectEventMsg
|
||||
{
|
||||
Id = "evt-1",
|
||||
Time = DateTime.UtcNow,
|
||||
Server = new EventServerInfo { Name = "s1", Id = "SRV1" },
|
||||
Client = new EventClientInfo { Id = 42, Name = "test-client" },
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
root.GetProperty("type").GetString().ShouldBe("io.nats.server.advisory.v1.client_connect");
|
||||
root.GetProperty("id").GetString().ShouldBe("evt-1");
|
||||
root.GetProperty("server").GetProperty("name").GetString().ShouldBe("s1");
|
||||
root.GetProperty("client").GetProperty("id").GetUInt64().ShouldBe(42UL);
|
||||
}
|
||||
|
||||
// --- DisconnectEventMsg ---
|
||||
|
||||
[Fact]
|
||||
public void DisconnectEventMsg_has_correct_type_and_data_stats()
|
||||
{
|
||||
var evt = new DisconnectEventMsg
|
||||
{
|
||||
Id = "evt-2",
|
||||
Time = DateTime.UtcNow,
|
||||
Server = new EventServerInfo { Name = "s1", Id = "SRV1" },
|
||||
Client = new EventClientInfo { Id = 42 },
|
||||
Sent = new DataStats { Msgs = 100, Bytes = 2000 },
|
||||
Received = new DataStats { Msgs = 50, Bytes = 1000 },
|
||||
Reason = "Client Closed",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
root.GetProperty("type").GetString().ShouldBe("io.nats.server.advisory.v1.client_disconnect");
|
||||
root.GetProperty("sent").GetProperty("msgs").GetInt64().ShouldBe(100);
|
||||
root.GetProperty("received").GetProperty("bytes").GetInt64().ShouldBe(1000);
|
||||
root.GetProperty("reason").GetString().ShouldBe("Client Closed");
|
||||
}
|
||||
|
||||
// --- AccountNumConns ---
|
||||
|
||||
[Fact]
|
||||
public void AccountNumConns_serializes_all_Go_AccountStat_fields()
|
||||
{
|
||||
var evt = new AccountNumConns
|
||||
{
|
||||
Id = "evt-3",
|
||||
Time = DateTime.UtcNow,
|
||||
Server = new EventServerInfo { Name = "s1", Id = "SRV1" },
|
||||
AccountName = "$G",
|
||||
Name = "Global",
|
||||
Connections = 5,
|
||||
LeafNodes = 2,
|
||||
TotalConnections = 100,
|
||||
NumSubscriptions = 42,
|
||||
Sent = new DataStats { Msgs = 500, Bytes = 10_000 },
|
||||
Received = new DataStats { Msgs = 400, Bytes = 8_000 },
|
||||
SlowConsumers = 1,
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
root.GetProperty("type").GetString().ShouldBe("io.nats.server.advisory.v1.account_connections");
|
||||
root.GetProperty("acc").GetString().ShouldBe("$G");
|
||||
root.GetProperty("name").GetString().ShouldBe("Global");
|
||||
root.GetProperty("conns").GetInt32().ShouldBe(5);
|
||||
root.GetProperty("leafnodes").GetInt32().ShouldBe(2);
|
||||
root.GetProperty("total_conns").GetInt32().ShouldBe(100);
|
||||
root.GetProperty("num_subscriptions").GetUInt32().ShouldBe(42u);
|
||||
root.GetProperty("sent").GetProperty("msgs").GetInt64().ShouldBe(500);
|
||||
root.GetProperty("received").GetProperty("bytes").GetInt64().ShouldBe(8_000);
|
||||
root.GetProperty("slow_consumers").GetInt64().ShouldBe(1);
|
||||
}
|
||||
|
||||
// --- ServerStatsMsg ---
|
||||
|
||||
[Fact]
|
||||
public void ServerStatsMsg_has_sent_received_and_breakdown_fields()
|
||||
{
|
||||
var msg = new ServerStatsMsg
|
||||
{
|
||||
Server = new EventServerInfo { Name = "s1", Id = "SRV1", Seq = 1 },
|
||||
Stats = new ServerStatsData
|
||||
{
|
||||
Start = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
Mem = 100_000_000,
|
||||
Cores = 8,
|
||||
Cpu = 12.5,
|
||||
Connections = 10,
|
||||
TotalConnections = 500,
|
||||
ActiveAccounts = 3,
|
||||
Subscriptions = 50,
|
||||
Sent = new DataStats { Msgs = 1000, Bytes = 50_000 },
|
||||
Received = new DataStats { Msgs = 800, Bytes = 40_000 },
|
||||
InMsgs = 800,
|
||||
OutMsgs = 1000,
|
||||
InBytes = 40_000,
|
||||
OutBytes = 50_000,
|
||||
SlowConsumers = 2,
|
||||
SlowConsumerStats = new NATS.Server.Events.SlowConsumersStats { Clients = 1, Routes = 1 },
|
||||
StaleConnections = 3,
|
||||
StaleConnectionStats = new NATS.Server.Events.StaleConnectionStats { Clients = 2, Leafs = 1 },
|
||||
ActiveServers = 3,
|
||||
Routes = [new RouteStat { Id = 1, Name = "r1", Sent = new DataStats { Msgs = 10 }, Received = new DataStats { Msgs = 5 }, Pending = 0 }],
|
||||
Gateways = [new GatewayStat { Id = 1, Name = "gw1", Sent = new DataStats { Msgs = 20 }, Received = new DataStats { Msgs = 15 }, InboundConnections = 2 }],
|
||||
},
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(msg);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
var stats = root.GetProperty("statsz");
|
||||
|
||||
stats.GetProperty("mem").GetInt64().ShouldBe(100_000_000);
|
||||
stats.GetProperty("cores").GetInt32().ShouldBe(8);
|
||||
stats.GetProperty("cpu").GetDouble().ShouldBe(12.5);
|
||||
stats.GetProperty("connections").GetInt32().ShouldBe(10);
|
||||
stats.GetProperty("total_connections").GetInt64().ShouldBe(500);
|
||||
stats.GetProperty("active_accounts").GetInt32().ShouldBe(3);
|
||||
stats.GetProperty("subscriptions").GetInt64().ShouldBe(50);
|
||||
stats.GetProperty("sent").GetProperty("msgs").GetInt64().ShouldBe(1000);
|
||||
stats.GetProperty("received").GetProperty("bytes").GetInt64().ShouldBe(40_000);
|
||||
stats.GetProperty("in_msgs").GetInt64().ShouldBe(800);
|
||||
stats.GetProperty("out_msgs").GetInt64().ShouldBe(1000);
|
||||
stats.GetProperty("slow_consumers").GetInt64().ShouldBe(2);
|
||||
stats.GetProperty("slow_consumer_stats").GetProperty("clients").GetInt64().ShouldBe(1);
|
||||
stats.GetProperty("stale_connections").GetInt64().ShouldBe(3);
|
||||
stats.GetProperty("stale_connection_stats").GetProperty("leafs").GetInt64().ShouldBe(1);
|
||||
stats.GetProperty("active_servers").GetInt32().ShouldBe(3);
|
||||
stats.GetProperty("routes").GetArrayLength().ShouldBe(1);
|
||||
stats.GetProperty("routes")[0].GetProperty("rid").GetUInt64().ShouldBe(1UL);
|
||||
stats.GetProperty("gateways").GetArrayLength().ShouldBe(1);
|
||||
stats.GetProperty("gateways")[0].GetProperty("name").GetString().ShouldBe("gw1");
|
||||
}
|
||||
|
||||
// --- AuthErrorEventMsg ---
|
||||
|
||||
[Fact]
|
||||
public void AuthErrorEventMsg_has_correct_type()
|
||||
{
|
||||
var evt = new AuthErrorEventMsg
|
||||
{
|
||||
Id = "evt-4",
|
||||
Time = DateTime.UtcNow,
|
||||
Server = new EventServerInfo { Name = "s1", Id = "SRV1" },
|
||||
Client = new EventClientInfo { Id = 99, Host = "10.0.0.1" },
|
||||
Reason = "Authorization Violation",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
root.GetProperty("type").GetString().ShouldBe("io.nats.server.advisory.v1.client_auth");
|
||||
root.GetProperty("reason").GetString().ShouldBe("Authorization Violation");
|
||||
root.GetProperty("client").GetProperty("host").GetString().ShouldBe("10.0.0.1");
|
||||
}
|
||||
|
||||
// --- OcspPeerRejectEventMsg ---
|
||||
|
||||
[Fact]
|
||||
public void OcspPeerRejectEventMsg_has_correct_type()
|
||||
{
|
||||
var evt = new OcspPeerRejectEventMsg
|
||||
{
|
||||
Id = "evt-5",
|
||||
Time = DateTime.UtcNow,
|
||||
Kind = "client",
|
||||
Server = new EventServerInfo { Name = "s1", Id = "SRV1" },
|
||||
Reason = "OCSP revoked",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
root.GetProperty("type").GetString().ShouldBe("io.nats.server.advisory.v1.ocsp_peer_reject");
|
||||
root.GetProperty("kind").GetString().ShouldBe("client");
|
||||
root.GetProperty("reason").GetString().ShouldBe("OCSP revoked");
|
||||
}
|
||||
|
||||
// --- ShutdownEventMsg ---
|
||||
|
||||
[Fact]
|
||||
public void ShutdownEventMsg_serializes_reason()
|
||||
{
|
||||
var evt = new ShutdownEventMsg
|
||||
{
|
||||
Server = new EventServerInfo { Name = "s1", Id = "SRV1" },
|
||||
Reason = "Server Shutdown",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(evt);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
doc.RootElement.GetProperty("reason").GetString().ShouldBe("Server Shutdown");
|
||||
}
|
||||
|
||||
// --- AccNumConnsReq ---
|
||||
|
||||
[Fact]
|
||||
public void AccNumConnsReq_serializes_account()
|
||||
{
|
||||
var req = new AccNumConnsReq
|
||||
{
|
||||
Server = new EventServerInfo { Name = "s1", Id = "SRV1" },
|
||||
Account = "myAccount",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(req);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
doc.RootElement.GetProperty("acc").GetString().ShouldBe("myAccount");
|
||||
}
|
||||
|
||||
// --- Round-trip deserialization ---
|
||||
|
||||
[Fact]
|
||||
public void ConnectEventMsg_roundtrips_through_json()
|
||||
{
|
||||
var original = new ConnectEventMsg
|
||||
{
|
||||
Id = "rt-1",
|
||||
Time = new DateTime(2025, 6, 15, 12, 0, 0, DateTimeKind.Utc),
|
||||
Server = new EventServerInfo { Name = "srv", Id = "SRV1", Version = "2.10.0", Seq = 5 },
|
||||
Client = new EventClientInfo
|
||||
{
|
||||
Id = 42,
|
||||
Host = "10.0.0.1",
|
||||
Account = "$G",
|
||||
Name = "test",
|
||||
Lang = "dotnet",
|
||||
Version = "1.0.0",
|
||||
RttNanos = 1_000_000,
|
||||
Kind = "Client",
|
||||
},
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(original);
|
||||
var deserialized = JsonSerializer.Deserialize<ConnectEventMsg>(json);
|
||||
|
||||
deserialized.ShouldNotBeNull();
|
||||
deserialized.Type.ShouldBe(ConnectEventMsg.EventType);
|
||||
deserialized.Id.ShouldBe("rt-1");
|
||||
deserialized.Server.Name.ShouldBe("srv");
|
||||
deserialized.Server.Seq.ShouldBe(5UL);
|
||||
deserialized.Client.Id.ShouldBe(42UL);
|
||||
deserialized.Client.Kind.ShouldBe("Client");
|
||||
deserialized.Client.RttNanos.ShouldBe(1_000_000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerStatsMsg_roundtrips_through_json()
|
||||
{
|
||||
var original = new ServerStatsMsg
|
||||
{
|
||||
Server = new EventServerInfo { Name = "srv", Id = "SRV1" },
|
||||
Stats = new ServerStatsData
|
||||
{
|
||||
Connections = 10,
|
||||
Sent = new DataStats { Msgs = 100, Bytes = 5000 },
|
||||
Received = new DataStats { Msgs = 80, Bytes = 4000 },
|
||||
InMsgs = 80,
|
||||
OutMsgs = 100,
|
||||
},
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(original);
|
||||
var deserialized = JsonSerializer.Deserialize<ServerStatsMsg>(json);
|
||||
|
||||
deserialized.ShouldNotBeNull();
|
||||
deserialized.Stats.Connections.ShouldBe(10);
|
||||
deserialized.Stats.Sent.Msgs.ShouldBe(100);
|
||||
deserialized.Stats.Received.Bytes.ShouldBe(4000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Text.Json;
|
||||
using NATS.Server.Events;
|
||||
|
||||
namespace NATS.Server.Monitoring.Tests.Events;
|
||||
|
||||
public class EventServerInfoCapabilityParityBatch1Tests
|
||||
{
|
||||
[Fact]
|
||||
public void ServerCapability_flags_match_expected_values()
|
||||
{
|
||||
((ulong)ServerCapability.JetStreamEnabled).ShouldBe(1UL << 0);
|
||||
((ulong)ServerCapability.BinaryStreamSnapshot).ShouldBe(1UL << 1);
|
||||
((ulong)ServerCapability.AccountNRG).ShouldBe(1UL << 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventServerInfo_capability_methods_set_and_read_flags()
|
||||
{
|
||||
var info = new EventServerInfo();
|
||||
|
||||
info.SetJetStreamEnabled();
|
||||
info.SetBinaryStreamSnapshot();
|
||||
info.SetAccountNRG();
|
||||
|
||||
info.JetStream.ShouldBeTrue();
|
||||
info.JetStreamEnabled().ShouldBeTrue();
|
||||
info.BinaryStreamSnapshot().ShouldBeTrue();
|
||||
info.AccountNRG().ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerID_serializes_with_name_host_id_fields()
|
||||
{
|
||||
var payload = new ServerID
|
||||
{
|
||||
Name = "srv-a",
|
||||
Host = "127.0.0.1",
|
||||
Id = "N1",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
json.ShouldContain("\"name\":\"srv-a\"");
|
||||
json.ShouldContain("\"host\":\"127.0.0.1\"");
|
||||
json.ShouldContain("\"id\":\"N1\"");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
using System.Text.Json;
|
||||
using NATS.Server.Events;
|
||||
|
||||
namespace NATS.Server.Monitoring.Tests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Tests that EventBuilder produces fully-populated system event messages
|
||||
/// and that all field contracts are met.
|
||||
/// Go reference: events.go sendConnectEventMsg, sendDisconnectEventMsg,
|
||||
/// sendAccountNumConns, sendStatsz — Gap 10.6.
|
||||
/// </summary>
|
||||
public class FullEventPayloadTests
|
||||
{
|
||||
// ========================================================================
|
||||
// BuildConnectEvent
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void BuildConnectEvent_AllFieldsPopulated()
|
||||
{
|
||||
// Go reference: events.go sendConnectEventMsg — all client fields must be set.
|
||||
var evt = EventBuilder.BuildConnectEvent(
|
||||
serverId: "SRV-001",
|
||||
serverName: "test-server",
|
||||
cluster: "east-cluster",
|
||||
clientId: 42,
|
||||
host: "10.0.0.1",
|
||||
account: "$G",
|
||||
user: "alice",
|
||||
name: "my-client",
|
||||
lang: "csharp",
|
||||
version: "1.0.0");
|
||||
|
||||
evt.ShouldNotBeNull();
|
||||
evt.Id.ShouldNotBeNullOrEmpty();
|
||||
evt.Time.ShouldBeGreaterThan(DateTime.MinValue);
|
||||
evt.Server.Id.ShouldBe("SRV-001");
|
||||
evt.Server.Name.ShouldBe("test-server");
|
||||
evt.Server.Cluster.ShouldBe("east-cluster");
|
||||
evt.Client.Id.ShouldBe(42UL);
|
||||
evt.Client.Host.ShouldBe("10.0.0.1");
|
||||
evt.Client.Account.ShouldBe("$G");
|
||||
evt.Client.User.ShouldBe("alice");
|
||||
evt.Client.Name.ShouldBe("my-client");
|
||||
evt.Client.Lang.ShouldBe("csharp");
|
||||
evt.Client.Version.ShouldBe("1.0.0");
|
||||
evt.Client.Start.ShouldBeGreaterThan(DateTime.MinValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildConnectEvent_HasCorrectEventType()
|
||||
{
|
||||
// Go reference: events.go — connect advisory type constant.
|
||||
var evt = EventBuilder.BuildConnectEvent(
|
||||
serverId: "SRV-001",
|
||||
serverName: "test-server",
|
||||
cluster: null,
|
||||
clientId: 1,
|
||||
host: "127.0.0.1",
|
||||
account: null,
|
||||
user: null,
|
||||
name: null,
|
||||
lang: null,
|
||||
version: null);
|
||||
|
||||
evt.Type.ShouldBe(ConnectEventMsg.EventType);
|
||||
evt.Type.ShouldBe("io.nats.server.advisory.v1.client_connect");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// BuildDisconnectEvent
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void BuildDisconnectEvent_AllFieldsPopulated()
|
||||
{
|
||||
// Go reference: events.go sendDisconnectEventMsg — sent, received, and reason required.
|
||||
var sent = new DataStats { Msgs = 100, Bytes = 2_048 };
|
||||
var received = new DataStats { Msgs = 50, Bytes = 1_024 };
|
||||
|
||||
var evt = EventBuilder.BuildDisconnectEvent(
|
||||
serverId: "SRV-002",
|
||||
serverName: "test-server",
|
||||
cluster: "west-cluster",
|
||||
clientId: 99,
|
||||
host: "192.168.1.5",
|
||||
account: "MYACC",
|
||||
user: "bob",
|
||||
reason: "Client Closed",
|
||||
sent: sent,
|
||||
received: received);
|
||||
|
||||
evt.ShouldNotBeNull();
|
||||
evt.Id.ShouldNotBeNullOrEmpty();
|
||||
evt.Time.ShouldBeGreaterThan(DateTime.MinValue);
|
||||
evt.Server.Id.ShouldBe("SRV-002");
|
||||
evt.Server.Name.ShouldBe("test-server");
|
||||
evt.Server.Cluster.ShouldBe("west-cluster");
|
||||
evt.Client.Id.ShouldBe(99UL);
|
||||
evt.Client.Host.ShouldBe("192.168.1.5");
|
||||
evt.Client.Account.ShouldBe("MYACC");
|
||||
evt.Client.User.ShouldBe("bob");
|
||||
evt.Client.Stop.ShouldBeGreaterThan(DateTime.MinValue);
|
||||
evt.Reason.ShouldBe("Client Closed");
|
||||
evt.Sent.Msgs.ShouldBe(100);
|
||||
evt.Sent.Bytes.ShouldBe(2_048);
|
||||
evt.Received.Msgs.ShouldBe(50);
|
||||
evt.Received.Bytes.ShouldBe(1_024);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildDisconnectEvent_HasCorrectEventType()
|
||||
{
|
||||
// Go reference: events.go — disconnect advisory type constant.
|
||||
var evt = EventBuilder.BuildDisconnectEvent(
|
||||
serverId: "SRV-002",
|
||||
serverName: "test-server",
|
||||
cluster: null,
|
||||
clientId: 1,
|
||||
host: "127.0.0.1",
|
||||
account: null,
|
||||
user: null,
|
||||
reason: "Server Closed",
|
||||
sent: new DataStats(),
|
||||
received: new DataStats());
|
||||
|
||||
evt.Type.ShouldBe(DisconnectEventMsg.EventType);
|
||||
evt.Type.ShouldBe("io.nats.server.advisory.v1.client_disconnect");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// BuildAccountConnsEvent
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void BuildAccountConnsEvent_AllFieldsPopulated()
|
||||
{
|
||||
// Go reference: events.go sendAccountNumConns — all AccountStat fields must transfer.
|
||||
var sent = new DataStats { Msgs = 500, Bytes = 10_000 };
|
||||
var received = new DataStats { Msgs = 400, Bytes = 8_000 };
|
||||
|
||||
var evt = EventBuilder.BuildAccountConnsEvent(
|
||||
serverId: "SRV-003",
|
||||
serverName: "test-server",
|
||||
accountName: "ACCT-A",
|
||||
connections: 7,
|
||||
leafNodes: 2,
|
||||
totalConnections: 150,
|
||||
numSubscriptions: 33,
|
||||
sent: sent,
|
||||
received: received,
|
||||
slowConsumers: 3);
|
||||
|
||||
evt.ShouldNotBeNull();
|
||||
evt.Id.ShouldNotBeNullOrEmpty();
|
||||
evt.Time.ShouldBeGreaterThan(DateTime.MinValue);
|
||||
evt.Server.Id.ShouldBe("SRV-003");
|
||||
evt.Server.Name.ShouldBe("test-server");
|
||||
evt.AccountName.ShouldBe("ACCT-A");
|
||||
evt.Connections.ShouldBe(7);
|
||||
evt.LeafNodes.ShouldBe(2);
|
||||
evt.TotalConnections.ShouldBe(150);
|
||||
evt.NumSubscriptions.ShouldBe(33u);
|
||||
evt.Sent.Msgs.ShouldBe(500);
|
||||
evt.Received.Bytes.ShouldBe(8_000);
|
||||
evt.SlowConsumers.ShouldBe(3);
|
||||
evt.Type.ShouldBe(AccountNumConns.EventType);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// BuildServerStats
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void BuildServerStats_AllFieldsPopulated()
|
||||
{
|
||||
// Go reference: events.go sendStatsz — all stat fields must be set.
|
||||
var sent = new DataStats { Msgs = 1_000, Bytes = 50_000 };
|
||||
var received = new DataStats { Msgs = 800, Bytes = 40_000 };
|
||||
|
||||
var msg = EventBuilder.BuildServerStats(
|
||||
serverId: "SRV-004",
|
||||
serverName: "stats-server",
|
||||
mem: 134_217_728,
|
||||
cores: 8,
|
||||
cpu: 15.5,
|
||||
connections: 12,
|
||||
totalConnections: 600,
|
||||
activeAccounts: 4,
|
||||
subscriptions: 55,
|
||||
sent: sent,
|
||||
received: received);
|
||||
|
||||
msg.ShouldNotBeNull();
|
||||
msg.Server.Id.ShouldBe("SRV-004");
|
||||
msg.Server.Name.ShouldBe("stats-server");
|
||||
msg.Stats.Mem.ShouldBe(134_217_728);
|
||||
msg.Stats.Cores.ShouldBe(8);
|
||||
msg.Stats.Cpu.ShouldBe(15.5);
|
||||
msg.Stats.Connections.ShouldBe(12);
|
||||
msg.Stats.TotalConnections.ShouldBe(600);
|
||||
msg.Stats.ActiveAccounts.ShouldBe(4);
|
||||
msg.Stats.Subscriptions.ShouldBe(55);
|
||||
msg.Stats.Sent.Msgs.ShouldBe(1_000);
|
||||
msg.Stats.Received.Bytes.ShouldBe(40_000);
|
||||
// Flat counters mirror the structured stats
|
||||
msg.Stats.OutMsgs.ShouldBe(1_000);
|
||||
msg.Stats.InMsgs.ShouldBe(800);
|
||||
msg.Stats.OutBytes.ShouldBe(50_000);
|
||||
msg.Stats.InBytes.ShouldBe(40_000);
|
||||
msg.Stats.Start.ShouldBeGreaterThan(DateTime.MinValue);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// GenerateEventId
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void GenerateEventId_UniquePerCall()
|
||||
{
|
||||
// Go reference: events.go uses nuid.Next() — every call produces a distinct ID.
|
||||
var ids = Enumerable.Range(0, 20).Select(_ => EventBuilder.GenerateEventId()).ToList();
|
||||
|
||||
ids.Distinct().Count().ShouldBe(20);
|
||||
foreach (var id in ids)
|
||||
{
|
||||
id.ShouldNotBeNullOrEmpty();
|
||||
id.Length.ShouldBe(32); // Guid.ToString("N") is always 32 hex chars
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// GetTimestamp
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void GetTimestamp_ReturnsIso8601()
|
||||
{
|
||||
// Go reference: events use RFC3339Nano timestamps; .NET "O" format is ISO 8601.
|
||||
var ts = EventBuilder.GetTimestamp();
|
||||
|
||||
ts.ShouldNotBeNullOrEmpty();
|
||||
// "O" format: 2025-02-25T12:34:56.7890000Z — parseable as UTC DateTimeOffset
|
||||
var parsed = DateTimeOffset.Parse(ts);
|
||||
parsed.Year.ShouldBeGreaterThanOrEqualTo(2024);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// DataStats default values
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void DataStats_DefaultZeroValues()
|
||||
{
|
||||
// Go reference: zero-value DataStats is valid and all fields default to 0.
|
||||
var ds = new DataStats();
|
||||
|
||||
ds.Msgs.ShouldBe(0L);
|
||||
ds.Bytes.ShouldBe(0L);
|
||||
ds.Gateways.ShouldBeNull();
|
||||
ds.Routes.ShouldBeNull();
|
||||
ds.Leafs.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// JSON roundtrip
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ConnectEventMsg_SerializesToJson()
|
||||
{
|
||||
// Go reference: events.go — connect event serializes to JSON and round-trips.
|
||||
var original = EventBuilder.BuildConnectEvent(
|
||||
serverId: "SRV-RT",
|
||||
serverName: "roundtrip-server",
|
||||
cluster: "rt-cluster",
|
||||
clientId: 77,
|
||||
host: "10.1.2.3",
|
||||
account: "RTACC",
|
||||
user: "rtuser",
|
||||
name: "rt-client",
|
||||
lang: "dotnet",
|
||||
version: "2.0.0");
|
||||
|
||||
var json = JsonSerializer.Serialize(original);
|
||||
|
||||
json.ShouldNotBeNullOrEmpty();
|
||||
json.ShouldContain("\"type\":");
|
||||
json.ShouldContain(ConnectEventMsg.EventType);
|
||||
json.ShouldContain("\"server\":");
|
||||
json.ShouldContain("\"client\":");
|
||||
json.ShouldContain("\"SRV-RT\"");
|
||||
|
||||
var deserialized = JsonSerializer.Deserialize<ConnectEventMsg>(json);
|
||||
|
||||
deserialized.ShouldNotBeNull();
|
||||
deserialized!.Type.ShouldBe(ConnectEventMsg.EventType);
|
||||
deserialized.Id.ShouldBe(original.Id);
|
||||
deserialized.Server.Id.ShouldBe("SRV-RT");
|
||||
deserialized.Server.Name.ShouldBe("roundtrip-server");
|
||||
deserialized.Server.Cluster.ShouldBe("rt-cluster");
|
||||
deserialized.Client.Id.ShouldBe(77UL);
|
||||
deserialized.Client.Account.ShouldBe("RTACC");
|
||||
deserialized.Client.User.ShouldBe("rtuser");
|
||||
deserialized.Client.Lang.ShouldBe("dotnet");
|
||||
deserialized.Client.Version.ShouldBe("2.0.0");
|
||||
}
|
||||
}
|
||||
266
tests/NATS.Server.Monitoring.Tests/Events/OcspEventTests.cs
Normal file
266
tests/NATS.Server.Monitoring.Tests/Events/OcspEventTests.cs
Normal file
@@ -0,0 +1,266 @@
|
||||
// Port of Go server/ocsp_test.go — OCSP peer reject and chain validation advisory tests.
|
||||
// Go reference: golang/nats-server/server/ocsp.go — postOCSPPeerRejectEvent,
|
||||
// OCSP chain validation advisory publishing (Gap 10.10).
|
||||
//
|
||||
// Tests cover: OcspPeerRejectEventMsg, OcspChainValidationEvent, OcspEventBuilder,
|
||||
// OcspStatus enum, subject constants, and JSON serialisation round-trip.
|
||||
|
||||
using System.Text.Json;
|
||||
using NATS.Server.Events;
|
||||
|
||||
namespace NATS.Server.Monitoring.Tests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="OcspPeerRejectEventMsg"/>, <see cref="OcspChainValidationEvent"/>,
|
||||
/// <see cref="OcspEventBuilder"/>, and the <see cref="OcspStatus"/> enum.
|
||||
/// Go reference: ocsp.go — OCSP peer verification advisory events (Gap 10.10).
|
||||
/// </summary>
|
||||
public class OcspEventTests
|
||||
{
|
||||
// ========================================================================
|
||||
// OcspPeerRejectEventMsg
|
||||
// Go reference: ocsp.go postOCSPPeerRejectEvent
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// EventType constant must match the Go advisory type string.
|
||||
/// Go reference: ocsp.go — "io.nats.server.advisory.v1.ocsp_peer_reject".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OcspPeerRejectEventMsg_HasCorrectEventType()
|
||||
{
|
||||
// Go reference: ocsp.go — advisory type constant.
|
||||
OcspPeerRejectEventMsg.EventType.ShouldBe("io.nats.server.advisory.v1.ocsp_peer_reject");
|
||||
var ev = new OcspPeerRejectEventMsg();
|
||||
ev.Type.ShouldBe(OcspPeerRejectEventMsg.EventType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Each OcspPeerRejectEventMsg gets its own unique non-empty Id.
|
||||
/// Go reference: ocsp.go — nuid.Next() generates a unique event ID per advisory.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OcspPeerRejectEventMsg_GeneratesUniqueId()
|
||||
{
|
||||
// Go reference: ocsp.go — each advisory is assigned a unique ID.
|
||||
var ev1 = new OcspPeerRejectEventMsg { Id = EventBuilder.GenerateEventId() };
|
||||
var ev2 = new OcspPeerRejectEventMsg { Id = EventBuilder.GenerateEventId() };
|
||||
ev1.Id.ShouldNotBeNullOrEmpty();
|
||||
ev2.Id.ShouldNotBeNullOrEmpty();
|
||||
ev1.Id.ShouldNotBe(ev2.Id);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// OcspChainValidationEvent
|
||||
// Go reference: ocsp.go — OCSP chain validation advisory
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// EventType constant must match the advisory type string.
|
||||
/// Go reference: ocsp.go — "io.nats.server.advisory.v1.ocsp_chain_validation".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OcspChainValidationEvent_HasCorrectEventType()
|
||||
{
|
||||
// Go reference: ocsp.go — chain validation advisory type constant.
|
||||
OcspChainValidationEvent.EventType.ShouldBe("io.nats.server.advisory.v1.ocsp_chain_validation");
|
||||
var ev = new OcspChainValidationEvent();
|
||||
ev.Type.ShouldBe(OcspChainValidationEvent.EventType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All optional fields on OcspChainValidationEvent can be assigned.
|
||||
/// Go reference: ocsp.go — chain validation advisory carries cert metadata.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OcspChainValidationEvent_AllFieldsSettable()
|
||||
{
|
||||
// Go reference: ocsp.go — OCSP advisory fields: subject, issuer, serial, status.
|
||||
var server = new EventServerInfo { Id = "srv1", Name = "test-server" };
|
||||
var checkedAt = new DateTime(2026, 2, 25, 12, 0, 0, DateTimeKind.Utc);
|
||||
var ev = new OcspChainValidationEvent
|
||||
{
|
||||
Server = server,
|
||||
CertSubject = "CN=leaf.example.com",
|
||||
CertIssuer = "CN=Intermediate CA",
|
||||
SerialNumber = "0123456789abcdef",
|
||||
OcspStatus = "good",
|
||||
CheckedAt = checkedAt,
|
||||
Error = null,
|
||||
};
|
||||
|
||||
ev.Server.ShouldBeSameAs(server);
|
||||
ev.CertSubject.ShouldBe("CN=leaf.example.com");
|
||||
ev.CertIssuer.ShouldBe("CN=Intermediate CA");
|
||||
ev.SerialNumber.ShouldBe("0123456789abcdef");
|
||||
ev.OcspStatus.ShouldBe("good");
|
||||
ev.CheckedAt.ShouldBe(checkedAt);
|
||||
ev.Error.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// OcspEventBuilder
|
||||
// Go reference: ocsp.go — postOCSPPeerRejectEvent, chain validation advisory
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// BuildPeerReject populates all required fields of OcspPeerRejectEventMsg.
|
||||
/// Go reference: ocsp.go postOCSPPeerRejectEvent — kind and reason always set.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildPeerReject_PopulatesAllFields()
|
||||
{
|
||||
// Go reference: ocsp.go — peer reject advisory carries kind + reason.
|
||||
var ev = OcspEventBuilder.BuildPeerReject(
|
||||
serverId: "srv-abc",
|
||||
serverName: "my-server",
|
||||
kind: "client",
|
||||
reason: "certificate revoked");
|
||||
|
||||
ev.Id.ShouldNotBeNullOrEmpty();
|
||||
ev.Type.ShouldBe(OcspPeerRejectEventMsg.EventType);
|
||||
ev.Server.ShouldNotBeNull();
|
||||
ev.Server.Id.ShouldBe("srv-abc");
|
||||
ev.Server.Name.ShouldBe("my-server");
|
||||
ev.Kind.ShouldBe("client");
|
||||
ev.Reason.ShouldBe("certificate revoked");
|
||||
ev.Time.ShouldNotBe(default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BuildChainValidation populates all fields of OcspChainValidationEvent.
|
||||
/// Go reference: ocsp.go — chain validation advisory carries full cert metadata.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildChainValidation_PopulatesAllFields()
|
||||
{
|
||||
// Go reference: ocsp.go — chain validation advisory fields.
|
||||
var before = DateTime.UtcNow;
|
||||
var ev = OcspEventBuilder.BuildChainValidation(
|
||||
serverId: "srv-xyz",
|
||||
serverName: "nats-1",
|
||||
certSubject: "CN=client.example.com",
|
||||
certIssuer: "CN=Root CA",
|
||||
serialNumber: "deadbeef",
|
||||
ocspStatus: "good",
|
||||
error: null);
|
||||
var after = DateTime.UtcNow;
|
||||
|
||||
ev.Id.ShouldNotBeNullOrEmpty();
|
||||
ev.Type.ShouldBe(OcspChainValidationEvent.EventType);
|
||||
ev.Server.ShouldNotBeNull();
|
||||
ev.Server!.Id.ShouldBe("srv-xyz");
|
||||
ev.Server.Name.ShouldBe("nats-1");
|
||||
ev.CertSubject.ShouldBe("CN=client.example.com");
|
||||
ev.CertIssuer.ShouldBe("CN=Root CA");
|
||||
ev.SerialNumber.ShouldBe("deadbeef");
|
||||
ev.OcspStatus.ShouldBe("good");
|
||||
ev.CheckedAt.ShouldNotBeNull();
|
||||
ev.CheckedAt!.Value.ShouldBeInRange(before, after);
|
||||
ev.Error.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// OcspStatus enum — ParseStatus
|
||||
// Go reference: ocsp.go — ocspStatusGood, ocspStatusRevoked constants
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// ParseStatus("good") returns OcspStatus.Good.
|
||||
/// Go reference: ocsp.go — ocspStatusGood = "good".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParseStatus_Good()
|
||||
{
|
||||
// Go reference: ocsp.go — ocspStatusGood.
|
||||
OcspEventBuilder.ParseStatus("good").ShouldBe(OcspStatus.Good);
|
||||
OcspEventBuilder.ParseStatus("Good").ShouldBe(OcspStatus.Good);
|
||||
OcspEventBuilder.ParseStatus("GOOD").ShouldBe(OcspStatus.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ParseStatus("revoked") returns OcspStatus.Revoked.
|
||||
/// Go reference: ocsp.go — ocspStatusRevoked = "revoked".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParseStatus_Revoked()
|
||||
{
|
||||
// Go reference: ocsp.go — ocspStatusRevoked.
|
||||
OcspEventBuilder.ParseStatus("revoked").ShouldBe(OcspStatus.Revoked);
|
||||
OcspEventBuilder.ParseStatus("Revoked").ShouldBe(OcspStatus.Revoked);
|
||||
OcspEventBuilder.ParseStatus("REVOKED").ShouldBe(OcspStatus.Revoked);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ParseStatus with null or unrecognised string returns OcspStatus.Unknown.
|
||||
/// Go reference: ocsp.go — unknown/unrecognised OCSP status treated as unknown.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParseStatus_Unknown()
|
||||
{
|
||||
// Go reference: ocsp.go — default case maps to unknown status.
|
||||
OcspEventBuilder.ParseStatus(null).ShouldBe(OcspStatus.Unknown);
|
||||
OcspEventBuilder.ParseStatus("unknown").ShouldBe(OcspStatus.Unknown);
|
||||
OcspEventBuilder.ParseStatus("").ShouldBe(OcspStatus.Unknown);
|
||||
OcspEventBuilder.ParseStatus("invalid-status").ShouldBe(OcspStatus.Unknown);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// JSON serialisation round-trip
|
||||
// Go reference: ocsp.go — advisories are published as JSON payloads
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// OcspPeerRejectEventMsg and OcspChainValidationEvent round-trip through JSON
|
||||
/// preserving all set fields.
|
||||
/// Go reference: ocsp.go — OCSP advisories serialised as JSON before publishing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OcspEvents_SerializeToJson()
|
||||
{
|
||||
// Go reference: ocsp.go — advisories are JSON-encoded for publishing.
|
||||
var peerReject = OcspEventBuilder.BuildPeerReject(
|
||||
serverId: "srv-1",
|
||||
serverName: "node-a",
|
||||
kind: "route",
|
||||
reason: "OCSP status revoked");
|
||||
|
||||
var chainValidation = OcspEventBuilder.BuildChainValidation(
|
||||
serverId: "srv-2",
|
||||
serverName: "node-b",
|
||||
certSubject: "CN=server.nats.io",
|
||||
certIssuer: "CN=NATS CA",
|
||||
serialNumber: "cafebabe",
|
||||
ocspStatus: "revoked",
|
||||
error: "certificate has been revoked");
|
||||
|
||||
// Serialise OcspPeerRejectEventMsg
|
||||
var rejectJson = JsonSerializer.Serialize(peerReject);
|
||||
rejectJson.ShouldContain("io.nats.server.advisory.v1.ocsp_peer_reject");
|
||||
rejectJson.ShouldContain("route");
|
||||
rejectJson.ShouldContain("OCSP status revoked");
|
||||
|
||||
var rejectDeserialized = JsonSerializer.Deserialize<OcspPeerRejectEventMsg>(rejectJson);
|
||||
rejectDeserialized.ShouldNotBeNull();
|
||||
rejectDeserialized!.Type.ShouldBe(OcspPeerRejectEventMsg.EventType);
|
||||
rejectDeserialized.Kind.ShouldBe("route");
|
||||
rejectDeserialized.Reason.ShouldBe("OCSP status revoked");
|
||||
rejectDeserialized.Server.Id.ShouldBe("srv-1");
|
||||
rejectDeserialized.Server.Name.ShouldBe("node-a");
|
||||
|
||||
// Serialise OcspChainValidationEvent
|
||||
var chainJson = JsonSerializer.Serialize(chainValidation);
|
||||
chainJson.ShouldContain("io.nats.server.advisory.v1.ocsp_chain_validation");
|
||||
chainJson.ShouldContain("CN=server.nats.io");
|
||||
chainJson.ShouldContain("revoked");
|
||||
|
||||
var chainDeserialized = JsonSerializer.Deserialize<OcspChainValidationEvent>(chainJson);
|
||||
chainDeserialized.ShouldNotBeNull();
|
||||
chainDeserialized!.Type.ShouldBe(OcspChainValidationEvent.EventType);
|
||||
chainDeserialized.CertSubject.ShouldBe("CN=server.nats.io");
|
||||
chainDeserialized.CertIssuer.ShouldBe("CN=NATS CA");
|
||||
chainDeserialized.SerialNumber.ShouldBe("cafebabe");
|
||||
chainDeserialized.OcspStatus.ShouldBe("revoked");
|
||||
chainDeserialized.Error.ShouldBe("certificate has been revoked");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
using System.Text.Json;
|
||||
using NATS.Server.Events;
|
||||
|
||||
namespace NATS.Server.Monitoring.Tests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for remote server and leaf node event DTOs and subject constants.
|
||||
/// Go reference: events.go — remote server lifecycle, leaf node advisory subjects.
|
||||
/// Gap 10.8: RemoteServerShutdown, RemoteServerUpdate, LeafNodeConnected events.
|
||||
/// </summary>
|
||||
public class RemoteServerEventTests
|
||||
{
|
||||
// --- RemoteServerShutdownEvent ---
|
||||
|
||||
[Fact]
|
||||
public void RemoteServerShutdownEvent_HasCorrectEventType()
|
||||
{
|
||||
RemoteServerShutdownEvent.EventType.ShouldBe("io.nats.server.advisory.v1.remote_shutdown");
|
||||
var ev = new RemoteServerShutdownEvent();
|
||||
ev.Type.ShouldBe(RemoteServerShutdownEvent.EventType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoteServerShutdownEvent_GeneratesUniqueId()
|
||||
{
|
||||
var ev1 = new RemoteServerShutdownEvent();
|
||||
var ev2 = new RemoteServerShutdownEvent();
|
||||
ev1.Id.ShouldNotBeNullOrEmpty();
|
||||
ev2.Id.ShouldNotBeNullOrEmpty();
|
||||
ev1.Id.ShouldNotBe(ev2.Id);
|
||||
}
|
||||
|
||||
// --- RemoteServerUpdateEvent ---
|
||||
|
||||
[Fact]
|
||||
public void RemoteServerUpdateEvent_HasCorrectEventType()
|
||||
{
|
||||
RemoteServerUpdateEvent.EventType.ShouldBe("io.nats.server.advisory.v1.remote_update");
|
||||
var ev = new RemoteServerUpdateEvent();
|
||||
ev.Type.ShouldBe(RemoteServerUpdateEvent.EventType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoteServerUpdateEvent_AllFieldsSettable()
|
||||
{
|
||||
var server = new EventServerInfo { Id = "srv1", Name = "my-server" };
|
||||
var ev = new RemoteServerUpdateEvent
|
||||
{
|
||||
Server = server,
|
||||
RemoteServerId = "remote-id-123",
|
||||
RemoteServerName = "remote-server",
|
||||
UpdateType = "routes_changed",
|
||||
};
|
||||
|
||||
ev.Server.ShouldBeSameAs(server);
|
||||
ev.RemoteServerId.ShouldBe("remote-id-123");
|
||||
ev.RemoteServerName.ShouldBe("remote-server");
|
||||
ev.UpdateType.ShouldBe("routes_changed");
|
||||
}
|
||||
|
||||
// --- LeafNodeConnectEvent ---
|
||||
|
||||
[Fact]
|
||||
public void LeafNodeConnectEvent_HasCorrectEventType()
|
||||
{
|
||||
LeafNodeConnectEvent.EventType.ShouldBe("io.nats.server.advisory.v1.leafnode_connect");
|
||||
var ev = new LeafNodeConnectEvent();
|
||||
ev.Type.ShouldBe(LeafNodeConnectEvent.EventType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeafNodeConnectEvent_AllFieldsSettable()
|
||||
{
|
||||
var server = new EventServerInfo { Id = "srv1", Name = "hub" };
|
||||
var ev = new LeafNodeConnectEvent
|
||||
{
|
||||
Server = server,
|
||||
LeafNodeId = "leaf-id-abc",
|
||||
LeafNodeName = "leaf-node-1",
|
||||
RemoteUrl = "nats://10.0.0.1:7422",
|
||||
Account = "ACC",
|
||||
};
|
||||
|
||||
ev.Server.ShouldBeSameAs(server);
|
||||
ev.LeafNodeId.ShouldBe("leaf-id-abc");
|
||||
ev.LeafNodeName.ShouldBe("leaf-node-1");
|
||||
ev.RemoteUrl.ShouldBe("nats://10.0.0.1:7422");
|
||||
ev.Account.ShouldBe("ACC");
|
||||
}
|
||||
|
||||
// --- LeafNodeDisconnectEvent ---
|
||||
|
||||
[Fact]
|
||||
public void LeafNodeDisconnectEvent_HasCorrectEventType()
|
||||
{
|
||||
LeafNodeDisconnectEvent.EventType.ShouldBe("io.nats.server.advisory.v1.leafnode_disconnect");
|
||||
var ev = new LeafNodeDisconnectEvent();
|
||||
ev.Type.ShouldBe(LeafNodeDisconnectEvent.EventType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeafNodeDisconnectEvent_AllFieldsSettable()
|
||||
{
|
||||
var server = new EventServerInfo { Id = "srv1", Name = "hub" };
|
||||
var ev = new LeafNodeDisconnectEvent
|
||||
{
|
||||
Server = server,
|
||||
LeafNodeId = "leaf-id-xyz",
|
||||
Reason = "connection closed",
|
||||
};
|
||||
|
||||
ev.Server.ShouldBeSameAs(server);
|
||||
ev.LeafNodeId.ShouldBe("leaf-id-xyz");
|
||||
ev.Reason.ShouldBe("connection closed");
|
||||
}
|
||||
|
||||
// --- EventSubjects ---
|
||||
|
||||
[Fact]
|
||||
public void EventSubjects_RemoteShutdown_HasPlaceholder()
|
||||
{
|
||||
EventSubjects.RemoteServerShutdown.ShouldContain("{0}");
|
||||
EventSubjects.RemoteServerUpdate.ShouldContain("{0}");
|
||||
EventSubjects.LeafNodeConnected.ShouldContain("{0}");
|
||||
EventSubjects.LeafNodeDisconnected.ShouldContain("{0}");
|
||||
|
||||
// Verify format strings produce expected subjects when formatted
|
||||
var serverId = "ABCDEF123456";
|
||||
string.Format(EventSubjects.RemoteServerShutdown, serverId)
|
||||
.ShouldBe($"$SYS.SERVER.{serverId}.REMOTE.SHUTDOWN");
|
||||
string.Format(EventSubjects.LeafNodeConnected, serverId)
|
||||
.ShouldBe($"$SYS.ACCOUNT.{serverId}.LEAFNODE.CONNECT");
|
||||
}
|
||||
|
||||
// --- JSON serialization ---
|
||||
|
||||
[Fact]
|
||||
public void AllRemoteEvents_SerializeToJson()
|
||||
{
|
||||
var server = new EventServerInfo { Id = "srv-id", Name = "test-server" };
|
||||
|
||||
var shutdown = new RemoteServerShutdownEvent
|
||||
{
|
||||
Server = server,
|
||||
RemoteServerId = "r1",
|
||||
RemoteServerName = "remote",
|
||||
Reason = "graceful",
|
||||
};
|
||||
var shutdownJson = JsonSerializer.Serialize(shutdown);
|
||||
var shutdownDoc = JsonDocument.Parse(shutdownJson).RootElement;
|
||||
shutdownDoc.GetProperty("type").GetString().ShouldBe(RemoteServerShutdownEvent.EventType);
|
||||
shutdownDoc.GetProperty("id").GetString().ShouldNotBeNullOrEmpty();
|
||||
shutdownDoc.GetProperty("remote_server_id").GetString().ShouldBe("r1");
|
||||
shutdownDoc.GetProperty("reason").GetString().ShouldBe("graceful");
|
||||
|
||||
var update = new RemoteServerUpdateEvent
|
||||
{
|
||||
Server = server,
|
||||
RemoteServerId = "r2",
|
||||
RemoteServerName = "remote2",
|
||||
UpdateType = "config_updated",
|
||||
};
|
||||
var updateJson = JsonSerializer.Serialize(update);
|
||||
var updateDoc = JsonDocument.Parse(updateJson).RootElement;
|
||||
updateDoc.GetProperty("type").GetString().ShouldBe(RemoteServerUpdateEvent.EventType);
|
||||
updateDoc.GetProperty("update_type").GetString().ShouldBe("config_updated");
|
||||
|
||||
var connect = new LeafNodeConnectEvent
|
||||
{
|
||||
Server = server,
|
||||
LeafNodeId = "leaf1",
|
||||
LeafNodeName = "leaf-node",
|
||||
RemoteUrl = "nats://10.0.0.1:7422",
|
||||
Account = "ACC",
|
||||
};
|
||||
var connectJson = JsonSerializer.Serialize(connect);
|
||||
var connectDoc = JsonDocument.Parse(connectJson).RootElement;
|
||||
connectDoc.GetProperty("type").GetString().ShouldBe(LeafNodeConnectEvent.EventType);
|
||||
connectDoc.GetProperty("leaf_node_id").GetString().ShouldBe("leaf1");
|
||||
connectDoc.GetProperty("account").GetString().ShouldBe("ACC");
|
||||
|
||||
var disconnect = new LeafNodeDisconnectEvent
|
||||
{
|
||||
Server = server,
|
||||
LeafNodeId = "leaf1",
|
||||
Reason = "timeout",
|
||||
};
|
||||
var disconnectJson = JsonSerializer.Serialize(disconnect);
|
||||
var disconnectDoc = JsonDocument.Parse(disconnectJson).RootElement;
|
||||
disconnectDoc.GetProperty("type").GetString().ShouldBe(LeafNodeDisconnectEvent.EventType);
|
||||
disconnectDoc.GetProperty("leaf_node_id").GetString().ShouldBe("leaf1");
|
||||
disconnectDoc.GetProperty("reason").GetString().ShouldBe("timeout");
|
||||
|
||||
// Roundtrip: deserialize back and verify type field survives
|
||||
var shutdownRt = JsonSerializer.Deserialize<RemoteServerShutdownEvent>(shutdownJson);
|
||||
shutdownRt.ShouldNotBeNull();
|
||||
shutdownRt!.Type.ShouldBe(RemoteServerShutdownEvent.EventType);
|
||||
shutdownRt.RemoteServerId.ShouldBe("r1");
|
||||
}
|
||||
}
|
||||
421
tests/NATS.Server.Monitoring.Tests/Events/ServerEventTests.cs
Normal file
421
tests/NATS.Server.Monitoring.Tests/Events/ServerEventTests.cs
Normal file
@@ -0,0 +1,421 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server;
|
||||
using NATS.Server.Auth;
|
||||
using NATS.Server.Events;
|
||||
using NATS.Server.TestUtilities;
|
||||
|
||||
namespace NATS.Server.Monitoring.Tests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for server lifecycle events, stats tracking, advisory messages, and
|
||||
/// $SYS subject infrastructure.
|
||||
/// Go reference: events_test.go (51 tests).
|
||||
/// </summary>
|
||||
public class ServerEventTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NatsServer _server;
|
||||
private readonly int _port;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
public ServerEventTests()
|
||||
{
|
||||
_port = TestPortAllocator.GetFreePort();
|
||||
_server = new NatsServer(new NatsOptions { Port = _port }, NullLoggerFactory.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_ = _server.StartAsync(_cts.Token);
|
||||
await _server.WaitForReadyAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
_server.Dispose();
|
||||
}
|
||||
|
||||
|
||||
private async Task<Socket> ConnectAndHandshakeAsync()
|
||||
{
|
||||
var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await sock.ConnectAsync(IPAddress.Loopback, _port);
|
||||
// Read INFO
|
||||
var buf = new byte[4096];
|
||||
await sock.ReceiveAsync(buf, SocketFlags.None);
|
||||
// Send CONNECT + PING
|
||||
await sock.SendAsync(Encoding.ASCII.GetBytes("CONNECT {}\r\nPING\r\n"));
|
||||
// Read PONG (may include -ERR or other lines)
|
||||
await SocketTestHelper.ReadUntilAsync(sock, "PONG");
|
||||
return sock;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Server lifecycle events
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Server exposes Stats property at startup with all counters at zero.
|
||||
/// Go reference: events_test.go TestServerEventsStatsZ (line ~100).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Server_stats_initialized_to_zero_at_startup()
|
||||
{
|
||||
var stats = _server.Stats;
|
||||
stats.InMsgs.ShouldBe(0L);
|
||||
stats.OutMsgs.ShouldBe(0L);
|
||||
stats.InBytes.ShouldBe(0L);
|
||||
stats.OutBytes.ShouldBe(0L);
|
||||
stats.SlowConsumers.ShouldBe(0L);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TotalConnections increments each time a new client connects.
|
||||
/// Go reference: events_test.go TestServerEventsTotalConnections (line ~150).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task TotalConnections_increments_on_each_new_connection()
|
||||
{
|
||||
var before = Interlocked.Read(ref _server.Stats.TotalConnections);
|
||||
|
||||
using var c1 = await ConnectAndHandshakeAsync();
|
||||
using var c2 = await ConnectAndHandshakeAsync();
|
||||
|
||||
var after = Interlocked.Read(ref _server.Stats.TotalConnections);
|
||||
(after - before).ShouldBeGreaterThanOrEqualTo(2L);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ClientCount reflects only currently connected clients.
|
||||
/// Go reference: events_test.go TestServerEventsStatsCID (line ~200).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ClientCount_decrements_when_client_disconnects()
|
||||
{
|
||||
var sock = await ConnectAndHandshakeAsync();
|
||||
var countWhileConnected = _server.ClientCount;
|
||||
countWhileConnected.ShouldBeGreaterThanOrEqualTo(1);
|
||||
|
||||
sock.Shutdown(SocketShutdown.Both);
|
||||
sock.Dispose();
|
||||
|
||||
// Allow server time to process the disconnection
|
||||
await Task.Delay(100);
|
||||
_server.ClientCount.ShouldBeLessThan(countWhileConnected + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiple simultaneous connections are tracked independently.
|
||||
/// Go reference: events_test.go TestServerEventsConcurrentConns (line ~230).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Multiple_connections_tracked_independently()
|
||||
{
|
||||
var before = Interlocked.Read(ref _server.Stats.TotalConnections);
|
||||
|
||||
using var c1 = await ConnectAndHandshakeAsync();
|
||||
using var c2 = await ConnectAndHandshakeAsync();
|
||||
using var c3 = await ConnectAndHandshakeAsync();
|
||||
|
||||
var after = Interlocked.Read(ref _server.Stats.TotalConnections);
|
||||
(after - before).ShouldBeGreaterThanOrEqualTo(3L);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stats are accurate after rapid connect/disconnect cycles.
|
||||
/// Go reference: events_test.go TestServerEventsStatsCounting (line ~260).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Stats_accurate_after_rapid_connect_disconnect()
|
||||
{
|
||||
var before = Interlocked.Read(ref _server.Stats.TotalConnections);
|
||||
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
using var sock = await ConnectAndHandshakeAsync();
|
||||
}
|
||||
|
||||
var after = Interlocked.Read(ref _server.Stats.TotalConnections);
|
||||
(after - before).ShouldBeGreaterThanOrEqualTo(5L);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ServerStats counters — message/byte tracking
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// InMsgs and InBytes increment when clients publish.
|
||||
/// Go reference: events_test.go TestServerEventsStatsz (line ~100).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task InMsgs_and_InBytes_increment_on_publish()
|
||||
{
|
||||
using var sock = await ConnectAndHandshakeAsync();
|
||||
|
||||
var beforeMsgs = Interlocked.Read(ref _server.Stats.InMsgs);
|
||||
var beforeBytes = Interlocked.Read(ref _server.Stats.InBytes);
|
||||
|
||||
var payload = "Hello"u8.ToArray();
|
||||
var pub = Encoding.ASCII.GetBytes($"PUB test.subject {payload.Length}\r\nHello\r\n");
|
||||
await sock.SendAsync(pub);
|
||||
// Flush via PING/PONG
|
||||
await sock.SendAsync(Encoding.ASCII.GetBytes("PING\r\n"));
|
||||
await SocketTestHelper.ReadUntilAsync(sock, "PONG");
|
||||
|
||||
var afterMsgs = Interlocked.Read(ref _server.Stats.InMsgs);
|
||||
var afterBytes = Interlocked.Read(ref _server.Stats.InBytes);
|
||||
|
||||
(afterMsgs - beforeMsgs).ShouldBeGreaterThanOrEqualTo(1L);
|
||||
(afterBytes - beforeBytes).ShouldBeGreaterThanOrEqualTo(payload.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OutMsgs and OutBytes increment when messages are delivered to subscribers.
|
||||
/// Go reference: events_test.go TestServerEventsStatsz (line ~100).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OutMsgs_and_OutBytes_increment_on_delivery()
|
||||
{
|
||||
using var sub = await ConnectAndHandshakeAsync();
|
||||
using var pub = await ConnectAndHandshakeAsync();
|
||||
|
||||
// Subscribe
|
||||
await sub.SendAsync(Encoding.ASCII.GetBytes("SUB test.out 1\r\nPING\r\n"));
|
||||
await SocketTestHelper.ReadUntilAsync(sub, "PONG");
|
||||
|
||||
var beforeOut = Interlocked.Read(ref _server.Stats.OutMsgs);
|
||||
|
||||
var payload = "World"u8.ToArray();
|
||||
await pub.SendAsync(Encoding.ASCII.GetBytes($"PUB test.out {payload.Length}\r\nWorld\r\n"));
|
||||
await pub.SendAsync(Encoding.ASCII.GetBytes("PING\r\n"));
|
||||
await SocketTestHelper.ReadUntilAsync(pub, "PONG");
|
||||
|
||||
// Give delivery loop time to flush
|
||||
await SocketTestHelper.ReadUntilAsync(sub, "World", timeoutMs: 2000);
|
||||
|
||||
var afterOut = Interlocked.Read(ref _server.Stats.OutMsgs);
|
||||
(afterOut - beforeOut).ShouldBeGreaterThanOrEqualTo(1L);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Account stats events
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Account.InMsgs and InBytes track messages received by clients in that account.
|
||||
/// Go reference: events_test.go TestServerEventsStatsz (line ~100),
|
||||
/// TestAccountStats (line ~400).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Account_InMsgs_and_InBytes_increment_correctly()
|
||||
{
|
||||
// Account.IncrementInbound is the mechanism tracked server-side
|
||||
var account = new Account("test-account");
|
||||
account.IncrementInbound(3, 300);
|
||||
account.InMsgs.ShouldBe(3L);
|
||||
account.InBytes.ShouldBe(300L);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Account.OutMsgs and OutBytes track messages delivered to clients in that account.
|
||||
/// Go reference: events_test.go TestAccountStats (line ~400).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Account_OutMsgs_and_OutBytes_increment_correctly()
|
||||
{
|
||||
var account = new Account("test-account");
|
||||
account.IncrementOutbound(2, 200);
|
||||
account.OutMsgs.ShouldBe(2L);
|
||||
account.OutBytes.ShouldBe(200L);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-account stats are isolated — changes to one account do not affect another.
|
||||
/// Go reference: events_test.go TestAccountStats, TestServerEventsAccountIsolation (line ~420).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Account_stats_are_isolated_between_accounts()
|
||||
{
|
||||
var a1 = new Account("account-one");
|
||||
var a2 = new Account("account-two");
|
||||
|
||||
a1.IncrementInbound(10, 1000);
|
||||
a2.IncrementInbound(5, 500);
|
||||
|
||||
a1.InMsgs.ShouldBe(10L);
|
||||
a2.InMsgs.ShouldBe(5L);
|
||||
a1.InBytes.ShouldBe(1000L);
|
||||
a2.InBytes.ShouldBe(500L);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Account stats start at zero and are independent of each other.
|
||||
/// Go reference: events_test.go TestAccountStats (line ~400).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Account_stats_start_at_zero()
|
||||
{
|
||||
var account = new Account("fresh");
|
||||
account.InMsgs.ShouldBe(0L);
|
||||
account.OutMsgs.ShouldBe(0L);
|
||||
account.InBytes.ShouldBe(0L);
|
||||
account.OutBytes.ShouldBe(0L);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Advisory messages — slow consumers, stale connections
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// ServerStats contains SlowConsumers counter for aggregate slow consumer tracking.
|
||||
/// Go reference: events_test.go TestServerEventsSlowConsumer (line ~500).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Stats_has_SlowConsumers_field()
|
||||
{
|
||||
var stats = _server.Stats;
|
||||
// Field exists and starts at zero
|
||||
Interlocked.Read(ref stats.SlowConsumers).ShouldBe(0L);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ServerStats differentiates slow consumers by connection type.
|
||||
/// Go reference: events_test.go TestServerEventsSlowConsumer (line ~500).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Stats_has_per_type_SlowConsumer_fields()
|
||||
{
|
||||
var stats = _server.Stats;
|
||||
// All per-type slow-consumer counters exist and start at zero
|
||||
Interlocked.Read(ref stats.SlowConsumerClients).ShouldBe(0L);
|
||||
Interlocked.Read(ref stats.SlowConsumerRoutes).ShouldBe(0L);
|
||||
Interlocked.Read(ref stats.SlowConsumerLeafs).ShouldBe(0L);
|
||||
Interlocked.Read(ref stats.SlowConsumerGateways).ShouldBe(0L);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// StaleConnections and per-type stale counters are tracked in ServerStats.
|
||||
/// Go reference: events_test.go TestServerEventsStaleConnection (line ~550).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Stats_has_StaleConnection_fields()
|
||||
{
|
||||
var stats = _server.Stats;
|
||||
Interlocked.Read(ref stats.StaleConnections).ShouldBe(0L);
|
||||
Interlocked.Read(ref stats.StaleConnectionClients).ShouldBe(0L);
|
||||
Interlocked.Read(ref stats.StaleConnectionRoutes).ShouldBe(0L);
|
||||
Interlocked.Read(ref stats.StaleConnectionLeafs).ShouldBe(0L);
|
||||
Interlocked.Read(ref stats.StaleConnectionGateways).ShouldBe(0L);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// JetStream API stats
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// JetStreamApiTotal and JetStreamApiErrors counters exist in ServerStats.
|
||||
/// Go reference: events_test.go TestServerEventsStatsZ JetStream fields (line ~100).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Stats_has_JetStream_api_counters()
|
||||
{
|
||||
var stats = _server.Stats;
|
||||
Interlocked.Read(ref stats.JetStreamApiTotal).ShouldBe(0L);
|
||||
Interlocked.Read(ref stats.JetStreamApiErrors).ShouldBe(0L);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// $SYS subject event infrastructure
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// EventSubjects constants use $SYS prefix matching Go's event subject patterns.
|
||||
/// Go reference: events.go:41-97 subject constants.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EventSubjects_have_correct_SYS_prefixes()
|
||||
{
|
||||
EventSubjects.ConnectEvent.ShouldStartWith("$SYS.ACCOUNT.");
|
||||
EventSubjects.DisconnectEvent.ShouldStartWith("$SYS.ACCOUNT.");
|
||||
EventSubjects.ServerStats.ShouldStartWith("$SYS.SERVER.");
|
||||
EventSubjects.ServerShutdown.ShouldStartWith("$SYS.SERVER.");
|
||||
EventSubjects.AuthError.ShouldStartWith("$SYS.SERVER.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EventSubjects include format placeholders for account and server IDs.
|
||||
/// Go reference: events.go:41-97 format string subject constants.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EventSubjects_format_correctly_with_account_and_server_ids()
|
||||
{
|
||||
var connectSubject = string.Format(EventSubjects.ConnectEvent, "MY_ACCOUNT");
|
||||
connectSubject.ShouldBe("$SYS.ACCOUNT.MY_ACCOUNT.CONNECT");
|
||||
|
||||
var statsSubject = string.Format(EventSubjects.ServerStats, "SERVER123");
|
||||
statsSubject.ShouldBe("$SYS.SERVER.SERVER123.STATSZ");
|
||||
|
||||
var shutdownSubject = string.Format(EventSubjects.ServerShutdown, "SERVER123");
|
||||
shutdownSubject.ShouldBe("$SYS.SERVER.SERVER123.SHUTDOWN");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NatsServer exposes a non-null EventSystem after startup.
|
||||
/// Go reference: events.go initEventTracking — event system initialised during server start.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Server_has_EventSystem_after_start()
|
||||
{
|
||||
_server.EventSystem.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InternalEventSystem.PublishServerStats produces a ServerStatsMsg with server
|
||||
/// identity and current stats data without throwing.
|
||||
/// Go reference: events.go sendStatsz (line ~495).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EventSystem_PublishServerStats_does_not_throw()
|
||||
{
|
||||
var eventSystem = _server.EventSystem;
|
||||
eventSystem.ShouldNotBeNull();
|
||||
|
||||
// Calling PublishServerStats directly must not throw
|
||||
var ex = Record.Exception(() => eventSystem!.PublishServerStats());
|
||||
ex.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InternalEventSystem generates unique, monotonically increasing sequence numbers.
|
||||
/// Go reference: events.go NextSequence / sequence counter (line ~59).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EventSystem_sequence_numbers_are_monotonically_increasing()
|
||||
{
|
||||
var es = _server.EventSystem;
|
||||
es.ShouldNotBeNull();
|
||||
|
||||
var s1 = es!.NextSequence();
|
||||
var s2 = es.NextSequence();
|
||||
var s3 = es.NextSequence();
|
||||
|
||||
s2.ShouldBeGreaterThan(s1);
|
||||
s3.ShouldBeGreaterThan(s2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BuildEventServerInfo embeds the server name and ID in advisory messages.
|
||||
/// Go reference: events.go serverInfo() helper (line ~1368 in NatsServer.cs).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildEventServerInfo_contains_server_identity()
|
||||
{
|
||||
var info = _server.BuildEventServerInfo();
|
||||
info.ShouldNotBeNull();
|
||||
info.Id.ShouldNotBeNullOrWhiteSpace();
|
||||
info.Name.ShouldNotBeNullOrWhiteSpace();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user