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"));
}
}