Files
wwtools/mbproxy/tests/Mbproxy.Tests/Admin/StatusHubTests.cs
T
Joseph Doherty e719dd51c1 mbproxy: replace status page with a live SignalR web dashboard
The single auto-refreshing zero-JS status page gave operators a 25-column
wall and no way to drill into one connection. This adds a Bootstrap fleet
dashboard (filterable/sortable KPI table) and a per-PLC detail page with a
real-time debug view of raw PLC-side BCD vs. decoded client-side values,
streamed live over a SignalR feed. The debug view is fed by an on-demand
per-tag value capture, armed only while a detail page is open. All assets
(Bootstrap, SignalR client, fonts) are embedded so the UI works unchanged
on firewalled networks; GET /status.json is untouched for scrapers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:41:02 -04:00

92 lines
3.0 KiB
C#

using Mbproxy.Admin;
using Mbproxy.Bcd;
using Mbproxy.Proxy;
using Shouldly;
using Xunit;
namespace Mbproxy.Tests.Admin;
/// <summary>
/// Unit tests for <see cref="StatusHub"/> — group joins and on-demand capture
/// arming. Uses hand-written SignalR test doubles (see <see cref="SignalRFakes"/>);
/// no SignalR host is started.
/// </summary>
[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();
}
}