Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundScriptExecutorStalenessTests.cs
T

107 lines
4.4 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests;
/// <summary>
/// Arch-review S1: the compiled-handler cache must be revision-checked against the
/// freshly-fetched ApiMethod.Script so a standby node (whose cache was populated at
/// its own startup) never serves a stale delegate after failover, and a method fixed
/// out-of-band is never stuck behind a stale known-bad record.
/// </summary>
public class InboundScriptExecutorStalenessTests
{
private readonly InboundScriptExecutor _executor;
private readonly RouteHelper _route;
public InboundScriptExecutorStalenessTests()
{
_executor = new InboundScriptExecutor(
NullLogger<InboundScriptExecutor>.Instance, Substitute.For<IServiceProvider>());
_route = new RouteHelper(
Substitute.For<IInstanceLocator>(), Substitute.For<IInstanceRouter>());
}
private Task<InboundScriptResult> Run(ApiMethod m) => _executor.ExecuteAsync(
m, new Dictionary<string, object?>(), _route, TimeSpan.FromSeconds(10));
[Fact]
public async Task UpdatedScript_RecompilesInsteadOfServingStaleHandler()
{
// Simulates the failover scenario: this node compiled v1 at startup...
var v1 = new ApiMethod("stale", "return 1;") { Id = 1, TimeoutSeconds = 10 };
Assert.True(_executor.CompileAndRegister(v1));
var r1 = await Run(v1);
Assert.Contains("1", r1.ResultJson);
// ...the active node updated the method (this node never heard about it);
// the per-request DB fetch hands ExecuteAsync the NEW row.
var v2 = new ApiMethod("stale", "return 2;") { Id = 1, TimeoutSeconds = 10 };
var r2 = await Run(v2);
Assert.True(r2.Success);
Assert.Contains("2", r2.ResultJson); // old cache would have returned 1
}
[Fact]
public async Task KnownBadMethod_FixedScript_CompilesInsteadOfFastFailing()
{
// The mirror-image variant: broken at this node's startup...
var broken = new ApiMethod("fixme", "%%% not C# %%%") { Id = 2, TimeoutSeconds = 10 };
Assert.False(_executor.CompileAndRegister(broken));
var r1 = await Run(broken);
Assert.False(r1.Success);
// ...fixed via the management path on the OTHER node; this node's fresh
// DB fetch carries the fixed script.
var fixedMethod = new ApiMethod("fixme", "return 42;") { Id = 2, TimeoutSeconds = 10 };
var r2 = await Run(fixedMethod);
Assert.True(r2.Success);
Assert.Contains("42", r2.ResultJson);
}
[Fact]
public async Task KnownBadMethod_SameScript_StillFastFails()
{
var broken = new ApiMethod("stillbad", "%%% not C# %%%") { Id = 3, TimeoutSeconds = 10 };
Assert.False(_executor.CompileAndRegister(broken));
// Same script text → the fast-fail record must hold (no per-request Roslyn).
var again = new ApiMethod("stillbad", "%%% not C# %%%") { Id = 3, TimeoutSeconds = 10 };
var r = await Run(again);
Assert.False(r.Success);
Assert.Contains("Script compilation failed", r.ErrorMessage);
}
[Fact]
public async Task TestSeamRegisterHandler_WithoutScript_IsNotRevisionChecked()
{
// RegisterHandler (script-less test seam) pins the handler regardless of row content.
var m = new ApiMethod("pinned", "return 0;") { Id = 4, TimeoutSeconds = 10 };
_executor.RegisterHandler("pinned", async _ => { await Task.CompletedTask; return 99; });
var r = await Run(m);
Assert.True(r.Success);
Assert.Contains("99", r.ResultJson);
}
[Fact]
public async Task InvalidateMethod_DropsHandlerAndKnownBadRecord()
{
var good = new ApiMethod("inv", "return 5;") { Id = 5, TimeoutSeconds = 10 };
Assert.True(_executor.CompileAndRegister(good));
var broken = new ApiMethod("invbad", "%%%") { Id = 6, TimeoutSeconds = 10 };
Assert.False(_executor.CompileAndRegister(broken));
_executor.InvalidateMethod("inv");
_executor.InvalidateMethod("invbad");
Assert.Equal(0, _executor.KnownBadMethodCount);
// After invalidation the method lazily recompiles from the row — still serves.
var r = await Run(good);
Assert.True(r.Success);
}
}