/* ======================================================================== * 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.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Time.Testing; using Moq; using Opc.Ua.Tests; using StackManagedSession = global::Opc.Ua.Client.ManagedSession; namespace Opc.Ua.Client.Tests.SessionContexts { /// /// Exercises real managed dispatch with a deterministic, caller-scripted transport. /// internal sealed class SessionRequestContextTestContext : IAsyncDisposable { /// /// Creates a connected managed session without opening a socket or starting recovery. /// internal SessionRequestContextTestContext( ITelemetryContext? telemetry = null, MessageSecurityMode securityMode = MessageSecurityMode.None) { Telemetry = telemetry ?? NUnitTelemetryContext.Create(); TimeProvider = new FakeTimeProvider(); MessageContext = ServiceMessageContext.Create(Telemetry); MessageContext.NamespaceUris.Append("urn:request-context:original"); var endpointConfiguration = EndpointConfiguration.Create(); var endpoint = new ConfiguredEndpoint(null, new EndpointDescription { EndpointUrl = "opc.tcp://localhost:4840", SecurityMode = securityMode, SecurityPolicyUri = securityMode == MessageSecurityMode.None ? SecurityPolicies.None : SecurityPolicies.Basic256Sha256, Server = new ApplicationDescription { ApplicationUri = "urn:request-context:server" } }, endpointConfiguration); var configuration = new ApplicationConfiguration(Telemetry) { ClientConfiguration = new ClientConfiguration(), SecurityConfiguration = new SecurityConfiguration() }; Channel = new Mock(); Channel.SetupGet(channel => channel.MessageContext).Returns(MessageContext); Channel.SetupGet(channel => channel.EndpointDescription).Returns(endpoint.Description); Channel.SetupGet(channel => channel.EndpointConfiguration).Returns(endpointConfiguration); Channel.SetupGet(channel => channel.OperationTimeout).Returns(10_000); Channel.Setup(channel => channel.SendRequestAsync( It.IsAny(), It.IsAny())) .Returns(DispatchAsync); InnerSession = new Session( Channel.Object, configuration, endpoint, engineFactory: DefaultSubscriptionEngineFactory.Instance, timeProvider: TimeProvider); InnerSession.SessionCreated(NodeId.Parse("s=original-session"), NodeId.Parse("s=original-token")); var factory = new Mock(); factory.SetupGet(value => value.Telemetry).Returns(Telemetry); ConstructorInfo constructor = typeof(StackManagedSession) .GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance) .Single(); ManagedSession = (StackManagedSession)constructor.Invoke( [ configuration, endpoint, factory.Object, new ReconnectPolicy(), null, Telemetry.CreateLogger(), null, null, TimeProvider, default(ArrayOf), "RequestContextTests", 60_000u, false, false, false, false, null, null, null, null ]); SetField(ManagedSession, typeof(StackManagedSession), "m_session", InnerSession); SetField(ManagedSession.StateMachine, typeof(ConnectionStateMachine), "m_servicesAvailable", true); SetField(ManagedSession.StateMachine, typeof(ConnectionStateMachine), "m_state", ConnectionState.Connected); foreach (string name in new[] { "m_servicesReady", "m_settled" }) { var ready = (AsyncManualResetEvent)typeof(ConnectionStateMachine) .GetField(name, BindingFlags.NonPublic | BindingFlags.Instance)! .GetValue(ManagedSession.StateMachine)!; ready.Set(); } ServiceGate = (AsyncReaderWriterLock)typeof(StackManagedSession) .GetField("m_serviceLock", BindingFlags.NonPublic | BindingFlags.Instance)! .GetValue(ManagedSession)!; } /// /// Gets the telemetry used by the real sessions. /// internal ITelemetryContext Telemetry { get; } /// /// Gets controllable time for the recovery drain. /// internal FakeTimeProvider TimeProvider { get; } /// /// Gets the original wire message context. /// internal IServiceMessageContext MessageContext { get; } /// /// Gets the transport whose lifetime remains owned by the managed session. /// internal Mock Channel { get; } /// /// Gets the real session that can be recreated in place. /// internal Session InnerSession { get; } /// /// Gets the managed owner passed to the public capture API. /// internal StackManagedSession ManagedSession { get; } /// /// Gets the existing private gate solely for deterministic race tests. /// internal AsyncReaderWriterLock ServiceGate { get; } /// /// Gets all requests actually delivered to the transport. /// internal List Requests { get; } = []; /// /// Gets or sets the response script. /// internal Func> Handler { get; set; } = static (_, _) => new ValueTask(new CloseSessionResponse()); /// /// Replaces both server identifiers without changing the inner Session reference. /// internal void Recreate() { InnerSession.SessionCreated(NodeId.Parse("s=replacement-session"), NodeId.Parse("s=replacement-token")); } /// /// Replaces the physical transport without changing public identifiers, as an identity-collision regression. /// internal void ReplaceTransport(ITransportChannel channel) { SetField(InnerSession, typeof(ClientBase), "m_channel", channel); } /// public async ValueTask DisposeAsync() { InnerSession.AbandonServerSession(); await ManagedSession.DisposeAsync().ConfigureAwait(false); } /// /// Applies a test-only state change without adding a production testing API. /// private static void SetField(object target, Type type, string name, object value) { type.GetField(name, BindingFlags.NonPublic | BindingFlags.Instance)!.SetValue(target, value); } /// /// Records the request before running the scripted transport operation. /// /// private async ValueTask DispatchAsync(IServiceRequest request, CancellationToken ct) { Requests.Add(request); IServiceResponse response = await Handler(request, ct).ConfigureAwait(false); ResponseHeader header = response switch { CallResponse value => value.ResponseHeader ??= new ResponseHeader(), HistoryReadResponse value => value.ResponseHeader ??= new ResponseHeader(), ReadResponse value => value.ResponseHeader ??= new ResponseHeader(), TranslateBrowsePathsToNodeIdsResponse value => value.ResponseHeader ??= new ResponseHeader(), CloseSessionResponse value => value.ResponseHeader ??= new ResponseHeader(), _ => response.ResponseHeader ?? throw new InvalidOperationException("The scripted response has no header.") }; header.RequestHandle = request.RequestHeader.RequestHandle; return response; } } }