using System; using System.Collections.Generic; 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 MXAccess client connection lifecycle behavior, including transitions, registration, and reconnect handling. /// public class MxAccessClientConnectionTests : IDisposable { private readonly StaComThread _staThread; private readonly FakeMxProxy _proxy; private readonly PerformanceMetrics _metrics; private readonly MxAccessClient _client; private readonly List<(ConnectionState Previous, ConnectionState Current)> _stateChanges = new(); /// /// Initializes the connection test fixture with a fake runtime proxy and state-change recorder. /// public MxAccessClientConnectionTests() { _staThread = new StaComThread(); _staThread.Start(); _proxy = new FakeMxProxy(); _metrics = new PerformanceMetrics(); var config = new MxAccessConfiguration(); _client = new MxAccessClient(_staThread, _proxy, config, _metrics); _client.ConnectionStateChanged += (_, e) => _stateChanges.Add((e.PreviousState, e.CurrentState)); } /// /// Disposes the connection test fixture and its supporting resources. /// public void Dispose() { _client.Dispose(); _staThread.Dispose(); _metrics.Dispose(); } /// /// Confirms that a newly created MXAccess client starts in the disconnected state. /// [Fact] public void InitialState_IsDisconnected() { _client.State.ShouldBe(ConnectionState.Disconnected); } /// /// Confirms that connecting drives the expected disconnected-to-connecting-to-connected transitions. /// [Fact] public async Task Connect_TransitionsToConnected() { await _client.ConnectAsync(); _client.State.ShouldBe(ConnectionState.Connected); _stateChanges.ShouldContain(s => s.Previous == ConnectionState.Disconnected && s.Current == ConnectionState.Connecting); _stateChanges.ShouldContain(s => s.Previous == ConnectionState.Connecting && s.Current == ConnectionState.Connected); } /// /// Confirms that a successful connect registers exactly once with the runtime proxy. /// [Fact] public async Task Connect_RegistersCalled() { await _client.ConnectAsync(); _proxy.RegisterCallCount.ShouldBe(1); } /// /// Confirms that disconnecting drives the expected shutdown transitions back to disconnected. /// [Fact] public async Task Disconnect_TransitionsToDisconnected() { await _client.ConnectAsync(); await _client.DisconnectAsync(); _client.State.ShouldBe(ConnectionState.Disconnected); _stateChanges.ShouldContain(s => s.Current == ConnectionState.Disconnecting); _stateChanges.ShouldContain(s => s.Current == ConnectionState.Disconnected); } /// /// Confirms that disconnecting unregisters the runtime proxy session. /// [Fact] public async Task Disconnect_UnregistersCalled() { await _client.ConnectAsync(); await _client.DisconnectAsync(); _proxy.UnregisterCallCount.ShouldBe(1); } /// /// Confirms that registration failures move the client into the error state. /// [Fact] public async Task ConnectFails_TransitionsToError() { _proxy.ShouldFailRegister = true; await Should.ThrowAsync(_client.ConnectAsync()); _client.State.ShouldBe(ConnectionState.Error); } /// /// Confirms that repeated connect calls do not perform duplicate runtime registrations. /// [Fact] public async Task DoubleConnect_NoOp() { await _client.ConnectAsync(); await _client.ConnectAsync(); // Should be no-op _proxy.RegisterCallCount.ShouldBe(1); } /// /// Confirms that reconnect increments the reconnect counter and restores the connected state. /// [Fact] public async Task Reconnect_IncrementsCount() { await _client.ConnectAsync(); _client.ReconnectCount.ShouldBe(0); await _client.ReconnectAsync(); _client.ReconnectCount.ShouldBe(1); _client.State.ShouldBe(ConnectionState.Connected); } } }