using Mbproxy.Admin; using Mbproxy.Bcd; using Mbproxy.Proxy; using Shouldly; using Xunit; namespace Mbproxy.Tests.Admin; /// /// Unit tests for — group joins and on-demand capture /// arming. Uses hand-written SignalR test doubles (see ); /// no SignalR host is started. /// [Trait("Category", "Unit")] public sealed class StatusHubTests { private static StatusHub MakeHub( string connectionId, PlcSubscriptionTracker tracker, TagCaptureRegistry registry, out FakeGroupManager groups) { groups = new FakeGroupManager(); return new StatusHub(tracker, registry) { Context = new FakeHubCallerContext(connectionId), Groups = groups, }; } [Fact] public async Task SubscribeFleet_JoinsFleetGroup() { var hub = MakeHub("conn-1", new PlcSubscriptionTracker(), new TagCaptureRegistry(), out var groups); await hub.SubscribeFleet(); groups.Added.ShouldContain(("conn-1", StatusHub.FleetGroup)); } [Fact] public async Task SubscribePlc_JoinsPlcGroup_AndArmsCapture() { var registry = new TagCaptureRegistry(); registry.GetOrCreate("plc-1", BcdTagMap.Empty); var hub = MakeHub("conn-1", new PlcSubscriptionTracker(), registry, out var groups); await hub.SubscribePlc("plc-1"); groups.Added.ShouldContain(("conn-1", StatusHub.PlcGroup("plc-1"))); registry.TryGet("plc-1", out var capture).ShouldBeTrue(); capture.IsArmed.ShouldBeTrue(); } [Fact] public async Task SecondSubscriber_FirstLeaveKeepsArmed_LastLeaveDisarms() { var tracker = new PlcSubscriptionTracker(); var registry = new TagCaptureRegistry(); registry.GetOrCreate("plc-1", BcdTagMap.Empty); var hub1 = MakeHub("conn-1", tracker, registry, out _); var hub2 = MakeHub("conn-2", tracker, registry, out _); await hub1.SubscribePlc("plc-1"); await hub2.SubscribePlc("plc-1"); registry.TryGet("plc-1", out var capture).ShouldBeTrue(); capture.IsArmed.ShouldBeTrue(); // First viewer leaves — a second viewer remains, so capture stays armed. await hub1.OnDisconnectedAsync(null); capture.IsArmed.ShouldBeTrue("capture must stay armed while another detail page is open"); // Last viewer leaves — capture disarms. await hub2.OnDisconnectedAsync(null); capture.IsArmed.ShouldBeFalse("capture must disarm when the last viewer leaves"); } [Fact] public async Task SubscribePlc_UnknownPlc_DoesNotThrow_AndArmsNothing() { var registry = new TagCaptureRegistry(); // no captures registered var hub = MakeHub("conn-1", new PlcSubscriptionTracker(), registry, out var groups); await Should.NotThrowAsync(async () => await hub.SubscribePlc("ghost")); groups.Added.ShouldContain(("conn-1", StatusHub.PlcGroup("ghost"))); registry.TryGet("ghost", out _).ShouldBeFalse(); } }