/* ======================================================================== * 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.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Moq; using Opc.Ua.Client; using Opc.Ua.Tests; namespace Opc.Ua.Machinery.Tests { /// /// Builds a Moq-backed that dispatches browse, /// read, browse-path translation and method calls straight into a /// 's in-process address space, so the /// client surface can be exercised end to end without a TCP server. /// internal static class MachineryInProcessSessionBridge { public static Mock Build(MachineryServerFixture fixture) { var mock = new Mock(); NamespaceTable namespaceUris = fixture.Manager.Server.NamespaceUris; mock.SetupGet(session => session.NamespaceUris).Returns(namespaceUris); ServiceMessageContext context = ServiceMessageContext.Create( NUnitTelemetryContext.Create()); context.NamespaceUris = namespaceUris; mock.SetupGet(session => session.MessageContext).Returns(context); // Browser (used by MachineryClient's own browse helper, and by // BrowseNext-aware fixes to it) reads these three properties the // moment a Browser is attached to a session, regardless of // whether pagination is ever exercised. ISession declares // OperationLimits and ServerCapabilities non-nullable, so a real // session always has them; this bridge must too. // Type checks go through the node cache; answer them from the // server's own type tree. TypeTable typeTree = fixture.Manager.Server.TypeTree; var nodeCache = new Mock(); nodeCache .Setup(cache => cache.IsTypeOfAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns((NodeId subType, NodeId superType, CancellationToken _) => new ValueTask(typeTree.IsTypeOf(subType, superType))); mock.SetupGet(session => session.NodeCache).Returns(nodeCache.Object); mock.SetupGet(session => session.OperationLimits).Returns(new OperationLimits()); mock.SetupGet(session => session.ServerCapabilities).Returns(new ServerCapabilities()); mock.SetupGet(session => session.ContinuationPointPolicy) .Returns(ContinuationPointPolicy.Default); mock.Setup(session => session.TranslateBrowsePathsToNodeIdsAsync( It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync((RequestHeader? _, ArrayOf paths, CancellationToken _) => { var results = new BrowsePathResult[paths.Count]; for (int ii = 0; ii < paths.Count; ii++) { results[ii] = ResolveBrowsePath(fixture, paths[ii]); } return new TranslateBrowsePathsToNodeIdsResponse { ResponseHeader = new ResponseHeader(), Results = ArrayOf.Wrapped(results), DiagnosticInfos = default }; }); mock.Setup(session => session.ReadAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync(( RequestHeader? _, double _, TimestampsToReturn _, ArrayOf nodesToRead, CancellationToken _) => { var results = new DataValue[nodesToRead.Count]; for (int ii = 0; ii < nodesToRead.Count; ii++) { results[ii] = ReadValue(fixture, nodesToRead[ii]); } return new ReadResponse { ResponseHeader = new ResponseHeader(), Results = ArrayOf.Wrapped(results), DiagnosticInfos = default }; }); mock.Setup(session => session.BrowseAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync(( RequestHeader? _, ViewDescription? _, uint _, ArrayOf nodesToBrowse, CancellationToken _) => { var results = new BrowseResult[nodesToBrowse.Count]; for (int ii = 0; ii < nodesToBrowse.Count; ii++) { results[ii] = Browse(fixture, nodesToBrowse[ii]); } return new BrowseResponse { ResponseHeader = new ResponseHeader(), Results = ArrayOf.Wrapped(results), DiagnosticInfos = default }; }); mock.Setup(session => session.CallAsync( It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync(( RequestHeader? _, ArrayOf calls, CancellationToken _) => { var results = new CallMethodResult[calls.Count]; for (int ii = 0; ii < calls.Count; ii++) { results[ii] = InvokeCall(fixture, calls[ii]); } return new CallResponse { ResponseHeader = new ResponseHeader(), Results = ArrayOf.Wrapped(results), DiagnosticInfos = default }; }); return mock; } private static BrowsePathResult ResolveBrowsePath( MachineryServerFixture fixture, BrowsePath path) { NodeState? current = fixture.Manager.FindPredefinedNode(path.StartingNode); if (current is null) { return new BrowsePathResult { StatusCode = StatusCodes.BadNodeIdUnknown, Targets = [] }; } foreach (RelativePathElement element in path.RelativePath.Elements) { NodeState? next = current.FindChild( fixture.Manager.SystemContext, element.TargetName); if (next is null) { return new BrowsePathResult { StatusCode = StatusCodes.BadNoMatch, Targets = [] }; } current = next; } return new BrowsePathResult { StatusCode = StatusCodes.Good, Targets = ArrayOf.Wrapped( [ new BrowsePathTarget { TargetId = (ExpandedNodeId)current.NodeId, RemainingPathIndex = uint.MaxValue } ]) }; } private static DataValue ReadValue( MachineryServerFixture fixture, ReadValueId nodeToRead) { NodeState? node = fixture.Manager.FindPredefinedNode(nodeToRead.NodeId); return node is BaseVariableState variable ? new DataValue(variable.WrappedValue, StatusCodes.Good) : DataValue.FromStatusCode(StatusCodes.BadNodeIdUnknown); } private static BrowseResult Browse( MachineryServerFixture fixture, BrowseDescription description) { NodeState? node = fixture.Manager.FindPredefinedNode(description.NodeId); if (node is null) { return new BrowseResult { StatusCode = StatusCodes.BadNodeIdUnknown, References = [] }; } var children = new List(); node.GetChildren(fixture.Manager.SystemContext, children); var references = new List(); foreach (BaseInstanceState child in children) { if (description.NodeClassMask != 0 && (description.NodeClassMask & (uint)child.NodeClass) == 0) { continue; } references.Add(new ReferenceDescription { NodeId = (ExpandedNodeId)child.NodeId, BrowseName = child.BrowseName, DisplayName = child.DisplayName, NodeClass = child.NodeClass, ReferenceTypeId = child.ReferenceTypeId, IsForward = true, TypeDefinition = child is BaseObjectState objectState ? (ExpandedNodeId)objectState.TypeDefinitionId : ExpandedNodeId.Null }); } return new BrowseResult { StatusCode = StatusCodes.Good, References = ArrayOf.Wrapped(references.ToArray()) }; } private static CallMethodResult InvokeCall( MachineryServerFixture fixture, CallMethodRequest call) { NodeState? node = fixture.Manager.FindPredefinedNode(call.MethodId); if (node is not MethodState method) { return new CallMethodResult { StatusCode = StatusCodes.BadMethodInvalid, InputArgumentResults = [], InputArgumentDiagnosticInfos = default, OutputArguments = [] }; } var outputs = new List(); var argumentErrors = new List(); ServiceResult status = method .CallAsync( fixture.Manager.SystemContext, call.ObjectId, call.InputArguments, argumentErrors, outputs) .AsTask() .GetAwaiter() .GetResult(); var inputResults = new StatusCode[argumentErrors.Count]; for (int ii = 0; ii < argumentErrors.Count; ii++) { inputResults[ii] = argumentErrors[ii].StatusCode; } return new CallMethodResult { StatusCode = status.StatusCode, InputArgumentResults = ArrayOf.Wrapped(inputResults), InputArgumentDiagnosticInfos = default, OutputArguments = ArrayOf.Wrapped(outputs.ToArray()) }; } } }