using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime; /// /// Connect / recreate orchestration tests for . The SDK /// session/client types are sealed with internal ctors and cannot be faked, so these /// drive the open body through the OpenAndRegisterOverrideForTests seam. The /// core regression guard is : /// a stale-session reconnect must rebuild rather than no-op (the gateway-restart bug). /// public sealed class GalaxyMxSessionReconnectTests { private static GalaxyMxAccessOptions MinimalOptions() => new(ClientName: "OtOpcUaTest"); private static GalaxyMxSession NewSession() => new(MinimalOptions()); /// A second ConnectAsync while connected must be a no-op (idempotent guard). [Fact] public async Task Connect_then_connect_again_is_a_noop() { var session = NewSession(); var openCount = 0; session.OpenAndRegisterOverrideForTests = _ => { openCount++; return Task.CompletedTask; }; await session.ConnectAsync(null!, CancellationToken.None); openCount.ShouldBe(1); await session.ConnectAsync(null!, CancellationToken.None); openCount.ShouldBe(1); } /// /// The regression guard for the gateway-restart bug: RecreateAsync must bypass /// the no-op guard and re-run the open body even though a (stale) session is present. /// [Fact] public async Task Recreate_after_connect_opens_a_fresh_session() { var session = NewSession(); var openCount = 0; session.OpenAndRegisterOverrideForTests = _ => { openCount++; return Task.CompletedTask; }; await session.ConnectAsync(null!, CancellationToken.None); openCount.ShouldBe(1); await session.RecreateAsync(null!, CancellationToken.None); openCount.ShouldBe(2); } /// RecreateAsync on a never-connected session still opens (teardown is a no-op first). [Fact] public async Task Recreate_when_never_connected_still_opens() { var session = NewSession(); var openCount = 0; session.OpenAndRegisterOverrideForTests = _ => { openCount++; return Task.CompletedTask; }; await session.RecreateAsync(null!, CancellationToken.None); openCount.ShouldBe(1); } /// /// A failed first connect must not leave _connected set — the next attempt has /// to reach the open body again (partial-open teardown). /// [Fact] public async Task Connect_failure_is_not_left_half_open() { var session = NewSession(); var openCount = 0; session.OpenAndRegisterOverrideForTests = _ => { // Throw on the first attempt, succeed on the second. if (openCount++ == 0) { throw new InvalidOperationException("simulated open failure"); } return Task.CompletedTask; }; await Should.ThrowAsync( async () => await session.ConnectAsync(null!, CancellationToken.None)); openCount.ShouldBe(1); session.IsConnected.ShouldBeFalse(); // _connected must NOT be latched by the failed attempt. // The failed first attempt must not have latched _connected — the retry reaches the body. await session.ConnectAsync(null!, CancellationToken.None); openCount.ShouldBe(2); } /// /// tracks the _connected guard across the /// full lifecycle: false when fresh, true after connect, still true after a recreate, and /// false again after dispose. /// [Fact] public async Task IsConnected_reflects_connect_recreate_and_dispose() { var session = NewSession(); var openCount = 0; session.OpenAndRegisterOverrideForTests = _ => { openCount++; return Task.CompletedTask; }; session.IsConnected.ShouldBeFalse(); await session.ConnectAsync(null!, CancellationToken.None); session.IsConnected.ShouldBeTrue(); await session.RecreateAsync(null!, CancellationToken.None); session.IsConnected.ShouldBeTrue(); await session.DisposeAsync(); session.IsConnected.ShouldBeFalse(); } }