using System.Diagnostics; using Akka.Actor; using Akka.TestKit.Xunit2; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Lifecycle; using ZB.MOM.WW.ScadaBridge.Communication.Grpc; namespace ZB.MOM.WW.ScadaBridge.Communication.Tests; /// /// WP1.6: a send to a site with no configured gRPC channel must fail the caller's Ask immediately /// ( wrapping ) instead of /// warning-and-dropping the message and leaving the Ask to expire at its full timeout. /// public class GrpcSiteTransportFailFastTests : TestKit { private static readonly CommunicationOptions Opts = new() { DeploymentTimeout = TimeSpan.FromSeconds(120), LifecycleTimeout = TimeSpan.FromSeconds(30), ArtifactDeploymentTimeout = TimeSpan.FromSeconds(60), QueryTimeout = TimeSpan.FromSeconds(30), IntegrationTimeout = TimeSpan.FromSeconds(30), DebugViewTimeout = TimeSpan.FromSeconds(10) }; private static GrpcSiteTransport BuildTransport() => new( new SitePairChannelProvider( new NoKeyProvider(), Options.Create(Opts), NullLogger.Instance), Opts, NullLogger.Instance); [Fact] public void Send_ToUnconfiguredSite_FaultsTheAskFastInsteadOfTimingOut() { // No ReconcileSites/UpdateSite call was ever made for this site — exactly the "unknown // site" shape SitePairChannelProvider throws SiteChannelUnavailableException for. var transport = BuildTransport(); var probe = CreateTestProbe(); var envelope = new SiteEnvelope( "unconfigured-site", new EnableInstanceCommand("cmd-1", "Site1.Pump1", DateTimeOffset.UtcNow)); var stopwatch = Stopwatch.StartNew(); transport.Send(envelope, probe.Ref); // The old warn-and-drop behavior would leave the caller silent until its own Ask timeout // (30s in production). Well under 1s here proves the fail-fast path, not a lucky race. var failure = probe.ExpectMsg(TimeSpan.FromSeconds(1)); stopwatch.Stop(); Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(1), $"expected the Ask to fault well under 1s; took {stopwatch.Elapsed}"); var cause = Assert.IsType(failure.Cause); Assert.Equal("unconfigured-site", cause.SiteId); Assert.Contains("unconfigured-site", cause.Message); } [Fact] public async Task Ask_AgainstUnconfiguredSite_CompletesFaultedFastViaTellStatusFailure() { // Mirrors how CommunicationService actually calls this seam: Ask a temp actor, transport // Tells it back. Proves the fail-fast reply satisfies a real Akka Ask, not just a TestProbe. var transport = BuildTransport(); var askActor = Sys.ActorOf(Props.Create(() => new EchoingAskTarget(transport))); var stopwatch = Stopwatch.StartNew(); var ex = await Assert.ThrowsAsync(async () => await askActor.Ask( new SiteEnvelope("unconfigured-site", new EnableInstanceCommand( "cmd-1", "Site1.Pump1", DateTimeOffset.UtcNow)), TimeSpan.FromSeconds(30))); stopwatch.Stop(); Assert.Equal("unconfigured-site", ex.SiteId); Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(1), $"expected the Ask to fault well under the 30s timeout; took {stopwatch.Elapsed}"); } /// Relays an Ask into the transport, exactly as CentralCommunicationActor does. private sealed class EchoingAskTarget : ReceiveActor { public EchoingAskTarget(GrpcSiteTransport transport) { Receive(env => transport.Send(env, Sender)); } } private sealed class NoKeyProvider : ISitePskProvider { public ValueTask GetAsync(string siteId, CancellationToken ct) => new("k"); public void Invalidate(string siteId) { } } }