using Akka.Actor; using Akka.Cluster.Tools.PublishSubscribe; using Akka.TestKit.Xunit2; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Communication; using ZB.MOM.WW.ScadaBridge.Communication.Grpc; using ZB.MOM.WW.Secrets.Abstractions; namespace ZB.MOM.WW.ScadaBridge.Host.Tests; /// /// Gitea #96: an in-place rotation of SB-GRPC-PSK-{siteId} written through the secrets /// write path must drop central's cached key on this node and on the peer central node. Since /// ScadaBridge#100 item 3 the peer is told over the shared central cache-invalidation topic. /// public class SitePskRotationInvalidatorTests : TestKit { private readonly ISecretCacheInvalidator _inner = Substitute.For(); private readonly ISitePskProvider _psk = Substitute.For(); private readonly CentralCacheInvalidationBus _bus = new(NullLogger.Instance); private SitePskRotationInvalidator Create(ICentralCacheInvalidationPublisher? publisher) => new(_inner, _psk, publisher, NullLogger.Instance); /// /// Verifies that writing a site's PSK secret evicts the resolver cache, drops the site's /// cached key and publishes the invalidation on the shared topic and, for a mixed-version /// peer, bare on the legacy #96 topic. /// [Fact] public void WritingAPskSecret_DropsTheCachedKey_AndTellsThePeer() { var mediator = CreateTestProbe(); _bus.SetMediator(mediator.Ref); var invalidator = Create(_bus); // The secrets library lowercases names, so the site id arrives lowercased too. invalidator.Invalidate(new SecretName("SB-GRPC-PSK-site-a")); _inner.Received(1).Invalidate(new SecretName("SB-GRPC-PSK-site-a")); _psk.Received(1).Invalidate("site-a"); var shared = mediator.ExpectMsg(); Assert.Equal(CentralCacheInvalidationBus.Topic, shared.Topic); var envelope = Assert.IsType(shared.Message); Assert.Equal(new SitePskInvalidated("site-a"), envelope.Invalidation); Assert.Equal(_bus.OriginId, envelope.OriginId); var legacy = mediator.ExpectMsg(); Assert.Equal("site-psk-invalidation", legacy.Topic); Assert.Equal(new SitePskInvalidated("site-a"), legacy.Message); } /// Negative control: any other secret only evicts the resolver cache. [Fact] public void WritingAnyOtherSecret_OnlyEvictsTheResolverCache() { var mediator = CreateTestProbe(); _bus.SetMediator(mediator.Ref); var invalidator = Create(_bus); invalidator.Invalidate(new SecretName("SMTP-PASSWORD")); _inner.Received(1).Invalidate(new SecretName("SMTP-PASSWORD")); _psk.DidNotReceiveWithAnyArgs().Invalidate(default!); mediator.ExpectNoMsg(TimeSpan.FromMilliseconds(200)); } /// Verifies that without a publisher (or before the actor system exists) an invalidation still applies locally. [Fact] public void WithoutAPublisher_TheLocalInvalidationStillApplies() { Create(null).Invalidate(new SecretName("SB-GRPC-PSK-site-a")); Create(_bus).Invalidate(new SecretName("SB-GRPC-PSK-site-b")); // bus has no mediator yet _psk.Received(1).Invalidate("site-a"); _psk.Received(1).Invalidate("site-b"); } /// /// Verifies that the subscriber applies a peer's invalidation to this node's resolver and PSK /// caches, without re-publishing it. /// [Fact] public void Subscriber_AppliesAPeerInvalidation_WithoutRepublishing() { var mediator = CreateTestProbe(); _bus.SetMediator(mediator.Ref); var invalidator = Create(_bus); var subscriber = Sys.ActorOf(Props.Create(() => new CentralCacheInvalidationSubscriber( _bus, new ICentralCacheInvalidationHandler[] { invalidator }))); subscriber.Tell(new CentralCacheInvalidationEnvelope("peer-node", new SitePskInvalidated("site-b"))); AwaitAssert(() => { _psk.Received(1).Invalidate("site-b"); _inner.Received(1).Invalidate(new SecretName("SB-GRPC-PSK-site-b")); }); mediator.ExpectNoMsg(TimeSpan.FromMilliseconds(200)); } /// /// Migration: a bare from a peer still on the #96 topic is /// applied too. /// [Fact] public void Subscriber_AppliesALegacyBarePskInvalidation() { var invalidator = Create(_bus); var subscriber = Sys.ActorOf(Props.Create(() => new CentralCacheInvalidationSubscriber( _bus, new ICentralCacheInvalidationHandler[] { invalidator }))); subscriber.Tell(new SitePskInvalidated("site-c")); AwaitAssert(() => _psk.Received(1).Invalidate("site-c")); } }