From 87dd65b97a0d3ea783d601473287343c8d335329 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 15 Jun 2026 14:39:26 -0400 Subject: [PATCH] test(alarms): native ack wrong-role deny + tidy NativeAlarmAck doc (code-review) --- .../NativeAlarmAck.cs | 6 ++-- .../AlarmCommandRouterTests.cs | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/NativeAlarmAck.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/NativeAlarmAck.cs index 7595ebd2..a3a9842b 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/NativeAlarmAck.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/NativeAlarmAck.cs @@ -12,9 +12,9 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer; /// ref, the operator's acknowledge comment, and the authenticated operator's display name. /// /// -/// The acknowledged condition's folder-scoped NodeId identifier string -/// (the same value the DriverHostActor inverse map keys native conditions by), used to resolve the -/// command back to its backing driver + alarm ref. +/// The folder-scoped condition NodeId identifier string — the same value +/// stored in DriverHostActor._alarmNodeIdByDriverRef (keyed by (DriverInstanceId, FullName)), +/// used to resolve the ack back to its backing driver. /// The operator's acknowledge comment text, or null when none was supplied. /// The authenticated operator's display name (empty when none resolves). public sealed record NativeAlarmAck(string ConditionNodeId, string? Comment, string OperatorUser); diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs index c2fbb8a2..cf683ac4 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs @@ -480,6 +480,35 @@ public sealed class AlarmCommandRouterTests : IDisposable await host.DisposeAsync(); } + /// H6c — a NATIVE condition's Acknowledge from a non-null identity that lacks the + /// AlarmAck role is vetoed (BadUserAccessDenied) and NEITHER router is invoked — the gate + /// fails closed before any route, exactly as for scripted conditions. + [Fact] + public async Task Native_OnAcknowledge_without_AlarmAck_returns_denied_and_routes_nothing() + { + var (host, server) = await BootAsync(); + var nm = server.NodeManager!; + + var scripted = new List(); + var native = new List(); + nm.AlarmCommandRouter = scripted.Add; + nm.NativeAlarmAckRouter = native.Add; + + nm.EnsureFolder("eq-nak4", parentNodeId: null, displayName: "Equipment NAK4"); + nm.MaterialiseAlarmCondition("alm-nak4", "eq-nak4", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true); + var condition = nm.TryGetAlarmCondition("alm-nak4"); + condition.ShouldNotBeNull(); + + var ctx = SessionContext(server, "pete", "ReadOnly"); // non-null identity, but no AlarmAck + var result = condition!.OnAcknowledge!(ctx, condition, EventIdBytes(), new LocalizedText("nope")); + + result.StatusCode.Code.ShouldBe(StatusCodes.BadUserAccessDenied); + native.ShouldBeEmpty(); + scripted.ShouldBeEmpty(); + + await host.DisposeAsync(); + } + /// 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. [Fact]