Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Server.Tests/DriverNodeManagerSourceDispatchTests.cs
Joseph Doherty f0851af6b5 Phase 7 Stream G follow-up — DriverNodeManager dispatch routing by NodeSourceKind
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.
2026-04-20 20:12:17 -04:00

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();
}
}