/* ======================================================================== * 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.Threading.Tasks; using Moq; using NUnit.Framework; using Opc.Ua.Server.Fluent; using Opc.Ua.Tests; namespace Opc.Ua.Server.Tests.Fluent { /// /// Verifies exclusive complete-result registration across all built-in node-builder families. /// [TestFixture] [Parallelizable] public sealed class MethodInvocationBuilderExtensionsTests { /// /// Verifies that both ordinary overloads reject complete handlers in either registration order. /// /// The public builder entry point exercised by this case. /// Whether the ordinary handler is asynchronous. /// Whether the complete-result handler is registered first. [Test] public void BuiltInBuildersRejectMixedHandlersInEitherOrder( [Values("predefined", "instance", "reference-child", "resolved")] string family, [Values] bool asynchronous, [Values] bool completeFirst) { var root = new BaseObjectState(null) { NodeId = new NodeId("Root", kNamespace), BrowseName = new QualifiedName("Root", kNamespace) }; INodeBuilder builder = CreateBuilder(family, root); MethodState method = builder.Node; GenericMethodCalledEventHandler2 ordinary = s_ordinaryHandler; GenericMethodCalledEventHandler2Async ordinaryAsync = s_ordinaryHandlerAsync; void RegisterOrdinary() { if (asynchronous) { builder.OnCall(ordinaryAsync); } else { builder.OnCall(ordinary); } } ServiceResultException? error; if (completeFirst) { Assert.That(builder.OnCallWithResult(s_completeHandler), Is.SameAs(builder)); error = Assert.Throws(RegisterOrdinary); Assert.That(method.OnCallMethodWithResultAsync, Is.SameAs(s_completeHandler)); Assert.That(method.OnCallMethod2, Is.Null); Assert.That(method.OnCallMethod2Async, Is.Null); } else { RegisterOrdinary(); error = Assert.Throws(() => builder.OnCallWithResult(s_completeHandler)); Assert.That(method.OnCallMethodWithResultAsync, Is.Null); if (asynchronous) { Assert.That(method.OnCallMethod2Async, Is.SameAs(ordinaryAsync)); } else { Assert.That(method.OnCallMethod2, Is.SameAs(ordinary)); } } Assert.That(error!.StatusCode, Is.EqualTo(StatusCodes.BadConfigurationError)); } /// /// Verifies ordinary registration remains available when no complete-result handler is installed. /// /// The public builder entry point exercised by this case. /// Whether the ordinary handler is asynchronous. [Test] public void BuiltInBuildersAllowOrdinaryHandlersWithoutACompleteHandler( [Values("predefined", "instance", "reference-child", "resolved")] string family, [Values] bool asynchronous) { var root = new BaseObjectState(null) { NodeId = new NodeId("Root", kNamespace), BrowseName = new QualifiedName("Root", kNamespace) }; INodeBuilder builder = CreateBuilder(family, root); GenericMethodCalledEventHandler2 handler = s_ordinaryHandler; GenericMethodCalledEventHandler2Async asyncHandler = s_ordinaryHandlerAsync; INodeBuilder configured = asynchronous ? builder.OnCall(asyncHandler) : builder.OnCall(handler); Assert.That(configured, Is.SameAs(builder)); Assert.That(builder.Node.OnCallMethodWithResultAsync, Is.Null); if (asynchronous) { Assert.That(builder.Node.OnCallMethod2Async, Is.SameAs(asyncHandler)); } else { Assert.That(builder.Node.OnCallMethod2, Is.SameAs(handler)); } } /// /// Creates each built-in builder through its public entry point. /// /// The public entry point to use. /// The owning root of the authored method's node graph. /// The family is not a supported test case. private static INodeBuilder CreateBuilder(string family, BaseObjectState root) { var namespaces = new NamespaceTable(); var context = new SystemContext(NUnitTelemetryContext.Create()) { NamespaceUris = namespaces, TypeTable = new TypeTable(namespaces) }; var manager = new NodeManagerBuilder( context, FluentTestNodeManager.Create(kNamespace), kNamespace, _ => root, id => id == root.NodeId ? root : null!, _ => []); INodeBuilder parent = manager.Node(root.NodeId); if (family == "instance") { return parent.CreateInstance( new QualifiedName("Run", kNamespace), static owner => new MethodState(owner)).AsNode(); } if (family == "reference-child") { INodeBuilder group = parent.AddObject("Group"); var method = new MethodState(group.Node) { NodeId = new NodeId("Run", kNamespace), BrowseName = new QualifiedName("Run", kNamespace) }; group.Node.AddChild(method); return group.Child(method.BrowseName); } var resolved = new MethodState(root) { NodeId = new NodeId("Run", kNamespace), BrowseName = new QualifiedName("Run", kNamespace) }; root.AddChild(resolved); if (family == "predefined") { return manager.Node(resolved); } if (family == "resolved") { var facade = new Mock(); facade.SetupGet(value => value.Context).Returns(context); facade.SetupGet(value => value.NodeManager).Returns(manager.NodeManager); facade.SetupGet(value => value.DefaultNamespaceIndex).Returns(kNamespace); return facade.Object.Node(resolved); } throw new ArgumentOutOfRangeException(nameof(family)); } /// The namespace used for the authored node graph. private const ushort kNamespace = 2; /// The ordinary synchronous callback used to verify preserved registration identity. private static readonly GenericMethodCalledEventHandler2 s_ordinaryHandler = static (_, _, _, _, _) => ServiceResult.Good; /// The ordinary asynchronous callback used to verify preserved registration identity. private static readonly GenericMethodCalledEventHandler2Async s_ordinaryHandlerAsync = static (_, _, _, _, _, _) => new ValueTask(ServiceResult.Good); /// The complete-result handler whose registration must remain exclusive. private static readonly MethodCalledWithResultEventHandlerAsync s_completeHandler = static (_, _, _, _, _) => new ValueTask(new MethodInvocationResult(ServiceResult.Good)); } }