Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/ClusterFormationTests.cs
Joseph Doherty d6fac2d81d test(host): 2-node integration test harness + consolidate to one ActorSystem (Task 58)
Builds TwoNodeClusterHarness: two in-process Host-equivalent nodes sharing
an in-memory ConfigDb. Forms a 2-member Akka cluster. ClusterFormationTests
proves both nodes see each other as admin+driver role members.

Fixes a real production bug uncovered while wiring the harness — Program.cs
ran two separate ActorSystems (one from AddOtOpcUaCluster.AkkaHostedService
with cluster HOCON, one from Akka.Hosting.AddAkka with bare HOCON). Cluster
singletons landed on the bare ActorSystem and could not actually form a
cluster ("Configuration does not contain `akka.cluster` node").

Consolidation:
- AddOtOpcUaCluster now only binds AkkaClusterOptions + registers IClusterRoleInfo
- New WithOtOpcUaClusterBootstrap pushes embedded HOCON + Remote/Cluster options
  into Akka.Hosting's AkkaConfigurationBuilder
- AkkaHostedService.cs deleted — Akka.Hosting now owns the lifecycle
- Program.cs + harness call WithOtOpcUaClusterBootstrap inside AddAkka

Why not WebApplicationFactory<Program>? Program.cs reads OTOPCUA_ROLES from
process env (shared across in-process WAFs); the harness replays Program.cs's
DI graph from a clean WebApplicationBuilder per node with per-node config
overrides. Same production extensions, isolated config + Kestrel + Akka ports.

Tests: 93 v2 tests pass (was 91 + 2 new cluster formation), 0 skipped.
2026-05-26 06:27:04 -04:00

44 lines
1.5 KiB
C#

using Akka.Cluster;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
/// <summary>
/// Smoke test: verifies <see cref="TwoNodeClusterHarness"/> boots two nodes and they form
/// a 2-member cluster with the expected role topology. Failover + deploy scenarios layer
/// on top in Task 59.
/// </summary>
public sealed class ClusterFormationTests
{
[Fact]
public async Task Two_nodes_form_a_2_member_cluster()
{
await using var harness = await TwoNodeClusterHarness.StartAsync();
var aCluster = Akka.Cluster.Cluster.Get(harness.NodeASystem);
var bCluster = Akka.Cluster.Cluster.Get(harness.NodeBSystem);
aCluster.State.Members.Count(m => m.Status == MemberStatus.Up).ShouldBe(2);
bCluster.State.Members.Count(m => m.Status == MemberStatus.Up).ShouldBe(2);
var aRoles = aCluster.State.Members.SelectMany(m => m.Roles).Distinct().ToHashSet();
aRoles.ShouldContain("admin");
aRoles.ShouldContain("driver");
}
[Fact]
public async Task Both_nodes_see_each_other_as_role_members()
{
await using var harness = await TwoNodeClusterHarness.StartAsync();
var aCluster = Akka.Cluster.Cluster.Get(harness.NodeASystem);
aCluster.State.Members
.Where(m => m.Roles.Contains("driver") && m.Status == MemberStatus.Up)
.Count().ShouldBe(2);
aCluster.State.Members
.Where(m => m.Roles.Contains("admin") && m.Status == MemberStatus.Up)
.Count().ShouldBe(2);
}
}