Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ClientAlarmMethodTests.cs
T

151 lines
8.4 KiB
C#

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;
/// <summary>
/// The shared client's alarm APIs (<see cref="OpcUaClientService" />, the code behind Client.CLI's
/// <c>ack</c>/<c>confirm</c>/<c>shelve</c>) 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 <c>&lt;id&gt;.Condition</c> / <c>&lt;id&gt;.ShelvingState</c>
/// node ids); until this class it was only exercised against a fake session adapter.
/// </summary>
/// <remarks>
/// The condition id is passed exactly as a CLI user passes <c>--node</c>: the condition node's own id.
/// Each call must return Good <b>and</b> move the condition — read back from the condition events the
/// server reports, not from the call result alone. AddComment is covered on both the scripted and the native condition (the native one supports Acknowledge and AddComment only).
/// </remarks>
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<VariantCollection> 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.AddCommentAsync(
conditionId, events.LatestEventId(item, "base"), "note", ct)).ShouldBeTrue();
((await Next(++n))[RecordedEvents.CommentField].Value as LocalizedText).ShouldNotBeNull().Text.ShouldBe("note");
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);
StatusCode.IsGood(await client.AddCommentAsync(
conditionId, events.LatestEventId(item, "base"), "note", ct)).ShouldBeTrue();
await rig.DrainProjectionsAsync();
await RecordedEvents.WaitUntilAsync(() => events.ConditionEvents(item, "base").Count >= 3);
(events.ConditionEvents(item, "base")[2][RecordedEvents.CommentField].Value as LocalizedText).ShouldNotBeNull().Text.ShouldBe("note");
await subscription.DeleteAsync(true, ct);
}
finally { await rig.DisposeAsync(); }
}
private static async Task<OpcUaClientService> 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;
}
}