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"); + } +}