feat: complete system event payload fields (Gap 10.6)
Add EventBuilder static class to EventTypes.cs with helpers for constructing fully-populated ConnectEventMsg, DisconnectEventMsg, AccountNumConns, and ServerStatsMsg. Also add RemoteServerShutdownEvent, RemoteServerUpdateEvent, LeafNodeConnectEvent, and LeafNodeDisconnectEvent advisory types. Add FullEventPayloadTests.cs (10 tests) covering all builders, GenerateEventId uniqueness, GetTimestamp ISO 8601 format, DataStats zero defaults, and ConnectEventMsg JSON roundtrip.
This commit is contained in:
308
tests/NATS.Server.Tests/Events/FullEventPayloadTests.cs
Normal file
308
tests/NATS.Server.Tests/Events/FullEventPayloadTests.cs
Normal file
@@ -0,0 +1,308 @@
|
||||
using System.Text.Json;
|
||||
using NATS.Server.Events;
|
||||
|
||||
namespace NATS.Server.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");
|
||||
}
|
||||
}
|
||||
152
tests/NATS.Server.Tests/Monitoring/ClosedReasonTests.cs
Normal file
152
tests/NATS.Server.Tests/Monitoring/ClosedReasonTests.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
// Go reference: server/monitor.go ClosedState.String() — reason strings emitted by
|
||||
// the /connz endpoint, and server/auth.go getAuthErrClosedState — auth-related reasons.
|
||||
// These tests verify the ClosedReason enum and ClosedReasonHelper helpers introduced
|
||||
// in Task 89 (Gap 10.7: consistently populate closed connection reasons).
|
||||
|
||||
using NATS.Server.Monitoring;
|
||||
|
||||
namespace NATS.Server.Tests.Monitoring;
|
||||
|
||||
public class ClosedReasonTests
|
||||
{
|
||||
// -----------------------------------------------------------------------
|
||||
// 1. ToReasonString_ClientClosed_ReturnsExpected
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// ClientClosed maps to the Go-compatible "Client Closed" string.
|
||||
/// Go reference: server/monitor.go ClosedState.String() case ClientClosed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ToReasonString_ClientClosed_ReturnsExpected()
|
||||
{
|
||||
ClosedReasonHelper.ToReasonString(ClosedReason.ClientClosed).ShouldBe("Client Closed");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 2. ToReasonString_AllReasonsHaveStrings
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Every ClosedReason enum value must produce a non-null, non-empty string.
|
||||
/// Go reference: server/monitor.go ClosedState.String() — all cases covered.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ToReasonString_AllReasonsHaveStrings()
|
||||
{
|
||||
foreach (var reason in Enum.GetValues<ClosedReason>())
|
||||
{
|
||||
var s = ClosedReasonHelper.ToReasonString(reason);
|
||||
s.ShouldNotBeNull();
|
||||
s.ShouldNotBeEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 3. FromReasonString_ValidString_ReturnsEnum
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// A valid Go-compatible reason string parses back to the correct enum value.
|
||||
/// Go reference: server/monitor.go ClosedState.String() "Server Shutdown".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromReasonString_ValidString_ReturnsEnum()
|
||||
{
|
||||
ClosedReasonHelper.FromReasonString("Server Shutdown").ShouldBe(ClosedReason.ServerShutdown);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 4. FromReasonString_Unknown_ReturnsUnknown
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// An unrecognised string returns ClosedReason.Unknown.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromReasonString_Unknown_ReturnsUnknown()
|
||||
{
|
||||
ClosedReasonHelper.FromReasonString("Not a real reason").ShouldBe(ClosedReason.Unknown);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 5. FromReasonString_Null_ReturnsUnknown
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// A null reason string returns ClosedReason.Unknown.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromReasonString_Null_ReturnsUnknown()
|
||||
{
|
||||
ClosedReasonHelper.FromReasonString(null).ShouldBe(ClosedReason.Unknown);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 6. IsClientInitiated_ClientClosed_True
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// ClientClosed is the only client-initiated close reason.
|
||||
/// Go reference: server/client.go closeConnection — client disconnect path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsClientInitiated_ClientClosed_True()
|
||||
{
|
||||
ClosedReasonHelper.IsClientInitiated(ClosedReason.ClientClosed).ShouldBeTrue();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 7. IsClientInitiated_ServerShutdown_False
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// ServerShutdown is not a client-initiated close.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsClientInitiated_ServerShutdown_False()
|
||||
{
|
||||
ClosedReasonHelper.IsClientInitiated(ClosedReason.ServerShutdown).ShouldBeFalse();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 8. IsAuthRelated_AuthTimeout_True
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// AuthTimeout is auth-related.
|
||||
/// Go reference: server/auth.go getAuthErrClosedState — auth timeout path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsAuthRelated_AuthTimeout_True()
|
||||
{
|
||||
ClosedReasonHelper.IsAuthRelated(ClosedReason.AuthTimeout).ShouldBeTrue();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 9. IsAuthRelated_WriteError_False
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// WriteError is not auth-related.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsAuthRelated_WriteError_False()
|
||||
{
|
||||
ClosedReasonHelper.IsAuthRelated(ClosedReason.WriteError).ShouldBeFalse();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 10. IsResourceLimit_MaxConnections_True
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// MaxConnectionsExceeded is a resource-limit close reason.
|
||||
/// Go reference: server/client.go maxConnectionsExceeded — max connections path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsResourceLimit_MaxConnections_True()
|
||||
{
|
||||
ClosedReasonHelper.IsResourceLimit(ClosedReason.MaxConnectionsExceeded).ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user