Files
UA-.NETStandard/tests/Opc.Ua.Client.Tests/Session/ManagedSessionTests.cs
romanett 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# Description

Nullable reference types are now enabled once, in `common.props`, and
every project in the repository builds warning-clean with them.

**Configuration**
- `<Nullable>enable</Nullable>` lives in `common.props`. All per-project
`<Nullable>` settings are removed: 178 project files (enable,
annotations, and the `disable` in the three Aot test projects).
- 419 per-file `#nullable enable`/`restore` directives and the 4
`#nullable disable` directives are removed.
- Kept on purpose:
- The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type
Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`,
`PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the
project setting in generated code, so the directive is required there.
- `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against
the 1.5.378 packages and does not import the repository props.
- `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now
describe the central setting.

**Migration** (the 43 unset and 13 annotations-only projects, roughly
10k diagnostics)
- **Production code: annotated at the declarations.** Covers
`tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the
LDS sample.
- Members, parameters and returns are nullable where null actually
flows.
- Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`,
`IsMethodTypeDesign`).
- `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take
`params object?[]`. `Template.AddReplacement` takes `string?` (rendering
already skips null replacements).
- `IRegisteredServerStore.RegisterAsync` returns
`Task<RegistrationEntry?>`: it returns null when a registration is
removed.
- Dereferences backed by an invariant (for example, the validated model)
use `!`, so runtime behaviour is unchanged.
- **Behavioural changes in production code** (small, deliberate):
- `NodeSetToModelDesign` throws `InvalidOperationException` when the
file is not a NodeSet. Before, it threw a NullReferenceException on the
next line.
- `ModelDesignValidator` starts its lookup tables empty instead of null.
A redundant `if (m_nodes == null) return;` guard is gone.
- **Test code: minimal annotations.**
  - `?` on values that can be null, `!` where the test asserts non-null.
  - `= null!` for fixture members assigned in SetUp.
- `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the
`IEncodeable` constraint.
- A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`)
folded into the adjacent `IsNull`/`Count` checks.

**Notes for reviewers**
- The diff is large (~1,500 files) but almost entirely mechanical: `?`,
`!` and directive removal. The non-annotation changes are listed above.
- Known follow-up, not done here:
`IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay
non-nullable even though the validator returns null before it loads a
dictionary. Making them nullable would ripple through every generator
call site.

## Related Issues

- Fixes #4439

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

The "no new warnings" box covers net10.0 and net48: full non-incremental
builds of `UA.slnx`, `tools/SourceGeneration.slnx`,
`tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0
warnings and 0 errors under the repository's warnings-as-errors
settings. net9.0 and net8.0 also had no nullable diagnostics, but they
were not rebuilt after the final fixes. Tests have not been run locally
yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:48:13 +02:00

1757 lines
79 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 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/
* ======================================================================*/
// CA2000: test code; many disposables are ownership-transferred to test fixtures or short-lived,
// making CA2000 noisy without a real leak risk. Disabled file-level for the suite.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Time.Testing;
using Moq;
using NUnit.Framework;
using Opc.Ua.Client.TestFramework;
using Opc.Ua.Identity;
using Opc.Ua.Tests;
#pragma warning disable CA2000
namespace Opc.Ua.Client.Tests.ManagedSession
{
/// <summary>
/// Unit tests for ManagedSession components including
/// <see cref="ReconnectPolicy"/>, <see cref="ConnectionState"/>,
/// <see cref="ConnectionStateMachine"/>, and
/// <see cref="Client.ManagedSession"/> factory methods.
/// </summary>
[TestFixture]
public sealed class ManagedSessionTests
{
[Test]
public async Task DisposeAsyncRetainsRevalidationWorkerUntilItCompletesAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
using var inner = SessionMock.Create();
await using Client.ManagedSession managed = CreateManagedSessionWithInner(
CreateClientConfiguration(telemetry), CreateEndpoint(), inner, telemetry);
using var cancellation = new CancellationTokenSource();
var pending = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
typeof(Client.ManagedSession).GetField(
"m_revalidationCancellation", BindingFlags.Instance | BindingFlags.NonPublic)!
.SetValue(managed, cancellation);
typeof(Client.ManagedSession).GetField(
"m_revalidationTask", BindingFlags.Instance | BindingFlags.NonPublic)!
.SetValue(managed, pending.Task);
Task disposal = managed.DisposeAsync().AsTask();
try
{
Assert.That(cancellation.IsCancellationRequested, Is.True);
Assert.That(disposal.IsCompleted, Is.False);
}
finally
{
pending.TrySetResult(true);
await disposal.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
}
Assert.That(() => cancellation.Token, Throws.TypeOf<ObjectDisposedException>());
}
[Test]
public void ExponentialBackoffIncreasesDelay()
{
var policy = new ReconnectPolicy
{
Strategy = BackoffStrategy.Exponential,
InitialDelay = TimeSpan.FromSeconds(1),
MaxDelay = TimeSpan.FromMinutes(10),
JitterFactor = 0.0
};
TimeSpan? delay0 = policy.GetNextDelay(0);
TimeSpan? delay1 = policy.GetNextDelay(1);
TimeSpan? delay2 = policy.GetNextDelay(2);
Assert.That(delay0, Is.Not.Null);
Assert.That(delay1, Is.Not.Null);
Assert.That(delay2, Is.Not.Null);
// Exponential: 1s * 2^0 = 1s, 1s * 2^1 = 2s, 1s * 2^2 = 4s
Assert.That(delay0.Value.TotalSeconds, Is.EqualTo(1.0));
Assert.That(delay1.Value.TotalSeconds, Is.EqualTo(2.0));
Assert.That(delay2.Value.TotalSeconds, Is.EqualTo(4.0));
}
[Test]
public void LinearBackoffIncreasesLinearly()
{
var policy = new ReconnectPolicy
{
Strategy = BackoffStrategy.Linear,
InitialDelay = TimeSpan.FromSeconds(2),
MaxDelay = TimeSpan.FromMinutes(10),
JitterFactor = 0.0
};
TimeSpan? delay0 = policy.GetNextDelay(0);
TimeSpan? delay1 = policy.GetNextDelay(1);
TimeSpan? delay2 = policy.GetNextDelay(2);
Assert.That(delay0, Is.Not.Null);
Assert.That(delay1, Is.Not.Null);
Assert.That(delay2, Is.Not.Null);
// Linear: 2s * (0+1) = 2s, 2s * (1+1) = 4s, 2s * (2+1) = 6s
Assert.That(delay0.Value.TotalSeconds, Is.EqualTo(2.0));
Assert.That(delay1.Value.TotalSeconds, Is.EqualTo(4.0));
Assert.That(delay2.Value.TotalSeconds, Is.EqualTo(6.0));
}
[Test]
public void ConstantBackoffReturnsSameDelay()
{
var policy = new ReconnectPolicy
{
Strategy = BackoffStrategy.Constant,
InitialDelay = TimeSpan.FromSeconds(5),
JitterFactor = 0.0
};
TimeSpan? delay0 = policy.GetNextDelay(0);
TimeSpan? delay1 = policy.GetNextDelay(1);
TimeSpan? delay2 = policy.GetNextDelay(5);
Assert.That(delay0, Is.Not.Null);
Assert.That(delay1, Is.Not.Null);
Assert.That(delay2, Is.Not.Null);
Assert.That(delay0.Value.TotalSeconds, Is.EqualTo(5.0));
Assert.That(delay1.Value.TotalSeconds, Is.EqualTo(5.0));
Assert.That(delay2.Value.TotalSeconds, Is.EqualTo(5.0));
}
[Test]
public void MaxDelayIsCapped()
{
var policy = new ReconnectPolicy
{
Strategy = BackoffStrategy.Exponential,
InitialDelay = TimeSpan.FromSeconds(1),
MaxDelay = TimeSpan.FromSeconds(10),
JitterFactor = 0.0
};
// Attempt 10: 1s * 2^10 = 1024s, should be capped to 10s
TimeSpan? delay = policy.GetNextDelay(10);
Assert.That(delay, Is.Not.Null);
Assert.That(
delay.Value.TotalSeconds,
Is.EqualTo(10.0));
}
[Test]
public void MaxRetriesStopsAfterLimit()
{
var policy = new ReconnectPolicy
{
Strategy = BackoffStrategy.Constant,
InitialDelay = TimeSpan.FromSeconds(1),
MaxRetries = 3,
JitterFactor = 0.0
};
Assert.That(policy.GetNextDelay(0), Is.Not.Null);
Assert.That(policy.GetNextDelay(1), Is.Not.Null);
Assert.That(policy.GetNextDelay(2), Is.Not.Null);
// attempt >= MaxRetries => null
Assert.That(policy.GetNextDelay(3), Is.Null);
Assert.That(policy.GetNextDelay(4), Is.Null);
}
[Test]
public void UnlimitedRetriesNeverReturnsNull()
{
var policy = new ReconnectPolicy
{
Strategy = BackoffStrategy.Constant,
InitialDelay = TimeSpan.FromSeconds(1),
MaxRetries = 0,
JitterFactor = 0.0
};
for (int i = 0; i < 100; i++)
{
Assert.That(
policy.GetNextDelay(i),
Is.Not.Null,
$"GetNextDelay returned null at attempt {i}");
}
}
[Test]
public void JitterAppliesVariation()
{
var policy = new ReconnectPolicy
{
Strategy = BackoffStrategy.Constant,
InitialDelay = TimeSpan.FromSeconds(10),
MaxDelay = TimeSpan.FromSeconds(30),
JitterFactor = 0.5
};
var delays = new HashSet<double>();
for (int i = 0; i < 50; i++)
{
TimeSpan? delay = policy.GetNextDelay(0);
Assert.That(delay, Is.Not.Null);
delays.Add(delay.Value.TotalMilliseconds);
}
// With 50% jitter over 50 iterations we expect variation
Assert.That(delays, Has.Count.GreaterThan(1),
"Jitter should produce varying delays");
}
[Test]
public async Task ManagedSessionPropagatesBudgetOrUsesLegacyReconnectContractAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint endpoint = CreateEndpoint();
IServiceMessageContext messageContext = configuration.CreateMessageContext();
var managedChannel = new Mock<IManagedTransportChannel>();
managedChannel.SetupGet(c => c.MessageContext).Returns(messageContext);
var channelManager = new Mock<IClientChannelManager>();
#if NET8_0_OR_GREATER
IRetryBudget? capturedBudget = null;
channelManager.Setup(m => m.ReconnectAsync(
managedChannel.Object,
It.IsAny<IRetryBudget>(),
It.IsAny<CancellationToken>()))
.Callback<IManagedTransportChannel, IRetryBudget, CancellationToken>(
(_, budget, _) => capturedBudget = budget)
.Returns(new ValueTask());
#else
channelManager.Setup(m => m.ReconnectAsync(
managedChannel.Object, It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
#endif
using var innerSession = new Session(
managedChannel.Object,
configuration,
endpoint,
engineFactory: DefaultSubscriptionEngineFactory.Instance);
innerSession.BindManagedChannel(channelManager.Object, managedChannel.Object);
using Client.ManagedSession managedSession = CreateManagedSessionWithInner(
configuration,
endpoint,
innerSession,
telemetry);
var budget = new RetryBudget(TimeSpan.FromSeconds(30));
ServiceResult result = await InvokeHandleReconnectAsync(
managedSession,
budget)
.ConfigureAwait(false);
Assert.That(ServiceResult.IsGood(result), Is.True);
#if NET8_0_OR_GREATER
Assert.That(capturedBudget, Is.SameAs(budget));
#else
channelManager.Verify(m => m.ReconnectAsync(
managedChannel.Object, It.IsAny<CancellationToken>()), Times.Once);
#endif
}
[Test]
public async Task HandleReconnectAsyncUsesAlternateNetworkEndpointAfterBadSecureChannelClosedAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint primaryEndpoint = CreateEndpoint();
ConfiguredEndpoint alternateEndpoint = CreateEndpoint();
alternateEndpoint.Description.EndpointUrl = "opc.tcp://alternate:4840";
IServiceMessageContext messageContext = configuration.CreateMessageContext();
var managedChannel = new Mock<IManagedTransportChannel>();
managedChannel.SetupGet(c => c.MessageContext).Returns(messageContext);
managedChannel.SetupGet(c => c.State).Returns(ChannelState.Closed);
ConfiguredEndpoint? capturedEndpoint = null;
var channelManager = new Mock<IClientChannelManager>();
#if NET8_0_OR_GREATER
channelManager.Setup(m => m.ReconnectAsync(
managedChannel.Object,
It.IsAny<IRetryBudget>(),
It.IsAny<CancellationToken>()))
#else
channelManager.Setup(m => m.ReconnectAsync(
managedChannel.Object, It.IsAny<CancellationToken>()))
#endif
.Throws(new ServiceResultException(StatusCodes.BadSecureChannelClosed));
channelManager.Setup(m => m.GetAsync(
It.IsAny<ConfiguredEndpoint>(),
It.IsAny<Func<IManagedTransportChannel, IReconnectParticipant>>(),
It.IsAny<ITransportWaitingConnection>(),
It.IsAny<CancellationToken>()))
.Callback<ConfiguredEndpoint, Func<IManagedTransportChannel, IReconnectParticipant>,
ITransportWaitingConnection?, CancellationToken>(
(endpoint, _, _, _) => capturedEndpoint = endpoint)
.Throws(new ServiceResultException(StatusCodes.BadUnexpectedError));
using var innerSession = new Session(
managedChannel.Object,
configuration,
primaryEndpoint,
engineFactory: DefaultSubscriptionEngineFactory.Instance);
innerSession.BindManagedChannel(channelManager.Object, managedChannel.Object);
using Client.ManagedSession managedSession = CreateManagedSessionWithInner(
configuration,
primaryEndpoint,
innerSession,
telemetry,
new NetworkRedundancyOptions
{
AlternateEndpoints = new ArrayOf<ConfiguredEndpoint>(new[] { alternateEndpoint }.AsMemory())
});
var budget = new RetryBudget(TimeSpan.FromSeconds(30));
ServiceResult result = await InvokeHandleReconnectAsync(
managedSession,
budget)
.ConfigureAwait(false);
Assert.That(ServiceResult.IsBad(result), Is.True);
Assert.That(capturedEndpoint, Is.SameAs(alternateEndpoint));
}
[Test]
public async Task HandleFailoverAsyncUsesInnerSessionCurrentEndpointForTargetSelectionAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint initialEndpoint = CreateEndpoint();
initialEndpoint.Description.EndpointUrl = "opc.tcp://initial:4840";
ConfiguredEndpoint activeEndpoint = CreateEndpoint();
activeEndpoint.Description.EndpointUrl = "opc.tcp://active:4840";
IServiceMessageContext messageContext = configuration.CreateMessageContext();
var managedChannel = new Mock<IManagedTransportChannel>();
managedChannel.SetupGet(c => c.MessageContext).Returns(messageContext);
ConfiguredEndpoint? capturedCurrentEndpoint = null;
var redundancyHandler = new Mock<IServerRedundancyHandler>(MockBehavior.Strict);
redundancyHandler.Setup(h => h.SelectFailoverTarget(
It.IsAny<ServerRedundancyInfo>(),
It.IsAny<ConfiguredEndpoint>()))
.Callback<ServerRedundancyInfo, ConfiguredEndpoint>(
(_, currentEndpoint) => capturedCurrentEndpoint = currentEndpoint)
.Returns((ConfiguredEndpoint?)null);
using var innerSession = new Session(
managedChannel.Object,
configuration,
activeEndpoint,
engineFactory: DefaultSubscriptionEngineFactory.Instance);
using Client.ManagedSession managedSession = CreateManagedSessionWithInner(
configuration,
initialEndpoint,
innerSession,
telemetry,
redundancyHandler: redundancyHandler.Object);
typeof(Client.ManagedSession)
.GetField(
"m_redundancyInfo",
BindingFlags.NonPublic | BindingFlags.Instance)!
.SetValue(managedSession, new ServerRedundancyInfo());
ServiceResult result = await InvokeHandleFailoverAsync(
managedSession,
new RetryBudget(TimeSpan.FromSeconds(30)))
.ConfigureAwait(false);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.BadNothingToDo));
Assert.That(capturedCurrentEndpoint, Is.SameAs(activeEndpoint));
redundancyHandler.Verify(h => h.SelectFailoverTarget(
It.IsAny<ServerRedundancyInfo>(),
It.IsAny<ConfiguredEndpoint>()),
Times.Once);
}
[TestCase(false)]
[TestCase(true)]
public async Task ReconnectKeepsCachedFailoverAfterBoundedRedundancyRefreshAsync(bool ignoreCancellation)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint endpoint = CreateEndpoint();
TimeProvider time = TimeProvider.System;
var channel = new Mock<IManagedTransportChannel>();
channel.SetupGet(value => value.MessageContext).Returns(configuration.CreateMessageContext());
var manager = new Mock<IClientChannelManager>();
#if NET8_0_OR_GREATER
manager.Setup(value => value.ReconnectAsync(
channel.Object, It.IsAny<IRetryBudget>(), It.IsAny<CancellationToken>()))
#else
manager.Setup(value => value.ReconnectAsync(channel.Object, It.IsAny<CancellationToken>()))
#endif
.Returns(new ValueTask());
using var inner = new Session(channel.Object, configuration, endpoint);
inner.BindManagedChannel(manager.Object, channel.Object);
var cached = new ServerRedundancyInfo { Mode = RedundancySupport.Cold };
var refresh = new TaskCompletionSource<ServerRedundancyInfo>(
TaskCreationOptions.RunContinuationsAsynchronously);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using CancellationTokenRegistration abort = deadline.Token.Register(() => refresh.TrySetCanceled());
var redundancy = new Mock<IServerRedundancyHandler>(MockBehavior.Strict);
redundancy.Setup(value => value.FetchRedundancyInfoAsync(
It.IsAny<ISession>(), It.IsAny<CancellationToken>()))
.Returns(() =>
{
if (!ignoreCancellation)
{
throw new ServiceResultException(StatusCodes.BadConnectionClosed);
}
return new ValueTask<ServerRedundancyInfo>(refresh.Task);
});
redundancy.Setup(value => value.SelectFailoverTarget(cached, endpoint))
.Returns((ConfiguredEndpoint?)null);
Client.ManagedSession managed = CreateManagedSessionWithInner(
configuration, endpoint, inner, telemetry, redundancyHandler: redundancy.Object, timeProvider: time);
typeof(Client.ManagedSession).GetField(
"m_redundancyInfo", BindingFlags.NonPublic | BindingFlags.Instance)!.SetValue(managed, cached);
var budget = new RetryBudget(TimeSpan.FromSeconds(30), time);
try
{
ServiceResult result = await InvokeHandleReconnectAsync(managed, budget).ConfigureAwait(false);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good));
redundancy.Setup(value => value.FetchRedundancyInfoAsync(
It.IsAny<ISession>(), It.IsAny<CancellationToken>()))
.Throws(new ServiceResultException(StatusCodes.BadConnectionClosed));
ServiceResult failover = await InvokeHandleFailoverAsync(managed, budget)
.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
Assert.That(failover.StatusCode, Is.EqualTo(StatusCodes.BadNothingToDo));
redundancy.Verify(value => value.SelectFailoverTarget(cached, endpoint), Times.Once);
}
finally
{
refresh.TrySetResult(cached);
await managed.DisposeAsync().ConfigureAwait(false);
}
}
[TestCase(false)]
[TestCase(true)]
public async Task FailoverResolvesCachedPeersAndInvalidatesFailedPeerAsync(bool initiallyResolved)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint primary = CreateEndpoint();
primary.UpdateBeforeConnect = false;
primary.Description.Server.ApplicationUri = "urn:primary";
ConfiguredEndpoint oldPeer = CreateEndpoint();
oldPeer.Description.Server.ApplicationUri = "urn:backup";
oldPeer.Description.EndpointUrl = "opc.tcp://backup:4840";
ConfiguredEndpoint refreshedPeer = CreateEndpoint();
refreshedPeer.Description.Server.ApplicationUri = "urn:backup";
refreshedPeer.Description.EndpointUrl = "opc.tcp://backup:4841";
var resolver = new Mock<IRedundantServerEndpointResolver>();
int resolutions = 0;
resolver.Setup(value => value.ResolveAsync(
"urn:backup", It.IsAny<ConfiguredEndpoint>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(() =>
{
resolutions++;
return resolutions == 1 ? initiallyResolved ? oldPeer : null : refreshedPeer;
});
var redundancy = new DefaultServerRedundancyHandler(resolver.Object);
var response = new ReadResponse
{
Results =
[
new DataValue((int)RedundancySupport.Hot),
new DataValue((byte)0),
DataValue.FromStatusCode(StatusCodes.BadNodeIdUnknown),
new DataValue(ArrayOf.Create(["urn:backup"])),
new DataValue(new Variant(DateTime.MinValue))
]
};
var channel = new Mock<ITransportChannel>();
channel.SetupGet(value => value.MessageContext).Returns(configuration.CreateMessageContext());
channel.SetupSequence(value => value.SendRequestAsync(
It.IsAny<ReadRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(response)
.Throws(new ServiceResultException(StatusCodes.BadConnectionClosed));
using var inner = new SessionMock(channel, configuration, primary);
inner.SetConnected();
var lease = new Mock<IManagedTransportChannel>();
lease.SetupGet(value => value.MessageContext).Returns(inner.MessageContext);
var manager = new Mock<IClientChannelManager>();
ConfiguredEndpoint? attemptedPeer = null;
manager.Setup(value => value.GetAsync(
It.IsAny<ConfiguredEndpoint>(),
It.IsAny<Func<IManagedTransportChannel, IReconnectParticipant>>(),
It.IsAny<ITransportWaitingConnection>(), It.IsAny<CancellationToken>()))
.Callback<ConfiguredEndpoint, Func<IManagedTransportChannel, IReconnectParticipant>,
ITransportWaitingConnection?, CancellationToken>((endpoint, _, _, _) => attemptedPeer = endpoint)
.Throws(new ServiceResultException(StatusCodes.BadServerHalted));
inner.BindManagedChannel(manager.Object, lease.Object);
var factory = new Mock<ISessionFactory>();
factory.SetupGet(value => value.Telemetry).Returns(telemetry);
factory.SetupGet(value => value.SubscriptionEngineFactory)
.Returns(DefaultSubscriptionEngineFactory.Instance);
factory.Setup(value => value.CreateAsync(
configuration, primary, false, false, It.IsAny<string>(), It.IsAny<uint>(),
It.IsAny<IUserIdentity>(), It.IsAny<ArrayOf<string>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(inner);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
await using Client.ManagedSession managed = await Client.ManagedSession.CreateAsync(
configuration, primary, factory.Object, redundancyHandler: redundancy, ct: timeout.Token)
.ConfigureAwait(false);
Assert.That(managed.RedundancySupport, Is.EqualTo(RedundancySupport.Hot));
ServiceResult result = await managed.StateMachine.FailoverWithBudgetAsync!(
new RetryBudget(TimeSpan.FromSeconds(30)), timeout.Token).ConfigureAwait(false);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.BadServerHalted));
Assert.That(attemptedPeer, Is.SameAs(initiallyResolved ? oldPeer : refreshedPeer));
var source = new Mock<ISession>();
source.SetupGet(value => value.ConfiguredEndpoint).Returns(primary);
source.Setup(value => value.ReadAsync(
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(response);
ServerRedundancyInfo next = await redundancy.FetchRedundancyInfoAsync(source.Object, timeout.Token)
.ConfigureAwait(false);
Assert.That(next.RedundantServers[0].Endpoint, Is.SameAs(refreshedPeer));
Assert.That(resolutions, Is.EqualTo(initiallyResolved ? 2 : 3));
}
[TestCase(RedundancySupport.Cold, false)]
[TestCase(RedundancySupport.Cold, true)]
[TestCase(RedundancySupport.Warm, false)]
[TestCase(RedundancySupport.Warm, true)]
public async Task ConnectRetainsBasicRedundancyBeforeUnavailablePeerDiscoveryAsync(
RedundancySupport mode,
bool waitForPeerTimeout)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint primary = CreateEndpoint();
primary.UpdateBeforeConnect = false;
primary.Description.Server.ApplicationUri = "urn:primary";
ConfiguredEndpoint backup = CreateEndpoint();
backup.Description.Server.ApplicationUri = "urn:backup";
backup.Description.EndpointUrl = "opc.tcp://backup:4840";
var time = new FakeTimeProvider();
var peerEntered = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
var backupEntered = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
var peerRetried = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
var blocked = new TaskCompletionSource<ConfiguredEndpoint?>(
TaskCreationOptions.RunContinuationsAsynchronously);
int offlineAttempts = 0;
bool backupAvailable = false;
var resolver = new Mock<IRedundantServerEndpointResolver>(MockBehavior.Strict);
resolver.Setup(value => value.ResolveAsync(
"urn:offline", It.IsAny<ConfiguredEndpoint>(), It.IsAny<CancellationToken>()))
.Returns(async (string _, ConfiguredEndpoint _, CancellationToken ct) =>
{
if (Interlocked.Increment(ref offlineAttempts) == 1)
{
peerEntered.TrySetResult(true);
}
else
{
peerRetried.TrySetResult(true);
}
return await blocked.Task.WaitAsync(ct).ConfigureAwait(false);
});
resolver.Setup(value => value.ResolveAsync(
"urn:backup", It.IsAny<ConfiguredEndpoint>(), It.IsAny<CancellationToken>()))
.Callback(() => backupEntered.TrySetResult(true))
.ReturnsAsync(() => backupAvailable ? backup : null);
var redundancy = new DefaultServerRedundancyHandler(resolver.Object, time);
var response = new ReadResponse
{
Results =
[
new DataValue((int)mode),
new DataValue(ServiceLevels.Maximum),
DataValue.FromStatusCode(StatusCodes.BadNodeIdUnknown),
new DataValue(ArrayOf.Create(["urn:offline", "urn:backup"])),
new DataValue(new Variant(DateTime.MinValue))
]
};
var channel = new Mock<ITransportChannel>();
channel.SetupGet(value => value.MessageContext).Returns(configuration.CreateMessageContext());
channel.SetupSequence(value => value.SendRequestAsync(
It.IsAny<ReadRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(response)
.Throws(new ServiceResultException(StatusCodes.BadConnectionClosed));
using var inner = new SessionMock(channel, configuration, primary);
inner.SetConnected();
var lease = new Mock<IManagedTransportChannel>();
lease.SetupGet(value => value.MessageContext).Returns(inner.MessageContext);
var manager = new Mock<IClientChannelManager>();
ConfiguredEndpoint? attemptedPeer = null;
manager.Setup(value => value.GetAsync(
It.IsAny<ConfiguredEndpoint>(), It.IsAny<Func<IManagedTransportChannel, IReconnectParticipant>>(),
It.IsAny<ITransportWaitingConnection>(), It.IsAny<CancellationToken>()))
.Callback<ConfiguredEndpoint, Func<IManagedTransportChannel, IReconnectParticipant>,
ITransportWaitingConnection?, CancellationToken>((endpoint, _, _, _) => attemptedPeer = endpoint)
.Throws(new ServiceResultException(StatusCodes.BadServerHalted));
inner.BindManagedChannel(manager.Object, lease.Object);
var factory = new Mock<ISessionFactory>();
factory.SetupGet(value => value.Telemetry).Returns(telemetry);
factory.SetupGet(value => value.SubscriptionEngineFactory)
.Returns(DefaultSubscriptionEngineFactory.Instance);
factory.Setup(value => value.CreateAsync(
configuration, primary, false, false, It.IsAny<string>(), It.IsAny<uint>(),
It.IsAny<IUserIdentity>(), It.IsAny<ArrayOf<string>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(inner);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
Task<Client.ManagedSession> connecting = Client.ManagedSession.CreateAsync(
configuration, primary, factory.Object, redundancyHandler: redundancy, timeProvider: time,
ct: timeout.Token);
// Advancing before both parallel lookups enter can expire a peer before it is called.
await Task.WhenAll(peerEntered.Task, backupEntered.Task).WaitAsync(timeout.Token).ConfigureAwait(false);
if (waitForPeerTimeout)
{
time.Advance(TimeSpan.FromSeconds(2));
}
await using Client.ManagedSession managed = await connecting.WaitAsync(timeout.Token)
.ConfigureAwait(false);
Assert.That(managed.RedundancySupport, Is.EqualTo(mode));
var snapshot = (ServerRedundancyInfo?)typeof(Client.ManagedSession)
.GetField("m_redundancyInfo", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(managed);
Assert.That(snapshot, Is.Not.Null);
Assert.That(snapshot!.RedundantServers.Count, Is.EqualTo(2));
Assert.That(snapshot.RedundantServers[0].ServerUri, Is.EqualTo("urn:offline"));
Assert.That(snapshot.RedundantServers[1].ServerUri, Is.EqualTo("urn:backup"));
Assert.That(snapshot.ServiceLevel, Is.EqualTo(ServiceLevels.Maximum));
Assert.That(snapshot.RedundantServers[0].Endpoint, Is.Null);
var discovery = (Task<ServerRedundancyInfo>?)typeof(Client.ManagedSession)
.GetField("m_redundancyEndpointRefreshTask", BindingFlags.NonPublic | BindingFlags.Instance)!
.GetValue(managed);
Assert.That(discovery, Is.Not.Null);
if (!waitForPeerTimeout)
{
Assert.That(discovery!.IsCompleted, Is.False, "Peer discovery must not gate the connected state.");
time.Advance(TimeSpan.FromSeconds(2));
}
await discovery!.WaitAsync(timeout.Token).ConfigureAwait(false);
backupAvailable = true;
// The offline peer's retry returns no endpoint without timing out the healthy peer alongside it.
blocked.TrySetResult(null);
Task<ServiceResult> failover = managed.StateMachine.FailoverWithBudgetAsync!(
new RetryBudget(TimeSpan.FromSeconds(30), time), timeout.Token);
await peerRetried.Task.WaitAsync(timeout.Token).ConfigureAwait(false);
ServiceResult result = await failover.WaitAsync(timeout.Token).ConfigureAwait(false);
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.BadServerHalted));
Assert.That(attemptedPeer, Is.SameAs(backup));
Assert.That(managed.RedundancySupport, Is.EqualTo(mode));
resolver.Verify(value => value.ResolveAsync(
"urn:backup", It.IsAny<ConfiguredEndpoint>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
}
[Test]
public void ConnectionStateChangedEventArgsHasCorrectProperties()
{
var error = new ServiceResult(StatusCodes.BadCommunicationError);
var args = new ConnectionStateChangedEventArgs
{
PreviousState = ConnectionState.Connected,
NewState = ConnectionState.Reconnecting,
Error = error,
ReconnectAttempt = 3
};
Assert.That(args.PreviousState, Is.EqualTo(ConnectionState.Connected));
Assert.That(args.NewState, Is.EqualTo(ConnectionState.Reconnecting));
Assert.That(args.Error, Is.Not.Null);
Assert.That(args.Error.StatusCode, Is.EqualTo(StatusCodes.BadCommunicationError));
Assert.That(args.ReconnectAttempt, Is.EqualTo(3));
}
[Test]
public void InitialStateIsDisconnected()
{
var policy = new ReconnectPolicy();
ILogger logger = NUnitTelemetryContext.Create().CreateLogger("ManagedSession");
using var cts = new CancellationTokenSource();
var sm = new ConnectionStateMachine(policy, logger);
Assert.That(sm.State, Is.EqualTo(ConnectionState.Disconnected));
}
[Test]
public async Task DisposeTransitionsToClosedState()
{
var policy = new ReconnectPolicy();
ILogger logger = NUnitTelemetryContext.Create().CreateLogger("ManagedSession");
var sm = new ConnectionStateMachine(policy, logger);
sm.Start();
await sm.DisposeAsync().ConfigureAwait(false);
Assert.That(sm.State, Is.EqualTo(ConnectionState.Closed));
}
[Test]
public async Task CreateAsyncCancellationStopsStartedConnectAttemptAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint endpoint = CreateEndpoint();
var gate = new BlockingConnectGate();
using var cancellation = new CancellationTokenSource();
Task<Client.ManagedSession> creation = Client.ManagedSession.CreateAsync(
configuration,
endpoint,
new DefaultSessionFactory(telemetry),
connectGate: gate,
ct: cancellation.Token);
Task startedTask = await Task.WhenAny(
gate.Started,
Task.Delay(TimeSpan.FromSeconds(5)))
.ConfigureAwait(false);
Assert.That(startedTask, Is.SameAs(gate.Started));
cancellation.Cancel();
Task completedTask = await Task.WhenAny(
creation,
Task.Delay(TimeSpan.FromSeconds(5)))
.ConfigureAwait(false);
Assert.That(completedTask, Is.SameAs(creation));
OperationCanceledException exception =
Assert.CatchAsync<OperationCanceledException>(async () =>
await creation.ConfigureAwait(false))!;
Assert.That(exception.CancellationToken, Is.EqualTo(cancellation.Token));
Task stoppedTask = await Task.WhenAny(
gate.Stopped,
Task.Delay(TimeSpan.FromSeconds(5)))
.ConfigureAwait(false);
Assert.That(stoppedTask, Is.SameAs(gate.Stopped));
}
[Test]
public void CreateAsyncThrowsOnNullConfiguration()
{
var mockFactory = new Mock<ISessionFactory>();
mockFactory.Setup(f => f.Telemetry)
.Returns(new Mock<ITelemetryContext>().Object);
var endpoint = new ConfiguredEndpoint(
null!,
new EndpointDescription("opc.tcp://localhost:4840"));
Assert.ThrowsAsync<ArgumentNullException>(async () =>
await Client.ManagedSession.CreateAsync(
configuration: null!,
endpoint: endpoint,
sessionFactory: mockFactory.Object).ConfigureAwait(false));
}
[Test]
public void CreateAsyncThrowsOnNullEndpoint()
{
var mockFactory = new Mock<ISessionFactory>();
mockFactory.Setup(f => f.Telemetry)
.Returns(new Mock<ITelemetryContext>().Object);
var config = new ApplicationConfiguration
{
ApplicationName = "TestApp",
ApplicationType = ApplicationType.Client,
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier()
}
};
Assert.ThrowsAsync<ArgumentNullException>(async () =>
await Client.ManagedSession.CreateAsync(
configuration: config,
endpoint: null!,
sessionFactory: mockFactory.Object).ConfigureAwait(false));
}
[Test]
public void ConnectConsultsTheProviderWhenNoTokenPoliciesAndNoFallbackIdentityExist()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint endpoint = CreateEndpointWithoutTokenPolicies();
IUserIdentity? observed = null;
var sessionFactory = new Mock<ISessionFactory>();
sessionFactory.SetupGet(f => f.Telemetry).Returns(telemetry);
sessionFactory.Setup(f => f.WithSubscriptionEngine(
It.IsAny<ISubscriptionEngineFactory>(), It.IsAny<TimeProvider?>()))
.Returns(sessionFactory.Object);
sessionFactory.Setup(f => f.CreateAsync(
It.IsAny<ApplicationConfiguration>(), It.IsAny<ConfiguredEndpoint>(),
It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<string>(), It.IsAny<uint>(),
It.IsAny<IUserIdentity?>(), It.IsAny<ArrayOf<string>>(),
It.IsAny<CancellationToken>()))
.Callback<ApplicationConfiguration, ConfiguredEndpoint, bool, bool, string, uint,
IUserIdentity?, ArrayOf<string>, CancellationToken>(
(_, _, _, _, _, _, identity, _, _) => observed = identity)
.ThrowsAsync(new ServiceResultException(StatusCodes.BadCertificateUntrusted));
var provider = new Mock<IClientIdentityProvider>();
ServiceResultException exception = Assert.ThrowsAsync<ServiceResultException>(
async () => await Client.ManagedSession.CreateAsync(
configuration,
endpoint,
sessionFactory.Object,
reconnectPolicy: NoRetryPolicy(),
identityProvider: provider.Object).ConfigureAwait(false));
// Without a configured identity there is nothing to defer to, so the
// provider must be consulted. Deferring would hand the factory null,
// which the inner session silently turns into Anonymous and then
// rejects with BadIdentityTokenInvalid on a non-anonymous endpoint.
Assert.Multiple(() =>
{
Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenRejected));
Assert.That(observed, Is.Null);
});
sessionFactory.Verify(f => f.CreateAsync(
It.IsAny<ApplicationConfiguration>(), It.IsAny<ConfiguredEndpoint>(),
It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<string>(), It.IsAny<uint>(),
It.IsAny<IUserIdentity?>(), It.IsAny<ArrayOf<string>>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Test]
public void ConnectKeepsTheConfiguredIdentityWhenTheEndpointAdvertisesNoTokenPolicies()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint endpoint = CreateEndpointWithoutTokenPolicies();
var configured = new UserIdentity();
IUserIdentity? observed = null;
var sessionFactory = new Mock<ISessionFactory>();
sessionFactory.SetupGet(f => f.Telemetry).Returns(telemetry);
sessionFactory.Setup(f => f.WithSubscriptionEngine(
It.IsAny<ISubscriptionEngineFactory>(), It.IsAny<TimeProvider?>()))
.Returns(sessionFactory.Object);
sessionFactory.Setup(f => f.CreateAsync(
It.IsAny<ApplicationConfiguration>(), It.IsAny<ConfiguredEndpoint>(),
It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<string>(), It.IsAny<uint>(),
It.IsAny<IUserIdentity?>(), It.IsAny<ArrayOf<string>>(),
It.IsAny<CancellationToken>()))
.Callback<ApplicationConfiguration, ConfiguredEndpoint, bool, bool, string, uint,
IUserIdentity?, ArrayOf<string>, CancellationToken>(
(_, _, _, _, _, _, identity, _, _) => observed = identity)
.ThrowsAsync(new ServiceResultException(StatusCodes.BadCertificateUntrusted));
var provider = new Mock<IClientIdentityProvider>(MockBehavior.Strict);
ServiceResultException exception = Assert.ThrowsAsync<ServiceResultException>(
async () => await Client.ManagedSession.CreateAsync(
configuration,
endpoint,
sessionFactory.Object,
identity: configured,
reconnectPolicy: NoRetryPolicy(),
identityProvider: provider.Object).ConfigureAwait(false));
// A fallback identity exists, so selection is deferred until discovery
// populates the policies and the attempt fails at the transport layer
// for the reconnect policy to retry.
Assert.Multiple(() =>
{
Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadCertificateUntrusted));
Assert.That(observed, Is.SameAs(configured));
});
provider.VerifyNoOtherCalls();
}
private static ReconnectPolicy NoRetryPolicy()
{
return new ReconnectPolicy
{
Strategy = BackoffStrategy.Constant,
InitialDelay = TimeSpan.FromMilliseconds(1),
MaxRetries = 0,
JitterFactor = 0.0,
MaxTotalReconnectTime = TimeSpan.FromSeconds(5)
};
}
private static ConfiguredEndpoint CreateEndpointWithoutTokenPolicies()
{
var description = new EndpointDescription
{
EndpointUrl = "opc.tcp://localhost:4840",
SecurityMode = MessageSecurityMode.None,
SecurityPolicyUri = SecurityPolicies.None,
TransportProfileUri = Profiles.UaTcpTransport
};
description.Server.ApplicationUri = description.EndpointUrl;
description.Server.ApplicationType = ApplicationType.Server;
return new ConfiguredEndpoint(
null,
description,
new EndpointConfiguration { OperationTimeout = 6000 })
{
UpdateBeforeConnect = false
};
}
[Test]
public void CreateAsyncThrowsInitialConnectErrorInsteadOfReturningUnconnectedSession()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint endpoint = CreateEndpoint();
int connectAttempts = 0;
var sessionFactory = new Mock<ISessionFactory>();
sessionFactory.SetupGet(f => f.Telemetry).Returns(telemetry);
sessionFactory.Setup(f => f.WithSubscriptionEngine(
It.IsAny<ISubscriptionEngineFactory>(), It.IsAny<TimeProvider?>()))
.Returns(sessionFactory.Object);
sessionFactory.Setup(f => f.CreateAsync(
It.IsAny<ApplicationConfiguration>(),
It.IsAny<ConfiguredEndpoint>(),
It.IsAny<bool>(),
It.IsAny<bool>(),
It.IsAny<string>(),
It.IsAny<uint>(),
It.IsAny<IUserIdentity?>(),
It.IsAny<ArrayOf<string>>(),
It.IsAny<CancellationToken>()))
.Callback(() => Interlocked.Increment(ref connectAttempts))
.ThrowsAsync(new ServiceResultException(
StatusCodes.BadCertificateUntrusted));
var reconnectPolicy = new ReconnectPolicy
{
Strategy = BackoffStrategy.Constant,
InitialDelay = TimeSpan.FromMilliseconds(5),
MaxRetries = 2,
JitterFactor = 0.0,
MaxTotalReconnectTime = TimeSpan.FromSeconds(10)
};
ServiceResultException exception = Assert.ThrowsAsync<ServiceResultException>(
async () => await Client.ManagedSession.CreateAsync(
configuration,
endpoint,
sessionFactory.Object,
reconnectPolicy: reconnectPolicy).ConfigureAwait(false));
// The original connect error must survive, not the BadNotSupported
// reported by the failover that has no redundant server to try, and
// not the BadNotConnected of a session handed back half-built.
Assert.That(
exception.StatusCode,
Is.EqualTo(StatusCodes.BadCertificateUntrusted));
// A reconnect with no inner session runs a full connect, so the
// retry policy actually retries the initial connect.
Assert.That(connectAttempts, Is.GreaterThan(1));
}
[Test]
[TestCase(false)]
[TestCase(true)]
public void CreateAsyncConnectsThroughAFactoryThatCarriesTheSubscriptionEngine(
bool engineConfigured)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint endpoint = CreateEndpoint();
ISubscriptionEngineFactory configuredEngine = new Mock<ISubscriptionEngineFactory>().Object;
var specialised = new Mock<ISessionFactory>();
specialised.SetupGet(f => f.Telemetry).Returns(telemetry);
var original = new Mock<ISessionFactory>();
original.SetupGet(f => f.Telemetry).Returns(telemetry);
original.SetupGet(f => f.SubscriptionEngineFactory)
.Returns(engineConfigured ? configuredEngine : null);
original.Setup(f => f.WithSubscriptionEngine(
It.IsAny<ISubscriptionEngineFactory>(), It.IsAny<TimeProvider?>()))
.Returns(specialised.Object);
Mock<ISessionFactory> expected = engineConfigured ? original : specialised;
expected.Setup(f => f.CreateAsync(
It.IsAny<ApplicationConfiguration>(),
It.IsAny<ConfiguredEndpoint>(),
It.IsAny<bool>(),
It.IsAny<bool>(),
It.IsAny<string>(),
It.IsAny<uint>(),
It.IsAny<IUserIdentity?>(),
It.IsAny<ArrayOf<string>>(),
It.IsAny<CancellationToken>()))
.ThrowsAsync(new ServiceResultException(StatusCodes.BadCertificateUntrusted));
var reconnectPolicy = new ReconnectPolicy
{
Strategy = BackoffStrategy.Constant,
InitialDelay = TimeSpan.FromMilliseconds(5),
MaxRetries = 0,
JitterFactor = 0.0,
MaxTotalReconnectTime = TimeSpan.FromSeconds(10)
};
Assert.ThrowsAsync<ServiceResultException>(
async () => await Client.ManagedSession.CreateAsync(
configuration,
endpoint,
original.Object,
reconnectPolicy: reconnectPolicy).ConfigureAwait(false));
// A factory without an engine is specialised with the V2 default
// and the copy connects; a factory that has one is used as-is.
original.Verify(
f => f.WithSubscriptionEngine(It.IsAny<ISubscriptionEngineFactory>(), It.IsAny<TimeProvider?>()),
engineConfigured ? Times.Never() : Times.Once());
expected.Verify(
f => f.CreateAsync(
It.IsAny<ApplicationConfiguration>(),
It.IsAny<ConfiguredEndpoint>(),
It.IsAny<bool>(),
It.IsAny<bool>(),
It.IsAny<string>(),
It.IsAny<uint>(),
It.IsAny<IUserIdentity?>(),
It.IsAny<ArrayOf<string>>(),
It.IsAny<CancellationToken>()),
Times.AtLeastOnce());
}
[Test]
public async Task CreateAsyncDisposesManagedSessionWhenInitialWaitIsCanceledAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint endpoint = CreateEndpoint();
IServiceMessageContext messageContext = configuration.CreateMessageContext();
var channel = new Mock<IManagedTransportChannel>();
channel.SetupGet(c => c.MessageContext).Returns(messageContext);
var innerSession = new Session(
channel.Object,
configuration,
endpoint,
engineFactory: DefaultSubscriptionEngineFactory.Instance);
var sessionFactory = new Mock<ISessionFactory>();
sessionFactory.SetupGet(f => f.Telemetry).Returns(telemetry);
sessionFactory.Setup(f => f.WithSubscriptionEngine(
It.IsAny<ISubscriptionEngineFactory>(), It.IsAny<TimeProvider?>()))
.Returns(sessionFactory.Object);
sessionFactory.Setup(f => f.CreateAsync(
It.IsAny<ApplicationConfiguration>(),
It.IsAny<ConfiguredEndpoint>(),
It.IsAny<bool>(),
It.IsAny<bool>(),
It.IsAny<string>(),
It.IsAny<uint>(),
It.IsAny<IUserIdentity?>(),
It.IsAny<ArrayOf<string>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(innerSession);
Client.ManagedSession? managedSession = null;
var fetchStarted = new TaskCompletionSource<bool>(
TaskCreationOptions.RunContinuationsAsynchronously);
var fetchCompletion = new TaskCompletionSource<ServerRedundancyInfo>(
TaskCreationOptions.RunContinuationsAsynchronously);
var redundancyHandler = new Mock<IServerRedundancyHandler>();
redundancyHandler.Setup(h => h.FetchRedundancyInfoAsync(
It.IsAny<ISession>(),
It.IsAny<CancellationToken>()))
.Returns((ISession session, CancellationToken ct) =>
{
managedSession = (Client.ManagedSession)session;
fetchStarted.TrySetResult(true);
ct.Register(() => fetchCompletion.TrySetCanceled());
return new ValueTask<ServerRedundancyInfo>(fetchCompletion.Task);
});
using var cancellation = new CancellationTokenSource();
Task<Client.ManagedSession> createTask = Client.ManagedSession.CreateAsync(
configuration,
endpoint,
sessionFactory.Object,
redundancyHandler: redundancyHandler.Object,
ct: cancellation.Token);
Task startedTask = await Task.WhenAny(
fetchStarted.Task,
Task.Delay(TimeSpan.FromSeconds(5)))
.ConfigureAwait(false);
Assert.That(startedTask, Is.SameAs(fetchStarted.Task));
cancellation.Cancel();
Task completedTask = await Task.WhenAny(
createTask,
Task.Delay(TimeSpan.FromSeconds(5)))
.ConfigureAwait(false);
Assert.That(completedTask, Is.SameAs(createTask));
Assert.CatchAsync<OperationCanceledException>(() => createTask);
Assert.That(fetchCompletion.Task.IsCanceled, Is.True);
Assert.That(managedSession, Is.Not.Null);
Assert.That(
managedSession!.StateMachine.State,
Is.EqualTo(ConnectionState.Closed));
Assert.Throws<ServiceResultException>(() => _ = managedSession.InnerSession);
}
[Test]
public async Task CloseAsyncCompletesWhileConnectAttemptIsInFlightAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint endpoint = CreateEndpoint();
IServiceMessageContext messageContext = configuration.CreateMessageContext();
var channel = new Mock<IManagedTransportChannel>();
channel.SetupGet(c => c.MessageContext).Returns(messageContext);
var innerSession = new Session(
channel.Object,
configuration,
endpoint,
engineFactory: DefaultSubscriptionEngineFactory.Instance);
var sessionFactory = new Mock<ISessionFactory>();
sessionFactory.SetupGet(f => f.Telemetry).Returns(telemetry);
sessionFactory.Setup(f => f.WithSubscriptionEngine(
It.IsAny<ISubscriptionEngineFactory>(), It.IsAny<TimeProvider?>()))
.Returns(sessionFactory.Object);
sessionFactory.Setup(f => f.CreateAsync(
It.IsAny<ApplicationConfiguration>(),
It.IsAny<ConfiguredEndpoint>(),
It.IsAny<bool>(),
It.IsAny<bool>(),
It.IsAny<string>(),
It.IsAny<uint>(),
It.IsAny<IUserIdentity?>(),
It.IsAny<ArrayOf<string>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(innerSession);
Client.ManagedSession? managedSession = null;
var fetchStarted = new TaskCompletionSource<bool>(
TaskCreationOptions.RunContinuationsAsynchronously);
var fetchCompletion = new TaskCompletionSource<ServerRedundancyInfo>(
TaskCreationOptions.RunContinuationsAsynchronously);
// Models a peer that never answers: the connect attempt in flight
// only ends when its token is cancelled.
var redundancyHandler = new Mock<IServerRedundancyHandler>();
redundancyHandler.Setup(h => h.FetchRedundancyInfoAsync(
It.IsAny<ISession>(),
It.IsAny<CancellationToken>()))
.Returns((ISession session, CancellationToken ct) =>
{
managedSession = (Client.ManagedSession)session;
fetchStarted.TrySetResult(true);
ct.Register(() => fetchCompletion.TrySetCanceled());
return new ValueTask<ServerRedundancyInfo>(fetchCompletion.Task);
});
Task<Client.ManagedSession> createTask = Client.ManagedSession.CreateAsync(
configuration,
endpoint,
sessionFactory.Object,
redundancyHandler: redundancyHandler.Object);
Task startedTask = await Task.WhenAny(
fetchStarted.Task,
Task.Delay(TimeSpan.FromSeconds(5)))
.ConfigureAwait(false);
Assert.That(startedTask, Is.SameAs(fetchStarted.Task));
Assert.That(managedSession, Is.Not.Null);
Task<StatusCode> closeTask = managedSession!.CloseAsync(
10_000,
closeChannel: true);
Task completedClose = await Task.WhenAny(
closeTask,
Task.Delay(TimeSpan.FromSeconds(5)))
.ConfigureAwait(false);
Assert.That(
completedClose,
Is.SameAs(closeTask),
"CloseAsync must not wait for the attempt in flight.");
Assert.That(
await closeTask.ConfigureAwait(false),
Is.EqualTo(StatusCodes.Good));
Assert.That(
managedSession.StateMachine.State,
Is.EqualTo(ConnectionState.Closed));
Assert.CatchAsync<ServiceResultException>(() => createTask);
}
[Test]
public async Task CloseAsyncReturnsBadTimeoutWhenInnerCloseDoesNotCompleteAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint endpoint = CreateEndpoint();
var channel = new Mock<IManagedTransportChannel>();
channel.SetupGet(c => c.MessageContext).Returns(
configuration.CreateMessageContext());
var release = new TaskCompletionSource<bool>(
TaskCreationOptions.RunContinuationsAsynchronously);
var innerSession = new BlockingCloseSession(
channel.Object,
configuration,
endpoint)
{
CloseBlocker = release.Task
};
using Client.ManagedSession managedSession = CreateManagedSessionWithInner(
configuration,
endpoint,
innerSession,
telemetry);
managedSession.StateMachine.Start();
StatusCode result = await managedSession
.CloseAsync(250, closeChannel: true)
.ConfigureAwait(false);
Assert.That(result, Is.EqualTo(StatusCodes.BadTimeout));
// The close continues in the background: let it finish so the
// session is torn down before the fixture completes.
release.TrySetResult(true);
await managedSession.StateMachine
.WaitForClosedAsync(default)
.ConfigureAwait(false);
}
[Test]
public async Task CloseAsyncPropagatesInnerCloseStatusAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint endpoint = CreateEndpoint();
var channel = new Mock<IManagedTransportChannel>();
channel.SetupGet(c => c.MessageContext).Returns(
configuration.CreateMessageContext());
var innerSession = new BlockingCloseSession(
channel.Object,
configuration,
endpoint)
{
CloseResult = StatusCodes.BadSessionClosed
};
using Client.ManagedSession managedSession = CreateManagedSessionWithInner(
configuration,
endpoint,
innerSession,
telemetry);
managedSession.StateMachine.Start();
StatusCode result = await managedSession
.CloseAsync(10_000, closeChannel: true)
.ConfigureAwait(false);
Assert.That(result, Is.EqualTo(StatusCodes.BadSessionClosed));
Assert.That(innerSession.CloseChannelRequested, Is.True);
channel.Verify(c => c.Dispose(), Times.Once);
}
[Test]
public async Task CloseAsyncWithCloseChannelFalseKeepsTransportChannelAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint endpoint = CreateEndpoint();
var channel = new Mock<IManagedTransportChannel>();
channel.SetupGet(c => c.MessageContext).Returns(
configuration.CreateMessageContext());
var innerSession = new BlockingCloseSession(
channel.Object,
configuration,
endpoint);
using Client.ManagedSession managedSession = CreateManagedSessionWithInner(
configuration,
endpoint,
innerSession,
telemetry);
managedSession.StateMachine.Start();
StatusCode result = await managedSession
.CloseAsync(10_000, closeChannel: false)
.ConfigureAwait(false);
Assert.That(result, Is.EqualTo(StatusCodes.Good));
Assert.That(innerSession.CloseChannelRequested, Is.False);
// The channel stays owned by the caller: neither closed nor
// disposed by the session teardown.
channel.Verify(
c => c.CloseAsync(It.IsAny<CancellationToken>()),
Times.Never);
channel.Verify(c => c.Dispose(), Times.Never);
}
/// <summary>
/// G6 (review of L1-3): disposing or closing a managed session from a
/// Notification handler of its inner session made the state machine
/// worker dispose the inner session, which waited the full drain
/// timeout (30 s) for the very handler that waited for the close.
/// </summary>
[TestCase(true)]
[TestCase(false)]
public async Task CloseFromInnerNotificationHandlerDoesNotWaitForItsDrainAsync(bool dispose)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ApplicationConfiguration configuration = CreateClientConfiguration(telemetry);
ConfiguredEndpoint endpoint = CreateEndpoint();
var channel = new Mock<IManagedTransportChannel>();
channel.SetupGet(c => c.MessageContext).Returns(
configuration.CreateMessageContext());
var innerSession = new BlockingCloseSession(
channel.Object,
configuration,
endpoint);
using Client.ManagedSession managedSession = CreateManagedSessionWithInner(
configuration,
endpoint,
innerSession,
telemetry);
managedSession.StateMachine.Start();
var closed = new TaskCompletionSource<TimeSpan>(
TaskCreationOptions.RunContinuationsAsynchronously);
Assert.That(innerSession.RunBackgroundWork("Notification", () =>
{
var watch = Stopwatch.StartNew();
if (dispose)
{
managedSession.Dispose();
}
else
{
managedSession.CloseAsync(0, closeChannel: true)
.GetAwaiter().GetResult();
}
closed.SetResult(watch.Elapsed);
}), Is.True);
TimeSpan elapsed = await closed.Task
.WaitAsync(TimeSpan.FromSeconds(60))
.ConfigureAwait(false);
Assert.That(elapsed, Is.LessThan(TimeSpan.FromSeconds(8)));
Assert.That(innerSession.Disposed, Is.True);
}
[Test]
public async Task CreateAsyncLogsDisposeFailureAndPreservesCancellationAsync()
{
var logger = new Mock<ILogger>();
logger.Setup(l => l.IsEnabled(It.IsAny<LogLevel>())).Returns(true);
var disposeFailure = new InvalidOperationException("Test cleanup failure.");
// The stack emits source-generated log messages whose state has an empty
// ToString(); match on the EventId name (the log method name) instead.
#pragma warning disable CA1873 // Moq Setup on ILogger.Log is an expression tree, not an executed logging call.
logger.Setup(l => l.Log(
It.IsAny<LogLevel>(),
It.Is<EventId>(eventId => eventId.Name == "ConnectionStateMachineWorkerExiting"),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()))
.Throws(disposeFailure);
#pragma warning restore CA1873
var loggerFactory = new Mock<ILoggerFactory>();
loggerFactory.Setup(f => f.CreateLogger(It.IsAny<string>()))
.Returns(logger.Object);
var telemetry = new Mock<ITelemetryContext>();
telemetry.SetupGet(t => t.LoggerFactory).Returns(loggerFactory.Object);
var sessionFactory = new Mock<ISessionFactory>();
sessionFactory.SetupGet(f => f.Telemetry).Returns(telemetry.Object);
sessionFactory.Setup(f => f.WithSubscriptionEngine(
It.IsAny<ISubscriptionEngineFactory>(), It.IsAny<TimeProvider?>()))
.Returns(sessionFactory.Object);
var connectStarted = new TaskCompletionSource<bool>(
TaskCreationOptions.RunContinuationsAsynchronously);
var connectCompletion = new TaskCompletionSource<IDisposable>(
TaskCreationOptions.RunContinuationsAsynchronously);
var connectGate = new Mock<IClientConnectGate>();
connectGate.Setup(g => g.AcquireAsync(It.IsAny<CancellationToken>()))
.Returns((CancellationToken ct) =>
{
connectStarted.TrySetResult(true);
ct.Register(() => connectCompletion.TrySetCanceled());
return new ValueTask<IDisposable>(connectCompletion.Task);
});
using var cancellation = new CancellationTokenSource();
Task<Client.ManagedSession> createTask = Client.ManagedSession.CreateAsync(
CreateClientConfiguration(telemetry.Object),
CreateEndpoint(),
sessionFactory.Object,
connectGate: connectGate.Object,
ct: cancellation.Token);
Task startedTask = await Task.WhenAny(
connectStarted.Task,
Task.Delay(TimeSpan.FromSeconds(5)))
.ConfigureAwait(false);
Assert.That(startedTask, Is.SameAs(connectStarted.Task));
cancellation.Cancel();
Task completedTask = await Task.WhenAny(
createTask,
Task.Delay(TimeSpan.FromSeconds(5)))
.ConfigureAwait(false);
Assert.That(completedTask, Is.SameAs(createTask));
Assert.CatchAsync<OperationCanceledException>(() => createTask);
#pragma warning disable CA1873 // Moq Verify on ILogger.Log is an expression tree, not an executed logging call.
logger.Verify(
l => l.Log(
LogLevel.Error,
It.Is<EventId>(eventId =>
eventId.Name == "ManagedSessionDisposalAfterConnectionFailureFailed"),
It.IsAny<It.IsAnyType>(),
It.Is<Exception?>(exception => ReferenceEquals(exception, disposeFailure)),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
#pragma warning restore CA1873
}
#pragma warning disable IDE0051, RCS1213 // Test scaffold kept for reconnect-path tests that need the private handler.
private static Task<ServiceResult> InvokeHandleReconnectAsync(
Client.ManagedSession managedSession,
IRetryBudget budget)
{
MethodInfo? method = typeof(Client.ManagedSession).GetMethod(
"HandleReconnectAsync",
BindingFlags.NonPublic | BindingFlags.Instance,
null,
[typeof(IRetryBudget), typeof(CancellationToken)],
null);
Assert.That(method, Is.Not.Null);
var task = (Task<ServiceResult>?)method!.Invoke(
managedSession,
[budget, CancellationToken.None]);
Assert.That(task, Is.Not.Null);
return task!;
}
#pragma warning restore IDE0051, RCS1213
#pragma warning disable IDE0051, RCS1213 // Test scaffold kept for failover-path tests that need the private handler.
private static Task<ServiceResult> InvokeHandleFailoverAsync(
Client.ManagedSession managedSession,
IRetryBudget budget)
{
MethodInfo? method = typeof(Client.ManagedSession).GetMethod(
"HandleFailoverAsync",
BindingFlags.NonPublic | BindingFlags.Instance,
null,
[typeof(IRetryBudget), typeof(CancellationToken)],
null);
Assert.That(method, Is.Not.Null);
var task = (Task<ServiceResult>?)method!.Invoke(
managedSession,
[budget, CancellationToken.None]);
Assert.That(task, Is.Not.Null);
return task!;
}
#pragma warning restore IDE0051, RCS1213
#pragma warning disable IDE0051, RCS1213 // Test scaffold kept for tests that inject an already-created inner Session.
private static Client.ManagedSession CreateManagedSessionWithInner(
ApplicationConfiguration configuration,
ConfiguredEndpoint endpoint,
Session innerSession,
ITelemetryContext telemetry,
NetworkRedundancyOptions? networkRedundancy = null,
IServerRedundancyHandler? redundancyHandler = null,
TimeProvider? timeProvider = null)
{
ILogger<Client.ManagedSession> logger = telemetry.CreateLogger<Client.ManagedSession>();
var sessionFactory = new Mock<ISessionFactory>();
sessionFactory.SetupGet(f => f.Telemetry).Returns(telemetry);
var reconnectPolicy = new ReconnectPolicy(new ReconnectPolicyOptions
{
MaxTotalReconnectTime = TimeSpan.FromSeconds(30)
});
ConstructorInfo? ctor = typeof(Client.ManagedSession).GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance,
null,
[
typeof(ApplicationConfiguration),
typeof(ConfiguredEndpoint),
typeof(ISessionFactory),
typeof(IReconnectPolicy),
typeof(IServerRedundancyHandler),
typeof(ILogger),
typeof(IUserIdentity),
typeof(IClientIdentityProvider),
typeof(TimeProvider),
typeof(ArrayOf<string>),
typeof(string),
typeof(uint),
typeof(bool),
typeof(bool),
typeof(bool),
typeof(bool),
typeof(NetworkRedundancyOptions),
typeof(ServerRedundancyOptions),
typeof(IClientChannelManager),
typeof(IClientConnectGate)
],
null);
Assert.That(ctor, Is.Not.Null);
var managedSession = (Client.ManagedSession)ctor!.Invoke(
[
configuration,
endpoint,
sessionFactory.Object,
reconnectPolicy,
redundancyHandler,
logger,
null,
null,
timeProvider,
default(ArrayOf<string>),
"TestManagedSession",
60000u,
false,
false,
false,
false,
networkRedundancy,
null,
null,
null
]);
typeof(Client.ManagedSession)
.GetField(
"m_session",
BindingFlags.NonPublic | BindingFlags.Instance)!
.SetValue(managedSession, innerSession);
return managedSession;
}
#pragma warning restore IDE0051, RCS1213
/// <summary>
/// Inner session whose close can be observed and delayed by the test.
/// </summary>
private sealed class BlockingCloseSession : Session
{
public BlockingCloseSession(
ITransportChannel channel,
ApplicationConfiguration configuration,
ConfiguredEndpoint endpoint)
: base(
channel,
configuration,
endpoint,
engineFactory: DefaultSubscriptionEngineFactory.Instance)
{
}
/// <summary>
/// The status the inner close reports.
/// </summary>
public StatusCode CloseResult { get; set; } = StatusCodes.Good;
/// <summary>
/// Completes when the inner close is allowed to return.
/// </summary>
public Task CloseBlocker { get; set; } = Task.CompletedTask;
/// <summary>
/// The value the managed session forwarded for closeChannel.
/// </summary>
public bool? CloseChannelRequested { get; private set; }
public override async Task<StatusCode> CloseAsync(
int timeout,
bool closeChannel,
CancellationToken ct = default)
{
CloseChannelRequested = closeChannel;
await CloseBlocker.ConfigureAwait(false);
return CloseResult;
}
}
private sealed class BlockingConnectGate : IClientConnectGate
{
public Task Started => m_started.Task;
public Task Stopped => m_stopped.Task;
public async ValueTask<IDisposable> AcquireAsync(
CancellationToken ct = default)
{
m_started.TrySetResult(null);
try
{
await Task.Delay(Timeout.Infinite, ct).ConfigureAwait(false);
}
finally
{
m_stopped.TrySetResult(null);
}
throw new InvalidOperationException(
"The blocking connect gate unexpectedly completed without cancellation.");
}
private readonly TaskCompletionSource<object?> m_started =
new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly TaskCompletionSource<object?> m_stopped =
new(TaskCreationOptions.RunContinuationsAsynchronously);
}
#pragma warning disable IDE0051, RCS1213 // Test scaffold kept for ManagedSession construction tests.
private static ApplicationConfiguration CreateClientConfiguration(
ITelemetryContext telemetry)
{
return new ApplicationConfiguration(telemetry)
{
ApplicationName = "ManagedSessionTests",
ApplicationType = ApplicationType.Client,
ApplicationUri = "urn:localhost:ManagedSessionTests",
ProductUri = "urn:localhost:ManagedSessionTests",
ClientConfiguration = new ClientConfiguration
{
DefaultSessionTimeout = 60000,
MinSubscriptionLifetime = 10000
},
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier()
},
TransportQuotas = new TransportQuotas
{
OperationTimeout = 6000
}
};
}
#pragma warning restore IDE0051, RCS1213
#pragma warning disable IDE0051, RCS1213 // Test scaffold kept for ManagedSession endpoint construction tests.
private static ConfiguredEndpoint CreateEndpoint()
{
var description = new EndpointDescription
{
EndpointUrl = "opc.tcp://localhost:4840",
SecurityMode = MessageSecurityMode.None,
SecurityPolicyUri = SecurityPolicies.None,
TransportProfileUri = Profiles.UaTcpTransport,
UserIdentityTokens =
[
new UserTokenPolicy
{
PolicyId = "anonymous",
TokenType = UserTokenType.Anonymous,
SecurityPolicyUri = SecurityPolicies.None
}
]
};
description.Server.ApplicationUri = description.EndpointUrl;
description.Server.ApplicationType = ApplicationType.Server;
return new ConfiguredEndpoint(
null,
description,
new EndpointConfiguration { OperationTimeout = 6000 })
{
UpdateBeforeConnect = false
};
}
#pragma warning restore IDE0051, RCS1213
}
}