From e5a03e3cceeeabb0be469cad0a9ab1cd0d8f3537 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 12 Nov 2026 08:54:43 -0500 Subject: [PATCH] test(acl): pin the fail-closed rule for non-condition events on folder items (#838 item 3) A folder event item under enforcement withholds an event that names no live condition; flag off, the SDK's own type + SourceNode check applies. Positive controls: the same event reaches the item flag-off and a condition event reaches it flag-on. Mutation-checked: IsNotifierEventVisibleTo returning true for a non-condition event fails the test. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi --- .../FolderEventVerdictInvalidationTests.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/FolderEventVerdictInvalidationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/FolderEventVerdictInvalidationTests.cs index f20a6ea7a..f820e5f75 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/FolderEventVerdictInvalidationTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/FolderEventVerdictInvalidationTests.cs @@ -167,6 +167,55 @@ public sealed class FolderEventVerdictInvalidationTests "MonitoredNode2.InvalidatePermissionCaches() is gone — re-check InvalidateFolderEventVerdicts against the new SDK"); } + /// + /// Gitea #838 item 3: an event on a folder that names no live condition (none is produced today — model-change and + /// rejected-write audits go to the Server object) is WITHHELD under enforcement, fail closed, and delivered with + /// the flag off (the SDK's own type + SourceNode check only). Positive controls: the same event reaches the same + /// item with enforcement off, and a condition event reaches it with enforcement on. + /// + [Fact] + public async Task A_non_condition_event_on_a_folder_is_withheld_under_enforcement_and_delivered_without_it() + { + var ct = TestContext.Current.CancellationToken; + await using var rig = await ServerEventRig.StartAsync("Folder838NonCondition", encrypted: false, ct); + rig.KeyRoles = []; + rig.MaterialiseNative(); + rig.Enforce(false, [], ServerEventRig.EquipmentGrant(EqA, NodePermissions.ReadOnly)); + + using var session = await rig.ConnectAsync(anonymous: true, ct); + var events = new RecordedEvents(); + var subscription = await RecordedEvents.SubscribeAsync(session, events, ct); + var onA = RecordedEvents.AddEventItem(subscription, ServerEventRig.UnsNodeId(session, EqA)); + await subscription.ApplyChangesAsync(ct); + onA.Status.Error.ShouldBeNull(); + + var folder = rig.Nm.FindPredefinedNode(ServerEventRig.UnsNodeId(session, EqA)).ShouldNotBeNull(); + void ReportPlainEvent(string message) + { + var e = new BaseEventState(null); + lock (rig.Nm.Lock) + { + e.Initialize(rig.Nm.SystemContext, folder, EventSeverity.Low, new LocalizedText(message)); + folder.ReportEvent(rig.Nm.SystemContext, e); + } + } + + ReportPlainEvent("plain-off"); + await RecordedEvents.WaitUntilAsync(() => events.Has(onA, "plain-off")); + + rig.Enforce(true, [], ServerEventRig.EquipmentGrant(EqA, NodePermissions.ReadOnly)); + rig.Project(native: true, State("cond-on")); + await RecordedEvents.WaitUntilAsync(() => events.Has(onA, "cond-on")); + + ReportPlainEvent("plain-on"); + rig.Project(native: true, State("cond-on-2")); + await RecordedEvents.WaitUntilAsync(() => events.Has(onA, "cond-on-2")); + await Task.Delay(TimeSpan.FromMilliseconds(700), ct); + events.Has(onA, "plain-on").ShouldBeFalse("a non-condition event on a folder fails closed under enforcement"); + + await session.CloseAsync(ct); + } + private static AlarmConditionSnapshot State(string message) => new(true, false, false, true, AlarmShelvingKind.Unshelved, 700, message);