Files
UA-.NETStandard/tests/Opc.Ua.Sessions.Tests/ClientTest.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

2592 lines
107 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/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Moq;
using NUnit.Framework;
using Opc.Ua.Bindings;
using Opc.Ua.Client;
using Opc.Ua.Client.TestFramework;
using Opc.Ua.Configuration;
using Opc.Ua.Security.Certificates;
using Opc.Ua.Security.Certificates.Tests;
using Opc.Ua.Server.TestFramework;
using Opc.Ua.Tests;
using ISession = Opc.Ua.Client.ISession;
using Subscription = Opc.Ua.Client.Subscription;
using TraceableRequestHeaderClientSession = Opc.Ua.Client.TraceableRequestHeaderClientSession;
// 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.
#pragma warning disable CA2000
// CS0618: legacy AttachChannel/DetachChannel paths are explicitly tested here to keep
// back-compat behavior validated; suppression is intentional for these legacy tests.
#pragma warning disable CS0618
namespace Opc.Ua.Sessions.Tests
{
/// <summary>
/// Client tests.
/// </summary>
[TestFixture]
[Category("Client")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[TestFixtureSource(nameof(FixtureArgs))]
[Parallelizable(ParallelScope.Fixtures)]
[MemoryDiagnoser]
[DisassemblyDiagnoser]
public class ClientTest : ClientTestFramework
{
public ClientTest()
: base(Utils.UriSchemeOpcTcp)
{
}
public ClientTest(string uriScheme = Utils.UriSchemeOpcTcp)
: base(uriScheme)
{
}
public static readonly NodeId[] TypeSystems =
[
ObjectIds.OPCBinarySchema_TypeSystem,
ObjectIds.XmlSchema_TypeSystem
];
public static readonly string[] SupportedEccPolicies =
[
.. GetSupportedEccPolicyUris(includeCurvePolicies: false)
];
public static readonly string[] SupportedEccX509Policies =
[
.. SupportedEccPolicies.Where(policyUri =>
{
CertificateKeyAlgorithm certificateKeyAlgorithm =
SecurityPolicies.Default.GetInfo(policyUri)!.CertificateKeyAlgorithm;
return certificateKeyAlgorithm is not CertificateKeyAlgorithm.Curve25519 and
not CertificateKeyAlgorithm.Curve448;
})
];
public static IEnumerable<TestCaseData> ReconnectSessionOnAlternateChannelWithSavedSessionSecretsEccTestCases()
{
foreach (string securityPolicy in SupportedEccPolicies)
{
yield return new TestCaseData(securityPolicy, true);
yield return new TestCaseData(securityPolicy, false);
}
}
/// <summary>
/// Set up a Server and a Client instance.
/// </summary>
[OneTimeSetUp]
public override Task OneTimeSetUpAsync()
{
SupportsExternalServerUrl = true;
return base.OneTimeSetUpAsync();
}
/// <summary>
/// Tear down the Server and the Client.
/// </summary>
[OneTimeTearDown]
public override Task OneTimeTearDownAsync()
{
return base.OneTimeTearDownAsync();
}
/// <summary>
/// Test setup.
/// </summary>
[SetUp]
public override Task SetUpAsync()
{
return base.SetUpAsync();
}
/// <summary>
/// Test teardown.
/// </summary>
[TearDown]
public override Task TearDownAsync()
{
return base.TearDownAsync();
}
/// <summary>
/// Global Setup for benchmarks.
/// </summary>
[GlobalSetup]
public override void GlobalSetup()
{
base.GlobalSetup();
}
/// <summary>
/// Global cleanup for benchmarks.
/// </summary>
[GlobalCleanup]
public override void GlobalCleanup()
{
base.GlobalCleanup();
}
[Test]
[Order(100)]
public async Task GetEndpointsAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var endpointConfiguration = EndpointConfiguration.Create();
endpointConfiguration.OperationTimeout = 10000;
using DiscoveryClient client = await DiscoveryClient.CreateAsync(
ServerUrl,
endpointConfiguration,
telemetry).ConfigureAwait(false);
Endpoints = await client.GetEndpointsAsync(default, CancellationToken.None)
.ConfigureAwait(false);
StatusCode statusCode = await client.CloseAsync(CancellationToken.None)
.ConfigureAwait(false);
Assert.That(statusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(Endpoints.IsNull, Is.False);
Assert.That(Endpoints.Count, Is.GreaterThan(0),
"Server must advertise at least one endpoint.");
TestContext.Out.WriteLine("Endpoints:");
foreach (EndpointDescription endpoint in Endpoints)
{
TestContext.Out.WriteLine("{0}", endpoint.Server.ApplicationName);
TestContext.Out.WriteLine(" {0}", endpoint.Server.ApplicationUri);
TestContext.Out.WriteLine(" {0}", endpoint.EndpointUrl);
TestContext.Out.WriteLine(" {0}", endpoint.EncodingSupport);
TestContext.Out.WriteLine(
" {0}/{1}/{2}",
endpoint.SecurityLevel,
endpoint.SecurityMode,
endpoint.SecurityPolicyUri);
if (!endpoint.ServerCertificate.IsEmpty)
{
using var cert = Certificate.FromRawData(
endpoint.ServerCertificate);
TestContext.Out.WriteLine(" [{0}]", cert.Thumbprint);
}
else
{
TestContext.Out.WriteLine(" [no certificate]");
}
foreach (UserTokenPolicy userIdentity in endpoint.UserIdentityTokens)
{
TestContext.Out.WriteLine(" {0}", userIdentity.TokenType);
TestContext.Out.WriteLine(" {0}", userIdentity.PolicyId);
TestContext.Out.WriteLine(" {0}", userIdentity.SecurityPolicyUri);
}
}
}
[Test]
[Order(101)]
public async Task PublishWithUInt32MaxTimeoutHintAsync()
{
CreateSubscriptionResponse createResponse = await Session.CreateSubscriptionAsync(
null,
100,
100,
1,
0,
true,
0,
CancellationToken.None).ConfigureAwait(false);
try
{
var requestHeader = new RequestHeader
{
Timestamp = DateTime.UtcNow,
TimeoutHint = uint.MaxValue
};
PublishResponse response = await Session.PublishAsync(
requestHeader,
ArrayOf<SubscriptionAcknowledgement>.Empty,
CancellationToken.None).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(
response.ResponseHeader.ServiceResult,
Is.EqualTo((StatusCode)StatusCodes.Good));
Assert.That(response.SubscriptionId, Is.EqualTo(createResponse.SubscriptionId));
}
finally
{
await Session.DeleteSubscriptionsAsync(
null,
new uint[] { createResponse.SubscriptionId }.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
}
}
[Test]
[Order(100)]
public async Task FindServersAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var endpointConfiguration = EndpointConfiguration.Create();
endpointConfiguration.OperationTimeout = 10000;
using DiscoveryClient client = await DiscoveryClient.CreateAsync(
ServerUrl,
endpointConfiguration,
telemetry).ConfigureAwait(false);
ArrayOf<ApplicationDescription> servers = await client.FindServersAsync(default)
.ConfigureAwait(false);
StatusCode statusCode = await client.CloseAsync(CancellationToken.None)
.ConfigureAwait(false);
Assert.That(statusCode, Is.EqualTo(StatusCodes.Good));
Assert.That(servers.IsNull, Is.False);
Assert.That(servers.Count, Is.GreaterThan(0),
"FindServers must return at least the server itself.");
foreach (ApplicationDescription server in servers)
{
TestContext.Out.WriteLine("{0}", server.ApplicationName);
TestContext.Out.WriteLine(" {0}", server.ApplicationUri);
TestContext.Out.WriteLine(" {0}", server.ApplicationType);
TestContext.Out.WriteLine(" {0}", server.ProductUri);
foreach (string discoveryUrl in server.DiscoveryUrls)
{
TestContext.Out.WriteLine(" {0}", discoveryUrl);
}
}
}
/// <summary>
/// FindServers and GetEndpoints return the ApplicationName in the requested
/// locale when the server advertises it (CTT Discovery Find Servers Filter
/// 003.js/006.js, Get Endpoints 002.js request de-DE).
/// </summary>
[Test]
[Order(101)]
[TestCase("de-DE")]
[TestCase("en-US")]
public async Task DiscoveryReturnsApplicationNameInRequestedLocaleAsync(string locale)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var endpointConfiguration = EndpointConfiguration.Create();
endpointConfiguration.OperationTimeout = 10000;
using DiscoveryClient client = await DiscoveryClient.CreateAsync(
ServerUrl,
endpointConfiguration,
telemetry).ConfigureAwait(false);
ArrayOf<string> localeIds = [locale];
FindServersResponse servers = await client.FindServersAsync(
null,
client.Endpoint.EndpointUrl,
localeIds,
default,
CancellationToken.None).ConfigureAwait(false);
GetEndpointsResponse endpoints = await client.GetEndpointsAsync(
null,
client.Endpoint.EndpointUrl,
localeIds,
default,
CancellationToken.None).ConfigureAwait(false);
await client.CloseAsync(CancellationToken.None).ConfigureAwait(false);
ApplicationDescription self = servers.Servers.ToArray()!.Single(s =>
s.ApplicationUri == ServerFixture.Config.ApplicationUri);
Assert.That(self.ApplicationName.Locale, Is.EqualTo(locale));
Assert.That(self.ApplicationName.Text, Is.Not.Empty);
Assert.That(endpoints.Endpoints.Count, Is.GreaterThan(0));
foreach (EndpointDescription endpoint in endpoints.Endpoints)
{
Assert.That(endpoint.Server.ApplicationName.Locale, Is.EqualTo(locale));
}
}
[Test]
[Order(100)]
public async Task FindServersOnNetworkAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var endpointConfiguration = EndpointConfiguration.Create();
endpointConfiguration.OperationTimeout = 10000;
using DiscoveryClient client = await DiscoveryClient.CreateAsync(
ServerUrl,
endpointConfiguration,
telemetry).ConfigureAwait(false);
try
{
FindServersOnNetworkResponse response = await client
.FindServersOnNetworkAsync(null, 0, 100, default, CancellationToken.None)
.ConfigureAwait(false);
StatusCode statusCode = await client.CloseAsync(CancellationToken.None)
.ConfigureAwait(false);
Assert.That(statusCode, Is.EqualTo(StatusCodes.Good));
foreach (ServerOnNetwork server in response.Servers)
{
TestContext.Out.WriteLine("{0}", server.ServerName);
TestContext.Out.WriteLine(" {0}", server.RecordId);
TestContext.Out.WriteLine(" {0}", server.ServerCapabilities);
TestContext.Out.WriteLine(" {0}", server.DiscoveryUrl);
}
}
catch (ServiceResultException sre) when (sre.StatusCode == StatusCodes
.BadServiceUnsupported)
{
Assert.Ignore("FindServersOnNetwork not supported on server.");
}
}
/// <summary>
/// Try to use the discovery channel to read a node.
/// </summary>
[Test]
[Order(105)]
[TestCase(1000)]
[TestCase(10000)]
public async Task ReadOnDiscoveryChannelAsync(int readCount)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var endpointConfiguration = EndpointConfiguration.Create();
endpointConfiguration.OperationTimeout = 10000;
using DiscoveryClient client = await DiscoveryClient.CreateAsync(
ServerUrl,
endpointConfiguration,
telemetry).ConfigureAwait(false);
ArrayOf<EndpointDescription> endpoints =
await client.GetEndpointsAsync(default).ConfigureAwait(false);
Assert.That(endpoints.IsNull, Is.False);
ITransportChannel channel = client.TransportChannel;
var sessionClient = new SessionClient(channel, telemetry)
{
ReturnDiagnostics = DiagnosticsMasks.SymbolicIdAndText
};
var request = new ReadRequest { RequestHeader = null! };
var readValueId = new ReadValueId
{
NodeId = new NodeId(Guid.NewGuid()),
AttributeId = Attributes.Value
};
var readValues = new List<ReadValueId>();
for (int i = 0; i < readCount; i++)
{
readValues.Add(readValueId);
}
// try to read nodes using discovery channel
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(async () =>
await sessionClient.ReadAsync(
null,
0,
TimestampsToReturn.Neither,
readValues,
default).ConfigureAwait(false));
StatusCode statusCode = StatusCodes.BadSecurityPolicyRejected;
// race condition, if socket closed is detected before the error was returned,
// client may report channel closed instead of security policy rejected
if (StatusCodes.BadSecureChannelClosed == sre.StatusCode)
{
Assert.Inconclusive($"Unexpected Status: {sre}");
}
Assert.That(
sre.StatusCode,
Is.EqualTo(StatusCodes.BadSecurityPolicyRejected),
$"Unexpected Status: {sre}");
}
/// <summary>
/// GetEndpoints on the discovery channel,
/// but an oversized message should return an error.
/// </summary>
[Test]
[Order(105)]
[TestCase(false)]
public async Task GetEndpointsOnDiscoveryChannelAsync(bool securityNoneEnabled)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var endpointConfiguration = EndpointConfiguration.Create();
endpointConfiguration.OperationTimeout = 10000;
using DiscoveryClient client = await DiscoveryClient.CreateAsync(
ServerUrl,
endpointConfiguration,
telemetry).ConfigureAwait(false);
var profileUris = new List<string>();
for (int i = 0; i < 10000; i++)
{
// dummy uri to create a bigger message
profileUris.Add($"https://opcfoundation.org/ProfileUri={i}");
}
if (securityNoneEnabled)
{
// test can pass, there is no limit for discovery messages
// because the server supports security None
await client.GetEndpointsAsync(profileUris).ConfigureAwait(false);
}
else
{
ServiceResultException sre = Assert
.ThrowsAsync<ServiceResultException>(() =>
client.GetEndpointsAsync(profileUris));
// race condition, if socket closed is detected before the error was returned,
// client may report channel closed instead of security policy rejected
if (StatusCodes.BadSecureChannelClosed == sre.StatusCode)
{
Assert.Inconclusive($"Unexpected Status: {sre}");
}
Assert.That(
sre.StatusCode,
Is.EqualTo(StatusCodes.BadSecurityPolicyRejected),
$"Unexpected Status: {sre}");
}
}
[Test]
[Order(110)]
public async Task InvalidConfigurationAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var applicationInstance = new ApplicationInstance(telemetry)
{
ApplicationName = ClientFixture.Config.ApplicationName
};
Assert.That(applicationInstance, Is.Not.Null);
ArrayOf<CertificateIdentifier> applicationCerts =
ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates(
ClientFixture.Config.SecurityConfiguration.ApplicationCertificate!.SubjectName!);
_ = await applicationInstance
.Build(ClientFixture.Config.ApplicationUri!, ClientFixture.Config.ProductUri!)
.AsClient()
.AddSecurityConfiguration(applicationCerts)
.CreateAsync()
.ConfigureAwait(false);
}
[Theory]
[Order(200)]
public async Task ConnectAndCloseSyncAsync(string securityPolicy)
{
bool closeChannel = securityPolicy != SecurityPolicies.Aes128_Sha256_RsaOaep;
ISession session = await ClientFixture
.ConnectAsync(ServerUrl, securityPolicy, Endpoints)
.ConfigureAwait(false);
Assert.That(session, Is.Not.Null);
Session.SessionClosing += SessionClosing;
StatusCode result =
await session.CloseAsync(5_000, closeChannel).ConfigureAwait(false);
session.Dispose();
}
[Theory]
[Order(201)]
public async Task ConnectAndCloseAsync(string securityPolicy)
{
bool closeChannel = securityPolicy != SecurityPolicies.Basic128Rsa15;
ISession session = await ClientFixture
.ConnectAsync(ServerUrl, securityPolicy, Endpoints)
.ConfigureAwait(false);
Assert.That(session, Is.Not.Null);
Session.SessionClosing += SessionClosing;
StatusCode result = await session
.CloseAsync(5_000, closeChannel, CancellationToken.None)
.ConfigureAwait(false);
session.Dispose();
}
[Test]
[Order(202)]
public async Task ConnectAndCloseAsyncReadAfterCloseAsync()
{
const string securityPolicy = SecurityPolicies.Basic256Sha256;
using ISession session = await ClientFixture
.ConnectAsync(ServerUrl, securityPolicy, Endpoints)
.ConfigureAwait(false);
Assert.That(session, Is.Not.Null);
Session.SessionClosing += SessionClosing;
NodeId nodeId = VariableIds.ServerStatusType_BuildInfo;
Node node = await session.ReadNodeAsync(nodeId, CancellationToken.None)
.ConfigureAwait(false);
DataValue value = await session.ReadValueAsync(nodeId, CancellationToken.None)
.ConfigureAwait(false);
// keep channel open/inactive
StatusCode result = await session.CloseAsync(1_000, false).ConfigureAwait(false);
Assert.That(result, Is.EqualTo(StatusCodes.Good));
await Task.Delay(5_000).ConfigureAwait(false);
ServiceResultException sre = Assert
.ThrowsAsync<ServiceResultException>(async () =>
await session.ReadNodeAsync(nodeId, CancellationToken.None)
.ConfigureAwait(false));
Assert.That(
sre.StatusCode,
Is.EqualTo(StatusCodes.BadSessionIdInvalid));
}
[Test]
[Order(204)]
public async Task ConnectAndCloseAsyncReadAfterCloseSessionReconnectAsync()
{
const string securityPolicy = SecurityPolicies.Basic256Sha256;
using ISession session = await ClientFixture
.ConnectAsync(ServerUrl, securityPolicy, Endpoints)
.ConfigureAwait(false);
Assert.That(session, Is.Not.Null);
Session.SessionClosing += SessionClosing;
IUserIdentity userIdentity = session.Identity;
string sessionName = session.SessionName;
NodeId nodeId = VariableIds.ServerStatusType_BuildInfo;
Node node = await session.ReadNodeAsync(nodeId, CancellationToken.None)
.ConfigureAwait(false);
DataValue value = await session.ReadValueAsync(nodeId, CancellationToken.None)
.ConfigureAwait(false);
// keep channel open/inactive
StatusCode result = await session.CloseAsync(1_000, false).ConfigureAwait(false);
Assert.That(result, Is.EqualTo(StatusCodes.Good));
await Task.Delay(5_000).ConfigureAwait(false);
ServiceResultException sre = Assert
.ThrowsAsync<ServiceResultException>(async () =>
await session.ReadNodeAsync(nodeId, CancellationToken.None)
.ConfigureAwait(false));
Assert.That(
sre.StatusCode,
Is.EqualTo(StatusCodes.BadSessionIdInvalid));
// reconnect/reactivate
await session.OpenAsync(sessionName, userIdentity, CancellationToken.None)
.ConfigureAwait(false);
node = await session.ReadNodeAsync(nodeId, CancellationToken.None)
.ConfigureAwait(false);
value = await session.ReadValueAsync(nodeId, CancellationToken.None)
.ConfigureAwait(false);
}
[Test]
[Order(206)]
public async Task ConnectCloseSessionCloseChannelAsync()
{
const string securityPolicy = SecurityPolicies.Basic256Sha256;
using ISession session = await ClientFixture
.ConnectAsync(ServerUrl, securityPolicy, Endpoints)
.ConfigureAwait(false);
Assert.That(session, Is.Not.Null);
Session.SessionClosing += SessionClosing;
IUserIdentity userIdentity = session.Identity;
string sessionName = session.SessionName;
NodeId nodeId = VariableIds.ServerStatusType_BuildInfo;
Node node = await session.ReadNodeAsync(nodeId, CancellationToken.None)
.ConfigureAwait(false);
DataValue value = await session.ReadValueAsync(nodeId, CancellationToken.None)
.ConfigureAwait(false);
// keep channel opened but detach so no comm goes through
ITransportChannel channel = session.TransportChannel;
session.DetachChannel();
int waitTime =
ServerFixture.Application.ApplicationConfiguration!.TransportQuotas!.ChannelLifetime +
(ServerFixture.Application.ApplicationConfiguration.TransportQuotas
.ChannelLifetime /
2) +
5_000;
await Task.Delay(waitTime).ConfigureAwait(false);
// Channel handling checked for TcpTransportChannel only
if (channel is TcpTransportChannel tcp)
{
Assert.That(tcp.Transport, Is.Null);
}
channel.Dispose();
}
[Theory]
[Order(210)]
[CancelAfter(120_000)]
public async Task ConnectAndReconnectAsync(
bool reconnectAbort,
bool useMaxReconnectPeriod,
CancellationToken ct)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
const int connectTimeout = MaxTimeout;
ISession session = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.Basic256Sha256, Endpoints)
.ConfigureAwait(false);
Assert.That(session, Is.Not.Null);
int sessionConfigChanged = 0;
session.SessionConfigurationChanged += (sender, e) => sessionConfigChanged++;
int sessionClosing = 0;
session.SessionClosing += (sender, e) => sessionClosing++;
var reconnected = new TaskCompletionSource<bool>(
TaskCreationOptions.RunContinuationsAsynchronously);
var reconnectHandler = new SessionReconnectHandler(
telemetry,
reconnectAbort,
useMaxReconnectPeriod ? MaxTimeout : -1);
reconnectHandler.BeginReconnect(
session,
connectTimeout / 5,
(sender, e) =>
{
// ignore callbacks from discarded objects.
if (!ReferenceEquals(sender, reconnectHandler))
{
Assert.Fail("Unexpected sender");
}
if (reconnectHandler.Session != null)
{
if (!ReferenceEquals(reconnectHandler.Session, session))
{
session.Dispose();
session = reconnectHandler.Session;
TestContext.Out.WriteLine("Reconnected with new session.");
}
else
{
TestContext.Out.WriteLine("Reconnected with the same session.");
}
}
else
{
TestContext.Out.WriteLine("Reconnect aborted reusing secure channel.");
}
reconnected.TrySetResult(true);
});
// Await rather than block: the reconnect this waits for runs on
// the thread pool, and parking a pool thread on it (WaitOne) is
// what lets parallel fixtures starve a small CI runner.
Task completed = await Task
.WhenAny(reconnected.Task, Task.Delay(connectTimeout, ct))
.ConfigureAwait(false);
bool timeout = ReferenceEquals(completed, reconnected.Task);
Assert.That(timeout, Is.True);
if (reconnectAbort)
{
Assert.That(reconnectHandler.Session, Is.Null);
}
else
{
Assert.That(reconnectHandler.Session, Is.EqualTo(session));
}
Assert.That(sessionConfigChanged, Is.EqualTo(reconnectAbort ? 0 : 1));
Assert.That(sessionClosing, Is.Zero);
StatusCode result = await session.CloseAsync(ct).ConfigureAwait(false);
reconnectHandler.Dispose();
session.Dispose();
Assert.That(sessionClosing, Is.GreaterThanOrEqualTo(0));
}
[Theory]
[Order(220)]
public async Task ConnectJWTAsync(string securityPolicy)
{
byte[] identityToken = "fakeTokenString"u8.ToArray();
var issuedToken = new IssuedIdentityTokenHandler(Profiles.JwtUserToken, identityToken);
var userIdentity = new UserIdentity(issuedToken);
ISession session = await ClientFixture
.ConnectAsync(ServerUrl, securityPolicy, Endpoints, userIdentity)
.ConfigureAwait(false);
Assert.That(session, Is.Not.Null);
Assert.That(TokenValidator.LastIssuedToken, Is.Not.Null);
byte[] receivedToken = TokenValidator.LastIssuedToken.DecryptedTokenData!;
Assert.That(receivedToken, Is.EqualTo(identityToken));
_ = await session.CloseAsync().ConfigureAwait(false);
session.Dispose();
}
[Theory]
[Order(230)]
public async Task ReconnectJWTAsync(string securityPolicy)
{
static UserIdentity CreateUserIdentity(byte[] tokenData)
{
var issuedToken = new IssuedIdentityTokenHandler(Profiles.JwtUserToken, tokenData);
return new UserIdentity(issuedToken);
}
byte[] identityToken = "fakeTokenString"u8.ToArray();
UserIdentity userIdentity = CreateUserIdentity(identityToken);
ISession session = await ClientFixture
.ConnectAsync(ServerUrl, securityPolicy, Endpoints, userIdentity)
.ConfigureAwait(false);
Assert.That(session, Is.Not.Null);
Assert.That(TokenValidator.LastIssuedToken, Is.Not.Null);
byte[] receivedToken = TokenValidator.LastIssuedToken.DecryptedTokenData!;
Assert.That(receivedToken, Is.EqualTo(identityToken));
Array.Clear(receivedToken, 0, receivedToken.Length);
byte[] newIdentityToken = "fakeTokenStringNew"u8.ToArray();
session.RenewUserIdentity += (_, _) => CreateUserIdentity(newIdentityToken);
await session.ReconnectAsync().ConfigureAwait(false);
receivedToken = TokenValidator.LastIssuedToken.DecryptedTokenData!;
Assert.That(receivedToken, Is.EqualTo(newIdentityToken));
Array.Clear(receivedToken, 0, receivedToken.Length);
StatusCode result = await session.CloseAsync().ConfigureAwait(false);
session.Dispose();
}
[TestCase(true)]
[TestCase(false)]
[Order(240)]
public async Task TransferJWTSubscriptionUsesAuthenticatedClientUserIdAsync(
bool sameSubject)
{
const string sourceSubject = "transfer-source";
string targetSubject = sameSubject ? sourceSubject : "transfer-target";
var sourceIdentity = new UserIdentity(
new IssuedIdentityTokenHandler(
Profiles.JwtUserToken,
"source-issued-token"u8));
var targetIdentity = new UserIdentity(
new IssuedIdentityTokenHandler(
Profiles.JwtUserToken,
"target-issued-token"u8));
ISession? sourceSession = null;
ISession? targetSession = null;
Subscription? subscription = null;
try
{
TokenValidator.Subject = sourceSubject;
sourceSession = await ClientFixture
.ConnectAsync(
ServerUrl,
SecurityPolicies.Basic256Sha256,
Endpoints,
sourceIdentity)
.ConfigureAwait(false);
subscription = new Subscription(sourceSession.DefaultSubscription)
{
PublishingInterval = 1_000,
LifetimeCount = 30,
KeepAliveCount = 5,
PublishingEnabled = true
};
Assert.That(sourceSession.AddSubscription(subscription), Is.True);
await subscription.CreateAsync().ConfigureAwait(false);
sourceSession.DeleteSubscriptionsOnClose = false;
StatusCode closeStatus = await sourceSession.CloseAsync().ConfigureAwait(false);
Assert.That(StatusCode.IsGood(closeStatus), Is.True);
TokenValidator.Subject = targetSubject;
targetSession = await ClientFixture
.ConnectAsync(
ServerUrl,
SecurityPolicies.Basic256Sha256,
Endpoints,
targetIdentity)
.ConfigureAwait(false);
TransferSubscriptionsResponse response =
await targetSession.TransferSubscriptionsAsync(
null,
[subscription.Id],
false,
default).ConfigureAwait(false);
Assert.That(response.Results, Has.Count.EqualTo(1));
Assert.That(
response.Results[0].StatusCode,
sameSubject
? Is.EqualTo(StatusCodes.Good)
: Is.EqualTo(StatusCodes.BadUserAccessDenied));
}
finally
{
TokenValidator.Issuer = TokenValidatorMock.DefaultIssuer;
TokenValidator.Subject = TokenValidatorMock.DefaultSubject;
if (targetSession != null)
{
await targetSession.CloseAsync().ConfigureAwait(false);
targetSession.Dispose();
}
sourceSession?.Dispose();
subscription?.Dispose();
}
}
[Test]
[Order(240)]
public async Task ConnectMultipleSessionsAsync()
{
ConfiguredEndpoint endpoint = await ClientFixture
.GetEndpointAsync(ServerUrl, SecurityPolicies.Basic256Sha256, Endpoints)
.ConfigureAwait(false);
Assert.That(endpoint, Is.Not.Null);
ITransportChannel channel = await ClientFixture.CreateChannelAsync(endpoint, false)
.ConfigureAwait(false);
Assert.That(channel, Is.Not.Null);
ISession session1 = ClientFixture.CreateSession(channel, endpoint);
await session1.OpenAsync("Session1", null!, CancellationToken.None).ConfigureAwait(false);
ISession session2 = ClientFixture.CreateSession(channel, endpoint);
await session2.OpenAsync("Session2", null!, CancellationToken.None).ConfigureAwait(false);
await session1.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus).ConfigureAwait(false);
await session1.CloseAsync(closeChannel: false).ConfigureAwait(false);
session1.DetachChannel();
session1.Dispose();
await session2.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus).ConfigureAwait(false);
await session2.CloseAsync(closeChannel: false).ConfigureAwait(false);
session2.DetachChannel();
session2.Dispose();
//Recreate session using same channel
using ISession session3 = await ClientFixture
.SessionFactory.RecreateAsync(session1, channel)
.ConfigureAwait(false);
await session3.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus).ConfigureAwait(false);
channel.Dispose();
}
/// <summary>
/// Open a session on a channel, then reconnect (activate) the
/// same session on a new channel.
/// Close the first channel before or after the new channel is activated.
/// </summary>
[Theory]
[Order(250)]
[CancelAfter(120_000)]
public async Task ReconnectSessionOnAlternateChannelAsync(
bool closeChannel,
CancellationToken ct)
{
ServiceResultException sre;
// the endpoint to use
ConfiguredEndpoint endpoint = await ClientFixture
.GetEndpointAsync(ServerUrl, SecurityPolicies.Basic256Sha256, Endpoints)
.ConfigureAwait(false);
Assert.That(endpoint, Is.Not.Null);
// the active channel
ISession session1 = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.Basic256Sha256, Endpoints)
.ConfigureAwait(false);
Assert.That(session1, Is.Not.Null);
// test by reading a value
ServerStatusDataType value1 = await session1.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus, ct).ConfigureAwait(false);
Assert.That(value1, Is.Not.Null);
// save the channel to close it later
ITransportChannel channel1 = session1.TransportChannel;
// test case: close the channel before reconnecting
if (closeChannel)
{
session1.DetachChannel();
channel1.Dispose();
Assert.That(session1.NullableTransportChannel, Is.Null);
}
// the inactive channel
ITransportChannel channel2 =
await ClientFixture.CreateChannelAsync(endpoint, false).ConfigureAwait(false);
Assert.That(channel2, Is.Not.Null);
// activate the session on the new channel
await session1.ReconnectAsync(channel2, ct)
.ConfigureAwait(false);
// test by reading a value
ServerStatusDataType value2 = await session1.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus, ct).ConfigureAwait(false);
Assert.That(value2, Is.Not.Null);
Assert.That(value2.State, Is.EqualTo(value1.State));
if (!closeChannel)
{
// Closing channel should throw because it was disposed during reconnect
Assert.ThrowsAsync<ObjectDisposedException>(
() => channel1.CloseAsync(default).AsTask());
// Calling dispose twice will not throw.
Assert.DoesNotThrow(channel1.Dispose);
}
// test by reading a value
ServerStatusDataType value3 = await session1.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus, ct).ConfigureAwait(false);
Assert.That(value3, Is.Not.Null);
Assert.That(value3.State, Is.EqualTo(value1.State));
// close the session, keep the channel open
await session1.CloseAsync(closeChannel: false, ct)
.ConfigureAwait(false);
// cannot read using a closed session, validate the status code
sre = Assert.ThrowsAsync<ServiceResultException>(async () =>
await session1.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus, ct).ConfigureAwait(false));
Assert.That(
sre.StatusCode,
Is.EqualTo(StatusCodes.BadSessionIdInvalid),
sre.Message);
// close the channel
await channel2.CloseAsync(ct).ConfigureAwait(false);
channel2.Dispose();
// cannot read using a closed channel, validate the status code
sre = Assert.ThrowsAsync<ServiceResultException>(async () =>
await session1.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus, ct).ConfigureAwait(false));
// the Session is closed as well, so the client refuses the request
// (it would go out without authenticationToken, which is a
// session-less invocation per Part 4 §6.3.1) before it reaches the
// closed channel.
Assert.That(
sre.StatusCode,
Is.EqualTo(StatusCodes.BadSessionIdInvalid),
sre.Message);
session1.Dispose();
}
/// <summary>
/// Open a session on a channel, then reconnect (activate)
/// the same session on a new channel with saved session secrets
/// </summary>
[Test]
[Order(260)]
[TestCase(SecurityPolicies.None, true)]
[TestCase(SecurityPolicies.None, false)]
[TestCase(SecurityPolicies.Basic256Sha256, true)]
[TestCase(SecurityPolicies.Basic256Sha256, false)]
[TestCase(SecurityPolicies.RSA_DH_AesGcm, true)]
[TestCase(SecurityPolicies.RSA_DH_AesGcm, false)]
[TestCase(SecurityPolicies.RSA_DH_ChaChaPoly, true)]
[TestCase(SecurityPolicies.RSA_DH_ChaChaPoly, false)]
[TestCaseSource(nameof(ReconnectSessionOnAlternateChannelWithSavedSessionSecretsEccTestCases))]
[CancelAfter(120_000)]
public async Task ReconnectSessionOnAlternateChannelWithSavedSessionSecretsAsync(
string securityPolicy,
bool anonymous,
CancellationToken ct)
{
await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false);
ServiceResultException sre;
UserIdentity userIdentity = anonymous
? new UserIdentity()
: new UserIdentity("user1", "password"u8);
// the first channel determines the endpoint
ConfiguredEndpoint endpoint = await ClientFixture
.GetEndpointAsync(ServerUrl, securityPolicy, Endpoints)
.ConfigureAwait(false);
Assert.That(endpoint, Is.Not.Null);
UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy(
userIdentity.TokenType,
userIdentity.IssuedTokenType,
endpoint.Description.SecurityPolicyUri!)!;
if (identityPolicy == null)
{
Assert.Ignore(
$"No UserTokenPolicy found for {userIdentity.TokenType}" +
$" / {userIdentity.IssuedTokenType}");
}
// the active channel
ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity)
.ConfigureAwait(false);
Assert.That(session1, Is.Not.Null);
ServerStatusDataType value1 = await session1.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus, ct).ConfigureAwait(false);
Assert.That(value1, Is.Not.Null);
// save the session configuration
var stream = new MemoryStream();
session1.SaveSessionConfiguration(stream);
byte[] streamArray = stream.ToArray();
TestContext.Out.WriteLine($"SessionSecrets: {stream.Length} bytes");
TestContext.Out.WriteLine(Encoding.UTF8.GetString(streamArray));
// read the session configuration
var loadStream = new MemoryStream(streamArray);
var sessionConfiguration = SessionConfiguration.Create(loadStream, Telemetry);
// create the inactive channel
ITransportChannel channel2 = await ClientFixture
.CreateChannelAsync(sessionConfiguration!.ConfiguredEndpoint!, false)
.ConfigureAwait(false);
Assert.That(channel2, Is.Not.Null);
// prepare the inactive session with the new channel
ISession session2 = ClientFixture.CreateSession(
channel2,
sessionConfiguration.ConfiguredEndpoint!);
// apply the saved session configuration
bool success = session2.ApplySessionConfiguration(sessionConfiguration);
// hook callback to renew the user identity
session2.RenewUserIdentity += (_, _) => userIdentity;
// activate the session from saved session secrets on the new channel
await session2.ReconnectAsync(channel2, ct)
.ConfigureAwait(false);
// Awaited, not Thread.Sleep: blocking a pool thread inside an async
// test is what starves a small runner when fixtures run in parallel.
await Task.Delay(500, ct).ConfigureAwait(false);
Assert.That(session2.SessionId, Is.EqualTo(session1.SessionId));
ServerStatusDataType value2 = await session2.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus, ct).ConfigureAwait(false);
Assert.That(value2, Is.Not.Null);
await Task.Delay(500, ct).ConfigureAwait(false);
// cannot read using a closed channel, validate the status code
if (endpoint.EndpointUrl!.ToString()
.StartsWith(Utils.UriSchemeOpcTcp, StringComparison.Ordinal))
{
sre = Assert.ThrowsAsync<ServiceResultException>(
async () => await session1.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus, ct).ConfigureAwait(false));
Assert.That(
sre.StatusCode,
Is.EqualTo(StatusCodes.BadSecureChannelIdInvalid),
sre.Message);
}
else
{
object result = await session1.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus, ct).ConfigureAwait(false);
Assert.That(result, Is.Not.Null);
}
session1.DeleteSubscriptionsOnClose = true;
await session1.CloseAsync(1000, ct).ConfigureAwait(false);
session1?.Dispose();
session2.DeleteSubscriptionsOnClose = true;
await session2.CloseAsync(1000, ct).ConfigureAwait(false);
session2?.Dispose();
}
/// <summary>
/// Open a session on a channel using a usertokenpolicy from another endpoint, then reconnect (activate)
/// </summary>
[Test]
[Order(260)]
[TestCase(SecurityPolicies.Basic256Sha256, SecurityPolicies.Basic128Rsa15)]
public async Task ReconnectSession_ReuseUsertokenPolicyAsync(
string securityPolicy, string userTokenPolicy)
{
await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false);
await IgnoreIfPolicyNotAdvertisedAsync(userTokenPolicy).ConfigureAwait(false);
var userIdentity = new UserIdentity("user1", "password"u8);
// the first channel determines the endpoint
ConfiguredEndpoint endpoint = await ClientFixture
.GetEndpointAsync(ServerUrl, securityPolicy, Endpoints)
.ConfigureAwait(false);
Assert.That(endpoint, Is.Not.Null);
endpoint = new ConfiguredEndpoint(
null,
(EndpointDescription)endpoint.Description.MemberwiseClone(),
endpoint.Configuration);
ConfiguredEndpoint tokenPolicyEndpoint = await ClientFixture
.GetEndpointAsync(ServerUrl, userTokenPolicy, Endpoints)
.ConfigureAwait(false);
Assert.That(tokenPolicyEndpoint, Is.Not.Null);
UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy(
userIdentity.TokenType,
userIdentity.IssuedTokenType,
tokenPolicyEndpoint.Description.SecurityPolicyUri!)!;
if (identityPolicy == null)
{
Assert.Ignore(
$"No UserTokenPolicy found for {userIdentity.TokenType}" +
$" / {userIdentity.IssuedTokenType}");
}
if (identityPolicy.SecurityPolicyUri != userTokenPolicy)
{
Assert.Fail(
$"UserTokenPolicy SecurityPolicyUri {identityPolicy.SecurityPolicyUri} does not match test expected SecurityPolicyUri {userTokenPolicy}. " +
"Please fix test parameters or the test server configuration.");
}
userIdentity.PolicyId = identityPolicy.PolicyId!;
// the active channel
ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity)
.ConfigureAwait(false);
Assert.That(session1, Is.Not.Null);
try
{
await session1.ReconnectAsync(null, null).ConfigureAwait(false);
Assert.That(session1.Identity.PolicyId, Is.EqualTo(identityPolicy.PolicyId),
"User Token PolicyId needs to be preserved after reconnect.");
}
finally
{
session1.DeleteSubscriptionsOnClose = true;
await session1.CloseAsync(1000).ConfigureAwait(false);
session1?.Dispose();
}
}
/// <summary>
/// Open a session on a channel, then recreate using the session as a template,
/// verify the renewUserIdentityHandler is brought to the new session and called
/// before Session.Open
/// /// </summary>
[Test]
[Order(270)]
public async Task RecreateSessionWithRenewUserIdentityAsync()
{
var userIdentityAnonymous = new UserIdentity();
var userIdentityPW = new UserIdentity("user1", "password"u8);
// the first channel determines the endpoint
ConfiguredEndpoint endpoint = await ClientFixture
.GetEndpointAsync(ServerUrl, SecurityPolicies.Basic256Sha256, Endpoints)
.ConfigureAwait(false);
Assert.That(endpoint, Is.Not.Null);
UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy(
userIdentityAnonymous.TokenType,
userIdentityAnonymous.IssuedTokenType,
endpoint.Description.SecurityPolicyUri!)!;
if (identityPolicy == null)
{
Assert.Ignore(
$"No UserTokenPolicy found for {userIdentityAnonymous.TokenType}" +
$" / {userIdentityAnonymous.IssuedTokenType}");
}
// the active channel
ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentityAnonymous)
.ConfigureAwait(false);
Assert.That(session1, Is.Not.Null);
ServerStatusDataType value1 = await session1.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus).ConfigureAwait(false);
Assert.That(value1, Is.Not.Null);
// hook callback to renew the user identity
session1.RenewUserIdentity += (_, _) => userIdentityPW;
ISession session2 = await session1.SessionFactory
.RecreateAsync(session1)
.ConfigureAwait(false);
// create new channel
ITransportChannel channel2 = await ClientFixture
.CreateChannelAsync(session1.ConfiguredEndpoint, false)
.ConfigureAwait(false);
Assert.That(channel2, Is.Not.Null);
ISession session3 = await session1.SessionFactory
.RecreateAsync(session1, channel2)
.ConfigureAwait(false);
// validate new Session Ids are used and also UserName PW identity token is
// injected as renewed token
Assert.That(session2.SessionId, Is.Not.EqualTo(session1.SessionId));
Assert.That(session2.Identity.TokenType, Is.EqualTo(userIdentityPW.TokenType));
Assert.That(session3.SessionId, Is.Not.EqualTo(session1.SessionId));
Assert.That(session3.Identity.TokenType, Is.EqualTo(userIdentityPW.TokenType));
ServerStatusDataType value2 = await session2.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus).ConfigureAwait(false);
Assert.That(value2, Is.Not.Null);
session1.DeleteSubscriptionsOnClose = true;
await session1.CloseAsync(1000).ConfigureAwait(false);
session1?.Dispose();
session2.DeleteSubscriptionsOnClose = true;
await session2.CloseAsync(1000).ConfigureAwait(false);
session2?.Dispose();
session3.DeleteSubscriptionsOnClose = true;
await session3.CloseAsync(1000).ConfigureAwait(false);
session3?.Dispose();
}
[Test]
[Order(300)]
public async Task GetOperationLimitsTestAsync()
{
await GetOperationLimitsAsync().ConfigureAwait(false);
ValidateOperationLimit(
OperationLimits.MaxNodesPerRead,
Session.OperationLimits.MaxNodesPerRead);
ValidateOperationLimit(
OperationLimits.MaxNodesPerHistoryReadData,
Session.OperationLimits.MaxNodesPerHistoryReadData);
ValidateOperationLimit(
OperationLimits.MaxNodesPerHistoryReadEvents,
Session.OperationLimits.MaxNodesPerHistoryReadEvents);
ValidateOperationLimit(
OperationLimits.MaxNodesPerBrowse,
Session.OperationLimits.MaxNodesPerBrowse);
ValidateOperationLimit(
OperationLimits.MaxMonitoredItemsPerCall,
Session.OperationLimits.MaxMonitoredItemsPerCall);
ValidateOperationLimit(
OperationLimits.MaxNodesPerHistoryUpdateData,
Session.OperationLimits.MaxNodesPerHistoryUpdateData);
ValidateOperationLimit(
OperationLimits.MaxNodesPerHistoryUpdateEvents,
Session.OperationLimits.MaxNodesPerHistoryUpdateEvents);
ValidateOperationLimit(
OperationLimits.MaxNodesPerMethodCall,
Session.OperationLimits.MaxNodesPerMethodCall);
ValidateOperationLimit(
OperationLimits.MaxNodesPerNodeManagement,
Session.OperationLimits.MaxNodesPerNodeManagement);
ValidateOperationLimit(
OperationLimits.MaxNodesPerRegisterNodes,
Session.OperationLimits.MaxNodesPerRegisterNodes);
ValidateOperationLimit(
OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds,
Session.OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds);
ValidateOperationLimit(
OperationLimits.MaxNodesPerWrite,
Session.OperationLimits.MaxNodesPerWrite);
}
[Test]
public void ReadPublicProperties()
{
TestContext.Out.WriteLine("Identity : {0}", Session.Identity);
TestContext.Out.WriteLine("IdentityHistory : {0}", Session.IdentityHistory);
TestContext.Out.WriteLine("NamespaceUris : {0}", string.Join(", ", Session.NamespaceUris));
TestContext.Out.WriteLine("ServerUris : {0}", Session.ServerUris);
TestContext.Out.WriteLine("SystemContext : {0}", Session.SystemContext);
TestContext.Out.WriteLine("Factory : {0}", Session.Factory);
TestContext.Out.WriteLine("FilterContext : {0}", Session.FilterContext);
TestContext.Out.WriteLine("PreferredLocales : {0}", Session.PreferredLocales);
TestContext.Out.WriteLine("Subscriptions : {0}", Session.Subscriptions);
TestContext.Out.WriteLine("SubscriptionCount: {0}", Session.SubscriptionCount);
TestContext.Out.WriteLine("DefaultSubscription: {0}", Session.DefaultSubscription);
TestContext.Out.WriteLine("LastKeepAliveTime: {0}", Session.LastKeepAliveTime);
TestContext.Out
.WriteLine("LastKeepAliveTimestamp: {0}", Session.LastKeepAliveTimestamp);
TestContext.Out.WriteLine("KeepAliveInterval: {0}", Session.KeepAliveInterval);
Session.KeepAliveInterval += 1000;
TestContext.Out.WriteLine("KeepAliveInterval: {0}", Session.KeepAliveInterval);
Session.KeepAliveInterval -= 1000;
TestContext.Out.WriteLine("KeepAliveInterval: {0}", Session.KeepAliveInterval);
TestContext.Out.WriteLine("KeepAliveStopped : {0}", Session.KeepAliveStopped);
TestContext.Out
.WriteLine("OutstandingRequestCount : {0}", Session.OutstandingRequestCount);
TestContext.Out.WriteLine("DefunctRequestCount : {0}", Session.DefunctRequestCount);
TestContext.Out
.WriteLine("GoodPublishRequestCount : {0}", Session.GoodPublishRequestCount);
}
[Test]
public async Task ChangePreferredLocalesAsync()
{
// change locale
ArrayOf<string> localeCollection = ["de-de", "en-us"];
await Session.ChangePreferredLocalesAsync(localeCollection).ConfigureAwait(false);
}
[Test]
public Task ReadValueAsync()
{
// Test ReadValue
Task task1 = Session.ReadValueAsync(
VariableIds.Server_ServerRedundancy_RedundancySupport);
Task task2 = Session.ReadValueAsync(VariableIds.Server_ServerStatus);
Task task3 = Session.ReadValueAsync(VariableIds.Server_ServerStatus_BuildInfo);
return Task.WhenAll(task1, task2, task3);
}
[Test]
public async Task ReadValueTypedAsync()
{
// Test ReadValue
await Session.ReadValueAsync<int>(
VariableIds.Server_ServerRedundancy_RedundancySupport).ConfigureAwait(false);
await Session.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus).ConfigureAwait(false);
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
async () => await Session.ReadValueAsync<ServiceHost>(
VariableIds.Server_ServerStatus).ConfigureAwait(false));
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadTypeMismatch));
}
[Test]
public async Task ReadValueFromTestSimulationAsync()
{
NamespaceTable namespaceUris = Session.NamespaceUris;
var testSet = GetTestSetStatic(namespaceUris).ToList();
testSet.AddRange(GetTestSetSimulation(namespaceUris));
foreach (NodeId nodeId in testSet)
{
DataValue dataValue = await Session.ReadValueAsync(nodeId).ConfigureAwait(false);
Assert.That(dataValue.IsNull, Is.False);
Assert.That(dataValue.WrappedValue.IsNull, Is.False);
Assert.That(dataValue.SourceTimestamp, Is.Not.EqualTo(DateTime.MinValue));
Assert.That(dataValue.ServerTimestamp, Is.Not.EqualTo(DateTime.MinValue));
}
}
[Test]
public async Task ReadValuesAsync()
{
NamespaceTable namespaceUris = Session.NamespaceUris;
var testSet = GetTestSetStatic(namespaceUris).ToList();
testSet.AddRange(GetTestSetFullSimulation(namespaceUris));
(ArrayOf<DataValue> values, ArrayOf<ServiceResult> errors) =
await Session.ReadValuesAsync([.. testSet]).ConfigureAwait(false);
Assert.That(values.Count, Is.EqualTo(testSet.Count));
Assert.That(errors.Count, Is.EqualTo(testSet.Count));
}
[Test]
public async Task ReadDataTypeDefinitionAsync()
{
// Test Read a DataType Node
INode node = await Session.ReadNodeAsync(DataTypeIds.ProgramDiagnosticDataType)
.ConfigureAwait(false);
ValidateDataTypeDefinition(node);
}
[Test]
public async Task ReadDataTypeDefinition2Async()
{
// Test Read a DataType Node, the nodeclass is known
INode node = await Session
.ReadNodeAsync(DataTypeIds.ProgramDiagnosticDataType, NodeClass.DataType, false)
.ConfigureAwait(false);
ValidateDataTypeDefinition(node);
}
[Test]
public async Task ReadDataTypeDefinitionNodesAsync()
{
// Test Read a DataType Node, the nodeclass is known
(ArrayOf<Node> nodes, ArrayOf<ServiceResult> errors) = await Session
.ReadNodesAsync([DataTypeIds.ProgramDiagnosticDataType], NodeClass.DataType, false)
.ConfigureAwait(false);
Assert.That(errors.Count, Is.EqualTo(nodes.Count));
ValidateDataTypeDefinition(nodes[0]);
}
private static void ValidateDataTypeDefinition(INode node)
{
Assert.That(node, Is.Not.Null);
var dataTypeNode = (DataTypeNode)node;
Assert.That(dataTypeNode, Is.Not.Null);
ExtensionObject dataTypeDefinition = dataTypeNode.DataTypeDefinition;
Assert.That(dataTypeDefinition.IsNull, Is.False);
Assert.That(dataTypeDefinition.TryGetValue(out StructureDefinition? structureDefinition), Is.True);
Assert.That(
structureDefinition!.DefaultEncodingId,
Is.EqualTo(ObjectIds.ProgramDiagnosticDataType_Encoding_DefaultBinary));
}
[Theory]
[Order(400)]
public async Task BrowseFullAddressSpaceAsync(
string securityPolicy,
bool operationLimits = false)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
if (OperationLimits == null)
{
await GetOperationLimitsAsync().ConfigureAwait(false);
}
var requestHeader = new RequestHeader
{
Timestamp = DateTime.UtcNow,
TimeoutHint = MaxTimeout
};
// Session
ISession session;
if (securityPolicy != null)
{
session = await ClientFixture.ConnectAsync(ServerUrl, securityPolicy, Endpoints)
.ConfigureAwait(false);
if (operationLimits)
{
// disable the operation limit handler in SessionClientOperationLimits
session.OperationLimits.MaxNodesPerBrowse = 0;
}
}
else
{
session = Session;
}
var clientTestServices = new ClientTestServices(session, telemetry);
ReferenceDescriptions = await CommonTestWorkers.BrowseFullAddressSpaceWorkerAsync(
clientTestServices,
requestHeader,
operationLimits ? OperationLimits : null,
outputResult: true).ConfigureAwait(false);
if (securityPolicy != null)
{
await session.CloseAsync().ConfigureAwait(false);
session.Dispose();
}
}
[Test]
[Order(410)]
[NonParallelizable]
public async Task ReadDisplayNamesAsync()
{
if (ReferenceDescriptions.IsNull)
{
await BrowseFullAddressSpaceAsync(null!).ConfigureAwait(false);
}
ArrayOf<NodeId> nodeIds = ReferenceDescriptions
.ConvertAll(n => ExpandedNodeId.ToNodeId(n.NodeId, Session.NamespaceUris));
if (OperationLimits.MaxNodesPerRead > 0 &&
nodeIds.Count > OperationLimits.MaxNodesPerRead)
{
// force error
try
{
Session.OperationLimits.MaxNodesPerRead = 0;
ServiceResultException sre = Assert
.ThrowsAsync<ServiceResultException>(async () =>
await Session.ReadDisplayNameAsync(
nodeIds).ConfigureAwait(false));
Assert.That(
sre.StatusCode,
Is.EqualTo(StatusCodes.BadTooManyOperations));
while (nodeIds.Count > 0)
{
ArrayOf<NodeId> slice = nodeIds.Count <= OperationLimits.MaxNodesPerRead ?
nodeIds :
nodeIds[..(int)OperationLimits.MaxNodesPerRead];
(ArrayOf<string> displayNames, ArrayOf<ServiceResult> errors) =
await Session.ReadDisplayNameAsync(slice).ConfigureAwait(false);
foreach (string name in displayNames)
{
TestContext.Out.WriteLine("{0}", name);
}
nodeIds = nodeIds.Count <= OperationLimits.MaxNodesPerRead ?
default :
nodeIds[(int)OperationLimits.MaxNodesPerRead..];
}
}
finally
{
Session.OperationLimits.MaxNodesPerRead = OperationLimits.MaxNodesPerRead;
}
}
else
{
(ArrayOf<string> displayNames, ArrayOf<ServiceResult> errors) =
await Session.ReadDisplayNameAsync(nodeIds).ConfigureAwait(false);
foreach (string name in displayNames)
{
TestContext.Out.WriteLine("{0}", name);
}
}
}
[Test]
[Order(480)]
public async Task SubscriptionAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var requestHeader = new RequestHeader
{
Timestamp = DateTime.UtcNow,
TimeoutHint = MaxTimeout
};
var clientTestServices = new ClientTestServices(Session, telemetry);
await CommonTestWorkers.SubscriptionTestAsync(clientTestServices, requestHeader).ConfigureAwait(false);
}
[Test]
[Order(550)]
public async Task ReadNodeSyncAsync()
{
if (ReferenceDescriptions.IsNull)
{
await BrowseFullAddressSpaceAsync(null!).ConfigureAwait(false);
}
foreach (ReferenceDescription reference in ReferenceDescriptions[..MaxReferences].ToList())
{
var nodeId = ExpandedNodeId.ToNodeId(reference.NodeId, Session.NamespaceUris);
Node node = await Session.ReadNodeAsync(nodeId).ConfigureAwait(false);
Assert.That(node, Is.Not.Null);
TestContext.Out.WriteLine("NodeId: {0} Node: {1}", nodeId, node);
if (node is VariableNode)
{
try
{
DataValue value = await Session.ReadValueAsync(nodeId).ConfigureAwait(false);
Assert.That(value.IsNull, Is.False);
TestContext.Out.WriteLine("-- Value {0} ", value);
}
catch (ServiceResultException sre)
{
TestContext.Out.WriteLine("-- Read Value {0} ", sre.Message);
}
}
}
}
[Test]
[Order(550)]
public async Task ReadNodeAsync()
{
if (ReferenceDescriptions.IsNull)
{
await BrowseFullAddressSpaceAsync(null!).ConfigureAwait(false);
}
foreach (ReferenceDescription reference in ReferenceDescriptions[..MaxReferences].ToList())
{
var nodeId = ExpandedNodeId.ToNodeId(reference.NodeId, Session.NamespaceUris);
INode node = await Session.ReadNodeAsync(nodeId).ConfigureAwait(false);
Assert.That(node, Is.Not.Null);
TestContext.Out.WriteLine("NodeId: {0} Node: {1}", nodeId, node);
if (node is VariableNode)
{
try
{
DataValue value = await Session.ReadValueAsync(nodeId)
.ConfigureAwait(false);
Assert.That(value.IsNull, Is.False);
TestContext.Out.WriteLine("-- Value {0} ", value);
}
catch (ServiceResultException sre)
{
TestContext.Out.WriteLine("-- Read Value {0} ", sre.Message);
}
}
}
}
[Test]
[Order(560)]
[TestCase(0)]
[TestCase(MaxReferences)]
public async Task ReadNodesSyncAsync(int nodeCount)
{
if (ReferenceDescriptions.IsNull)
{
await BrowseFullAddressSpaceAsync(null!).ConfigureAwait(false);
}
ArrayOf<NodeId> nodes =
ReferenceDescriptions
[..nodeCount]
.ConvertAll(reference => ExpandedNodeId.ToNodeId(
reference.NodeId,
Session.NamespaceUris));
(ArrayOf<Node> nodeCollection, ArrayOf<ServiceResult> errors) =
await Session.ReadNodesAsync(nodes).ConfigureAwait(false);
Assert.That(nodeCollection.IsNull, Is.False);
Assert.That(errors.IsNull, Is.False);
Assert.That(nodeCollection.Count, Is.EqualTo(nodes.Count));
Assert.That(errors.Count, Is.EqualTo(nodes.Count));
(nodeCollection, errors) = await Session.ReadNodesAsync(
nodes, NodeClass.Unspecified).ConfigureAwait(false);
Assert.That(nodeCollection.IsNull, Is.False);
Assert.That(errors.IsNull, Is.False);
Assert.That(nodeCollection.Count, Is.EqualTo(nodes.Count));
Assert.That(errors.Count, Is.EqualTo(nodes.Count));
int ii = 0;
var variableNodes = new List<NodeId>();
foreach (Node node in nodeCollection.ToList())
{
Assert.That(node, Is.Not.Null);
TestContext.Out.WriteLine("NodeId: {0} Node: {1}", node.NodeId, node);
Assert.That(errors[ii], Is.EqualTo(ServiceResult.Good));
if (node is VariableNode)
{
try
{
variableNodes.Add(node.NodeId);
DataValue value =
await Session.ReadValueAsync(node.NodeId).ConfigureAwait(false);
Assert.That(value.IsNull, Is.False);
TestContext.Out.WriteLine("-- Value {0} ", value);
}
catch (ServiceResultException sre)
{
TestContext.Out.WriteLine("-- Read Value {0} ", sre.Message);
}
}
ii++;
}
(ArrayOf<DataValue> values, errors) = await Session.ReadValuesAsync(nodes)
.ConfigureAwait(false);
Assert.That(values.IsNull, Is.False);
Assert.That(values.Count, Is.EqualTo(nodes.Count));
Assert.That(errors.Count, Is.EqualTo(nodes.Count));
(values, errors) = await Session.ReadValuesAsync(variableNodes)
.ConfigureAwait(false);
Assert.That(values.IsNull, Is.False);
Assert.That(values.Count, Is.EqualTo(variableNodes.Count));
Assert.That(errors.Count, Is.EqualTo(variableNodes.Count));
}
[Test]
[Order(570)]
[TestCase(0)]
[TestCase(MaxReferences)]
public async Task ReadNodesAsync(int nodeCount)
{
if (ReferenceDescriptions.IsNull)
{
await BrowseFullAddressSpaceAsync(null!).ConfigureAwait(false);
}
ArrayOf<NodeId> nodes =
ReferenceDescriptions
.Filter(reference => reference.NodeClass == NodeClass.Variable)
[..nodeCount]
.ConvertAll(reference => ExpandedNodeId.ToNodeId(
reference.NodeId,
Session.NamespaceUris));
(ArrayOf<Node> nodeCollection, ArrayOf<ServiceResult> errors) = await Session
.ReadNodesAsync(nodes, true)
.ConfigureAwait(false);
Assert.That(nodeCollection.IsNull, Is.False);
Assert.That(errors.IsNull, Is.False);
Assert.That(nodeCollection.Count, Is.EqualTo(nodes.Count));
Assert.That(errors.Count, Is.EqualTo(nodes.Count));
(nodeCollection, errors) = await Session
.ReadNodesAsync(nodes, NodeClass.Unspecified, true)
.ConfigureAwait(false);
Assert.That(nodeCollection.IsNull, Is.False);
Assert.That(errors.IsNull, Is.False);
Assert.That(nodeCollection.Count, Is.EqualTo(nodes.Count));
Assert.That(errors.Count, Is.EqualTo(nodes.Count));
int ii = 0;
var variableNodes = new List<NodeId>();
foreach (Node node in nodeCollection.ToList())
{
Assert.That(node, Is.Not.Null);
Assert.That(errors[ii], Is.EqualTo(ServiceResult.Good));
TestContext.Out.WriteLine("NodeId: {0} Node: {1}", node.NodeId, node);
if (node is VariableNode)
{
try
{
variableNodes.Add(node.NodeId);
DataValue value = await Session.ReadValueAsync(node.NodeId)
.ConfigureAwait(false);
Assert.That(value.IsNull, Is.False);
TestContext.Out.WriteLine("-- Value {0} ", value);
}
catch (ServiceResultException sre)
{
TestContext.Out.WriteLine("-- Read Value {0} ", sre.Message);
}
}
ii++;
}
(ArrayOf<DataValue> values, errors) = await Session.ReadValuesAsync(nodes).ConfigureAwait(false);
Assert.That(values.IsNull, Is.False);
Assert.That(errors.IsNull, Is.False);
Assert.That(values.Count, Is.EqualTo(nodes.Count));
Assert.That(errors.Count, Is.EqualTo(nodes.Count));
(values, errors) = await Session.ReadValuesAsync(variableNodes).ConfigureAwait(false);
Assert.That(values.IsNull, Is.False);
Assert.That(errors.IsNull, Is.False);
Assert.That(values.Count, Is.EqualTo(variableNodes.Count));
Assert.That(errors.Count, Is.EqualTo(variableNodes.Count));
}
[Test]
[Order(620)]
public async Task ReadAvailableEncodingsAsync()
{
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
async () => await Session.ReadAvailableEncodingsAsync(DataTypeIds.BaseDataType)
.ConfigureAwait(false));
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadNodeIdInvalid));
ArrayOf<ReferenceDescription> encoding = await Session.ReadAvailableEncodingsAsync(
VariableIds.Server_ServerStatus_CurrentTime).ConfigureAwait(false);
Assert.That(encoding.IsNull, Is.False);
Assert.That(encoding.Count, Is.Zero);
}
/// <summary>
/// Transfer the subscription using the native service calls, not the client SDK layer.
/// </summary>
/// <remarks>
/// Create a subscription with a monitored item using the native service calls.
/// Create a secondary Session.
/// </remarks>
[Theory]
[Order(800)]
[NonParallelizable]
public async Task TransferSubscriptionNativeAsync(bool sendInitialData)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ISession? transferSession = null;
try
{
var requestHeader = new RequestHeader
{
Timestamp = DateTime.UtcNow,
TimeoutHint = MaxTimeout
};
// to validate the behavior of the sendInitialValue flag,
// use a static variable to avoid sampled notifications in publish requests
NamespaceTable namespaceUris = Session.NamespaceUris;
NodeId[] testSet =
[
.. CommonTestWorkers.NodeIdTestSetStatic
.Select(n => ExpandedNodeId.ToNodeId(n, namespaceUris))
];
var clientTestServices = new ClientTestServices(Session, telemetry);
ArrayOf<uint> subscriptionIds = await CommonTestWorkers.CreateSubscriptionForTransferAsync(
clientTestServices,
requestHeader,
testSet,
0,
-1).ConfigureAwait(false);
TestContext.Out.WriteLine("Transfer SubscriptionIds: {0}", subscriptionIds[0]);
transferSession = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.Basic256Sha256, Endpoints)
.ConfigureAwait(false);
Assert.That(transferSession.SessionId, Is.Not.EqualTo(Session.SessionId));
requestHeader = new RequestHeader
{
Timestamp = DateTime.UtcNow,
TimeoutHint = MaxTimeout
};
var transferTestServices = new ClientTestServices(transferSession, telemetry);
await CommonTestWorkers.TransferSubscriptionTestAsync(
transferTestServices,
requestHeader,
subscriptionIds,
sendInitialData,
false).ConfigureAwait(false);
// verify the notification of message transfer
requestHeader = new RequestHeader
{
Timestamp = DateTime.UtcNow,
TimeoutHint = MaxTimeout
};
await CommonTestWorkers.VerifySubscriptionTransferredAsync(
clientTestServices,
requestHeader,
subscriptionIds,
true).ConfigureAwait(false);
await transferSession.CloseAsync().ConfigureAwait(false);
}
finally
{
transferSession?.Dispose();
}
}
/// <summary>
/// Test class for testing protected methods in TraceableRequestHeaderClientSession
/// </summary>
public class TestableTraceableRequestHeaderClientSession : TraceableRequestHeaderClientSession
{
public TestableTraceableRequestHeaderClientSession(
ITransportChannel channel,
ApplicationConfiguration configuration,
ConfiguredEndpoint endpoint)
: base(channel, configuration, endpoint, null!)
{
ActivityTraceFlags = ClientTraceFlags.Traces;
}
/// <summary>
/// Expose the protected method for testing
/// </summary>
public void TestableUpdateRequestHeader(IServiceRequest request, bool useDefaults)
{
base.UpdateRequestHeader(request, useDefaults);
}
}
private static ActivityContext TestExtractActivityContextFromParameters(
AdditionalParametersType parameters)
{
if (parameters == null)
{
return default;
}
foreach (KeyValuePair item in parameters.Parameters)
{
if (item.Key != "SpanContext")
{
continue;
}
if (item.Value.TryGetStructure<SpanContextDataType>(out SpanContextDataType? spanContext))
{
#if NET8_0_OR_GREATER
Span<byte> spanIdBytes = stackalloc byte[8];
Span<byte> traceIdBytes = stackalloc byte[16];
((Guid)spanContext.TraceId).TryWriteBytes(traceIdBytes);
BitConverter.TryWriteBytes(spanIdBytes, spanContext.SpanId);
#else
byte[] spanIdBytes = BitConverter.GetBytes(spanContext.SpanId);
byte[] traceIdBytes = spanContext.TraceId.ToByteArray();
#endif
var traceId = ActivityTraceId.CreateFromBytes(traceIdBytes);
var spanId = ActivitySpanId.CreateFromBytes(spanIdBytes);
return new ActivityContext(traceId, spanId, ActivityTraceFlags.None);
}
}
// no traceparent header found
return default;
}
[Test]
[Order(900)]
public async Task ClientTestRequestHeaderUpdateAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
using Activity rootActivity = new Activity("Test_Activity_Root")
{
ActivityTraceFlags = ActivityTraceFlags.Recorded
}.Start();
var activityListener = new ActivityListener
{
ShouldListenTo = _ => true,
Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded
};
ActivitySource.AddActivityListener(activityListener);
using (Activity activity = new ActivitySource("TestActivitySource").StartActivity(
"Test_Activity")!)
{
if (activity != null && activity.Id != null)
{
ConfiguredEndpoint endpoint = await ClientFixture
.GetEndpointAsync(ServerUrl, SecurityPolicies.Basic256Sha256, Endpoints)
.ConfigureAwait(false);
Assert.That(endpoint, Is.Not.Null);
// Mock the channel and session
var channelMock = new Mock<ITransportChannel>();
var messageContext = ServiceMessageContext.Create(telemetry);
channelMock.Setup(mock => mock.MessageContext).Returns(messageContext);
var testableTraceableRequestHeaderClientSession
= new TestableTraceableRequestHeaderClientSession(
channelMock.Object,
ClientFixture.Config,
endpoint);
var request = new CreateSessionRequest { RequestHeader = new RequestHeader() };
// Mock call TestableUpdateRequestHeader() to simulate the header update
testableTraceableRequestHeaderClientSession.TestableUpdateRequestHeader(
request,
true);
// Get the AdditionalHeader from the request
ExtensionObject additionalHeader = request.RequestHeader.AdditionalHeader;
Assert.That(additionalHeader.IsNull, Is.False);
Assert.That(additionalHeader.TryGetValue(out AdditionalParametersType? additionalParams), Is.True);
// Simulate extraction
ActivityContext extractedContext = TestExtractActivityContextFromParameters(additionalParams!);
// Verify that the trace context is propagated.
Assert.That(extractedContext.TraceId, Is.EqualTo(activity.TraceId));
Assert.That(extractedContext.SpanId, Is.EqualTo(activity.SpanId));
TestContext.Out.WriteLine(
$"Activity TraceId: {activity.TraceId}, Activity SpanId: {activity.SpanId}");
TestContext.Out.WriteLine(
$"Extracted TraceId: {extractedContext.TraceId}, Extracted SpanId: {extractedContext.SpanId}");
}
}
rootActivity.Stop();
}
/// <summary>
/// Read BuildInfo and ensure the values in the structure are the same as in the properties.
/// </summary>
[Test]
[Order(10000)]
public async Task ReadBuildInfoAsync()
{
ArrayOf<NodeId> nodes =
[
VariableIds.Server_ServerStatus_BuildInfo,
VariableIds.Server_ServerStatus_BuildInfo_ProductName,
VariableIds.Server_ServerStatus_BuildInfo_ProductUri,
VariableIds.Server_ServerStatus_BuildInfo_ManufacturerName,
VariableIds.Server_ServerStatus_BuildInfo_SoftwareVersion,
VariableIds.Server_ServerStatus_BuildInfo_BuildNumber,
VariableIds.Server_ServerStatus_BuildInfo_BuildDate
];
(ArrayOf<Node> nodeCollection, ArrayOf<ServiceResult> errors) =
await Session.ReadNodesAsync(nodes).ConfigureAwait(false);
Assert.That(nodeCollection.IsNull, Is.False);
Assert.That(errors.IsNull, Is.False);
Assert.That(nodeCollection.Count, Is.EqualTo(nodes.Count));
Assert.That(errors.Count, Is.EqualTo(nodes.Count));
(ArrayOf<DataValue> values, ArrayOf<ServiceResult> errors2) =
await Session.ReadValuesAsync(nodes).ConfigureAwait(false);
Assert.That(values.IsNull, Is.False);
Assert.That(errors2.IsNull, Is.False);
Assert.That(values.Count, Is.EqualTo(nodes.Count));
Assert.That(errors2.Count, Is.EqualTo(nodes.Count));
ArrayOf<VariableNode> variableNodes = nodeCollection.ConvertAll(n => (VariableNode)n);
Assert.That(variableNodes.IsNull, Is.False);
// test build info contains the equal values as the properties
(values[0].WrappedValue.TryGetValue(out ExtensionObject eo) ? eo : default)
.TryGetValue(out BuildInfo? buildInfo);
Assert.That(buildInfo, Is.Not.Null);
Assert.That((string)values[1].WrappedValue, Is.EqualTo(buildInfo.ProductName));
Assert.That((string)values[2].WrappedValue, Is.EqualTo(buildInfo.ProductUri));
Assert.That((string)values[3].WrappedValue, Is.EqualTo(buildInfo.ManufacturerName));
Assert.That((string)values[4].WrappedValue, Is.EqualTo(buildInfo.SoftwareVersion));
Assert.That((string)values[5].WrappedValue, Is.EqualTo(buildInfo.BuildNumber));
Assert.That((DateTimeUtc)values[6].WrappedValue, Is.EqualTo(buildInfo.BuildDate));
}
/// <summary>
/// Open a session on a channel using ECC encrypted UserIdentityToken
/// </summary>
[Test]
[Combinatorial]
[Order(10100)]
public async Task OpenSessionECCUserNamePwdIdentityTokenAsync(
[ValueSource(nameof(SupportedEccPolicies))] string securityPolicy)
{
IgnoreUnsupportedBrainpoolOnMacOs(securityPolicy);
await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false);
var userIdentity = new UserIdentity("user1", "password"u8);
// the first channel determines the endpoint
ConfiguredEndpoint endpoint = await ClientFixture
.GetEndpointAsync(ServerUrl, securityPolicy, Endpoints)
.ConfigureAwait(false);
Assert.NotNull(endpoint);
UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy(
userIdentity.TokenType,
userIdentity.IssuedTokenType,
endpoint.Description.SecurityPolicyUri!)!;
if (identityPolicy == null)
{
Assert.Ignore(
$"No UserTokenPolicy found for {userIdentity.TokenType}" +
$" / {userIdentity.IssuedTokenType}");
}
// the active channel
using ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity)
.ConfigureAwait(false);
Assert.NotNull(session1);
ServerStatusDataType value1 =
await session1.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus).ConfigureAwait(false);
Assert.NotNull(value1);
}
/// <summary>
/// Open a session on a channel using ECC encrypted IssuedIdentityToken
/// </summary>
[Test]
[Combinatorial]
[Order(10200)]
public async Task OpenSessionECCIssuedIdentityTokenAsync(
[ValueSource(nameof(SupportedEccPolicies))] string securityPolicy)
{
IgnoreUnsupportedBrainpoolOnMacOs(securityPolicy);
await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false);
const string identityToken = "fakeTokenString";
var issuedToken = new IssuedIdentityTokenHandler(
Profiles.JwtUserToken,
Encoding.UTF8.GetBytes(identityToken));
var userIdentity = new UserIdentity(issuedToken);
// the first channel determines the endpoint
ConfiguredEndpoint endpoint = await ClientFixture
.GetEndpointAsync(ServerUrl, securityPolicy, Endpoints)
.ConfigureAwait(false);
Assert.NotNull(endpoint);
UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy(
userIdentity.TokenType,
userIdentity.IssuedTokenType,
securityPolicy)!;
if (identityPolicy == null)
{
Assert.Ignore(
$"No UserTokenPolicy found for {userIdentity.TokenType}" +
$" / {userIdentity.IssuedTokenType}");
}
// the active channel
using ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity)
.ConfigureAwait(false);
Assert.NotNull(session1);
ServerStatusDataType value1 = await session1.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus).ConfigureAwait(false);
Assert.NotNull(value1);
}
/// <summary>
/// Open a session on a channel using ECC encrypted UserCertificateIdentityToken
/// </summary>
[Test]
[Combinatorial]
[Order(10300)]
public async Task OpenSessionECCUserCertIdentityTokenAsync(
[ValueSource(nameof(SupportedEccX509Policies))] string securityPolicy)
{
IgnoreUnsupportedBrainpoolOnMacOs(securityPolicy);
await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false);
var eccCurveHashPairs = new ECCurveHashPairCollection
{
{ ECCurve.NamedCurves.nistP256, HashAlgorithmName.SHA256 },
{ ECCurve.NamedCurves.nistP384, HashAlgorithmName.SHA384 }
};
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
eccCurveHashPairs.AddRange(
new ECCurveHashPairCollection
{
{ ECCurve.NamedCurves.brainpoolP256r1, HashAlgorithmName.SHA256 },
{ ECCurve.NamedCurves.brainpoolP384r1, HashAlgorithmName.SHA384 }
});
}
foreach (ECCurveHashPair eccurveHashPair in eccCurveHashPairs)
{
string? extractedFriendlyNamae = null;
string[] friendlyNameContext = securityPolicy.Split('_');
if (friendlyNameContext.Length > 1)
{
extractedFriendlyNamae = friendlyNameContext[1];
}
if (eccurveHashPair.Curve.Oid.FriendlyName!
.Contains(extractedFriendlyNamae!, StringComparison.Ordinal))
{
using Certificate cert = CertificateBuilder
.Create("CN=Client Test ECC Subject, O=OPC Foundation")
.SetECCurve(eccurveHashPair.Curve)
.CreateForECDsa();
using var inProcProvider = new InProcessCertificateProvider(cert);
var certIdentifier = new CertificateIdentifier
{
Thumbprint = cert.Thumbprint,
SubjectName = cert.Subject
};
var userIdentity = new UserIdentity(
new X509IdentityTokenHandler(
certIdentifier,
new CertificatePasswordProvider(),
inProcProvider));
// the first channel determines the endpoint
ConfiguredEndpoint endpoint = await ClientFixture
.GetEndpointAsync(ServerUrl, securityPolicy, Endpoints)
.ConfigureAwait(false);
Assert.That(endpoint, Is.Not.Null);
UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy(
userIdentity.TokenType,
userIdentity.IssuedTokenType,
endpoint.Description.SecurityPolicyUri!)!;
if (identityPolicy == null)
{
Assert.Ignore(
$"No UserTokenPolicy found for {userIdentity.TokenType}" +
$" / {userIdentity.IssuedTokenType}");
}
// the active channel
using ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity)
.ConfigureAwait(false);
Assert.That(session1, Is.Not.Null);
ServerStatusDataType value1 =
await session1.ReadValueAsync<ServerStatusDataType>(
VariableIds.Server_ServerStatus).ConfigureAwait(false);
Assert.That(value1, Is.Not.Null);
}
}
}
/// <summary>
/// Happy SetSubscriptionDurable
/// </summary>
[Test]
[Order(11000)]
public async Task SetSubscriptionDurableSuccessAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
const uint expectedRevised = 5;
ArrayOf<Variant> outputParameters = [expectedRevised];
var sessionMock = new Mock<ISession>();
sessionMock
.Setup(mock => mock.CallAsync(
It.IsAny<RequestHeader>(),
It.Is<ArrayOf<CallMethodRequest>>(c => c.HasArgsOfType(typeof(uint), typeof(uint))),
It.IsAny<CancellationToken>()))
.ReturnsAsync(outputParameters.ToResponse());
using var subscription = new Subscription(telemetry) { Session = sessionMock.Object };
(bool result, uint revised) =
await subscription.SetSubscriptionDurableAsync(1).ConfigureAwait(false);
if (result)
{
Assert.That(revised, Is.EqualTo(expectedRevised));
}
else
{
Assert.Fail("Unexpected Error in SetSubscriptionDurable");
}
}
/// <summary>
/// SetSubscriptionDurable Typical Failure
/// </summary>
[Test]
[Order(11010)]
public async Task SetSubscriptionDurableExceptionAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var sessionMock = new Mock<ISession>();
sessionMock
.Setup(mock => mock.CallAsync(
It.IsAny<RequestHeader>(),
It.Is<ArrayOf<CallMethodRequest>>(c => c.HasArgsOfType(typeof(uint), typeof(uint))),
It.IsAny<CancellationToken>()))
.ThrowsAsync(new ServiceResultException(StatusCodes.BadSubscriptionIdInvalid));
using var subscription = new Subscription(telemetry) { Session = sessionMock.Object };
(bool result, uint revised) =
await subscription.SetSubscriptionDurableAsync(1).ConfigureAwait(false);
Assert.That(result, Is.False);
}
/// <summary>
/// SetSubscriptionDurable No Output Parameters
/// Not an expected case
/// </summary>
[Test]
[Order(11020)]
public async Task SetSubscriptionDurableNoOutputParametersAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ArrayOf<Variant> outputParameters = [];
var sessionMock = new Mock<ISession>();
sessionMock
.Setup(mock => mock.CallAsync(
It.IsAny<RequestHeader>(),
It.Is<ArrayOf<CallMethodRequest>>(c => c.HasArgsOfType(typeof(uint), typeof(uint))),
It.IsAny<CancellationToken>()))
.ReturnsAsync(outputParameters.ToResponse());
using var subscription = new Subscription(telemetry) { Session = sessionMock.Object };
(bool result, uint revised) =
await subscription.SetSubscriptionDurableAsync(1).ConfigureAwait(false);
Assert.That(result, Is.False);
}
/// <summary>
/// SetSubscriptionDurable No Output Parameters
/// Not an expected case
/// </summary>
[Test]
[Order(11030)]
public async Task SetSubscriptionDurableNullOutputParametersAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ArrayOf<Variant> outputParameters = [];
var sessionMock = new Mock<ISession>();
sessionMock
.Setup(mock => mock.CallAsync(
It.IsAny<RequestHeader>(),
It.Is<ArrayOf<CallMethodRequest>>(c => c.HasArgsOfType(typeof(uint), typeof(uint))),
It.IsAny<CancellationToken>()))
.ReturnsAsync(outputParameters.ToResponse());
using var subscription = new Subscription(telemetry) { Session = sessionMock.Object };
(bool result, uint revised) =
await subscription.SetSubscriptionDurableAsync(1).ConfigureAwait(false);
Assert.That(result, Is.False);
}
/// <summary>
/// SetSubscriptionDurable with in invalid number of Output Parameters
/// Not an expected case
/// </summary>
[Test]
[Order(11040)]
public async Task SetSubscriptionDurableTooManyOutputParametersAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
const uint expectedRevised = 5;
ArrayOf<Variant> outputParameters = [expectedRevised, expectedRevised];
var sessionMock = new Mock<ISession>();
sessionMock
.Setup(mock => mock.CallAsync(
It.IsAny<RequestHeader>(),
It.Is<ArrayOf<CallMethodRequest>>(c => c.HasArgsOfType(typeof(uint), typeof(uint))),
It.IsAny<CancellationToken>()))
.ReturnsAsync(outputParameters.ToResponse());
using var subscription = new Subscription(telemetry) { Session = sessionMock.Object };
(bool result, uint revised) =
await subscription.SetSubscriptionDurableAsync(1).ConfigureAwait(false);
Assert.That(result, Is.False);
}
/// <summary>
/// GetMonitoredItems Success Case
/// </summary>
[Test]
[Order(11100)]
public async Task GetMonitoredItemsSuccessAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ArrayOf<Variant> outputParameters =
[
Variant.From(new uint[] { 1, 2, 3, 4, 5 }),
Variant.From(new uint[] { 6, 7, 8, 9, 10 })
];
var sessionMock = new Mock<ISession>();
sessionMock
.Setup(mock => mock.CallAsync(
It.IsAny<RequestHeader>(),
It.Is<ArrayOf<CallMethodRequest>>(c => c.HasArgsOfType(typeof(uint))),
It.IsAny<CancellationToken>()))
.ReturnsAsync(outputParameters.ToResponse());
using var subscription = new Subscription(telemetry) { Session = sessionMock.Object };
(bool success, ArrayOf<uint> serverHandles, ArrayOf<uint> clientHandles) =
await subscription.GetMonitoredItemsAsync().ConfigureAwait(false);
Assert.That(success, Is.True);
Assert.That(serverHandles.Count, Is.EqualTo(5));
Assert.That(clientHandles.Count, Is.EqualTo(5));
}
/// <summary>
/// GetMonitoredItems Error Case
/// </summary>
[Test]
[Order(11110)]
public async Task GetMonitoredItemsExceptionAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var sessionMock = new Mock<ISession>();
sessionMock
.Setup(mock => mock.CallAsync(
It.IsAny<RequestHeader>(),
It.Is<ArrayOf<CallMethodRequest>>(c => c.HasArgsOfType(typeof(uint))),
It.IsAny<CancellationToken>()))
.ThrowsAsync(new ServiceResultException(StatusCodes.BadSubscriptionIdInvalid));
using var subscription = new Subscription(telemetry) { Session = sessionMock.Object };
(bool success, ArrayOf<uint> serverHandles, ArrayOf<uint> clientHandles) =
await subscription.GetMonitoredItemsAsync().ConfigureAwait(false);
Assert.That(success, Is.False);
}
/// <summary>
/// GetMonitoredItems No Output Parameters
/// Not an expected case
/// </summary>
[Test]
[Order(11120)]
public async Task GetMonitoredItemsNoOutputParametersAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ArrayOf<Variant> outputParameters = [];
var sessionMock = new Mock<ISession>();
sessionMock
.Setup(mock => mock.CallAsync(
It.IsAny<RequestHeader>(),
It.Is<ArrayOf<CallMethodRequest>>(c => c.HasArgsOfType(typeof(uint))),
It.IsAny<CancellationToken>()))
.ReturnsAsync(outputParameters.ToResponse());
using var subscription = new Subscription(telemetry) { Session = sessionMock.Object };
(bool success, ArrayOf<uint> serverHandles, ArrayOf<uint> clientHandles) =
await subscription.GetMonitoredItemsAsync().ConfigureAwait(false);
Assert.That(success, Is.False);
}
/// <summary>
/// GetMonitoredItems Null Output Parameters
/// Not an expected case
/// </summary>
[Test]
[Order(11130)]
public async Task GetMonitoredItemsNullOutputParametersAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ArrayOf<Variant> outputParameters = default;
var sessionMock = new Mock<ISession>();
sessionMock
.Setup(mock => mock.CallAsync(
It.IsAny<RequestHeader>(),
It.Is<ArrayOf<CallMethodRequest>>(c => c.HasArgsOfType(typeof(uint))),
It.IsAny<CancellationToken>()))
.ReturnsAsync(outputParameters.ToResponse());
using var subscription = new Subscription(telemetry) { Session = sessionMock.Object };
(bool success, ArrayOf<uint> serverHandles, ArrayOf<uint> clientHandles) =
await subscription.GetMonitoredItemsAsync().ConfigureAwait(false);
Assert.That(success, Is.False);
}
/// <summary>
/// GetMonitoredItems invalid number of Output Parameters
/// Not an expected case
/// </summary>
[Test]
[Order(11140)]
public async Task GetMonitoredItemsTooManyOutputParametersAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ArrayOf<Variant> outputParameters =
[
Variant.From(new uint[] { 1, 2, 3, 4, 5 }),
Variant.From(new uint[] { 6, 7, 8, 9, 10 }),
Variant.From(new uint[] { 11, 12, 13, 14, 15 })
];
var sessionMock = new Mock<ISession>();
sessionMock
.Setup(mock => mock.CallAsync(
It.IsAny<RequestHeader>(),
It.Is<ArrayOf<CallMethodRequest>>(c => c.HasArgsOfType(typeof(uint))),
It.IsAny<CancellationToken>()))
.ReturnsAsync(outputParameters.ToResponse());
using var subscription = new Subscription(telemetry) { Session = sessionMock.Object };
(bool success, ArrayOf<uint> serverHandles, ArrayOf<uint> clientHandles) =
await subscription.GetMonitoredItemsAsync().ConfigureAwait(false);
Assert.That(success, Is.False);
}
/// <summary>
/// Benchmark wrapper for browse tests.
/// </summary>
[Benchmark]
public async Task BrowseFullAddressSpaceBenchmarkAsync()
{
await BrowseFullAddressSpaceAsync(null!).ConfigureAwait(false);
}
private static void ValidateOperationLimit(uint serverLimit, uint clientLimit)
{
if (serverLimit != 0)
{
Assert.That(serverLimit, Is.GreaterThanOrEqualTo(clientLimit));
Assert.That(clientLimit, Is.Not.Zero);
}
}
private static void IgnoreUnsupportedBrainpoolOnMacOs(string securityPolicyUri)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) &&
(securityPolicyUri.Contains("ECC_brainpoolP256r1", StringComparison.Ordinal) ||
securityPolicyUri.Contains("ECC_brainpoolP384r1", StringComparison.Ordinal)))
{
Assert.Ignore("Brainpool curve is not supported on Mac OS.");
}
}
}
}