using System; using System.Threading.Tasks; using Shouldly; using Xunit; using ZB.MOM.WW.LmxOpcUa.Host.Configuration; using ZB.MOM.WW.LmxOpcUa.Host.Domain; using ZB.MOM.WW.LmxOpcUa.Host.Metrics; using ZB.MOM.WW.LmxOpcUa.Host.MxAccess; using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess { /// /// Verifies the background connectivity monitor used to reconnect the MXAccess bridge after faults or stale probes. /// public class MxAccessClientMonitorTests : IDisposable { private readonly StaComThread _staThread; private readonly FakeMxProxy _proxy; private readonly PerformanceMetrics _metrics; /// /// Initializes the monitor test fixture with a shared STA thread, fake proxy, and metrics collector. /// public MxAccessClientMonitorTests() { _staThread = new StaComThread(); _staThread.Start(); _proxy = new FakeMxProxy(); _metrics = new PerformanceMetrics(); } /// /// Disposes the monitor test fixture resources. /// public void Dispose() { _staThread.Dispose(); _metrics.Dispose(); } /// /// Confirms that the monitor reconnects the client after an observed disconnect. /// [Fact] public async Task Monitor_ReconnectsOnDisconnect() { var config = new MxAccessConfiguration { MonitorIntervalSeconds = 1, AutoReconnect = true }; var client = new MxAccessClient(_staThread, _proxy, config, _metrics); await client.ConnectAsync(); await client.DisconnectAsync(); client.StartMonitor(); // Wait for monitor to detect disconnect and reconnect await Task.Delay(2500); client.StopMonitor(); client.State.ShouldBe(ConnectionState.Connected); client.ReconnectCount.ShouldBeGreaterThan(0); client.Dispose(); } /// /// Confirms that the monitor can be started and stopped without throwing. /// [Fact] public async Task Monitor_StopsOnCancel() { var config = new MxAccessConfiguration { MonitorIntervalSeconds = 1 }; var client = new MxAccessClient(_staThread, _proxy, config, _metrics); await client.ConnectAsync(); client.StartMonitor(); client.StopMonitor(); // Should not throw await Task.Delay(200); client.Dispose(); } /// /// Confirms that a stale probe tag triggers a reconnect when monitoring is enabled. /// [Fact] public async Task Monitor_ProbeStale_ForcesReconnect() { var config = new MxAccessConfiguration { ProbeTag = "TestProbe", ProbeStaleThresholdSeconds = 2, MonitorIntervalSeconds = 1, AutoReconnect = true }; var client = new MxAccessClient(_staThread, _proxy, config, _metrics); await client.ConnectAsync(); client.StartMonitor(); // Wait long enough for probe to go stale (threshold=2s, monitor interval=1s) // No data changes simulated → probe becomes stale → reconnect triggered await Task.Delay(4000); client.StopMonitor(); client.ReconnectCount.ShouldBeGreaterThan(0); client.Dispose(); } /// /// Confirms that fresh probe updates prevent unnecessary reconnects. /// [Fact] public async Task Monitor_ProbeDataChange_PreventsStaleReconnect() { var config = new MxAccessConfiguration { ProbeTag = "TestProbe", ProbeStaleThresholdSeconds = 2, MonitorIntervalSeconds = 1, AutoReconnect = true }; var client = new MxAccessClient(_staThread, _proxy, config, _metrics); await client.ConnectAsync(); client.StartMonitor(); // Continuously simulate probe data changes to keep it fresh for (int i = 0; i < 8; i++) { await Task.Delay(500); _proxy.SimulateDataChangeByAddress("TestProbe", i, 192); } client.StopMonitor(); // Probe was kept fresh → no reconnect should have happened client.ReconnectCount.ShouldBe(0); client.State.ShouldBe(ConnectionState.Connected); client.Dispose(); } /// /// Confirms that enabling the monitor without a probe tag does not trigger false reconnects. /// [Fact] public async Task Monitor_NoProbeConfigured_NoFalseReconnect() { var config = new MxAccessConfiguration { ProbeTag = null, // No probe MonitorIntervalSeconds = 1, AutoReconnect = true }; var client = new MxAccessClient(_staThread, _proxy, config, _metrics); await client.ConnectAsync(); client.StartMonitor(); // Wait several monitor cycles — should stay connected with no reconnects await Task.Delay(3000); client.StopMonitor(); client.State.ShouldBe(ConnectionState.Connected); client.ReconnectCount.ShouldBe(0); client.Dispose(); } } }