using System; using System.Threading.Tasks; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Tests.Helpers; namespace ZB.MOM.WW.OtOpcUa.Tests.OpcUa { /// /// Verifies that subscription and unsubscription failures in the MXAccess client /// are handled gracefully by the node manager instead of silently lost. /// public class LmxNodeManagerSubscriptionFaultTests { /// /// Confirms that a faulted SubscribeAsync is caught and logged rather than silently discarded. /// [Fact] public async Task SubscribeTag_WhenClientFaults_DoesNotThrowAndDoesNotHang() { var mxClient = new FakeMxAccessClient { SubscribeException = new InvalidOperationException("COM connection lost") }; var fixture = OpcUaServerFixture.WithFakeMxAccessClient(mxClient); await fixture.InitializeAsync(); try { var nodeManager = fixture.Service.NodeManagerInstance!; // SubscribeTag should catch the fault — not throw and not hang Should.NotThrow(() => nodeManager.SubscribeTag("TestMachine_001.MachineID")); } finally { await fixture.DisposeAsync(); } } /// /// Confirms that a faulted UnsubscribeAsync is caught and logged rather than silently discarded. /// [Fact] public async Task UnsubscribeTag_WhenClientFaults_DoesNotThrowAndDoesNotHang() { var mxClient = new FakeMxAccessClient(); var fixture = OpcUaServerFixture.WithFakeMxAccessClient(mxClient); await fixture.InitializeAsync(); try { var nodeManager = fixture.Service.NodeManagerInstance!; // Subscribe first (succeeds) nodeManager.SubscribeTag("TestMachine_001.MachineID"); mxClient.ActiveSubscriptionCount.ShouldBe(1); // Now inject fault for unsubscribe mxClient.UnsubscribeException = new InvalidOperationException("COM connection lost"); // UnsubscribeTag should catch the fault — not throw and not hang Should.NotThrow(() => nodeManager.UnsubscribeTag("TestMachine_001.MachineID")); } finally { await fixture.DisposeAsync(); } } /// /// Confirms that subscription failure does not corrupt the ref-count bookkeeping, /// allowing a retry to succeed after the fault clears. /// [Fact] public async Task SubscribeTag_AfterFaultClears_CanSubscribeAgain() { var mxClient = new FakeMxAccessClient { SubscribeException = new InvalidOperationException("transient fault") }; var fixture = OpcUaServerFixture.WithFakeMxAccessClient(mxClient); await fixture.InitializeAsync(); try { var nodeManager = fixture.Service.NodeManagerInstance!; // First subscribe faults (caught) nodeManager.SubscribeTag("TestMachine_001.MachineID"); mxClient.ActiveSubscriptionCount.ShouldBe(0); // subscribe failed // Clear the fault mxClient.SubscribeException = null; // Unsubscribe to reset ref count, then subscribe again nodeManager.UnsubscribeTag("TestMachine_001.MachineID"); nodeManager.SubscribeTag("TestMachine_001.MachineID"); mxClient.ActiveSubscriptionCount.ShouldBe(1); } finally { await fixture.DisposeAsync(); } } } }