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.
This commit is contained in:
Joseph Doherty
2026-02-25 13:11:59 -05:00
parent a6e7778c6c
commit 10ac904b5c
2 changed files with 206 additions and 0 deletions

View File

@@ -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}";

View File

@@ -0,0 +1,200 @@
using System.Text.Json;
using NATS.Server.Events;
namespace NATS.Server.Tests.Events;
/// <summary>
/// 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.
/// </summary>
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<RemoteServerShutdownEvent>(shutdownJson);
shutdownRt.ShouldNotBeNull();
shutdownRt!.Type.ShouldBe(RemoteServerShutdownEvent.EventType);
shutdownRt.RemoteServerId.ShouldBe("r1");
}
}