/* ========================================================================
* 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