494da22cd1
- DriverTestConnectE2eTests: 3 scenarios (sim/wrong-port/black-hole) against the Modbus Docker fixture. Sim + wrong-port skip if fixture unreachable; black-hole uses ModbusDriverProbe directly (no fixture). - DriverReconnectE2eTests: message round-trip through AdminOperationsActor cluster singleton — Ok=true + audit write, without live driver side effect. - DriverStatusHubE2eTests: bridge-mocked fallback — spawns DriverStatusSignalRBridge in the harness ActorSystem with a mock IHubContext, publishes DriverHealthChanged to the driver-health DPS topic, asserts store upsert + hub SendAsync call. - DockerFixtureAvailability helper: TCP-connect probe for skip guards. - Moq 4.20.72 added to central package management for hub mocking. - Design doc §8.3 replaced with concrete pre-ship operator runbook.
87 lines
4.1 KiB
C#
87 lines
4.1 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// E2E integration coverage for the <c>ReconnectDriver</c> command path through
|
|
/// <see cref="IAdminOperationsClient"/>.
|
|
///
|
|
/// <para><b>Scope note:</b> wiring a live <c>DriverInstanceActor</c> for the full
|
|
/// Healthy → Reconnecting → Healthy health-transition assertion requires a deployed
|
|
/// driver row in the config DB, a real fixture endpoint, and the
|
|
/// <c>DriverHostActor</c> 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 <c>AdminOperationsActor</c> singleton: the command is
|
|
/// accepted, persisted as a <c>ConfigEdit</c> audit row, and the reply carries
|
|
/// <c>Ok = true</c> with the matching <c>CorrelationId</c>. The DPS broadcast
|
|
/// that triggers the actor-side reconnect is exercised by the control-plane unit
|
|
/// tests that mock <c>IActorRef</c>.</para>
|
|
/// </summary>
|
|
[Trait("Category", "Integration")]
|
|
public sealed class DriverReconnectE2eTests
|
|
{
|
|
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
|
|
|
/// <summary>
|
|
/// Verifies that a <see cref="ReconnectDriver"/> message dispatched through
|
|
/// <see cref="IAdminOperationsClient.AskAsync{T}"/> returns a
|
|
/// <see cref="ReconnectDriverResult"/> with <c>Ok = true</c> and the matching
|
|
/// correlation ID, confirming the cluster-singleton round-trip works end-to-end.
|
|
///
|
|
/// <para>The instance ID used here ("reconnect-e2e-nonexistent") does not correspond
|
|
/// to a deployed driver, so no <c>DriverInstanceActor</c> will act on the DPS
|
|
/// broadcast — the test is validating the command ingestion and reply path only.</para>
|
|
/// </summary>
|
|
[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<IAdminOperationsClient>();
|
|
|
|
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<ReconnectDriverResult>(msg, Ct);
|
|
|
|
result.CorrelationId.ShouldBe(correlationId);
|
|
result.Ok.ShouldBeTrue($"ReconnectDriver round-trip failed: {result.Message}");
|
|
result.Message.ShouldBeNull();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that a second <see cref="ReconnectDriver"/> for the same instance ID
|
|
/// is also accepted (idempotent at the actor layer — the actor simply re-broadcasts
|
|
/// to DPS and writes another <c>ConfigEdit</c> row).
|
|
/// </summary>
|
|
[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<IAdminOperationsClient>();
|
|
|
|
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<ReconnectDriverResult>(first, Ct);
|
|
var r2 = await client.AskAsync<ReconnectDriverResult>(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);
|
|
}
|
|
}
|