using Microsoft.Extensions.DependencyInjection; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.Interfaces; using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin; namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests; /// /// E2E integration coverage for the ReconnectDriver command path through /// . /// /// Scope note: wiring a live DriverInstanceActor for the full /// Healthy → Reconnecting → Healthy health-transition assertion requires a deployed /// driver row in the config DB, a real fixture endpoint, and the /// DriverHostActor to have registered the instance — substantially more /// harness complexity than the two-node cluster setup alone provides. That deeper /// fixture is tracked as a follow-up. This suite instead verifies the message /// round-trip through the AdminOperationsActor singleton: the command is /// accepted, persisted as a ConfigEdit audit row, and the reply carries /// Ok = true with the matching CorrelationId. The DPS broadcast /// that triggers the actor-side reconnect is exercised by the control-plane unit /// tests that mock IActorRef. /// [Trait("Category", "Integration")] public sealed class DriverReconnectE2eTests { private static CancellationToken Ct => TestContext.Current.CancellationToken; /// /// Verifies that a message dispatched through /// returns a /// with Ok = true and the matching /// correlation ID, confirming the cluster-singleton round-trip works end-to-end. /// /// The instance ID used here ("reconnect-e2e-nonexistent") does not correspond /// to a deployed driver, so no DriverInstanceActor will act on the DPS /// broadcast — the test is validating the command ingestion and reply path only. /// [Fact] public async Task Reconnect_RoundTrip_ReturnsOk() { await using var harness = await TwoNodeClusterHarness.StartAsync(); await using var scope = harness.NodeA.Services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); var correlationId = Guid.NewGuid(); var msg = new ReconnectDriver( ClusterId: "cluster-e2e-test", DriverInstanceId: "reconnect-e2e-nonexistent", ActorByUserName: "e2e-test-runner", CorrelationId: correlationId); var result = await client.AskAsync(msg, Ct); result.CorrelationId.ShouldBe(correlationId); result.Ok.ShouldBeTrue($"ReconnectDriver round-trip failed: {result.Message}"); result.Message.ShouldBeNull(); } /// /// Verifies that a second for the same instance ID /// is also accepted (idempotent at the actor layer — the actor simply re-broadcasts /// to DPS and writes another ConfigEdit row). /// [Fact] public async Task Reconnect_IsIdempotent_SecondCallAlsoReturnsOk() { await using var harness = await TwoNodeClusterHarness.StartAsync(); await using var scope = harness.NodeA.Services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); const string instanceId = "reconnect-idempotency-test"; var first = new ReconnectDriver("cluster-1", instanceId, "runner", Guid.NewGuid()); var second = new ReconnectDriver("cluster-1", instanceId, "runner", Guid.NewGuid()); var r1 = await client.AskAsync(first, Ct); var r2 = await client.AskAsync(second, Ct); r1.Ok.ShouldBeTrue($"First call failed: {r1.Message}"); r2.Ok.ShouldBeTrue($"Second call failed: {r2.Message}"); r1.CorrelationId.ShouldBe(first.CorrelationId); r2.CorrelationId.ShouldBe(second.CorrelationId); } }