using Akka.Actor; using Akka.Cluster.Tools.PublishSubscribe; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging; using ZB.MOM.WW.OtOpcUa.Runtime.Scripting; using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness; using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags; namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Scripting; /// /// Verifies routes a /// onto the Akka DistributedPubSub script-logs topic and never throws back into /// the calling logging pipeline. /// public sealed class DpsScriptLogPublisherTests : RuntimeActorTestBase { /// Builds a representative entry for assertions. private static ScriptLogEntry SampleEntry() => new( ScriptId: "S1", Level: "Information", Message: "hello from script", TimestampUtc: DateTime.UtcNow, VirtualTagId: "V1", AlarmId: null, EquipmentId: "EQ1"); /// /// A published entry is delivered verbatim to a probe subscribed to the /// topic via the DPS mediator. /// [Fact] public void Publish_routes_entry_to_subscribers_of_the_script_logs_topic() { var probe = CreateTestProbe(); var mediator = DistributedPubSub.Get(Sys).Mediator; // Send the Subscribe FROM the probe so the SubscribeAck returns to the probe's mailbox // (a bare mediator.Tell would route the ack to the implicit TestActor instead). probe.Send(mediator, new Subscribe(VirtualTagActor.ScriptLogsTopic, probe.Ref)); probe.ExpectMsg(TimeSpan.FromSeconds(10)); var publisher = new DpsScriptLogPublisher(() => Sys); var entry = SampleEntry(); // The SubscribeAck confirms the subscribe was registered, but DPS topic membership is // eventually-consistent — publish in a short retry loop until the probe sees the entry // (mirrors the Host.IntegrationTests DPS round-trip pattern), then assert the payload. AwaitAssert(() => { publisher.Publish(entry); probe.ExpectMsg(TimeSpan.FromMilliseconds(200)).ShouldBe(entry); }, duration: TimeSpan.FromSeconds(5), interval: TimeSpan.FromMilliseconds(250)); } /// /// A logging sink must never throw into the logging pipeline: if the system /// accessor throws, swallows it. /// [Fact] public void Publish_does_not_throw_when_the_system_accessor_throws() { var publisher = new DpsScriptLogPublisher( () => throw new InvalidOperationException("system not ready")); Should.NotThrow(() => publisher.Publish(SampleEntry())); } /// The constructor rejects a null system accessor. [Fact] public void Null_system_accessor_throws_ArgumentNullException() { Should.Throw(() => new DpsScriptLogPublisher(null!)); } }