feat(alarms): native condition Acknowledge routes to NativeAlarmAckRouter with principal [H6c]
This commit is contained in:
@@ -411,6 +411,104 @@ public sealed class AlarmCommandRouterTests : IDisposable
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>H6c — a NATIVE (driver-fed) condition's Acknowledge routes to <see
|
||||
/// cref="OtOpcUaNodeManager.NativeAlarmAckRouter"/> (the driver-bound seam), NOT the scripted
|
||||
/// <see cref="OtOpcUaNodeManager.AlarmCommandRouter"/>. With the AlarmAck role present, OnAcknowledge
|
||||
/// returns Good, the captured <see cref="NativeAlarmAck"/> carries the condition NodeId, the operator
|
||||
/// DisplayName, and the comment text, and the scripted router is NOT invoked.</summary>
|
||||
[Fact]
|
||||
public async Task Native_OnAcknowledge_routes_to_NativeAlarmAckRouter_not_scripted()
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
var scripted = new List<AlarmCommand>();
|
||||
var native = new List<NativeAlarmAck>();
|
||||
nm.AlarmCommandRouter = scripted.Add;
|
||||
nm.NativeAlarmAckRouter = native.Add;
|
||||
|
||||
nm.EnsureFolder("eq-nak1", parentNodeId: null, displayName: "Equipment NAK1");
|
||||
nm.MaterialiseAlarmCondition("alm-nak1", "eq-nak1", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true);
|
||||
var condition = nm.TryGetAlarmCondition("alm-nak1");
|
||||
condition.ShouldNotBeNull();
|
||||
condition!.OnAcknowledge.ShouldNotBeNull();
|
||||
|
||||
var ctx = SessionContext(server, "nora", OpcUaDataPlaneRoles.AlarmAck);
|
||||
var result = condition.OnAcknowledge!(ctx, condition, EventIdBytes(), new LocalizedText("native ack"));
|
||||
|
||||
result.ShouldBe(ServiceResult.Good);
|
||||
native.Count.ShouldBe(1);
|
||||
native[0].ConditionNodeId.ShouldBe("alm-nak1"); // folder-scoped condition NodeId identifier
|
||||
native[0].OperatorUser.ShouldBe("nora");
|
||||
native[0].Comment.ShouldBe("native ack");
|
||||
scripted.ShouldBeEmpty(); // scripted engine NOT involved
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>H6c — a SCRIPTED condition's Acknowledge still routes through the scripted
|
||||
/// <see cref="OtOpcUaNodeManager.AlarmCommandRouter"/> (Operation == "Acknowledge"); the native
|
||||
/// router is NOT invoked.</summary>
|
||||
[Fact]
|
||||
public async Task Scripted_OnAcknowledge_still_uses_AlarmCommandRouter()
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
var scripted = new List<AlarmCommand>();
|
||||
var native = new List<NativeAlarmAck>();
|
||||
nm.AlarmCommandRouter = scripted.Add;
|
||||
nm.NativeAlarmAckRouter = native.Add;
|
||||
|
||||
nm.EnsureFolder("eq-nak2", parentNodeId: null, displayName: "Equipment NAK2");
|
||||
nm.MaterialiseAlarmCondition("alm-nak2", "eq-nak2", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false);
|
||||
var condition = nm.TryGetAlarmCondition("alm-nak2");
|
||||
condition.ShouldNotBeNull();
|
||||
condition!.OnAcknowledge.ShouldNotBeNull();
|
||||
|
||||
var ctx = SessionContext(server, "owen", OpcUaDataPlaneRoles.AlarmAck);
|
||||
var result = condition.OnAcknowledge!(ctx, condition, EventIdBytes(), new LocalizedText("scripted ack"));
|
||||
|
||||
result.ShouldBe(ServiceResult.Good);
|
||||
scripted.Count.ShouldBe(1);
|
||||
scripted[0].AlarmId.ShouldBe("alm-nak2");
|
||||
scripted[0].Operation.ShouldBe("Acknowledge");
|
||||
scripted[0].User.ShouldBe("owen");
|
||||
scripted[0].Comment.ShouldBe("scripted ack");
|
||||
native.ShouldBeEmpty(); // native seam NOT involved
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>H6c — a NATIVE condition's Acknowledge from an anonymous / role-less identity is vetoed
|
||||
/// (BadUserAccessDenied) and NEITHER router is invoked — the gate fails closed before any route.</summary>
|
||||
[Fact]
|
||||
public async Task Native_OnAcknowledge_anonymous_is_denied()
|
||||
{
|
||||
var (host, server) = await BootAsync();
|
||||
var nm = server.NodeManager!;
|
||||
|
||||
var scripted = new List<AlarmCommand>();
|
||||
var native = new List<NativeAlarmAck>();
|
||||
nm.AlarmCommandRouter = scripted.Add;
|
||||
nm.NativeAlarmAckRouter = native.Add;
|
||||
|
||||
nm.EnsureFolder("eq-nak3", parentNodeId: null, displayName: "Equipment NAK3");
|
||||
nm.MaterialiseAlarmCondition("alm-nak3", "eq-nak3", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true);
|
||||
var condition = nm.TryGetAlarmCondition("alm-nak3");
|
||||
condition.ShouldNotBeNull();
|
||||
|
||||
// ServerSystemContext with no UserIdentity ⇒ anonymous / no role-carrying identity.
|
||||
var ctx = new ServerSystemContext(server.CurrentInstance);
|
||||
var result = condition!.OnAcknowledge!(ctx, condition, EventIdBytes(), new LocalizedText("x"));
|
||||
|
||||
result.StatusCode.Code.ShouldBe(StatusCodes.BadUserAccessDenied);
|
||||
native.ShouldBeEmpty();
|
||||
scripted.ShouldBeEmpty();
|
||||
|
||||
await host.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>A null router is a safe no-op: handler still gates + returns Good, just routes nowhere.</summary>
|
||||
[Fact]
|
||||
public async Task OnAcknowledge_with_null_router_is_safe_noop_and_returns_good()
|
||||
|
||||
Reference in New Issue
Block a user