Files
natsdotnet/tests/NATS.Server.Tests/Events/AuthErrorEventTests.cs
Joseph Doherty 4b9384dfcf feat: add auth error event publication (Gap 10.5)
Add SendAuthErrorEvent, SendConnectEvent, SendDisconnectEvent to
InternalEventSystem, plus AuthErrorEventCount counter and the three
companion detail record types (AuthErrorDetail, ConnectEventDetail,
DisconnectEventDetail). 10 new tests in AuthErrorEventTests all pass.
2026-02-25 13:12:52 -05:00

295 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;
namespace NATS.Server.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 = 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();
}
private static int GetFreePort()
{
using var sock = new System.Net.Sockets.Socket(
System.Net.Sockets.AddressFamily.InterNetwork,
System.Net.Sockets.SocketType.Stream,
System.Net.Sockets.ProtocolType.Tcp);
sock.Bind(new System.Net.IPEndPoint(System.Net.IPAddress.Loopback, 0));
return ((System.Net.IPEndPoint)sock.LocalEndPoint!).Port;
}
// ========================================================================
// 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);
}
}