Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CentralCacheInvalidationBusTests.cs
T
Joseph Doherty 89d2becba9 refactor(host): remove legacy site-psk-invalidation topic (#105)
Removes the #96 compatibility path that the unified central-cache-invalidation
topic (#100 item 3, 153609ef; compat 3a31dd2d) 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 (carries 153609ef and 3a31dd2d) runs on every central
node - DEV vd03 (record 581e25ca) and QA vq03/vq04 (record 8ba5d5d7).

Mixed-version / rolling-upgrade constraint: a central node built before
153609ef listens 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
2026-11-13 08:43:55 -05:00

81 lines
3.3 KiB
C#

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;
/// <summary>
/// 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.
/// </summary>
public class CentralCacheInvalidationBusTests : TestKit
{
private sealed class RecordingHandler : ICentralCacheInvalidationHandler
{
public List<ICentralCacheInvalidation> 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<CentralCacheInvalidationBus>.Instance);
/// <summary>Publishing wraps the invalidation with this node's origin id on the shared topic.</summary>
[Fact]
public void Publish_SendsAnEnvelopeOnTheSharedTopic()
{
var mediator = CreateTestProbe();
_bus.SetMediator(mediator.Ref);
_bus.Publish(new RoleMappingsInvalidated());
var publish = mediator.ExpectMsg<Publish>();
Assert.Equal("central-cache-invalidation", publish.Topic);
var envelope = Assert.IsType<CentralCacheInvalidationEnvelope>(publish.Message);
Assert.Equal(_bus.OriginId, envelope.OriginId);
Assert.IsType<RoleMappingsInvalidated>(envelope.Invalidation);
mediator.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); // one topic, one publish (#105)
}
/// <summary>Before the mediator exists publishing is a silent no-op.</summary>
[Fact]
public void Publish_WithoutAMediator_DoesNotThrow()
{
_bus.Publish(new ExternalDefinitionsInvalidated());
}
/// <summary>The peer's invalidation reaches every handler; this node's own broadcast is skipped.</summary>
[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);
}
});
}
}