48bd266e19
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
151 lines
8.4 KiB
C#
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><id>.Condition</c> / <c><id>.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;
|
|
}
|
|
}
|