diff --git a/tests/Opc.Ua.Client.Tests/Session/ManagedSessionComplianceTests.cs b/tests/Opc.Ua.Client.Tests/Session/ManagedSessionComplianceTests.cs index 6e3b3517e8..c3b1c00cc3 100644 --- a/tests/Opc.Ua.Client.Tests/Session/ManagedSessionComplianceTests.cs +++ b/tests/Opc.Ua.Client.Tests/Session/ManagedSessionComplianceTests.cs @@ -579,6 +579,28 @@ namespace Opc.Ua.Client.Tests.ManagedSession yield return Case( "Cancel", m => m.CancelAsync(null, 0, default).AsTask()); + + // The data channel Services are marked [Experimental("DataChannels")] + // so consumers opt in deliberately. This fixture asserts that every + // Service on the managed session reaches the inner one, and that + // obligation applies to an experimental Service as much as a stable + // one, so the opt-in is scoped to these three cases rather than + // switched on for the whole project. +#pragma warning disable DataChannels + yield return Case( + "OpenDataChannel", + m => m.OpenDataChannelAsync(null, + new NodeId(1u), 0, 0, + new DataChannelParametersDataType(), default).AsTask()); + yield return Case( + "ModifyDataChannel", + m => m.ModifyDataChannelAsync(null, 1, + new DataChannelParametersDataType(), default).AsTask()); + yield return Case( + "CloseDataChannel", + m => m.CloseDataChannelAsync(null, 1, + StatusCodes.Good, false, default).AsTask()); +#pragma warning restore DataChannels } private static TestCaseData Case( diff --git a/tests/Opc.Ua.Redundancy.Client.Tests/RedundantClientSessionFacadeTests.cs b/tests/Opc.Ua.Redundancy.Client.Tests/RedundantClientSessionFacadeTests.cs index 55b299d97a..0e64b3b251 100644 --- a/tests/Opc.Ua.Redundancy.Client.Tests/RedundantClientSessionFacadeTests.cs +++ b/tests/Opc.Ua.Redundancy.Client.Tests/RedundantClientSessionFacadeTests.cs @@ -659,6 +659,65 @@ namespace Opc.Ua.Client.Redundancy.Tests Assert.That(await facade.CancelAsync(null, 1, ct).ConfigureAwait(false), Is.SameAs(cancel)); } + /// + /// The data channel Services delegate to the current session like every + /// other Service on the facade. A redundant client that silently dropped + /// them would leave a channel opened against a session the caller can no + /// longer reach after a failover. + /// + [Test] + public async Task LeaderDataChannelServiceCallsDelegateToCurrentSessionAsync() + { + var session = new Mock(); + RedundantClientSession facade = CreateLeaderFacade(session); + CancellationToken ct = CancellationToken.None; + + var opened = new OpenDataChannelResponse(); + var modified = new ModifyDataChannelResponse(); + var closed = new CloseDataChannelResponse(); + + // Scoped opt-in: the Services carry [Experimental("DataChannels")], + // and the facade's delegation obligation applies to them regardless. +#pragma warning disable DataChannels + session.Setup(s => s.OpenDataChannelAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(opened)); + session.Setup(s => s.ModifyDataChannelAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(modified)); + session.Setup(s => s.CloseDataChannelAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(closed)); + + Assert.Multiple(async () => + { + Assert.That( + await facade.OpenDataChannelAsync(null, new NodeId(1u), 0, 0, null, ct) + .ConfigureAwait(false), + Is.SameAs(opened)); + Assert.That( + await facade.ModifyDataChannelAsync(null, 1, null, ct).ConfigureAwait(false), + Is.SameAs(modified)); + Assert.That( + await facade.CloseDataChannelAsync(null, 1, StatusCodes.Good, false, ct) + .ConfigureAwait(false), + Is.SameAs(closed)); + }); +#pragma warning restore DataChannels + } + [Test] public async Task LeaderConvenienceMethodsDelegateToCurrentSessionAsync() {