using Opc.Ua; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Client.CLI.Commands; using ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.Fakes; using ZB.MOM.WW.OtOpcUa.Client.Shared.Models; namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests; /// /// Regression tests for Client.CLI-009: long-running commands must detach their event handlers /// from the service before the command finishes, so notifications fired during teardown don't /// land in a disposed console. /// public class EventHandlerLifecycleTests { [Fact] public async Task SubscribeCommand_AfterExit_DataChangedEventHasNoSubscribers() { var fakeService = new FakeOpcUaClientService(); var factory = new FakeOpcUaClientServiceFactory(fakeService); var command = new SubscribeCommand(factory) { Url = "opc.tcp://localhost:4840", NodeId = "ns=2;s=Node", DurationSeconds = 1 }; using var console = TestConsoleHelper.CreateConsole(); await command.ExecuteAsync(console); // The fake's event should have no subscribers after the command completes — every // handler attached by the command must have been detached during teardown. fakeService.HasDataChangedSubscribers.ShouldBeFalse( "SubscribeCommand must detach its DataChanged handler before returning."); } [Fact] public async Task AlarmsCommand_AfterExit_AlarmEventHasNoSubscribers() { var fakeService = new FakeOpcUaClientService(); var factory = new FakeOpcUaClientServiceFactory(fakeService); var command = new AlarmsCommand(factory) { Url = "opc.tcp://localhost:4840" }; using var console = TestConsoleHelper.CreateConsole(); var task = Task.Run(async () => await command.ExecuteAsync(console)); await Task.Delay(150); console.RequestCancellation(); await task; fakeService.HasAlarmEventSubscribers.ShouldBeFalse( "AlarmsCommand must detach its AlarmEvent handler before returning."); } }