513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
178 lines
7.7 KiB
C#
178 lines
7.7 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NSubstitute;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Scripting;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Scripting;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests;
|
|
|
|
/// <summary>
|
|
/// Arch-review R2 N1: the Inbound API must be a REAL subscriber of
|
|
/// IScriptArtifactChangeBus — a bundle-import publish must invalidate a cached
|
|
/// handler immediately, without waiting for the per-request revision-check
|
|
/// self-heal (which remains the correctness fallback per the invalidation
|
|
/// contract's normative semantics).
|
|
/// </summary>
|
|
public class ScriptArtifactChangeSubscriberTests
|
|
{
|
|
/// <summary>Minimal in-test bus: records subscriptions, delivers synchronously.</summary>
|
|
private sealed class RecordingBus : IScriptArtifactChangeBus
|
|
{
|
|
private readonly List<Action<ScriptArtifactsChanged>> _handlers = new();
|
|
/// <summary>Gets the number of currently registered handlers.</summary>
|
|
public int SubscriberCount => _handlers.Count;
|
|
|
|
/// <inheritdoc />
|
|
public void Publish(ScriptArtifactsChanged notification)
|
|
{
|
|
foreach (var handler in _handlers.ToArray()) handler(notification);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IDisposable Subscribe(Action<ScriptArtifactsChanged> handler)
|
|
{
|
|
_handlers.Add(handler);
|
|
return new Token(() => _handlers.Remove(handler));
|
|
}
|
|
|
|
private sealed class Token(Action dispose) : IDisposable
|
|
{
|
|
/// <summary>Releases the resources used by this instance.</summary>
|
|
public void Dispose() => dispose();
|
|
}
|
|
}
|
|
|
|
private readonly InboundScriptExecutor _executor = new(
|
|
NullLogger<InboundScriptExecutor>.Instance, Substitute.For<IServiceProvider>());
|
|
private readonly ApiMethodCache _methodCache = new(TimeSpan.FromMinutes(5));
|
|
private readonly RecordingBus _bus = new();
|
|
private readonly RouteHelper _route = new(
|
|
Substitute.For<IInstanceLocator>(), Substitute.For<IInstanceRouter>());
|
|
|
|
private ScriptArtifactChangeSubscriber CreateSubscriber(IScriptArtifactChangeBus? bus) =>
|
|
new(_executor, _methodCache, NullLogger<ScriptArtifactChangeSubscriber>.Instance, bus);
|
|
|
|
private Task<InboundScriptResult> Run(ApiMethod m) => _executor.ExecuteAsync(
|
|
m, new Dictionary<string, object?>(), _route, TimeSpan.FromSeconds(10));
|
|
|
|
private static ScriptArtifactsChanged ApiMethodChanged(params string[] names) =>
|
|
new(ScriptArtifactKinds.ApiMethod, names, "BundleImport", DateTimeOffset.UtcNow);
|
|
|
|
/// <summary>Verifies that an ApiMethod publish invalidates the cached handler without waiting for the revision-check self-heal.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ApiMethodPublish_InvalidatesCachedHandler_WithoutRevisionCheckSelfHeal()
|
|
{
|
|
var subscriber = CreateSubscriber(_bus);
|
|
await subscriber.StartAsync(CancellationToken.None);
|
|
|
|
// Pin a SCRIPT-LESS handler: the RegisterHandler seam is exempt from the
|
|
// per-request revision check, so the ONLY mechanism that can stop it
|
|
// serving is an explicit InvalidateMethod — exactly what the bus must trigger.
|
|
_executor.RegisterHandler("m", async _ => { await Task.CompletedTask; return 99; });
|
|
var row = new ApiMethod("m", "return 1;") { Id = 1, TimeoutSeconds = 10 };
|
|
|
|
var before = await Run(row);
|
|
Assert.Contains("99", before.ResultJson); // pinned handler serves; no self-heal fires
|
|
|
|
_bus.Publish(ApiMethodChanged("m"));
|
|
|
|
var after = await Run(row);
|
|
Assert.True(after.Success);
|
|
Assert.Contains("1", after.ResultJson); // recompiled from the fresh row — invalidation, not self-heal
|
|
}
|
|
|
|
/// <summary>Verifies that an ApiMethod publish purges the known-bad record.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ApiMethodPublish_PurgesKnownBadRecord()
|
|
{
|
|
var subscriber = CreateSubscriber(_bus);
|
|
await subscriber.StartAsync(CancellationToken.None);
|
|
|
|
var broken = new ApiMethod("bad", "%%% not C# %%%") { Id = 2, TimeoutSeconds = 10 };
|
|
Assert.False(_executor.CompileAndRegister(broken));
|
|
Assert.Equal(1, _executor.KnownBadMethodCount);
|
|
|
|
_bus.Publish(ApiMethodChanged("bad"));
|
|
|
|
Assert.Equal(0, _executor.KnownBadMethodCount);
|
|
}
|
|
|
|
/// <summary>Verifies that non-ApiMethod artifact kinds are ignored.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task NonApiMethodKinds_AreIgnored()
|
|
{
|
|
var subscriber = CreateSubscriber(_bus);
|
|
await subscriber.StartAsync(CancellationToken.None);
|
|
|
|
_executor.RegisterHandler("m", async _ => { await Task.CompletedTask; return 99; });
|
|
var row = new ApiMethod("m", "return 1;") { Id = 3, TimeoutSeconds = 10 };
|
|
|
|
_bus.Publish(new ScriptArtifactsChanged(
|
|
ScriptArtifactKinds.SharedScript, new[] { "m" }, "BundleImport", DateTimeOffset.UtcNow));
|
|
_bus.Publish(new ScriptArtifactsChanged(
|
|
ScriptArtifactKinds.Template, new[] { "m" }, "BundleImport", DateTimeOffset.UtcNow));
|
|
|
|
var result = await Run(row);
|
|
Assert.Contains("99", result.ResultJson); // pinned handler untouched
|
|
}
|
|
|
|
/// <summary>Verifies that stopping the subscriber unsubscribes it from the bus.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task StopAsync_Unsubscribes()
|
|
{
|
|
var subscriber = CreateSubscriber(_bus);
|
|
await subscriber.StartAsync(CancellationToken.None);
|
|
Assert.Equal(1, _bus.SubscriberCount);
|
|
|
|
await subscriber.StopAsync(CancellationToken.None);
|
|
|
|
Assert.Equal(0, _bus.SubscriberCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// An ApiMethod change notification must also drop the resolved-row cache
|
|
/// entry (not just the compiled handler) — proving EndpointExtensions' next lookup
|
|
/// re-fetches instead of serving a stale cached row for the rest of the TTL window.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ApiMethodPublish_InvalidatesResolvedMethodCache()
|
|
{
|
|
var subscriber = CreateSubscriber(_bus);
|
|
await subscriber.StartAsync(CancellationToken.None);
|
|
|
|
var fetchCount = 0;
|
|
Task<ApiMethod?> Fetch(CancellationToken _)
|
|
{
|
|
fetchCount++;
|
|
return Task.FromResult<ApiMethod?>(new ApiMethod("m", "return 1;") { Id = 1 });
|
|
}
|
|
|
|
await _methodCache.GetOrFetchAsync("m", Fetch, CancellationToken.None);
|
|
await _methodCache.GetOrFetchAsync("m", Fetch, CancellationToken.None);
|
|
Assert.Equal(1, fetchCount); // second call served from cache
|
|
|
|
_bus.Publish(ApiMethodChanged("m"));
|
|
|
|
await _methodCache.GetOrFetchAsync("m", Fetch, CancellationToken.None);
|
|
Assert.Equal(2, fetchCount); // cache entry was dropped — re-fetched
|
|
}
|
|
|
|
/// <summary>Verifies that a null bus is tolerated and the subscriber does nothing.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task NullBus_NoOps()
|
|
{
|
|
// Site roles / plain test hosts have no bus registered — the hosted
|
|
// service must start and stop cleanly (the revision check alone applies).
|
|
var subscriber = CreateSubscriber(bus: null);
|
|
await subscriber.StartAsync(CancellationToken.None);
|
|
await subscriber.StopAsync(CancellationToken.None);
|
|
}
|
|
}
|