fix(code-review): resolve Batch 3 wave A (OpcUaServer history/guard, ControlPlane topology gate)
- OpcUaServer-002: HistoryRead-Events NumValuesPerNode==0 now maps to unbounded (int.MaxValue) instead of the backend default-cap sentinel; no Core.Abstractions contract change (+EventMaxEvents helper tests) - OpcUaServer-004: EnsureAddressSpaceCreated guard on public mutators -> clear InvalidOperationException instead of bare NRE if called pre-start (+tests) - OpcUaServer-003: Deferred (endUtc inclusive/exclusive needs live Wonderware boundary confirmation) - Configuration-013: wire DraftValidator.ValidateClusterTopology into AdminOperationsActor deploy gate (read-only, no migration) (+2 tests)
This commit is contained in:
@@ -435,6 +435,123 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
|
||||
reply.Message.ShouldContain("1 script(s) will compile");
|
||||
}
|
||||
|
||||
/// <summary>Verifies the cluster-topology guard is wired into the deploy gate (Configuration-013):
|
||||
/// a <see cref="RedundancyMode.Hot"/> cluster with only ONE enabled <see cref="ClusterNode"/>
|
||||
/// (the second toggled off) is <see cref="StartDeploymentOutcome.Rejected"/> with the
|
||||
/// <c>ClusterEnabledNodeCountMismatch</c> topology error in the message — no coordinator dispatch,
|
||||
/// no Deployment row. The row-level SQL CHECK cannot see the disabled-node flag, so this proves the
|
||||
/// managed <see cref="Configuration.Validation.DraftValidator.ValidateClusterTopology"/> guard runs
|
||||
/// at deploy time rather than sitting inert.</summary>
|
||||
[Fact]
|
||||
public void StartDeployment_rejects_on_invalid_cluster_topology_disabled_node()
|
||||
{
|
||||
var dbFactory = NewInMemoryDbFactory();
|
||||
using (var db = dbFactory.CreateDbContext())
|
||||
{
|
||||
db.ServerClusters.Add(new Configuration.Entities.ServerCluster
|
||||
{
|
||||
ClusterId = "LINE3-OPCUA",
|
||||
Name = "Line 3",
|
||||
Enterprise = "zb",
|
||||
Site = "dev",
|
||||
NodeCount = 2,
|
||||
RedundancyMode = RedundancyMode.Hot, // declared 2 + Hot, but only 1 enabled below
|
||||
CreatedBy = "seed",
|
||||
});
|
||||
db.ClusterNodes.Add(new Configuration.Entities.ClusterNode
|
||||
{
|
||||
NodeId = "LINE3-OPCUA-A",
|
||||
ClusterId = "LINE3-OPCUA",
|
||||
Host = "host-a",
|
||||
ApplicationUri = "urn:line3:a",
|
||||
Enabled = true,
|
||||
CreatedBy = "seed",
|
||||
});
|
||||
db.ClusterNodes.Add(new Configuration.Entities.ClusterNode
|
||||
{
|
||||
NodeId = "LINE3-OPCUA-B",
|
||||
ClusterId = "LINE3-OPCUA",
|
||||
Host = "host-b",
|
||||
ApplicationUri = "urn:line3:b",
|
||||
Enabled = false, // toggled off → effective enabled-count = 1 while mode stays Hot
|
||||
CreatedBy = "seed",
|
||||
});
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
var coordinator = CreateTestProbe("coord");
|
||||
var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref, Enumerable.Empty<IDriverProbe>()));
|
||||
|
||||
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
|
||||
|
||||
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3));
|
||||
reply.Outcome.ShouldBe(StartDeploymentOutcome.Rejected);
|
||||
reply.Message.ShouldNotBeNull();
|
||||
reply.Message.ShouldContain("ClusterEnabledNodeCountMismatch");
|
||||
|
||||
using var verify = dbFactory.CreateDbContext();
|
||||
verify.Deployments.Count().ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the topology guard does NOT spuriously reject a well-formed cluster: a
|
||||
/// <see cref="RedundancyMode.Hot"/> cluster whose two <see cref="ClusterNode"/>s are both enabled
|
||||
/// passes the topology check, so a deploy of an otherwise-valid config is
|
||||
/// <see cref="StartDeploymentOutcome.Accepted"/> with no topology error in the message and a row
|
||||
/// inserted. Pairs with the rejecting test to prove the guard is discriminating, not blanket.</summary>
|
||||
[Fact]
|
||||
public void StartDeployment_accepts_when_cluster_topology_is_valid()
|
||||
{
|
||||
var dbFactory = NewInMemoryDbFactory();
|
||||
using (var db = dbFactory.CreateDbContext())
|
||||
{
|
||||
db.ServerClusters.Add(new Configuration.Entities.ServerCluster
|
||||
{
|
||||
ClusterId = "LINE3-OPCUA",
|
||||
Name = "Line 3",
|
||||
Enterprise = "zb",
|
||||
Site = "dev",
|
||||
NodeCount = 2,
|
||||
RedundancyMode = RedundancyMode.Hot,
|
||||
CreatedBy = "seed",
|
||||
});
|
||||
db.ClusterNodes.Add(new Configuration.Entities.ClusterNode
|
||||
{
|
||||
NodeId = "LINE3-OPCUA-A",
|
||||
ClusterId = "LINE3-OPCUA",
|
||||
Host = "host-a",
|
||||
ApplicationUri = "urn:line3:a",
|
||||
Enabled = true,
|
||||
CreatedBy = "seed",
|
||||
});
|
||||
db.ClusterNodes.Add(new Configuration.Entities.ClusterNode
|
||||
{
|
||||
NodeId = "LINE3-OPCUA-B",
|
||||
ClusterId = "LINE3-OPCUA",
|
||||
Host = "host-b",
|
||||
ApplicationUri = "urn:line3:b",
|
||||
Enabled = true, // both enabled → matches declared NodeCount=2 + Hot
|
||||
CreatedBy = "seed",
|
||||
});
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
var coordinator = CreateTestProbe("coord");
|
||||
var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref, Enumerable.Empty<IDriverProbe>()));
|
||||
|
||||
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
|
||||
|
||||
coordinator.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(3));
|
||||
|
||||
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3));
|
||||
reply.Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
|
||||
(reply.Message is null || !reply.Message.Contains("ClusterEnabledNodeCountMismatch")).ShouldBeTrue();
|
||||
(reply.Message is null || !reply.Message.Contains("ClusterRedundancyModeInvalid")).ShouldBeTrue();
|
||||
|
||||
using var verify = dbFactory.CreateDbContext();
|
||||
verify.Deployments.Count().ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that starting a deployment is refused when another is in flight.</summary>
|
||||
[Fact]
|
||||
public void StartDeployment_refuses_when_another_is_in_flight()
|
||||
|
||||
Reference in New Issue
Block a user