mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 07:07:24 +08:00
# Description Introduces a pluggable user-identity provider model covering all OPC UA Part 6 §6.5 mechanisms (Anonymous, UserName, X509, IssuedToken/JWT) on both client and server, with full `Microsoft.Extensions.DependencyInjection` integration, plus the Part 12 §9 `AuthorizationService` two-phase token-request flow **including refresh-token rotation**, the Part 12 §8 KeyCredential Push model, and a clean provider model for OPC 10000-12 §7.2 `ApplicationSelfAdmin`. The work is split across phase-by-phase commits so it can be reviewed independently; nothing here changes wire-level behaviour or forces existing applications to migrate. ### Documentation All under `Docs/`: - `IdentityProviders.md` — full developer guide: three-layer model, recipes, configuration reference, "what gets auto-wired" matrix, JWS algorithm table, identity-augmenters section, **"Implementing your own provider"** how-to subsections for Entra / OIDC / Windows / AspNetCore (sibling files merged in per review feedback). - `DependencyInjection.md` — `Identity` sections with `appsettings.json` examples, identity-extension matrix, `WithAuthorizationService` + `WithKeyCredentialPush` + `AddIdentityAugmenter` extensions documented. - `RoleBasedUserManagement.md` — claim-criteria callout describing `IIdentityClaims` probe surface. - `AuthorizationService.md` — Architecture diagram + `IAccessTokenProvider` conceptual narrative restored from master; modern `StartRequestToken` / `FinishRequestToken` flow leads; refresh-token recipe added. - `KeyCredentialService.md` — Push model + Pull vs Push vs Bridge decision matrix + Hybrid (Push + Pull) section; explicit experimental warning for the bridge. - `GDS.md` — KeyCredential Push, `StartRequestToken`, `GdsApplicationSelfAdminProvider`, **shipped Refresh-token support with rotation policy and audit semantics** all documented. - `MigrationGuide.md` — User Identity Providers section listing `[Obsolete]` markers + mechanical recipes; SelfAdmin migration via `IIdentityAugmenter`; `EnableRefreshTokens` default note. - `README.md` index entries. ## Checklist - [x] 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. - [x] 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. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [x] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received.
1293 lines
51 KiB
C#
1293 lines
51 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.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());
|
||
|
||
// On a SecurityPolicy=None channel the empty signature is
|
||
// not validated, so the open completes.
|
||
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("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));
|
||
}
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|