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