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

1591 lines
64 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ========================================================================
* 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.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using Opc.Ua.Client;
using Opc.Ua.Client.TestFramework;
namespace Opc.Ua.Sessions.Tests
{
/// <summary>
/// compliance tests for Session Service Set – base session
/// lifecycle operations.
/// </summary>
[TestFixture]
[Category("Conformance")]
[Category("SessionBase")]
public class SessionBaseTests : TestFixture
{
[Test]
public void CreateSessionWithSpecificName()
{
Assert.That(Session.SessionName, Is.Not.Null.And.Not.Empty);
}
[Test]
public void CreateSessionWithRequestedTimeout()
{
Assert.That(Session.SessionTimeout, Is.GreaterThan(0));
}
[Test]
public async Task ReadSessionDiagnosticsArrayFindOurSessionAsync()
{
DataValue result = await ReadNodeValueAsync(
VariableIds
.Server_ServerDiagnostics_ServerDiagnosticsSummary_CurrentSessionCount)
.ConfigureAwait(false);
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
uint count = result.WrappedValue.GetUInt32();
Assert.That(count, Is.GreaterThanOrEqualTo(1u));
}
[Test]
public async Task ActivateSessionWithAnonymousIdentityAsync()
{
ISession additionalSession = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
try
{
Assert.That(additionalSession.Connected, Is.True);
Assert.That(additionalSession.SessionId, Is.Not.Null);
}
finally
{
await additionalSession.CloseAsync(5000, true)
.ConfigureAwait(false);
additionalSession.Dispose();
}
}
[Test]
public async Task CloseSessionWithDeleteSubscriptionsTrueAsync()
{
ISession additionalSession = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
Assert.That(additionalSession.Connected, Is.True);
await additionalSession.CloseAsync(5000, true)
.ConfigureAwait(false);
additionalSession.Dispose();
}
[Test]
public async Task CloseSessionWithDeleteSubscriptionsFalseAsync()
{
ISession additionalSession = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
Assert.That(additionalSession.Connected, Is.True);
await additionalSession.CloseAsync(5000, false)
.ConfigureAwait(false);
additionalSession.Dispose();
}
[Test]
public async Task SessionKeepaliveVerifySessionStaysActiveAsync()
{
Assert.That(Session.Connected, Is.True);
await Task.Delay(500).ConfigureAwait(false);
Assert.That(Session.Connected, Is.True);
}
[Test]
public void VerifySessionEndpointDescription()
{
Assert.That(Session.Endpoint, Is.Not.Null);
Assert.That(
Session.Endpoint.EndpointUrl, Is.Not.Null.And.Not.Empty);
}
[Test]
public void VerifySessionServerCertificate()
{
Assert.That(Session.Endpoint, Is.Not.Null);
// With SecurityMode None the certificate may or may not be set.
// Just verify the endpoint itself is accessible.
Assert.That(
Session.Endpoint.SecurityMode,
Is.EqualTo(MessageSecurityMode.None));
}
[Test]
public void ReadMaxResponseMessageSize()
{
Assert.That(Session.Endpoint, Is.Not.Null);
Assert.That(
Session.Endpoint.TransportProfileUri,
Is.Not.Null.And.Not.Empty);
}
[Test]
public async Task CreateAndVerifyMultipleSessionsAsync()
{
ISession session1 = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
ISession session2 = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
try
{
Assert.That(session1.SessionId, Is.Not.Null);
Assert.That(session2.SessionId, Is.Not.Null);
Assert.That(
session1.SessionId,
Is.Not.EqualTo(session2.SessionId));
}
finally
{
await session1.CloseAsync(5000, true)
.ConfigureAwait(false);
session1.Dispose();
await session2.CloseAsync(5000, true)
.ConfigureAwait(false);
session2.Dispose();
}
}
[Test]
public void SessionIdentityToken()
{
Assert.That(Session.Identity, Is.Not.Null);
}
[Test]
public async Task CreateSessionWithEmptyNameAsync()
{
ISession additionalSession = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
try
{
Assert.That(additionalSession.SessionId, Is.Not.Null);
}
finally
{
await additionalSession.CloseAsync(5000, true)
.ConfigureAwait(false);
additionalSession.Dispose();
}
}
[Test]
public async Task CreateSessionWithLongNameAsync()
{
ISession additionalSession = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
try
{
Assert.That(additionalSession.Connected, Is.True);
Assert.That(additionalSession.SessionId, Is.Not.Null);
}
finally
{
await additionalSession.CloseAsync(5000, true)
.ConfigureAwait(false);
additionalSession.Dispose();
}
}
[Test]
public void SessionTimeoutIsRevisedByServer()
{
Assert.That(Session.SessionTimeout, Is.GreaterThan(0));
}
[Test]
public async Task ReadSessionDiagnosticsCurrentSubscriptionsCountAsync()
{
DataValue result = await ReadNodeValueAsync(
VariableIds
.Server_ServerDiagnostics_ServerDiagnosticsSummary_CurrentSubscriptionCount)
.ConfigureAwait(false);
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
int count = result.WrappedValue.GetInt32();
Assert.That(count, Is.GreaterThanOrEqualTo(0));
}
[Test]
public async Task ReadSessionDiagnosticsCurrentSessionCountAsync()
{
DataValue result = await ReadNodeValueAsync(
VariableIds
.Server_ServerDiagnostics_ServerDiagnosticsSummary_CurrentSessionCount)
.ConfigureAwait(false);
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
uint count = result.WrappedValue.GetUInt32();
Assert.That(count, Is.GreaterThanOrEqualTo(1u));
}
[Test]
public async Task ReadSessionSecurityDiagnosticsAsync()
{
// OPC UA well-known NodeId 2162 = SecurityRejectedSessionCount
DataValue result = await ReadNodeValueAsync(
new NodeId(2162))
.ConfigureAwait(false);
if (!StatusCode.IsGood(result.StatusCode))
{
Assert.Ignore(
"Server does not support SecurityRejectedSessionCount diagnostic.");
}
int count = result.WrappedValue.GetInt32();
Assert.That(count, Is.GreaterThanOrEqualTo(0));
}
[Test]
public async Task ActivateMultipleTimesOnSameSessionAsync()
{
ISession additionalSession = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
try
{
NodeId originalId = additionalSession.SessionId;
Assert.That(originalId, Is.Not.Null);
DataValue result = await ReadNodeValueAsync(
VariableIds.Server_ServerStatus_State)
.ConfigureAwait(false);
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
Assert.That(
additionalSession.SessionId,
Is.EqualTo(originalId));
}
finally
{
await additionalSession.CloseAsync(5000, true)
.ConfigureAwait(false);
additionalSession.Dispose();
}
}
[Test]
public async Task CreateSessionVerifySessionIdIsUniqueAsync()
{
ISession session1 = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
ISession session2 = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
try
{
Assert.That(session1.SessionId, Is.Not.Null);
Assert.That(session2.SessionId, Is.Not.Null);
Assert.That(
session1.SessionId,
Is.Not.EqualTo(session2.SessionId));
}
finally
{
await session1.CloseAsync(5000, true)
.ConfigureAwait(false);
session1.Dispose();
await session2.CloseAsync(5000, true)
.ConfigureAwait(false);
session2.Dispose();
}
}
[Test]
public void VerifySessionPreferredLocales()
{
Assert.That(Session, Is.Not.Null);
Assert.That(Session.Connected, Is.True);
}
[Test]
public async Task ReadSessionDiagnosticsCumulatedSessionCountAsync()
{
DataValue result = await ReadNodeValueAsync(
VariableIds
.Server_ServerDiagnostics_ServerDiagnosticsSummary_CumulatedSessionCount)
.ConfigureAwait(false);
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
uint count = result.WrappedValue.GetUInt32();
Assert.That(count, Is.GreaterThanOrEqualTo(1u));
}
[Test]
public async Task CreateSessionAndReadServerStateAsync()
{
ISession additionalSession = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
try
{
Assert.That(additionalSession.Connected, Is.True);
DataValue result = await ReadNodeValueAsync(
VariableIds.Server_ServerStatus_State)
.ConfigureAwait(false);
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
int state = result.WrappedValue.GetInt32();
Assert.That(
state,
Is.EqualTo((int)ServerState.Running));
}
finally
{
await additionalSession.CloseAsync(5000, true)
.ConfigureAwait(false);
additionalSession.Dispose();
}
}
[Test]
public async Task CloseSessionAndVerifyDisconnectedAsync()
{
ISession additionalSession = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
Assert.That(additionalSession.Connected, Is.True);
await additionalSession.CloseAsync(5000, true)
.ConfigureAwait(false);
Assert.That(additionalSession.Connected, Is.False);
additionalSession.Dispose();
}
[Test]
public void SessionEndpointHasTransportProfileUri()
{
Assert.That(Session.Endpoint, Is.Not.Null);
Assert.That(
Session.Endpoint.TransportProfileUri,
Is.Not.Null.And.Not.Empty);
}
[Test]
public async Task ReadRejectedSessionCountAsync()
{
// OPC UA well-known NodeId 2154 = RejectedSessionCount
DataValue result = await ReadNodeValueAsync(
new NodeId(2154))
.ConfigureAwait(false);
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
int count = result.WrappedValue.GetInt32();
Assert.That(count, Is.GreaterThanOrEqualTo(0));
}
[Test]
public async Task ReadSessionDiagnosticsSecurityRejectedRequestsCountAsync()
{
// OPC UA well-known NodeId 2163 = SecurityRejectedRequestsCount
DataValue result = await ReadNodeValueAsync(
new NodeId(2163))
.ConfigureAwait(false);
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
int count = result.WrappedValue.GetInt32();
Assert.That(count, Is.GreaterThanOrEqualTo(0));
}
private async Task<DataValue> ReadNodeValueAsync(NodeId nodeId)
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
return response.Results[0];
}
[Description("Invoke CreateSession specifying a RequestedSessionTimeout of 0. We expect the RevisedSessionTimeout != 0. */")]
[Test]
public async Task CreateSessionWithZeroTimeoutReturnsRevisedTimeoutAsync()
{
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() { NodeId = VariableIds.Server_ServerStatus, AttributeId = Attributes.Value }
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
}
[Description("CreateSession with default parameters, except for a small timeout of 10 seconds. Activate the session and stall (do not use) the session for a period GREATER than the timeout perio")]
[Test]
public async Task CreateSessionStallsBeyondTimeoutPeriodAsync()
{
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() { NodeId = VariableIds.Server_ServerStatus, AttributeId = Attributes.Value }
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
}
[Description("Invoke CreateSession and then check if the session appears within the server diagnostics. This script must first read the servers profile to see if diagnostics are supported and if")]
[Test]
public async Task CreateSessionAppearsInServerDiagnosticsAsync()
{
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() { NodeId = VariableIds.Server_ServerStatus, AttributeId = Attributes.Value }
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
}
[Description("activate a session using default parameters. */")]
[Test]
public async Task ActivateSessionWithDefaultParametersAsync()
{
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() { NodeId = VariableIds.Server_ServerStatus, AttributeId = Attributes.Value }
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
}
[Description("Specify numerous localeIds supported by the server, in a ranked order. */")]
[Test]
public async Task CreateSessionWithRankedLocaleIdsAsync()
{
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() { NodeId = VariableIds.Server_ServerStatus, AttributeId = Attributes.Value }
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
}
[Description("activate a session that has been transferred to another channel. */")]
[Test]
public async Task ActivateSessionTransferredToAnotherChannelAsync()
{
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() { NodeId = VariableIds.Server_ServerStatus, AttributeId = Attributes.Value }
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
}
[Description("provide NO software certificates. This used to be a problem, but UA 1.02 changed this behavior. */")]
[Test]
public async Task CreateSessionWithNoSoftwareCertificatesAsync()
{
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() { NodeId = VariableIds.Server_ServerStatus, AttributeId = Attributes.Value }
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
}
[Description("CloseSession using default parameters. This test works by first opening a session (default parameters) and then closes it. */")]
[Test]
public async Task CloseSessionWithDefaultParametersAsync()
{
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() { NodeId = VariableIds.Server_ServerStatus, AttributeId = Attributes.Value }
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
}
[Description("provide NO software certificates. This used to be a problem, but UA 1.02 changed this behavior. */")]
[Test]
public async Task ActivateSessionWithNoSoftwareCertificatesAsync()
{
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() { NodeId = VariableIds.Server_ServerStatus, AttributeId = Attributes.Value }
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
}
[Description("Using SecurityPolicy None/anonymous, create a session while specifying a not-trusted certificate.")]
[Test]
public async Task CreateSessionWithUntrustedCertificateAndNoneSecurityAsync()
{
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() { NodeId = VariableIds.Server_ServerStatus, AttributeId = Attributes.Value }
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
}
[Description("Create a session without specifying a SessionName (legal, but some servers crash) */")]
[Test]
public async Task CreateSessionWithoutSessionNameAsync()
{
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() { NodeId = VariableIds.Server_ServerStatus, AttributeId = Attributes.Value }
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
}
[Description("Over a non-secure channel; call ActivateSession() specifying an empty ClientSignature. */")]
[Test]
public async Task ActivateSessionWithEmptyClientSignatureOnNonSecureChannelAsync()
{
ReadResponse readResponse = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() { NodeId = VariableIds.Server_ServerStatus, AttributeId = Attributes.Value }
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(readResponse.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(readResponse.Results[0].StatusCode), Is.True);
}
[Description("CreateSession – injects service result Bad_SecureChannelIdInvalid in the response.")]
[Test]
public Task CreateSessionWithInjectedBadSecureChannelIdInvalidAsync()
{
return AssertCreateSessionInjectsServiceResultAsync(StatusCodes.BadSecureChannelIdInvalid);
}
private Task AssertCreateSessionInjectsServiceResultAsync(StatusCode injected)
{
// Session.OpenAsync retries CreateSession without the client
// certificate when the first attempt fails, so a one-shot
// expectation would only mutate the first response and the
// retry would succeed. Use a recurring expectation so both
// attempts return the injected error.
using IDisposable expectation = MockController.WhenRequest<CreateSessionRequest, CreateSessionResponse>(
(req, resp) => resp.ResponseHeader.ServiceResult = injected);
ServiceResultException ex = Assert.ThrowsAsync<ServiceResultException>(
async () => await OpenAuxSessionAsync().ConfigureAwait(false));
Assert.That(ex.StatusCode, Is.EqualTo(injected));
return Task.CompletedTask;
}
[Description("CreateSession – injects service result Bad_NonceInvalid in the response.")]
[Test]
public Task CreateSessionWithInjectedBadNonceInvalidAsync()
{
return AssertCreateSessionInjectsServiceResultAsync(StatusCodes.BadNonceInvalid);
}
[Description("CreateSession – injects service result Bad_SecurityChecksFailed in the response.")]
[Test]
public Task CreateSessionWithInjectedBadSecurityChecksFailedAsync()
{
return AssertCreateSessionInjectsServiceResultAsync(StatusCodes.BadSecurityChecksFailed);
}
[Description("CreateSession – injects service result Bad_CertificateTimeInvalid in the response.")]
[Test]
public Task CreateSessionWithInjectedBadCertificateTimeInvalidAsync()
{
return AssertCreateSessionInjectsServiceResultAsync(StatusCodes.BadCertificateTimeInvalid);
}
[Description("CreateSession – injects service result Bad_CertificateIssuerTimeInvalid in the response.")]
[Test]
public Task CreateSessionWithInjectedBadCertificateIssuerTimeInvalidAsync()
{
return AssertCreateSessionInjectsServiceResultAsync(StatusCodes.BadCertificateIssuerTimeInvalid);
}
[Description("CreateSession – injects service result Bad_CertificateHostnameInvalid in the response.")]
[Test]
public Task CreateSessionWithInjectedBadCertificateHostnameInvalidAsync()
{
return AssertCreateSessionInjectsServiceResultAsync(StatusCodes.BadCertificateHostNameInvalid);
}
[Description("CreateSession – the SessionId returned by the server is null for the second session.")]
[Test]
public Task CreateSessionWithInjectedNullSessionIdAsync()
{
// A null SessionId in the response makes ActivateSession fail
// because the client uses SessionId to identify the session.
using IDisposable expectation = MockController.WhenRequest<CreateSessionRequest, CreateSessionResponse>(
(req, resp) => resp.SessionId = NodeId.Null);
Assert.ThrowsAsync<ServiceResultException>(
async () => await OpenAuxSessionAsync().ConfigureAwait(false));
return Task.CompletedTask;
}
[Description("CreateSession – the server returns the same AuthenticationToken for two distinct sessions.")]
[Test]
public async Task CreateSessionWithInjectedDuplicateAuthenticationTokenAsync()
{
// Capture the AuthenticationToken from the first
// CreateSession response, then on the second response
// overwrite it with the captured value.
NodeId firstToken = NodeId.Null;
using (MockController.WhenRequest<CreateSessionRequest, CreateSessionResponse>(
(req, resp) =>
{
if (firstToken.IsNull)
{
firstToken = resp.AuthenticationToken;
}
else
{
resp.AuthenticationToken = firstToken;
}
}))
{
ISession first = await OpenAuxSessionAsync().ConfigureAwait(false);
ISession? second = null;
try
{
Assert.That(firstToken.IsNull, Is.False);
try
{
second = await OpenAuxSessionAsync().ConfigureAwait(false);
Assert.That(second.Connected, Is.True);
}
catch (ServiceResultException)
{
// Acceptable: the server may reject the
// duplicate token during ActivateSession.
}
}
finally
{
foreach (ISession? s in new[] { first, second })
{
if (s == null)
{
continue;
}
try
{
await s.CloseAsync(5000, true, CancellationToken.None).ConfigureAwait(false);
}
catch
{
// best effort
}
s.Dispose();
}
}
}
}
[Description("CreateSession – the server revises the session timeout to (client request * 10).")]
[Test]
public Task CreateSessionWithInjectedExcessiveRevisedSessionTimeoutAsync()
{
return AssertCreateSessionAcceptsTimeoutMutationAsync(
mutate: (req, resp) => resp.RevisedSessionTimeout = req.RequestedSessionTimeout * 10.0);
}
[Description("CreateSession – the server revises the session timeout to an unreasonably low value (1 second).")]
[Test]
public Task CreateSessionWithInjectedTooLowRevisedSessionTimeoutAsync()
{
return AssertCreateSessionAcceptsTimeoutMutationAsync(
mutate: (req, resp) => resp.RevisedSessionTimeout = 1000.0);
}
private async Task AssertCreateSessionAcceptsTimeoutMutationAsync(
Action<CreateSessionRequest, CreateSessionResponse> mutate)
{
using IDisposable expectation = MockController.WhenRequest(mutate);
// The client must accept whatever RevisedSessionTimeout the
// server returns — assert the connection completes without
// throwing.
ISession session = await OpenAuxSessionAsync().ConfigureAwait(false);
try
{
Assert.That(session.Connected, Is.True);
}
finally
{
try
{
await session.CloseAsync(5000, true, CancellationToken.None).ConfigureAwait(false);
}
catch
{
// best effort
}
session.Dispose();
}
}
[Description("CreateSession – the server returns a ServerNonce that is less than 32 bytes long.")]
[Test]
public async Task CreateSessionWithInjectedShortServerNonceAsync()
{
using IDisposable expectation = MockController.WhenRequest<CreateSessionRequest, CreateSessionResponse>(
(req, resp) => resp.ServerNonce = new ByteString(new byte[8].AsMemory()));
// On a SecurityPolicy=None channel the client does not
// require a 32-byte nonce, so the connection completes.
// On a signed channel the client would reject the response.
ISession? session = null;
try
{
session = await OpenAuxSessionAsync().ConfigureAwait(false);
Assert.That(session.Connected, Is.True);
}
catch (ServiceResultException)
{
// Acceptable: client validated and rejected.
}
finally
{
if (session != null)
{
try
{
await session.CloseAsync(5000, true, CancellationToken.None).ConfigureAwait(false);
}
catch
{
// best effort
}
session.Dispose();
}
}
}
[Description("CreateSession – the EndpointDescriptions returned in the response differ from those obtained from the Discovery endpoint.")]
[Test]
public Task CreateSessionWithMismatchedEndpointDescriptionsAsync()
{
return AssertCreateSessionToleratesServerEndpointsMutationAsync(
mutate: (req, resp) =>
{
if (resp.ServerEndpoints != null! && resp.ServerEndpoints.Count > 0)
{
// Tweak the application URI on every endpoint so it
// no longer matches the one returned by Discovery.
foreach (EndpointDescription ep in resp.ServerEndpoints)
{
ep.Server?.ApplicationUri = "urn:mock:tampered";
}
}
});
}
[Description("CreateSession – the server returns an empty ServerEndpoints array.")]
[Test]
public Task CreateSessionWithInjectedEmptyServerEndpointsAsync()
{
return AssertCreateSessionToleratesServerEndpointsMutationAsync(
mutate: (req, resp) => resp.ServerEndpoints = Array.Empty<EndpointDescription>().ToArrayOf());
}
private async Task AssertCreateSessionToleratesServerEndpointsMutationAsync(
Action<CreateSessionRequest, CreateSessionResponse> mutate)
{
using IDisposable expectation = MockController.WhenRequest(mutate);
ISession? session = null;
try
{
session = await OpenAuxSessionAsync().ConfigureAwait(false);
Assert.That(session.Connected, Is.True);
}
catch (ServiceResultException)
{
// Acceptable: client may detect the mismatch and reject.
}
finally
{
if (session != null)
{
try
{
await session.CloseAsync(5000, true, CancellationToken.None).ConfigureAwait(false);
}
catch
{
// best effort
}
session.Dispose();
}
}
}
[Description("CreateSession – the server returns an empty ServerSoftwareCertificates array.")]
[Test]
public async Task CreateSessionWithInjectedEmptyServerSoftwareCertificatesAsync()
{
using IDisposable expectation = MockController.WhenRequest<CreateSessionRequest, CreateSessionResponse>(
(req, resp) => resp.ServerSoftwareCertificates = Array.Empty<SignedSoftwareCertificate>().ToArrayOf());
ISession session = await OpenAuxSessionAsync().ConfigureAwait(false);
try
{
Assert.That(session.Connected, Is.True);
}
finally
{
try
{
await session.CloseAsync(5000, true, CancellationToken.None).ConfigureAwait(false);
}
catch
{
// best effort
}
session.Dispose();
}
}
[Description("CreateSession – the server returns an empty ServerSignature.")]
[Test]
public async Task CreateSessionWithInjectedEmptyServerSignatureAsync()
{
using IDisposable expectation = MockController.WhenRequest<CreateSessionRequest, CreateSessionResponse>(
(req, resp) => resp.ServerSignature = new SignatureData());
ISession? session = null;
try
{
session = await OpenAuxSessionAsync().ConfigureAwait(false);
Assert.That(session.Connected, Is.True);
}
finally
{
if (session != null)
{
try
{
await session.CloseAsync(5000, true, CancellationToken.None).ConfigureAwait(false);
}
catch
{
// best effort
}
session.Dispose();
}
}
}
[Test]
public async Task SecuredCreateSessionRejectsEmptyServerSignatureAsync()
{
using IDisposable expectation = MockController
.ExpectNextResponse<CreateSessionResponse>(
response => response.ServerSignature = new SignatureData());
ConfiguredEndpoint endpoint = await CreateConfiguredEndpointAsync(
MessageSecurityMode.SignAndEncrypt).ConfigureAwait(false);
ServiceResultException exception = Assert.ThrowsAsync<ServiceResultException>(
async () => await ClientFixture.ConnectAsync(endpoint).ConfigureAwait(false))!;
Assert.That(
exception.StatusCode,
Is.EqualTo(StatusCodes.BadApplicationSignatureInvalid));
}
[TestCase(InvalidNonceKind.Null)]
[TestCase(InvalidNonceKind.Empty)]
[TestCase(InvalidNonceKind.Short)]
[TestCase(InvalidNonceKind.AllZero)]
[TestCase(InvalidNonceKind.TooLong)]
public async Task SecuredCreateSessionRejectsInvalidServerNonceAsync(
InvalidNonceKind nonceKind)
{
using IDisposable expectation = MockController
.ExpectNextResponse<CreateSessionResponse>(
response => response.ServerNonce = CreateInvalidNonce(nonceKind));
ConfiguredEndpoint endpoint = await CreateConfiguredEndpointAsync(
MessageSecurityMode.SignAndEncrypt).ConfigureAwait(false);
ServiceResultException exception = Assert.ThrowsAsync<ServiceResultException>(
async () => await ClientFixture.ConnectAsync(endpoint).ConfigureAwait(false))!;
Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadNonceInvalid));
}
[TestCase(InvalidNonceKind.Null, false)]
[TestCase(InvalidNonceKind.Empty, false)]
[TestCase(InvalidNonceKind.Short, false)]
[TestCase(InvalidNonceKind.AllZero, false)]
[TestCase(InvalidNonceKind.TooLong, false)]
[TestCase(InvalidNonceKind.Null, true)]
[TestCase(InvalidNonceKind.Empty, true)]
[TestCase(InvalidNonceKind.Short, true)]
[TestCase(InvalidNonceKind.AllZero, true)]
[TestCase(InvalidNonceKind.TooLong, true)]
public async Task SecuredActivateSessionRejectsInvalidServerNonceForAllIdentitiesAsync(
InvalidNonceKind nonceKind,
bool useUserName)
{
using IDisposable expectation = MockController
.ExpectNextResponse<ActivateSessionResponse>(
response => response.ServerNonce = CreateInvalidNonce(nonceKind));
ConfiguredEndpoint endpoint = await CreateConfiguredEndpointAsync(
MessageSecurityMode.SignAndEncrypt).ConfigureAwait(false);
IUserIdentity identity = useUserName
? new UserIdentity("user1", "password"u8)
: new UserIdentity();
ServiceResultException exception = Assert.ThrowsAsync<ServiceResultException>(
async () => await ClientFixture
.ConnectAsync(endpoint, identity)
.ConfigureAwait(false))!;
Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadNonceInvalid));
}
[Test]
public async Task SecuredActivateSessionRejectsReusedServerNonceAsync()
{
ByteString createSessionNonce = default;
using IDisposable capture = MockController
.WhenRequest<CreateSessionRequest, CreateSessionResponse>(
(_, response) => createSessionNonce = response.ServerNonce);
using IDisposable reuse = MockController
.ExpectNextResponse<ActivateSessionResponse>(
response => response.ServerNonce = createSessionNonce);
ConfiguredEndpoint endpoint = await CreateConfiguredEndpointAsync(
MessageSecurityMode.SignAndEncrypt).ConfigureAwait(false);
ServiceResultException exception = Assert.ThrowsAsync<ServiceResultException>(
async () => await ClientFixture.ConnectAsync(endpoint).ConfigureAwait(false))!;
Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadNonceInvalid));
}
[Test]
public async Task SecuredActivateSessionRejectsNonConsecutiveReusedServerNonceAsync()
{
ByteString firstActivationNonce = default;
int activationCount = 0;
using IDisposable mutation = MockController
.WhenRequest<ActivateSessionRequest, ActivateSessionResponse>(
(_, response) =>
{
activationCount++;
if (activationCount == 1)
{
firstActivationNonce = response.ServerNonce;
}
else if (activationCount == 3)
{
response.ServerNonce = firstActivationNonce;
}
});
ConfiguredEndpoint endpoint = await CreateConfiguredEndpointAsync(
MessageSecurityMode.SignAndEncrypt).ConfigureAwait(false);
ISession session = await ClientFixture.ConnectAsync(endpoint).ConfigureAwait(false);
try
{
await session.UpdateSessionAsync(
session.Identity,
default,
CancellationToken.None).ConfigureAwait(false);
ServiceResultException exception = Assert.ThrowsAsync<ServiceResultException>(
async () => await session.UpdateSessionAsync(
session.Identity,
default,
CancellationToken.None).ConfigureAwait(false))!;
Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadNonceInvalid));
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task ApplySessionConfigurationSeedsServerNonceHistoryAsync()
{
ConfiguredEndpoint endpoint = await CreateConfiguredEndpointAsync(
MessageSecurityMode.SignAndEncrypt).ConfigureAwait(false);
ISession session = await ClientFixture.ConnectAsync(endpoint).ConfigureAwait(false);
try
{
SessionConfiguration configuration = session.SaveSessionConfiguration();
// A secured session exposes a server nonce; re-applying the saved
// configuration must seed the lifetime nonce-reuse history with it.
Assert.That(configuration.ServerNonce.IsNull, Is.False);
using IDisposable expectation = MockController
.ExpectNextResponse<ActivateSessionResponse>(
response => response.ServerNonce = configuration.ServerNonce);
bool applied = session.ApplySessionConfiguration(configuration);
Assert.That(applied, Is.True);
Assert.That(session.Connected, Is.True);
ServiceResultException exception = Assert.ThrowsAsync<ServiceResultException>(
async () => await session.UpdateSessionAsync(
session.Identity,
default,
CancellationToken.None).ConfigureAwait(false))!;
Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadNonceInvalid));
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task SuppressNonceValidationErrorsDoesNotAllowInvalidNonceAsync()
{
bool original = ClientFixture.Config.SecurityConfiguration
.SuppressNonceValidationErrors;
ClientFixture.Config.SecurityConfiguration.SuppressNonceValidationErrors = true;
try
{
using IDisposable expectation = MockController
.ExpectNextResponse<ActivateSessionResponse>(
response => response.ServerNonce = CreateInvalidNonce(
InvalidNonceKind.AllZero));
ConfiguredEndpoint endpoint = await CreateConfiguredEndpointAsync(
MessageSecurityMode.SignAndEncrypt).ConfigureAwait(false);
ServiceResultException exception = Assert.ThrowsAsync<ServiceResultException>(
async () => await ClientFixture.ConnectAsync(endpoint).ConfigureAwait(false))!;
Assert.That(exception.StatusCode, Is.EqualTo(StatusCodes.BadNonceInvalid));
}
finally
{
ClientFixture.Config.SecurityConfiguration.SuppressNonceValidationErrors =
original;
}
}
[Test]
public async Task AnonymousSignReconnectRecreatesSessionAsync()
{
ConfiguredEndpoint endpoint = await CreateConfiguredEndpointAsync(
MessageSecurityMode.Sign).ConfigureAwait(false);
ISession session = await ClientFixture.ConnectAsync(endpoint).ConfigureAwait(false);
try
{
NodeId originalSessionId = session.SessionId;
await session.ReconnectAsync(
connection: null,
channel: null,
CancellationToken.None).ConfigureAwait(false);
Assert.That(session.SessionId, Is.Not.EqualTo(originalSessionId));
Assert.That(session.Connected, Is.True);
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task AnonymousSignAndEncryptReconnectReactivatesSessionAsync()
{
ConfiguredEndpoint endpoint = await CreateConfiguredEndpointAsync(
MessageSecurityMode.SignAndEncrypt).ConfigureAwait(false);
ISession session = await ClientFixture.ConnectAsync(endpoint).ConfigureAwait(false);
try
{
NodeId originalSessionId = session.SessionId;
await session.ReconnectAsync(
connection: null,
channel: null,
CancellationToken.None).ConfigureAwait(false);
Assert.That(session.SessionId, Is.EqualTo(originalSessionId));
Assert.That(session.Connected, Is.True);
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Description("CreateSession – the server returns a MaxRequestMessageSize of 500 bytes.")]
[Test]
public Task CreateSessionWithInjectedSmallMaxRequestMessageSizeAsync()
{
return AssertCreateSessionAcceptsMessageSizeMutationAsync(
mutate: (req, resp) => resp.MaxRequestMessageSize = 500);
}
[Description("CreateSession – the server returns a MaxResponseMessageSize equal to the client's request multiplied by 10.")]
[Test]
public Task CreateSessionWithInjectedExcessiveMaxResponseMessageSizeAsync()
{
return AssertCreateSessionAcceptsMessageSizeMutationAsync(
mutate: (req, resp) => resp.MaxRequestMessageSize = req.MaxResponseMessageSize * 10);
}
private async Task AssertCreateSessionAcceptsMessageSizeMutationAsync(
Action<CreateSessionRequest, CreateSessionResponse> mutate)
{
using IDisposable expectation = MockController.WhenRequest(mutate);
ISession session = await OpenAuxSessionAsync().ConfigureAwait(false);
try
{
Assert.That(session.Connected, Is.True);
}
finally
{
try
{
await session.CloseAsync(5000, true, CancellationToken.None).ConfigureAwait(false);
}
catch
{
// best effort
}
session.Dispose();
}
}
[Description("ActivateSession – injects service result Bad_IdentityTokenInvalid.")]
[Test]
public Task ActivateSessionWithInjectedBadIdentityTokenInvalidAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadIdentityTokenInvalid);
}
[Description("ActivateSession – injects service result Bad_IdentityTokenRejected.")]
[Test]
public Task ActivateSessionWithInjectedBadIdentityTokenRejectedAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadIdentityTokenRejected);
}
[Description("ActivateSession – injects service result Bad_UserAccessDenied.")]
[Test]
public Task ActivateSessionWithInjectedBadUserAccessDeniedAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadUserAccessDenied);
}
[Description("ActivateSession – injects service result Bad_ApplicationSignatureInvalid.")]
[Test]
public Task ActivateSessionWithInjectedBadApplicationSignatureInvalidAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadApplicationSignatureInvalid);
}
[Description("ActivateSession – injects service result Bad_UserSignatureInvalid.")]
[Test]
public Task ActivateSessionWithInjectedBadUserSignatureInvalidAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadUserSignatureInvalid);
}
[Description("ActivateSession – injects service result Bad_NoValidCertificates.")]
[Test]
public Task ActivateSessionWithInjectedBadNoValidCertificatesAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadNoValidCertificates);
}
[Description("ActivateSession – injects service result Bad_IdentityChangeNotSupported.")]
[Test]
public Task ActivateSessionWithInjectedBadIdentityChangeNotSupportedAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadIdentityChangeNotSupported);
}
[Description("ActivateSession – injects service result Bad_CertificateTimeInvalid.")]
[Test]
public Task ActivateSessionWithInjectedBadCertificateTimeInvalidAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadCertificateTimeInvalid);
}
[Description("ActivateSession – injects service result Bad_CertificateIssuerTimeInvalid.")]
[Test]
public Task ActivateSessionWithInjectedBadCertificateIssuerTimeInvalidAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadCertificateIssuerTimeInvalid);
}
[Description("ActivateSession – injects service result Bad_CertificateHostNameInvalid.")]
[Test]
public Task ActivateSessionWithInjectedBadCertificateHostNameInvalidAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadCertificateHostNameInvalid);
}
[Description("ActivateSession – injects service result Bad_CertificateUriInvalid.")]
[Test]
public Task ActivateSessionWithInjectedBadCertificateUriInvalidAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadCertificateUriInvalid);
}
[Description("ActivateSession – injects service result Bad_CertificateUseNotAllowed.")]
[Test]
public Task ActivateSessionWithInjectedBadCertificateUseNotAllowedAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadCertificateUseNotAllowed);
}
[Description("ActivateSession – injects service result Bad_CertificateIssuerUseNotAllowed.")]
[Test]
public Task ActivateSessionWithInjectedBadCertificateIssuerUseNotAllowedAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadCertificateIssuerUseNotAllowed);
}
[Description("ActivateSession – injects service result Bad_CertificateUntrusted.")]
[Test]
public Task ActivateSessionWithInjectedBadCertificateUntrustedAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadCertificateUntrusted);
}
[Description("ActivateSession – injects service result Bad_CertificateRevocationUnknown.")]
[Test]
public Task ActivateSessionWithInjectedBadCertificateRevocationUnknownAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadCertificateRevocationUnknown);
}
[Description("ActivateSession – injects service result Bad_CertificateIssuerRevocationUnknown.")]
[Test]
public Task ActivateSessionWithInjectedBadCertificateIssuerRevocationUnknownAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadCertificateIssuerRevocationUnknown);
}
[Description("ActivateSession – injects service result Bad_CertificateRevoked.")]
[Test]
public Task ActivateSessionWithInjectedBadCertificateRevokedAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadCertificateRevoked);
}
[Description("ActivateSession – injects service result Bad_CertificateIssuerRevoked.")]
[Test]
public Task ActivateSessionWithInjectedBadCertificateIssuerRevokedAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadCertificateIssuerRevoked);
}
[Description("ActivateSession – injects service result Bad_SecurityChecksFailed.")]
[Test]
public Task ActivateSessionWithInjectedBadSecurityChecksFailedAsync()
{
return AssertActivateSessionInjectsServiceResultAsync(StatusCodes.BadSecurityChecksFailed);
}
private Task AssertActivateSessionInjectsServiceResultAsync(StatusCode injected)
{
using IDisposable expectation = MockController.ExpectNextResponse<ActivateSessionResponse>(
r => r.ResponseHeader.ServiceResult = injected);
ServiceResultException ex = Assert.ThrowsAsync<ServiceResultException>(
async () => await OpenAuxSessionAsync().ConfigureAwait(false));
Assert.That(ex.StatusCode, Is.EqualTo(injected));
return Task.CompletedTask;
}
[Description("CloseSession – the server returns Bad_SessionIdInvalid as the service result.")]
[Test]
public async Task CloseSessionWithInjectedBadSessionIdInvalidAsync()
{
ISession aux = await OpenAuxSessionAsync().ConfigureAwait(false);
try
{
using IDisposable expectation = MockController.ExpectNextResponse<CloseSessionResponse>(
r => r.ResponseHeader.ServiceResult = StatusCodes.BadSessionIdInvalid);
// Session.CloseAsync swallows ServiceResultException and
// returns the status code so callers can log it without
// a try/catch — assert against the returned code.
StatusCode result = await aux.CloseAsync(5000, true, CancellationToken.None).ConfigureAwait(false);
Assert.That(result, Is.EqualTo(StatusCodes.BadSessionIdInvalid));
Assert.That(MockController.HasPendingOneShotExpectations, Is.False);
}
finally
{
aux.Dispose();
}
}
[Description("ActivateSession – the server returns an empty ServerNonce in the response.")]
[Test]
public async Task ActivateSessionWithInjectedEmptyServerNonceAsync()
{
using IDisposable expectation = MockController.WhenRequest<ActivateSessionRequest, ActivateSessionResponse>(
(req, resp) => resp.ServerNonce = ByteString.Empty);
// On a SecurityPolicy=None channel the empty nonce passes
// through. On a signed channel the client would reject.
ISession? session = null;
try
{
session = await OpenAuxSessionAsync().ConfigureAwait(false);
Assert.That(session.Connected, Is.True);
}
catch (ServiceResultException)
{
// Acceptable on signed channels.
}
finally
{
if (session != null)
{
try
{
await session.CloseAsync(5000, true, CancellationToken.None).ConfigureAwait(false);
}
catch
{
// best effort
}
session.Dispose();
}
}
}
[Description("ActivateSession – the first entry of the per-result Results array contains Bad_CertificateUriInvalid.")]
[Test]
public async Task ActivateSessionWithInjectedFirstResultBadCertificateUriInvalidAsync()
{
using IDisposable expectation = MockController.WhenRequest<ActivateSessionRequest, ActivateSessionResponse>(
(req, resp) =>
{
int n = resp.Results == null! ? 0 : resp.Results.Count;
var mutated = new StatusCode[Math.Max(n, 1)];
for (int i = 0; i < mutated.Length; i++)
{
mutated[i] = i < n && i > 0 ? resp.Results[i] : StatusCodes.Good;
}
mutated[0] = StatusCodes.BadCertificateUriInvalid;
resp.Results = mutated.ToArrayOf();
});
ISession? session = null;
try
{
session = await OpenAuxSessionAsync().ConfigureAwait(false);
Assert.That(session.Connected, Is.True);
}
catch (ServiceResultException)
{
// Acceptable: a stricter client would reject.
}
finally
{
if (session != null)
{
try
{
await session.CloseAsync(5000, true, CancellationToken.None).ConfigureAwait(false);
}
catch
{
// best effort
}
session.Dispose();
}
}
}
[Description("ActivateSession – every entry of the Results array contains a Bad_ status code.")]
[Test]
public async Task ActivateSessionWithInjectedAllResultsBadAsync()
{
using IDisposable expectation = MockController.WhenRequest<ActivateSessionRequest, ActivateSessionResponse>(
(req, resp) =>
{
int n = resp.Results == null! ? 0 : resp.Results.Count;
var mutated = new StatusCode[Math.Max(n, 1)];
for (int i = 0; i < mutated.Length; i++)
{
mutated[i] = StatusCodes.BadCertificateUriInvalid;
}
resp.Results = mutated.ToArrayOf();
});
ISession? session = null;
try
{
session = await OpenAuxSessionAsync().ConfigureAwait(false);
Assert.That(session.Connected, Is.True);
}
catch (ServiceResultException)
{
// Acceptable.
}
finally
{
if (session != null)
{
try
{
await session.CloseAsync(5000, true, CancellationToken.None).ConfigureAwait(false);
}
catch
{
// best effort
}
session.Dispose();
}
}
}
private async Task<ConfiguredEndpoint> CreateConfiguredEndpointAsync(
MessageSecurityMode securityMode)
{
ArrayOf<EndpointDescription> endpoints = await ClientFixture
.GetEndpointsAsync(ServerUrl)
.ConfigureAwait(false);
EndpointDescription? selected = null;
for (int i = 0; i < endpoints.Count; i++)
{
EndpointDescription candidate = endpoints[i];
if (candidate.SecurityMode == securityMode &&
candidate.SecurityPolicyUri == SecurityPolicies.Basic256Sha256)
{
selected = candidate;
break;
}
}
Assert.That(selected, Is.Not.Null);
return new ConfiguredEndpoint(
null,
selected,
EndpointConfiguration.Create(ClientFixture.Config));
}
private static ByteString CreateInvalidNonce(InvalidNonceKind nonceKind)
{
return nonceKind switch
{
InvalidNonceKind.Null => default,
InvalidNonceKind.Empty => ByteString.Empty,
InvalidNonceKind.Short => ByteString.From(CreateNonceBytes(31)),
InvalidNonceKind.AllZero => ByteString.From(new byte[32]),
InvalidNonceKind.TooLong => ByteString.From(CreateNonceBytes(129)),
_ => throw new ArgumentOutOfRangeException(nameof(nonceKind))
};
}
private static byte[] CreateNonceBytes(int length)
{
byte[] bytes = new byte[length];
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = (byte)(i + 1);
}
return bytes;
}
public enum InvalidNonceKind
{
Null,
Empty,
Short,
AllZero,
TooLong
}
}
}