/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Moq; using NUnit.Framework; using Opc.Ua.Server.TestFramework; using Opc.Ua.Tests; namespace Opc.Ua.Server.Tests { /// /// Unit tests for the NodeManagement dispatcher methods on /// : per-item validation paths /// (BadNothingToDo / BadBrowseNameInvalid / BadParentNodeIdInvalid / /// BadReferenceTypeIdInvalid / BadReferenceNotAllowed / /// BadSourceNodeIdInvalid / BadTargetNodeIdInvalid / /// BadNodeIdInvalid / BadUserAccessDenied) and per-item routing. /// [TestFixture] [Category("MasterNodeManager")] [Category("NodeManagement")] [SetCulture("en-us")] [SetUICulture("en-us")] [NonParallelizable] public class MasterNodeManagerNodeManagementTests { private const string TestNamespaceUri = "http://test.org/UA/MasterNodeManagement/"; private const string E2eNamespaceUri = "http://test.org/UA/MasterNodeManagement/E2E/"; private ServerFixture m_fixture = null!; private StandardServer m_server = null!; [OneTimeSetUp] public async Task OneTimeSetUpAsync() { m_fixture = new ServerFixture(t => new StandardServer(t)); m_server = await m_fixture.StartAsync().ConfigureAwait(false); } [OneTimeTearDown] public async Task OneTimeTearDownAsync() { await m_fixture.StopAsync().ConfigureAwait(false); } [TestCase(RequestType.AddNodes, false)] [TestCase(RequestType.DeleteNodes, false)] [TestCase(RequestType.AddReferences, false)] [TestCase(RequestType.DeleteReferences, false)] [TestCase(RequestType.AddNodes, true)] [TestCase(RequestType.DeleteNodes, true)] [TestCase(RequestType.AddReferences, true)] [TestCase(RequestType.DeleteReferences, true)] public async Task NodeManagementOwnerFailureDoesNotAbortBatch( RequestType requestType, bool unrelatedCancellation) { using var harness = new AuthorizationHarness(); Exception failure = unrelatedCancellation ? new OperationCanceledException("The owner's backend cancelled.") : new InvalidOperationException("The owner failed."); ArrayOf results = await DispatchFailingNodeManagementBatchAsync( harness, requestType, () => failure, CancellationToken.None).ConfigureAwait(false); Assert.That(results, Is.EqualTo( [ StatusCodes.Good, StatusCodes.BadUnexpectedError, StatusCodes.Good ])); } [TestCase(RequestType.AddNodes)] [TestCase(RequestType.DeleteNodes)] [TestCase(RequestType.AddReferences)] [TestCase(RequestType.DeleteReferences)] public void NodeManagementRequestCancellationPropagates(RequestType requestType) { using var harness = new AuthorizationHarness(); using var cancellation = new CancellationTokenSource(); Assert.ThrowsAsync(async () => await DispatchFailingNodeManagementBatchAsync( harness, requestType, () => { cancellation.Cancel(); return new OperationCanceledException(cancellation.Token); }, cancellation.Token).ConfigureAwait(false)); } [TestCase("")] [TestCase(null)] public void InvalidPathBrowseNameReturnsOperationStatus(string? name) { var factory = new DefaultNodeIdFactory(); var namespaces = new NamespaceTable(); ushort namespaceIndex = (ushort)namespaces.Append(TestNamespaceUri); ServiceResultException exception = Assert.Throws(() => factory.CreateChildNodeId( NodeId.Null, new QualifiedName(name, namespaceIndex), namespaceIndex, namespaces))!; Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadBrowseNameInvalid)); } [Test] public async Task AddNodesAsync_NullItem_ReturnsBadNothingToDoAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); (ArrayOf results, _) = await sut.AddNodesAsync( ctx, new AddNodesItem[] { null! }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results.Count, Is.EqualTo(1)); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNothingToDo)); } [Test] public async Task AddNodesAsync_NullBrowseName_ReturnsBadBrowseNameInvalidAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); var item = new AddNodesItem { ParentNodeId = ObjectIds.ObjectsFolder, ReferenceTypeId = ReferenceTypeIds.Organizes, NodeClass = NodeClass.Object }; (ArrayOf results, _) = await sut.AddNodesAsync( ctx, new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadBrowseNameInvalid)); } [Test] public async Task AddNodesAsync_NullParentNodeId_ReturnsBadParentNodeIdInvalidAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); var item = new AddNodesItem { ReferenceTypeId = ReferenceTypeIds.Organizes, BrowseName = new QualifiedName("Test", 2), NodeClass = NodeClass.Object }; (ArrayOf results, _) = await sut.AddNodesAsync( ctx, new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadParentNodeIdInvalid)); } [Test] public async Task AddNodesAsync_NullReferenceTypeId_ReturnsBadReferenceTypeIdInvalidAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); var item = new AddNodesItem { ParentNodeId = ObjectIds.ObjectsFolder, BrowseName = new QualifiedName("Test", 2), NodeClass = NodeClass.Object }; (ArrayOf results, _) = await sut.AddNodesAsync( ctx, new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadReferenceTypeIdInvalid)); } [Test] public async Task AddNodesAsync_NonHierarchicalReferenceType_ReturnsBadReferenceNotAllowedAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); var item = new AddNodesItem { ParentNodeId = ObjectIds.ObjectsFolder, ReferenceTypeId = ReferenceTypeIds.HasTypeDefinition, BrowseName = new QualifiedName("Test", 2), NodeClass = NodeClass.Object }; (ArrayOf results, _) = await sut.AddNodesAsync( ctx, new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadReferenceNotAllowed)); } [Test] public async Task AddNodesAsyncRequestedIdInUnownedNamespaceReturnsBadNodeIdRejectedAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); var item = new AddNodesItem { ParentNodeId = ObjectIds.RootFolder, ReferenceTypeId = ReferenceTypeIds.Organizes, RequestedNewNodeId = new NodeId("Requested", ushort.MaxValue), BrowseName = new QualifiedName("Requested", ushort.MaxValue), NodeClass = NodeClass.Object }; (ArrayOf results, _) = await sut.AddNodesAsync( ctx, new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdRejected)); } [Test] public async Task AddNodesAsyncRequestedIdInOptedOutNamespaceReturnsBadNodeIdRejectedAsync() { Mock manager = CreateNodeManagementManager(false); using MasterNodeManager sut = CreateMasterNodeManager(manager.Object); ushort namespaceIndex = GetTestNamespaceIndex(); var item = new AddNodesItem { ParentNodeId = ObjectIds.RootFolder, ReferenceTypeId = ReferenceTypeIds.Organizes, RequestedNewNodeId = new NodeId("Requested", namespaceIndex), BrowseName = new QualifiedName("Requested", namespaceIndex), NodeClass = NodeClass.Object }; (ArrayOf results, _) = await sut.AddNodesAsync( CreateContext(), new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdRejected)); } [Test] public async Task AddNodesAsyncRequestedIdInSupportedNamespaceIsDispatchedAsync() { Mock manager = CreateNodeManagementManager(true); using MasterNodeManager sut = CreateMasterNodeManager(manager.Object); ushort namespaceIndex = GetTestNamespaceIndex(); NodeId parentNodeId = ConfigureParent(manager, namespaceIndex); var requestedNodeId = new NodeId("Requested", namespaceIndex); manager .Setup(m => m.AddNodeAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((ServiceResult.Good, requestedNodeId)); var item = new AddNodesItem { ParentNodeId = parentNodeId, ReferenceTypeId = ReferenceTypeIds.Organizes, RequestedNewNodeId = requestedNodeId, BrowseName = new QualifiedName("Requested", namespaceIndex), NodeClass = NodeClass.Object }; (ArrayOf results, _) = await sut.AddNodesAsync( CreateContext(), new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.Good)); Assert.That(results[0].AddedNodeId, Is.EqualTo(requestedNodeId)); } [Test] public async Task AddNodesAsyncAuthorizationFailureFromOwnerIsPreservedAsync() { Mock manager = CreateNodeManagementManager(true); using MasterNodeManager sut = CreateMasterNodeManager(manager.Object); ushort namespaceIndex = GetTestNamespaceIndex(); NodeId parentNodeId = ConfigureParent(manager, namespaceIndex); manager .Setup(m => m.AddNodeAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(( new ServiceResult(StatusCodes.BadUserAccessDenied), NodeId.Null)); var item = new AddNodesItem { ParentNodeId = parentNodeId, ReferenceTypeId = ReferenceTypeIds.Organizes, RequestedNewNodeId = new NodeId("Requested", namespaceIndex), BrowseName = new QualifiedName("Requested", namespaceIndex), NodeClass = NodeClass.Object }; (ArrayOf results, _) = await sut.AddNodesAsync( CreateContext(), new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadUserAccessDenied)); } [Test] public async Task AddNodesAsyncHasSubtypeWithIncompatibleNodeClassesReturnsBadReferenceNotAllowedAsync() { var item = new AddNodesItem { ParentNodeId = ObjectIds.RootFolder, ReferenceTypeId = ReferenceTypeIds.HasSubtype, BrowseName = new QualifiedName("Variable", 2), NodeClass = NodeClass.Variable }; (ArrayOf results, _) = await m_server.CurrentInstance.NodeManager.AddNodesAsync( CreateContext(), new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadReferenceNotAllowed)); } [TestCase(ValidReferenceKind.Organizes, NodeClass.Object)] [TestCase(ValidReferenceKind.HasComponent, NodeClass.Variable)] [TestCase(ValidReferenceKind.HasProperty, NodeClass.Variable)] public async Task AddNodesAsyncValidReferenceNodeClassesAreNotRejectedAsync( ValidReferenceKind referenceKind, NodeClass nodeClass) { Mock manager = CreateNodeManagementManager(true); using MasterNodeManager sut = CreateMasterNodeManager(manager.Object); ushort namespaceIndex = GetTestNamespaceIndex(); NodeId parentNodeId = ConfigureParent(manager, namespaceIndex); var addedNodeId = new NodeId($"Added{referenceKind}", namespaceIndex); manager .Setup(m => m.AddNodeAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((ServiceResult.Good, addedNodeId)); NodeId referenceTypeId = referenceKind switch { ValidReferenceKind.Organizes => ReferenceTypeIds.Organizes, ValidReferenceKind.HasComponent => ReferenceTypeIds.HasComponent, _ => ReferenceTypeIds.HasProperty }; var item = new AddNodesItem { ParentNodeId = parentNodeId, ReferenceTypeId = referenceTypeId, BrowseName = new QualifiedName($"Added{referenceKind}", namespaceIndex), NodeClass = nodeClass }; (ArrayOf results, _) = await sut.AddNodesAsync( CreateContext(), new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.Good)); } [Test] public async Task DeleteNodesAsync_NullItem_ReturnsBadNothingToDoAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); (ArrayOf results, _) = await sut.DeleteNodesAsync( ctx, new DeleteNodesItem[] { null! }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadNothingToDo)); } [Test] public async Task DeleteNodesAsync_NullNodeId_ReturnsBadNodeIdInvalidAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); var item = new DeleteNodesItem { DeleteTargetReferences = false }; (ArrayOf results, _) = await sut.DeleteNodesAsync( ctx, new DeleteNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadNodeIdInvalid)); } [Test] public async Task DeleteNodesAsync_UnknownNodeId_ReturnsBadNodeIdUnknownAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); var item = new DeleteNodesItem { NodeId = new NodeId("DoesNotExist", 999), DeleteTargetReferences = false }; (ArrayOf results, _) = await sut.DeleteNodesAsync( ctx, new DeleteNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadNodeIdUnknown)); } [Test] public async Task AddReferencesAsync_NullItem_ReturnsBadNothingToDoAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); (ArrayOf results, _) = await sut.AddReferencesAsync( ctx, new AddReferencesItem[] { null! }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadNothingToDo)); } [Test] public async Task AddReferencesAsync_NullSourceNodeId_ReturnsBadSourceNodeIdInvalidAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); var item = new AddReferencesItem { ReferenceTypeId = ReferenceTypeIds.Organizes, TargetNodeId = ObjectIds.Server }; (ArrayOf results, _) = await sut.AddReferencesAsync( ctx, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadSourceNodeIdInvalid)); } [Test] public async Task AddReferencesAsync_NullTargetNodeId_ReturnsBadTargetNodeIdInvalidAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); var item = new AddReferencesItem { SourceNodeId = ObjectIds.ObjectsFolder, ReferenceTypeId = ReferenceTypeIds.Organizes }; (ArrayOf results, _) = await sut.AddReferencesAsync( ctx, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadTargetNodeIdInvalid)); } [Test] public async Task AddReferencesAsync_NullReferenceTypeId_ReturnsBadReferenceTypeIdInvalidAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); var item = new AddReferencesItem { SourceNodeId = ObjectIds.ObjectsFolder, TargetNodeId = ObjectIds.Server }; (ArrayOf results, _) = await sut.AddReferencesAsync( ctx, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadReferenceTypeIdInvalid)); } /// /// A known type that is not a ReferenceType (ObjectType, DataType) is not a valid /// ReferenceTypeId (Part 4 7.38.2 Bad_ReferenceTypeIdInvalid). /// [TestCase(ObjectTypes.BaseObjectType)] [TestCase(DataTypes.BaseDataType)] public async Task NodeManagementWithNonReferenceTypeIdReturnsBadReferenceTypeIdInvalidAsync( uint typeId) { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); var referenceTypeId = new NodeId(typeId); (ArrayOf addResults, _) = await sut.AddReferencesAsync( ctx, new AddReferencesItem[] { new() { SourceNodeId = ObjectIds.ObjectsFolder, ReferenceTypeId = referenceTypeId, IsForward = true, TargetNodeId = ObjectIds.Server, TargetNodeClass = NodeClass.Object } }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); (ArrayOf deleteResults, _) = await sut.DeleteReferencesAsync( ctx, new DeleteReferencesItem[] { new() { SourceNodeId = ObjectIds.ObjectsFolder, ReferenceTypeId = referenceTypeId, IsForward = true, TargetNodeId = ObjectIds.Server } }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); (ArrayOf addNodesResults, _) = await sut.AddNodesAsync( ctx, new AddNodesItem[] { new() { ParentNodeId = ObjectIds.ObjectsFolder, ReferenceTypeId = referenceTypeId, BrowseName = new QualifiedName("Test", 2), NodeClass = NodeClass.Object } }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(addResults[0], Is.EqualTo(StatusCodes.BadReferenceTypeIdInvalid)); Assert.That(deleteResults[0], Is.EqualTo(StatusCodes.BadReferenceTypeIdInvalid)); Assert.That(addNodesResults[0].StatusCode, Is.EqualTo(StatusCodes.BadReferenceTypeIdInvalid)); } /// /// No Reference of an abstract ReferenceType shall exist (Part 3 5.3.1), so AddNodes, /// AddReferences and DeleteReferences reject abstract ReferenceTypeIds with /// Bad_ReferenceTypeIdInvalid. /// [TestCase(ReferenceTypes.References)] [TestCase(ReferenceTypes.HierarchicalReferences)] [TestCase(ReferenceTypes.NonHierarchicalReferences)] [TestCase(ReferenceTypes.HasChild)] [TestCase(ReferenceTypes.Aggregates)] public async Task NodeManagementWithAbstractReferenceTypeIdReturnsBadReferenceTypeIdInvalidAsync( uint typeId) { IMasterNodeManager sut = m_server.CurrentInstance.NodeManager; OperationContext ctx = CreateContext(); var referenceTypeId = new NodeId(typeId); (ArrayOf addResults, _) = await sut.AddReferencesAsync( ctx, new AddReferencesItem[] { new() { SourceNodeId = ObjectIds.ObjectsFolder, ReferenceTypeId = referenceTypeId, IsForward = true, TargetNodeId = ObjectIds.Server, TargetNodeClass = NodeClass.Object } }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); (ArrayOf addNodesResults, _) = await sut.AddNodesAsync( ctx, new AddNodesItem[] { new() { ParentNodeId = ObjectIds.ObjectsFolder, ReferenceTypeId = referenceTypeId, BrowseName = new QualifiedName("AbstractReferenceTest", 2), NodeClass = NodeClass.Object } }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); (ArrayOf deleteResults, _) = await sut.DeleteReferencesAsync( ctx, new DeleteReferencesItem[] { new() { SourceNodeId = ObjectIds.ObjectsFolder, ReferenceTypeId = referenceTypeId, IsForward = true, TargetNodeId = ObjectIds.Server } }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(addResults[0], Is.EqualTo(StatusCodes.BadReferenceTypeIdInvalid)); Assert.That(addNodesResults[0].StatusCode, Is.EqualTo(StatusCodes.BadReferenceTypeIdInvalid)); Assert.That(deleteResults[0], Is.EqualTo(StatusCodes.BadReferenceTypeIdInvalid)); } [Test] public async Task DeleteReferencesAsync_NullItem_ReturnsBadNothingToDoAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); (ArrayOf results, _) = await sut.DeleteReferencesAsync( ctx, new DeleteReferencesItem[] { null! }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadNothingToDo)); } [Test] public async Task DeleteReferencesAsync_NullSourceNodeId_ReturnsBadSourceNodeIdInvalidAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); var item = new DeleteReferencesItem { ReferenceTypeId = ReferenceTypeIds.Organizes, TargetNodeId = ObjectIds.Server }; (ArrayOf results, _) = await sut.DeleteReferencesAsync( ctx, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadSourceNodeIdInvalid)); } [Test] public async Task DeleteReferencesAsync_NullReferenceTypeId_ReturnsBadReferenceTypeIdInvalidAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); var item = new DeleteReferencesItem { SourceNodeId = ObjectIds.ObjectsFolder, TargetNodeId = ObjectIds.Server }; (ArrayOf results, _) = await sut.DeleteReferencesAsync( ctx, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadReferenceTypeIdInvalid)); } [Test] public async Task DeleteReferencesAsync_BadItem_OmitsDiagnostics_WhenNotRequestedAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(DiagnosticsMasks.None); var item = new DeleteReferencesItem { SourceNodeId = ObjectIds.ObjectsFolder, TargetNodeId = ObjectIds.Server }; (ArrayOf results, ArrayOf diagnostics) = await sut.DeleteReferencesAsync( ctx, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None) .ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadReferenceTypeIdInvalid)); Assert.That(diagnostics.IsNull || diagnostics.Count == 0, Is.True); } [Test] public async Task DeleteReferencesAsync_BadItem_ReturnsDiagnostics_WhenRequestedAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(DiagnosticsMasks.OperationAll); var item = new DeleteReferencesItem { SourceNodeId = ObjectIds.ObjectsFolder, TargetNodeId = ObjectIds.Server }; (ArrayOf results, ArrayOf diagnostics) = await sut.DeleteReferencesAsync( ctx, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None) .ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadReferenceTypeIdInvalid)); Assert.That(diagnostics.Count, Is.EqualTo(1)); } [Test] public async Task AddNodesAsync_BadItem_OmitsDiagnostics_WhenNotRequestedAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(DiagnosticsMasks.None); (ArrayOf results, ArrayOf diagnostics) = await sut.AddNodesAsync( ctx, new AddNodesItem[] { null! }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNothingToDo)); Assert.That(diagnostics.IsNull || diagnostics.Count == 0, Is.True); } [Test] public async Task AddNodesAsync_BadItem_ReturnsDiagnostics_WhenRequestedAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(DiagnosticsMasks.OperationAll); (ArrayOf results, ArrayOf diagnostics) = await sut.AddNodesAsync( ctx, new AddNodesItem[] { null! }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNothingToDo)); Assert.That(diagnostics.Count, Is.EqualTo(1)); } [Test] public async Task AddNodesAsync_EmptyBatch_ReturnsEmptyResultsAsync() { using MasterNodeManager sut = CreateMasterNodeManager(); OperationContext ctx = CreateContext(); (ArrayOf results, ArrayOf diagnostics) = await sut.AddNodesAsync( ctx, Array.Empty().ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results.Count, Is.Zero); Assert.That(diagnostics.IsNull || diagnostics.Count == 0, Is.True); } [Test] public void AddNodesAsync_NullContext_ThrowsArgumentNullException() { using MasterNodeManager sut = CreateMasterNodeManager(); Assert.That( async () => await sut.AddNodesAsync( null!, Array.Empty().ToArrayOf(), CancellationToken.None).ConfigureAwait(false), Throws.TypeOf()); } [Test] public void DeleteNodesAsync_NullContext_ThrowsArgumentNullException() { using MasterNodeManager sut = CreateMasterNodeManager(); Assert.That( async () => await sut.DeleteNodesAsync( null!, Array.Empty().ToArrayOf(), CancellationToken.None).ConfigureAwait(false), Throws.TypeOf()); } [Test] public void AddReferencesAsync_NullContext_ThrowsArgumentNullException() { using MasterNodeManager sut = CreateMasterNodeManager(); Assert.That( async () => await sut.AddReferencesAsync( null!, Array.Empty().ToArrayOf(), CancellationToken.None).ConfigureAwait(false), Throws.TypeOf()); } [Test] public void DeleteReferencesAsync_NullContext_ThrowsArgumentNullException() { using MasterNodeManager sut = CreateMasterNodeManager(); Assert.That( async () => await sut.DeleteReferencesAsync( null!, Array.Empty().ToArrayOf(), CancellationToken.None).ConfigureAwait(false), Throws.TypeOf()); } [Test] public async Task AddNodesDeniedAddNodePermissionDoesNotMutateAsync() { using var harness = new AuthorizationHarness(); harness.SetAddNodePermissions(PermissionType.Browse); AddNodesItem item = harness.CreateAddNodesItem(); (ArrayOf results, _) = await harness.Sut.AddNodesAsync( harness.AddNodesContext, new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadUserAccessDenied)); harness.SourceManager.Verify( manager => manager.AddNodeAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [TestCase(0, false)] [TestCase(1, false)] [TestCase(2, false)] [TestCase(0, true)] [TestCase(1, true)] [TestCase(2, true)] public async Task NamespaceAddNodePermissionsFollowIdentityNotSessionOrChannelAsync(int requestKind, bool allowed) { using var harness = new AuthorizationHarness(); harness.SetAddNodePermissions(allowed ? PermissionType.AddNode : PermissionType.Browse); using OperationContext context = harness.CreateIdentityContext(RequestType.AddNodes, requestKind); (ArrayOf results, _) = await harness.Sut.AddNodesAsync( context, [harness.CreateAddNodesItem()]).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(allowed ? StatusCodes.Good : StatusCodes.BadUserAccessDenied)); harness.SourceManager.Verify( manager => manager.AddNodeAsync( It.IsAny(), It.IsAny(), It.IsAny()), allowed ? Times.Once() : Times.Never()); } [TestCase(0)] [TestCase(1)] [TestCase(2)] public async Task MethodPermissionsFollowIdentityNotSessionOrChannelAsync(int requestKind) { using var harness = new AuthorizationHarness(); harness.TargetMetadata.NodeClass = NodeClass.Method; harness.SetTargetPermissions(PermissionType.None); harness.SourceManager.Setup(manager => manager.FindMethodStateAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask(new MethodState(null) { NodeId = harness.TargetNodeId })); using OperationContext context = harness.CreateIdentityContext(RequestType.Call, requestKind); (ArrayOf results, _) = await harness.Sut.CallAsync( context, [new CallMethodRequest { ObjectId = harness.SourceNodeId, MethodId = harness.TargetNodeId }]) .ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadUserAccessDenied)); } [Test] public async Task AddNodesGrantedAddNodePermissionMutatesAsync() { using var harness = new AuthorizationHarness(); harness.SetAddNodePermissions(PermissionType.AddNode); AddNodesItem item = harness.CreateAddNodesItem(); (ArrayOf results, _) = await harness.Sut.AddNodesAsync( harness.AddNodesContext, new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.Good)); harness.SourceManager.Verify( manager => manager.AddNodeAsync( harness.AddNodesContext, item, It.IsAny()), Times.Once); } [Test] public async Task AddNodesDeniedParentAddReferencePermissionDoesNotMutateAsync() { using var harness = new AuthorizationHarness(); harness.SetAddNodePermissions(PermissionType.AddNode); harness.SetSourcePermissions(PermissionType.Browse); AddNodesItem item = harness.CreateAddNodesItem(); (ArrayOf results, _) = await harness.Sut.AddNodesAsync( harness.AddNodesContext, new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadUserAccessDenied)); harness.SourceManager.Verify( manager => manager.AddNodeAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task AddNodesParentAccessRestrictionDoesNotMutateAsync() { using var harness = new AuthorizationHarness(); harness.SetAddNodePermissions(PermissionType.AddNode); harness.SourceMetadata.AccessRestrictions = AccessRestrictionType.SigningRequired; AddNodesItem item = harness.CreateAddNodesItem(); OperationContext insecureContext = harness.CreateContext( RequestType.AddNodes, MessageSecurityMode.None); (ArrayOf results, _) = await harness.Sut.AddNodesAsync( insecureContext, new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadSecurityModeInsufficient)); harness.SourceManager.Verify( manager => manager.AddNodeAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task AddNodesUsesRequestedTargetNamespacePolicyAsync() { using var harness = new AuthorizationHarness(); harness.SetAddNodePermissions( harness.SourceNamespaceIndex, PermissionType.Browse); harness.SetAddNodePermissions( harness.TargetNamespaceIndex, PermissionType.AddNode); AddNodesItem item = harness.CreateAddNodesItem(harness.TargetNamespaceIndex); (ArrayOf results, _) = await harness.Sut.AddNodesAsync( harness.AddNodesContext, new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.Good)); harness.TargetManager.Verify( manager => manager.AddNodeAsync( harness.AddNodesContext, item, It.IsAny()), Times.Once); } [Test] public async Task AddNodesWithoutRequestedIdUsesBrowseNameNamespacePolicyAsync() { using var harness = new AuthorizationHarness(); harness.SetAddNodePermissions( harness.SourceNamespaceIndex, PermissionType.Browse); harness.SetAddNodePermissions( harness.TargetNamespaceIndex, PermissionType.AddNode); var item = new AddNodesItem { ParentNodeId = harness.SourceNodeId, ReferenceTypeId = ReferenceTypeIds.Organizes, BrowseName = new QualifiedName("Added", harness.TargetNamespaceIndex), NodeClass = NodeClass.Object }; (ArrayOf results, _) = await harness.Sut.AddNodesAsync( harness.AddNodesContext, new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.Good)); harness.TargetManager.Verify( manager => manager.AddNodeAsync( harness.AddNodesContext, item, It.IsAny()), Times.Once); } [Test] public async Task AddNodesNamespaceAccessRestrictionDoesNotMutateAsync() { using var harness = new AuthorizationHarness(); harness.SetAddNodePermissions( harness.TargetNamespaceIndex, PermissionType.AddNode); harness.SetNamespaceAccessRestrictions( harness.TargetNamespaceIndex, AccessRestrictionType.SigningRequired); AddNodesItem item = harness.CreateAddNodesItem(harness.TargetNamespaceIndex); OperationContext insecureContext = harness.CreateContext( RequestType.AddNodes, MessageSecurityMode.None); (ArrayOf results, _) = await harness.Sut.AddNodesAsync( insecureContext, new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadSecurityModeInsufficient)); harness.TargetManager.Verify( manager => manager.AddNodeAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task DeleteNodesDeniedDeleteNodePermissionDoesNotMutateAsync() { using var harness = new AuthorizationHarness(); harness.SetSourcePermissions(PermissionType.Browse); var item = new DeleteNodesItem { NodeId = harness.SourceNodeId }; (ArrayOf results, _) = await harness.Sut.DeleteNodesAsync( harness.DeleteNodesContext, new DeleteNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadUserAccessDenied)); harness.SourceManager.Verify( manager => manager.GetPermissionMetadataAsync( harness.DeleteNodesContext, It.IsAny(), It.IsAny(), It.IsAny>(), true, It.IsAny()), Times.Once); harness.SourceManager.Verify( manager => manager.DeleteNodeAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task DeleteNodesGrantedDeleteNodePermissionMutatesAsync() { using var harness = new AuthorizationHarness(); var item = new DeleteNodesItem { NodeId = harness.SourceNodeId }; (ArrayOf results, _) = await harness.Sut.DeleteNodesAsync( harness.DeleteNodesContext, new DeleteNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.Good)); harness.SourceManager.Verify( manager => manager.DeleteNodeAsync( harness.DeleteNodesContext, item, It.IsAny()), Times.Once); } [Test] public async Task DeleteNodesAccessRestrictionDoesNotMutateAsync() { using var harness = new AuthorizationHarness(); harness.SourceMetadata.AccessRestrictions = AccessRestrictionType.EncryptionRequired; var item = new DeleteNodesItem { NodeId = harness.SourceNodeId }; OperationContext signedContext = harness.CreateContext( RequestType.DeleteNodes, MessageSecurityMode.Sign); (ArrayOf results, _) = await harness.Sut.DeleteNodesAsync( signedContext, new DeleteNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadSecurityModeInsufficient)); harness.SourceManager.Verify( manager => manager.DeleteNodeAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task AddReferencesDeniedTargetPermissionDoesNotHalfApplyAsync() { using var harness = new AuthorizationHarness(); harness.SetTargetPermissions(PermissionType.Browse); AddReferencesItem item = harness.CreateAddReferencesItem(); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadUserAccessDenied)); harness.SourceManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); harness.TargetManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task AddReferencesTargetAccessRestrictionDoesNotHalfApplyAsync() { using var harness = new AuthorizationHarness(); harness.TargetMetadata.AccessRestrictions = AccessRestrictionType.SigningRequired; AddReferencesItem item = harness.CreateAddReferencesItem(); OperationContext insecureContext = harness.CreateContext( RequestType.AddReferences, MessageSecurityMode.None); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( insecureContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadSecurityModeInsufficient)); harness.SourceManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); harness.TargetManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task AddReferencesGrantedPermissionsMutateBothSidesAsync() { using var harness = new AuthorizationHarness(); harness.UseOptimizedMetadataWithoutNodeClasses(); AddReferencesItem item = harness.CreateAddReferencesItem(); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.Good)); harness.SourceManager.Verify( manager => manager.AddReferenceAsync( harness.AddReferencesContext, item, It.IsAny()), Times.Once); harness.TargetManager.Verify( manager => manager.AddReferenceAsync( harness.AddReferencesContext, It.Is(inverse => inverse.SourceNodeId == harness.TargetNodeId && inverse.TargetNodeId == harness.SourceNodeId && inverse.ReferenceTypeId == item.ReferenceTypeId && inverse.IsForward != item.IsForward && inverse.TargetServerUri == string.Empty && inverse.TargetNodeClass == harness.SourceMetadata.NodeClass), It.IsAny()), Times.Once); } [Test] public async Task AddReferencesTargetServerUriSkipsLocalTargetProcessingAsync() { using var harness = new AuthorizationHarness(); harness.SetTargetPermissions(PermissionType.Browse); AddReferencesItem item = harness.CreateAddReferencesItem(); item.TargetServerUri = "urn:remote:server"; (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.Good)); harness.SourceManager.Verify( manager => manager.AddReferenceAsync( harness.AddReferencesContext, item, It.IsAny()), Times.Once); harness.TargetManager.Verify( manager => manager.GetPermissionMetadataAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never); harness.TargetManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task AddReferencesRemoteServerIndexSkipsLocalTargetProcessingAsync() { using var harness = new AuthorizationHarness(); AddReferencesItem item = harness.CreateAddReferencesItem(); item.TargetNodeId = new ExpandedNodeId( harness.TargetNodeId, "urn:remote:namespace", serverIndex: 1); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.Good)); harness.TargetManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task AddReferencesNamespaceUriTargetSkipsLocalTargetProcessingAsync() { using var harness = new AuthorizationHarness(); AddReferencesItem item = harness.CreateAddReferencesItem(); item.TargetNodeId = new ExpandedNodeId( harness.TargetNodeId, "urn:remote:namespace"); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.Good)); harness.TargetManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task AddReferencesLocalNamespaceUriProcessesTargetAsync() { using var harness = new AuthorizationHarness(); AddReferencesItem item = harness.CreateAddReferencesItem(); item.TargetNodeId = harness.CreateTargetExpandedNodeIdWithUri(); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.Good)); harness.TargetManager.Verify( manager => manager.AddReferenceAsync( harness.AddReferencesContext, It.Is(inverse => inverse.SourceNodeId == harness.TargetNodeId && inverse.TargetNodeId == harness.SourceNodeId), It.IsAny()), Times.Once); } [Test] public async Task AddReferencesInverseFailureRollsBackSourceAsync() { using var harness = new AuthorizationHarness(); AddReferencesItem item = harness.CreateAddReferencesItem(); harness.TargetManager .Setup(manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask( new ServiceResult(StatusCodes.BadUnexpectedError))); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That( results[0], Is.EqualTo(StatusCodes.BadUnexpectedError)); harness.SourceManager.Verify( manager => manager.DeleteReferenceAsync( harness.AddReferencesContext, It.Is(rollback => rollback.SourceNodeId == item.SourceNodeId && rollback.TargetNodeId == item.TargetNodeId && rollback.ReferenceTypeId == item.ReferenceTypeId && rollback.IsForward == item.IsForward && !rollback.DeleteBidirectional), It.IsAny()), Times.Once); } [TestCase(false)] [TestCase(true)] public async Task AddReferenceWithExistingInverseSucceedsWithoutRollbackAsync(bool throws) { using var harness = new AuthorizationHarness(); harness.TargetManager.Setup(manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => throws ? throw new ServiceResultException(StatusCodes.BadDuplicateReferenceNotAllowed) : new ValueTask(StatusCodes.BadDuplicateReferenceNotAllowed)); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, [harness.CreateAddReferencesItem()]).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.Good)); harness.SourceManager.Verify(manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [TestCase(false)] [TestCase(true)] public async Task DeleteReferenceWithMissingInverseSucceedsWithoutRollbackAsync(bool throws) { using var harness = new AuthorizationHarness(); harness.TargetManager.Setup(manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => throws ? throw new ServiceResultException(StatusCodes.BadNoMatch) : new ValueTask(StatusCodes.BadNoMatch)); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, [harness.CreateDeleteReferencesItem()]).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.Good)); harness.SourceManager.Verify(manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public void AddReferencesCancellationUsesIndependentRollbackToken() { using var harness = new AuthorizationHarness(); AddReferencesItem item = harness.CreateAddReferencesItem(); using var requestCts = new CancellationTokenSource(); var cleanupToken = default(CancellationToken); harness.SourceManager .Setup(manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Callback(( OperationContext _, DeleteReferencesItem _, CancellationToken token) => cleanupToken = token) .Returns(new ValueTask(ServiceResult.Good)); harness.TargetManager .Setup(manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(( OperationContext _, AddReferencesItem _, CancellationToken token) => { requestCts.Cancel(); throw new OperationCanceledException(token); }); Assert.That( async () => await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), requestCts.Token).ConfigureAwait(false), Throws.TypeOf()); Assert.Multiple(() => { Assert.That(requestCts.IsCancellationRequested, Is.True); Assert.That(cleanupToken.CanBeCanceled, Is.True); Assert.That(cleanupToken, Is.Not.EqualTo(requestCts.Token)); Assert.That(cleanupToken.IsCancellationRequested, Is.False); }); } [Test] public async Task DeleteReferencesDeniedTargetPermissionDoesNotHalfApplyAsync() { using var harness = new AuthorizationHarness(); harness.SetTargetPermissions(PermissionType.Browse); DeleteReferencesItem item = harness.CreateDeleteReferencesItem(); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadUserAccessDenied)); harness.SourceManager.Verify( manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); harness.TargetManager.Verify( manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task DeleteReferencesTargetAccessRestrictionDoesNotHalfApplyAsync() { using var harness = new AuthorizationHarness(); harness.TargetMetadata.AccessRestrictions = AccessRestrictionType.SigningRequired; DeleteReferencesItem item = harness.CreateDeleteReferencesItem(); OperationContext insecureContext = harness.CreateContext( RequestType.DeleteReferences, MessageSecurityMode.None); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( insecureContext, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadSecurityModeInsufficient)); harness.SourceManager.Verify( manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); harness.TargetManager.Verify( manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task DeleteReferencesGrantedPermissionsMutateBothSidesAsync() { using var harness = new AuthorizationHarness(); DeleteReferencesItem item = harness.CreateDeleteReferencesItem(); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.Good)); harness.SourceManager.Verify( manager => manager.DeleteReferenceAsync( harness.DeleteReferencesContext, It.Is(source => source.SourceNodeId == item.SourceNodeId && source.TargetNodeId == item.TargetNodeId && source.ReferenceTypeId == item.ReferenceTypeId && source.IsForward == item.IsForward && !source.DeleteBidirectional), It.IsAny()), Times.Once); harness.TargetManager.Verify( manager => manager.DeleteReferenceAsync( harness.DeleteReferencesContext, It.Is(inverse => inverse.SourceNodeId == harness.TargetNodeId && inverse.TargetNodeId == harness.SourceNodeId && inverse.ReferenceTypeId == item.ReferenceTypeId && inverse.IsForward != item.IsForward && !inverse.DeleteBidirectional), It.IsAny()), Times.Once); } [Test] public async Task DeleteReferencesRemoteTargetSkipsReciprocalProcessingAsync() { using var harness = new AuthorizationHarness(); DeleteReferencesItem item = harness.CreateDeleteReferencesItem(); item.TargetNodeId = new ExpandedNodeId( harness.TargetNodeId, "urn:remote:namespace", serverIndex: 1); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.Good)); harness.SourceManager.Verify( manager => manager.DeleteReferenceAsync( harness.DeleteReferencesContext, It.Is(source => source.SourceNodeId == item.SourceNodeId && source.TargetNodeId == item.TargetNodeId && !source.DeleteBidirectional), It.IsAny()), Times.Once); harness.TargetManager.Verify( manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task DeleteReferencesLocalNamespaceUriProcessesTargetAsync() { using var harness = new AuthorizationHarness(); DeleteReferencesItem item = harness.CreateDeleteReferencesItem(); item.TargetNodeId = harness.CreateTargetExpandedNodeIdWithUri(); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.Good)); harness.TargetManager.Verify( manager => manager.DeleteReferenceAsync( harness.DeleteReferencesContext, It.Is(inverse => inverse.SourceNodeId == harness.TargetNodeId && inverse.TargetNodeId == harness.SourceNodeId), It.IsAny()), Times.Once); } [Test] public async Task DeleteReferencesInverseFailureRestoresSourceAsync() { using var harness = new AuthorizationHarness(); harness.UseOptimizedMetadataWithoutNodeClasses(); DeleteReferencesItem item = harness.CreateDeleteReferencesItem(); harness.TargetManager .Setup(manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask( new ServiceResult(StatusCodes.BadUnexpectedError))); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadUnexpectedError)); harness.SourceManager.Verify( manager => manager.AddReferenceAsync( harness.DeleteReferencesContext, It.Is(rollback => rollback.SourceNodeId == item.SourceNodeId && rollback.TargetNodeId == item.TargetNodeId && rollback.ReferenceTypeId == item.ReferenceTypeId && rollback.IsForward == item.IsForward && rollback.TargetServerUri == string.Empty && rollback.TargetNodeClass == harness.TargetMetadata.NodeClass), It.IsAny()), Times.Once); } [Test] public async Task DeleteReferencesUnexpectedFailureRestoresSourceWithIndependentToken() { using var harness = new AuthorizationHarness(); DeleteReferencesItem item = harness.CreateDeleteReferencesItem(); var cleanupToken = default(CancellationToken); harness.SourceManager .Setup(manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Callback(( OperationContext _, AddReferencesItem _, CancellationToken token) => cleanupToken = token) .Returns(new ValueTask(ServiceResult.Good)); harness.TargetManager .Setup(manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new InvalidOperationException("Target mutation failed.")); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.Multiple(() => { Assert.That(results[0], Is.EqualTo(StatusCodes.BadUnexpectedError)); Assert.That(cleanupToken.CanBeCanceled, Is.True); Assert.That(cleanupToken.IsCancellationRequested, Is.False); }); } [Test] public async Task AddNodesRequestedIdWithUnknownNamespaceUriReturnsBadNodeIdRejectedAsync() { using var harness = new AuthorizationHarness(); harness.SetAddNodePermissions(PermissionType.AddNode); var item = new AddNodesItem { ParentNodeId = harness.SourceNodeId, ReferenceTypeId = ReferenceTypeIds.Organizes, BrowseName = new QualifiedName("Added", harness.SourceNamespaceIndex), NodeClass = NodeClass.Object, RequestedNewNodeId = new ExpandedNodeId( new NodeId("Added", 0), "urn:opcfoundation:server:tests:unregistered") }; (ArrayOf results, _) = await harness.Sut.AddNodesAsync( harness.AddNodesContext, new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.BadNodeIdRejected)); harness.SourceManager.Verify( manager => manager.AddNodeAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task AddNodesWithoutSessionSkipsAuthorizationAndDispatchesAsync() { using var harness = new AuthorizationHarness(); // A restrictive policy that would deny an authenticated session. harness.SetAddNodePermissions(PermissionType.Browse); harness.SetSourcePermissions(PermissionType.Browse); AddNodesItem item = harness.CreateAddNodesItem(); var contextWithoutSession = new OperationContext( new RequestHeader(), null, RequestType.AddNodes, RequestLifetime.None); (ArrayOf results, _) = await harness.Sut.AddNodesAsync( contextWithoutSession, new AddNodesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0].StatusCode, Is.EqualTo(StatusCodes.Good)); harness.SourceManager.Verify( manager => manager.AddNodeAsync( contextWithoutSession, item, It.IsAny()), Times.Once); } [Test] public async Task AddReferencesDeniedSourcePermissionDoesNotMutateAsync() { using var harness = new AuthorizationHarness(); harness.SetSourcePermissions(PermissionType.Browse); AddReferencesItem item = harness.CreateAddReferencesItem(); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadUserAccessDenied)); harness.SourceManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); // The source denial precedes any target-side metadata lookup or mutation. harness.TargetManager.Verify( manager => manager.GetPermissionMetadataAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task AddReferencesExplicitLocalTargetUnknownReturnsBadTargetNodeIdInvalidAsync() { using var harness = new AuthorizationHarness(); AddReferencesItem item = harness.CreateAddReferencesItem(); item.TargetNodeId = new NodeId("UnknownTarget", harness.TargetNamespaceIndex); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadTargetNodeIdInvalid)); harness.SourceManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task AddReferencesSameManagerLocalTargetMirrorsInverseEdgeAsync() { ushort namespaceIndex = GetTestNamespaceIndex(); var sourceNodeId = new NodeId("Source", namespaceIndex); var targetNodeId = new NodeId("Target", namespaceIndex); Mock manager = CreateSameManagerNodeManagementManager( namespaceIndex, sourceNodeId, targetNodeId); using MasterNodeManager sut = CreateMasterNodeManager(manager.Object); OperationContext context = CreateContext(); var item = new AddReferencesItem { SourceNodeId = sourceNodeId, ReferenceTypeId = ReferenceTypeIds.Organizes, IsForward = true, TargetNodeId = targetNodeId, TargetNodeClass = NodeClass.Object }; (ArrayOf results, _) = await sut.AddReferencesAsync( context, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.Good)); manager.Verify( m => m.AddReferenceAsync( context, It.Is(request => request.SourceNodeId == sourceNodeId && request.TargetNodeId == targetNodeId && request.IsForward), It.IsAny()), Times.Once); manager.Verify( m => m.AddReferenceAsync( context, It.Is(request => request.SourceNodeId == targetNodeId && request.TargetNodeId == sourceNodeId && !request.IsForward), It.IsAny()), Times.Once); } [Test] public async Task AddReferencesTargetOwnerNotOptedInReturnsBadUserAccessDeniedAsync() { using var harness = new AuthorizationHarness(); harness.TargetManager .Setup(manager => manager.AllowNodeManagement) .Returns(false); AddReferencesItem item = harness.CreateAddReferencesItem(); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadUserAccessDenied)); harness.SourceManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task AddReferencesCrossManagerSourceMetadataNullReturnsBadSourceNodeIdInvalidAsync() { using var harness = new AuthorizationHarness(); ReturnNullMetadata(harness.SourceManager); AddReferencesItem item = harness.CreateAddReferencesItem(); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadSourceNodeIdInvalid)); harness.SourceManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); harness.TargetManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task AddReferencesCrossManagerTargetMetadataNullReturnsBadTargetNodeIdInvalidAsync() { using var harness = new AuthorizationHarness(); ReturnNullMetadata(harness.TargetManager); AddReferencesItem item = harness.CreateAddReferencesItem(); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadTargetNodeIdInvalid)); harness.SourceManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); harness.TargetManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task AddReferencesInverseServiceResultExceptionRollsBackSourceAsync() { using var harness = new AuthorizationHarness(); AddReferencesItem item = harness.CreateAddReferencesItem(); harness.TargetManager .Setup(manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new ServiceResultException(StatusCodes.BadInvalidState)); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadInvalidState)); harness.SourceManager.Verify( manager => manager.DeleteReferenceAsync( harness.AddReferencesContext, It.Is(rollback => rollback.SourceNodeId == item.SourceNodeId && rollback.TargetNodeId == item.TargetNodeId && rollback.ReferenceTypeId == item.ReferenceTypeId && rollback.IsForward == item.IsForward && !rollback.DeleteBidirectional), It.IsAny()), Times.Once); } [Test] public async Task AddReferencesInverseFailureWithRollbackFailureReturnsInverseStatusAsync() { using var harness = new AuthorizationHarness(); AddReferencesItem item = harness.CreateAddReferencesItem(); harness.TargetManager .Setup(manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask( new ServiceResult(StatusCodes.BadUnexpectedError))); harness.SourceManager .Setup(manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask( new ServiceResult(StatusCodes.BadNodeIdUnknown))); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); // The original inverse failure is surfaced, not the rollback failure. Assert.That(results[0], Is.EqualTo(StatusCodes.BadUnexpectedError)); harness.SourceManager.Verify( manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Test] public async Task AddReferencesInverseFailureWithRollbackExceptionReturnsInverseStatusAsync() { using var harness = new AuthorizationHarness(); AddReferencesItem item = harness.CreateAddReferencesItem(); harness.TargetManager .Setup(manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask( new ServiceResult(StatusCodes.BadUnexpectedError))); harness.SourceManager .Setup(manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new InvalidOperationException("Rollback failed.")); (ArrayOf results, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); // The rollback exception is swallowed; the inverse failure is returned. Assert.That(results[0], Is.EqualTo(StatusCodes.BadUnexpectedError)); harness.SourceManager.Verify( manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Test] public async Task DeleteReferencesDeniedSourcePermissionDoesNotMutateAsync() { using var harness = new AuthorizationHarness(); harness.SetSourcePermissions(PermissionType.Browse); DeleteReferencesItem item = harness.CreateDeleteReferencesItem(); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadUserAccessDenied)); harness.SourceManager.Verify( manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); harness.TargetManager.Verify( manager => manager.GetPermissionMetadataAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task DeleteReferencesExplicitLocalTargetUnknownReturnsBadTargetNodeIdInvalidAsync() { using var harness = new AuthorizationHarness(); DeleteReferencesItem item = harness.CreateDeleteReferencesItem(); item.TargetNodeId = new NodeId("UnknownTarget", harness.TargetNamespaceIndex); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadTargetNodeIdInvalid)); harness.SourceManager.Verify( manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task DeleteReferencesSameManagerLocalTargetDeletesInverseEdgeAsync() { ushort namespaceIndex = GetTestNamespaceIndex(); var sourceNodeId = new NodeId("Source", namespaceIndex); var targetNodeId = new NodeId("Target", namespaceIndex); Mock manager = CreateSameManagerNodeManagementManager( namespaceIndex, sourceNodeId, targetNodeId); using MasterNodeManager sut = CreateMasterNodeManager(manager.Object); OperationContext context = CreateContext(); var item = new DeleteReferencesItem { SourceNodeId = sourceNodeId, ReferenceTypeId = ReferenceTypeIds.Organizes, IsForward = true, TargetNodeId = targetNodeId, DeleteBidirectional = true }; (ArrayOf results, _) = await sut.DeleteReferencesAsync( context, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.Good)); manager.Verify( m => m.DeleteReferenceAsync( context, It.Is(request => request.SourceNodeId == sourceNodeId && request.TargetNodeId == targetNodeId && request.IsForward && !request.DeleteBidirectional), It.IsAny()), Times.Once); manager.Verify( m => m.DeleteReferenceAsync( context, It.Is(request => request.SourceNodeId == targetNodeId && request.TargetNodeId == sourceNodeId && !request.IsForward && !request.DeleteBidirectional), It.IsAny()), Times.Once); } [Test] public async Task DeleteReferencesTargetOwnerNotOptedInReturnsBadUserAccessDeniedAsync() { using var harness = new AuthorizationHarness(); harness.TargetManager .Setup(manager => manager.AllowNodeManagement) .Returns(false); DeleteReferencesItem item = harness.CreateDeleteReferencesItem(); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadUserAccessDenied)); harness.SourceManager.Verify( manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task DeleteReferencesCrossManagerTargetMetadataNullReturnsBadTargetNodeIdInvalidAsync() { using var harness = new AuthorizationHarness(); ReturnNullMetadata(harness.TargetManager); DeleteReferencesItem item = harness.CreateDeleteReferencesItem(); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadTargetNodeIdInvalid)); harness.SourceManager.Verify( manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task DeleteReferencesInverseServiceResultExceptionRestoresSourceAsync() { using var harness = new AuthorizationHarness(); DeleteReferencesItem item = harness.CreateDeleteReferencesItem(); harness.TargetManager .Setup(manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new ServiceResultException(StatusCodes.BadInvalidState)); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); Assert.That(results[0], Is.EqualTo(StatusCodes.BadInvalidState)); harness.SourceManager.Verify( manager => manager.AddReferenceAsync( harness.DeleteReferencesContext, It.Is(restore => restore.SourceNodeId == item.SourceNodeId && restore.TargetNodeId == item.TargetNodeId && restore.ReferenceTypeId == item.ReferenceTypeId && restore.IsForward == item.IsForward && restore.TargetServerUri == string.Empty && restore.TargetNodeClass == harness.TargetMetadata.NodeClass), It.IsAny()), Times.Once); } [Test] public async Task DeleteReferencesInverseFailureWithRestoreFailureReturnsInverseStatusAsync() { using var harness = new AuthorizationHarness(); DeleteReferencesItem item = harness.CreateDeleteReferencesItem(); harness.TargetManager .Setup(manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask( new ServiceResult(StatusCodes.BadUnexpectedError))); harness.SourceManager .Setup(manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask( new ServiceResult(StatusCodes.BadNodeIdUnknown))); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); // The original inverse failure is surfaced, not the restore failure. Assert.That(results[0], Is.EqualTo(StatusCodes.BadUnexpectedError)); harness.SourceManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Test] public async Task DeleteReferencesInverseFailureWithRestoreExceptionReturnsInverseStatusAsync() { using var harness = new AuthorizationHarness(); DeleteReferencesItem item = harness.CreateDeleteReferencesItem(); harness.TargetManager .Setup(manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask( new ServiceResult(StatusCodes.BadUnexpectedError))); harness.SourceManager .Setup(manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new InvalidOperationException("Restore failed.")); (ArrayOf results, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, new DeleteReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); // The restore exception is swallowed; the inverse failure is returned. Assert.That(results[0], Is.EqualTo(StatusCodes.BadUnexpectedError)); harness.SourceManager.Verify( manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } private static void ReturnNullMetadata(Mock manager) { manager .Setup(nodeManager => nodeManager.GetPermissionMetadataAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())) .Returns(new ValueTask((NodeMetadata?)null)); manager .Setup(nodeManager => nodeManager.GetNodeMetadataAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask((NodeMetadata)null!)); } /// /// End-to-end regression for issue #4061: a child added to an /// already-subscribed (cached) parent at runtime via CreateNodeAsync /// must be visible when the parent is browsed through the full /// MasterNodeManager dispatch pipeline, and must disappear again once /// the child is deleted. /// [Test] public async Task BrowseThroughMaster_ReflectsRuntimeAddAndDeleteOnCachedParentAsync() { // Ownership of the manager transfers to the MasterNodeManager, // which disposes its registered node managers; do not dispose it // a second time here. #pragma warning disable CA2000 var manager = new TestableAsyncCustomNodeManager( m_server.CurrentInstance, m_fixture.Config, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance, E2eNamespaceUri); #pragma warning restore CA2000 using var sut = new MasterNodeManager( m_server.CurrentInstance, m_fixture.Config, null, manager); ServerSystemContext ctx = manager.SystemContext; ushort ns = manager.NamespaceIndexes[0]; var parent = new BaseObjectState(null); parent.CreateAsPredefinedNode(ctx); parent.NodeId = new NodeId("E2eParent", ns); parent.BrowseName = new QualifiedName("E2eParent", ns); await manager.AddNodeAsync(ctx, default, parent).ConfigureAwait(false); // parent is subscribed/browsed -> cached in the component cache. manager.AddNodeToComponentCachePublic(ctx, new NodeHandle(parent.NodeId, parent), parent); // runtime-add a child. var child = new BaseObjectState(null); NodeId childId = await manager.CreateNodeAsync( ctx, parent.NodeId, ReferenceTypeIds.Organizes, new QualifiedName("E2eRuntimeChild", ns), child).ConfigureAwait(false); (ArrayOf afterAdd, _) = await BrowseChildrenAsync(sut, parent.NodeId).ConfigureAwait(false); Assert.That(afterAdd[0].StatusCode, Is.EqualTo(StatusCodes.Good)); Assert.That( ChildNodeIds(afterAdd[0]), Has.Member(new ExpandedNodeId(childId)), "runtime-added child must be visible when browsing the cached parent through the master"); // delete the child and re-browse -> gone. await manager.DeleteNodeAsync(ctx, childId).ConfigureAwait(false); (ArrayOf afterDelete, _) = await BrowseChildrenAsync(sut, parent.NodeId).ConfigureAwait(false); Assert.That(afterDelete[0].StatusCode, Is.EqualTo(StatusCodes.Good)); Assert.That( ChildNodeIds(afterDelete[0]), Has.No.Member(new ExpandedNodeId(childId)), "deleted child must no longer be visible when browsing the parent through the master"); } private static async Task> DispatchFailingNodeManagementBatchAsync( AuthorizationHarness harness, RequestType requestType, Func failure, CancellationToken cancellationToken) { switch (requestType) { case RequestType.AddNodes: harness.SetAddNodePermissions(PermissionType.AddNode); var firstId = new NodeId("First", harness.SourceNamespaceIndex); var lastId = new NodeId("Last", harness.SourceNamespaceIndex); harness.SourceManager.SetupSequence(manager => manager.AddNodeAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask<(ServiceResult, NodeId)>((ServiceResult.Good, firstId))) .Returns(() => throw failure()) .Returns(new ValueTask<(ServiceResult, NodeId)>((ServiceResult.Good, lastId))); (ArrayOf additions, _) = await harness.Sut.AddNodesAsync( harness.AddNodesContext, [harness.CreateAddNodesItem(), harness.CreateAddNodesItem(), harness.CreateAddNodesItem()], cancellationToken).ConfigureAwait(false); Assert.That(additions[0].AddedNodeId, Is.EqualTo(firstId)); Assert.That(additions[1].AddedNodeId.IsNull, Is.True); Assert.That(additions[2].AddedNodeId, Is.EqualTo(lastId)); return [additions[0].StatusCode, additions[1].StatusCode, additions[2].StatusCode]; case RequestType.DeleteNodes: harness.SourceManager.SetupSequence(manager => manager.DeleteNodeAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask(ServiceResult.Good)) .Returns(() => throw failure()) .Returns(new ValueTask(ServiceResult.Good)); var deletion = new DeleteNodesItem { NodeId = harness.SourceNodeId }; (ArrayOf deletions, _) = await harness.Sut.DeleteNodesAsync( harness.DeleteNodesContext, [deletion, deletion, deletion], cancellationToken) .ConfigureAwait(false); return deletions; case RequestType.AddReferences: harness.SourceManager.SetupSequence(manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask(ServiceResult.Good)) .Returns(() => throw failure()) .Returns(new ValueTask(ServiceResult.Good)); AddReferencesItem addition = harness.CreateAddReferencesItem(); (ArrayOf addedReferences, _) = await harness.Sut.AddReferencesAsync( harness.AddReferencesContext, [addition, addition, addition], cancellationToken) .ConfigureAwait(false); return addedReferences; case RequestType.DeleteReferences: harness.SourceManager.SetupSequence(manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask(ServiceResult.Good)) .Returns(() => throw failure()) .Returns(new ValueTask(ServiceResult.Good)); DeleteReferencesItem referenceDeletion = harness.CreateDeleteReferencesItem(); (ArrayOf deletedReferences, _) = await harness.Sut.DeleteReferencesAsync( harness.DeleteReferencesContext, [referenceDeletion, referenceDeletion, referenceDeletion], cancellationToken).ConfigureAwait(false); return deletedReferences; default: throw new ArgumentOutOfRangeException(nameof(requestType)); } } private static List ChildNodeIds(BrowseResult result) { var ids = new List(); foreach (ReferenceDescription reference in result.References) { ids.Add(reference.NodeId); } return ids; } private async Task<(ArrayOf results, ArrayOf diagnostics)> BrowseChildrenAsync( MasterNodeManager sut, NodeId parentId) { var browseDescription = new BrowseDescription { NodeId = parentId, BrowseDirection = BrowseDirection.Forward, ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, IncludeSubtypes = true, ResultMask = (uint)BrowseResultMask.All }; return await sut.BrowseAsync( CreateContext(), new ViewDescription(), 0u, new[] { browseDescription }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false); } private MasterNodeManager CreateMasterNodeManager(params INodeManager[] additional) { var nodeManagers = new List(additional); return new MasterNodeManager( m_server.CurrentInstance, m_fixture.Config, null, [.. nodeManagers]); } private ushort GetTestNamespaceIndex() { int namespaceIndex = m_server.CurrentInstance.NamespaceUris.GetIndex(TestNamespaceUri); Assert.That(namespaceIndex, Is.GreaterThanOrEqualTo(0)); return checked((ushort)namespaceIndex); } private static Mock CreateNodeManagementManager( bool allowNodeManagement) { var manager = new Mock(); manager.Setup(m => m.NamespaceUris).Returns([TestNamespaceUri]); manager.Setup(m => m.AllowNodeManagement).Returns(allowNodeManagement); return manager; } private static Mock CreateSameManagerNodeManagementManager( ushort namespaceIndex, NodeId sourceNodeId, NodeId targetNodeId) { Mock manager = CreateNodeManagementManager(true); object sourceHandle = new(); object targetHandle = new(); manager.Setup(m => m.GetManagerHandle(sourceNodeId)).Returns(sourceHandle); manager.Setup(m => m.GetManagerHandle(targetNodeId)).Returns(targetHandle); manager .Setup(m => m.GetNodeMetadata( It.IsAny(), sourceHandle, BrowseResultMask.NodeClass)) .Returns(new NodeMetadata(sourceHandle, sourceNodeId) { NodeClass = NodeClass.Object }); manager .Setup(m => m.GetNodeMetadata( It.IsAny(), targetHandle, BrowseResultMask.NodeClass)) .Returns(new NodeMetadata(targetHandle, targetNodeId) { NodeClass = NodeClass.Object }); manager .Setup(m => m.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(ServiceResult.Good); manager .Setup(m => m.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(ServiceResult.Good); return manager; } private static NodeId ConfigureParent( Mock manager, ushort namespaceIndex) { var parentNodeId = new NodeId("Parent", namespaceIndex); object parentHandle = new(); manager.Setup(m => m.GetManagerHandle(parentNodeId)).Returns(parentHandle); manager .Setup(m => m.GetNodeMetadata( It.IsAny(), parentHandle, BrowseResultMask.NodeClass)) .Returns(new NodeMetadata(parentHandle, parentNodeId) { NodeClass = NodeClass.Object }); return parentNodeId; } private static OperationContext CreateContext() { var session = new Mock(); session.Setup(s => s.EffectiveIdentity).Returns(new Mock().Object); session.Setup(s => s.PreferredLocales).Returns([]); return new OperationContext( new RequestHeader(), null!, RequestType.AddNodes, RequestLifetime.None, session.Object); } private static OperationContext CreateContext(DiagnosticsMasks diagnosticsMask) { var session = new Mock(); session.Setup(s => s.EffectiveIdentity).Returns(new Mock().Object); session.Setup(s => s.PreferredLocales).Returns([]); var requestHeader = new RequestHeader { ReturnDiagnostics = (uint)diagnosticsMask }; return new OperationContext( requestHeader, null!, RequestType.AddNodes, RequestLifetime.None, session.Object); } /// /// Combines the legacy manager contract with the asynchronous NodeManagement facet. /// public interface INodeManagerWithNodeManagement : INodeManager, INodeManagementAsyncNodeManager; /// /// Standard concrete hierarchical references used by the valid-semantics test. /// public enum ValidReferenceKind { Organizes, HasComponent, HasProperty } private sealed class AuthorizationHarness : IDisposable { private const string SourceNamespaceUri = "urn:opcfoundation:server:tests:node-management-source"; private const string TargetNamespaceUri = "urn:opcfoundation:server:tests:node-management-target"; private static readonly NodeId s_roleId = ObjectIds.WellKnownRole_SecurityAdmin; private readonly Mock m_session; private readonly NamespaceMetadataState m_sourceNamespaceMetadata; private readonly NamespaceMetadataState m_targetNamespaceMetadata; public AuthorizationHarness() { var namespaceTable = new NamespaceTable(); namespaceTable.Append("urn:opcfoundation:server:tests"); SourceNamespaceIndex = (ushort)namespaceTable.Append(SourceNamespaceUri); TargetNamespaceIndex = (ushort)namespaceTable.Append(TargetNamespaceUri); var typeTree = new TypeTable(namespaceTable); typeTree.AddSubtype(ReferenceTypeIds.References, NodeId.Null); typeTree.AddSubtype( ReferenceTypeIds.HierarchicalReferences, ReferenceTypeIds.References); typeTree.AddSubtype( ReferenceTypeIds.Organizes, ReferenceTypeIds.HierarchicalReferences); SourceNodeId = new NodeId("Source", SourceNamespaceIndex); TargetNodeId = new NodeId("Target", TargetNamespaceIndex); SourceMetadata = new NodeMetadata(SourceNodeId, SourceNodeId) { NodeClass = NodeClass.Object }; TargetMetadata = new NodeMetadata(TargetNodeId, TargetNodeId) { NodeClass = NodeClass.Variable }; var configurationManager = new Mock(); var coreNodeManager = new Mock(); var factory = new Mock(); factory.Setup(managerFactory => managerFactory.CreateConfigurationNodeManager()) .Returns(configurationManager.Object); factory.Setup(managerFactory => managerFactory.CreateCoreNodeManager(It.IsAny())) .Returns(coreNodeManager.Object); var server = new Mock(); server.Setup(instance => instance.Telemetry) .Returns(NUnitTelemetryContext.Create()); server.Setup(instance => instance.NamespaceUris).Returns(namespaceTable); server.Setup(instance => instance.ServerUris).Returns(new StringTable()); server.Setup(instance => instance.TypeTree).Returns(typeTree); server.Setup(instance => instance.Factory).Returns(EncodeableFactory.Create()); server.Setup(instance => instance.MainNodeManagerFactory).Returns(factory.Object); var defaultSystemContext = new ServerSystemContext(server.Object); server.Setup(instance => instance.DefaultSystemContext) .Returns(defaultSystemContext); SourceManager = CreateNodeManager( SourceNamespaceUri, SourceNodeId, SourceMetadata); TargetManager = CreateNodeManager( TargetNamespaceUri, TargetNodeId, TargetMetadata); var addedNodeId = new NodeId("Added", SourceNamespaceIndex); SourceManager .Setup(manager => manager.AddNodeAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask<(ServiceResult result, NodeId addedNodeId)>( (ServiceResult.Good, addedNodeId))); TargetManager .Setup(manager => manager.AddNodeAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask<(ServiceResult result, NodeId addedNodeId)>( (ServiceResult.Good, new NodeId("Added", TargetNamespaceIndex)))); SourceManager .Setup(manager => manager.DeleteNodeAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask(ServiceResult.Good)); SourceManager .Setup(manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask(ServiceResult.Good)); SourceManager .Setup(manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask(ServiceResult.Good)); TargetManager .Setup(manager => manager.AddReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask(ServiceResult.Good)); TargetManager .Setup(manager => manager.DeleteReferenceAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask(ServiceResult.Good)); m_sourceNamespaceMetadata = new NamespaceMetadataState(null) { NodeId = new NodeId("SourceNamespaceMetadata", 0) }; m_sourceNamespaceMetadata .AddDefaultAccessRestrictions(defaultSystemContext) .AddDefaultRolePermissions(defaultSystemContext) .AddDefaultUserRolePermissions(defaultSystemContext); m_targetNamespaceMetadata = new NamespaceMetadataState(null) { NodeId = new NodeId("TargetNamespaceMetadata", 0) }; m_targetNamespaceMetadata .AddDefaultAccessRestrictions(defaultSystemContext) .AddDefaultRolePermissions(defaultSystemContext) .AddDefaultUserRolePermissions(defaultSystemContext); configurationManager .Setup(manager => manager.GetNamespaceMetadataStateAsync( It.IsAny(), It.IsAny())) .Returns((ushort namespaceIndex, CancellationToken _) => new ValueTask( (namespaceIndex == SourceNamespaceIndex ? m_sourceNamespaceMetadata : namespaceIndex == TargetNamespaceIndex ? m_targetNamespaceMetadata : null)!)); var identity = new Mock(); identity.Setup(user => user.GrantedRoleIds) .Returns(new NodeId[] { s_roleId }.ToArrayOf()); m_session = new Mock(); m_session.Setup(activeSession => activeSession.Id) .Returns(new NodeId("NodeManagementSession", 0)); m_session.Setup(activeSession => activeSession.EffectiveIdentity) .Returns(identity.Object); m_session.Setup(activeSession => activeSession.PreferredLocales) .Returns([]); AddNodesContext = CreateContext( RequestType.AddNodes, MessageSecurityMode.SignAndEncrypt); DeleteNodesContext = CreateContext( RequestType.DeleteNodes, MessageSecurityMode.SignAndEncrypt); AddReferencesContext = CreateContext( RequestType.AddReferences, MessageSecurityMode.SignAndEncrypt); DeleteReferencesContext = CreateContext( RequestType.DeleteReferences, MessageSecurityMode.SignAndEncrypt); var configuration = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration() }; Sut = new MasterNodeManager( server.Object, configuration, null, [ SourceManager.Object, TargetManager.Object ]); } public MasterNodeManager Sut { get; } public OperationContext AddNodesContext { get; } public OperationContext DeleteNodesContext { get; } public OperationContext AddReferencesContext { get; } public OperationContext DeleteReferencesContext { get; } public Mock SourceManager { get; } public Mock TargetManager { get; } public ushort SourceNamespaceIndex { get; } public ushort TargetNamespaceIndex { get; } public NodeId SourceNodeId { get; } public NodeId TargetNodeId { get; } public NodeMetadata SourceMetadata { get; } public NodeMetadata TargetMetadata { get; } public AddNodesItem CreateAddNodesItem() { return CreateAddNodesItem(SourceNamespaceIndex); } public AddNodesItem CreateAddNodesItem(ushort namespaceIndex) { return new AddNodesItem { ParentNodeId = SourceNodeId, ReferenceTypeId = ReferenceTypeIds.Organizes, BrowseName = new QualifiedName("Added", namespaceIndex), NodeClass = NodeClass.Object, RequestedNewNodeId = new NodeId("Added", namespaceIndex) }; } public AddReferencesItem CreateAddReferencesItem() { return new AddReferencesItem { SourceNodeId = SourceNodeId, ReferenceTypeId = ReferenceTypeIds.Organizes, IsForward = true, TargetNodeId = TargetNodeId, TargetNodeClass = TargetMetadata.NodeClass }; } public DeleteReferencesItem CreateDeleteReferencesItem() { return new DeleteReferencesItem { SourceNodeId = SourceNodeId, ReferenceTypeId = ReferenceTypeIds.Organizes, IsForward = true, TargetNodeId = TargetNodeId, DeleteBidirectional = true }; } public ExpandedNodeId CreateTargetExpandedNodeIdWithUri() { return new ExpandedNodeId(TargetNodeId, TargetNamespaceUri); } public void SetAddNodePermissions(PermissionType permissions) { SetAddNodePermissions(SourceNamespaceIndex, permissions); } public void SetAddNodePermissions( ushort namespaceIndex, PermissionType permissions) { NamespaceMetadataState namespaceMetadata = namespaceIndex == SourceNamespaceIndex ? m_sourceNamespaceMetadata : m_targetNamespaceMetadata; namespaceMetadata.DefaultRolePermissions!.Value = [ new RolePermissionType { RoleId = s_roleId, Permissions = (uint)permissions } ]; } public void SetNamespaceAccessRestrictions( ushort namespaceIndex, AccessRestrictionType restrictions) { NamespaceMetadataState namespaceMetadata = namespaceIndex == SourceNamespaceIndex ? m_sourceNamespaceMetadata : m_targetNamespaceMetadata; namespaceMetadata.DefaultAccessRestrictions!.Value = (ushort)restrictions; } public void SetSourcePermissions(PermissionType permissions) { SourceMetadata.RolePermissions = [ new RolePermissionType { RoleId = s_roleId, Permissions = (uint)permissions } ]; } public void SetTargetPermissions(PermissionType permissions) { TargetMetadata.RolePermissions = [ new RolePermissionType { RoleId = s_roleId, Permissions = (uint)permissions } ]; } public OperationContext CreateContext( RequestType requestType, MessageSecurityMode securityMode) { var endpoint = new EndpointDescription { SecurityMode = securityMode, SecurityPolicyUri = securityMode == MessageSecurityMode.None ? SecurityPolicies.None : SecurityPolicies.Basic256Sha256, TransportProfileUri = Profiles.UaTcpTransport }; var channelContext = new SecureChannelContext( "NodeManagementChannel", endpoint, RequestEncoding.Binary); return new OperationContext( new RequestHeader(), channelContext, requestType, RequestLifetime.None, m_session.Object); } public void UseOptimizedMetadataWithoutNodeClasses() { SetupOptimizedPermissionMetadata(SourceManager, SourceMetadata); SetupOptimizedPermissionMetadata(TargetManager, TargetMetadata); } public OperationContext CreateIdentityContext(RequestType requestType, int requestKind) { return requestKind == 0 ? CreateContext(requestType, MessageSecurityMode.SignAndEncrypt) : new OperationContext( new RequestHeader(), requestKind == 1 ? AddNodesContext.ChannelContext : null, requestType, RequestLifetime.None, m_session.Object.EffectiveIdentity); } public void Dispose() { Sut.Dispose(); } private static void SetupOptimizedPermissionMetadata( Mock manager, NodeMetadata fullMetadata) { var permissionMetadata = new NodeMetadata( fullMetadata.Handle, fullMetadata.NodeId) { AccessRestrictions = fullMetadata.AccessRestrictions, DefaultAccessRestrictions = fullMetadata.DefaultAccessRestrictions, RolePermissions = fullMetadata.RolePermissions, DefaultRolePermissions = fullMetadata.DefaultRolePermissions, UserRolePermissions = fullMetadata.UserRolePermissions, DefaultUserRolePermissions = fullMetadata.DefaultUserRolePermissions, IsPartOfTypeHierarchy = fullMetadata.IsPartOfTypeHierarchy }; manager .Setup(nodeManager => nodeManager.GetPermissionMetadataAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())) .Returns(new ValueTask(permissionMetadata)); } private static Mock CreateNodeManager( string namespaceUri, NodeId nodeId, NodeMetadata metadata) { var manager = new Mock(); manager.Setup(nodeManager => nodeManager.NamespaceUris) .Returns([namespaceUri]); manager.Setup(nodeManager => nodeManager.AllowNodeManagement) .Returns(true); manager .Setup(nodeManager => nodeManager.GetManagerHandleAsync( It.IsAny(), It.IsAny())) .Returns((NodeId requestedNodeId, CancellationToken _) => new ValueTask( requestedNodeId == nodeId ? nodeId : null!)); manager .Setup(nodeManager => nodeManager.GetPermissionMetadataAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())) .Returns(( OperationContext _, object _, BrowseResultMask _, Dictionary _, bool _, CancellationToken _) => new ValueTask(metadata)); manager .Setup(nodeManager => nodeManager.GetNodeMetadataAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask(metadata)); return manager; } } } }