using Akka.Actor; using Akka.Cluster.Tools.Client; 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.Messages.Mesh; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; using ZB.MOM.WW.OtOpcUa.Commons.Types; using ZB.MOM.WW.OtOpcUa.Runtime.Communication; using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness; namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Communication; public class NodeCommunicationActorTests : RuntimeActorTestBase { private static DispatchDeployment NewDispatch() => new( new DeploymentId(Guid.NewGuid()), new RevisionHash("rev-1"), new CorrelationId(Guid.NewGuid())); [Fact] public void DispatchDeployment_from_central_lands_on_the_node_local_event_stream() { var subscriber = CreateTestProbe(); Sys.EventStream.Subscribe(subscriber.Ref, typeof(DispatchDeployment)); var actor = Sys.ActorOf(NodeCommunicationActor.Props(centralClient: null)); var dispatch = NewDispatch(); actor.Tell(dispatch); subscriber.ExpectMsg(TimeSpan.FromSeconds(5)).ShouldBe(dispatch); } [Fact] public void A_command_is_published_exactly_once() { // The reason inbound commands do NOT go back onto their DPS topic: central SendToAll-s to // every node's comm actor, so a mesh-wide republish would deliver N copies to every // subscriber. One publish per inbound command is the whole point. var subscriber = CreateTestProbe(); Sys.EventStream.Subscribe(subscriber.Ref, typeof(DispatchDeployment)); var actor = Sys.ActorOf(NodeCommunicationActor.Props(centralClient: null)); actor.Tell(NewDispatch()); subscriber.ExpectMsg(TimeSpan.FromSeconds(5)); subscriber.ExpectNoMsg(TimeSpan.FromMilliseconds(300)); } [Fact] public void RestartDriver_from_central_lands_on_the_event_stream() { var subscriber = CreateTestProbe(); Sys.EventStream.Subscribe(subscriber.Ref, typeof(RestartDriver)); var actor = Sys.ActorOf(NodeCommunicationActor.Props(centralClient: null)); var msg = new RestartDriver("C1", "driver-1", "alice", Guid.NewGuid()); actor.Tell(msg); subscriber.ExpectMsg(TimeSpan.FromSeconds(5)).ShouldBe(msg); } [Fact] public void ReconnectDriver_from_central_lands_on_the_event_stream() { var subscriber = CreateTestProbe(); Sys.EventStream.Subscribe(subscriber.Ref, typeof(ReconnectDriver)); var actor = Sys.ActorOf(NodeCommunicationActor.Props(centralClient: null)); var msg = new ReconnectDriver("C1", "driver-1", "alice", Guid.NewGuid()); actor.Tell(msg); subscriber.ExpectMsg(TimeSpan.FromSeconds(5)).ShouldBe(msg); } [Fact] public void AlarmCommand_from_central_lands_on_the_event_stream() { // ScriptedAlarmHostActor is a CHILD of DriverHostActor with no registry key, so there is no // ref to hand this actor at wiring time — the EventStream is what makes the alarm-command // leg possible without a registration handshake. var subscriber = CreateTestProbe(); Sys.EventStream.Subscribe(subscriber.Ref, typeof(AlarmCommand)); var actor = Sys.ActorOf(NodeCommunicationActor.Props(centralClient: null)); var msg = new AlarmCommand("alarm-1", "Acknowledge", "alice", "comment", null); actor.Tell(msg); subscriber.ExpectMsg(TimeSpan.FromSeconds(5)).ShouldBe(msg); } [Fact] public void ApplyAck_goes_to_central_as_a_Send_not_a_SendToAll() { // Send, not SendToAll: the deploy coordinator is one singleton behind the central comm // actor's proxy. SendToAll would deliver one ack per central node and the coordinator would // count this node twice. var client = CreateTestProbe(); var actor = Sys.ActorOf(NodeCommunicationActor.Props(client.Ref)); var ack = new ApplyAck( new DeploymentId(Guid.NewGuid()), new NodeId("host-a:4053"), ApplyAckOutcome.Applied, null, new CorrelationId(Guid.NewGuid())); actor.Tell(ack); var sent = client.ExpectMsg(TimeSpan.FromSeconds(5)); sent.Path.ShouldBe(MeshPaths.CentralCommunication); sent.Message.ShouldBe(ack); } [Fact] public void An_ack_with_no_central_client_is_dropped_with_a_warning() { var actor = Sys.ActorOf(NodeCommunicationActor.Props(centralClient: null)); var ack = new ApplyAck( new DeploymentId(Guid.NewGuid()), new NodeId("host-a:4053"), ApplyAckOutcome.Applied, null, new CorrelationId(Guid.NewGuid())); EventFilter.Warning(contains: "dropping ApplyAck").ExpectOne( TimeSpan.FromSeconds(5), () => actor.Tell(ack)); } }