fix(galaxy): reconnect recreates a faulted session instead of no-op'ing
This commit is contained in:
+106
@@ -0,0 +1,106 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Connect / recreate orchestration tests for <see cref="GalaxyMxSession"/>. The SDK
|
||||
/// session/client types are sealed with internal ctors and cannot be faked, so these
|
||||
/// drive the open body through the <c>OpenAndRegisterOverrideForTests</c> seam. The
|
||||
/// core regression guard is <see cref="Recreate_after_connect_opens_a_fresh_session"/>:
|
||||
/// a stale-session reconnect must rebuild rather than no-op (the gateway-restart bug).
|
||||
/// </summary>
|
||||
public sealed class GalaxyMxSessionReconnectTests
|
||||
{
|
||||
private static GalaxyMxAccessOptions MinimalOptions() => new(ClientName: "OtOpcUaTest");
|
||||
|
||||
private static GalaxyMxSession NewSession() => new(MinimalOptions());
|
||||
|
||||
/// <summary>A second <c>ConnectAsync</c> while connected must be a no-op (idempotent guard).</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The regression guard for the gateway-restart bug: <c>RecreateAsync</c> must bypass
|
||||
/// the no-op guard and re-run the open body even though a (stale) session is present.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary><c>RecreateAsync</c> on a never-connected session still opens (teardown is a no-op first).</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A failed first connect must not leave <c>_connected</c> set — the next attempt has
|
||||
/// to reach the open body again (partial-open teardown).
|
||||
/// </summary>
|
||||
[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<InvalidOperationException>(
|
||||
async () => await session.ConnectAsync(null!, CancellationToken.None));
|
||||
openCount.ShouldBe(1);
|
||||
|
||||
// The failed first attempt must not have latched _connected — the retry reaches the body.
|
||||
await session.ConnectAsync(null!, CancellationToken.None);
|
||||
openCount.ShouldBe(2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user