Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ScriptList/ScriptListQueriesTests.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

74 lines
3.0 KiB
C#

using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.ScriptList;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.ScriptList;
/// <summary>R04 P8: the <c>/scripts</c> list is a projection without source; source loads per dialog.</summary>
[Trait("Category", "Unit")]
public sealed class ScriptListQueriesTests
{
private static OtOpcUaConfigDbContext NewDb(string name) =>
new(new DbContextOptionsBuilder<OtOpcUaConfigDbContext>().UseInMemoryDatabase(name).Options);
private static async Task<string> SeedAsync()
{
var name = Guid.NewGuid().ToString("N");
await using var db = NewDb(name);
db.Scripts.AddRange(
new Script { ScriptId = "s-b", Name = "Beta", SourceCode = "return 2;", SourceHash = "hb" },
new Script { ScriptId = "s-a", Name = "Alpha", SourceCode = "return 1;", SourceHash = "ha", Language = "CSharp" });
await db.SaveChangesAsync();
return name;
}
[Fact]
public async Task LoadRowsAsync_returns_name_ordered_rows_without_source()
{
var name = await SeedAsync();
await using var db = NewDb(name);
var rows = await ScriptListQueries.LoadRowsAsync(db);
rows.Select(r => r.ScriptId).ShouldBe(["s-a", "s-b"]);
rows[0].ShouldBe(new ScriptListRow("s-a", "Alpha", "CSharp", "ha"));
// The row type has no source member at all — the list cannot load it by accident.
typeof(ScriptListRow).GetProperty("SourceCode").ShouldBeNull();
}
[Fact]
public async Task LoadSourceAsync_returns_one_scripts_source_or_null_when_gone()
{
var name = await SeedAsync();
await using var db = NewDb(name);
(await ScriptListQueries.LoadSourceAsync(db, "s-b")).ShouldBe("return 2;");
(await ScriptListQueries.LoadSourceAsync(db, "missing")).ShouldBeNull();
}
[Fact]
public void Scripts_page_uses_the_projection()
{
var src = AdminUiSource.Read("Components", "Pages", "Scripts.razor");
src.ShouldContain("ScriptListQueries.LoadRowsAsync(");
src.ShouldNotContain("List<Script>");
}
[Fact]
public void Source_fetch_failure_is_shown_in_the_dialog_not_thrown()
{
// Review r0 M7 source pin: the fetch sits inside a try whose catch sets _sourceError.
var src = AdminUiSource.Read("Components", "Pages", "Scripts.razor");
var fetch = src.IndexOf("ScriptListQueries.LoadSourceAsync(", StringComparison.Ordinal);
var tryAt = src.LastIndexOf("try", fetch, StringComparison.Ordinal);
var catchAt = src.IndexOf("catch (Exception ex)", fetch, StringComparison.Ordinal);
tryAt.ShouldBeGreaterThan(src.IndexOf("private async Task ShowSource", StringComparison.Ordinal));
catchAt.ShouldBeGreaterThan(fetch);
src.IndexOf("_sourceError = ", catchAt, StringComparison.Ordinal).ShouldBeGreaterThan(catchAt);
src.ShouldContain("@_sourceError");
}
}