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();
}
}