Honors the ADR-002 discriminator at OPC UA Read/Write dispatch time. Virtual tag reads route to the VirtualTagEngine-backed IReadable; scripted alarm reads route to the ScriptedAlarmEngine-backed IReadable; driver reads continue to route to the driver's own IReadable (no regression for any existing driver test). ## Changes DriverNodeManager ctor gains optional `virtualReadable` + `scriptedAlarmReadable` parameters. When callers omit them (every existing driver test) the manager behaves exactly as before. SealedBootstrap wires the engines' IReadable adapters once the Phase 7 composition root is added. Per-variable NodeSourceKind tracked in `_sourceByFullRef` during Variable() registration alongside the existing `_writeIdempotentByFullRef` / `_securityByFullRef` maps. OnReadValue now picks the IReadable by source kind via the new internal SelectReadable helper. When the engine-backed IReadable isn't wired (virtual tag node but no engine provided), returns BadNotFound rather than silently falling back to the driver — surfaces a misconfiguration instead of masking it. OnWriteValue gates on IsWriteAllowedBySource which returns true only for Driver. Plan decision #6: virtual tags + scripted alarms reject direct OPC UA writes with BadUserAccessDenied. Scripts write virtual tags via `ctx.SetVirtualTag`; operators ack alarms via the Part 9 method nodes. ## Tests — 7/7 (internal helpers exposed via InternalsVisibleTo) DriverNodeManagerSourceDispatchTests covers: - Driver source routes to driver IReadable - Virtual source routes to virtual IReadable - ScriptedAlarm source routes to alarm IReadable - Virtual source with null virtual IReadable returns null (→ BadNotFound) - ScriptedAlarm source with null alarm IReadable returns null - Driver source with null driver IReadable returns null (preserves BadNotReadable) - IsWriteAllowedBySource: only Driver=true (Virtual=false, ScriptedAlarm=false) Full solution builds clean. Phase 7 test total now 197 green.
90 lines
3.3 KiB
C#
90 lines
3.3 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Server.Tests;
|
|
|
|
/// <summary>
|
|
/// Phase 7 Stream G follow-up — verifies the NodeSourceKind dispatch kernel that
|
|
/// DriverNodeManager's OnReadValue + OnWriteValue use to route per-node calls to
|
|
/// the right backend per ADR-002. Pure functions; no OPC UA stack required.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class DriverNodeManagerSourceDispatchTests
|
|
{
|
|
private sealed class FakeReadable : IReadable
|
|
{
|
|
public string Name { get; init; } = "";
|
|
public Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
|
|
IReadOnlyList<string> fullReferences, CancellationToken cancellationToken) =>
|
|
Task.FromResult<IReadOnlyList<DataValueSnapshot>>([]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Driver_source_routes_to_driver_readable()
|
|
{
|
|
var drv = new FakeReadable { Name = "drv" };
|
|
var vt = new FakeReadable { Name = "vt" };
|
|
var al = new FakeReadable { Name = "al" };
|
|
|
|
DriverNodeManager.SelectReadable(NodeSourceKind.Driver, drv, vt, al).ShouldBeSameAs(drv);
|
|
}
|
|
|
|
[Fact]
|
|
public void Virtual_source_routes_to_virtual_readable()
|
|
{
|
|
var drv = new FakeReadable();
|
|
var vt = new FakeReadable();
|
|
var al = new FakeReadable();
|
|
|
|
DriverNodeManager.SelectReadable(NodeSourceKind.Virtual, drv, vt, al).ShouldBeSameAs(vt);
|
|
}
|
|
|
|
[Fact]
|
|
public void ScriptedAlarm_source_routes_to_alarm_readable()
|
|
{
|
|
var drv = new FakeReadable();
|
|
var vt = new FakeReadable();
|
|
var al = new FakeReadable();
|
|
|
|
DriverNodeManager.SelectReadable(NodeSourceKind.ScriptedAlarm, drv, vt, al).ShouldBeSameAs(al);
|
|
}
|
|
|
|
[Fact]
|
|
public void Virtual_source_without_virtual_readable_returns_null()
|
|
{
|
|
// Engine not wired → dispatch layer surfaces BadNotFound (the null propagates
|
|
// through to the OnReadValue null-check).
|
|
DriverNodeManager.SelectReadable(
|
|
NodeSourceKind.Virtual, driverReadable: new FakeReadable(),
|
|
virtualReadable: null, scriptedAlarmReadable: null).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ScriptedAlarm_source_without_alarm_readable_returns_null()
|
|
{
|
|
DriverNodeManager.SelectReadable(
|
|
NodeSourceKind.ScriptedAlarm, driverReadable: new FakeReadable(),
|
|
virtualReadable: new FakeReadable(), scriptedAlarmReadable: null).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Driver_source_without_driver_readable_returns_null()
|
|
{
|
|
// Pre-existing BadNotReadable behavior — unchanged by Phase 7 wiring.
|
|
DriverNodeManager.SelectReadable(
|
|
NodeSourceKind.Driver, driverReadable: null,
|
|
virtualReadable: new FakeReadable(), scriptedAlarmReadable: new FakeReadable()).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void IsWriteAllowedBySource_only_Driver_returns_true()
|
|
{
|
|
// Plan decision #6 — OPC UA writes to virtual tags / scripted alarms rejected.
|
|
DriverNodeManager.IsWriteAllowedBySource(NodeSourceKind.Driver).ShouldBeTrue();
|
|
DriverNodeManager.IsWriteAllowedBySource(NodeSourceKind.Virtual).ShouldBeFalse();
|
|
DriverNodeManager.IsWriteAllowedBySource(NodeSourceKind.ScriptedAlarm).ShouldBeFalse();
|
|
}
|
|
}
|