using Akka.Actor; using Akka.Cluster.Tools.PublishSubscribe; using Akka.TestKit.Xunit2; using Microsoft.Extensions.Logging.Abstractions; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Communication; namespace ZB.MOM.WW.ScadaBridge.Host.Tests; /// /// ScadaBridge#100 item 3: the central cache-invalidation bus publishes typed invalidations on /// one topic, and the subscriber hands the peer's (never its own) to every handler. /// public class CentralCacheInvalidationBusTests : TestKit { private sealed class RecordingHandler : ICentralCacheInvalidationHandler { public List Applied { get; } = new(); public void Apply(ICentralCacheInvalidation invalidation) { lock (Applied) Applied.Add(invalidation); } } private sealed class ThrowingHandler : ICentralCacheInvalidationHandler { public void Apply(ICentralCacheInvalidation invalidation) => throw new InvalidOperationException("boom"); } private readonly CentralCacheInvalidationBus _bus = new(NullLogger.Instance); /// Publishing wraps the invalidation with this node's origin id on the shared topic. [Fact] public void Publish_SendsAnEnvelopeOnTheSharedTopic() { var mediator = CreateTestProbe(); _bus.SetMediator(mediator.Ref); _bus.Publish(new RoleMappingsInvalidated()); var publish = mediator.ExpectMsg(); Assert.Equal("central-cache-invalidation", publish.Topic); var envelope = Assert.IsType(publish.Message); Assert.Equal(_bus.OriginId, envelope.OriginId); Assert.IsType(envelope.Invalidation); mediator.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); // only PSK also uses the legacy topic } /// Before the mediator exists publishing is a silent no-op. [Fact] public void Publish_WithoutAMediator_DoesNotThrow() { _bus.Publish(new ExternalDefinitionsInvalidated()); } /// The peer's invalidation reaches every handler; this node's own broadcast is skipped. [Fact] public void Subscriber_AppliesPeerInvalidations_AndSkipsItsOwn() { var handler = new RecordingHandler(); var subscriber = Sys.ActorOf(Props.Create(() => new CentralCacheInvalidationSubscriber( _bus, new ICentralCacheInvalidationHandler[] { new ThrowingHandler(), handler }))); subscriber.Tell(new CentralCacheInvalidationEnvelope(_bus.OriginId, new RoleMappingsInvalidated())); subscriber.Tell(new CentralCacheInvalidationEnvelope("peer", new ExternalDefinitionsInvalidated())); subscriber.Tell(new CentralCacheInvalidationEnvelope("peer", new InboundApiMethodInvalidated("M"))); AwaitAssert(() => { lock (handler.Applied) { // A throwing handler ahead of it does not stop delivery. Assert.Equal( new ICentralCacheInvalidation[] { new ExternalDefinitionsInvalidated(), new InboundApiMethodInvalidated("M") }, handler.Applied); } }); } }