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.
1644 lines
66 KiB
C#
1644 lines
66 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
// CA2000: test code; many disposables are ownership-transferred to test fixtures or short-lived,
|
|
// making CA2000 noisy without a real leak risk. Disabled file-level for the suite.
|
|
#pragma warning disable CA2000
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Client.TestFramework;
|
|
using Opc.Ua.Tests;
|
|
|
|
namespace Opc.Ua.Client.Tests
|
|
{
|
|
[TestFixture]
|
|
[Category("Client")]
|
|
[Category("Session")]
|
|
[SetCulture("en-us")]
|
|
[SetUICulture("en-us")]
|
|
public sealed class SessionTests
|
|
{
|
|
[Test]
|
|
public async Task FetchOperationLimitsAsyncShouldFetchAllOperationLimitsAsync()
|
|
{
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
ArrayOf<DataValue> dataValues =
|
|
[
|
|
new DataValue(new Variant(2000u)),
|
|
new DataValue(new Variant(3000u)),
|
|
new DataValue(new Variant(4000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(5000u)),
|
|
new DataValue(new Variant(6000u)),
|
|
new DataValue(new Variant(7000u)),
|
|
new DataValue(new Variant(8000u)),
|
|
new DataValue(new Variant(9000u)),
|
|
new DataValue(new Variant(10000u)),
|
|
new DataValue(new Variant(11000u)),
|
|
new DataValue(new Variant(12000u)),
|
|
new DataValue(new Variant((ushort)13000)),
|
|
new DataValue(new Variant((ushort)14000u)),
|
|
new DataValue(new Variant((ushort)15000u)),
|
|
new DataValue(new Variant(16000u)),
|
|
new DataValue(new Variant(17000u)),
|
|
new DataValue(new Variant(18000u)),
|
|
new DataValue(new Variant((double)19000.0)),
|
|
new DataValue(new Variant(20000u)),
|
|
new DataValue(new Variant(21000u)),
|
|
new DataValue(new Variant(22000u)),
|
|
new DataValue(new Variant(23000u)),
|
|
new DataValue(new Variant(24000u)),
|
|
new DataValue(new Variant(25000u)),
|
|
new DataValue(new Variant(26000u)),
|
|
new DataValue(new Variant(27000u))
|
|
];
|
|
|
|
sut.Channel
|
|
.SetupSequence(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = [new DataValue(new Variant(1000u))],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = dataValues,
|
|
DiagnosticInfos = []
|
|
}));
|
|
|
|
// Act
|
|
await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(sut.OperationLimits.MaxNodesPerRead, Is.EqualTo(1000));
|
|
Assert.That(sut.OperationLimits.MaxNodesPerHistoryReadData, Is.EqualTo(2000));
|
|
Assert.That(sut.OperationLimits.MaxNodesPerHistoryReadEvents, Is.EqualTo(3000));
|
|
Assert.That(sut.OperationLimits.MaxNodesPerWrite, Is.EqualTo(4000));
|
|
Assert.That(sut.OperationLimits.MaxNodesPerHistoryUpdateData, Is.EqualTo(5000));
|
|
Assert.That(sut.OperationLimits.MaxNodesPerHistoryUpdateEvents, Is.EqualTo(6000));
|
|
Assert.That(sut.OperationLimits.MaxNodesPerMethodCall, Is.EqualTo(7000));
|
|
Assert.That(sut.OperationLimits.MaxNodesPerBrowse, Is.EqualTo(8000));
|
|
Assert.That(sut.OperationLimits.MaxNodesPerRegisterNodes, Is.EqualTo(9000));
|
|
Assert.That(sut.OperationLimits.MaxNodesPerNodeManagement, Is.EqualTo(10000));
|
|
Assert.That(sut.OperationLimits.MaxMonitoredItemsPerCall, Is.EqualTo(11000));
|
|
Assert.That(sut.OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds, Is.EqualTo(12000));
|
|
Assert.That(sut.ServerCapabilities.MaxBrowseContinuationPoints, Is.EqualTo(13000));
|
|
Assert.That(sut.ServerCapabilities.MaxHistoryContinuationPoints, Is.EqualTo(14000));
|
|
Assert.That(sut.ServerCapabilities.MaxQueryContinuationPoints, Is.EqualTo(15000));
|
|
Assert.That(sut.ServerCapabilities.MaxStringLength, Is.EqualTo(16000));
|
|
Assert.That(sut.ServerCapabilities.MaxArrayLength, Is.EqualTo(17000));
|
|
Assert.That(sut.ServerCapabilities.MaxByteStringLength, Is.EqualTo(18000));
|
|
Assert.That(sut.ServerCapabilities.MinSupportedSampleRate, Is.EqualTo(19000.0));
|
|
Assert.That(sut.ServerCapabilities.MaxSessions, Is.EqualTo(20000));
|
|
Assert.That(sut.ServerCapabilities.MaxSubscriptions, Is.EqualTo(21000));
|
|
Assert.That(sut.ServerCapabilities.MaxMonitoredItems, Is.EqualTo(22000));
|
|
Assert.That(sut.ServerCapabilities.MaxMonitoredItemsPerSubscription, Is.EqualTo(23000));
|
|
Assert.That(sut.ServerCapabilities.MaxMonitoredItemsQueueSize, Is.EqualTo(24000));
|
|
Assert.That(sut.ServerCapabilities.MaxSubscriptionsPerSession, Is.EqualTo(25000));
|
|
Assert.That(sut.ServerCapabilities.MaxWhereClauseParameters, Is.EqualTo(26000));
|
|
Assert.That(sut.ServerCapabilities.MaxSelectClauseParameters, Is.EqualTo(27000));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void FetchOperationLimitsAsyncShouldHandleEmptyResponse()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
ArrayOf<DataValue> dataValues = [];
|
|
ArrayOf<DiagnosticInfo> diagnosticInfos = [];
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = dataValues,
|
|
DiagnosticInfos = diagnosticInfos
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void FetchOperationLimitsAsyncShouldHandlePartialData()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
ArrayOf<DataValue> dataValues =
|
|
[
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(2000u)),
|
|
new DataValue(new Variant(3000u))
|
|
];
|
|
|
|
sut.Channel
|
|
.SetupSequence(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = [new DataValue(new Variant(1000u))],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = dataValues,
|
|
DiagnosticInfos = []
|
|
}));
|
|
|
|
Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void FetchOperationLimitsAsyncShouldHandleErrors()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
ArrayOf<DataValue> dataValues = [DataValue.FromStatusCode(StatusCodes.BadUnexpectedError)];
|
|
ArrayOf<DiagnosticInfo> diagnosticInfos = [];
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = dataValues,
|
|
DiagnosticInfos = diagnosticInfos
|
|
}))
|
|
.Verifiable(Times.Exactly(2));
|
|
|
|
Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void FetchOperationLimitsAsyncShouldThrowWhenInvalidDataTypes()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
ArrayOf<DataValue> dataValues = [new DataValue("InvalidDataType")];
|
|
|
|
ArrayOf<DiagnosticInfo> diagnosticInfos = [];
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = dataValues,
|
|
DiagnosticInfos = diagnosticInfos
|
|
}))
|
|
.Verifiable(Times.Exactly(2));
|
|
|
|
Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void FetchOperationLimitsAsyncShouldHandleTimeout()
|
|
{
|
|
using var sut = SessionMock.Create();
|
|
using var cts = new CancellationTokenSource(100);
|
|
CancellationToken ct = cts.Token; // Set a short timeout
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ThrowsAsync(new TaskCanceledException())
|
|
.Verifiable(Times.Once);
|
|
|
|
Assert.ThrowsAsync<TaskCanceledException>(
|
|
async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task FetchOperationLimitsAsyncShouldApplyClientLimitsWhenSmallerAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
var channel = new Mock<ITransportChannel>();
|
|
channel
|
|
.SetupGet(s => s.MessageContext)
|
|
.Returns(ServiceMessageContext.Create(telemetry));
|
|
channel
|
|
.SetupGet(s => s.SupportedFeatures)
|
|
.Returns(TransportChannelFeatures.Reconnect);
|
|
|
|
// Configure client with smaller limits than server
|
|
var configuration = new ApplicationConfiguration(telemetry)
|
|
{
|
|
ClientConfiguration = new ClientConfiguration
|
|
{
|
|
OperationLimits = new OperationLimits
|
|
{
|
|
MaxNodesPerRead = 500,
|
|
MaxNodesPerWrite = 2000,
|
|
MaxNodesPerBrowse = 4000
|
|
}
|
|
}
|
|
};
|
|
|
|
var endpoint = new EndpointDescription
|
|
{
|
|
SecurityMode = MessageSecurityMode.None,
|
|
SecurityPolicyUri = SecurityPolicies.None,
|
|
EndpointUrl = "opc.tcp://localhost:4840"
|
|
};
|
|
|
|
using var sut = new SessionMock(
|
|
channel,
|
|
configuration,
|
|
new ConfiguredEndpoint(null, endpoint));
|
|
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
// Server returns larger values
|
|
ArrayOf<DataValue> dataValues =
|
|
[
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerHistoryReadData - no client limit
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerHistoryReadEvents - no client limit
|
|
new DataValue(new Variant(5000u)), // MaxNodesPerWrite - client has 2000
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerRead - client has 500
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerHistoryUpdateData
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerHistoryUpdateEvents
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerMethodCall
|
|
new DataValue(new Variant(10000u)), // MaxNodesPerBrowse - client has 4000
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerRegisterNodes
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerNodeManagement
|
|
new DataValue(new Variant(1000u)), // MaxMonitoredItemsPerCall
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerTranslateBrowsePathsToNodeIds
|
|
new DataValue(new Variant((ushort)100)),
|
|
new DataValue(new Variant((ushort)100)),
|
|
new DataValue(new Variant((ushort)100)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(100.0)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u))
|
|
];
|
|
|
|
channel
|
|
.SetupSequence(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = [new DataValue(new Variant(1000u))],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = dataValues,
|
|
DiagnosticInfos = []
|
|
}));
|
|
|
|
// Act
|
|
await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert - should use smaller of client and server values
|
|
Assert.That(sut.OperationLimits.MaxNodesPerRead, Is.EqualTo(500)); // min(500, 1000)
|
|
Assert.That(sut.OperationLimits.MaxNodesPerWrite, Is.EqualTo(2000)); // min(2000, 5000)
|
|
Assert.That(sut.OperationLimits.MaxNodesPerBrowse, Is.EqualTo(4000)); // min(4000, 10000)
|
|
Assert.That(sut.OperationLimits.MaxNodesPerHistoryReadData, Is.EqualTo(1000)); // 0 -> server
|
|
Assert.That(sut.OperationLimits.MaxNodesPerHistoryReadEvents, Is.EqualTo(1000)); // 0 -> server
|
|
|
|
channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task FetchOperationLimitsAsyncShouldApplyServerLimitsWhenSmallerAsync()
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
var channel = new Mock<ITransportChannel>();
|
|
channel
|
|
.SetupGet(s => s.MessageContext)
|
|
.Returns(ServiceMessageContext.Create(telemetry));
|
|
channel
|
|
.SetupGet(s => s.SupportedFeatures)
|
|
.Returns(TransportChannelFeatures.Reconnect);
|
|
|
|
// Configure client with larger limits than server
|
|
var configuration = new ApplicationConfiguration(telemetry)
|
|
{
|
|
ClientConfiguration = new ClientConfiguration
|
|
{
|
|
OperationLimits = new OperationLimits
|
|
{
|
|
MaxNodesPerRead = 2000,
|
|
MaxNodesPerWrite = 10000,
|
|
MaxNodesPerBrowse = 8000
|
|
}
|
|
}
|
|
};
|
|
|
|
var endpoint = new EndpointDescription
|
|
{
|
|
SecurityMode = MessageSecurityMode.None,
|
|
SecurityPolicyUri = SecurityPolicies.None,
|
|
EndpointUrl = "opc.tcp://localhost:4840"
|
|
};
|
|
|
|
using var sut = new SessionMock(
|
|
channel,
|
|
configuration,
|
|
new ConfiguredEndpoint(null, endpoint));
|
|
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
// Server returns smaller values
|
|
ArrayOf<DataValue> dataValues =
|
|
[
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerHistoryReadData
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerHistoryReadEvents
|
|
new DataValue(new Variant(5000u)), // MaxNodesPerWrite - server has 5000, client has 10000
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerRead - server has 1000, client has 2000
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerHistoryUpdateData
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerHistoryUpdateEvents
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerMethodCall
|
|
new DataValue(new Variant(5000u)), // MaxNodesPerBrowse - server has 5000, client has 8000
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerRegisterNodes
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerNodeManagement
|
|
new DataValue(new Variant(1000u)), // MaxMonitoredItemsPerCall
|
|
new DataValue(new Variant(1000u)), // MaxNodesPerTranslateBrowsePathsToNodeIds
|
|
new DataValue(new Variant((ushort)100)),
|
|
new DataValue(new Variant((ushort)100)),
|
|
new DataValue(new Variant((ushort)100)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(100.0)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u)),
|
|
new DataValue(new Variant(1000u))
|
|
];
|
|
|
|
channel
|
|
.SetupSequence(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = [new DataValue(new Variant(1000u))],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = dataValues,
|
|
DiagnosticInfos = []
|
|
}));
|
|
|
|
// Act
|
|
await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert - should use smaller of client and server values
|
|
Assert.That(sut.OperationLimits.MaxNodesPerRead, Is.EqualTo(1000)); // min(2000, 1000)
|
|
Assert.That(sut.OperationLimits.MaxNodesPerWrite, Is.EqualTo(5000)); // min(10000, 5000)
|
|
Assert.That(sut.OperationLimits.MaxNodesPerBrowse, Is.EqualTo(5000)); // min(8000, 5000)
|
|
|
|
channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task FetchNamespaceTablesAsyncShouldFetchAndUpdateTablesAsync()
|
|
{
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
var namespaceArray = new DataValue(new Variant([Namespaces.OpcUa, "http://namespace2"]));
|
|
var serverArray = new DataValue(new Variant(["http://server1", "http://server2"]));
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = [namespaceArray, serverArray],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
// Act
|
|
await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Namespaces.OpcUa, "http://namespace2"]));
|
|
Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1", "http://server2"]));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task FetchNamespaceTablesAsyncShouldFetchAndUpdateTablesAndLogDifferences1Async()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
var namespaceArray1 = new DataValue(Variant.From([Namespaces.OpcUa, "http://namespace2", "http://namespace3"]));
|
|
var serverArray1 = new DataValue(Variant.From(["http://server1", "http://server2"]));
|
|
var namespaceArray2 = new DataValue(Variant.From([Namespaces.OpcUa, "http://namespace3", "http://namespace2"]));
|
|
var serverArray2 = new DataValue(Variant.From(["http://server1", "http://server2", "http://server3"]));
|
|
|
|
sut.Channel
|
|
.SetupSequence(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = [namespaceArray1, serverArray1],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = [namespaceArray2, serverArray2],
|
|
DiagnosticInfos = []
|
|
}));
|
|
|
|
// Act
|
|
await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Namespaces.OpcUa, "http://namespace2", "http://namespace3"]));
|
|
Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1", "http://server2"]));
|
|
|
|
// Act
|
|
await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Namespaces.OpcUa, "http://namespace3", "http://namespace2"]));
|
|
Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1", "http://server2", "http://server3"]));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task FetchNamespaceTablesAsyncShouldFetchAndUpdateTablesAndLogDifferences2Async()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
var namespaceArray1 = new DataValue(new Variant([Namespaces.OpcUa, "http://namespace2", "http://namespace3"]));
|
|
var serverArray1 = new DataValue(new Variant(["http://server1", "http://server2"]));
|
|
var namespaceArray2 = new DataValue(new Variant([Namespaces.OpcUa, "http://namespace3"]));
|
|
var serverArray2 = new DataValue(new Variant(["http://server1"]));
|
|
|
|
sut.Channel
|
|
.SetupSequence(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = [namespaceArray1, serverArray1],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = [namespaceArray2, serverArray2],
|
|
DiagnosticInfos = []
|
|
}));
|
|
|
|
// Act
|
|
await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Namespaces.OpcUa, "http://namespace2", "http://namespace3"]));
|
|
Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1", "http://server2"]));
|
|
|
|
// Act
|
|
await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Namespaces.OpcUa, "http://namespace3"]));
|
|
Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1"]));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task FetchNamespaceTablesAsyncShouldHandlePartialSuccessAsync()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
var namespaceArray = new DataValue(new Variant([Namespaces.OpcUa, "http://namespace2"]));
|
|
var serverArray = DataValue.FromStatusCode(StatusCodes.BadUnexpectedError);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = [namespaceArray, serverArray],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
// Act
|
|
await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Namespaces.OpcUa, "http://namespace2"]));
|
|
Assert.That(sut.ServerUris.ToArray(), Is.Empty);
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void FetchNamespaceTablesAsyncShouldThrowInCaseOfBadResponse()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
var namespaceArray = new DataValue(new Variant([Namespaces.OpcUa, "http://namespace2"]));
|
|
var serverArray = new DataValue(new Variant(["http://server1", "http://server2"]));
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadUnexpectedError },
|
|
Results = [namespaceArray, serverArray],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false));
|
|
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void FetchNamespaceTablesAsyncShouldThrowWhenNamespaceArrayCouldNotBeRetrieved()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
var namespaceArray = DataValue.FromStatusCode(StatusCodes.BadUnexpectedError);
|
|
var serverArray = DataValue.FromStatusCode(StatusCodes.BadUnexpectedError);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = [namespaceArray, serverArray],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false));
|
|
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void FetchNamespaceTablesAsyncShoulThrowWhenEmptyResponse()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
ArrayOf<DataValue> dataValues = [];
|
|
ArrayOf<DiagnosticInfo> diagnosticInfos = [];
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = dataValues,
|
|
DiagnosticInfos = diagnosticInfos
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false));
|
|
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void FetchNamespaceTablesAsyncShouldThrowWhenInvalidDataTypesForNamespaceTable()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
var namespaceArray = new DataValue(new Variant(12345));
|
|
var serverArray = new DataValue(new Variant(67890));
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = [namespaceArray, serverArray],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false));
|
|
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadTypeMismatch));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void FetchNamespaceTablesAsyncShouldThrowWhenInvalidDataTypeForServerUrls()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
var namespaceArray = new DataValue(new Variant([Namespaces.OpcUa, "http://namespace2"]));
|
|
var serverArray = new DataValue(new Variant(67890));
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ReadRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = [namespaceArray, serverArray],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false));
|
|
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadTypeMismatch));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task CloseAsyncShouldCloseSessionAndChannelSuccessfullyAsync()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
sut.SetConnected();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<CloseSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new CloseSessionResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good }
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.CloseAsync(It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask())
|
|
.Verifiable(Times.Once);
|
|
|
|
// Act
|
|
StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(StatusCodes.Good));
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task CloseAsyncShouldHandleAlreadyClosedSessionAsync()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
sut.Dispose();
|
|
|
|
// Act
|
|
StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(StatusCodes.Good));
|
|
}
|
|
|
|
[Test]
|
|
public async Task CloseAsyncShouldHandleErrorsDuringCloseAsync()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
sut.SetConnected();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<CloseSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError))
|
|
.Verifiable(Times.Once);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.CloseAsync(It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask())
|
|
.Verifiable(Times.Once);
|
|
|
|
// Act
|
|
StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(StatusCodes.BadUnexpectedError));
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task CloseAsyncShouldCloseSessionWithoutDeletingSubscriptionsAsync()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
sut.SetConnected();
|
|
sut.DeleteSubscriptionsOnClose = false;
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<CloseSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new CloseSessionResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good }
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.CloseAsync(It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask())
|
|
.Verifiable(Times.Once);
|
|
|
|
// Act
|
|
StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(StatusCodes.Good));
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task CloseAsyncShouldHandleChannelErrorsAsync()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
sut.SetConnected();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<CloseSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new CloseSessionResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good }
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.CloseAsync(It.IsAny<CancellationToken>()))
|
|
.ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError))
|
|
.Verifiable(Times.Once);
|
|
|
|
StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.EqualTo(StatusCodes.Good)); // Eats the error from channel close
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task CloseAsyncShouldCloseSessionSuccessfullyAsync()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
sut.SetConnected();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<CloseSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new CloseSessionResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good }
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.CloseAsync(It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask())
|
|
.Verifiable(Times.Once);
|
|
|
|
// Act
|
|
StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(StatusCodes.Good));
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task CloseAsyncShouldHandleErrorsDuringCloseSessionAsync()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
sut.SetConnected();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<CloseSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError))
|
|
.Verifiable(Times.Once);
|
|
sut.Channel
|
|
.Setup(c => c.CloseAsync(It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask())
|
|
.Verifiable(Times.Once);
|
|
|
|
// Act
|
|
StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.EqualTo(StatusCodes.BadUnexpectedError));
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task ReconnectAsyncShouldReconnectSuccessfullyAsync()
|
|
{
|
|
// Arrange
|
|
using var sut = SessionMock.Create();
|
|
sut.SetConnected();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
ByteString serverNonce = [1, 2, 3, 4];
|
|
|
|
sut.Channel
|
|
.Setup(c => c.ReconnectAsync(
|
|
It.IsAny<ITransportWaitingConnection>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask())
|
|
.Verifiable(Times.Once);
|
|
sut.Channel
|
|
.Setup(c => c.SupportedFeatures)
|
|
.Returns(TransportChannelFeatures.Reconnect);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ActivateSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ActivateSessionResponse
|
|
{
|
|
ServerNonce = serverNonce,
|
|
Results = [],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
await sut.ReconnectAsync(ct).ConfigureAwait(false);
|
|
|
|
Assert.That(sut.ServerNonce, Is.EqualTo(serverNonce));
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void ReconnectAsyncShouldHandleReconnectFailure()
|
|
{
|
|
using var sut = SessionMock.Create();
|
|
sut.SetConnected();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
sut.Channel
|
|
.Setup(c => c.ReconnectAsync(
|
|
It.IsAny<ITransportWaitingConnection>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError))
|
|
.Verifiable(Times.Once);
|
|
sut.Channel
|
|
.Setup(c => c.SupportedFeatures)
|
|
.Returns(TransportChannelFeatures.Reconnect);
|
|
|
|
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.ReconnectAsync(ct).ConfigureAwait(false));
|
|
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
[Explicit("Requires proper channel creation mocking")]
|
|
public void ReconnectAsyncShouldHandleNoSupportedFeatures()
|
|
{
|
|
using var sut = SessionMock.Create();
|
|
sut.SetConnected();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SupportedFeatures)
|
|
.Returns(TransportChannelFeatures.None);
|
|
|
|
// TODO: Should properly mock channel creation
|
|
|
|
Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.ReconnectAsync(ct).ConfigureAwait(false));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void ReconnectAsyncShouldHandleCancellation()
|
|
{
|
|
using var sut = SessionMock.Create();
|
|
sut.SetConnected();
|
|
|
|
sut.Channel
|
|
.Setup(c => c.ReconnectAsync(
|
|
It.IsAny<ITransportWaitingConnection>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ThrowsAsync(new TaskCanceledException())
|
|
.Verifiable(Times.Once);
|
|
sut.Channel
|
|
.Setup(c => c.SupportedFeatures)
|
|
.Returns(TransportChannelFeatures.Reconnect);
|
|
|
|
Assert.ThrowsAsync<TaskCanceledException>(
|
|
async () => await sut.ReconnectAsync(default).ConfigureAwait(false));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task ReconnectAsyncShouldHandleServerResponseWithNullNonceAsync()
|
|
{
|
|
using var sut = SessionMock.Create();
|
|
sut.SetConnected();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
sut.Channel
|
|
.Setup(c => c.ReconnectAsync(
|
|
It.IsAny<ITransportWaitingConnection>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask())
|
|
.Verifiable(Times.Once);
|
|
sut.Channel
|
|
.Setup(c => c.SupportedFeatures)
|
|
.Returns(TransportChannelFeatures.Reconnect);
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ActivateSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ActivateSessionResponse
|
|
{
|
|
ServerNonce = default,
|
|
Results = [],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
await sut.ReconnectAsync(ct).ConfigureAwait(false);
|
|
|
|
Assert.That(sut.ServerNonce.IsEmpty, Is.True);
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void ReconnectAsyncShouldThrowWithIncompatibleIdentity()
|
|
{
|
|
var ep = new EndpointDescription
|
|
{
|
|
SecurityMode = MessageSecurityMode.None,
|
|
SecurityPolicyUri = SecurityPolicies.None,
|
|
EndpointUrl = "opc.tcp://localhost:4840",
|
|
UserIdentityTokens =
|
|
[
|
|
new UserTokenPolicy
|
|
{
|
|
PolicyId = "T",
|
|
TokenType = UserTokenType.Certificate
|
|
}
|
|
]
|
|
};
|
|
using var sut = SessionMock.Create(ep);
|
|
sut.SetConnected();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.ReconnectAsync(ct).ConfigureAwait(false));
|
|
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenInvalid));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void ReconnectAsyncShouldThrowWithBadActivationResponse()
|
|
{
|
|
using var sut = SessionMock.Create();
|
|
sut.SetConnected();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
sut.Channel
|
|
.Setup(c => c.ReconnectAsync(
|
|
It.IsAny<ITransportWaitingConnection>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask())
|
|
.Verifiable(Times.Once);
|
|
sut.Channel
|
|
.Setup(c => c.SupportedFeatures)
|
|
.Returns(TransportChannelFeatures.Reconnect);
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ActivateSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ActivateSessionResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadSessionNotActivated },
|
|
Results = [],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.ReconnectAsync(ct).ConfigureAwait(false));
|
|
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadSessionNotActivated));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void ReconnectAsyncShouldThrowWhenTimingOut()
|
|
{
|
|
using var sut = SessionMock.Create();
|
|
sut.SetConnected();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
sut.Channel
|
|
.Setup(c => c.ReconnectAsync(
|
|
It.IsAny<ITransportWaitingConnection>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask())
|
|
.Verifiable(Times.Once);
|
|
sut.Channel
|
|
.Setup(c => c.SupportedFeatures)
|
|
.Returns(TransportChannelFeatures.Reconnect);
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<ActivateSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ThrowsAsync(new OperationCanceledException())
|
|
.Verifiable(Times.Once);
|
|
|
|
Assert.ThrowsAsync<OperationCanceledException>(
|
|
async () => await sut.ReconnectAsync(ct).ConfigureAwait(false));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public async Task OpenAsyncShouldOpenSessionSuccessfullyAsync()
|
|
{
|
|
// Arrange
|
|
var ep = new EndpointDescription
|
|
{
|
|
SecurityMode = MessageSecurityMode.None,
|
|
SecurityPolicyUri = SecurityPolicies.None,
|
|
EndpointUrl = "opc.tcp://localhost:4840",
|
|
UserIdentityTokens =
|
|
[
|
|
new UserTokenPolicy()
|
|
]
|
|
};
|
|
using var sut = SessionMock.Create(ep);
|
|
CancellationToken ct = CancellationToken.None;
|
|
ByteString serverNonce = [1, 2, 3, 4];
|
|
var authToken = NodeId.Parse("s=cookie");
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<CreateSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new CreateSessionResponse
|
|
{
|
|
ServerNonce = serverNonce,
|
|
SessionId = NodeId.Parse("s=connected"),
|
|
AuthenticationToken = authToken,
|
|
ServerEndpoints = [ep]
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.Is<ActivateSessionRequest>(r => r.RequestHeader.AuthenticationToken == authToken),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ActivateSessionResponse
|
|
{
|
|
ServerNonce = serverNonce,
|
|
Results = [],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
// Read limit and also first keep alive timers
|
|
// Server_ServerStatus_State is a ServerState enum which is wire-encoded as Int32.
|
|
// Using UInt32 here causes ClientBase.ValidateDataValue to log spurious
|
|
// BadTypeMismatch warnings on every keep-alive while the test runs.
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.Is<ReadRequest>(r => r.NodesToRead.Count == 1),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results =
|
|
[
|
|
new (new Variant((int)ServerState.Running))
|
|
],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.AtLeastOnce);
|
|
|
|
// Operation limits
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.Is<ReadRequest>(r => r.NodesToRead.Count == 27),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results = [.. Enumerable
|
|
.Range(0, 27)
|
|
.Select(_ => new DataValue(Variant.Null))],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
// Namespaces
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.Is<ReadRequest>(r => r.NodesToRead.Count == 2),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ReadResponse
|
|
{
|
|
Results =
|
|
[
|
|
new(ArrayOf.Create([Namespaces.OpcUa])),
|
|
new(ArrayOf.Empty<string>())
|
|
],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
var identity = new UserIdentity();
|
|
await sut.OpenAsync("test", identity, ct).ConfigureAwait(false);
|
|
|
|
Assert.That(sut.ServerNonce, Is.EqualTo(ByteString.From([1, 2, 3, 4])));
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void OpenAsyncShouldHandleCreateSessionSuccessButActivationError()
|
|
{
|
|
// Arrange
|
|
var ep = new EndpointDescription
|
|
{
|
|
SecurityMode = MessageSecurityMode.None,
|
|
SecurityPolicyUri = SecurityPolicies.None,
|
|
EndpointUrl = "opc.tcp://localhost:4840",
|
|
UserIdentityTokens =
|
|
[
|
|
new UserTokenPolicy()
|
|
]
|
|
};
|
|
using var sut = SessionMock.Create(ep);
|
|
CancellationToken ct = CancellationToken.None;
|
|
ByteString serverNonce = [1, 2, 3, 4];
|
|
var authToken = NodeId.Parse("s=cookie");
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<CreateSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new CreateSessionResponse
|
|
{
|
|
ServerNonce = serverNonce,
|
|
SessionId = NodeId.Parse("s=connected"),
|
|
AuthenticationToken = authToken,
|
|
ServerEndpoints = [ep]
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.Is<ActivateSessionRequest>(r => r.RequestHeader.AuthenticationToken == authToken),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ActivateSessionResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadSessionNotActivated },
|
|
ServerNonce = serverNonce,
|
|
Results = [],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<CloseSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new CloseSessionResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good }
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.CloseAsync(It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask())
|
|
.Verifiable(Times.Once);
|
|
|
|
var identity = new UserIdentity();
|
|
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.OpenAsync("test", identity, default).ConfigureAwait(false));
|
|
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadSessionNotActivated));
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void OpenAsyncShouldHandleCreateSessionSuccessButActivationErrorAndThenCloseAlsoFails()
|
|
{
|
|
// Arrange
|
|
var ep = new EndpointDescription
|
|
{
|
|
SecurityMode = MessageSecurityMode.None,
|
|
SecurityPolicyUri = SecurityPolicies.None,
|
|
EndpointUrl = "opc.tcp://localhost:4840",
|
|
UserIdentityTokens =
|
|
[
|
|
new UserTokenPolicy()
|
|
]
|
|
};
|
|
using var sut = SessionMock.Create(ep);
|
|
CancellationToken ct = CancellationToken.None;
|
|
ByteString serverNonce = [1, 2, 3, 4];
|
|
var authToken = NodeId.Parse("s=cookie");
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<CreateSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new CreateSessionResponse
|
|
{
|
|
ServerNonce = serverNonce,
|
|
SessionId = NodeId.Parse("s=connected"),
|
|
AuthenticationToken = authToken,
|
|
ServerEndpoints = [ep]
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.Is<ActivateSessionRequest>(r => r.RequestHeader.AuthenticationToken == authToken),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new ActivateSessionResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadSessionNotActivated },
|
|
ServerNonce = serverNonce,
|
|
Results = [],
|
|
DiagnosticInfos = []
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<CloseSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new CloseSessionResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadNotConnected }
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.CloseAsync(It.IsAny<CancellationToken>()))
|
|
.Throws(new ServiceResultException(StatusCodes.BadNotConnected))
|
|
.Verifiable(Times.Once);
|
|
|
|
var identity = new UserIdentity();
|
|
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.OpenAsync(
|
|
"test",
|
|
60000,
|
|
identity,
|
|
default,
|
|
true,
|
|
closeChannel: true,
|
|
default).ConfigureAwait(false));
|
|
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadSessionNotActivated));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void OpenAsyncShouldHandleSessionOpeningFailure()
|
|
{
|
|
var ep = new EndpointDescription
|
|
{
|
|
SecurityMode = MessageSecurityMode.None,
|
|
SecurityPolicyUri = SecurityPolicies.None,
|
|
EndpointUrl = "opc.tcp://localhost:4840",
|
|
UserIdentityTokens =
|
|
[
|
|
new UserTokenPolicy()
|
|
]
|
|
};
|
|
using var sut = SessionMock.Create(ep);
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<CreateSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError))
|
|
.Verifiable(Times.Exactly(2));
|
|
|
|
var identity = new UserIdentity();
|
|
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.OpenAsync("test", identity, default).ConfigureAwait(false));
|
|
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
#if FALSE // TODO: Enable when moving certificate loading into OpenAsync
|
|
[Test]
|
|
public void OpenAsyncShouldHandleBadSecurityPolicy()
|
|
{
|
|
var ep = new EndpointDescription
|
|
{
|
|
SecurityMode = MessageSecurityMode.None,
|
|
SecurityPolicyUri = "Bad",
|
|
EndpointUrl = "opc.tcp://localhost:4840",
|
|
UserIdentityTokens =
|
|
[
|
|
new UserTokenPolicy()
|
|
]
|
|
};
|
|
using var sut = SessionMock.Create(ep);
|
|
|
|
var identity = new UserIdentity();
|
|
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.OpenAsync("test", identity, default).ConfigureAwait(false));
|
|
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadSecurityPolicyRejected));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
#endif
|
|
|
|
[Test]
|
|
public void OpenAsyncShouldHandleBadIdentityTokenPolicy()
|
|
{
|
|
var ep = new EndpointDescription
|
|
{
|
|
SecurityMode = MessageSecurityMode.None,
|
|
SecurityPolicyUri = SecurityPolicies.None,
|
|
EndpointUrl = "opc.tcp://localhost:4840",
|
|
UserIdentityTokens =
|
|
[
|
|
new UserTokenPolicy
|
|
{
|
|
PolicyId = "PolicyId",
|
|
TokenType = UserTokenType.IssuedToken
|
|
}
|
|
]
|
|
};
|
|
using var sut = SessionMock.Create(ep);
|
|
|
|
var identity = new UserIdentity();
|
|
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.OpenAsync("test", identity, default).ConfigureAwait(false));
|
|
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenInvalid));
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void OpenAsyncShouldHandleCancellation()
|
|
{
|
|
using var sut = SessionMock.Create();
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<CreateSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ThrowsAsync(new TaskCanceledException())
|
|
.Verifiable(Times.Once);
|
|
|
|
var identity = new UserIdentity();
|
|
Assert.ThrowsAsync<TaskCanceledException>(
|
|
async () => await sut.OpenAsync("test", identity, default).ConfigureAwait(false));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void OpenAsyncShouldHandleInvalidServerResponse()
|
|
{
|
|
using var sut = SessionMock.Create();
|
|
CancellationToken ct = CancellationToken.None;
|
|
|
|
sut.Channel
|
|
.Setup(c => c.SendRequestAsync(
|
|
It.IsAny<CreateSessionRequest>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(new ValueTask<IServiceResponse>(new CreateSessionResponse
|
|
{
|
|
ServerNonce = default,
|
|
SessionId = NodeId.Parse("s=connected")
|
|
}))
|
|
.Verifiable(Times.Once);
|
|
|
|
var identity = new UserIdentity();
|
|
Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await sut.OpenAsync("test", identity, ct).ConfigureAwait(false));
|
|
|
|
sut.Channel.Verify();
|
|
}
|
|
|
|
[Test]
|
|
public void SaveShouldOnlySaveSpecifiedSubscriptions()
|
|
{
|
|
using var sut = SessionMock.Create();
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
|
|
using var subscription1 = new Subscription(telemetry, new SubscriptionOptions { DisplayName = "Subscription1" });
|
|
using var subscription2 = new Subscription(telemetry, new SubscriptionOptions { DisplayName = "Subscription2" });
|
|
using var subscription3 = new Subscription(telemetry, new SubscriptionOptions { DisplayName = "Subscription3" });
|
|
|
|
sut.AddSubscription(subscription1);
|
|
sut.AddSubscription(subscription2);
|
|
sut.AddSubscription(subscription3);
|
|
|
|
Assert.That(sut.SubscriptionCount, Is.EqualTo(3));
|
|
|
|
// Only save a subset of subscriptions (subscription1 and subscription3)
|
|
Subscription[] subscriptionsToSave = [subscription1, subscription3];
|
|
|
|
using var stream = new MemoryStream();
|
|
sut.Save(stream, subscriptionsToSave);
|
|
stream.Position = 0;
|
|
|
|
using var loadSession = SessionMock.Create();
|
|
var loadedSubscriptions = loadSession.Load(stream).ToList();
|
|
|
|
Assert.That(loadedSubscriptions, Has.Count.EqualTo(2), "Only the specified subscriptions should be saved");
|
|
Assert.That(loadedSubscriptions.Select(s => s.DisplayName), Is.EquivalentTo(["Subscription1", "Subscription3"]));
|
|
}
|
|
|
|
[Test]
|
|
public void SaveAndApplySessionConfigurationShouldPersistClientNonce()
|
|
{
|
|
var source = SessionMock.Create();
|
|
source.SetConnected();
|
|
|
|
byte[] clientNonce = [1, 3, 5, 7, 9];
|
|
byte[] serverNonce = [2, 4, 6, 8];
|
|
|
|
SetClientNonce(source, clientNonce);
|
|
SetServerNonce(source, serverNonce);
|
|
|
|
SessionConfiguration configuration = source.SaveSessionConfiguration();
|
|
|
|
Assert.That(configuration.ClientNonce.ToArray(), Is.EquivalentTo(clientNonce));
|
|
Assert.That(configuration.ServerNonce.ToArray(), Is.EquivalentTo(serverNonce));
|
|
|
|
var target = SessionMock.Create();
|
|
|
|
bool success = target.ApplySessionConfiguration(configuration);
|
|
|
|
Assert.That(success, Is.True);
|
|
Assert.That(GetClientNonce(target), Is.EquivalentTo(clientNonce));
|
|
Assert.That(GetServerNonce(target), Is.EquivalentTo(serverNonce));
|
|
}
|
|
|
|
private const BindingFlags PrivateInstance =
|
|
BindingFlags.NonPublic | BindingFlags.Instance;
|
|
|
|
private static void SetClientNonce(Session session, byte[] value)
|
|
{
|
|
typeof(Session)
|
|
.GetField("m_clientNonce", PrivateInstance)
|
|
.SetValue(session, value.ToArray());
|
|
}
|
|
|
|
private static void SetServerNonce(Session session, byte[] value)
|
|
{
|
|
typeof(Session)
|
|
.GetField("m_serverNonce", PrivateInstance)
|
|
.SetValue(session, ByteString.From(value));
|
|
}
|
|
|
|
private static byte[] GetClientNonce(Session session)
|
|
{
|
|
return typeof(Session)
|
|
.GetField("m_clientNonce", PrivateInstance)
|
|
.GetValue(session) is byte[] bytes
|
|
? [.. bytes]
|
|
: [];
|
|
}
|
|
|
|
private static byte[] GetServerNonce(Session session)
|
|
{
|
|
return ((ByteString)typeof(Session)
|
|
.GetField("m_serverNonce", PrivateInstance)
|
|
.GetValue(session)).ToArray();
|
|
}
|
|
}
|
|
}
|