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