mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 00:31:32 +08:00
9bfc780e14
# Description Update 55 NuGet dependencies to the latest stable releases available on 2026-10-19, preserving documented compatibility pins and framework-specific version bands. Existing OpenUSD prereleases remain unchanged because no newer releases are available. Migrate existing tests to NUnit 5: await asynchronous assertions, propagate async through helpers, handle nullable predicates, and replace obsolete `Order` attributes with sequencing dependencies. Sequencing dependencies use `AllowFailure = true`; filtering a dependent test also runs prerequisites. Adjust fixture-level parallelism where NUnit dependency scheduling requires it. Keep Roslyn API/runtime pins intact; select newer Immutable/Metadata runtime packages only for test and profiling hosts requiring them. Update the approved SDK pin to 10.0.402 across local, CI, and container builds. Document NUnit 5 conventions.
1080 lines
46 KiB
C#
1080 lines
46 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 Microsoft.Extensions.Logging.Abstractions;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Security.Certificates;
|
|
using Opc.Ua.Tests;
|
|
using ServerSession = Opc.Ua.Server.Session;
|
|
|
|
namespace Opc.Ua.Server.Tests
|
|
{
|
|
/// <summary>
|
|
/// Direct-construction unit tests for <see cref="Session"/> that exercise the
|
|
/// branches reachable without a full secure-channel crypto handshake:
|
|
/// constructor guards, request validation, diagnostic counters, locale
|
|
/// updates, continuation points and the anonymous / username identity token
|
|
/// validation and activation paths under <see cref="MessageSecurityMode.None"/>.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("Session")]
|
|
[Parallelizable]
|
|
public class SessionCoverageTests
|
|
{
|
|
private static readonly ICertificateFactory s_factory = DefaultCertificateFactory.Instance;
|
|
private static readonly string[] s_preferredLocales = ["en-US", "de-DE"];
|
|
private static readonly string[] s_singlePreferredLocale = ["en-US"];
|
|
|
|
private ITelemetryContext m_telemetry = null!;
|
|
private Mock<IServerInternal> m_serverMock = null!;
|
|
private Certificate m_serverCertificate = null!;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
m_telemetry = NUnitTelemetryContext.Create();
|
|
m_serverMock = new Mock<IServerInternal>();
|
|
m_serverMock.Setup(s => s.Telemetry).Returns(m_telemetry);
|
|
m_serverMock.Setup(s => s.NamespaceUris).Returns(new NamespaceTable());
|
|
var sessionManagerMock = new Mock<ISessionManager>();
|
|
m_serverMock.Setup(s => s.SessionManager).Returns(sessionManagerMock.Object);
|
|
m_serverCertificate = s_factory.CreateCertificate("CN=SessionCoverageServer").CreateForRSA();
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
m_serverCertificate?.Dispose();
|
|
}
|
|
|
|
private static EndpointDescription CreateEndpoint(
|
|
string securityPolicyUri = null!,
|
|
MessageSecurityMode securityMode = MessageSecurityMode.None,
|
|
UserTokenPolicy[]? tokens = null)
|
|
{
|
|
return new EndpointDescription
|
|
{
|
|
EndpointUrl = "opc.tcp://localhost:4840/Coverage",
|
|
SecurityMode = securityMode,
|
|
SecurityPolicyUri = securityPolicyUri ?? SecurityPolicies.None,
|
|
UserIdentityTokens = tokens != null ? new ArrayOf<UserTokenPolicy>(tokens) : default
|
|
};
|
|
}
|
|
|
|
private static OperationContext CreateContext(
|
|
EndpointDescription endpoint,
|
|
string channelId = "channel-1",
|
|
RequestType requestType = RequestType.ActivateSession)
|
|
{
|
|
var channelContext = new SecureChannelContext(channelId, endpoint, RequestEncoding.Binary);
|
|
var requestHeader = new RequestHeader();
|
|
return new OperationContext(requestHeader, channelContext, requestType, RequestLifetime.None);
|
|
}
|
|
|
|
private ServerSession CreateSession(
|
|
EndpointDescription endpoint,
|
|
string channelId = "channel-1",
|
|
Certificate? clientCertificate = null)
|
|
{
|
|
OperationContext context = CreateContext(endpoint, channelId);
|
|
return new ServerSession(
|
|
context,
|
|
m_serverMock.Object,
|
|
m_serverCertificate,
|
|
new NodeId(Guid.NewGuid()),
|
|
ByteString.Empty,
|
|
Nonce.CreateNonce(SecurityPolicies.None),
|
|
"CoverageSession",
|
|
new ApplicationDescription { ApplicationUri = "urn:coverage:client" },
|
|
endpoint.EndpointUrl!,
|
|
clientCertificate!,
|
|
[],
|
|
60_000,
|
|
10,
|
|
10);
|
|
}
|
|
|
|
[Test]
|
|
public void ConstructorWithNullContextThrows()
|
|
{
|
|
Assert.That(
|
|
() => new ServerSession(
|
|
null!,
|
|
m_serverMock.Object,
|
|
m_serverCertificate,
|
|
new NodeId(Guid.NewGuid()),
|
|
ByteString.Empty,
|
|
Nonce.CreateNonce(SecurityPolicies.None),
|
|
"s",
|
|
new ApplicationDescription(),
|
|
"opc.tcp://localhost:4840",
|
|
null!,
|
|
[],
|
|
60_000,
|
|
10,
|
|
10),
|
|
Throws.TypeOf<ArgumentNullException>());
|
|
}
|
|
|
|
[Test]
|
|
public void ConstructorWithNullServerThrows()
|
|
{
|
|
EndpointDescription endpoint = CreateEndpoint();
|
|
OperationContext context = CreateContext(endpoint);
|
|
|
|
Assert.That(
|
|
() => new ServerSession(
|
|
context,
|
|
null!,
|
|
m_serverCertificate,
|
|
new NodeId(Guid.NewGuid()),
|
|
ByteString.Empty,
|
|
Nonce.CreateNonce(SecurityPolicies.None),
|
|
"s",
|
|
new ApplicationDescription(),
|
|
endpoint.EndpointUrl!,
|
|
null!,
|
|
[],
|
|
60_000,
|
|
10,
|
|
10),
|
|
Throws.TypeOf<ArgumentNullException>());
|
|
}
|
|
|
|
[Test]
|
|
public void ConstructorInitializesDiagnostics()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
|
|
Assert.That(session.SecureChannelId, Is.EqualTo("channel-1"));
|
|
Assert.That(session.Activated, Is.False);
|
|
Assert.That(session.SessionDiagnostics, Is.Not.Null);
|
|
Assert.That(session.ReadDiagnostics(d => d.SessionName), Is.EqualTo("CoverageSession"));
|
|
Assert.That(session.Identity, Is.Not.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void SetUserTokenSecurityPolicyThenGetNewEphemeralKeyReturnsNullForNoPolicy()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
|
|
Assert.That(session.GetNewEphemeralKey(), Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void GetNewEphemeralKeyReturnsKeyWhenPolicySet()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
session.SetUserTokenSecurityPolicy(SecurityPolicies.Basic256Sha256);
|
|
|
|
EphemeralKeyType? key = session.GetNewEphemeralKey();
|
|
|
|
Assert.That(key, Is.Not.Null);
|
|
Assert.That(key!.PublicKey.IsEmpty, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void IsSecureChannelValidReflectsChannelId()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint(), channelId: "abc");
|
|
|
|
Assert.That(session.IsSecureChannelValid("abc"), Is.True);
|
|
Assert.That(session.IsSecureChannelValid("other"), Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void UpdateLocaleIdsReturnsTrueOnChangeAndFalseOnRepeat()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
|
|
Assert.That(session.UpdateLocaleIds(new ArrayOf<string>(s_preferredLocales)), Is.True);
|
|
Assert.That(session.UpdateLocaleIds(new ArrayOf<string>(s_preferredLocales)), Is.False);
|
|
Assert.That(session.PreferredLocales, Is.EqualTo(s_preferredLocales));
|
|
}
|
|
|
|
[Test]
|
|
public void UpdateLocaleIdsDropsNullLocaleIds()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
|
|
Assert.That(session.UpdateLocaleIds(new ArrayOf<string>(new string[] { null!, "de-DE" })), Is.True);
|
|
Assert.That(session.PreferredLocales, Has.Length.EqualTo(1));
|
|
Assert.That(session.PreferredLocales[0], Is.EqualTo("de-DE"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// SessionDiagnostics.LocaleIds reports the LocaleIds as specified by the client,
|
|
/// null entries included (Part 5 12.11); only PreferredLocales is filtered.
|
|
/// </summary>
|
|
[Test]
|
|
public void UpdateLocaleIdsKeepsClientLocaleIdsInDiagnostics()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
|
|
string[] first = [null!, "de-DE"];
|
|
Assert.That(session.UpdateLocaleIds(new ArrayOf<string>(first)), Is.True);
|
|
string[] diagnostics = [.. session.SessionDiagnostics.LocaleIds];
|
|
Assert.That(diagnostics, Is.EqualTo(first));
|
|
|
|
// a change of the null entries alone still reaches the diagnostics.
|
|
string[] second = ["de-DE", null!];
|
|
Assert.That(session.UpdateLocaleIds(new ArrayOf<string>(second)), Is.False);
|
|
diagnostics = [.. session.SessionDiagnostics.LocaleIds];
|
|
Assert.That(diagnostics, Is.EqualTo(second));
|
|
Assert.That(session.PreferredLocales, Has.Length.EqualTo(1));
|
|
Assert.That(session.PreferredLocales[0], Is.EqualTo("de-DE"));
|
|
}
|
|
|
|
[Test]
|
|
public void ValidateRequestWithNullHeaderThrows()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
var channelContext = new SecureChannelContext("channel-1", CreateEndpoint(), RequestEncoding.Binary);
|
|
|
|
Assert.That(
|
|
() => session.ValidateRequest(null!, channelContext, RequestType.Read),
|
|
Throws.TypeOf<ArgumentNullException>());
|
|
}
|
|
|
|
[Test]
|
|
public void ValidateRequestWithNullChannelOnCloseThrowsSessionIdInvalid()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
|
|
ServiceResultException? ex = Assert.Throws<ServiceResultException>(
|
|
() => session.ValidateRequest(new RequestHeader(), null!, RequestType.CloseSession));
|
|
Assert.That(ex!.StatusCode, Is.EqualTo(StatusCodes.BadSessionIdInvalid));
|
|
}
|
|
|
|
[Test]
|
|
public void ValidateRequestWithWrongChannelThrowsSecureChannelIdInvalid()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
var channelContext = new SecureChannelContext("wrong-channel", CreateEndpoint(), RequestEncoding.Binary);
|
|
|
|
ServiceResultException? ex = Assert.Throws<ServiceResultException>(
|
|
() => session.ValidateRequest(new RequestHeader(), channelContext, RequestType.Read));
|
|
Assert.That(ex!.StatusCode, Is.EqualTo(StatusCodes.BadSecureChannelIdInvalid));
|
|
}
|
|
|
|
[Test]
|
|
public void ValidateRequestWhenNotActivatedThrowsSessionNotActivated()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
var channelContext = new SecureChannelContext("channel-1", CreateEndpoint(), RequestEncoding.Binary);
|
|
|
|
ServiceResultException? ex = Assert.Throws<ServiceResultException>(
|
|
() => session.ValidateRequest(new RequestHeader(), channelContext, RequestType.Read));
|
|
Assert.That(ex!.StatusCode, Is.EqualTo(StatusCodes.BadSessionNotActivated));
|
|
}
|
|
|
|
[TestCase(RequestType.Read)]
|
|
[TestCase(RequestType.HistoryRead)]
|
|
[TestCase(RequestType.Write)]
|
|
[TestCase(RequestType.HistoryUpdate)]
|
|
[TestCase(RequestType.Call)]
|
|
[TestCase(RequestType.CreateMonitoredItems)]
|
|
[TestCase(RequestType.ModifyMonitoredItems)]
|
|
[TestCase(RequestType.SetMonitoringMode)]
|
|
[TestCase(RequestType.SetTriggering)]
|
|
[TestCase(RequestType.DeleteMonitoredItems)]
|
|
[TestCase(RequestType.CreateSubscription)]
|
|
[TestCase(RequestType.ModifySubscription)]
|
|
[TestCase(RequestType.SetPublishingMode)]
|
|
[TestCase(RequestType.Publish)]
|
|
[TestCase(RequestType.Republish)]
|
|
[TestCase(RequestType.TransferSubscriptions)]
|
|
[TestCase(RequestType.DeleteSubscriptions)]
|
|
[TestCase(RequestType.AddNodes)]
|
|
[TestCase(RequestType.AddReferences)]
|
|
[TestCase(RequestType.DeleteNodes)]
|
|
[TestCase(RequestType.DeleteReferences)]
|
|
[TestCase(RequestType.Browse)]
|
|
[TestCase(RequestType.BrowseNext)]
|
|
[TestCase(RequestType.TranslateBrowsePathsToNodeIds)]
|
|
[TestCase(RequestType.QueryFirst)]
|
|
[TestCase(RequestType.QueryNext)]
|
|
[TestCase(RequestType.RegisterNodes)]
|
|
[TestCase(RequestType.UnregisterNodes)]
|
|
public void ValidateRequestUpdatesDiagnosticCounterForRequestType(RequestType requestType)
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
var channelContext = new SecureChannelContext("channel-1", CreateEndpoint(), RequestEncoding.Binary);
|
|
|
|
// Non-activated session records the counter (error path) then throws.
|
|
Assert.That(
|
|
() => session.ValidateRequest(new RequestHeader(), channelContext, requestType),
|
|
Throws.TypeOf<ServiceResultException>());
|
|
Assert.That(session.ReadDiagnostics(d => d.TotalRequestCount.TotalCount), Is.GreaterThan(0));
|
|
}
|
|
|
|
[Test]
|
|
public void ValidateRequestWithUnknownRequestTypeThrowsUnexpected()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
var channelContext = new SecureChannelContext("channel-1", CreateEndpoint(), RequestEncoding.Binary);
|
|
|
|
Assert.That(
|
|
() => session.ValidateRequest(new RequestHeader(), channelContext, (RequestType)9999),
|
|
Throws.TypeOf<ServiceResultException>());
|
|
}
|
|
|
|
[Test]
|
|
public void ValidateDiagnosticInfoWithoutAdditionalInfoMaskDoesNothing()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
var header = new RequestHeader { ReturnDiagnostics = 0 };
|
|
|
|
Assert.That(() => session.ValidateDiagnosticInfo(header), Throws.Nothing);
|
|
Assert.That(header.ReturnDiagnostics, Is.Zero);
|
|
}
|
|
|
|
[Test]
|
|
public void SaveAndRestoreContinuationPointRoundTrips()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
var id = Guid.NewGuid();
|
|
var cp = new ContinuationPoint { Id = id };
|
|
|
|
session.ContinuationPoints.SaveBrowse(cp);
|
|
ContinuationPoint? restored = session.ContinuationPoints.RestoreBrowse(
|
|
new ByteString(id.ToByteArray()));
|
|
|
|
Assert.That(restored, Is.Not.Null);
|
|
Assert.That(restored!.Id, Is.EqualTo(id));
|
|
restored.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public void SaveContinuationPointWithNullThrows()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
|
|
Assert.That(
|
|
() => session.ContinuationPoints.SaveBrowse(null!),
|
|
Throws.TypeOf<ArgumentNullException>());
|
|
}
|
|
|
|
[Test]
|
|
public void RestoreContinuationPointReturnsNullForUnknownAndBadLength()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
session.ContinuationPoints.SaveBrowse(new ContinuationPoint { Id = Guid.NewGuid() });
|
|
|
|
Assert.That(session.ContinuationPoints.RestoreBrowse(new ByteString(new byte[] { 1, 2, 3 })), Is.Null);
|
|
Assert.That(session.ContinuationPoints.RestoreBrowse(new ByteString(Guid.NewGuid().ToByteArray())), Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void SaveContinuationPointEvictsOldestWhenOverCapacity()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
|
|
// Capacity is 10 (maxBrowseContinuationPoints); adding 12 evicts the oldest.
|
|
var first = new ContinuationPoint { Id = Guid.NewGuid() };
|
|
session.ContinuationPoints.SaveBrowse(first);
|
|
for (int i = 0; i < 11; i++)
|
|
{
|
|
session.ContinuationPoints.SaveBrowse(new ContinuationPoint { Id = Guid.NewGuid() });
|
|
}
|
|
|
|
Assert.That(
|
|
session.ContinuationPoints.RestoreBrowse(new ByteString(first.Id.ToByteArray())),
|
|
Is.Null,
|
|
"The oldest continuation point should have been evicted.");
|
|
}
|
|
|
|
[Test]
|
|
public void ClientApplicationUriReadsTheDiagnosticsUnderTheLock()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
|
|
// The four server-side callers that used to reach through SessionDiagnostics
|
|
// want exactly this, and it is read under the diagnostics lock rather than off
|
|
// a handed-out structure.
|
|
Assert.That(
|
|
session.ClientApplicationUri,
|
|
Is.EqualTo(session.ReadDiagnostics(d => d.ClientDescription?.ApplicationUri)));
|
|
}
|
|
|
|
[Test]
|
|
public void SessionNameDoesNotDependOnTheDiagnostics()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
|
|
// Assigned once during construction, so it is read from the field: a caller
|
|
// must not have to take the diagnostics lock for a value that never changes.
|
|
Assert.That(
|
|
session.SessionName,
|
|
Is.EqualTo(session.ReadDiagnostics(d => d.SessionName)));
|
|
}
|
|
|
|
[Test]
|
|
public void SaveAndRestoreHistoryContinuationPointRoundTrips()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
var id = Guid.NewGuid();
|
|
var payload = new TestHistoryContinuationPoint(id);
|
|
|
|
session.ContinuationPoints.SaveHistory(payload);
|
|
IHistoryContinuationPoint? restored = session.ContinuationPoints.RestoreHistory(new ByteString(id.ToByteArray()));
|
|
|
|
Assert.That(restored, Is.SameAs(payload));
|
|
}
|
|
|
|
[Test]
|
|
public void SaveHistoryContinuationPointWithNullThrows()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
|
|
Assert.That(
|
|
() => session.ContinuationPoints.SaveHistory(null!),
|
|
Throws.TypeOf<ArgumentNullException>());
|
|
}
|
|
|
|
[Test]
|
|
public void RestoreHistoryContinuationPointReturnsNullForBadLength()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
session.ContinuationPoints.SaveHistory(new TestHistoryContinuationPoint());
|
|
|
|
Assert.That(session.ContinuationPoints.RestoreHistory(new ByteString(new byte[] { 1, 2 })), Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void ConstructorWithNullChannelContextThrows()
|
|
{
|
|
var requestHeader = new RequestHeader();
|
|
var context = new OperationContext(
|
|
requestHeader, null, RequestType.ActivateSession, RequestLifetime.None);
|
|
|
|
ServiceResultException? ex = Assert.Throws<ServiceResultException>(
|
|
() => new ServerSession(
|
|
context,
|
|
m_serverMock.Object,
|
|
m_serverCertificate,
|
|
new NodeId(Guid.NewGuid()),
|
|
ByteString.Empty,
|
|
Nonce.CreateNonce(SecurityPolicies.None),
|
|
"s",
|
|
new ApplicationDescription(),
|
|
"opc.tcp://localhost:4840",
|
|
null!,
|
|
[],
|
|
60_000,
|
|
10,
|
|
10));
|
|
Assert.That(ex!.StatusCode, Is.EqualTo(StatusCodes.BadSecureChannelIdInvalid));
|
|
}
|
|
|
|
[TestCase(true)]
|
|
[TestCase(false)]
|
|
public async Task ValidateDiagnosticInfoUsesEffectiveSessionRoleForBothLevelsAsync(bool authorized)
|
|
{
|
|
UserTokenPolicy[] tokens =
|
|
[
|
|
new UserTokenPolicy { PolicyId = "anon", TokenType = UserTokenType.Anonymous }
|
|
];
|
|
EndpointDescription endpoint = CreateEndpoint(tokens: tokens);
|
|
using ServerSession session = CreateSession(endpoint, channelId: "channel-1");
|
|
OperationContext context = CreateContext(endpoint, channelId: "channel-1");
|
|
|
|
(IUserIdentityTokenHandler handler, UserTokenPolicy? _) = await session.ValidateBeforeActivateAsync(
|
|
context,
|
|
new SignatureData(),
|
|
default,
|
|
new SignatureData(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
|
|
var effectiveIdentity = new Mock<IUserIdentity>();
|
|
effectiveIdentity.Setup(i => i.TokenType).Returns(UserTokenType.Anonymous);
|
|
effectiveIdentity.Setup(i => i.DisplayName).Returns("diagnostic-test");
|
|
effectiveIdentity
|
|
.Setup(i => i.GrantedRoleIds)
|
|
.Returns(authorized ? [ObjectIds.WellKnownRole_SecurityAdmin] : [ObjectIds.WellKnownRole_Observer]);
|
|
|
|
session.Activate(
|
|
context,
|
|
handler!,
|
|
new UserIdentity(),
|
|
effectiveIdentity.Object,
|
|
default,
|
|
Nonce.CreateNonce(SecurityPolicies.None));
|
|
|
|
const uint mask = (uint)(DiagnosticsMasks.ServiceAdditionalInfo |
|
|
DiagnosticsMasks.OperationAdditionalInfo |
|
|
DiagnosticsMasks.UserPermissionAdditionalInfo);
|
|
var header = new RequestHeader { ReturnDiagnostics = mask };
|
|
|
|
session.ValidateDiagnosticInfo(header);
|
|
|
|
Assert.That(
|
|
(header.ReturnDiagnostics & (uint)DiagnosticsMasks.UserPermissionAdditionalInfo) != 0,
|
|
Is.EqualTo(authorized));
|
|
Assert.That(header.ReturnDiagnostics & (uint)DiagnosticsMasks.AdditionalInfo,
|
|
Is.EqualTo((uint)DiagnosticsMasks.AdditionalInfo));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ValidateBeforeActivateWithClientCertificateAndEmptySignatureThrows()
|
|
{
|
|
using Certificate clientCertificate =
|
|
s_factory.CreateCertificate("CN=CoverageClient").CreateForRSA();
|
|
EndpointDescription endpoint = CreateEndpoint(
|
|
SecurityPolicies.Basic256Sha256, MessageSecurityMode.Sign);
|
|
using ServerSession session = CreateSession(
|
|
endpoint, channelId: "channel-1", clientCertificate: clientCertificate);
|
|
OperationContext context = CreateContext(endpoint, channelId: "channel-1");
|
|
|
|
ServiceResultException? ex = await Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await session.ValidateBeforeActivateAsync(
|
|
context,
|
|
new SignatureData(),
|
|
default,
|
|
new SignatureData(),
|
|
CancellationToken.None).ConfigureAwait(false));
|
|
Assert.That(ex!.StatusCode, Is.EqualTo(StatusCodes.BadApplicationSignatureInvalid));
|
|
}
|
|
|
|
[Test]
|
|
public void SaveHistoryContinuationPointEvictsOldestWhenOverCapacity()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint());
|
|
var first = Guid.NewGuid();
|
|
|
|
session.ContinuationPoints.SaveHistory(new TestHistoryContinuationPoint(first));
|
|
for (int i = 0; i < 11; i++)
|
|
{
|
|
session.ContinuationPoints.SaveHistory(new TestHistoryContinuationPoint());
|
|
}
|
|
|
|
Assert.That(
|
|
session.ContinuationPoints.RestoreHistory(new ByteString(first.ToByteArray())),
|
|
Is.Null,
|
|
"The oldest history continuation point should have been evicted.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task ValidateBeforeActivateWithSecurityPolicyMismatchThrows()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint(SecurityPolicies.None));
|
|
OperationContext context = CreateContext(
|
|
CreateEndpoint(SecurityPolicies.Basic256Sha256, MessageSecurityMode.Sign));
|
|
|
|
ServiceResultException? ex = await Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await session.ValidateBeforeActivateAsync(
|
|
context,
|
|
new SignatureData(),
|
|
default,
|
|
new SignatureData(),
|
|
CancellationToken.None).ConfigureAwait(false));
|
|
Assert.That(ex!.StatusCode, Is.EqualTo(StatusCodes.BadSecurityPolicyRejected));
|
|
}
|
|
|
|
/// <summary>
|
|
/// PR review 4166517637: a stale prepared replacement nonce that the
|
|
/// activation detaches is disposed even when the channel check throws.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ValidateBeforeActivateFailureDisposesStalePreparedNonce()
|
|
{
|
|
if (SecurityPolicies.Default.GetInfo(SecurityPolicies.ECC_nistP256) == null)
|
|
{
|
|
Assert.Ignore("ECC_nistP256 is not supported on this platform.");
|
|
}
|
|
|
|
using ServerSession session = CreateSession(CreateEndpoint(SecurityPolicies.None));
|
|
OperationContext context = CreateContext(
|
|
CreateEndpoint(SecurityPolicies.Basic256Sha256, MessageSecurityMode.Sign));
|
|
|
|
Nonce stale = Nonce.CreateNonce(SecurityPolicies.ECC_nistP256);
|
|
System.Reflection.FieldInfo ecdhField = typeof(Nonce).GetField(
|
|
"m_ecdh",
|
|
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;
|
|
Assert.That(ecdhField.GetValue(stale), Is.Not.Null);
|
|
typeof(ServerSession).GetField(
|
|
"m_preparedUserTokenNonce",
|
|
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
|
|
.SetValue(session, stale);
|
|
|
|
ServiceResultException? ex = await Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await session.ValidateBeforeActivateAsync(
|
|
context,
|
|
new SignatureData(),
|
|
default,
|
|
new SignatureData(),
|
|
CancellationToken.None).ConfigureAwait(false));
|
|
Assert.That(ex!.StatusCode, Is.EqualTo(StatusCodes.BadSecurityPolicyRejected));
|
|
Assert.That(ecdhField.GetValue(stale), Is.Null, "The stale prepared nonce was not disposed.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task ValidateBeforeActivateWithWrongChannelThrows()
|
|
{
|
|
using ServerSession session = CreateSession(CreateEndpoint(), channelId: "channel-1");
|
|
OperationContext context = CreateContext(CreateEndpoint(), channelId: "other-channel");
|
|
|
|
ServiceResultException? ex = await Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await session.ValidateBeforeActivateAsync(
|
|
context,
|
|
new SignatureData(),
|
|
default,
|
|
new SignatureData(),
|
|
CancellationToken.None).ConfigureAwait(false));
|
|
Assert.That(ex!.StatusCode, Is.EqualTo(StatusCodes.BadSecureChannelIdInvalid));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ValidateBeforeActivateWithAnonymousTokenSucceedsAsync()
|
|
{
|
|
var tokens = new UserTokenPolicy[]
|
|
{
|
|
new() { PolicyId = "anon", TokenType = UserTokenType.Anonymous }
|
|
};
|
|
EndpointDescription endpoint = CreateEndpoint(tokens: tokens);
|
|
using ServerSession session = CreateSession(endpoint);
|
|
OperationContext context = CreateContext(endpoint);
|
|
|
|
(IUserIdentityTokenHandler handler, UserTokenPolicy? policy) =
|
|
await session.ValidateBeforeActivateAsync(
|
|
context,
|
|
new SignatureData(),
|
|
default,
|
|
new SignatureData(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
|
|
Assert.That(handler, Is.Not.Null);
|
|
Assert.That(policy, Is.Not.Null);
|
|
Assert.That(policy!.TokenType, Is.EqualTo(UserTokenType.Anonymous));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ValidateBeforeActivateAnonymousNotSupportedThrows()
|
|
{
|
|
var tokens = new UserTokenPolicy[]
|
|
{
|
|
new() { PolicyId = "user", TokenType = UserTokenType.UserName }
|
|
};
|
|
EndpointDescription endpoint = CreateEndpoint(tokens: tokens);
|
|
using ServerSession session = CreateSession(endpoint);
|
|
OperationContext context = CreateContext(endpoint);
|
|
|
|
ServiceResultException? ex = await Assert.ThrowsAsync<ServiceResultException>(
|
|
async () => await session.ValidateBeforeActivateAsync(
|
|
context,
|
|
new SignatureData(),
|
|
default,
|
|
new SignatureData(),
|
|
CancellationToken.None).ConfigureAwait(false));
|
|
Assert.That(ex!.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenRejected));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ValidateBeforeActivateWithUserNameTokenResolvesPolicyAsync()
|
|
{
|
|
var tokens = new UserTokenPolicy[]
|
|
{
|
|
new() {
|
|
PolicyId = "user",
|
|
TokenType = UserTokenType.UserName,
|
|
SecurityPolicyUri = SecurityPolicies.None
|
|
}
|
|
};
|
|
EndpointDescription endpoint = CreateEndpoint(tokens: tokens);
|
|
using ServerSession session = CreateSession(endpoint);
|
|
OperationContext context = CreateContext(endpoint);
|
|
|
|
var userToken = new UserNameIdentityToken
|
|
{
|
|
PolicyId = "user",
|
|
UserName = "bob",
|
|
Password = new ByteString(new byte[] { 1, 2, 3 })
|
|
};
|
|
|
|
(IUserIdentityTokenHandler handler, UserTokenPolicy? policy) =
|
|
await session.ValidateBeforeActivateAsync(
|
|
context,
|
|
new SignatureData(),
|
|
new ExtensionObject(userToken),
|
|
new SignatureData(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
|
|
Assert.That(handler, Is.Not.Null);
|
|
Assert.That(policy, Is.Not.Null);
|
|
Assert.That(policy!.TokenType, Is.EqualTo(UserTokenType.UserName));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ActivateFirstThenReactivateSwitchesChannelAsync()
|
|
{
|
|
var tokens = new UserTokenPolicy[]
|
|
{
|
|
new() { PolicyId = "anon", TokenType = UserTokenType.Anonymous }
|
|
};
|
|
EndpointDescription endpoint = CreateEndpoint(tokens: tokens);
|
|
using ServerSession session = CreateSession(endpoint, channelId: "channel-1");
|
|
OperationContext context = CreateContext(endpoint, channelId: "channel-1");
|
|
|
|
(IUserIdentityTokenHandler handler, UserTokenPolicy? _) = await session.ValidateBeforeActivateAsync(
|
|
context,
|
|
new SignatureData(),
|
|
default,
|
|
new SignatureData(),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
|
|
bool changed = session.Activate(
|
|
context,
|
|
handler!,
|
|
new UserIdentity(),
|
|
new UserIdentity(),
|
|
new ArrayOf<string>(s_singlePreferredLocale),
|
|
Nonce.CreateNonce(SecurityPolicies.None));
|
|
|
|
Assert.That(session.Activated, Is.True);
|
|
Assert.That(changed, Is.True);
|
|
|
|
// Re-activate on a different channel exercises the RE-ACTIVATION branch.
|
|
OperationContext reactivateContext = CreateContext(endpoint, channelId: "channel-2");
|
|
session.Activate(
|
|
reactivateContext,
|
|
handler!,
|
|
new UserIdentity(),
|
|
new UserIdentity(),
|
|
new ArrayOf<string>(s_singlePreferredLocale),
|
|
Nonce.CreateNonce(SecurityPolicies.None));
|
|
|
|
Assert.That(session.SecureChannelId, Is.EqualTo("channel-2"));
|
|
}
|
|
|
|
[Test]
|
|
public void ProcessCreateSessionAdditionalParametersReturnsNullForNullInput()
|
|
{
|
|
AdditionalParametersType? result =
|
|
SessionSecurityPolicyHelper.ProcessCreateSessionAdditionalParameters(
|
|
Mock.Of<ISession>(),
|
|
null!,
|
|
NullLogger<SessionCoverageTests>.Instance);
|
|
|
|
Assert.That(result, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void ProcessCreateSessionAdditionalParametersPreservesUnknownParameters()
|
|
{
|
|
var input = new AdditionalParametersType
|
|
{
|
|
Parameters =
|
|
[
|
|
new KeyValuePair
|
|
{
|
|
Key = new QualifiedName("Unknown"),
|
|
Value = Variant.From("value")
|
|
}
|
|
]
|
|
};
|
|
|
|
AdditionalParametersType? result =
|
|
SessionSecurityPolicyHelper.ProcessCreateSessionAdditionalParameters(
|
|
Mock.Of<ISession>(),
|
|
input,
|
|
NullLogger<SessionCoverageTests>.Instance);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.Parameters, Has.Count.EqualTo(1));
|
|
Assert.That(result.Parameters[0].Key, Is.EqualTo(new QualifiedName("Unknown")));
|
|
Assert.That(result.Parameters[0].Value.TryGetValue(out string value), Is.True);
|
|
Assert.That(value, Is.EqualTo("value"));
|
|
}
|
|
|
|
[Test]
|
|
public void ProcessCreateSessionAdditionalParametersRejectsUnsupportedEcdhPolicy()
|
|
{
|
|
var input = new AdditionalParametersType
|
|
{
|
|
Parameters =
|
|
[
|
|
new KeyValuePair
|
|
{
|
|
Key = QualifiedName.From(AdditionalParameterNames.ECDHPolicyUri),
|
|
Value = Variant.From(SecurityPolicies.None)
|
|
}
|
|
]
|
|
};
|
|
|
|
AdditionalParametersType? result =
|
|
SessionSecurityPolicyHelper.ProcessCreateSessionAdditionalParameters(
|
|
Mock.Of<ISession>(),
|
|
input,
|
|
NullLogger<SessionCoverageTests>.Instance);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.Parameters, Has.Count.EqualTo(1));
|
|
Assert.That(result.Parameters[0].Key, Is.EqualTo(QualifiedName.From(AdditionalParameterNames.ECDHKey)));
|
|
Assert.That(result.Parameters[0].Value.TryGetValue(out StatusCode status), Is.True);
|
|
Assert.That(status.Code, Is.EqualTo(StatusCodes.BadSecurityPolicyRejected));
|
|
}
|
|
|
|
[Test]
|
|
public void ProcessCreateSessionAdditionalParametersCreatesEcdhKeyForSupportedPolicy()
|
|
{
|
|
var key = new EphemeralKeyType();
|
|
const string policyUri = SecurityPolicies.ECC_nistP256;
|
|
var policies = new Mock<ISecurityPolicyRegistry>();
|
|
policies.Setup(p => p.GetInfo(policyUri)).Returns(SecurityPolicyInfo.ECC_nistP256);
|
|
var session = new Mock<ISession>();
|
|
session.Setup(s => s.GetNewEphemeralKey()).Returns(key);
|
|
var input = new AdditionalParametersType
|
|
{
|
|
Parameters =
|
|
[
|
|
new KeyValuePair
|
|
{
|
|
Key = QualifiedName.From(AdditionalParameterNames.ECDHPolicyUri),
|
|
Value = Variant.From(policyUri)
|
|
}
|
|
]
|
|
};
|
|
|
|
AdditionalParametersType? result =
|
|
SessionSecurityPolicyHelper.ProcessCreateSessionAdditionalParameters(
|
|
session.Object,
|
|
input,
|
|
NullLogger<SessionCoverageTests>.Instance,
|
|
policies.Object);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.Parameters, Has.Count.EqualTo(1));
|
|
Assert.That(result.Parameters[0].Key, Is.EqualTo(QualifiedName.From(AdditionalParameterNames.ECDHKey)));
|
|
Variant value = result.Parameters[0].Value;
|
|
Assert.That(value.TypeInfo, Is.EqualTo(TypeInfo.Scalars.ExtensionObject));
|
|
Assert.That(
|
|
value.TryGetStructure<EphemeralKeyType>(out EphemeralKeyType? actualKey),
|
|
Is.True);
|
|
Assert.That(actualKey, Is.SameAs(key));
|
|
session.Verify(s => s.SetUserTokenSecurityPolicy(policyUri), Times.Once);
|
|
session.Verify(s => s.GetNewEphemeralKey(), Times.Once);
|
|
}
|
|
|
|
[Test]
|
|
public void ProcessCreateSessionAdditionalParametersHonorsRawEcdhPlatformSupport()
|
|
{
|
|
const string policyUri = SecurityPolicies.ECC_nistP256;
|
|
var key = new EphemeralKeyType();
|
|
var session = new Mock<ISession>();
|
|
session.Setup(s => s.GetNewEphemeralKey()).Returns(key);
|
|
var input = new AdditionalParametersType
|
|
{
|
|
Parameters =
|
|
[
|
|
new KeyValuePair
|
|
{
|
|
Key = QualifiedName.From(AdditionalParameterNames.ECDHPolicyUri),
|
|
Value = Variant.From(policyUri)
|
|
}
|
|
]
|
|
};
|
|
|
|
AdditionalParametersType? result =
|
|
SessionSecurityPolicyHelper.ProcessCreateSessionAdditionalParameters(
|
|
session.Object,
|
|
input,
|
|
NullLogger<SessionCoverageTests>.Instance);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.Parameters, Has.Count.EqualTo(1));
|
|
Assert.That(result.Parameters[0].Key, Is.EqualTo(QualifiedName.From(AdditionalParameterNames.ECDHKey)));
|
|
Variant value = result.Parameters[0].Value;
|
|
Assert.That(SecurityPolicies.Default.GetInfo(policyUri), Is.Not.Null);
|
|
Assert.That(value.TypeInfo, Is.EqualTo(TypeInfo.Scalars.ExtensionObject));
|
|
Assert.That(value.TryGetStructure<EphemeralKeyType>(out EphemeralKeyType? actualKey), Is.True);
|
|
Assert.That(actualKey, Is.SameAs(key));
|
|
session.Verify(s => s.SetUserTokenSecurityPolicy(policyUri), Times.Once);
|
|
session.Verify(s => s.GetNewEphemeralKey(), Times.Once);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The response carries only the parameters the server produces; request
|
|
/// parameters such as "Existing" are not echoed back (OPC 10000-6 6.8.2).
|
|
/// </summary>
|
|
[Test]
|
|
public void ProcessActivateSessionAdditionalParametersReturnsOnlyTheNewKey()
|
|
{
|
|
var key = new EphemeralKeyType();
|
|
var session = new Mock<ISession>();
|
|
session.Setup(s => s.GetNewEphemeralKey()).Returns(key);
|
|
var input = new AdditionalParametersType
|
|
{
|
|
Parameters =
|
|
[
|
|
new KeyValuePair
|
|
{
|
|
Key = new QualifiedName("Existing"),
|
|
Value = Variant.From(1)
|
|
}
|
|
]
|
|
};
|
|
|
|
AdditionalParametersType result =
|
|
SessionSecurityPolicyHelper.ProcessActivateSessionAdditionalParameters(
|
|
session.Object,
|
|
input);
|
|
|
|
Assert.That(result, Is.Not.SameAs(input));
|
|
Assert.That(result.Parameters, Has.Count.EqualTo(1));
|
|
Assert.That(result.Parameters[0].Key, Is.EqualTo(QualifiedName.From(AdditionalParameterNames.ECDHKey)));
|
|
Variant value = result.Parameters[0].Value;
|
|
Assert.That(value.TypeInfo, Is.EqualTo(TypeInfo.Scalars.ExtensionObject));
|
|
Assert.That(
|
|
value.TryGetStructure<EphemeralKeyType>(out EphemeralKeyType? actualKey),
|
|
Is.True);
|
|
Assert.That(actualKey, Is.SameAs(key));
|
|
session.Verify(s => s.GetNewEphemeralKey(), Times.Once);
|
|
}
|
|
|
|
[Test]
|
|
public void ProcessActivateSessionAdditionalParametersReturnsNoParametersWhenNoKeyAvailable()
|
|
{
|
|
var session = new Mock<ISession>();
|
|
session.Setup(s => s.GetNewEphemeralKey()).Returns((EphemeralKeyType?)null);
|
|
var input = new AdditionalParametersType();
|
|
|
|
AdditionalParametersType result =
|
|
SessionSecurityPolicyHelper.ProcessActivateSessionAdditionalParameters(
|
|
session.Object,
|
|
input);
|
|
|
|
Assert.That(result.Parameters.IsEmpty, Is.True);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A client that omits the additional header still receives a new key once
|
|
/// the session uses an ECC user token policy (OPC 10000-6 6.8.2).
|
|
/// </summary>
|
|
[Test]
|
|
public void ProcessActivateSessionAdditionalParametersRotatesKeyWithoutRequestParameters()
|
|
{
|
|
var key = new EphemeralKeyType();
|
|
var session = new Mock<ISession>();
|
|
session.Setup(s => s.GetNewEphemeralKey()).Returns(key);
|
|
|
|
AdditionalParametersType result =
|
|
SessionSecurityPolicyHelper.ProcessActivateSessionAdditionalParameters(
|
|
session.Object,
|
|
null);
|
|
|
|
Assert.That(result.Parameters, Has.Count.EqualTo(1));
|
|
Assert.That(result.Parameters[0].Key, Is.EqualTo(QualifiedName.From(AdditionalParameterNames.ECDHKey)));
|
|
Assert.That(
|
|
result.Parameters[0].Value.TryGetStructure<EphemeralKeyType>(out EphemeralKeyType? actualKey),
|
|
Is.True);
|
|
Assert.That(actualKey, Is.SameAs(key));
|
|
}
|
|
|
|
/// <summary>
|
|
/// A valid ECDHPolicyUri in ActivateSession selects the policy of the returned
|
|
/// key (OPC 10000-6 6.8.2).
|
|
/// </summary>
|
|
[Test]
|
|
public void ProcessActivateSessionAdditionalParametersHonorsEcdhPolicyUri()
|
|
{
|
|
const string policyUri = SecurityPolicies.ECC_nistP256;
|
|
var key = new EphemeralKeyType();
|
|
var policies = new Mock<ISecurityPolicyRegistry>();
|
|
policies.Setup(p => p.GetInfo(policyUri)).Returns(SecurityPolicyInfo.ECC_nistP256);
|
|
var session = new Mock<ISession>();
|
|
session.Setup(s => s.GetNewEphemeralKey()).Returns(key);
|
|
var input = new AdditionalParametersType
|
|
{
|
|
Parameters =
|
|
[
|
|
new KeyValuePair
|
|
{
|
|
Key = QualifiedName.From(AdditionalParameterNames.ECDHPolicyUri),
|
|
Value = Variant.From(policyUri)
|
|
}
|
|
]
|
|
};
|
|
|
|
AdditionalParametersType result =
|
|
SessionSecurityPolicyHelper.ProcessActivateSessionAdditionalParameters(
|
|
session.Object,
|
|
input,
|
|
NullLogger<SessionCoverageTests>.Instance,
|
|
policies.Object);
|
|
|
|
Assert.That(result.Parameters, Has.Count.EqualTo(1));
|
|
Assert.That(result.Parameters[0].Key, Is.EqualTo(QualifiedName.From(AdditionalParameterNames.ECDHKey)));
|
|
Assert.That(
|
|
result.Parameters[0].Value.TryGetStructure<EphemeralKeyType>(out EphemeralKeyType? actualKey),
|
|
Is.True);
|
|
Assert.That(actualKey, Is.SameAs(key));
|
|
session.Verify(s => s.SetUserTokenSecurityPolicy(policyUri), Times.Once);
|
|
session.Verify(s => s.GetNewEphemeralKey(), Times.Once);
|
|
}
|
|
|
|
/// <summary>
|
|
/// An unsupported ECDHPolicyUri in ActivateSession is answered with
|
|
/// Bad_SecurityPolicyRejected and leaves the session's policy alone.
|
|
/// </summary>
|
|
[Test]
|
|
public void ProcessActivateSessionAdditionalParametersRejectsUnsupportedEcdhPolicy()
|
|
{
|
|
var session = new Mock<ISession>();
|
|
var input = new AdditionalParametersType
|
|
{
|
|
Parameters =
|
|
[
|
|
new KeyValuePair
|
|
{
|
|
Key = QualifiedName.From(AdditionalParameterNames.ECDHPolicyUri),
|
|
Value = Variant.From(SecurityPolicies.None)
|
|
}
|
|
]
|
|
};
|
|
|
|
AdditionalParametersType result =
|
|
SessionSecurityPolicyHelper.ProcessActivateSessionAdditionalParameters(
|
|
session.Object,
|
|
input,
|
|
NullLogger<SessionCoverageTests>.Instance);
|
|
|
|
Assert.That(result.Parameters, Has.Count.EqualTo(1));
|
|
Assert.That(result.Parameters[0].Key, Is.EqualTo(QualifiedName.From(AdditionalParameterNames.ECDHKey)));
|
|
Assert.That(result.Parameters[0].Value.TryGetValue(out StatusCode status), Is.True);
|
|
Assert.That(status.Code, Is.EqualTo(StatusCodes.BadSecurityPolicyRejected));
|
|
session.Verify(s => s.SetUserTokenSecurityPolicy(It.IsAny<string>()), Times.Never);
|
|
session.Verify(s => s.GetNewEphemeralKey(), Times.Never);
|
|
}
|
|
}
|
|
}
|