95 lines
4.0 KiB
C#
95 lines
4.0 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;
|
|
|
|
/// <summary>
|
|
/// Covers the in-process push contract the Blazor Server <c>DriverStatusPanel</c> relies on:
|
|
/// <see cref="IDriverStatusSnapshotStore.SnapshotChanged"/> fires on every
|
|
/// <see cref="IDriverStatusSnapshotStore.Upsert"/>, and <c>TryGet</c> returns the latest.
|
|
/// The panel subscribes to this store directly instead of opening a self-targeted SignalR
|
|
/// connection (which a server-side component can't reach behind a reverse proxy).
|
|
/// </summary>
|
|
public sealed class DriverStatusSnapshotStoreTests
|
|
{
|
|
private static DriverHealthChanged Snap(string instance, string state = "Healthy") =>
|
|
new("MAIN", instance, state, null, null, 0, new DateTime(2026, 5, 29, 0, 0, 0, DateTimeKind.Utc));
|
|
|
|
[Fact]
|
|
public void Upsert_raises_SnapshotChanged_with_the_stored_snapshot()
|
|
{
|
|
var store = new InMemoryDriverStatusSnapshotStore();
|
|
var received = new List<DriverHealthChanged>();
|
|
store.SnapshotChanged += received.Add;
|
|
|
|
var snap = Snap("drv-1", "Faulted");
|
|
store.Upsert(snap);
|
|
|
|
received.Count.ShouldBe(1);
|
|
received[0].ShouldBeSameAs(snap);
|
|
}
|
|
|
|
[Fact]
|
|
public void Upsert_then_TryGet_returns_the_latest_snapshot()
|
|
{
|
|
var store = new InMemoryDriverStatusSnapshotStore();
|
|
store.Upsert(Snap("drv-1", "Healthy"));
|
|
store.Upsert(Snap("drv-1", "Degraded"));
|
|
|
|
store.TryGet("drv-1", out var latest).ShouldBeTrue();
|
|
latest.State.ShouldBe("Degraded");
|
|
}
|
|
|
|
[Fact]
|
|
public void Unsubscribed_handler_stops_receiving_after_removal()
|
|
{
|
|
var store = new InMemoryDriverStatusSnapshotStore();
|
|
var count = 0;
|
|
void Handler(DriverHealthChanged _) => count++;
|
|
|
|
store.SnapshotChanged += Handler;
|
|
store.Upsert(Snap("drv-1"));
|
|
store.SnapshotChanged -= Handler;
|
|
store.Upsert(Snap("drv-1"));
|
|
|
|
count.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public void Snapshots_from_two_nodes_for_one_driver_are_kept_side_by_side_847()
|
|
{
|
|
var store = new InMemoryDriverStatusSnapshotStore();
|
|
var t0 = new DateTime(2026, 11, 11, 0, 0, 0, DateTimeKind.Utc);
|
|
store.Upsert(new DriverHealthChanged("MAIN", "drv", "Faulted", null, "x", 1, t0, ReportingNodeId: "central-1"));
|
|
store.Upsert(new DriverHealthChanged("MAIN", "drv", "Healthy", null, null, 0, t0.AddSeconds(1), ReportingNodeId: "central-2"));
|
|
// A re-publish from central-1 replaces only central-1's entry.
|
|
store.Upsert(new DriverHealthChanged("MAIN", "drv", "Faulted", null, "y", 2, t0.AddSeconds(2), ReportingNodeId: "central-1"));
|
|
|
|
var all = store.GetAll();
|
|
all.Count.ShouldBe(2);
|
|
all.Single(s => s.ReportingNodeId == "central-1").LastError.ShouldBe("y");
|
|
all.Single(s => s.ReportingNodeId == "central-2").State.ShouldBe("Healthy");
|
|
|
|
// TryGet (the per-driver panel / hub) answers the most recently published across nodes.
|
|
store.TryGet("drv", out var latest).ShouldBeTrue();
|
|
latest.ReportingNodeId.ShouldBe("central-1");
|
|
latest.LastError.ShouldBe("y");
|
|
store.TryGet("other", out _).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Eviction_removes_every_node_entry_of_an_unconfigured_stale_driver_847()
|
|
{
|
|
var store = new InMemoryDriverStatusSnapshotStore();
|
|
var old = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
|
store.Upsert(new DriverHealthChanged("MAIN", "gone", "Healthy", null, null, 0, old, ReportingNodeId: "a"));
|
|
store.Upsert(new DriverHealthChanged("MAIN", "gone", "Healthy", null, null, 0, old, ReportingNodeId: "b"));
|
|
store.Upsert(new DriverHealthChanged("MAIN", "kept", "Healthy", null, null, 0, old, ReportingNodeId: "a"));
|
|
|
|
store.EvictUnconfiguredStale(new HashSet<string> { "kept" }, old.AddMinutes(1)).ShouldBe(2);
|
|
store.GetAll().ShouldHaveSingleItem().DriverInstanceId.ShouldBe("kept");
|
|
}
|
|
}
|