using Moq; using Opc.Ua; using Opc.Ua.Client; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests; /// /// Review 2026-10-21 R06:O-8 — AcknowledgeAsync swallowed every non-cancellation transport failure, so /// the host logged the Part 9 ack as applied while upstream still showed the condition unacknowledged. A /// transport failure (no reply) now propagates with its status; a reply — even one carrying per-ack refusals — /// still returns normally (that is the server answering, logged per ack). /// [Trait("Category", "Unit")] public sealed class OpcUaClientAcknowledgeFailureTests { private const string Condition = "ns=2;s=Pump17"; private static Mock NewSession() { var mock = new Mock(MockBehavior.Loose); mock.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(SdkTelemetry.Context)); mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable()); return mock; } private static AlarmAcknowledgeRequest[] Ack() => [new(Condition, Condition, "operator ack")]; /// A transport failure on the Acknowledge call reaches the caller with the transport status. [Fact] public async Task A_transport_failure_propagates_with_its_status() { using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-o8"); var session = NewSession(); session.Setup(s => s.CallAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new ServiceResultException(StatusCodes.BadConnectionClosed)); drv.SetSessionForTest(session.Object); var ex = await Should.ThrowAsync(() => drv.AcknowledgeAsync(Ack(), TestContext.Current.CancellationToken)); ex.StatusCode.ShouldBe(StatusCodes.BadConnectionClosed); } /// Positive control: a reply returns normally and the call reached the wire. [Fact] public async Task Positive_control_a_reply_returns_normally() { using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-o8-ok"); var session = NewSession(); session.Setup(s => s.CallAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new CallResponse { ResponseHeader = new ResponseHeader(), Results = [new CallMethodResult { StatusCode = StatusCodes.BadConditionBranchAlreadyAcked }], DiagnosticInfos = [], }); drv.SetSessionForTest(session.Object); await drv.AcknowledgeAsync(Ack(), TestContext.Current.CancellationToken); session.Verify(s => s.CallAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } /// A session lost while the ack queued on the gate fails the ack rather than reporting it applied. [Fact] public async Task A_session_lost_while_queued_fails_the_ack() { var ct = TestContext.Current.CancellationToken; using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-o8-lost"); drv.SetSessionForTest(NewSession().Object); await drv.Gate.WaitAsync(ct); var call = drv.AcknowledgeAsync(Ack(), ct); drv.SetSessionForTest(null); drv.Gate.Release(); await Should.ThrowAsync(() => call); } }