89d2becba9
Removes the #96 compatibility path that the unified central-cache-invalidation topic (#100 item 3, 153609ef; compat3a31dd2d) carried for one release: - CentralCacheInvalidationBus no longer publishes a bare SitePskInvalidated on site-psk-invalidation; LegacySitePskTopic constant deleted. - CentralCacheInvalidationSubscriber no longer subscribes to that topic or handles a bare SitePskInvalidated; it applies envelopes only. central-cache-invalidation behaviour is unchanged (one envelope per invalidation, origin filtering, per-handler fault isolation). Gate met:8e32f353(carries153609efand3a31dd2d) runs on every central node - DEV vd03 (record581e25ca) and QA vq03/vq04 (record8ba5d5d7). Mixed-version / rolling-upgrade constraint: a central node built before153609eflistens only on site-psk-invalidation and no longer receives PSK invalidations from a newer peer (only its TTL and the site-rejection refresh bound the staleness). Mixed pairs across that boundary are no longer supported; central pairs are stopped and started together anyway. Documented in Component-Host.md and Component-Communication.md. Tests: the legacy publish/subscribe tests are replaced by PskRotation_OnOneNode_ReachesThePeersCaches_ThroughTheUnifiedTopic (node A publishes, node B's PSK cache and secret-resolver cache are invalidated, and nothing else is published) and a bare-message-ignored test with an envelope positive control. The two-node cluster test now asserts the PSK invalidation arrives exactly once and the publisher applies nothing. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
157 lines
7.0 KiB
C#
157 lines
7.0 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Gitea #96: an in-place rotation of <c>SB-GRPC-PSK-{siteId}</c> 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.
|
|
/// </summary>
|
|
public class SitePskRotationInvalidatorTests : TestKit
|
|
{
|
|
private readonly ISecretCacheInvalidator _inner = Substitute.For<ISecretCacheInvalidator>();
|
|
private readonly ISitePskProvider _psk = Substitute.For<ISitePskProvider>();
|
|
private readonly CentralCacheInvalidationBus _bus = new(NullLogger<CentralCacheInvalidationBus>.Instance);
|
|
|
|
private SitePskRotationInvalidator Create(ICentralCacheInvalidationPublisher? publisher) =>
|
|
new(_inner, _psk, publisher, NullLogger<SitePskRotationInvalidator>.Instance);
|
|
|
|
/// <summary>
|
|
/// Verifies that writing a site's PSK secret evicts the resolver cache, drops the site's
|
|
/// cached key and publishes the invalidation once, on the shared topic only (the #96
|
|
/// <c>site-psk-invalidation</c> topic was removed in ScadaBridge#105).
|
|
/// </summary>
|
|
[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<Publish>();
|
|
Assert.Equal(CentralCacheInvalidationBus.Topic, shared.Topic);
|
|
var envelope = Assert.IsType<CentralCacheInvalidationEnvelope>(shared.Message);
|
|
Assert.Equal(new SitePskInvalidated("site-a"), envelope.Invalidation);
|
|
Assert.Equal(_bus.OriginId, envelope.OriginId);
|
|
mediator.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
|
}
|
|
|
|
/// <summary>Negative control: any other secret only evicts the resolver cache.</summary>
|
|
[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));
|
|
}
|
|
|
|
/// <summary>Verifies that without a publisher (or before the actor system exists) an invalidation still applies locally.</summary>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the subscriber applies a peer's invalidation to this node's resolver and PSK
|
|
/// caches, without re-publishing it.
|
|
/// </summary>
|
|
[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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// ScadaBridge#105: a PSK rotation written on one central node reaches the peer node's PSK
|
|
/// cache and its secret-resolver cache through the unified topic alone. Node A's invalidator
|
|
/// publishes; whatever node A puts on the mediator is delivered to node B's subscriber exactly
|
|
/// as <c>DistributedPubSub</c> would deliver it to a subscriber of that topic.
|
|
/// </summary>
|
|
[Fact]
|
|
public void PskRotation_OnOneNode_ReachesThePeersCaches_ThroughTheUnifiedTopic()
|
|
{
|
|
// Node A: the node the secret is written on.
|
|
var mediatorA = CreateTestProbe();
|
|
_bus.SetMediator(mediatorA.Ref);
|
|
var invalidatorA = Create(_bus);
|
|
|
|
// Node B: the peer, with its own bus (own origin id) and its own caches.
|
|
var innerB = Substitute.For<ISecretCacheInvalidator>();
|
|
var pskB = Substitute.For<ISitePskProvider>();
|
|
var busB = new CentralCacheInvalidationBus(NullLogger<CentralCacheInvalidationBus>.Instance);
|
|
var invalidatorB = new SitePskRotationInvalidator(innerB, pskB, busB, NullLogger<SitePskRotationInvalidator>.Instance);
|
|
var subscriberB = Sys.ActorOf(Props.Create(() => new CentralCacheInvalidationSubscriber(
|
|
busB, new ICentralCacheInvalidationHandler[] { invalidatorB })));
|
|
|
|
invalidatorA.Invalidate(new SecretName("SB-GRPC-PSK-site-d"));
|
|
|
|
// Deliver every publish on the unified topic to B; nothing else is published.
|
|
var publish = mediatorA.ExpectMsg<Publish>();
|
|
Assert.Equal("central-cache-invalidation", publish.Topic);
|
|
subscriberB.Tell(publish.Message);
|
|
mediatorA.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
|
|
|
AwaitAssert(() =>
|
|
{
|
|
pskB.Received(1).Invalidate("site-d");
|
|
innerB.Received(1).Invalidate(new SecretName("SB-GRPC-PSK-site-d"));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// ScadaBridge#105: the bare <see cref="SitePskInvalidated"/> that the removed #96 topic
|
|
/// carried is no longer handled — the subscriber only applies envelopes. The envelope row is
|
|
/// the positive control.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Subscriber_IgnoresABarePskInvalidation_ButAppliesTheEnvelope()
|
|
{
|
|
var invalidator = Create(_bus);
|
|
var subscriber = Sys.ActorOf(Props.Create(() => new CentralCacheInvalidationSubscriber(
|
|
_bus, new ICentralCacheInvalidationHandler[] { invalidator })));
|
|
|
|
subscriber.Tell(new SitePskInvalidated("site-c"));
|
|
subscriber.Tell(new CentralCacheInvalidationEnvelope("peer-node", new SitePskInvalidated("site-e")));
|
|
|
|
AwaitAssert(() => _psk.Received(1).Invalidate("site-e"));
|
|
_psk.DidNotReceive().Invalidate("site-c");
|
|
}
|
|
}
|