From 10ac904b5c2092776f2fabd9880b36f49acd74d5 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 25 Feb 2026 13:11:59 -0500 Subject: [PATCH] feat: add remote server events for cluster visibility (Gap 10.8) Add RemoteServerShutdownEvent, RemoteServerUpdateEvent, LeafNodeConnectEvent, and LeafNodeDisconnectEvent types plus matching EventSubjects constants, with 10 unit tests covering type identity, field assignment, format placeholders, and JSON roundtrip serialization. --- src/NATS.Server/Events/EventSubjects.cs | 6 + .../Events/RemoteServerEventTests.cs | 200 ++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 tests/NATS.Server.Tests/Events/RemoteServerEventTests.cs diff --git a/src/NATS.Server/Events/EventSubjects.cs b/src/NATS.Server/Events/EventSubjects.cs index 4eed2bd..81401c6 100644 --- a/src/NATS.Server/Events/EventSubjects.cs +++ b/src/NATS.Server/Events/EventSubjects.cs @@ -22,6 +22,12 @@ public static class EventSubjects public const string AuthError = "$SYS.SERVER.{0}.CLIENT.AUTH.ERR"; public const string AuthErrorAccount = "$SYS.ACCOUNT.CLIENT.AUTH.ERR"; + // Remote server and leaf node events + public const string RemoteServerShutdown = "$SYS.SERVER.{0}.REMOTE.SHUTDOWN"; + public const string RemoteServerUpdate = "$SYS.SERVER.{0}.REMOTE.UPDATE"; + public const string LeafNodeConnected = "$SYS.SERVER.{0}.LEAFNODE.CONNECT"; + public const string LeafNodeDisconnected = "$SYS.SERVER.{0}.LEAFNODE.DISCONNECT"; + // Request-reply subjects (server-specific) public const string ServerReq = "$SYS.REQ.SERVER.{0}.{1}"; diff --git a/tests/NATS.Server.Tests/Events/RemoteServerEventTests.cs b/tests/NATS.Server.Tests/Events/RemoteServerEventTests.cs new file mode 100644 index 0000000..661b355 --- /dev/null +++ b/tests/NATS.Server.Tests/Events/RemoteServerEventTests.cs @@ -0,0 +1,200 @@ +using System.Text.Json; +using NATS.Server.Events; + +namespace NATS.Server.Tests.Events; + +/// +/// Tests for remote server and leaf node event DTOs and subject constants. +/// Go reference: events.go — remote server lifecycle, leaf node advisory subjects. +/// Gap 10.8: RemoteServerShutdown, RemoteServerUpdate, LeafNodeConnected events. +/// +public class RemoteServerEventTests +{ + // --- RemoteServerShutdownEvent --- + + [Fact] + public void RemoteServerShutdownEvent_HasCorrectEventType() + { + RemoteServerShutdownEvent.EventType.ShouldBe("io.nats.server.advisory.v1.remote_shutdown"); + var ev = new RemoteServerShutdownEvent(); + ev.Type.ShouldBe(RemoteServerShutdownEvent.EventType); + } + + [Fact] + public void RemoteServerShutdownEvent_GeneratesUniqueId() + { + var ev1 = new RemoteServerShutdownEvent(); + var ev2 = new RemoteServerShutdownEvent(); + ev1.Id.ShouldNotBeNullOrEmpty(); + ev2.Id.ShouldNotBeNullOrEmpty(); + ev1.Id.ShouldNotBe(ev2.Id); + } + + // --- RemoteServerUpdateEvent --- + + [Fact] + public void RemoteServerUpdateEvent_HasCorrectEventType() + { + RemoteServerUpdateEvent.EventType.ShouldBe("io.nats.server.advisory.v1.remote_update"); + var ev = new RemoteServerUpdateEvent(); + ev.Type.ShouldBe(RemoteServerUpdateEvent.EventType); + } + + [Fact] + public void RemoteServerUpdateEvent_AllFieldsSettable() + { + var server = new EventServerInfo { Id = "srv1", Name = "my-server" }; + var ev = new RemoteServerUpdateEvent + { + Server = server, + RemoteServerId = "remote-id-123", + RemoteServerName = "remote-server", + UpdateType = "routes_changed", + }; + + ev.Server.ShouldBeSameAs(server); + ev.RemoteServerId.ShouldBe("remote-id-123"); + ev.RemoteServerName.ShouldBe("remote-server"); + ev.UpdateType.ShouldBe("routes_changed"); + } + + // --- LeafNodeConnectEvent --- + + [Fact] + public void LeafNodeConnectEvent_HasCorrectEventType() + { + LeafNodeConnectEvent.EventType.ShouldBe("io.nats.server.advisory.v1.leafnode_connect"); + var ev = new LeafNodeConnectEvent(); + ev.Type.ShouldBe(LeafNodeConnectEvent.EventType); + } + + [Fact] + public void LeafNodeConnectEvent_AllFieldsSettable() + { + var server = new EventServerInfo { Id = "srv1", Name = "hub" }; + var ev = new LeafNodeConnectEvent + { + Server = server, + LeafNodeId = "leaf-id-abc", + LeafNodeName = "leaf-node-1", + RemoteUrl = "nats://10.0.0.1:7422", + Account = "ACC", + }; + + ev.Server.ShouldBeSameAs(server); + ev.LeafNodeId.ShouldBe("leaf-id-abc"); + ev.LeafNodeName.ShouldBe("leaf-node-1"); + ev.RemoteUrl.ShouldBe("nats://10.0.0.1:7422"); + ev.Account.ShouldBe("ACC"); + } + + // --- LeafNodeDisconnectEvent --- + + [Fact] + public void LeafNodeDisconnectEvent_HasCorrectEventType() + { + LeafNodeDisconnectEvent.EventType.ShouldBe("io.nats.server.advisory.v1.leafnode_disconnect"); + var ev = new LeafNodeDisconnectEvent(); + ev.Type.ShouldBe(LeafNodeDisconnectEvent.EventType); + } + + [Fact] + public void LeafNodeDisconnectEvent_AllFieldsSettable() + { + var server = new EventServerInfo { Id = "srv1", Name = "hub" }; + var ev = new LeafNodeDisconnectEvent + { + Server = server, + LeafNodeId = "leaf-id-xyz", + Reason = "connection closed", + }; + + ev.Server.ShouldBeSameAs(server); + ev.LeafNodeId.ShouldBe("leaf-id-xyz"); + ev.Reason.ShouldBe("connection closed"); + } + + // --- EventSubjects --- + + [Fact] + public void EventSubjects_RemoteShutdown_HasPlaceholder() + { + EventSubjects.RemoteServerShutdown.ShouldContain("{0}"); + EventSubjects.RemoteServerUpdate.ShouldContain("{0}"); + EventSubjects.LeafNodeConnected.ShouldContain("{0}"); + EventSubjects.LeafNodeDisconnected.ShouldContain("{0}"); + + // Verify format strings produce expected subjects when formatted + var serverId = "ABCDEF123456"; + string.Format(EventSubjects.RemoteServerShutdown, serverId) + .ShouldBe($"$SYS.SERVER.{serverId}.REMOTE.SHUTDOWN"); + string.Format(EventSubjects.LeafNodeConnected, serverId) + .ShouldBe($"$SYS.SERVER.{serverId}.LEAFNODE.CONNECT"); + } + + // --- JSON serialization --- + + [Fact] + public void AllRemoteEvents_SerializeToJson() + { + var server = new EventServerInfo { Id = "srv-id", Name = "test-server" }; + + var shutdown = new RemoteServerShutdownEvent + { + Server = server, + RemoteServerId = "r1", + RemoteServerName = "remote", + Reason = "graceful", + }; + var shutdownJson = JsonSerializer.Serialize(shutdown); + var shutdownDoc = JsonDocument.Parse(shutdownJson).RootElement; + shutdownDoc.GetProperty("type").GetString().ShouldBe(RemoteServerShutdownEvent.EventType); + shutdownDoc.GetProperty("id").GetString().ShouldNotBeNullOrEmpty(); + shutdownDoc.GetProperty("remote_server_id").GetString().ShouldBe("r1"); + shutdownDoc.GetProperty("reason").GetString().ShouldBe("graceful"); + + var update = new RemoteServerUpdateEvent + { + Server = server, + RemoteServerId = "r2", + RemoteServerName = "remote2", + UpdateType = "config_updated", + }; + var updateJson = JsonSerializer.Serialize(update); + var updateDoc = JsonDocument.Parse(updateJson).RootElement; + updateDoc.GetProperty("type").GetString().ShouldBe(RemoteServerUpdateEvent.EventType); + updateDoc.GetProperty("update_type").GetString().ShouldBe("config_updated"); + + var connect = new LeafNodeConnectEvent + { + Server = server, + LeafNodeId = "leaf1", + LeafNodeName = "leaf-node", + RemoteUrl = "nats://10.0.0.1:7422", + Account = "ACC", + }; + var connectJson = JsonSerializer.Serialize(connect); + var connectDoc = JsonDocument.Parse(connectJson).RootElement; + connectDoc.GetProperty("type").GetString().ShouldBe(LeafNodeConnectEvent.EventType); + connectDoc.GetProperty("leaf_node_id").GetString().ShouldBe("leaf1"); + connectDoc.GetProperty("account").GetString().ShouldBe("ACC"); + + var disconnect = new LeafNodeDisconnectEvent + { + Server = server, + LeafNodeId = "leaf1", + Reason = "timeout", + }; + var disconnectJson = JsonSerializer.Serialize(disconnect); + var disconnectDoc = JsonDocument.Parse(disconnectJson).RootElement; + disconnectDoc.GetProperty("type").GetString().ShouldBe(LeafNodeDisconnectEvent.EventType); + disconnectDoc.GetProperty("leaf_node_id").GetString().ShouldBe("leaf1"); + disconnectDoc.GetProperty("reason").GetString().ShouldBe("timeout"); + + // Roundtrip: deserialize back and verify type field survives + var shutdownRt = JsonSerializer.Deserialize(shutdownJson); + shutdownRt.ShouldNotBeNull(); + shutdownRt!.Type.ShouldBe(RemoteServerShutdownEvent.EventType); + shutdownRt.RemoteServerId.ShouldBe("r1"); + } +}