Files
UA-.NETStandard/tests/Opc.Ua.Client.Tests/Session/SessionClientBatchTests.cs
romanett 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# Description

Nullable reference types are now enabled once, in `common.props`, and
every project in the repository builds warning-clean with them.

**Configuration**
- `<Nullable>enable</Nullable>` lives in `common.props`. All per-project
`<Nullable>` settings are removed: 178 project files (enable,
annotations, and the `disable` in the three Aot test projects).
- 419 per-file `#nullable enable`/`restore` directives and the 4
`#nullable disable` directives are removed.
- Kept on purpose:
- The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type
Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`,
`PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the
project setting in generated code, so the directive is required there.
- `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against
the 1.5.378 packages and does not import the repository props.
- `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now
describe the central setting.

**Migration** (the 43 unset and 13 annotations-only projects, roughly
10k diagnostics)
- **Production code: annotated at the declarations.** Covers
`tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the
LDS sample.
- Members, parameters and returns are nullable where null actually
flows.
- Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`,
`IsMethodTypeDesign`).
- `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take
`params object?[]`. `Template.AddReplacement` takes `string?` (rendering
already skips null replacements).
- `IRegisteredServerStore.RegisterAsync` returns
`Task<RegistrationEntry?>`: it returns null when a registration is
removed.
- Dereferences backed by an invariant (for example, the validated model)
use `!`, so runtime behaviour is unchanged.
- **Behavioural changes in production code** (small, deliberate):
- `NodeSetToModelDesign` throws `InvalidOperationException` when the
file is not a NodeSet. Before, it threw a NullReferenceException on the
next line.
- `ModelDesignValidator` starts its lookup tables empty instead of null.
A redundant `if (m_nodes == null) return;` guard is gone.
- **Test code: minimal annotations.**
  - `?` on values that can be null, `!` where the test asserts non-null.
  - `= null!` for fixture members assigned in SetUp.
- `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the
`IEncodeable` constraint.
- A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`)
folded into the adjacent `IsNull`/`Count` checks.

**Notes for reviewers**
- The diff is large (~1,500 files) but almost entirely mechanical: `?`,
`!` and directive removal. The non-annotation changes are listed above.
- Known follow-up, not done here:
`IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay
non-nullable even though the validator returns null before it loads a
dictionary. Making them nullable would ripple through every generator
call site.

## Related Issues

- Fixes #4439

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

The "no new warnings" box covers net10.0 and net48: full non-incremental
builds of `UA.slnx`, `tools/SourceGeneration.slnx`,
`tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0
warnings and 0 errors under the repository's warnings-as-errors
settings. net9.0 and net8.0 also had no nullable diagnostics, but they
were not rebuilt after the final fixes. Tests have not been run locally
yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:48:13 +02:00

5674 lines
231 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Moq;
using NUnit.Framework;
using Opc.Ua.Client.TestFramework;
namespace Opc.Ua.Client.Tests
{
/// <summary>
/// Client tests.
/// </summary>
[TestFixture]
[Category("Client")]
[Category("SessionClient")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[MemoryDiagnoser]
[DisassemblyDiagnoser]
public sealed class SessionClientBatchTests
{
[DatapointSource]
public static readonly RequestHeader[] RequestHeaders =
[
null!,
new RequestHeader(),
new RequestHeader { AuditEntryId = "audit-entry-id" }
];
[Theory]
public async Task ActivateSessionAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
var clientSignature = new SignatureData();
var localeIds = new List<string>();
var userIdentityToken = new ExtensionObject();
var userTokenSignature = new SignatureData();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ActivateSessionRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new ActivateSessionResponse())
.Verifiable(Times.Once);
ActivateSessionResponse response = await sessionMock.ActivateSessionAsync(
requestHeader,
clientSignature,
default,
localeIds,
userIdentityToken,
userTokenSignature,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void ActivateSessionAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
var clientSignature = new SignatureData();
var localeIds = new List<string>();
var userIdentityToken = new ExtensionObject();
var userTokenSignature = new SignatureData();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ActivateSessionRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new ActivateSessionResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.ActivateSessionAsync(
requestHeader,
clientSignature,
default,
localeIds,
userIdentityToken,
userTokenSignature,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void ActivateSessionAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
var clientSignature = new SignatureData();
var localeIds = new List<string>();
var userIdentityToken = new ExtensionObject();
var userTokenSignature = new SignatureData();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ActivateSessionRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.ActivateSessionAsync(
requestHeader,
clientSignature,
default,
localeIds,
userIdentityToken,
userTokenSignature,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task AddNodesAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
var nodesToAdd = Enumerable.Repeat(new AddNodesItem(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerNodeManagement = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is AddNodesRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddNodesResponse
{
Results = Enumerable.Repeat(new AddNodesResult(), 10).ToArrayOf()
})
.ReturnsAsync(new AddNodesResponse
{
Results = Enumerable.Repeat(new AddNodesResult(), 5).ToArrayOf()
});
AddNodesResponse response = await sessionMock.AddNodesAsync(
requestHeader,
nodesToAdd,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void AddNodesAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
var nodesToAdd = Enumerable.Repeat(new AddNodesItem(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerNodeManagement = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is AddNodesRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddNodesResponse
{
Results = Enumerable.Repeat(new AddNodesResult(), 10).ToArrayOf()
})
.ReturnsAsync(new AddNodesResponse
{
Results = Enumerable.Repeat(new AddNodesResult(), 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.AddNodesAsync(
requestHeader,
nodesToAdd,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task AddNodesAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
ArrayOf<AddNodesItem> nodesToAdd = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is AddNodesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new AddNodesResponse())
.Verifiable(Times.Once);
AddNodesResponse response = await sessionMock.AddNodesAsync(
requestHeader,
nodesToAdd,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void AddNodesAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
ArrayOf<AddNodesItem> nodesToAdd = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is AddNodesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new AddNodesResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.AddNodesAsync(
requestHeader,
nodesToAdd,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void AddNodesAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
ArrayOf<AddNodesItem> nodesToAdd = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is AddNodesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.AddNodesAsync(requestHeader,
nodesToAdd, ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task AddNodesAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
ArrayOf<AddNodesItem> nodesToAdd = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is AddNodesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new AddNodesResponse
{
Results = [new AddNodesResult { StatusCode = StatusCodes.Good }],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
AddNodesResponse response = await sessionMock.AddNodesAsync(
requestHeader,
nodesToAdd,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task AddReferencesAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
var referencesToAdd = Enumerable.Repeat(new AddReferencesItem(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerNodeManagement = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is AddReferencesRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddReferencesResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
})
.ReturnsAsync(new AddReferencesResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 5).ToArrayOf()
});
AddReferencesResponse response = await sessionMock.AddReferencesAsync(
requestHeader,
referencesToAdd,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void AddReferencesAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
var referencesToAdd = Enumerable.Repeat(new AddReferencesItem(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerNodeManagement = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is AddReferencesRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddReferencesResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
})
.ReturnsAsync(new AddReferencesResponse
{
Results = Enumerable.Repeat(StatusCodes.Bad, 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.AddReferencesAsync(
requestHeader,
referencesToAdd,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task AddReferencesAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
ArrayOf<AddReferencesItem> referencesToAdd = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is AddReferencesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new AddReferencesResponse())
.Verifiable(Times.Once);
AddReferencesResponse response = await sessionMock.AddReferencesAsync(requestHeader,
referencesToAdd, ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void AddReferencesAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
ArrayOf<AddReferencesItem> referencesToAdd = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is AddReferencesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new AddReferencesResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.AddReferencesAsync(
requestHeader,
referencesToAdd,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void AddReferencesAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
ArrayOf<AddReferencesItem> referencesToAdd = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is AddReferencesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.AddReferencesAsync(
requestHeader,
referencesToAdd,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task AddReferencesAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
ArrayOf<AddReferencesItem> referencesToAdd = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is AddReferencesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new AddReferencesResponse
{
Results = [StatusCodes.Good],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
AddReferencesResponse response = await sessionMock.AddReferencesAsync(
requestHeader,
referencesToAdd,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task BrowseAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
var view = new ViewDescription();
const uint requestedMaxReferencesPerNode = 10u;
var nodesToBrowse = Enumerable.Repeat(new BrowseDescription(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerBrowse = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 10).ToArrayOf()
})
.ReturnsAsync(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 5).ToArrayOf()
});
BrowseResponse response = await sessionMock.BrowseAsync(
requestHeader,
view,
requestedMaxReferencesPerNode,
nodesToBrowse,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
/// <summary>
/// When a later batch fails, the continuation points returned by the
/// completed batches are released before the error propagates (L3-2).
/// </summary>
[Test]
public void BrowseAsyncReleasesContinuationPointsOfCompletedBatchesOnFailure()
{
var nodesToBrowse = Enumerable.Repeat(new BrowseDescription(), 15).ToArrayOf();
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerBrowse = 10;
var firstBatch = Enumerable.Range(0, 10).Select(_ => new BrowseResult()).ToList();
firstBatch[3].ContinuationPoint = ByteString.From([1, 2, 3]);
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new BrowseResponse { Results = firstBatch.ToArrayOf() })
.ThrowsAsync(new ServiceResultException(StatusCodes.BadTimeout));
var released = new List<BrowseNextRequest>();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseNextRequest),
It.IsAny<CancellationToken>()))
.Returns<IServiceRequest, CancellationToken>((r, _) =>
{
var request = (BrowseNextRequest)r;
released.Add(request);
return new ValueTask<IServiceResponse>(new BrowseNextResponse
{
Results = Enumerable.Repeat(new BrowseResult(), request.ContinuationPoints.Count)
.ToArrayOf()
});
});
ServiceResultException sre = Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.BrowseAsync(
null,
new ViewDescription(),
1,
nodesToBrowse,
CancellationToken.None).ConfigureAwait(false));
Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadTimeout));
Assert.That(released, Has.Count.EqualTo(1));
Assert.That(released[0].ReleaseContinuationPoints, Is.True);
Assert.That(released[0].ContinuationPoints.Count, Is.EqualTo(1));
Assert.That(released[0].ContinuationPoints[0], Is.EqualTo(ByteString.From([1, 2, 3])));
}
/// <summary>
/// Monitored items created by completed batches are deleted when a later
/// batch fails (L3-2).
/// </summary>
[Test]
public void CreateMonitoredItemsAsyncDeletesItemsOfCompletedBatchesOnFailure()
{
var itemsToCreate = Enumerable.Repeat(new MonitoredItemCreateRequest(), 15).ToArrayOf();
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxMonitoredItemsPerCall = 10;
ArrayOf<MonitoredItemCreateResult> firstBatch = Enumerable.Range(1, 10)
.Select(ii => new MonitoredItemCreateResult
{
StatusCode = ii == 5 ? StatusCodes.BadNodeIdUnknown : StatusCodes.Good,
MonitoredItemId = (uint)ii
})
.ToArrayOf();
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CreateMonitoredItemsRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new CreateMonitoredItemsResponse { Results = firstBatch })
.ThrowsAsync(new ServiceResultException(StatusCodes.BadTooManyOperations));
var deleted = new List<DeleteMonitoredItemsRequest>();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteMonitoredItemsRequest),
It.IsAny<CancellationToken>()))
.Returns<IServiceRequest, CancellationToken>((r, _) =>
{
var request = (DeleteMonitoredItemsRequest)r;
deleted.Add(request);
return new ValueTask<IServiceResponse>(new DeleteMonitoredItemsResponse
{
Results = Enumerable.Repeat((StatusCode)StatusCodes.Good, request.MonitoredItemIds.Count)
.ToArrayOf()
});
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.CreateMonitoredItemsAsync(
null,
42,
TimestampsToReturn.Both,
itemsToCreate,
CancellationToken.None).ConfigureAwait(false));
Assert.That(deleted, Has.Count.EqualTo(1));
Assert.That(deleted[0].SubscriptionId, Is.EqualTo(42u));
Assert.That(deleted[0].MonitoredItemIds.ToArray(),
Is.EqualTo(new uint[] { 1, 2, 3, 4, 6, 7, 8, 9, 10 }));
}
/// <summary>
/// Padding missing diagnostics must not skip rebasing the string table
/// indexes of the batch diagnostics (L3-3).
/// </summary>
[Test]
public void AddResponsesRebasesStringTableIndexesWhenPadding()
{
System.Reflection.MethodInfo addResponses = typeof(SessionClientBatched)
.GetMethod(
"AddResponses",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!
.MakeGenericMethod(typeof(StatusCode));
// An earlier batch (or the other list of SetTriggering) already filled
// the shared string table, but returned no diagnostics for this list.
var results = new List<StatusCode> { StatusCodes.Good };
var diagnosticInfos = new List<DiagnosticInfo>();
var stringTable = new List<string> { "a", "b" };
var batchDiagnostic = new DiagnosticInfo { SymbolicId = 0, LocalizedText = 1 };
string[] batchStrings = ["c", "d"];
string[] expectedStrings = ["a", "b", "c", "d"];
object[] args =
[
results,
diagnosticInfos,
stringTable,
new StatusCode[] { StatusCodes.BadNodeIdUnknown }.ToArrayOf(),
new[] { batchDiagnostic }.ToArrayOf(),
batchStrings.ToArrayOf()
];
addResponses.Invoke(null, args);
Assert.That(diagnosticInfos, Has.Count.EqualTo(2));
Assert.That(diagnosticInfos[0], Is.Null);
Assert.That(stringTable, Is.EqualTo(expectedStrings));
Assert.That(batchDiagnostic.SymbolicId, Is.EqualTo(2));
Assert.That(batchDiagnostic.LocalizedText, Is.EqualTo(3));
}
[Theory]
public async Task BrowseAsyncShouldContainTraceContextInRequestHeaderAsync(
RequestHeader requestHeader)
{
var view = new ViewDescription();
var nodesToBrowse = Enumerable.Repeat(new BrowseDescription(), 5).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerBrowse = 10;
ActivitySource activitySource = sessionMock.MessageContext.Telemetry.GetActivitySource();
ActivitySource.AddActivityListener(new ActivityListener
{
ShouldListenTo = a => a.Name == activitySource.Name,
Sample = (ref _) => ActivitySamplingResult.AllData,
SampleUsingParentId = (ref _) => ActivitySamplingResult.AllData
});
Assert.That(activitySource.HasListeners(), Is.True);
sessionMock.ActivityTraceFlags = ClientTraceFlags.Traces;
using Activity activity = activitySource.StartActivity("TestActivity")!;
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.IsAny<CancellationToken>()))
.Returns<IServiceRequest, CancellationToken>((r, ct) =>
{
requestHeader = r.RequestHeader;
return new ValueTask<IServiceResponse>(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 5).ToArrayOf()
});
});
BrowseResponse response = await sessionMock.BrowseAsync(
requestHeader,
view,
0,
nodesToBrowse,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
if (requestHeader != null)
{
Assert.That(requestHeader.AdditionalHeader.IsNull, Is.False);
Assert.That(requestHeader.AdditionalHeader.TryGetValue(
out AdditionalParametersType? additionalParameters), Is.True);
Assert.That(additionalParameters!.Parameters.Find(k => k.Key == "SpanContext"), Is.Not.Null);
}
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Theory]
public async Task BrowseAsyncShouldContainTraceContextInRequestHeaderWhenBatchedAsync(
RequestHeader requestHeader)
{
var view = new ViewDescription();
var nodesToBrowse = Enumerable.Repeat(new BrowseDescription(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerBrowse = 10;
ActivitySource activitySource = sessionMock.MessageContext.Telemetry.GetActivitySource();
ActivitySource.AddActivityListener(new ActivityListener
{
ShouldListenTo = a => a.Name == activitySource.Name,
Sample = (ref _) => ActivitySamplingResult.AllData,
SampleUsingParentId = (ref _) => ActivitySamplingResult.AllData
});
Assert.That(activitySource.HasListeners(), Is.True);
sessionMock.ActivityTraceFlags = ClientTraceFlags.Traces;
using Activity activity = activitySource.StartActivity("TestActivity")!;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 10).ToArrayOf()
})
.ReturnsAsync(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 5).ToArrayOf()
});
BrowseResponse response = await sessionMock.BrowseAsync(
requestHeader,
view,
0,
nodesToBrowse,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
if (requestHeader != null)
{
Assert.That(requestHeader.AdditionalHeader.IsNull, Is.False);
Assert.That(requestHeader.AdditionalHeader.TryGetValue(
out AdditionalParametersType? additionalParameters), Is.True);
Assert.That(additionalParameters, Is.Not.Null);
Assert.That(additionalParameters.Parameters.Find(k => k.Key == "SpanContext"), Is.Not.Null);
}
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void BrowseAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
var view = new ViewDescription();
const uint requestedMaxReferencesPerNode = 10u;
var nodesToBrowse = Enumerable.Repeat(new BrowseDescription(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerBrowse = 10;
sessionMock.ActivityTraceFlags = ClientTraceFlags.Log;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 10).ToArrayOf()
})
.ReturnsAsync(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.BrowseAsync(
requestHeader,
view,
requestedMaxReferencesPerNode,
nodesToBrowse,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task BrowseAsyncShouldHandleDiagnosticInfosCorrectlyAsync(
RequestHeader requestHeader)
{
var view = new ViewDescription();
var nodesToBrowse = Enumerable.Repeat(new BrowseDescription(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerBrowse = 10;
var diagnosticInfo1 = new DiagnosticInfo
{
SymbolicId = 1,
NamespaceUri = 2,
Locale = 3,
LocalizedText = 4
};
var diagnosticInfo2 = new DiagnosticInfo
{
SymbolicId = 5,
NamespaceUri = 6,
Locale = 7,
LocalizedText = 8,
InnerDiagnosticInfo = diagnosticInfo1
};
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 10).ToArrayOf(),
DiagnosticInfos = Enumerable.Repeat(diagnosticInfo1, 10).ToArrayOf()
})
.ReturnsAsync(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 5).ToArrayOf(),
DiagnosticInfos = Enumerable.Repeat(diagnosticInfo2, 5).ToArrayOf()
});
BrowseResponse response = await sessionMock.BrowseAsync(
requestHeader,
view,
0,
nodesToBrowse,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(15));
Assert.That(response.DiagnosticInfos[0].SymbolicId, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos[10].SymbolicId, Is.EqualTo(5));
Assert.That(response.DiagnosticInfos[10].InnerDiagnosticInfo, Is.Not.Null);
Assert.That(response.DiagnosticInfos[10].InnerDiagnosticInfo.SymbolicId, Is.EqualTo(1));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task BrowseAsyncShouldHandleEmptyDiagnosticInfosCorrectlyAsync(
RequestHeader requestHeader)
{
var view = new ViewDescription();
var nodesToBrowse = Enumerable.Repeat(new BrowseDescription(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerBrowse = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 10).ToArrayOf(),
DiagnosticInfos = []
})
.ReturnsAsync(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 5).ToArrayOf(),
DiagnosticInfos = []
});
BrowseResponse response = await sessionMock.BrowseAsync(
requestHeader,
view,
0,
nodesToBrowse,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
Assert.That(response.DiagnosticInfos.Count, Is.Zero);
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task BrowseAsyncShouldHandleEmptyStringTablesInDiagnosticInfosAsync(
RequestHeader requestHeader)
{
var view = new ViewDescription();
var nodesToBrowse = Enumerable.Repeat(new BrowseDescription(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerBrowse = 10;
var diagnosticInfo1 = new DiagnosticInfo
{
SymbolicId = 1,
NamespaceUri = 2,
Locale = 3,
LocalizedText = 4
};
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 10).ToArrayOf(),
DiagnosticInfos = Enumerable.Repeat(diagnosticInfo1, 10).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
StringTable = []
}
})
.ReturnsAsync(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 5).ToArrayOf(),
DiagnosticInfos = Enumerable.Repeat(diagnosticInfo1, 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
StringTable = []
}
});
BrowseResponse response = await sessionMock.BrowseAsync(
requestHeader,
view,
0,
nodesToBrowse,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(15));
Assert.That(response.ResponseHeader.StringTable.Count, Is.Zero);
// Verify that the indexes in the diagnostic infos are correctly updated
Assert.That(response.DiagnosticInfos[0].SymbolicId, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos[0].NamespaceUri, Is.EqualTo(2));
Assert.That(response.DiagnosticInfos[0].Locale, Is.EqualTo(3));
Assert.That(response.DiagnosticInfos[0].LocalizedText, Is.EqualTo(4));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task BrowseAsyncShouldHandleMixedDiagnosticInfosCorrectlyAsync(
RequestHeader requestHeader)
{
var view = new ViewDescription();
var nodesToBrowse = Enumerable.Repeat(new BrowseDescription(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerBrowse = 10;
var diagnosticInfo1 = new DiagnosticInfo
{
SymbolicId = 1,
NamespaceUri = 2,
Locale = 3,
LocalizedText = 4
};
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 10).ToArrayOf(),
DiagnosticInfos = Enumerable.Repeat(diagnosticInfo1, 10).ToArrayOf()
})
.ReturnsAsync(new BrowseResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 5).ToArrayOf(),
DiagnosticInfos = []
});
BrowseResponse response = await sessionMock.BrowseAsync(
requestHeader,
view,
0,
nodesToBrowse,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(15));
Assert.That(response.DiagnosticInfos[0].SymbolicId, Is.EqualTo(1));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task BrowseAsyncShouldRecombineStringTablesInDiagnosticInfos1Async(
RequestHeader requestHeader)
{
var view = new ViewDescription();
var nodesToBrowse = Enumerable.Range(0, 15).Select(_ => new BrowseDescription()).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerBrowse = 10;
static DiagnosticInfo DiagnosticInfo() => new()
{
SymbolicId = 1,
NamespaceUri = 2,
Locale = 3,
LocalizedText = 4
};
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new BrowseResponse
{
Results = [.. Enumerable.Range(0, 10).Select(_ => new BrowseResult())],
DiagnosticInfos = [.. Enumerable.Range(0, 10).Select(_ => DiagnosticInfo())],
ResponseHeader = new ResponseHeader
{
StringTable = ["String1", "String2", "String3", "String4"]
}
})
.ReturnsAsync(new BrowseResponse
{
Results = [.. Enumerable.Range(0, 5).Select(_ => new BrowseResult())],
DiagnosticInfos = [.. Enumerable.Range(0, 5).Select(_ => DiagnosticInfo())],
ResponseHeader = new ResponseHeader
{
StringTable = ["String5", "String6", "String7", "String8"]
}
});
BrowseResponse response = await sessionMock.BrowseAsync(
requestHeader,
view,
0,
nodesToBrowse,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(15));
Assert.That(response.ResponseHeader.StringTable.Count, Is.EqualTo(8));
Assert.That(response.ResponseHeader.StringTable.ToArray(),
Is.EquivalentTo(["String1", "String2", "String3", "String4", "String5", "String6", "String7", "String8"]));
// Verify that the indexes in the diagnostic infos are correctly updated
Assert.That(response.DiagnosticInfos[0].SymbolicId, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos[0].NamespaceUri, Is.EqualTo(2));
Assert.That(response.DiagnosticInfos[0].Locale, Is.EqualTo(3));
Assert.That(response.DiagnosticInfos[0].LocalizedText, Is.EqualTo(4));
Assert.That(response.DiagnosticInfos[10].SymbolicId, Is.EqualTo(5));
Assert.That(response.DiagnosticInfos[10].NamespaceUri, Is.EqualTo(6));
Assert.That(response.DiagnosticInfos[10].Locale, Is.EqualTo(7));
Assert.That(response.DiagnosticInfos[10].LocalizedText, Is.EqualTo(8));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task BrowseAsyncShouldRecombineStringTablesInDiagnosticInfos2Async(
RequestHeader requestHeader)
{
var view = new ViewDescription();
var nodesToBrowse = Enumerable.Range(0, 15).Select(_ => new BrowseDescription()).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerBrowse = 10;
DiagnosticInfo DiagnosticInfo1() => new()
{
SymbolicId = 1,
NamespaceUri = 2,
Locale = 3,
LocalizedText = 4
};
DiagnosticInfo DiagnosticInfo2() => new()
{
SymbolicId = 1,
NamespaceUri = 2,
Locale = 3,
LocalizedText = 4
};
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new BrowseResponse
{
Results = [.. Enumerable.Range(0, 10).Select(_ => new BrowseResult())],
DiagnosticInfos = [.. Enumerable.Range(0, 10).Select(_ => DiagnosticInfo1())],
ResponseHeader = new ResponseHeader
{
StringTable = ["String1", "String2", "String3", "String4"]
}
})
.ReturnsAsync(new BrowseResponse
{
Results = [.. Enumerable.Range(0, 5).Select(_ => new BrowseResult())],
DiagnosticInfos = [.. Enumerable.Range(0, 5).Select(_ => DiagnosticInfo2())],
ResponseHeader = new ResponseHeader
{
StringTable = ["String5", "String6", "String7", "String8"]
}
});
BrowseResponse response = await sessionMock.BrowseAsync(
requestHeader,
view,
0,
nodesToBrowse,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(15));
Assert.That(response.ResponseHeader.StringTable.Count, Is.EqualTo(8));
Assert.That(response.ResponseHeader.StringTable.ToList(),
Is.EquivalentTo(["String1", "String2", "String3", "String4", "String5", "String6", "String7", "String8"]));
// Verify that the indexes in the diagnostic infos are correctly updated
Assert.That(response.DiagnosticInfos[0].SymbolicId, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos[0].NamespaceUri, Is.EqualTo(2));
Assert.That(response.DiagnosticInfos[0].Locale, Is.EqualTo(3));
Assert.That(response.DiagnosticInfos[0].LocalizedText, Is.EqualTo(4));
Assert.That(response.DiagnosticInfos[10].SymbolicId, Is.EqualTo(5));
Assert.That(response.DiagnosticInfos[10].NamespaceUri, Is.EqualTo(6));
Assert.That(response.DiagnosticInfos[10].Locale, Is.EqualTo(7));
Assert.That(response.DiagnosticInfos[10].LocalizedText, Is.EqualTo(8));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task BrowseAsyncShouldRecombineStringTablesInDiagnosticInfos3Async(
RequestHeader requestHeader)
{
var view = new ViewDescription();
var nodesToBrowse = Enumerable.Range(0, 15).Select(_ => new BrowseDescription()).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerBrowse = 10;
DiagnosticInfo DiagnosticInfo1() => new()
{
SymbolicId = 1,
NamespaceUri = 2,
Locale = 1,
LocalizedText = 2
};
DiagnosticInfo DiagnosticInfo2() => new()
{
SymbolicId = 1,
NamespaceUri = 2,
Locale = 3,
LocalizedText = 4,
InnerDiagnosticInfo = DiagnosticInfo1()
};
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new BrowseResponse
{
Results = [.. Enumerable.Range(0, 10).Select(_ => new BrowseResult())],
DiagnosticInfos = [.. Enumerable.Range(0, 10).Select(_ => DiagnosticInfo1())],
ResponseHeader = new ResponseHeader
{
StringTable = ["String1", "String2"]
}
})
.ReturnsAsync(new BrowseResponse
{
Results = [.. Enumerable.Range(0, 5).Select(_ => new BrowseResult())],
DiagnosticInfos = [.. Enumerable.Range(0, 5).Select(_ => DiagnosticInfo2())],
ResponseHeader = new ResponseHeader
{
StringTable = ["String1", "String2", "String3", "String4"]
}
});
BrowseResponse response = await sessionMock.BrowseAsync(
requestHeader,
view,
0,
nodesToBrowse,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(15));
Assert.That(response.ResponseHeader.StringTable.Count, Is.EqualTo(6));
Assert.That(response.ResponseHeader.StringTable.ToList(),
Is.EquivalentTo(["String1", "String2", "String1", "String2", "String3", "String4"]));
// Verify that the indexes in the diagnostic infos are correctly updated
Assert.That(response.DiagnosticInfos[0].SymbolicId, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos[0].NamespaceUri, Is.EqualTo(2));
Assert.That(response.DiagnosticInfos[0].Locale, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos[0].LocalizedText, Is.EqualTo(2));
Assert.That(response.DiagnosticInfos[10].SymbolicId, Is.EqualTo(3));
Assert.That(response.DiagnosticInfos[10].NamespaceUri, Is.EqualTo(4));
Assert.That(response.DiagnosticInfos[10].Locale, Is.EqualTo(5));
Assert.That(response.DiagnosticInfos[10].LocalizedText, Is.EqualTo(6));
Assert.That(response.DiagnosticInfos[10].InnerDiagnosticInfo, Is.Not.Null);
Assert.That(response.DiagnosticInfos[10].InnerDiagnosticInfo.SymbolicId, Is.EqualTo(3));
Assert.That(response.DiagnosticInfos[10].InnerDiagnosticInfo.NamespaceUri, Is.EqualTo(4));
Assert.That(response.DiagnosticInfos[10].InnerDiagnosticInfo.Locale, Is.EqualTo(3));
Assert.That(response.DiagnosticInfos[10].InnerDiagnosticInfo.LocalizedText, Is.EqualTo(4));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task BrowseAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
var view = new ViewDescription();
const uint requestedMaxReferencesPerNode = 10u;
ArrayOf<BrowseDescription> nodesToBrowse = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new BrowseResponse())
.Verifiable(Times.Once);
BrowseResponse response = await sessionMock.BrowseAsync(
requestHeader,
view,
requestedMaxReferencesPerNode,
nodesToBrowse,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void BrowseAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
var view = new ViewDescription();
const uint requestedMaxReferencesPerNode = 10u;
ArrayOf<BrowseDescription> nodesToBrowse = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new BrowseResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.BrowseAsync(
requestHeader,
view,
requestedMaxReferencesPerNode,
nodesToBrowse,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void BrowseAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
var view = new ViewDescription();
const uint requestedMaxReferencesPerNode = 10u;
ArrayOf<BrowseDescription> nodesToBrowse = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.BrowseAsync(
requestHeader,
view,
requestedMaxReferencesPerNode,
nodesToBrowse,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task BrowseAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
var view = new ViewDescription();
const uint requestedMaxReferencesPerNode = 10u;
ArrayOf<BrowseDescription> nodesToBrowse = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new BrowseResponse
{
Results = [new BrowseResult { StatusCode = StatusCodes.Good }],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
BrowseResponse response = await sessionMock.BrowseAsync(
requestHeader,
view,
requestedMaxReferencesPerNode,
nodesToBrowse,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task BrowseNextAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
const bool releaseContinuationPoints = true;
var continuationPoints = Enumerable
.Repeat(ByteString.Empty, 15)
.ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.ServerCapabilities.MaxBrowseContinuationPoints = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseNextRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new BrowseNextResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 10).ToArrayOf()
})
.ReturnsAsync(new BrowseNextResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 5).ToArrayOf()
});
BrowseNextResponse response = await sessionMock.BrowseNextAsync(
requestHeader,
releaseContinuationPoints,
continuationPoints,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void BrowseNextAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
const bool releaseContinuationPoints = true;
var continuationPoints = Enumerable
.Repeat(ByteString.Empty, 15)
.ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.ServerCapabilities.MaxBrowseContinuationPoints = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseNextRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new BrowseNextResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 10).ToArrayOf()
})
.ReturnsAsync(new BrowseNextResponse
{
Results = Enumerable.Repeat(new BrowseResult(), 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.BrowseNextAsync(
requestHeader,
releaseContinuationPoints,
continuationPoints,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task BrowseNextAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
const bool releaseContinuationPoints = true;
var continuationPoints = new List<ByteString>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseNextRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new BrowseNextResponse())
.Verifiable(Times.Once);
BrowseNextResponse response = await sessionMock.BrowseNextAsync(requestHeader,
releaseContinuationPoints, continuationPoints, ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void BrowseNextAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
const bool releaseContinuationPoints = true;
var continuationPoints = new List<ByteString>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseNextRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new BrowseNextResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.BrowseNextAsync(
requestHeader,
releaseContinuationPoints,
continuationPoints,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void BrowseNextAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
const bool releaseContinuationPoints = true;
var continuationPoints = new List<ByteString>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseNextRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.BrowseNextAsync(
requestHeader,
releaseContinuationPoints,
continuationPoints,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task BrowseNextAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
const bool releaseContinuationPoints = true;
var continuationPoints = new List<ByteString>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is BrowseNextRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new BrowseNextResponse
{
Results = [new BrowseResult { StatusCode = StatusCodes.Good }],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
BrowseNextResponse response = await sessionMock.BrowseNextAsync(
requestHeader,
releaseContinuationPoints,
continuationPoints,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task CallAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
var methodsToCall = Enumerable.Repeat(new CallMethodRequest(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerMethodCall = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CallRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new CallResponse
{
Results = Enumerable.Repeat(new CallMethodResult(), 10).ToArrayOf()
})
.ReturnsAsync(new CallResponse
{
Results = Enumerable.Repeat(new CallMethodResult(), 5).ToArrayOf()
});
CallResponse response = await sessionMock.CallAsync(
requestHeader,
methodsToCall,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void CallAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
var methodsToCall = Enumerable.Repeat(new CallMethodRequest(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerMethodCall = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CallRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new CallResponse
{
Results = Enumerable.Repeat(new CallMethodResult(), 10).ToArrayOf()
})
.ReturnsAsync(new CallResponse
{
Results = Enumerable.Repeat(new CallMethodResult(), 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.CallAsync(
requestHeader,
methodsToCall,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task CallAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
ArrayOf<CallMethodRequest> methodsToCall = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CallRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new CallResponse())
.Verifiable(Times.Once);
CallResponse response = await sessionMock.CallAsync(requestHeader,
methodsToCall, ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void CallAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
ArrayOf<CallMethodRequest> methodsToCall = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CallRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new CallResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.CallAsync(
requestHeader,
methodsToCall,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void CallAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
ArrayOf<CallMethodRequest> methodsToCall = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CallRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.CallAsync(
requestHeader,
methodsToCall,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task CallAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
ArrayOf<CallMethodRequest> methodsToCall = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CallRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new CallResponse
{
Results = [new CallMethodResult { StatusCode = StatusCodes.Good }],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
CallResponse response = await sessionMock.CallAsync(
requestHeader,
methodsToCall,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task CancelAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
const uint requestHandle = 1u;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CancelRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new CancelResponse())
.Verifiable(Times.Once);
CancelResponse response = await sessionMock.CancelAsync(requestHeader,
requestHandle, ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void CancelAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
const uint requestHandle = 1u;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CancelRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new CancelResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.CancelAsync(
requestHeader,
requestHandle,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void CancelAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
const uint requestHandle = 1u;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CancelRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.CancelAsync(
requestHeader,
requestHandle,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task CloseSessionAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
const bool deleteSubscriptions = true;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CloseSessionRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new CloseSessionResponse())
.Verifiable(Times.Once);
CloseSessionResponse response = await sessionMock.CloseSessionAsync(requestHeader,
deleteSubscriptions, ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void CloseSessionAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
const bool deleteSubscriptions = true;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CloseSessionRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new CloseSessionResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.CloseSessionAsync(
requestHeader,
deleteSubscriptions,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void CloseSessionAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
const bool deleteSubscriptions = true;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CloseSessionRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.CloseSessionAsync(
requestHeader,
deleteSubscriptions,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task CreateMonitoredItemsAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
var itemsToCreate = Enumerable.Repeat(new MonitoredItemCreateRequest(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxMonitoredItemsPerCall = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CreateMonitoredItemsRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new CreateMonitoredItemsResponse
{
Results = Enumerable.Repeat(new MonitoredItemCreateResult(), 10).ToArrayOf()
})
.ReturnsAsync(new CreateMonitoredItemsResponse
{
Results = Enumerable.Repeat(new MonitoredItemCreateResult(), 5).ToArrayOf()
});
CreateMonitoredItemsResponse response = await sessionMock.CreateMonitoredItemsAsync(
requestHeader,
subscriptionId,
timestampsToReturn,
itemsToCreate,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void CreateMonitoredItemsAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
var itemsToCreate = Enumerable.Repeat(new MonitoredItemCreateRequest(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxMonitoredItemsPerCall = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CreateMonitoredItemsRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new CreateMonitoredItemsResponse
{
Results = Enumerable.Repeat(new MonitoredItemCreateResult(), 10).ToArrayOf()
})
.ReturnsAsync(new CreateMonitoredItemsResponse
{
Results = Enumerable.Repeat(new MonitoredItemCreateResult(), 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.CreateMonitoredItemsAsync(
requestHeader,
subscriptionId,
timestampsToReturn,
itemsToCreate,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CreateMonitoredItemsRequest),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
// The items created by the first batch are deleted again (L3-2).
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteMonitoredItemsRequest),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Theory]
public async Task CreateMonitoredItemsAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
ArrayOf<MonitoredItemCreateRequest> itemsToCreate = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CreateMonitoredItemsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new CreateMonitoredItemsResponse())
.Verifiable(Times.Once);
CreateMonitoredItemsResponse response = await sessionMock.CreateMonitoredItemsAsync(
requestHeader,
subscriptionId,
timestampsToReturn,
itemsToCreate,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void CreateMonitoredItemsAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
ArrayOf<MonitoredItemCreateRequest> itemsToCreate = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CreateMonitoredItemsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new CreateMonitoredItemsResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.CreateMonitoredItemsAsync(
requestHeader,
subscriptionId,
timestampsToReturn,
itemsToCreate,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void CreateMonitoredItemsAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
ArrayOf<MonitoredItemCreateRequest> itemsToCreate = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CreateMonitoredItemsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.CreateMonitoredItemsAsync(
requestHeader,
subscriptionId,
timestampsToReturn,
itemsToCreate,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task CreateMonitoredItemsAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
ArrayOf<MonitoredItemCreateRequest> itemsToCreate = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CreateMonitoredItemsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new CreateMonitoredItemsResponse
{
Results = [new MonitoredItemCreateResult { StatusCode = StatusCodes.Good }],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
CreateMonitoredItemsResponse response = await sessionMock.CreateMonitoredItemsAsync(
requestHeader,
subscriptionId,
timestampsToReturn,
itemsToCreate,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task CreateSubscriptionAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
const double requestedPublishingInterval = 1000.0;
const uint requestedLifetimeCount = 10u;
const uint requestedMaxKeepAliveCount = 5u;
const uint maxNotificationsPerPublish = 100u;
const bool publishingEnabled = true;
const byte priority = 1;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CreateSubscriptionRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new CreateSubscriptionResponse())
.Verifiable(Times.Once);
CreateSubscriptionResponse response = await sessionMock.CreateSubscriptionAsync(
requestHeader,
requestedPublishingInterval,
requestedLifetimeCount,
requestedMaxKeepAliveCount,
maxNotificationsPerPublish,
publishingEnabled,
priority,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void CreateSubscriptionAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
const double requestedPublishingInterval = 1000.0;
const uint requestedLifetimeCount = 10u;
const uint requestedMaxKeepAliveCount = 5u;
const uint maxNotificationsPerPublish = 100u;
const bool publishingEnabled = true;
const byte priority = 1;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CreateSubscriptionRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new CreateSubscriptionResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.CreateSubscriptionAsync(
requestHeader,
requestedPublishingInterval,
requestedLifetimeCount,
requestedMaxKeepAliveCount,
maxNotificationsPerPublish,
publishingEnabled,
priority,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void CreateSubscriptionAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
const double requestedPublishingInterval = 1000.0;
const uint requestedLifetimeCount = 10u;
const uint requestedMaxKeepAliveCount = 5u;
const uint maxNotificationsPerPublish = 100u;
const bool publishingEnabled = true;
const byte priority = 1;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is CreateSubscriptionRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.CreateSubscriptionAsync(
requestHeader,
requestedPublishingInterval,
requestedLifetimeCount,
requestedMaxKeepAliveCount,
maxNotificationsPerPublish,
publishingEnabled,
priority,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task DeleteMonitoredItemsAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
var monitoredItemIds = new List<uint>([.. Enumerable.Repeat(1u, 15)]);
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxMonitoredItemsPerCall = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteMonitoredItemsRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new DeleteMonitoredItemsResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
})
.ReturnsAsync(new DeleteMonitoredItemsResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 5).ToArrayOf()
});
DeleteMonitoredItemsResponse response = await sessionMock.DeleteMonitoredItemsAsync(
requestHeader,
subscriptionId,
monitoredItemIds,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void DeleteMonitoredItemsAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
var monitoredItemIds = new List<uint>([.. Enumerable.Repeat(1u, 15)]);
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxMonitoredItemsPerCall = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteMonitoredItemsRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new DeleteMonitoredItemsResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
})
.ReturnsAsync(new DeleteMonitoredItemsResponse
{
Results = Enumerable.Repeat(StatusCodes.Bad, 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.DeleteMonitoredItemsAsync(
requestHeader,
subscriptionId,
monitoredItemIds,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task DeleteMonitoredItemsAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
var monitoredItemIds = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteMonitoredItemsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new DeleteMonitoredItemsResponse())
.Verifiable(Times.Once);
DeleteMonitoredItemsResponse response = await sessionMock.DeleteMonitoredItemsAsync(
requestHeader,
subscriptionId,
monitoredItemIds,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void DeleteMonitoredItemsAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
var monitoredItemIds = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteMonitoredItemsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new DeleteMonitoredItemsResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.DeleteMonitoredItemsAsync(
requestHeader,
subscriptionId,
monitoredItemIds,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void DeleteMonitoredItemsAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
var monitoredItemIds = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteMonitoredItemsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.DeleteMonitoredItemsAsync(
requestHeader,
subscriptionId,
monitoredItemIds,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task DeleteNodesAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
var nodesToDelete = Enumerable.Repeat(new DeleteNodesItem(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerNodeManagement = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteNodesRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new DeleteNodesResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
})
.ReturnsAsync(new DeleteNodesResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 5).ToArrayOf()
});
DeleteNodesResponse response = await sessionMock.DeleteNodesAsync(
requestHeader,
nodesToDelete,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void DeleteNodesAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
var nodesToDelete = Enumerable.Repeat(new DeleteNodesItem(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerNodeManagement = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteNodesRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new DeleteNodesResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
})
.ReturnsAsync(new DeleteNodesResponse
{
Results = Enumerable.Repeat(StatusCodes.Bad, 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.DeleteNodesAsync(
requestHeader,
nodesToDelete,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task DeleteNodesAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
ArrayOf<DeleteNodesItem> nodesToDelete = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteNodesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new DeleteNodesResponse())
.Verifiable(Times.Once);
DeleteNodesResponse response = await sessionMock.DeleteNodesAsync(
requestHeader,
nodesToDelete,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void DeleteNodesAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
ArrayOf<DeleteNodesItem> nodesToDelete = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteNodesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new DeleteNodesResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.DeleteNodesAsync(
requestHeader,
nodesToDelete,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void DeleteNodesAsyncShouldThrowExceptionWhenSendRequestAsyncThrows
(RequestHeader requestHeader)
{
ArrayOf<DeleteNodesItem> nodesToDelete = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteNodesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.DeleteNodesAsync(
requestHeader,
nodesToDelete,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task DeleteNodesAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
ArrayOf<DeleteNodesItem> nodesToDelete = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteNodesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new DeleteNodesResponse
{
Results = [StatusCodes.Good],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
DeleteNodesResponse response = await sessionMock.DeleteNodesAsync(
requestHeader,
nodesToDelete,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task DeleteReferencesAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
var referencesToDelete = Enumerable.Repeat(new DeleteReferencesItem(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerNodeManagement = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteReferencesRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new DeleteReferencesResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
})
.ReturnsAsync(new DeleteReferencesResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 5).ToArrayOf()
});
DeleteReferencesResponse response = await sessionMock.DeleteReferencesAsync(
requestHeader,
referencesToDelete,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void DeleteReferencesAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
var referencesToDelete = Enumerable.Repeat(new DeleteReferencesItem(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerNodeManagement = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteReferencesRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new DeleteReferencesResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
})
.ReturnsAsync(new DeleteReferencesResponse
{
Results = Enumerable.Repeat(StatusCodes.Bad, 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.DeleteReferencesAsync(
requestHeader,
referencesToDelete,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task DeleteReferencesAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
ArrayOf<DeleteReferencesItem> referencesToDelete = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteReferencesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new DeleteReferencesResponse())
.Verifiable(Times.Once);
DeleteReferencesResponse response = await sessionMock.DeleteReferencesAsync(
requestHeader,
referencesToDelete,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void DeleteReferencesAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
ArrayOf<DeleteReferencesItem> referencesToDelete = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteReferencesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new DeleteReferencesResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.DeleteReferencesAsync(
requestHeader,
referencesToDelete,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void DeleteReferencesAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
ArrayOf<DeleteReferencesItem> referencesToDelete = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteReferencesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.DeleteReferencesAsync(
requestHeader,
referencesToDelete,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task DeleteReferencesAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
ArrayOf<DeleteReferencesItem> referencesToDelete = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteReferencesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new DeleteReferencesResponse
{
Results = [StatusCodes.Good],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
DeleteReferencesResponse response = await sessionMock.DeleteReferencesAsync(
requestHeader,
referencesToDelete,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task DeleteSubscriptionsAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
var subscriptionIds = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteSubscriptionsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new DeleteSubscriptionsResponse())
.Verifiable(Times.Once);
DeleteSubscriptionsResponse response = await sessionMock.DeleteSubscriptionsAsync(
requestHeader,
subscriptionIds,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void DeleteSubscriptionsAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
var subscriptionIds = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteSubscriptionsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new DeleteSubscriptionsResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.DeleteSubscriptionsAsync(
requestHeader,
subscriptionIds,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void DeleteSubscriptionsAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
var subscriptionIds = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is DeleteSubscriptionsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.DeleteSubscriptionsAsync(
requestHeader,
subscriptionIds,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task HistoryReadAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
var historyReadDetails = new ExtensionObject();
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
const bool releaseContinuationPoints = true;
var nodesToRead = Enumerable.Repeat(new HistoryReadValueId(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerHistoryReadData = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is HistoryReadRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new HistoryReadResponse
{
Results = Enumerable.Repeat(new HistoryReadResult(), 10).ToArrayOf()
})
.ReturnsAsync(new HistoryReadResponse
{
Results = Enumerable.Repeat(new HistoryReadResult(), 5).ToArrayOf()
});
HistoryReadResponse response = await sessionMock.HistoryReadAsync(
requestHeader,
historyReadDetails,
timestampsToReturn,
releaseContinuationPoints,
nodesToRead,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void HistoryReadAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
var historyReadDetails = new ExtensionObject(new ReadEventDetails());
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
const bool releaseContinuationPoints = true;
var nodesToRead = Enumerable.Repeat(new HistoryReadValueId(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerHistoryReadEvents = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is HistoryReadRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new HistoryReadResponse
{
Results = Enumerable.Repeat(new HistoryReadResult(), 10).ToArrayOf()
})
.ReturnsAsync(new HistoryReadResponse
{
Results = Enumerable.Repeat(new HistoryReadResult(), 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.HistoryReadAsync(
requestHeader,
historyReadDetails,
timestampsToReturn,
releaseContinuationPoints,
nodesToRead,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task HistoryReadAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
var historyReadDetails = new ExtensionObject();
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
const bool releaseContinuationPoints = true;
ArrayOf<HistoryReadValueId> nodesToRead = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is HistoryReadRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new HistoryReadResponse())
.Verifiable(Times.Once);
HistoryReadResponse response = await sessionMock.HistoryReadAsync(requestHeader,
historyReadDetails, timestampsToReturn, releaseContinuationPoints,
nodesToRead, ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void HistoryReadAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
var historyReadDetails = new ExtensionObject();
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
const bool releaseContinuationPoints = true;
ArrayOf<HistoryReadValueId> nodesToRead = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is HistoryReadRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new HistoryReadResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.HistoryReadAsync(
requestHeader,
historyReadDetails,
timestampsToReturn,
releaseContinuationPoints,
nodesToRead,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void HistoryReadAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
var historyReadDetails = new ExtensionObject();
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
const bool releaseContinuationPoints = true;
ArrayOf<HistoryReadValueId> nodesToRead = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is HistoryReadRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.HistoryReadAsync(
requestHeader,
historyReadDetails,
timestampsToReturn,
releaseContinuationPoints,
nodesToRead,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task HistoryReadAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
var historyReadDetails = new ExtensionObject();
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
const bool releaseContinuationPoints = true;
ArrayOf<HistoryReadValueId> nodesToRead = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is HistoryReadRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new HistoryReadResponse
{
Results = [new HistoryReadResult { StatusCode = StatusCodes.Good }],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
HistoryReadResponse response = await sessionMock.HistoryReadAsync(requestHeader, historyReadDetails,
timestampsToReturn, releaseContinuationPoints, nodesToRead, ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task HistoryUpdateAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
var historyUpdateDetails = Enumerable.Repeat(new ExtensionObject(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerHistoryUpdateData = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is HistoryUpdateRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new HistoryUpdateResponse
{
Results = Enumerable.Repeat(new HistoryUpdateResult(), 10).ToArrayOf()
})
.ReturnsAsync(new HistoryUpdateResponse
{
Results = Enumerable.Repeat(new HistoryUpdateResult(), 5).ToArrayOf()
});
HistoryUpdateResponse response = await sessionMock.HistoryUpdateAsync(
requestHeader,
historyUpdateDetails,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[TestCase(0u)]
[TestCase(5u)]
public async Task HistoryDeleteEventsUsesEventOperationLimitAsync(uint dataLimit)
{
using var session = SessionMock.Create();
session.OperationLimits.MaxNodesPerHistoryUpdateEvents = 2;
session.OperationLimits.MaxNodesPerHistoryUpdateData = dataLimit;
var batchSizes = new List<int>();
var requestedNodes = new List<NodeId>();
var details = Enumerable.Range(1, 5)
.Select(index => new ExtensionObject(new DeleteEventDetails
{
NodeId = new NodeId((uint)index)
}))
.ToArrayOf();
session.Channel
.Setup(channel => channel.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync((IServiceRequest request, CancellationToken _) =>
{
if (request is not HistoryUpdateRequest update)
{
throw new InvalidOperationException("Expected HistoryUpdate.");
}
if (update.HistoryUpdateDetails.Count > 2)
{
throw new ServiceResultException(StatusCodes.BadTooManyOperations);
}
batchSizes.Add(update.HistoryUpdateDetails.Count);
var results = new List<HistoryUpdateResult>();
foreach (ExtensionObject detail in update.HistoryUpdateDetails)
{
Assert.That(detail.TryGetValue(out DeleteEventDetails? delete), Is.True);
requestedNodes.Add(delete!.NodeId);
results.Add(new HistoryUpdateResult
{
OperationResults = [(uint)requestedNodes.Count]
});
}
return new HistoryUpdateResponse { Results = results };
});
HistoryUpdateResponse response = await session.HistoryUpdateAsync(
null, details, CancellationToken.None).ConfigureAwait(false);
int[] expectedBatchSizes = [2, 2, 1];
Assert.That(batchSizes, Is.EqualTo(expectedBatchSizes));
Assert.That(requestedNodes, Is.EqualTo(
[
new NodeId(1u), new NodeId(2u), new NodeId(3u), new NodeId(4u), new NodeId(5u)
]));
Assert.That(
response.Results.ToList().Select(result => result.OperationResults[0]),
Is.EqualTo(new StatusCode[] { 1u, 2u, 3u, 4u, 5u }));
}
[Theory]
public void HistoryUpdateAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
var historyUpdateDetails = Enumerable.Repeat(new ExtensionObject(new UpdateEventDetails()), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerHistoryUpdateEvents = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is HistoryUpdateRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new HistoryUpdateResponse
{
Results = Enumerable.Repeat(new HistoryUpdateResult(), 10).ToArrayOf()
})
.ReturnsAsync(new HistoryUpdateResponse
{
Results = Enumerable.Repeat(new HistoryUpdateResult(), 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.HistoryUpdateAsync(
requestHeader,
historyUpdateDetails,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task HistoryUpdateAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
ArrayOf<ExtensionObject> historyUpdateDetails = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is HistoryUpdateRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new HistoryUpdateResponse())
.Verifiable(Times.Once);
HistoryUpdateResponse response = await sessionMock.HistoryUpdateAsync(
requestHeader,
historyUpdateDetails,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void HistoryUpdateAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
ArrayOf<ExtensionObject> historyUpdateDetails = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is HistoryUpdateRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new HistoryUpdateResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.HistoryUpdateAsync(
requestHeader,
historyUpdateDetails,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void HistoryUpdateAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
ArrayOf<ExtensionObject> historyUpdateDetails = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is HistoryUpdateRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.HistoryUpdateAsync(
requestHeader,
historyUpdateDetails,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task HistoryUpdateAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
ArrayOf<ExtensionObject> historyUpdateDetails = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is HistoryUpdateRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new HistoryUpdateResponse
{
Results = [new HistoryUpdateResult { StatusCode = StatusCodes.Good }],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
HistoryUpdateResponse response = await sessionMock.HistoryUpdateAsync(
requestHeader,
historyUpdateDetails,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task ModifyMonitoredItemsAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
var itemsToModify = Enumerable.Repeat(new MonitoredItemModifyRequest(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxMonitoredItemsPerCall = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ModifyMonitoredItemsRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ModifyMonitoredItemsResponse
{
Results = Enumerable.Repeat(new MonitoredItemModifyResult(), 10).ToArrayOf()
})
.ReturnsAsync(new ModifyMonitoredItemsResponse
{
Results = Enumerable.Repeat(new MonitoredItemModifyResult(), 5).ToArrayOf()
});
ModifyMonitoredItemsResponse response = await sessionMock.ModifyMonitoredItemsAsync(
requestHeader,
subscriptionId,
timestampsToReturn,
itemsToModify,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void ModifyMonitoredItemsAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
var itemsToModify = Enumerable.Repeat(new MonitoredItemModifyRequest(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxMonitoredItemsPerCall = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ModifyMonitoredItemsRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ModifyMonitoredItemsResponse
{
Results = Enumerable.Repeat(new MonitoredItemModifyResult(), 10).ToArrayOf()
})
.ReturnsAsync(new ModifyMonitoredItemsResponse
{
Results = Enumerable.Repeat(new MonitoredItemModifyResult(), 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.ModifyMonitoredItemsAsync(
requestHeader,
subscriptionId,
timestampsToReturn,
itemsToModify,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task ModifyMonitoredItemsAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
ArrayOf<MonitoredItemModifyRequest> itemsToModify = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ModifyMonitoredItemsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new ModifyMonitoredItemsResponse())
.Verifiable(Times.Once);
ModifyMonitoredItemsResponse response = await sessionMock.ModifyMonitoredItemsAsync(
requestHeader,
subscriptionId,
timestampsToReturn,
itemsToModify,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void ModifyMonitoredItemsAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
ArrayOf<MonitoredItemModifyRequest> itemsToModify = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ModifyMonitoredItemsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new ModifyMonitoredItemsResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.ModifyMonitoredItemsAsync(
requestHeader,
subscriptionId,
timestampsToReturn,
itemsToModify,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void ModifyMonitoredItemsAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
ArrayOf<MonitoredItemModifyRequest> itemsToModify = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ModifyMonitoredItemsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.ModifyMonitoredItemsAsync(
requestHeader,
subscriptionId,
timestampsToReturn,
itemsToModify,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task ModifyMonitoredItemsAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
ArrayOf<MonitoredItemModifyRequest> itemsToModify = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ModifyMonitoredItemsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new ModifyMonitoredItemsResponse
{
Results = [new MonitoredItemModifyResult { StatusCode = StatusCodes.Good }],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
ModifyMonitoredItemsResponse response = await sessionMock.ModifyMonitoredItemsAsync(
requestHeader,
subscriptionId,
timestampsToReturn,
itemsToModify,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task ModifySubscriptionAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const double requestedPublishingInterval = 1000.0;
const uint requestedLifetimeCount = 10u;
const uint requestedMaxKeepAliveCount = 5u;
const uint maxNotificationsPerPublish = 100u;
const byte priority = 1;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ModifySubscriptionRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new ModifySubscriptionResponse())
.Verifiable(Times.Once);
ModifySubscriptionResponse response = await sessionMock.ModifySubscriptionAsync(
requestHeader,
subscriptionId,
requestedPublishingInterval,
requestedLifetimeCount,
requestedMaxKeepAliveCount,
maxNotificationsPerPublish,
priority,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void ModifySubscriptionAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const double requestedPublishingInterval = 1000.0;
const uint requestedLifetimeCount = 10u;
const uint requestedMaxKeepAliveCount = 5u;
const uint maxNotificationsPerPublish = 100u;
const byte priority = 1;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ModifySubscriptionRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new ModifySubscriptionResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(async () => await sessionMock.ModifySubscriptionAsync(
requestHeader,
subscriptionId,
requestedPublishingInterval,
requestedLifetimeCount,
requestedMaxKeepAliveCount,
maxNotificationsPerPublish,
priority,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void ModifySubscriptionAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const double requestedPublishingInterval = 1000.0;
const uint requestedLifetimeCount = 10u;
const uint requestedMaxKeepAliveCount = 5u;
const uint maxNotificationsPerPublish = 100u;
const byte priority = 1;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ModifySubscriptionRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.ModifySubscriptionAsync(
requestHeader,
subscriptionId,
requestedPublishingInterval,
requestedLifetimeCount,
requestedMaxKeepAliveCount,
maxNotificationsPerPublish,
priority,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task PublishAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
ArrayOf<SubscriptionAcknowledgement> subscriptionAcknowledgements = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is PublishRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new PublishResponse())
.Verifiable(Times.Once);
PublishResponse response = await sessionMock.PublishAsync(
requestHeader,
subscriptionAcknowledgements,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void PublishAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
ArrayOf<SubscriptionAcknowledgement> subscriptionAcknowledgements = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is PublishRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new PublishResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.PublishAsync(
requestHeader,
subscriptionAcknowledgements,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void PublishAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
ArrayOf<SubscriptionAcknowledgement> subscriptionAcknowledgements = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is PublishRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.PublishAsync(
requestHeader,
subscriptionAcknowledgements,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task QueryFirstAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
var view = new ViewDescription();
ArrayOf<NodeTypeDescription> nodeTypes = [];
var filter = new ContentFilter();
const uint maxDataSetsToReturn = 10u;
const uint maxReferencesToReturn = 10u;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is QueryFirstRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new QueryFirstResponse())
.Verifiable(Times.Once);
QueryFirstResponse response = await sessionMock.QueryFirstAsync(
requestHeader,
view,
nodeTypes,
filter,
maxDataSetsToReturn,
maxReferencesToReturn,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void QueryFirstAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
var view = new ViewDescription();
ArrayOf<NodeTypeDescription> nodeTypes = [];
var filter = new ContentFilter();
const uint maxDataSetsToReturn = 10u;
const uint maxReferencesToReturn = 10u;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is QueryFirstRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new QueryFirstResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.QueryFirstAsync(
requestHeader,
view,
nodeTypes,
filter,
maxDataSetsToReturn,
maxReferencesToReturn,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void QueryFirstAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
var view = new ViewDescription();
ArrayOf<NodeTypeDescription> nodeTypes = [];
var filter = new ContentFilter();
const uint maxDataSetsToReturn = 10u;
const uint maxReferencesToReturn = 10u;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is QueryFirstRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.QueryFirstAsync(
requestHeader,
view,
nodeTypes,
filter,
maxDataSetsToReturn,
maxReferencesToReturn,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task QueryNextAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
const bool releaseContinuationPoint = true;
ByteString continuationPoint = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is QueryNextRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new QueryNextResponse())
.Verifiable(Times.Once);
QueryNextResponse response = await sessionMock.QueryNextAsync(
requestHeader,
releaseContinuationPoint,
continuationPoint,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void QueryNextAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
const bool releaseContinuationPoint = true;
ByteString continuationPoint = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is QueryNextRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new QueryNextResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.QueryNextAsync(
requestHeader,
releaseContinuationPoint,
continuationPoint,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void QueryNextAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
const bool releaseContinuationPoint = true;
ByteString continuationPoint = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is QueryNextRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.QueryNextAsync(
requestHeader,
releaseContinuationPoint,
continuationPoint,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task ReadAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
const double maxAge = 1000.0;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
var nodesToRead = Enumerable.Repeat(new ReadValueId(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerRead = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ReadRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ReadResponse
{
Results = Enumerable.Repeat(new DataValue(), 10).ToArrayOf()
})
.ReturnsAsync(new ReadResponse
{
Results = Enumerable.Repeat(new DataValue(), 5).ToArrayOf()
});
ReadResponse response = await sessionMock.ReadAsync(
requestHeader,
maxAge,
timestampsToReturn,
nodesToRead,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void ReadAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
const double maxAge = 1000.0;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
var nodesToRead = Enumerable.Repeat(new ReadValueId(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerRead = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ReadRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ReadResponse
{
Results = Enumerable.Repeat(new DataValue(), 10).ToArrayOf()
})
.ReturnsAsync(new ReadResponse
{
Results = Enumerable.Repeat(new DataValue(), 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(async () => await sessionMock.ReadAsync(
requestHeader,
maxAge,
timestampsToReturn,
nodesToRead,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task ReadAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
const double maxAge = 1000.0;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
ArrayOf<ReadValueId> nodesToRead = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ReadRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new ReadResponse())
.Verifiable(Times.Once);
ReadResponse response = await sessionMock.ReadAsync(
requestHeader,
maxAge,
timestampsToReturn,
nodesToRead,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void ReadAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
const double maxAge = 1000.0;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
ArrayOf<ReadValueId> nodesToRead = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ReadRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new ReadResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.ReadAsync(
requestHeader,
maxAge,
timestampsToReturn,
nodesToRead,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void ReadAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
const double maxAge = 1000.0;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
ArrayOf<ReadValueId> nodesToRead = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ReadRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.ReadAsync(
requestHeader,
maxAge,
timestampsToReturn,
nodesToRead,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task ReadAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
const double maxAge = 1000.0;
const TimestampsToReturn timestampsToReturn = TimestampsToReturn.Both;
ArrayOf<ReadValueId> nodesToRead = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is ReadRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new ReadResponse
{
Results = [DataValue.FromStatusCode(StatusCodes.Good)],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
ReadResponse response = await sessionMock.ReadAsync(
requestHeader,
maxAge,
timestampsToReturn,
nodesToRead,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task RegisterNodesAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
var nodesToRegister = Enumerable.Repeat(new NodeId(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerRegisterNodes = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is RegisterNodesRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new RegisterNodesResponse
{
RegisteredNodeIds = Enumerable.Repeat(new NodeId(), 10).ToArrayOf()
})
.ReturnsAsync(new RegisterNodesResponse
{
RegisteredNodeIds = Enumerable.Repeat(new NodeId(), 5).ToArrayOf()
});
RegisterNodesResponse response = await sessionMock.RegisterNodesAsync(
requestHeader,
nodesToRegister,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.RegisteredNodeIds.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void RegisterNodesAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
var nodesToRegister = Enumerable.Repeat(new NodeId(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerRegisterNodes = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is RegisterNodesRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new RegisterNodesResponse
{
RegisteredNodeIds = Enumerable.Repeat(new NodeId(), 10).ToArrayOf()
})
.ReturnsAsync(new RegisterNodesResponse
{
RegisteredNodeIds = Enumerable.Repeat(new NodeId(), 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.RegisterNodesAsync(
requestHeader,
nodesToRegister,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task RegisterNodesAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
ArrayOf<NodeId> nodesToRegister = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is RegisterNodesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new RegisterNodesResponse())
.Verifiable(Times.Once);
RegisterNodesResponse response = await sessionMock.RegisterNodesAsync(
requestHeader, nodesToRegister, ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void RegisterNodesAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
ArrayOf<NodeId> nodesToRegister = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is RegisterNodesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new RegisterNodesResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.RegisterNodesAsync(
requestHeader,
nodesToRegister,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void RegisterNodesAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
ArrayOf<NodeId> nodesToRegister = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is RegisterNodesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.RegisterNodesAsync(
requestHeader,
nodesToRegister,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task RepublishAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const uint retransmitSequenceNumber = 1u;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is RepublishRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new RepublishResponse())
.Verifiable(Times.Once);
RepublishResponse response = await sessionMock.RepublishAsync(
requestHeader,
subscriptionId,
retransmitSequenceNumber,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void RepublishAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const uint retransmitSequenceNumber = 1u;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is RepublishRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new RepublishResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.RepublishAsync(
requestHeader,
subscriptionId,
retransmitSequenceNumber,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void RepublishAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const uint retransmitSequenceNumber = 1u;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is RepublishRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.RepublishAsync(
requestHeader,
subscriptionId,
retransmitSequenceNumber,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task SetMonitoringModeAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const MonitoringMode monitoringMode = MonitoringMode.Reporting;
var monitoredItemIds = new List<uint>([.. Enumerable.Repeat(1u, 15)]);
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxMonitoredItemsPerCall = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetMonitoringModeRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new SetMonitoringModeResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
})
.ReturnsAsync(new SetMonitoringModeResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 5).ToArrayOf()
});
SetMonitoringModeResponse response = await sessionMock.SetMonitoringModeAsync(
requestHeader,
subscriptionId,
monitoringMode,
monitoredItemIds,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void SetMonitoringModeAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const MonitoringMode monitoringMode = MonitoringMode.Reporting;
var monitoredItemIds = new List<uint>([.. Enumerable.Repeat(1u, 15)]);
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxMonitoredItemsPerCall = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetMonitoringModeRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new SetMonitoringModeResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
})
.ReturnsAsync(new SetMonitoringModeResponse
{
Results = Enumerable.Repeat(StatusCodes.Bad, 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.SetMonitoringModeAsync(
requestHeader,
subscriptionId,
monitoringMode,
monitoredItemIds,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task SetMonitoringModeAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const MonitoringMode monitoringMode = MonitoringMode.Reporting;
var monitoredItemIds = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetMonitoringModeRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new SetMonitoringModeResponse())
.Verifiable(Times.Once);
SetMonitoringModeResponse response = await sessionMock.SetMonitoringModeAsync(
requestHeader,
subscriptionId,
monitoringMode,
monitoredItemIds,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void SetMonitoringModeAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const MonitoringMode monitoringMode = MonitoringMode.Reporting;
var monitoredItemIds = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetMonitoringModeRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new SetMonitoringModeResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.SetMonitoringModeAsync(
requestHeader,
subscriptionId,
monitoringMode,
monitoredItemIds,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void SetMonitoringModeAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const MonitoringMode monitoringMode = MonitoringMode.Reporting;
var monitoredItemIds = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetMonitoringModeRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.SetMonitoringModeAsync(
requestHeader,
subscriptionId,
monitoringMode,
monitoredItemIds,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task SetMonitoringModeAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const MonitoringMode monitoringMode = MonitoringMode.Reporting;
var monitoredItemIds = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetMonitoringModeRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new SetMonitoringModeResponse
{
Results = [StatusCodes.Good],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
SetMonitoringModeResponse response = await sessionMock.SetMonitoringModeAsync(
requestHeader,
subscriptionId,
monitoringMode,
monitoredItemIds,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task SetPublishingModeAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
const bool publishingEnabled = true;
var subscriptionIds = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetPublishingModeRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new SetPublishingModeResponse())
.Verifiable(Times.Once);
SetPublishingModeResponse response = await sessionMock.SetPublishingModeAsync(
requestHeader,
publishingEnabled,
subscriptionIds,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void SetPublishingModeAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
const bool publishingEnabled = true;
var subscriptionIds = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetPublishingModeRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new SetPublishingModeResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.SetPublishingModeAsync(
requestHeader,
publishingEnabled,
subscriptionIds,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void SetPublishingModeAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
const bool publishingEnabled = true;
var subscriptionIds = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetPublishingModeRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.SetPublishingModeAsync(
requestHeader,
publishingEnabled,
subscriptionIds,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task SetTriggeringAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const uint triggeringItemId = 1u;
var linksToAdd = new List<uint>([.. Enumerable.Repeat(1u, 15)]);
var linksToRemove = new List<uint>([.. Enumerable.Repeat(1u, 15)]);
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxMonitoredItemsPerCall = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetTriggeringRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new SetTriggeringResponse
{
AddResults = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
})
.ReturnsAsync(new SetTriggeringResponse
{
AddResults = Enumerable.Repeat(StatusCodes.Good, 5).ToArrayOf(),
RemoveResults = Enumerable.Repeat(StatusCodes.Good, 5).ToArrayOf()
})
.ReturnsAsync(new SetTriggeringResponse
{
RemoveResults = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
});
SetTriggeringResponse response = await sessionMock.SetTriggeringAsync(
requestHeader,
subscriptionId,
triggeringItemId,
linksToAdd,
linksToRemove,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.AddResults.Count, Is.EqualTo(15));
Assert.That(response.RemoveResults.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(3));
}
[Theory]
public void SetTriggeringAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const uint triggeringItemId = 1u;
var linksToAdd = new List<uint>([.. Enumerable.Repeat(1u, 15)]);
var linksToRemove = new List<uint>([.. Enumerable.Repeat(1u, 15)]);
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxMonitoredItemsPerCall = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetTriggeringRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new SetTriggeringResponse
{
AddResults = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
})
.ReturnsAsync(new SetTriggeringResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.SetTriggeringAsync(
requestHeader,
subscriptionId,
triggeringItemId,
linksToAdd,
linksToRemove,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task SetTriggeringAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const uint triggeringItemId = 1u;
var linksToAdd = new List<uint>();
var linksToRemove = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetTriggeringRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new SetTriggeringResponse())
.Verifiable(Times.Once);
SetTriggeringResponse response = await sessionMock.SetTriggeringAsync(
requestHeader,
subscriptionId,
triggeringItemId,
linksToAdd,
linksToRemove,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void SetTriggeringAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const uint triggeringItemId = 1u;
var linksToAdd = new List<uint>();
var linksToRemove = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetTriggeringRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new SetTriggeringResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.SetTriggeringAsync(
requestHeader,
subscriptionId,
triggeringItemId,
linksToAdd,
linksToRemove,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void SetTriggeringAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const uint triggeringItemId = 1u;
var linksToAdd = new List<uint>();
var linksToRemove = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetTriggeringRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.SetTriggeringAsync(
requestHeader,
subscriptionId,
triggeringItemId,
linksToAdd,
linksToRemove,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task SetTriggeringAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
const uint subscriptionId = 1u;
const uint triggeringItemId = 1u;
var linksToAdd = new List<uint>();
var linksToRemove = new List<uint>();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is SetTriggeringRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new SetTriggeringResponse
{
AddResults = [StatusCodes.Good],
AddDiagnosticInfos = [new DiagnosticInfo()],
RemoveResults = [StatusCodes.Good],
RemoveDiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
SetTriggeringResponse response = await sessionMock.SetTriggeringAsync(
requestHeader,
subscriptionId,
triggeringItemId,
linksToAdd,
linksToRemove,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.AddResults.Count, Is.EqualTo(1));
Assert.That(response.AddDiagnosticInfos.Count, Is.EqualTo(1));
Assert.That(response.RemoveResults.Count, Is.EqualTo(1));
Assert.That(response.RemoveDiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task TransferSubscriptionsAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
var subscriptionIds = new List<uint>();
const bool sendInitialValues = true;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is TransferSubscriptionsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new TransferSubscriptionsResponse())
.Verifiable(Times.Once);
TransferSubscriptionsResponse response = await sessionMock.TransferSubscriptionsAsync(
requestHeader,
subscriptionIds,
sendInitialValues,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void TransferSubscriptionsAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
var subscriptionIds = new List<uint>();
const bool sendInitialValues = true;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is TransferSubscriptionsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new TransferSubscriptionsResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.TransferSubscriptionsAsync(
requestHeader,
subscriptionIds,
sendInitialValues,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void TransferSubscriptionsAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
var subscriptionIds = new List<uint>();
const bool sendInitialValues = true;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is TransferSubscriptionsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.TransferSubscriptionsAsync(
requestHeader,
subscriptionIds,
sendInitialValues,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task TransferSubscriptionsAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
var subscriptionIds = new List<uint>();
const bool sendInitialValues = true;
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is TransferSubscriptionsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new TransferSubscriptionsResponse
{
Results = [new TransferResult { StatusCode = StatusCodes.Good }],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
TransferSubscriptionsResponse response = await sessionMock.TransferSubscriptionsAsync(
requestHeader,
subscriptionIds,
sendInitialValues,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task TranslateBrowsePathsToNodeIdsAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
var browsePaths = Enumerable.Repeat(new BrowsePath(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is TranslateBrowsePathsToNodeIdsRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new TranslateBrowsePathsToNodeIdsResponse
{
Results = Enumerable.Repeat(new BrowsePathResult(), 10).ToArrayOf()
})
.ReturnsAsync(new TranslateBrowsePathsToNodeIdsResponse
{
Results = Enumerable.Repeat(new BrowsePathResult(), 5).ToArrayOf()
});
TranslateBrowsePathsToNodeIdsResponse response = await sessionMock.TranslateBrowsePathsToNodeIdsAsync(
requestHeader,
browsePaths,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void TranslateBrowsePathsToNodeIdsAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
var browsePaths = Enumerable.Repeat(new BrowsePath(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is TranslateBrowsePathsToNodeIdsRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new TranslateBrowsePathsToNodeIdsResponse
{
Results = Enumerable.Repeat(new BrowsePathResult(), 10).ToArrayOf()
})
.ReturnsAsync(new TranslateBrowsePathsToNodeIdsResponse
{
Results = Enumerable.Repeat(new BrowsePathResult(), 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.TranslateBrowsePathsToNodeIdsAsync(
requestHeader,
browsePaths,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task TranslateBrowsePathsToNodeIdsAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
ArrayOf<BrowsePath> browsePaths = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is TranslateBrowsePathsToNodeIdsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new TranslateBrowsePathsToNodeIdsResponse())
.Verifiable(Times.Once);
TranslateBrowsePathsToNodeIdsResponse response = await sessionMock.TranslateBrowsePathsToNodeIdsAsync(
requestHeader,
browsePaths,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
}
[Theory]
public async Task TranslateBrowsePathsToNodeIdsAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
ArrayOf<BrowsePath> browsePaths = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is TranslateBrowsePathsToNodeIdsRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new TranslateBrowsePathsToNodeIdsResponse
{
Results = [new BrowsePathResult { StatusCode = StatusCodes.Good }],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
TranslateBrowsePathsToNodeIdsResponse response = await sessionMock.TranslateBrowsePathsToNodeIdsAsync(
requestHeader,
browsePaths,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
[Theory]
public async Task UnregisterNodesAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
var nodesToUnregister = Enumerable.Repeat(new NodeId(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerRegisterNodes = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is UnregisterNodesRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new UnregisterNodesResponse())
.ReturnsAsync(new UnregisterNodesResponse());
UnregisterNodesResponse response = await sessionMock.UnregisterNodesAsync(
requestHeader,
nodesToUnregister,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void UnregisterNodesAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
var nodesToUnregister = Enumerable.Repeat(new NodeId(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerRegisterNodes = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is UnregisterNodesRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new UnregisterNodesResponse())
.ReturnsAsync(new UnregisterNodesResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.UnregisterNodesAsync(
requestHeader,
nodesToUnregister,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task UnregisterNodesAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
ArrayOf<NodeId> nodesToUnregister = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is UnregisterNodesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new UnregisterNodesResponse())
.Verifiable(Times.Once);
UnregisterNodesResponse response = await sessionMock.UnregisterNodesAsync(
requestHeader,
nodesToUnregister,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void UnregisterNodesAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
ArrayOf<NodeId> nodesToUnregister = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is UnregisterNodesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new UnregisterNodesResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.UnregisterNodesAsync(
requestHeader,
nodesToUnregister,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void UnregisterNodesAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
ArrayOf<NodeId> nodesToUnregister = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is UnregisterNodesRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.UnregisterNodesAsync(
requestHeader,
nodesToUnregister,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task WriteAsyncShouldBatchRequestsWhenExceedingOperationLimitsAsync(
RequestHeader requestHeader)
{
var nodesToWrite = Enumerable.Repeat(new WriteValue(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerWrite = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is WriteRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new WriteResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
})
.ReturnsAsync(new WriteResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 5).ToArrayOf()
});
WriteResponse response = await sessionMock.WriteAsync(
requestHeader,
nodesToWrite,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(15));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public void WriteAsyncShouldHandleBatchingWhenSecondOperationFails(
RequestHeader requestHeader)
{
var nodesToWrite = Enumerable.Repeat(new WriteValue(), 15).ToArrayOf();
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.OperationLimits.MaxNodesPerWrite = 10;
sessionMock.Channel
.SetupSequence(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is WriteRequest),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new WriteResponse
{
Results = Enumerable.Repeat(StatusCodes.Good, 10).ToArrayOf()
})
.ReturnsAsync(new WriteResponse
{
Results = Enumerable.Repeat(StatusCodes.Bad, 5).ToArrayOf(),
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
});
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.WriteAsync(
requestHeader,
nodesToWrite,
ct).ConfigureAwait(false));
sessionMock.Channel
.Verify(c => c.SendRequestAsync(
It.IsAny<IServiceRequest>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
[Theory]
public async Task WriteAsyncShouldSimplyCallBaseMethodWhenNoLimitsSetAsync(
RequestHeader requestHeader)
{
ArrayOf<WriteValue> nodesToWrite = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is WriteRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new WriteResponse())
.Verifiable(Times.Once);
WriteResponse response = await sessionMock.WriteAsync(requestHeader,
nodesToWrite, ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(requestHeader?.RequestHandle ?? 1, Is.Not.Zero);
Assert.That(
requestHeader?.Timestamp ?? DateTimeUtc.Now,
Is.LessThanOrEqualTo(DateTimeUtc.Now + TimeSpan.FromSeconds(1)));
sessionMock.Channel.Verify();
}
[Theory]
public void WriteAsyncShouldThrowExceptionWhenResponseContainsBadStatusCode(
RequestHeader requestHeader)
{
ArrayOf<WriteValue> nodesToWrite = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is WriteRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new WriteResponse
{
ResponseHeader = new ResponseHeader
{
ServiceResult = StatusCodes.Bad
}
})
.Verifiable(Times.Once);
Assert.ThrowsAsync<ServiceResultException>(
async () => await sessionMock.WriteAsync(
requestHeader,
nodesToWrite,
ct).ConfigureAwait(false));
sessionMock.Channel.Verify();
}
[Theory]
public void WriteAsyncShouldThrowExceptionWhenSendRequestAsyncThrows(
RequestHeader requestHeader)
{
ArrayOf<WriteValue> nodesToWrite = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is WriteRequest),
It.Is<CancellationToken>(t => t == ct)))
.ThrowsAsync(new IOException("Test exception"))
.Verifiable(Times.Once);
IOException ioex = Assert.ThrowsAsync<IOException>(
async () => await sessionMock.WriteAsync(
requestHeader,
nodesToWrite,
ct).ConfigureAwait(false));
Assert.That(ioex.Message, Is.EqualTo("Test exception"));
sessionMock.Channel.Verify();
}
[Theory]
public async Task WriteAsyncShouldValidateResponseAndHandleDiagnosticInfoAsync(
RequestHeader requestHeader)
{
ArrayOf<WriteValue> nodesToWrite = [];
CancellationToken ct = CancellationToken.None;
using var sessionMock = SessionMock.Create();
sessionMock.Channel
.Setup(c => c.SendRequestAsync(
It.Is<IServiceRequest>(r => r is WriteRequest),
It.Is<CancellationToken>(t => t == ct)))
.ReturnsAsync(new WriteResponse
{
Results = [StatusCodes.Good],
DiagnosticInfos = [new DiagnosticInfo()]
})
.Verifiable(Times.Once);
WriteResponse response = await sessionMock.WriteAsync(
requestHeader,
nodesToWrite,
ct).ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(response.DiagnosticInfos.Count, Is.EqualTo(1));
}
}
}