using System.Text.Json;
using NATS.Server.Events;
namespace NATS.Server.Monitoring.Tests.Events;
///
/// 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.
///
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(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");
}
}