159 lines
7.2 KiB
C#
159 lines
7.2 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Server;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
|
using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
|
|
|
public sealed class Sdk2ConditionMethodCompatibilityTests : IAsyncLifetime
|
|
{
|
|
private readonly string _pki = Path.Combine(Path.GetTempPath(), $"otua-sdk2-methods-{Guid.NewGuid():N}");
|
|
private OpcUaApplicationHost _host = null!;
|
|
private OtOpcUaSdkServer _server = null!;
|
|
private OtOpcUaNodeManager _manager = null!;
|
|
private readonly List<AlarmCommand> _scripted = [];
|
|
private readonly List<NativeAlarmAck> _native = [];
|
|
|
|
public async ValueTask InitializeAsync()
|
|
{
|
|
var started = await OpcUaHostStart.StartAsync(port => new OpcUaApplicationHost(new OpcUaApplicationHostOptions
|
|
{
|
|
ApplicationName = "SDK2.MethodContracts", ApplicationUri = "urn:SDK2.MethodContracts",
|
|
OpcUaPort = port, PublicHostname = "localhost", PkiStoreRoot = _pki,
|
|
}, NullLogger<OpcUaApplicationHost>.Instance), () => new OtOpcUaSdkServer(), TestContext.Current.CancellationToken);
|
|
_host = started.Host;
|
|
_server = started.Server;
|
|
_manager = _server.NodeManager!;
|
|
_manager.AlarmCommandRouter = _scripted.Add;
|
|
_manager.NativeAlarmAckRouter = _native.Add;
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await _host.DisposeAsync();
|
|
Directory.Delete(_pki, true);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false, null)]
|
|
[InlineData(false, "again")]
|
|
[InlineData(true, null)]
|
|
[InlineData(true, "again")]
|
|
public void Actual_SDK_method_preserves_current_trunk_reack_and_comment(bool native, string? comment)
|
|
{
|
|
var condition = Condition(native);
|
|
Call(condition, condition.Acknowledge!, condition.EventId!.Value, "first").ShouldBe(ServiceResult.Good);
|
|
if (!native) Call(condition, condition.Confirm!, condition.EventId.Value, "confirmed").ShouldBe(ServiceResult.Good);
|
|
var before = condition.EventId.Value;
|
|
Call(condition, condition.Acknowledge!, before, comment).ShouldBe(ServiceResult.Good);
|
|
condition.AckedState!.Id!.Value.ShouldBeTrue();
|
|
condition.ConfirmedState!.Id!.Value.ShouldBeTrue();
|
|
if (comment is null) condition.EventId.Value.ShouldBe(before);
|
|
else
|
|
{
|
|
condition.EventId.Value.ShouldNotBe(before);
|
|
condition.Comment!.Value.Text.ShouldBe(comment);
|
|
}
|
|
if (native) _native.Count.ShouldBe(2, "a native re-ack is an upstream retry");
|
|
else _scripted.Count(x => x.Operation == "Acknowledge").ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void Actual_SDK_native_confirm_keeps_the_application_unsupported_status()
|
|
{
|
|
var condition = Condition(native: true);
|
|
var before = condition.EventId!.Value;
|
|
Call(condition, condition.Confirm!, before, null).StatusCode.ShouldBe(StatusCodes.BadNotSupported);
|
|
condition.ConfirmedState!.Id!.Value.ShouldBeTrue();
|
|
condition.EventId.Value.ShouldBe(before);
|
|
_native.ShouldBeEmpty();
|
|
_scripted.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Current_trunk_reack_still_requires_app_authority_primary_and_native_preflight()
|
|
{
|
|
var condition = Condition(native: true);
|
|
Call(condition, condition.Acknowledge!, condition.EventId!.Value, null).ShouldBe(ServiceResult.Good);
|
|
var before = condition.EventId.Value;
|
|
Call(condition, condition.Acknowledge!, before, null, authorized: false).StatusCode.ShouldBe(StatusCodes.BadUserAccessDenied);
|
|
_manager.NativeAlarmAckPrimaryGate = () => false;
|
|
Call(condition, condition.Acknowledge!, before, null).StatusCode.ShouldBe(StatusCodes.BadInvalidState);
|
|
_manager.NativeAlarmAckPrimaryGate = () => true;
|
|
_manager.NativeAlarmAckPreflight = _ => "driver unavailable";
|
|
Call(condition, condition.Acknowledge!, before, null).StatusCode.ShouldBe(StatusCodes.BadUserAccessDenied);
|
|
condition.EventId.Value.ShouldBe(before);
|
|
_native.Count.ShouldBe(1);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("empty")]
|
|
[InlineData("foreign")]
|
|
[InlineData("superseded")]
|
|
[InlineData("disabled")]
|
|
public void Actual_SDK_method_keeps_event_and_enabled_guards(string invalid)
|
|
{
|
|
var condition = Condition(native: false);
|
|
var original = condition.EventId!.Value;
|
|
Call(condition, condition.Acknowledge!, original, null).ShouldBe(ServiceResult.Good);
|
|
var current = condition.EventId.Value;
|
|
var token = invalid switch
|
|
{
|
|
"empty" => default,
|
|
"foreign" => ByteString.From(Guid.NewGuid().ToByteArray()),
|
|
"superseded" => original,
|
|
_ => current,
|
|
};
|
|
if (invalid == "disabled") condition.SetEnableState(Context(), false);
|
|
var routes = _scripted.Count;
|
|
ServiceResult.IsBad(Call(condition, condition.Acknowledge!, token, null)).ShouldBeTrue();
|
|
_scripted.Count.ShouldBe(routes);
|
|
condition.EventId.Value.ShouldBe(current);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("OffNormalAlarm")]
|
|
[InlineData("DiscreteAlarm")]
|
|
[InlineData("AlarmCondition")]
|
|
public void An_acknowledged_branch_is_constructible_and_does_not_get_the_trunk_repeat_waiver(string type)
|
|
{
|
|
var condition = Condition(native: false, type);
|
|
var branch = condition.CreateBranch(Context(), new NodeId(Guid.NewGuid())).ShouldBeAssignableTo<AlarmConditionState>()!;
|
|
branch.EventId!.Value = ByteString.From(Guid.NewGuid().ToByteArray());
|
|
branch.SetAcknowledgedState(Context(), true);
|
|
var routes = _scripted.Count;
|
|
Call(condition, condition.Acknowledge!, branch.EventId!.Value, null).StatusCode.ShouldBe(StatusCodes.BadConditionBranchAlreadyAcked);
|
|
_scripted.Count.ShouldBe(routes);
|
|
}
|
|
|
|
private AlarmConditionState Condition(bool native, string type = "OffNormalAlarm")
|
|
{
|
|
var realm = native ? AddressSpaceRealm.Raw : AddressSpaceRealm.Uns;
|
|
_manager.EnsureFolder("eq", null, "Equipment", realm);
|
|
_manager.MaterialiseAlarmCondition("alarm", "eq", "Alarm", type, 700, realm, isNative: native);
|
|
_manager.WriteAlarmCondition("alarm", new AlarmConditionSnapshot(true, false, true, true,
|
|
AlarmShelvingKind.Unshelved, 700, "active"), DateTime.UtcNow, realm);
|
|
return _manager.TryGetAlarmCondition("alarm", realm)!;
|
|
}
|
|
|
|
private ServerSystemContext Context(bool authorized = true) => new(_server.CurrentInstance)
|
|
{
|
|
UserIdentity = new RoleCarryingUserIdentity(new AnonymousIdentityToken(),
|
|
authorized ? [OpcUaDataPlaneRoles.AlarmAck] : [], "operator", null),
|
|
};
|
|
|
|
private ServiceResult Call(AlarmConditionState condition, AddCommentMethodState method,
|
|
ByteString eventId, string? comment, bool authorized = true)
|
|
{
|
|
var argumentErrors = new List<ServiceResult>();
|
|
var result = method.Call(Context(authorized), condition.NodeId,
|
|
[Variant.From(eventId), Variant.From(comment is null ? default(LocalizedText) : new LocalizedText(comment))],
|
|
argumentErrors, new List<Variant>());
|
|
argumentErrors.All(ServiceResult.IsGood).ShouldBeTrue();
|
|
return result;
|
|
}
|
|
}
|