using Akka.Actor; using Akka.TestKit; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin; using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy; using ZB.MOM.WW.OtOpcUa.Commons.Types; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations; using ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests; public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase { /// Verifies that starting a deployment inserts a row and dispatches to the coordinator. [Fact] public void StartDeployment_inserts_deployment_and_dispatches_to_coordinator() { var dbFactory = NewInMemoryDbFactory(); var coordinator = CreateTestProbe("coord"); var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref, Enumerable.Empty())); actor.Tell(new StartDeployment("joe", CorrelationId.NewId())); var dispatch = coordinator.ExpectMsg(TimeSpan.FromSeconds(3)); dispatch.DeploymentId.Value.ShouldNotBe(Guid.Empty); dispatch.RevisionHash.Value.Length.ShouldBe(64); var reply = ExpectMsg(TimeSpan.FromSeconds(3)); reply.Outcome.ShouldBe(StartDeploymentOutcome.Accepted); reply.DeploymentId.ShouldBe(dispatch.DeploymentId); reply.RevisionHash.ShouldBe(dispatch.RevisionHash); using var db = dbFactory.CreateDbContext(); var row = db.Deployments.Single(); row.Status.ShouldBe(DeploymentStatus.Dispatching); row.CreatedBy.ShouldBe("joe"); row.ArtifactBlob.Length.ShouldBeGreaterThan(0); db.ConfigEdits.Count().ShouldBe(1); db.ConfigEdits.Single().EntityType.ShouldBe("Deployment"); } /// Verifies the surgical DraftValidator gate: a Tag↔VirtualTag NodeId collision in /// the live config rejects the deploy (422-mapped ) /// before any coordinator dispatch — and inserts no Deployment row. [Fact] public void StartDeployment_rejects_on_Tag_VirtualTag_NodeId_collision() { var dbFactory = NewInMemoryDbFactory(); using (var db = dbFactory.CreateDbContext()) { db.Equipment.Add(new Configuration.Entities.Equipment { EquipmentUuid = Guid.NewGuid(), EquipmentId = "eq-1", Name = "eq", DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m", }); db.Tags.Add(new Configuration.Entities.Tag { TagId = "tag-speed", DriverInstanceId = "d", EquipmentId = "eq-1", Name = "speed", DataType = "Float", AccessLevel = TagAccessLevel.Read, TagConfig = "{}", }); db.VirtualTags.Add(new Configuration.Entities.VirtualTag { VirtualTagId = "vtag-speed", EquipmentId = "eq-1", Name = "speed", DataType = "Float", ScriptId = "s-1", }); db.SaveChanges(); } var coordinator = CreateTestProbe("coord"); var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref, Enumerable.Empty())); actor.Tell(new StartDeployment("joe", CorrelationId.NewId())); coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500)); var reply = ExpectMsg(TimeSpan.FromSeconds(3)); reply.Outcome.ShouldBe(StartDeploymentOutcome.Rejected); reply.Message.ShouldNotBeNull(); reply.Message.ShouldContain("collide"); // the rule's message text using var verify = dbFactory.CreateDbContext(); verify.Deployments.Count().ShouldBe(0); } /// Verifies that starting a deployment is refused when another is in flight. [Fact] public void StartDeployment_refuses_when_another_is_in_flight() { var dbFactory = NewInMemoryDbFactory(); // Seed an in-flight Deployment. using (var db = dbFactory.CreateDbContext()) { db.Deployments.Add(new Configuration.Entities.Deployment { RevisionHash = new string('a', 64), Status = DeploymentStatus.Dispatching, CreatedBy = "earlier", }); db.SaveChanges(); } var coordinator = CreateTestProbe("coord"); var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref, Enumerable.Empty())); actor.Tell(new StartDeployment("joe", CorrelationId.NewId())); coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500)); var reply = ExpectMsg(TimeSpan.FromSeconds(3)); reply.Outcome.ShouldBe(StartDeploymentOutcome.AnotherDeploymentInFlight); reply.DeploymentId.ShouldNotBeNull(); } }