/* ======================================================================== * Copyright (c) 2005-2026 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.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Moq; using NUnit.Framework; using Opc.Ua.Server.Fluent; using Opc.Ua.Server.Hosting; namespace Opc.Ua.Server.Tests.Hosting { /// /// Coverage tests for and the /// AddNodeManager extension that creates and registers it. /// /// /// Tests that exercise the constructor validation and property projection /// run without any server infrastructure. The /// path uses a mocked constructed in the /// same way as . /// [TestFixture] [Category("Hosting")] [SetCulture("en-us")] [SetUICulture("en-us")] [Parallelizable] public sealed class FluentNodeManagerFactoryCoverageTests { private const string TestNamespaceUri = "http://test.org/UA/FluentCoverage/"; [Test] public void CtorWithEmptyNamespaceUriThrowsArgumentException() { Assert.That( () => new FluentNodeManagerFactory(string.Empty, _ => { }), Throws.TypeOf()); } [Test] public void CtorWithWhitespaceNamespaceUriThrowsArgumentException() { Assert.That( () => new FluentNodeManagerFactory(" ", _ => { }), Throws.TypeOf()); } [Test] public void CtorWithNullBuildThrowsArgumentNullException() { Action? nullBuild = null; Assert.That( () => new FluentNodeManagerFactory(TestNamespaceUri, nullBuild!), Throws.TypeOf()); } [Test] public void CtorWithWhitespaceRootBrowseNameThrowsArgumentException() { Assert.That( () => new FluentNodeManagerFactory(TestNamespaceUri, _ => { }, " "), Throws.TypeOf()); } [Test] public void CtorWithValidArgumentsSetsNamespacesUris() { var factory = new FluentNodeManagerFactory(TestNamespaceUri, _ => { }); Assert.That(factory.NamespacesUris, Has.Count.EqualTo(1)); Assert.That(factory.NamespacesUris[0], Is.EqualTo(TestNamespaceUri)); } [Test] public async Task CreateAsyncReturnsFluentNodeManagerAsync() { Mock mockServer = BuildMockServer(); var factory = new FluentNodeManagerFactory(TestNamespaceUri, _ => { }); IAsyncNodeManager manager = await factory.CreateAsync( mockServer.Object, new ApplicationConfiguration(), CancellationToken.None) .ConfigureAwait(false); Assert.That(manager, Is.Not.Null); Assert.That(manager, Is.InstanceOf()); ((IDisposable)manager).Dispose(); } [Test] public async Task CreateAsyncInvokesProvidedBuildCallbackAsync() { Mock mockServer = BuildMockServer(); bool buildCalled = false; var factory = new FluentNodeManagerFactory( TestNamespaceUri, _ => buildCalled = true); IAsyncNodeManager manager = await factory.CreateAsync( mockServer.Object, new ApplicationConfiguration(), CancellationToken.None) .ConfigureAwait(false); // The build callback is invoked during CreateAddressSpaceAsync, not CreateAsync. // CreateAsync only instantiates the manager; we verify it completed without error. Assert.That(manager, Is.Not.Null); Assert.That(buildCalled, Is.False, "Build callback must not be invoked during factory creation."); ((IDisposable)manager).Dispose(); } [Test] public async Task CreateAddressSpaceAsyncDoesNotCreateImplicitRootFolderAsync() { Mock mockServer = BuildMockServer(); var factory = new FluentNodeManagerFactory(TestNamespaceUri, _ => { }); IAsyncNodeManager manager = await factory.CreateAsync( mockServer.Object, new ApplicationConfiguration(), CancellationToken.None) .ConfigureAwait(false); var externalReferences = new Dictionary>(); await manager.CreateAddressSpaceAsync(externalReferences, CancellationToken.None) .ConfigureAwait(false); Assert.That( externalReferences.ContainsKey(ObjectIds.ObjectsFolder), Is.False); Assert.That( await manager.GetManagerHandleAsync( new NodeId("ReferenceServer", 1), CancellationToken.None).ConfigureAwait(false), Is.Null); ((IDisposable)manager).Dispose(); } [Test] public async Task ConfiguredRootFolderIsMirroredUnderObjectsFolderAsync() { Mock mockServer = BuildMockServer(); var factory = new FluentNodeManagerFactory( TestNamespaceUri, builder => builder.Node("ReferenceServer"), "ReferenceServer"); IAsyncNodeManager manager = await factory.CreateAsync( mockServer.Object, new ApplicationConfiguration(), CancellationToken.None) .ConfigureAwait(false); var externalReferences = new Dictionary>(); await manager.CreateAddressSpaceAsync(externalReferences, CancellationToken.None) .ConfigureAwait(false); var rootId = new NodeId("ReferenceServer", 1); Assert.That(externalReferences.ContainsKey(ObjectIds.ObjectsFolder), Is.True); Assert.That( externalReferences[ObjectIds.ObjectsFolder].Count(r => r.ReferenceTypeId == ReferenceTypeIds.Organizes && !r.IsInverse && r.TargetId == new ExpandedNodeId(rootId)), Is.EqualTo(1)); Assert.That( await manager.GetManagerHandleAsync(rootId, CancellationToken.None) .ConfigureAwait(false), Is.Not.Null); ((IDisposable)manager).Dispose(); } [Test] public async Task BuildCreatedInstanceUnderObjectsFolderIsMirroredToExternalReferencesAsync() { Mock mockServer = BuildMockServer(); NodeId instanceId = NodeId.Null; var factory = new FluentNodeManagerFactory( TestNamespaceUri, builder => instanceId = builder .CreateInstance( new QualifiedName("Boiler #2", 1), p => new BaseObjectState(p)) .Configure(n => n.UnderObjectsFolder()) .Node.NodeId); IAsyncNodeManager manager = await factory.CreateAsync( mockServer.Object, new ApplicationConfiguration(), CancellationToken.None) .ConfigureAwait(false); var externalReferences = new Dictionary>(); await manager.CreateAddressSpaceAsync(externalReferences, CancellationToken.None) .ConfigureAwait(false); // The forward Organizes edge from the Objects folder to the // build-created instance must have been published for the // master node manager to distribute (issue #4329). Assert.That(instanceId.IsNull, Is.False); Assert.That(externalReferences.ContainsKey(ObjectIds.ObjectsFolder), Is.True); Assert.That( externalReferences[ObjectIds.ObjectsFolder].Count(r => r.ReferenceTypeId == ReferenceTypeIds.Organizes && !r.IsInverse), Is.EqualTo(1)); Assert.That( externalReferences[ObjectIds.ObjectsFolder] .Single(r => r.ReferenceTypeId == ReferenceTypeIds.Organizes && !r.IsInverse) .TargetId, Is.EqualTo(new ExpandedNodeId(instanceId))); ((IDisposable)manager).Dispose(); } [Test] public void AddNodeIdFactoryRegistersTheFactoryForEveryNodeManager() { var services = new ServiceCollection(); services.AddLogging(); services.AddOpcUa() .AddServer(o => { o.ApplicationName = "NodeIdFactoryServer"; o.ApplicationUri = "urn:test:NodeIdFactoryServer"; o.ProductUri = "urn:test:product"; }) .AddNodeIdFactory(NodeIdAssignmentMode.Guid); using ServiceProvider provider = services.BuildServiceProvider(); var registered = provider.GetService(); Assert.That(registered, Is.Not.Null); Assert.That(registered!.Mode, Is.EqualTo(NodeIdAssignmentMode.Guid)); } [Test] public void AddNodeIdFactoryReplacesAPreviousRegistration() { var services = new ServiceCollection(); services.AddLogging(); services.AddOpcUa() .AddServer(o => { o.ApplicationName = "NodeIdFactoryServer"; o.ApplicationUri = "urn:test:NodeIdFactoryServer"; o.ProductUri = "urn:test:product"; }) .AddNodeIdFactory(NodeIdAssignmentMode.Guid) .AddNodeIdFactory(new DefaultNodeIdFactory(NodeIdAssignmentMode.Counter, 7)); using ServiceProvider provider = services.BuildServiceProvider(); var registered = provider.GetService(); Assert.Multiple(() => { Assert.That(registered!.Mode, Is.EqualTo(NodeIdAssignmentMode.Counter)); Assert.That(registered.DefaultNamespaceIndex, Is.EqualTo(7)); }); } [Test] public void AddNodeIdFactoryRejectsMissingArguments() { var services = new ServiceCollection(); services.AddLogging(); IOpcUaServerBuilder builder = services.AddOpcUa() .AddServer(o => { o.ApplicationName = "NodeIdFactoryServer"; o.ApplicationUri = "urn:test:NodeIdFactoryServer"; o.ProductUri = "urn:test:product"; }); Assert.Multiple(() => { Assert.Throws( () => builder.AddNodeIdFactory((IRebasableNodeIdFactory)null!)); Assert.Throws( () => ((IOpcUaServerBuilder)null!).AddNodeIdFactory( NodeIdAssignmentMode.String)); }); } [Test] public void AddNodeManagerExtensionRegistersAsyncNodeManagerFactory() { var services = new ServiceCollection(); services.AddLogging(); services.AddOpcUa() .AddServer(o => { o.ApplicationName = "FluentFactoryServer"; o.ApplicationUri = "urn:test:FluentFactoryServer"; o.ProductUri = "urn:test:product"; }) .AddNodeManager(TestNamespaceUri, _ => { }); Assert.That( services, Has.Some.Matches( d => d.ServiceType == typeof(IAsyncNodeManagerFactory))); } [Test] public void AddNodeManagerExtensionRegistersNodeManagerRegistration() { var services = new ServiceCollection(); services.AddLogging(); services.AddOpcUa() .AddServer(o => { o.ApplicationName = "FluentFactoryServer"; o.ApplicationUri = "urn:test:FluentFactoryServer"; o.ProductUri = "urn:test:product"; }) .AddNodeManager(TestNamespaceUri, _ => { }); Assert.That( services, Has.Some.Matches( d => d.ServiceType == typeof(OpcUaServerNodeManagerRegistration))); } [Test] public void AddNodeManagerExtensionResolvedFactoryHasCorrectNamespace() { var services = new ServiceCollection(); services.AddLogging(); services.AddOpcUa() .AddServer(o => { o.ApplicationName = "FluentFactoryServer"; o.ApplicationUri = "urn:test:FluentFactoryServer"; o.ProductUri = "urn:test:product"; }) .AddNodeManager(TestNamespaceUri, _ => { }); using ServiceProvider sp = services.BuildServiceProvider(); IAsyncNodeManagerFactory factory = sp.GetRequiredService(); Assert.That(factory.NamespacesUris, Has.Count.EqualTo(1)); Assert.That(factory.NamespacesUris[0], Is.EqualTo(TestNamespaceUri)); } [Test] public async Task AddReferenceServerConfiguresNamedRootFolderAsync() { const string referenceNamespaceUri = "http://opcfoundation.org/UA/ReferenceServer"; var services = new ServiceCollection(); services.AddLogging(); services.AddOpcUa().AddReferenceServer(); using ServiceProvider sp = services.BuildServiceProvider(); OpcUaServerNodeManagerRegistration registration = sp .GetServices() .Single(); IAsyncNodeManagerFactory factory = registration.AsyncFactory!; Mock mockServer = BuildMockServer(referenceNamespaceUri); IAsyncNodeManager manager = await factory.CreateAsync( mockServer.Object, new ApplicationConfiguration(), CancellationToken.None) .ConfigureAwait(false); try { var externalReferences = new Dictionary>(); await manager.CreateAddressSpaceAsync( externalReferences, CancellationToken.None).ConfigureAwait(false); var rootId = new NodeId("ReferenceServer", 1); Assert.That( externalReferences[ObjectIds.ObjectsFolder].Count(reference => reference.ReferenceTypeId == ReferenceTypeIds.Organizes && !reference.IsInverse && reference.TargetId == new ExpandedNodeId(rootId)), Is.EqualTo(1)); Assert.That( await manager.GetManagerHandleAsync(rootId, CancellationToken.None) .ConfigureAwait(false), Is.Not.Null); } finally { ((IDisposable)manager).Dispose(); } } private static Mock BuildMockServer( string namespaceUri = TestNamespaceUri) { var namespaceTable = new NamespaceTable(); namespaceTable.Append(namespaceUri); var mockTelemetry = new Mock(); mockTelemetry .SetupGet(t => t.LoggerFactory) .Returns(NullLoggerFactory.Instance); var mockServer = new Mock(); mockServer.SetupGet(s => s.Telemetry).Returns(mockTelemetry.Object); mockServer.SetupGet(s => s.NamespaceUris).Returns(namespaceTable); var systemContext = new ServerSystemContext(mockServer.Object); mockServer.SetupGet(s => s.DefaultSystemContext).Returns(systemContext); return mockServer; } } }