Files
natsdotnet/tests/NATS.Server.Monitoring.Tests/Events/AuthErrorEventTests.cs
Joseph Doherty 0c086522a4 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.
2026-03-12 15:44:12 -04:00

287 lines
11 KiB
C#

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