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