Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Drivers/TestConnectBudgetTests.cs
T
Joseph Doherty 0f1f8ad8fa fix(adminui): lane D-b review r0 fixes (I1, M1, M2, M5-M9, N1-N3, N5, H3)
I1: /hosts eviction keyed off the AUTHORED DriverInstances, so a driver
deleted in /raw but not yet deployed - still running, still publishing -
vanished every tick. The stores' sweep is now EvictUnconfiguredStale(set,
olderThanUtc): an entry goes only when unconfigured AND its PublishedUtc is
older than 60 s. Tests cover unconfigured+fresh kept (positive control),
unconfigured+stale evicted, configured+stale kept, and the deploy hand-off.

M1: Test Connect falls back to the driver's documented ProbeTimeoutSeconds
default (Galaxy 30, OpcUaClient 15, ...) when the key is absent.
M2: the budget never undercuts the config's root timeoutMs (+1 s).
M5: RawBrowseModal compares the open generation captured before the await,
so a close-then-reopen mid-open closes the stale session.
M6: /alarms-historian keeps a 30 s re-poll on 'not registered' (a driver
node may still be starting) instead of stopping for good.
M7: /scripts source fetch failure shows in the dialog, not the error boundary.
M8: the export modal maps only its preview rows; the endpoint writes via
Results.Text (no second byte[] copy).
M9: /hosts config queries are ordered, so SequenceEqual change detection holds.
N1 tooltip, N2 CRLF-safe regex, N3 throttled Warning for subscriber faults,
N5 equipment-import help text now describes RFC 4180 quoting.
H3: MonacoEditor documents that themes are page-global.

Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 18:21:46 -04:00

75 lines
3.4 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Drivers;
/// <summary>
/// G1-F5 / F-FU2: Test Connect honours the driver's configured <c>ProbeTimeoutSeconds</c> (so it can
/// lengthen the budget, not only shorten it), aligned across Galaxy and OpcUaClient, and the button
/// drains its clear timer (R04 L2).
/// </summary>
[Trait("Category", "Unit")]
public sealed class TestConnectBudgetTests
{
[Theory]
[InlineData("{\"ProbeTimeoutSeconds\":30}", 30)] // Galaxy default — was silently 10 s
[InlineData("{\"probeTimeoutSeconds\":15}", 15)] // OpcUaClient default, camelCase
[InlineData("{\"ProbeTimeoutSeconds\":\"25\"}", 25)]
[InlineData("{\"ProbeTimeoutSeconds\":2.5}", 3)]
[InlineData("{\"ProbeTimeoutSeconds\":600}", 60)] // clamped to the actor's max
public void A_configured_timeout_is_the_budget(string json, int expected)
=> TestConnectBudget.Resolve(json).ShouldBe(expected);
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("{}")]
[InlineData("{\"ProbeTimeoutSeconds\":0}")]
[InlineData("{\"ProbeTimeoutSeconds\":-5}")]
[InlineData("{\"ProbeTimeoutSeconds\":null}")]
[InlineData("not json")]
[InlineData("[1,2]")]
[InlineData("{\"Nested\":{\"ProbeTimeoutSeconds\":40}}")] // only the root key counts
public void No_valid_configured_timeout_falls_back(string? json)
=> TestConnectBudget.Resolve(json, fallbackSeconds: 10).ShouldBe(10);
[Theory]
[InlineData("GalaxyMxGateway", 30)]
[InlineData("OpcUaClient", 15)]
[InlineData("Modbus", 5)]
[InlineData("Sql", 10)] // no documented default ⇒ the caller's fallback
public void An_absent_key_uses_the_drivers_documented_default(string driverType, int expected)
=> TestConnectBudget.Resolve("{}", fallbackSeconds: 10, driverType).ShouldBe(expected);
[Fact]
public void A_configured_value_beats_the_driver_default()
=> TestConnectBudget.Resolve("{\"ProbeTimeoutSeconds\":7}", 10, "GalaxyMxGateway").ShouldBe(7);
[Theory]
[InlineData("{\"ProbeTimeoutSeconds\":5,\"timeoutMs\":8000}", 9)] // M2: 8 s request + 1 s, never cut short
[InlineData("{\"ProbeTimeoutSeconds\":5,\"timeoutMs\":2000}", 5)] // positive control: a short request timeout changes nothing
[InlineData("{\"timeoutMs\":120000}", 60)] // still clamped
public void The_budget_never_undercuts_the_configs_request_timeout(string json, int expected)
=> TestConnectBudget.Resolve(json, 10, "Modbus").ShouldBe(expected);
[Fact]
public void The_fallback_is_clamped_too()
{
TestConnectBudget.Resolve("{}", fallbackSeconds: 0).ShouldBe(TestConnectBudget.MinSeconds);
TestConnectBudget.Resolve("{}", fallbackSeconds: 999).ShouldBe(TestConnectBudget.MaxSeconds);
}
[Fact]
public void Button_resolves_its_budget_from_the_config_and_drains_a_threading_timer()
{
var src = AdminUiSource.Read("Components", "Shared", "Drivers", "DriverTestConnectButton.razor");
src.ShouldContain("TestConnectBudget.Resolve(json, TimeoutSeconds, DriverType)");
src.ShouldContain("@implements IAsyncDisposable");
src.ShouldContain("System.Threading.Timer");
src.ShouldNotContain("System.Timers.Timer");
src.ShouldNotContain("async (_, _)");
}
}