// Port of Go server/events_test.go — system event DTO and subject parity tests. // Reference: golang/nats-server/server/events_test.go // // Tests cover: ConnectEventMsg, DisconnectEventMsg, ServerStatsMsg, // AccountNumConns, AuthErrorEventMsg, ShutdownEventMsg serialization, // event subject pattern formatting, event filtering by tag/server ID, // and HealthZ status code mapping. using System.Text.Json; using NATS.Server.Events; namespace NATS.Server.Tests.Events; /// /// Parity tests ported from Go server/events_test.go exercising /// system event DTOs, JSON serialization shapes, event subjects, /// and event filtering logic. /// public class EventGoParityTests { // ======================================================================== // ConnectEventMsg serialization // Go reference: events_test.go TestSystemAccountNewConnection // ======================================================================== [Fact] public void ConnectEventMsg_JsonShape_MatchesGo() { // Go: TestSystemAccountNewConnection — verifies connect event JSON shape. var evt = new ConnectEventMsg { Id = "evt-001", Time = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), Server = new EventServerInfo { Name = "test-server", Id = "NSVR001", Cluster = "test-cluster", Version = "2.10.0", }, Client = new EventClientInfo { Id = 42, Account = "$G", User = "alice", Name = "test-client", Lang = "csharp", Version = "1.0", }, }; var json = JsonSerializer.Serialize(evt); json.ShouldContain("\"type\":"); json.ShouldContain(ConnectEventMsg.EventType); json.ShouldContain("\"server\":"); json.ShouldContain("\"client\":"); json.ShouldContain("\"id\":\"evt-001\""); } [Fact] public void ConnectEventMsg_EventType_Constant() { // Go: connect event type string. ConnectEventMsg.EventType.ShouldBe("io.nats.server.advisory.v1.client_connect"); } [Fact] public void ConnectEventMsg_DefaultType_MatchesConstant() { var evt = new ConnectEventMsg(); evt.Type.ShouldBe(ConnectEventMsg.EventType); } // ======================================================================== // DisconnectEventMsg serialization // Go reference: events_test.go TestSystemAccountNewConnection (disconnect part) // ======================================================================== [Fact] public void DisconnectEventMsg_JsonShape_MatchesGo() { // Go: TestSystemAccountNewConnection — verifies disconnect event includes // sent/received stats and reason. var evt = new DisconnectEventMsg { Id = "evt-002", Time = DateTime.UtcNow, Server = new EventServerInfo { Name = "test-server", Id = "NSVR001" }, Client = new EventClientInfo { Id = 42, Account = "$G" }, Sent = new DataStats { Msgs = 100, Bytes = 10240 }, Received = new DataStats { Msgs = 50, Bytes = 5120 }, Reason = "Client Closed", }; var json = JsonSerializer.Serialize(evt); json.ShouldContain("\"type\":"); json.ShouldContain(DisconnectEventMsg.EventType); json.ShouldContain("\"sent\":"); json.ShouldContain("\"received\":"); json.ShouldContain("\"reason\":"); } [Fact] public void DisconnectEventMsg_EventType_Constant() { DisconnectEventMsg.EventType.ShouldBe("io.nats.server.advisory.v1.client_disconnect"); } [Fact] public void DisconnectEventMsg_Reason_ClientClosed() { // Go: TestSystemAccountDisconnectBadLogin — reason is captured on disconnect. var evt = new DisconnectEventMsg { Reason = "Client Closed" }; evt.Reason.ShouldBe("Client Closed"); } [Fact] public void DisconnectEventMsg_Reason_AuthViolation() { // Go: TestSystemAccountDisconnectBadLogin — bad login reason. var evt = new DisconnectEventMsg { Reason = "Authentication Violation" }; evt.Reason.ShouldBe("Authentication Violation"); } // ======================================================================== // DataStats // Go reference: events_test.go TestSystemAccountingWithLeafNodes // ======================================================================== [Fact] public void DataStats_JsonSerialization() { // Go: TestSystemAccountingWithLeafNodes — verifies sent/received stats structure. var stats = new DataStats { Msgs = 1000, Bytes = 65536, Routes = new MsgBytesStats { Msgs = 200, Bytes = 10240 }, Gateways = new MsgBytesStats { Msgs = 50, Bytes = 2048 }, Leafs = new MsgBytesStats { Msgs = 100, Bytes = 5120 }, }; var json = JsonSerializer.Serialize(stats); json.ShouldContain("\"msgs\":"); json.ShouldContain("\"bytes\":"); json.ShouldContain("\"routes\":"); json.ShouldContain("\"gateways\":"); json.ShouldContain("\"leafs\":"); } [Fact] public void DataStats_NullSubStats_OmittedFromJson() { // Go: When no routes/gateways/leafs, those fields are omitted (omitempty). var stats = new DataStats { Msgs = 100, Bytes = 1024 }; var json = JsonSerializer.Serialize(stats); json.ShouldNotContain("\"routes\":"); json.ShouldNotContain("\"gateways\":"); json.ShouldNotContain("\"leafs\":"); } // ======================================================================== // AccountNumConns // Go reference: events_test.go TestAccountReqMonitoring // ======================================================================== [Fact] public void AccountNumConns_JsonShape_MatchesGo() { // Go: TestAccountReqMonitoring — verifies account connection count event shape. var evt = new AccountNumConns { Id = "evt-003", Time = DateTime.UtcNow, Server = new EventServerInfo { Name = "test-server", Id = "NSVR001" }, AccountName = "MYACCOUNT", Connections = 5, LeafNodes = 2, TotalConnections = 10, NumSubscriptions = 42, Sent = new DataStats { Msgs = 500, Bytes = 25600 }, Received = new DataStats { Msgs = 250, Bytes = 12800 }, }; var json = JsonSerializer.Serialize(evt); json.ShouldContain("\"type\":"); json.ShouldContain(AccountNumConns.EventType); json.ShouldContain("\"acc\":"); json.ShouldContain("\"conns\":"); json.ShouldContain("\"leafnodes\":"); json.ShouldContain("\"total_conns\":"); json.ShouldContain("\"num_subscriptions\":"); } [Fact] public void AccountNumConns_EventType_Constant() { AccountNumConns.EventType.ShouldBe("io.nats.server.advisory.v1.account_connections"); } [Fact] public void AccountNumConns_SlowConsumers_IncludedWhenNonZero() { var evt = new AccountNumConns { SlowConsumers = 3 }; var json = JsonSerializer.Serialize(evt); json.ShouldContain("\"slow_consumers\":3"); } [Fact] public void AccountNumConns_SlowConsumers_OmittedWhenZero() { // Go: omitempty behavior — zero slow_consumers omitted. var evt = new AccountNumConns { SlowConsumers = 0 }; var json = JsonSerializer.Serialize(evt); json.ShouldNotContain("\"slow_consumers\":"); } // ======================================================================== // ServerStatsMsg // Go reference: events_test.go TestServerEventsPingStatsZDedicatedRecvQ // ======================================================================== [Fact] public void ServerStatsMsg_JsonShape_MatchesGo() { // Go: TestServerEventsPingStatsZDedicatedRecvQ — verifies server stats shape. var msg = new ServerStatsMsg { Server = new EventServerInfo { Name = "test-server", Id = "NSVR001", Version = "2.10.0", JetStream = true, }, Stats = new ServerStatsData { Start = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), Mem = 134217728, Cores = 8, Cpu = 12.5, Connections = 10, TotalConnections = 100, ActiveAccounts = 5, Subscriptions = 42, Sent = new DataStats { Msgs = 1000, Bytes = 65536 }, Received = new DataStats { Msgs = 500, Bytes = 32768 }, InMsgs = 500, OutMsgs = 1000, InBytes = 32768, OutBytes = 65536, }, }; var json = JsonSerializer.Serialize(msg); json.ShouldContain("\"server\":"); json.ShouldContain("\"statsz\":"); json.ShouldContain("\"mem\":"); json.ShouldContain("\"cores\":"); json.ShouldContain("\"connections\":"); json.ShouldContain("\"total_connections\":"); json.ShouldContain("\"subscriptions\":"); json.ShouldContain("\"in_msgs\":"); json.ShouldContain("\"out_msgs\":"); } [Fact] public void ServerStatsData_SlowConsumerStats_JsonShape() { // Go: TestServerEventsPingStatsSlowConsumersStats — breakdown by type. var data = new ServerStatsData { SlowConsumers = 10, SlowConsumerStats = new SlowConsumersStats { Clients = 5, Routes = 2, Gateways = 1, Leafs = 2, }, }; var json = JsonSerializer.Serialize(data); json.ShouldContain("\"slow_consumers\":10"); json.ShouldContain("\"slow_consumer_stats\":"); json.ShouldContain("\"clients\":5"); json.ShouldContain("\"routes\":2"); } [Fact] public void ServerStatsData_StaleConnectionStats_JsonShape() { // Go: TestServerEventsPingStatsStaleConnectionStats — stale conn breakdown. var data = new ServerStatsData { StaleConnections = 7, StaleConnectionStats = new StaleConnectionStats { Clients = 3, Routes = 1, Gateways = 2, Leafs = 1, }, }; var json = JsonSerializer.Serialize(data); json.ShouldContain("\"stale_connections\":7"); json.ShouldContain("\"stale_connection_stats\":"); } [Fact] public void ServerStatsData_RouteStats_JsonShape() { // Go: TestServerEventsPingStatsZDedicatedRecvQ — route stats in statsz. var data = new ServerStatsData { Routes = [ new RouteStat { Id = 100, Name = "route-1", Sent = new DataStats { Msgs = 200, Bytes = 10240 }, Received = new DataStats { Msgs = 150, Bytes = 7680 }, Pending = 5, }, ], }; var json = JsonSerializer.Serialize(data); json.ShouldContain("\"routes\":"); json.ShouldContain("\"rid\":100"); json.ShouldContain("\"pending\":5"); } [Fact] public void ServerStatsData_GatewayStats_JsonShape() { // Go: TestGatewayNameClientInfo — gateway stats in statsz. var data = new ServerStatsData { Gateways = [ new GatewayStat { Id = 200, Name = "gw-east", Sent = new DataStats { Msgs = 500, Bytes = 25600 }, Received = new DataStats { Msgs = 300, Bytes = 15360 }, InboundConnections = 3, }, ], }; var json = JsonSerializer.Serialize(data); json.ShouldContain("\"gateways\":"); json.ShouldContain("\"gwid\":200"); json.ShouldContain("\"inbound_connections\":3"); } // ======================================================================== // ShutdownEventMsg // Go reference: events_test.go TestServerEventsLDMKick // ======================================================================== [Fact] public void ShutdownEventMsg_JsonShape_MatchesGo() { // Go: ShutdownEventMsg includes server info and reason. var evt = new ShutdownEventMsg { Server = new EventServerInfo { Name = "test-server", Id = "NSVR001" }, Reason = "process exit", }; var json = JsonSerializer.Serialize(evt); json.ShouldContain("\"server\":"); json.ShouldContain("\"reason\":"); json.ShouldContain("\"process exit\""); } // ======================================================================== // LameDuckEventMsg // Go reference: events_test.go TestServerEventsLDMKick // ======================================================================== [Fact] public void LameDuckEventMsg_JsonShape_MatchesGo() { // Go: TestServerEventsLDMKick — lame duck event emitted before shutdown. var evt = new LameDuckEventMsg { Server = new EventServerInfo { Name = "test-server", Id = "NSVR001" }, }; var json = JsonSerializer.Serialize(evt); json.ShouldContain("\"server\":"); json.ShouldContain("\"name\":\"test-server\""); } // ======================================================================== // AuthErrorEventMsg // Go reference: events_test.go TestSystemAccountDisconnectBadLogin // ======================================================================== [Fact] public void AuthErrorEventMsg_JsonShape_MatchesGo() { // Go: TestSystemAccountDisconnectBadLogin — auth error advisory. var evt = new AuthErrorEventMsg { Id = "evt-004", Time = DateTime.UtcNow, Server = new EventServerInfo { Name = "test-server", Id = "NSVR001" }, Client = new EventClientInfo { Id = 99, Host = "192.168.1.100" }, Reason = "Authorization Violation", }; var json = JsonSerializer.Serialize(evt); json.ShouldContain("\"type\":"); json.ShouldContain(AuthErrorEventMsg.EventType); json.ShouldContain("\"reason\":"); json.ShouldContain("\"Authorization Violation\""); } [Fact] public void AuthErrorEventMsg_EventType_Constant() { AuthErrorEventMsg.EventType.ShouldBe("io.nats.server.advisory.v1.client_auth"); } // ======================================================================== // OcspPeerRejectEventMsg // Go reference: events.go OCSPPeerRejectEventMsg struct // ======================================================================== [Fact] public void OcspPeerRejectEventMsg_JsonShape_MatchesGo() { var evt = new OcspPeerRejectEventMsg { Id = "evt-005", Time = DateTime.UtcNow, Kind = "client", Server = new EventServerInfo { Name = "test-server", Id = "NSVR001" }, Reason = "OCSP certificate revoked", }; var json = JsonSerializer.Serialize(evt); json.ShouldContain("\"type\":"); json.ShouldContain(OcspPeerRejectEventMsg.EventType); json.ShouldContain("\"kind\":\"client\""); json.ShouldContain("\"reason\":"); } [Fact] public void OcspPeerRejectEventMsg_EventType_Constant() { OcspPeerRejectEventMsg.EventType.ShouldBe("io.nats.server.advisory.v1.ocsp_peer_reject"); } // ======================================================================== // AccNumConnsReq // Go reference: events.go accNumConnsReq // ======================================================================== [Fact] public void AccNumConnsReq_JsonShape_MatchesGo() { var req = new AccNumConnsReq { Server = new EventServerInfo { Name = "test-server", Id = "NSVR001" }, Account = "$G", }; var json = JsonSerializer.Serialize(req); json.ShouldContain("\"server\":"); json.ShouldContain("\"acc\":\"$G\""); } // ======================================================================== // EventServerInfo // Go reference: events_test.go TestServerEventsFilteredByTag // ======================================================================== [Fact] public void EventServerInfo_Tags_Serialized() { // Go: TestServerEventsFilteredByTag — server info includes tags for filtering. var info = new EventServerInfo { Name = "test-server", Id = "NSVR001", Tags = ["region:us-east-1", "env:production"], }; var json = JsonSerializer.Serialize(info); json.ShouldContain("\"tags\":"); json.ShouldContain("\"region:us-east-1\""); json.ShouldContain("\"env:production\""); } [Fact] public void EventServerInfo_NullTags_OmittedFromJson() { // Go: omitempty — nil tags are not serialized. var info = new EventServerInfo { Name = "test-server", Id = "NSVR001" }; var json = JsonSerializer.Serialize(info); json.ShouldNotContain("\"tags\":"); } [Fact] public void EventServerInfo_Metadata_Serialized() { var info = new EventServerInfo { Name = "test-server", Id = "NSVR001", Metadata = new Dictionary { ["cloud"] = "aws", ["zone"] = "us-east-1a", }, }; var json = JsonSerializer.Serialize(info); json.ShouldContain("\"metadata\":"); json.ShouldContain("\"cloud\":"); json.ShouldContain("\"aws\""); } [Fact] public void EventServerInfo_NullMetadata_OmittedFromJson() { var info = new EventServerInfo { Name = "test-server", Id = "NSVR001" }; var json = JsonSerializer.Serialize(info); json.ShouldNotContain("\"metadata\":"); } [Fact] public void EventServerInfo_JetStream_IncludedWhenTrue() { var info = new EventServerInfo { Name = "s1", Id = "N1", JetStream = true }; var json = JsonSerializer.Serialize(info); json.ShouldContain("\"jetstream\":true"); } [Fact] public void EventServerInfo_JetStream_OmittedWhenFalse() { // Go: omitempty — JetStream false is not serialized. var info = new EventServerInfo { Name = "s1", Id = "N1", JetStream = false }; var json = JsonSerializer.Serialize(info); json.ShouldNotContain("\"jetstream\":"); } // ======================================================================== // EventClientInfo // Go reference: events_test.go TestGatewayNameClientInfo // ======================================================================== [Fact] public void EventClientInfo_AllFields_Serialized() { // Go: TestGatewayNameClientInfo — client info includes all connection metadata. var info = new EventClientInfo { Id = 42, Account = "MYACCOUNT", User = "alice", Name = "test-client", Lang = "go", Version = "1.30.0", RttNanos = 1_500_000, // 1.5ms Host = "192.168.1.100", Kind = "Client", ClientType = "nats", Tags = ["role:publisher"], }; var json = JsonSerializer.Serialize(info); json.ShouldContain("\"id\":42"); json.ShouldContain("\"acc\":\"MYACCOUNT\""); json.ShouldContain("\"user\":\"alice\""); json.ShouldContain("\"name\":\"test-client\""); json.ShouldContain("\"lang\":\"go\""); json.ShouldContain("\"rtt\":"); json.ShouldContain("\"kind\":\"Client\""); } [Fact] public void EventClientInfo_MqttClient_Serialized() { // Go: MQTT client ID is included in client info when present. var info = new EventClientInfo { Id = 10, MqttClient = "mqtt-device-42", }; var json = JsonSerializer.Serialize(info); json.ShouldContain("\"client_id\":\"mqtt-device-42\""); } [Fact] public void EventClientInfo_NullOptionalFields_OmittedFromJson() { // Go: omitempty — null optional fields are not serialized. var info = new EventClientInfo { Id = 1 }; var json = JsonSerializer.Serialize(info); json.ShouldNotContain("\"acc\":"); json.ShouldNotContain("\"user\":"); json.ShouldNotContain("\"name\":"); json.ShouldNotContain("\"lang\":"); json.ShouldNotContain("\"kind\":"); json.ShouldNotContain("\"tags\":"); } // ======================================================================== // Event Subject Patterns // Go reference: events.go subject constants // ======================================================================== [Fact] public void EventSubjects_ConnectEvent_Format() { // Go: $SYS.ACCOUNT.%s.CONNECT var subject = string.Format(EventSubjects.ConnectEvent, "$G"); subject.ShouldBe("$SYS.ACCOUNT.$G.CONNECT"); } [Fact] public void EventSubjects_DisconnectEvent_Format() { // Go: $SYS.ACCOUNT.%s.DISCONNECT var subject = string.Format(EventSubjects.DisconnectEvent, "$G"); subject.ShouldBe("$SYS.ACCOUNT.$G.DISCONNECT"); } [Fact] public void EventSubjects_AccountConns_Format() { // Go: $SYS.ACCOUNT.%s.SERVER.CONNS (new format) var subject = string.Format(EventSubjects.AccountConnsNew, "MYACCOUNT"); subject.ShouldBe("$SYS.ACCOUNT.MYACCOUNT.SERVER.CONNS"); } [Fact] public void EventSubjects_AccountConnsOld_Format() { // Go: $SYS.SERVER.ACCOUNT.%s.CONNS (old format for backward compat) var subject = string.Format(EventSubjects.AccountConnsOld, "MYACCOUNT"); subject.ShouldBe("$SYS.SERVER.ACCOUNT.MYACCOUNT.CONNS"); } [Fact] public void EventSubjects_ServerStats_Format() { // Go: $SYS.SERVER.%s.STATSZ var subject = string.Format(EventSubjects.ServerStats, "NSVR001"); subject.ShouldBe("$SYS.SERVER.NSVR001.STATSZ"); } [Fact] public void EventSubjects_ServerShutdown_Format() { // Go: $SYS.SERVER.%s.SHUTDOWN var subject = string.Format(EventSubjects.ServerShutdown, "NSVR001"); subject.ShouldBe("$SYS.SERVER.NSVR001.SHUTDOWN"); } [Fact] public void EventSubjects_ServerLameDuck_Format() { // Go: $SYS.SERVER.%s.LAMEDUCK var subject = string.Format(EventSubjects.ServerLameDuck, "NSVR001"); subject.ShouldBe("$SYS.SERVER.NSVR001.LAMEDUCK"); } [Fact] public void EventSubjects_AuthError_Format() { // Go: $SYS.SERVER.%s.CLIENT.AUTH.ERR var subject = string.Format(EventSubjects.AuthError, "NSVR001"); subject.ShouldBe("$SYS.SERVER.NSVR001.CLIENT.AUTH.ERR"); } [Fact] public void EventSubjects_AuthErrorAccount_IsConstant() { // Go: $SYS.ACCOUNT.CLIENT.AUTH.ERR (no server ID interpolation) EventSubjects.AuthErrorAccount.ShouldBe("$SYS.ACCOUNT.CLIENT.AUTH.ERR"); } [Fact] public void EventSubjects_ServerPing_Format() { // Go: $SYS.REQ.SERVER.PING.%s (e.g., STATSZ, VARZ) var subject = string.Format(EventSubjects.ServerPing, "STATSZ"); subject.ShouldBe("$SYS.REQ.SERVER.PING.STATSZ"); } [Fact] public void EventSubjects_ServerReq_Format() { // Go: $SYS.REQ.SERVER.%s.%s (server ID + request type) var subject = string.Format(EventSubjects.ServerReq, "NSVR001", "VARZ"); subject.ShouldBe("$SYS.REQ.SERVER.NSVR001.VARZ"); } [Fact] public void EventSubjects_AccountReq_Format() { // Go: $SYS.REQ.ACCOUNT.%s.%s (account + request type) var subject = string.Format(EventSubjects.AccountReq, "MYACCOUNT", "CONNZ"); subject.ShouldBe("$SYS.REQ.ACCOUNT.MYACCOUNT.CONNZ"); } // ======================================================================== // Event filtering by tag // Go reference: events_test.go TestServerEventsFilteredByTag // ======================================================================== [Fact] public void EventServerInfo_TagFiltering_MatchesTag() { // Go: TestServerEventsFilteredByTag — servers can be filtered by tag value. var server = new EventServerInfo { Name = "s1", Id = "NSVR001", Tags = ["region:us-east-1", "env:prod"], }; // Simulate filtering: check if server has a specific tag. server.Tags.ShouldContain("region:us-east-1"); server.Tags.ShouldContain("env:prod"); server.Tags.ShouldNotContain("region:eu-west-1"); } [Fact] public void EventServerInfo_TagFiltering_EmptyTags_NoMatch() { // Go: TestServerEventsFilteredByTag — server with no tags does not match any filter. var server = new EventServerInfo { Name = "s1", Id = "NSVR001" }; server.Tags.ShouldBeNull(); } [Fact] public void EventServerInfo_FilterByServerId() { // Go: TestServerEventsPingStatsZFilter — filter stats events by server ID. var servers = new[] { new EventServerInfo { Name = "s1", Id = "NSVR001" }, new EventServerInfo { Name = "s2", Id = "NSVR002" }, new EventServerInfo { Name = "s3", Id = "NSVR003" }, }; var filtered = servers.Where(s => s.Id == "NSVR002").ToArray(); filtered.Length.ShouldBe(1); filtered[0].Name.ShouldBe("s2"); } [Fact] public void EventServerInfo_FilterByServerId_NoMatch() { // Go: TestServerEventsPingStatsZFailFilter — non-existent server ID returns nothing. var servers = new[] { new EventServerInfo { Name = "s1", Id = "NSVR001" }, }; var filtered = servers.Where(s => s.Id == "NONEXISTENT").ToArray(); filtered.Length.ShouldBe(0); } // ======================================================================== // Event JSON roundtrip via source-generated context // Go reference: events_test.go TestServerEventsReceivedByQSubs // ======================================================================== [Fact] public void ConnectEventMsg_RoundTrip_ViaContext() { // Go: TestServerEventsReceivedByQSubs — events received and parsed correctly. var original = new ConnectEventMsg { Id = "roundtrip-001", Time = new DateTime(2024, 6, 15, 12, 0, 0, DateTimeKind.Utc), Server = new EventServerInfo { Name = "s1", Id = "NSVR001" }, Client = new EventClientInfo { Id = 42, Account = "$G", User = "alice" }, }; var json = JsonSerializer.Serialize(original, EventJsonContext.Default.ConnectEventMsg); var deserialized = JsonSerializer.Deserialize(json, EventJsonContext.Default.ConnectEventMsg); deserialized.ShouldNotBeNull(); deserialized!.Id.ShouldBe("roundtrip-001"); deserialized.Type.ShouldBe(ConnectEventMsg.EventType); deserialized.Server.Name.ShouldBe("s1"); deserialized.Client.Id.ShouldBe(42UL); deserialized.Client.Account.ShouldBe("$G"); } [Fact] public void DisconnectEventMsg_RoundTrip_ViaContext() { var original = new DisconnectEventMsg { Id = "roundtrip-002", Time = DateTime.UtcNow, Server = new EventServerInfo { Name = "s1", Id = "NSVR001" }, Client = new EventClientInfo { Id = 99 }, Sent = new DataStats { Msgs = 100, Bytes = 1024 }, Received = new DataStats { Msgs = 50, Bytes = 512 }, Reason = "Client Closed", }; var json = JsonSerializer.Serialize(original, EventJsonContext.Default.DisconnectEventMsg); var deserialized = JsonSerializer.Deserialize(json, EventJsonContext.Default.DisconnectEventMsg); deserialized.ShouldNotBeNull(); deserialized!.Reason.ShouldBe("Client Closed"); deserialized.Sent.Msgs.ShouldBe(100); deserialized.Received.Bytes.ShouldBe(512); } [Fact] public void ServerStatsMsg_RoundTrip_ViaContext() { var original = new ServerStatsMsg { Server = new EventServerInfo { Name = "s1", Id = "NSVR001", JetStream = true }, Stats = new ServerStatsData { Mem = 134217728, Cores = 8, Connections = 10, Subscriptions = 42, Sent = new DataStats { Msgs = 1000, Bytes = 65536 }, Received = new DataStats { Msgs = 500, Bytes = 32768 }, }, }; var json = JsonSerializer.Serialize(original, EventJsonContext.Default.ServerStatsMsg); var deserialized = JsonSerializer.Deserialize(json, EventJsonContext.Default.ServerStatsMsg); deserialized.ShouldNotBeNull(); deserialized!.Server.JetStream.ShouldBeTrue(); deserialized.Stats.Mem.ShouldBe(134217728); deserialized.Stats.Connections.ShouldBe(10); } [Fact] public void AccountNumConns_RoundTrip_ViaContext() { var original = new AccountNumConns { Id = "roundtrip-004", Time = DateTime.UtcNow, Server = new EventServerInfo { Name = "s1", Id = "NSVR001" }, AccountName = "$G", Connections = 5, TotalConnections = 20, NumSubscriptions = 15, }; var json = JsonSerializer.Serialize(original, EventJsonContext.Default.AccountNumConns); var deserialized = JsonSerializer.Deserialize(json, EventJsonContext.Default.AccountNumConns); deserialized.ShouldNotBeNull(); deserialized!.AccountName.ShouldBe("$G"); deserialized.Connections.ShouldBe(5); deserialized.TotalConnections.ShouldBe(20); } [Fact] public void AuthErrorEventMsg_RoundTrip_ViaContext() { var original = new AuthErrorEventMsg { Id = "roundtrip-005", Time = DateTime.UtcNow, Server = new EventServerInfo { Name = "s1", Id = "NSVR001" }, Client = new EventClientInfo { Id = 99, Host = "10.0.0.1" }, Reason = "Authorization Violation", }; var json = JsonSerializer.Serialize(original, EventJsonContext.Default.AuthErrorEventMsg); var deserialized = JsonSerializer.Deserialize(json, EventJsonContext.Default.AuthErrorEventMsg); deserialized.ShouldNotBeNull(); deserialized!.Reason.ShouldBe("Authorization Violation"); deserialized.Type.ShouldBe(AuthErrorEventMsg.EventType); } // ======================================================================== // Event subject $SYS prefix validation // Go reference: events.go — all system subjects start with $SYS // ======================================================================== [Fact] public void AllEventSubjects_StartWithSysDollarPrefix() { // Go: All system event subjects must start with $SYS. EventSubjects.ConnectEvent.ShouldStartWith("$SYS."); EventSubjects.DisconnectEvent.ShouldStartWith("$SYS."); EventSubjects.AccountConnsNew.ShouldStartWith("$SYS."); EventSubjects.AccountConnsOld.ShouldStartWith("$SYS."); EventSubjects.ServerStats.ShouldStartWith("$SYS."); EventSubjects.ServerShutdown.ShouldStartWith("$SYS."); EventSubjects.ServerLameDuck.ShouldStartWith("$SYS."); EventSubjects.AuthError.ShouldStartWith("$SYS."); EventSubjects.AuthErrorAccount.ShouldStartWith("$SYS."); EventSubjects.ServerPing.ShouldStartWith("$SYS."); EventSubjects.ServerReq.ShouldStartWith("$SYS."); EventSubjects.AccountReq.ShouldStartWith("$SYS."); EventSubjects.InboxResponse.ShouldStartWith("$SYS."); } }