Files
UA-.NETStandard/tests/Opc.Ua.Server.Tests/NodeManager/MasterNodeManagerNodeManagementTests.cs
romanett 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# Description

Nullable reference types are now enabled once, in `common.props`, and
every project in the repository builds warning-clean with them.

**Configuration**
- `<Nullable>enable</Nullable>` lives in `common.props`. All per-project
`<Nullable>` settings are removed: 178 project files (enable,
annotations, and the `disable` in the three Aot test projects).
- 419 per-file `#nullable enable`/`restore` directives and the 4
`#nullable disable` directives are removed.
- Kept on purpose:
- The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type
Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`,
`PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the
project setting in generated code, so the directive is required there.
- `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against
the 1.5.378 packages and does not import the repository props.
- `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now
describe the central setting.

**Migration** (the 43 unset and 13 annotations-only projects, roughly
10k diagnostics)
- **Production code: annotated at the declarations.** Covers
`tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the
LDS sample.
- Members, parameters and returns are nullable where null actually
flows.
- Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`,
`IsMethodTypeDesign`).
- `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take
`params object?[]`. `Template.AddReplacement` takes `string?` (rendering
already skips null replacements).
- `IRegisteredServerStore.RegisterAsync` returns
`Task<RegistrationEntry?>`: it returns null when a registration is
removed.
- Dereferences backed by an invariant (for example, the validated model)
use `!`, so runtime behaviour is unchanged.
- **Behavioural changes in production code** (small, deliberate):
- `NodeSetToModelDesign` throws `InvalidOperationException` when the
file is not a NodeSet. Before, it threw a NullReferenceException on the
next line.
- `ModelDesignValidator` starts its lookup tables empty instead of null.
A redundant `if (m_nodes == null) return;` guard is gone.
- **Test code: minimal annotations.**
  - `?` on values that can be null, `!` where the test asserts non-null.
  - `= null!` for fixture members assigned in SetUp.
- `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the
`IEncodeable` constraint.
- A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`)
folded into the adjacent `IsNull`/`Count` checks.

**Notes for reviewers**
- The diff is large (~1,500 files) but almost entirely mechanical: `?`,
`!` and directive removal. The non-annotation changes are listed above.
- Known follow-up, not done here:
`IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay
non-nullable even though the validator returns null before it loads a
dictionary. Making them nullable would ripple through every generator
call site.

## Related Issues

- Fixes #4439

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

The "no new warnings" box covers net10.0 and net48: full non-incremental
builds of `UA.slnx`, `tools/SourceGeneration.slnx`,
`tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0
warnings and 0 errors under the repository's warnings-as-errors
settings. net9.0 and net8.0 also had no nullable diagnostics, but they
were not rebuilt after the final fixes. Tests have not been run locally
yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:48:13 +02:00

2985 lines
133 KiB
C#

/* ========================================================================
* 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
{
/// <summary>
/// Unit tests for the NodeManagement dispatcher methods on
/// <see cref="MasterNodeManager"/>: per-item validation paths
/// (BadNothingToDo / BadBrowseNameInvalid / BadParentNodeIdInvalid /
/// BadReferenceTypeIdInvalid / BadReferenceNotAllowed /
/// BadSourceNodeIdInvalid / BadTargetNodeIdInvalid /
/// BadNodeIdInvalid / BadUserAccessDenied) and per-item routing.
/// </summary>
[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<StandardServer> m_fixture = null!;
private StandardServer m_server = null!;
[OneTimeSetUp]
public async Task OneTimeSetUpAsync()
{
m_fixture = new ServerFixture<StandardServer>(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<StatusCode> 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<OperationCanceledException>(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<ServiceResultException>(() =>
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<AddNodesResult> 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<AddNodesResult> 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<AddNodesResult> 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<AddNodesResult> 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<AddNodesResult> 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<AddNodesResult> 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<INodeManagerWithNodeManagement> 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<AddNodesResult> 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<INodeManagerWithNodeManagement> 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<OperationContext>(),
It.IsAny<AddNodesItem>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync((ServiceResult.Good, requestedNodeId));
var item = new AddNodesItem
{
ParentNodeId = parentNodeId,
ReferenceTypeId = ReferenceTypeIds.Organizes,
RequestedNewNodeId = requestedNodeId,
BrowseName = new QualifiedName("Requested", namespaceIndex),
NodeClass = NodeClass.Object
};
(ArrayOf<AddNodesResult> 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<INodeManagerWithNodeManagement> manager = CreateNodeManagementManager(true);
using MasterNodeManager sut = CreateMasterNodeManager(manager.Object);
ushort namespaceIndex = GetTestNamespaceIndex();
NodeId parentNodeId = ConfigureParent(manager, namespaceIndex);
manager
.Setup(m => m.AddNodeAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddNodesItem>(),
It.IsAny<CancellationToken>()))
.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<AddNodesResult> 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<AddNodesResult> 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<INodeManagerWithNodeManagement> 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<OperationContext>(),
It.IsAny<AddNodesItem>(),
It.IsAny<CancellationToken>()))
.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<AddNodesResult> 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<StatusCode> 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<StatusCode> 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<StatusCode> 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<StatusCode> 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<StatusCode> 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<StatusCode> 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<StatusCode> results, _) = await sut.AddReferencesAsync(
ctx, new AddReferencesItem[] { item }.ToArrayOf(), CancellationToken.None).ConfigureAwait(false);
Assert.That(results[0], Is.EqualTo(StatusCodes.BadReferenceTypeIdInvalid));
}
/// <summary>
/// A known type that is not a ReferenceType (ObjectType, DataType) is not a valid
/// ReferenceTypeId (Part 4 7.38.2 Bad_ReferenceTypeIdInvalid).
/// </summary>
[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<StatusCode> 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<StatusCode> deleteResults, _) = await sut.DeleteReferencesAsync(
ctx,
new DeleteReferencesItem[]
{
new()
{
SourceNodeId = ObjectIds.ObjectsFolder,
ReferenceTypeId = referenceTypeId,
IsForward = true,
TargetNodeId = ObjectIds.Server
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
(ArrayOf<AddNodesResult> 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));
}
/// <summary>
/// No Reference of an abstract ReferenceType shall exist (Part 3 5.3.1), so AddNodes,
/// AddReferences and DeleteReferences reject abstract ReferenceTypeIds with
/// Bad_ReferenceTypeIdInvalid.
/// </summary>
[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<StatusCode> 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<AddNodesResult> 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<StatusCode> 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<StatusCode> 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<StatusCode> 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<StatusCode> 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<StatusCode> results, ArrayOf<DiagnosticInfo> 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<StatusCode> results, ArrayOf<DiagnosticInfo> 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<AddNodesResult> results, ArrayOf<DiagnosticInfo> 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<AddNodesResult> results, ArrayOf<DiagnosticInfo> 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<AddNodesResult> results, ArrayOf<DiagnosticInfo> diagnostics) = await sut.AddNodesAsync(
ctx,
Array.Empty<AddNodesItem>().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<AddNodesItem>().ToArrayOf(),
CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf<ArgumentNullException>());
}
[Test]
public void DeleteNodesAsync_NullContext_ThrowsArgumentNullException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
Assert.That(
async () => await sut.DeleteNodesAsync(
null!,
Array.Empty<DeleteNodesItem>().ToArrayOf(),
CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf<ArgumentNullException>());
}
[Test]
public void AddReferencesAsync_NullContext_ThrowsArgumentNullException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
Assert.That(
async () => await sut.AddReferencesAsync(
null!,
Array.Empty<AddReferencesItem>().ToArrayOf(),
CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf<ArgumentNullException>());
}
[Test]
public void DeleteReferencesAsync_NullContext_ThrowsArgumentNullException()
{
using MasterNodeManager sut = CreateMasterNodeManager();
Assert.That(
async () => await sut.DeleteReferencesAsync(
null!,
Array.Empty<DeleteReferencesItem>().ToArrayOf(),
CancellationToken.None).ConfigureAwait(false),
Throws.TypeOf<ArgumentNullException>());
}
[Test]
public async Task AddNodesDeniedAddNodePermissionDoesNotMutateAsync()
{
using var harness = new AuthorizationHarness();
harness.SetAddNodePermissions(PermissionType.Browse);
AddNodesItem item = harness.CreateAddNodesItem();
(ArrayOf<AddNodesResult> 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<OperationContext>(),
It.IsAny<AddNodesItem>(),
It.IsAny<CancellationToken>()),
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<AddNodesResult> 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<OperationContext>(), It.IsAny<AddNodesItem>(), It.IsAny<CancellationToken>()),
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<OperationContext>(), It.IsAny<CallMethodRequest>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask<MethodState>(new MethodState(null) { NodeId = harness.TargetNodeId }));
using OperationContext context = harness.CreateIdentityContext(RequestType.Call, requestKind);
(ArrayOf<CallMethodResult> 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<AddNodesResult> 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<CancellationToken>()),
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<AddNodesResult> 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<OperationContext>(),
It.IsAny<AddNodesItem>(),
It.IsAny<CancellationToken>()),
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<AddNodesResult> 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<OperationContext>(),
It.IsAny<AddNodesItem>(),
It.IsAny<CancellationToken>()),
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<AddNodesResult> 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<CancellationToken>()),
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<AddNodesResult> 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<CancellationToken>()),
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<AddNodesResult> 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<OperationContext>(),
It.IsAny<AddNodesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task DeleteNodesDeniedDeleteNodePermissionDoesNotMutateAsync()
{
using var harness = new AuthorizationHarness();
harness.SetSourcePermissions(PermissionType.Browse);
var item = new DeleteNodesItem
{
NodeId = harness.SourceNodeId
};
(ArrayOf<StatusCode> 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<object>(),
It.IsAny<BrowseResultMask>(),
It.IsAny<Dictionary<NodeId, Variant[]>>(),
true,
It.IsAny<CancellationToken>()),
Times.Once);
harness.SourceManager.Verify(
manager => manager.DeleteNodeAsync(
It.IsAny<OperationContext>(),
It.IsAny<DeleteNodesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task DeleteNodesGrantedDeleteNodePermissionMutatesAsync()
{
using var harness = new AuthorizationHarness();
var item = new DeleteNodesItem
{
NodeId = harness.SourceNodeId
};
(ArrayOf<StatusCode> 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<CancellationToken>()),
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<StatusCode> 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<OperationContext>(),
It.IsAny<DeleteNodesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task AddReferencesDeniedTargetPermissionDoesNotHalfApplyAsync()
{
using var harness = new AuthorizationHarness();
harness.SetTargetPermissions(PermissionType.Browse);
AddReferencesItem item = harness.CreateAddReferencesItem();
(ArrayOf<StatusCode> 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<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
harness.TargetManager.Verify(
manager => manager.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
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<StatusCode> 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<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
harness.TargetManager.Verify(
manager => manager.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task AddReferencesGrantedPermissionsMutateBothSidesAsync()
{
using var harness = new AuthorizationHarness();
harness.UseOptimizedMetadataWithoutNodeClasses();
AddReferencesItem item = harness.CreateAddReferencesItem();
(ArrayOf<StatusCode> 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<CancellationToken>()),
Times.Once);
harness.TargetManager.Verify(
manager => manager.AddReferenceAsync(
harness.AddReferencesContext,
It.Is<AddReferencesItem>(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<CancellationToken>()),
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<StatusCode> 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<CancellationToken>()),
Times.Once);
harness.TargetManager.Verify(
manager => manager.GetPermissionMetadataAsync(
It.IsAny<OperationContext>(),
It.IsAny<object>(),
It.IsAny<BrowseResultMask>(),
It.IsAny<Dictionary<NodeId, Variant[]>>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()),
Times.Never);
harness.TargetManager.Verify(
manager => manager.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
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<StatusCode> 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<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
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<StatusCode> 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<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task AddReferencesLocalNamespaceUriProcessesTargetAsync()
{
using var harness = new AuthorizationHarness();
AddReferencesItem item = harness.CreateAddReferencesItem();
item.TargetNodeId = harness.CreateTargetExpandedNodeIdWithUri();
(ArrayOf<StatusCode> 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<AddReferencesItem>(inverse =>
inverse.SourceNodeId == harness.TargetNodeId &&
inverse.TargetNodeId == harness.SourceNodeId),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Test]
public async Task AddReferencesInverseFailureRollsBackSourceAsync()
{
using var harness = new AuthorizationHarness();
AddReferencesItem item = harness.CreateAddReferencesItem();
harness.TargetManager
.Setup(manager => manager.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(
new ServiceResult(StatusCodes.BadUnexpectedError)));
(ArrayOf<StatusCode> 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<DeleteReferencesItem>(rollback =>
rollback.SourceNodeId == item.SourceNodeId &&
rollback.TargetNodeId == item.TargetNodeId &&
rollback.ReferenceTypeId == item.ReferenceTypeId &&
rollback.IsForward == item.IsForward &&
!rollback.DeleteBidirectional),
It.IsAny<CancellationToken>()),
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<OperationContext>(), It.IsAny<AddReferencesItem>(), It.IsAny<CancellationToken>()))
.Returns(() => throws
? throw new ServiceResultException(StatusCodes.BadDuplicateReferenceNotAllowed)
: new ValueTask<ServiceResult>(StatusCodes.BadDuplicateReferenceNotAllowed));
(ArrayOf<StatusCode> 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<OperationContext>(), It.IsAny<DeleteReferencesItem>(), It.IsAny<CancellationToken>()),
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<OperationContext>(), It.IsAny<DeleteReferencesItem>(), It.IsAny<CancellationToken>()))
.Returns(() => throws
? throw new ServiceResultException(StatusCodes.BadNoMatch)
: new ValueTask<ServiceResult>(StatusCodes.BadNoMatch));
(ArrayOf<StatusCode> 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<OperationContext>(), It.IsAny<AddReferencesItem>(), It.IsAny<CancellationToken>()),
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<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()))
.Callback((
OperationContext _,
DeleteReferencesItem _,
CancellationToken token) => cleanupToken = token)
.Returns(new ValueTask<ServiceResult>(ServiceResult.Good));
harness.TargetManager
.Setup(manager => manager.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()))
.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<OperationCanceledException>());
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<StatusCode> 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<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
harness.TargetManager.Verify(
manager => manager.DeleteReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()),
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<StatusCode> 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<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
harness.TargetManager.Verify(
manager => manager.DeleteReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task DeleteReferencesGrantedPermissionsMutateBothSidesAsync()
{
using var harness = new AuthorizationHarness();
DeleteReferencesItem item = harness.CreateDeleteReferencesItem();
(ArrayOf<StatusCode> 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<DeleteReferencesItem>(source =>
source.SourceNodeId == item.SourceNodeId &&
source.TargetNodeId == item.TargetNodeId &&
source.ReferenceTypeId == item.ReferenceTypeId &&
source.IsForward == item.IsForward &&
!source.DeleteBidirectional),
It.IsAny<CancellationToken>()),
Times.Once);
harness.TargetManager.Verify(
manager => manager.DeleteReferenceAsync(
harness.DeleteReferencesContext,
It.Is<DeleteReferencesItem>(inverse =>
inverse.SourceNodeId == harness.TargetNodeId &&
inverse.TargetNodeId == harness.SourceNodeId &&
inverse.ReferenceTypeId == item.ReferenceTypeId &&
inverse.IsForward != item.IsForward &&
!inverse.DeleteBidirectional),
It.IsAny<CancellationToken>()),
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<StatusCode> 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<DeleteReferencesItem>(source =>
source.SourceNodeId == item.SourceNodeId &&
source.TargetNodeId == item.TargetNodeId &&
!source.DeleteBidirectional),
It.IsAny<CancellationToken>()),
Times.Once);
harness.TargetManager.Verify(
manager => manager.DeleteReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task DeleteReferencesLocalNamespaceUriProcessesTargetAsync()
{
using var harness = new AuthorizationHarness();
DeleteReferencesItem item = harness.CreateDeleteReferencesItem();
item.TargetNodeId = harness.CreateTargetExpandedNodeIdWithUri();
(ArrayOf<StatusCode> 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<DeleteReferencesItem>(inverse =>
inverse.SourceNodeId == harness.TargetNodeId &&
inverse.TargetNodeId == harness.SourceNodeId),
It.IsAny<CancellationToken>()),
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<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(
new ServiceResult(StatusCodes.BadUnexpectedError)));
(ArrayOf<StatusCode> 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<AddReferencesItem>(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<CancellationToken>()),
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<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()))
.Callback((
OperationContext _,
AddReferencesItem _,
CancellationToken token) => cleanupToken = token)
.Returns(new ValueTask<ServiceResult>(ServiceResult.Good));
harness.TargetManager
.Setup(manager => manager.DeleteReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()))
.Throws(new InvalidOperationException("Target mutation failed."));
(ArrayOf<StatusCode> 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<AddNodesResult> 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<OperationContext>(),
It.IsAny<AddNodesItem>(),
It.IsAny<CancellationToken>()),
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<AddNodesResult> 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<CancellationToken>()),
Times.Once);
}
[Test]
public async Task AddReferencesDeniedSourcePermissionDoesNotMutateAsync()
{
using var harness = new AuthorizationHarness();
harness.SetSourcePermissions(PermissionType.Browse);
AddReferencesItem item = harness.CreateAddReferencesItem();
(ArrayOf<StatusCode> 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<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
// The source denial precedes any target-side metadata lookup or mutation.
harness.TargetManager.Verify(
manager => manager.GetPermissionMetadataAsync(
It.IsAny<OperationContext>(),
It.IsAny<object>(),
It.IsAny<BrowseResultMask>(),
It.IsAny<Dictionary<NodeId, Variant[]>>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task AddReferencesExplicitLocalTargetUnknownReturnsBadTargetNodeIdInvalidAsync()
{
using var harness = new AuthorizationHarness();
AddReferencesItem item = harness.CreateAddReferencesItem();
item.TargetNodeId = new NodeId("UnknownTarget", harness.TargetNamespaceIndex);
(ArrayOf<StatusCode> 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<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task AddReferencesSameManagerLocalTargetMirrorsInverseEdgeAsync()
{
ushort namespaceIndex = GetTestNamespaceIndex();
var sourceNodeId = new NodeId("Source", namespaceIndex);
var targetNodeId = new NodeId("Target", namespaceIndex);
Mock<INodeManagerWithNodeManagement> 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<StatusCode> 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<AddReferencesItem>(request =>
request.SourceNodeId == sourceNodeId &&
request.TargetNodeId == targetNodeId &&
request.IsForward),
It.IsAny<CancellationToken>()),
Times.Once);
manager.Verify(
m => m.AddReferenceAsync(
context,
It.Is<AddReferencesItem>(request =>
request.SourceNodeId == targetNodeId &&
request.TargetNodeId == sourceNodeId &&
!request.IsForward),
It.IsAny<CancellationToken>()),
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<StatusCode> 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<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task AddReferencesCrossManagerSourceMetadataNullReturnsBadSourceNodeIdInvalidAsync()
{
using var harness = new AuthorizationHarness();
ReturnNullMetadata(harness.SourceManager);
AddReferencesItem item = harness.CreateAddReferencesItem();
(ArrayOf<StatusCode> 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<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
harness.TargetManager.Verify(
manager => manager.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task AddReferencesCrossManagerTargetMetadataNullReturnsBadTargetNodeIdInvalidAsync()
{
using var harness = new AuthorizationHarness();
ReturnNullMetadata(harness.TargetManager);
AddReferencesItem item = harness.CreateAddReferencesItem();
(ArrayOf<StatusCode> 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<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
harness.TargetManager.Verify(
manager => manager.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task AddReferencesInverseServiceResultExceptionRollsBackSourceAsync()
{
using var harness = new AuthorizationHarness();
AddReferencesItem item = harness.CreateAddReferencesItem();
harness.TargetManager
.Setup(manager => manager.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()))
.Throws(new ServiceResultException(StatusCodes.BadInvalidState));
(ArrayOf<StatusCode> 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<DeleteReferencesItem>(rollback =>
rollback.SourceNodeId == item.SourceNodeId &&
rollback.TargetNodeId == item.TargetNodeId &&
rollback.ReferenceTypeId == item.ReferenceTypeId &&
rollback.IsForward == item.IsForward &&
!rollback.DeleteBidirectional),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Test]
public async Task AddReferencesInverseFailureWithRollbackFailureReturnsInverseStatusAsync()
{
using var harness = new AuthorizationHarness();
AddReferencesItem item = harness.CreateAddReferencesItem();
harness.TargetManager
.Setup(manager => manager.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(
new ServiceResult(StatusCodes.BadUnexpectedError)));
harness.SourceManager
.Setup(manager => manager.DeleteReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(
new ServiceResult(StatusCodes.BadNodeIdUnknown)));
(ArrayOf<StatusCode> 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<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Test]
public async Task AddReferencesInverseFailureWithRollbackExceptionReturnsInverseStatusAsync()
{
using var harness = new AuthorizationHarness();
AddReferencesItem item = harness.CreateAddReferencesItem();
harness.TargetManager
.Setup(manager => manager.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(
new ServiceResult(StatusCodes.BadUnexpectedError)));
harness.SourceManager
.Setup(manager => manager.DeleteReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()))
.Throws(new InvalidOperationException("Rollback failed."));
(ArrayOf<StatusCode> 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<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Test]
public async Task DeleteReferencesDeniedSourcePermissionDoesNotMutateAsync()
{
using var harness = new AuthorizationHarness();
harness.SetSourcePermissions(PermissionType.Browse);
DeleteReferencesItem item = harness.CreateDeleteReferencesItem();
(ArrayOf<StatusCode> 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<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
harness.TargetManager.Verify(
manager => manager.GetPermissionMetadataAsync(
It.IsAny<OperationContext>(),
It.IsAny<object>(),
It.IsAny<BrowseResultMask>(),
It.IsAny<Dictionary<NodeId, Variant[]>>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task DeleteReferencesExplicitLocalTargetUnknownReturnsBadTargetNodeIdInvalidAsync()
{
using var harness = new AuthorizationHarness();
DeleteReferencesItem item = harness.CreateDeleteReferencesItem();
item.TargetNodeId = new NodeId("UnknownTarget", harness.TargetNamespaceIndex);
(ArrayOf<StatusCode> 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<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task DeleteReferencesSameManagerLocalTargetDeletesInverseEdgeAsync()
{
ushort namespaceIndex = GetTestNamespaceIndex();
var sourceNodeId = new NodeId("Source", namespaceIndex);
var targetNodeId = new NodeId("Target", namespaceIndex);
Mock<INodeManagerWithNodeManagement> 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<StatusCode> 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<DeleteReferencesItem>(request =>
request.SourceNodeId == sourceNodeId &&
request.TargetNodeId == targetNodeId &&
request.IsForward &&
!request.DeleteBidirectional),
It.IsAny<CancellationToken>()),
Times.Once);
manager.Verify(
m => m.DeleteReferenceAsync(
context,
It.Is<DeleteReferencesItem>(request =>
request.SourceNodeId == targetNodeId &&
request.TargetNodeId == sourceNodeId &&
!request.IsForward &&
!request.DeleteBidirectional),
It.IsAny<CancellationToken>()),
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<StatusCode> 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<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task DeleteReferencesCrossManagerTargetMetadataNullReturnsBadTargetNodeIdInvalidAsync()
{
using var harness = new AuthorizationHarness();
ReturnNullMetadata(harness.TargetManager);
DeleteReferencesItem item = harness.CreateDeleteReferencesItem();
(ArrayOf<StatusCode> 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<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public async Task DeleteReferencesInverseServiceResultExceptionRestoresSourceAsync()
{
using var harness = new AuthorizationHarness();
DeleteReferencesItem item = harness.CreateDeleteReferencesItem();
harness.TargetManager
.Setup(manager => manager.DeleteReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()))
.Throws(new ServiceResultException(StatusCodes.BadInvalidState));
(ArrayOf<StatusCode> 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<AddReferencesItem>(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<CancellationToken>()),
Times.Once);
}
[Test]
public async Task DeleteReferencesInverseFailureWithRestoreFailureReturnsInverseStatusAsync()
{
using var harness = new AuthorizationHarness();
DeleteReferencesItem item = harness.CreateDeleteReferencesItem();
harness.TargetManager
.Setup(manager => manager.DeleteReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(
new ServiceResult(StatusCodes.BadUnexpectedError)));
harness.SourceManager
.Setup(manager => manager.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(
new ServiceResult(StatusCodes.BadNodeIdUnknown)));
(ArrayOf<StatusCode> 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<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Test]
public async Task DeleteReferencesInverseFailureWithRestoreExceptionReturnsInverseStatusAsync()
{
using var harness = new AuthorizationHarness();
DeleteReferencesItem item = harness.CreateDeleteReferencesItem();
harness.TargetManager
.Setup(manager => manager.DeleteReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(
new ServiceResult(StatusCodes.BadUnexpectedError)));
harness.SourceManager
.Setup(manager => manager.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()))
.Throws(new InvalidOperationException("Restore failed."));
(ArrayOf<StatusCode> 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<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
private static void ReturnNullMetadata(Mock<IAsyncNodeManager> manager)
{
manager
.Setup(nodeManager => nodeManager.GetPermissionMetadataAsync(
It.IsAny<OperationContext>(),
It.IsAny<object>(),
It.IsAny<BrowseResultMask>(),
It.IsAny<Dictionary<NodeId, Variant[]>>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<NodeMetadata?>((NodeMetadata?)null));
manager
.Setup(nodeManager => nodeManager.GetNodeMetadataAsync(
It.IsAny<OperationContext>(),
It.IsAny<object>(),
It.IsAny<BrowseResultMask>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<NodeMetadata>((NodeMetadata)null!));
}
/// <summary>
/// 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.
/// </summary>
[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<BrowseResult> 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<BrowseResult> 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<ArrayOf<StatusCode>> DispatchFailingNodeManagementBatchAsync(
AuthorizationHarness harness,
RequestType requestType,
Func<Exception> 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<OperationContext>(), It.IsAny<AddNodesItem>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask<(ServiceResult, NodeId)>((ServiceResult.Good, firstId)))
.Returns(() => throw failure())
.Returns(new ValueTask<(ServiceResult, NodeId)>((ServiceResult.Good, lastId)));
(ArrayOf<AddNodesResult> 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<OperationContext>(), It.IsAny<DeleteNodesItem>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(ServiceResult.Good))
.Returns(() => throw failure())
.Returns(new ValueTask<ServiceResult>(ServiceResult.Good));
var deletion = new DeleteNodesItem { NodeId = harness.SourceNodeId };
(ArrayOf<StatusCode> 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<OperationContext>(), It.IsAny<AddReferencesItem>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(ServiceResult.Good))
.Returns(() => throw failure())
.Returns(new ValueTask<ServiceResult>(ServiceResult.Good));
AddReferencesItem addition = harness.CreateAddReferencesItem();
(ArrayOf<StatusCode> 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<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(ServiceResult.Good))
.Returns(() => throw failure())
.Returns(new ValueTask<ServiceResult>(ServiceResult.Good));
DeleteReferencesItem referenceDeletion = harness.CreateDeleteReferencesItem();
(ArrayOf<StatusCode> deletedReferences, _) = await harness.Sut.DeleteReferencesAsync(
harness.DeleteReferencesContext,
[referenceDeletion, referenceDeletion, referenceDeletion],
cancellationToken).ConfigureAwait(false);
return deletedReferences;
default:
throw new ArgumentOutOfRangeException(nameof(requestType));
}
}
private static List<ExpandedNodeId> ChildNodeIds(BrowseResult result)
{
var ids = new List<ExpandedNodeId>();
foreach (ReferenceDescription reference in result.References)
{
ids.Add(reference.NodeId);
}
return ids;
}
private async Task<(ArrayOf<BrowseResult> results, ArrayOf<DiagnosticInfo> 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<INodeManager>(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<INodeManagerWithNodeManagement> CreateNodeManagementManager(
bool allowNodeManagement)
{
var manager = new Mock<INodeManagerWithNodeManagement>();
manager.Setup(m => m.NamespaceUris).Returns([TestNamespaceUri]);
manager.Setup(m => m.AllowNodeManagement).Returns(allowNodeManagement);
return manager;
}
private static Mock<INodeManagerWithNodeManagement> CreateSameManagerNodeManagementManager(
ushort namespaceIndex,
NodeId sourceNodeId,
NodeId targetNodeId)
{
Mock<INodeManagerWithNodeManagement> 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<OperationContext>(),
sourceHandle,
BrowseResultMask.NodeClass))
.Returns(new NodeMetadata(sourceHandle, sourceNodeId)
{
NodeClass = NodeClass.Object
});
manager
.Setup(m => m.GetNodeMetadata(
It.IsAny<OperationContext>(),
targetHandle,
BrowseResultMask.NodeClass))
.Returns(new NodeMetadata(targetHandle, targetNodeId)
{
NodeClass = NodeClass.Object
});
manager
.Setup(m => m.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(ServiceResult.Good);
manager
.Setup(m => m.DeleteReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(ServiceResult.Good);
return manager;
}
private static NodeId ConfigureParent(
Mock<INodeManagerWithNodeManagement> 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<OperationContext>(),
parentHandle,
BrowseResultMask.NodeClass))
.Returns(new NodeMetadata(parentHandle, parentNodeId)
{
NodeClass = NodeClass.Object
});
return parentNodeId;
}
private static OperationContext CreateContext()
{
var session = new Mock<ISession>();
session.Setup(s => s.EffectiveIdentity).Returns(new Mock<IUserIdentity>().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<ISession>();
session.Setup(s => s.EffectiveIdentity).Returns(new Mock<IUserIdentity>().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);
}
/// <summary>
/// Combines the legacy manager contract with the asynchronous NodeManagement facet.
/// </summary>
public interface INodeManagerWithNodeManagement : INodeManager, INodeManagementAsyncNodeManager;
/// <summary>
/// Standard concrete hierarchical references used by the valid-semantics test.
/// </summary>
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<ISession> 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<IConfigurationNodeManager>();
var coreNodeManager = new Mock<ICoreNodeManager>();
var factory = new Mock<IMainNodeManagerFactory>();
factory.Setup(managerFactory => managerFactory.CreateConfigurationNodeManager())
.Returns(configurationManager.Object);
factory.Setup(managerFactory =>
managerFactory.CreateCoreNodeManager(It.IsAny<ushort>()))
.Returns(coreNodeManager.Object);
var server = new Mock<IServerInternal>();
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<OperationContext>(),
It.IsAny<AddNodesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<(ServiceResult result, NodeId addedNodeId)>(
(ServiceResult.Good, addedNodeId)));
TargetManager
.Setup(manager => manager.AddNodeAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddNodesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<(ServiceResult result, NodeId addedNodeId)>(
(ServiceResult.Good, new NodeId("Added", TargetNamespaceIndex))));
SourceManager
.Setup(manager => manager.DeleteNodeAsync(
It.IsAny<OperationContext>(),
It.IsAny<DeleteNodesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(ServiceResult.Good));
SourceManager
.Setup(manager => manager.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(ServiceResult.Good));
SourceManager
.Setup(manager => manager.DeleteReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(ServiceResult.Good));
TargetManager
.Setup(manager => manager.AddReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<AddReferencesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(ServiceResult.Good));
TargetManager
.Setup(manager => manager.DeleteReferenceAsync(
It.IsAny<OperationContext>(),
It.IsAny<DeleteReferencesItem>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<ServiceResult>(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<ushort>(),
It.IsAny<CancellationToken>()))
.Returns((ushort namespaceIndex, CancellationToken _) =>
new ValueTask<NamespaceMetadataState?>(
(namespaceIndex == SourceNamespaceIndex
? m_sourceNamespaceMetadata
: namespaceIndex == TargetNamespaceIndex
? m_targetNamespaceMetadata
: null)!));
var identity = new Mock<IUserIdentity>();
identity.Setup(user => user.GrantedRoleIds)
.Returns(new NodeId[] { s_roleId }.ToArrayOf());
m_session = new Mock<ISession>();
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<IAsyncNodeManager> SourceManager { get; }
public Mock<IAsyncNodeManager> 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<IAsyncNodeManager> 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<OperationContext>(),
It.IsAny<object>(),
It.IsAny<BrowseResultMask>(),
It.IsAny<Dictionary<NodeId, Variant[]>>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<NodeMetadata?>(permissionMetadata));
}
private static Mock<IAsyncNodeManager> CreateNodeManager(
string namespaceUri,
NodeId nodeId,
NodeMetadata metadata)
{
var manager = new Mock<IAsyncNodeManager>();
manager.Setup(nodeManager => nodeManager.NamespaceUris)
.Returns([namespaceUri]);
manager.Setup(nodeManager => nodeManager.AllowNodeManagement)
.Returns(true);
manager
.Setup(nodeManager => nodeManager.GetManagerHandleAsync(
It.IsAny<NodeId>(),
It.IsAny<CancellationToken>()))
.Returns((NodeId requestedNodeId, CancellationToken _) =>
new ValueTask<object>(
requestedNodeId == nodeId ? nodeId : null!));
manager
.Setup(nodeManager => nodeManager.GetPermissionMetadataAsync(
It.IsAny<OperationContext>(),
It.IsAny<object>(),
It.IsAny<BrowseResultMask>(),
It.IsAny<Dictionary<NodeId, Variant[]>>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()))
.Returns((
OperationContext _,
object _,
BrowseResultMask _,
Dictionary<NodeId, Variant[]> _,
bool _,
CancellationToken _) =>
new ValueTask<NodeMetadata?>(metadata));
manager
.Setup(nodeManager => nodeManager.GetNodeMetadataAsync(
It.IsAny<OperationContext>(),
It.IsAny<object>(),
It.IsAny<BrowseResultMask>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<NodeMetadata>(metadata));
return manager;
}
}
}
}