using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs; namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard; /// /// Verifies , the seam /// consults before cloning and sending /// an event: a session is "watched" only while at least one hub connection /// holds a subscription to it, and a dropped connection releases every /// subscription it held. /// public sealed class EventsHubViewerRegistryTests { /// A session with no subscriber is not watched. [Fact] public void HasViewers_WithNoSubscribers_IsFalse() { EventsHubViewerRegistry registry = new(); Assert.False(registry.HasViewers("session-1")); } /// Adding then removing the only viewer flips the session back to unwatched. [Fact] public void AddViewer_ThenRemoveViewer_TogglesWatchedState() { EventsHubViewerRegistry registry = new(); registry.AddViewer("conn-1", "session-1"); Assert.True(registry.HasViewers("session-1")); registry.RemoveViewer("conn-1", "session-1"); Assert.False(registry.HasViewers("session-1")); } /// Each connection counts once: the session stays watched until the last one leaves. [Fact] public void RemoveViewer_WithOtherConnectionsStillSubscribed_KeepsSessionWatched() { EventsHubViewerRegistry registry = new(); registry.AddViewer("conn-1", "session-1"); registry.AddViewer("conn-2", "session-1"); registry.RemoveViewer("conn-1", "session-1"); Assert.True(registry.HasViewers("session-1")); registry.RemoveViewer("conn-2", "session-1"); Assert.False(registry.HasViewers("session-1")); } /// A repeated subscribe from the same connection is idempotent, so one unsubscribe clears it. [Fact] public void AddViewer_CalledTwiceForSameConnection_CountsOnce() { EventsHubViewerRegistry registry = new(); registry.AddViewer("conn-1", "session-1"); registry.AddViewer("conn-1", "session-1"); registry.RemoveViewer("conn-1", "session-1"); Assert.False(registry.HasViewers("session-1")); } /// Viewer counts are tracked per session; unrelated sessions stay unwatched. [Fact] public void AddViewer_TracksSessionsIndependently() { EventsHubViewerRegistry registry = new(); registry.AddViewer("conn-1", "session-1"); Assert.True(registry.HasViewers("session-1")); Assert.False(registry.HasViewers("session-2")); } /// A dropped connection releases every session it held. [Fact] public void ReleaseConnection_ReleasesEverySessionTheConnectionHeld() { EventsHubViewerRegistry registry = new(); registry.AddViewer("conn-1", "session-1"); registry.AddViewer("conn-1", "session-2"); registry.AddViewer("conn-2", "session-2"); registry.ReleaseConnection("conn-1"); Assert.False(registry.HasViewers("session-1")); // conn-2 still watches session-2. Assert.True(registry.HasViewers("session-2")); } /// Releasing a connection twice does not double-decrement another connection's subscription. [Fact] public void ReleaseConnection_CalledTwice_DoesNotDropOtherViewers() { EventsHubViewerRegistry registry = new(); registry.AddViewer("conn-1", "session-1"); registry.AddViewer("conn-2", "session-1"); registry.ReleaseConnection("conn-1"); registry.ReleaseConnection("conn-1"); Assert.True(registry.HasViewers("session-1")); } /// Unmatched removals cannot drive the count negative and strand a session as unwatched. [Fact] public void RemoveViewer_WithoutMatchingAdd_LeavesCountAtZero() { EventsHubViewerRegistry registry = new(); registry.RemoveViewer("conn-1", "session-1"); registry.RemoveViewer("conn-1", "session-1"); registry.ReleaseConnection("conn-1"); Assert.False(registry.HasViewers("session-1")); // A subsequent genuine subscribe must still register as exactly one viewer. registry.AddViewer("conn-1", "session-1"); Assert.True(registry.HasViewers("session-1")); registry.RemoveViewer("conn-1", "session-1"); Assert.False(registry.HasViewers("session-1")); } /// Blank connection or session ids are ignored rather than tracked. /// The hub connection id, possibly blank. /// The session id, possibly blank. [Theory] [InlineData("", "session-1")] [InlineData(" ", "session-1")] [InlineData("conn-1", "")] [InlineData("conn-1", " ")] public void AddViewer_WithBlankIdentifiers_IsIgnored(string connectionId, string sessionId) { EventsHubViewerRegistry registry = new(); registry.AddViewer(connectionId, sessionId); Assert.False(registry.HasViewers(sessionId)); Assert.False(registry.HasViewers("session-1")); } /// Concurrent add/remove pairs settle at zero viewers, never at a stuck-on count. [Fact] public void AddAndRemoveViewer_UnderConcurrency_SettlesAtZero() { EventsHubViewerRegistry registry = new(); Parallel.For(0, 64, i => { string connectionId = $"conn-{i}"; for (int pass = 0; pass < 50; pass++) { registry.AddViewer(connectionId, "session-1"); registry.RemoveViewer(connectionId, "session-1"); } }); Assert.False(registry.HasViewers("session-1")); } }