// 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; /// /// Tests for , /// , /// , and the three /// companion detail record types. /// Go reference: events_test.go TestSystemAccountDisconnectBadLogin, /// TestSystemAccountNewConnection. /// 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 // ======================================================================== /// /// AuthErrorEventCount starts at zero before any advisories are sent. /// Go reference: events_test.go TestSystemAccountDisconnectBadLogin. /// [Fact] public void AuthErrorEventCount_StartsAtZero() { // Go reference: events_test.go TestSystemAccountDisconnectBadLogin — no events at startup. var es = _server.EventSystem!; es.AuthErrorEventCount.ShouldBe(0L); } /// /// Calling SendAuthErrorEvent once increments the counter to 1. /// Go reference: events.go:2631 sendAuthErrorEvent — each call is one advisory. /// [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); } /// /// Each call to SendAuthErrorEvent enqueues a message (counter grows by one per call). /// Go reference: events.go:2687 sendInternalMsg — advisory is always enqueued. /// [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); } /// /// Sending multiple auth error events increments the counter for each. /// Go reference: events.go:2631 sendAuthErrorEvent — cumulative count. /// [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 // ======================================================================== /// /// SendConnectEvent enqueues a message without throwing. /// Go reference: events.go postConnectEvent — advisory fired on client connect. /// [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 // ======================================================================== /// /// SendDisconnectEvent enqueues a message without throwing. /// Go reference: events.go postDisconnectEvent — advisory fired on client disconnect. /// [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 // ======================================================================== /// /// AuthErrorDetail preserves all fields passed to its constructor. /// Go reference: events.go:2631 — all client fields captured in the advisory. /// [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); } /// /// AuthErrorDetail accepts a non-empty Reason (the key advisory field). /// Go reference: events.go:2631 sendAuthErrorEvent — reason is always set. /// [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 // ======================================================================== /// /// ConnectEventDetail preserves all constructor fields. /// Go reference: events.go postConnectEvent — all fields captured on connect. /// [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 // ======================================================================== /// /// DisconnectEventDetail preserves all constructor fields. /// Go reference: events.go postDisconnectEvent — all fields captured on disconnect. /// [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); } }