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