using Akka.Actor; using Akka.Cluster.Tools.PublishSubscribe; using Akka.TestKit.Xunit2; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; 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. /// public class SitePskRotationInvalidatorTests : TestKit { private readonly ISecretCacheInvalidator _inner = Substitute.For(); private readonly ISitePskProvider _psk = Substitute.For(); private SitePskRotationInvalidator Create() => new(_inner, _psk, NullLogger.Instance); /// /// Verifies that writing a site's PSK secret evicts the resolver cache, drops the site's /// cached key and publishes the invalidation to the peer. /// [Fact] public void WritingAPskSecret_DropsTheCachedKey_AndTellsThePeer() { var mediator = CreateTestProbe(); var invalidator = Create(); invalidator.SetMediator(mediator.Ref); // 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 publish = mediator.ExpectMsg(); Assert.Equal(SitePskRotationInvalidator.Topic, publish.Topic); Assert.Equal(new SitePskInvalidated("site-a"), publish.Message); } /// Negative control: any other secret only evicts the resolver cache. [Fact] public void WritingAnyOtherSecret_OnlyEvictsTheResolverCache() { var mediator = CreateTestProbe(); var invalidator = Create(); invalidator.SetMediator(mediator.Ref); 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 before the actor system exists an invalidation still applies locally. [Fact] public void WithoutAMediator_TheLocalInvalidationStillApplies() { Create().Invalidate(new SecretName("SB-GRPC-PSK-site-a")); _psk.Received(1).Invalidate("site-a"); } /// /// 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(); var invalidator = Create(); invalidator.SetMediator(mediator.Ref); var subscriber = Sys.ActorOf(Props.Create(() => new SitePskInvalidationSubscriber(invalidator))); subscriber.Tell(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)); } }