Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundMethodWarmupServiceTests.cs
T
Joseph Doherty 23c35df234 refactor(inbound-api): use the shared ZB.MOM.WW.Health IActiveNodeGate (ScadaBridge#80)
The InboundAPI carried its own IActiveNodeGate (implemented by Host/Health/ActiveNodeGate)
whose rule already matched the shared Health 0.3.0 AkkaActiveNodeGate — both route to
ClusterActiveNode (oldest Up member). The private interface and implementation are deleted;
InboundAPI references ZB.MOM.WW.Health (0.3.0, already pinned) and InboundApiEndpointFilter,
AuditWriteMiddleware, InboundMethodWarmupService and FlowDebugRunner consume
ZB.MOM.WW.Health.IActiveNodeGate. The Central branch of Program.cs registers
AkkaActiveNodeGate (unscoped, like the active-node health check).

Warm-up behaviour is kept: the shared gate answers false while no ActorSystem resolves, while
this member is not Up, and on any cluster-init fault. It resolves the ActorSystem through the
existing DI singleton bridge (GetOrCreateActorSystem), the same path the health probes use.

Tests: CompositionRoot asserts the shared gate type (fails with the registration removed);
the evaluator wiring test now drives AkkaActiveNodeGate; new warm-up test (no ActorSystem ->
not active). Nothing was missing from the Health 0.3.0 package.

(ScadaBridge#80)

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 13:54:29 -05:00

71 lines
3.0 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.Health;
namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests;
/// <summary>Tests for <see cref="InboundMethodWarmupService"/> (review 2026-10-21 05/U3).</summary>
public class InboundMethodWarmupServiceTests
{
private sealed class Lifetime : IHostApplicationLifetime
{
public CancellationTokenSource StartedCts { get; } = new();
public CancellationToken ApplicationStarted => StartedCts.Token;
public CancellationToken ApplicationStopping => CancellationToken.None;
public CancellationToken ApplicationStopped => CancellationToken.None;
public void StopApplication() { }
}
private sealed class Gate(bool active) : IActiveNodeGate
{
public bool IsActiveNode => active;
}
private static (InboundMethodWarmupService svc, InboundScriptExecutor exec, Lifetime life) Build(bool active)
{
var repo = Substitute.For<IInboundApiRepository>();
repo.GetAllApiMethodsAsync(Arg.Any<CancellationToken>())
.Returns(new List<ApiMethod> { new("warm", "return 1;") { Id = 1, TimeoutSeconds = 10 } });
var provider = new ServiceCollection().AddSingleton(repo).BuildServiceProvider();
var exec = new InboundScriptExecutor(NullLogger<InboundScriptExecutor>.Instance, Substitute.For<IServiceProvider>());
var life = new Lifetime();
var svc = new InboundMethodWarmupService(
provider.GetRequiredService<IServiceScopeFactory>(), exec, life,
NullLogger<InboundMethodWarmupService>.Instance, new Gate(active));
return (svc, exec, life);
}
/// <summary>Verifies the warm-up waits for ApplicationStarted and then compiles on an active node.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ActiveNode_CompilesAfterApplicationStarted_NotBefore()
{
var (svc, exec, life) = Build(active: true);
await svc.StartAsync(CancellationToken.None);
await Task.Delay(200);
Assert.False(exec.HasHandler("warm"));
life.StartedCts.Cancel();
for (var i = 0; i < 100 && !exec.HasHandler("warm"); i++) await Task.Delay(50);
Assert.True(exec.HasHandler("warm"));
await svc.StopAsync(CancellationToken.None);
}
/// <summary>Verifies a standby node does not warm up.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task StandbyNode_DoesNotCompile()
{
var (svc, exec, life) = Build(active: false);
await svc.StartAsync(CancellationToken.None);
life.StartedCts.Cancel();
await Task.Delay(500);
Assert.False(exec.HasHandler("warm"));
await svc.StopAsync(CancellationToken.None);
}
}