using Opc.Ua; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Client.Shared; using ZB.MOM.WW.OtOpcUa.Client.Shared.Models; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using static ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.ServerEventRig; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests; /// /// The shared client's alarm APIs (, the code behind Client.CLI's /// ack/confirm/shelve) against the real in-process server, over the wire, with the real /// session adapter. Commit 85f00327 moved the client onto the Part 9 method ids invoked on the condition /// node itself (it used to target nonexistent <id>.Condition / <id>.ShelvingState /// node ids); until this class it was only exercised against a fake session adapter. /// /// /// The condition id is passed exactly as a CLI user passes --node: the condition node's own id. /// Each call must return Good and move the condition — read back from the condition events the /// server reports, not from the call result alone. The client exposes no AddComment API, so it is not covered. /// public sealed class ClientAlarmMethodTests { private const NodePermissions All = NodePermissions.Browse | NodePermissions.Read | NodePermissions.Subscribe | NodePermissions.AlarmRead | NodePermissions.AlarmAcknowledge | NodePermissions.AlarmConfirm | NodePermissions.AlarmShelve | NodePermissions.MethodCall; [Fact] public async Task The_client_acknowledges_confirms_and_shelves_a_scripted_condition() { var ct = TestContext.Current.CancellationToken; var rig = await StartAsync("ClientAlarmScripted", encrypted: false, ct); try { rig.Enforce(false, [Key()], ClusterGrant(KeyPrincipal, "acl-key", All)); rig.MaterialiseScripted(); var current = ConditionMethodEventTests.PreState("Acknowledge", native: false) with { Message = "base" }; ConditionMethodEventTests.WireSimulatedOwners(rig, native: false, () => current, s => current = s); using var observer = await rig.ConnectAsync(anonymous: false, ct); var events = new RecordedEvents(); var subscription = await RecordedEvents.SubscribeAsync(observer, events, ct); var item = RecordedEvents.AddEventItem(subscription, ObjectIds.Server); await subscription.ApplyChangesAsync(ct); rig.Project(native: false, current); await RecordedEvents.WaitUntilAsync(() => events.Has(item, "base")); using var client = await ConnectClientAsync(rig, ct); var conditionId = ConditionNodeId(observer, native: false).ToString(); async Task Next(int expectedCount) { await rig.DrainProjectionsAsync(); await RecordedEvents.WaitUntilAsync(() => events.ConditionEvents(item, "base").Count >= expectedCount); return events.ConditionEvents(item, "base")[expectedCount - 1]; } var n = 1; StatusCode.IsGood(await client.AcknowledgeAlarmAsync( conditionId, events.LatestEventId(item, "base"), "ack", ct)).ShouldBeTrue(); (await Next(++n))[RecordedEvents.AckedField].Value.ShouldBe(true); StatusCode.IsGood(await client.ConfirmAlarmAsync( conditionId, events.LatestEventId(item, "base"), "confirm", ct)).ShouldBeTrue(); (await Next(++n))[RecordedEvents.ConfirmedField].Value.ShouldBe(true); StatusCode.IsGood(await client.ShelveAlarmAsync(conditionId, ShelveKind.OneShot, 0, ct)).ShouldBeTrue(); (await Next(++n))[RecordedEvents.ShelvingField].Value.ShouldBe(ObjectIds.ShelvedStateMachineType_OneShotShelved); StatusCode.IsGood(await client.ShelveAlarmAsync(conditionId, ShelveKind.Unshelve, 0, ct)).ShouldBeTrue(); (await Next(++n))[RecordedEvents.ShelvingField].Value.ShouldBe(ObjectIds.ShelvedStateMachineType_Unshelved); StatusCode.IsGood(await client.ShelveAlarmAsync(conditionId, ShelveKind.Timed, 60, ct)).ShouldBeTrue(); (await Next(++n))[RecordedEvents.ShelvingField].Value.ShouldBe(ObjectIds.ShelvedStateMachineType_TimedShelved); StatusCode.IsGood(await client.ShelveAlarmAsync(conditionId, ShelveKind.Unshelve, 0, ct)).ShouldBeTrue(); (await Next(++n))[RecordedEvents.ShelvingField].Value.ShouldBe(ObjectIds.ShelvedStateMachineType_Unshelved); await subscription.DeleteAsync(true, ct); } finally { await rig.DisposeAsync(); } } [Fact] public async Task The_client_acknowledges_a_native_condition_and_is_refused_confirm_and_shelve() { var ct = TestContext.Current.CancellationToken; var rig = await StartAsync("ClientAlarmNative", encrypted: false, ct); try { rig.Enforce(false, [Key()], ClusterGrant(KeyPrincipal, "acl-key", All)); rig.MaterialiseNative(); var current = ConditionMethodEventTests.PreState("Acknowledge", native: true) with { Message = "base" }; ConditionMethodEventTests.WireSimulatedOwners(rig, native: true, () => current, s => current = s); using var observer = await rig.ConnectAsync(anonymous: false, ct); var events = new RecordedEvents(); var subscription = await RecordedEvents.SubscribeAsync(observer, events, ct); var item = RecordedEvents.AddEventItem(subscription, ObjectIds.Server); await subscription.ApplyChangesAsync(ct); rig.Project(native: true, current); await RecordedEvents.WaitUntilAsync(() => events.Has(item, "base")); using var client = await ConnectClientAsync(rig, ct); var conditionId = ConditionNodeId(observer, native: true).ToString(); var eventId = events.LatestEventId(item, "base"); // Refusals first: they must not disturb the condition, so the ack below still lands as event #2. (await client.ConfirmAlarmAsync(conditionId, eventId, "confirm", ct)).Code.ShouldBe(StatusCodes.BadNotSupported); (await client.ShelveAlarmAsync(conditionId, ShelveKind.OneShot, 0, ct)).Code.ShouldBe(StatusCodes.BadNotSupported); (await client.ShelveAlarmAsync(conditionId, ShelveKind.Timed, 60, ct)).Code.ShouldBe(StatusCodes.BadNotSupported); StatusCode.IsGood(await client.AcknowledgeAlarmAsync(conditionId, eventId, "ack", ct)).ShouldBeTrue(); await rig.DrainProjectionsAsync(); await RecordedEvents.WaitUntilAsync(() => events.ConditionEvents(item, "base").Count >= 2); var ack = events.ConditionEvents(item, "base")[1]; ack[RecordedEvents.AckedField].Value.ShouldBe(true); ack[RecordedEvents.ConfirmedField].Value.ShouldBe(true); ack[RecordedEvents.ShelvingField].Value.ShouldBe(ObjectIds.ShelvedStateMachineType_Unshelved); await subscription.DeleteAsync(true, ct); } finally { await rig.DisposeAsync(); } } private static async Task ConnectClientAsync(ServerEventRig rig, CancellationToken ct) { var client = new OpcUaClientService(); await client.ConnectAsync(new ConnectionSettings { EndpointUrl = rig.EndpointUrl, Username = "apikey", Password = $"otua_{KeyId}_secret", CertificateStorePath = Path.Combine(Path.GetTempPath(), $"otopcua-clientpki-{Guid.NewGuid():N}"), }, ct); return client; } }