/* ========================================================================
* 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));
}
}