fix(central-ui): resolve CentralUI-007..014 — nav authz, UTC date filters, disposal guards, N+1 fix, async script analysis

This commit is contained in:
Joseph Doherty
2026-05-16 20:58:03 -04:00
parent 738e67acc5
commit 71b90ba499
21 changed files with 976 additions and 81 deletions

View File

@@ -0,0 +1,91 @@
using Microsoft.Extensions.Caching.Memory;
using NSubstitute;
using ScadaLink.CentralUI.ScriptAnalysis;
using ScadaLink.TemplateEngine;
namespace ScadaLink.CentralUI.Tests.ScriptAnalysis;
/// <summary>
/// Regression tests for CentralUI-013. <c>ResolveCalledShape</c> resolved shared
/// script shapes with <c>_sharedScripts.GetShapesAsync().GetAwaiter().GetResult()</c>
/// — a sync-over-async block on a request thread that risks thread-pool
/// starvation and deadlock. <c>Hover</c> and <c>SignatureHelp</c> were synchronous
/// purely to accommodate that block. The fix makes both methods async and
/// <c>await</c>s the catalog.
/// </summary>
public class ScriptAnalysisAsyncResolveTests
{
private readonly ISharedScriptCatalog _catalog = Substitute.For<ISharedScriptCatalog>();
private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 100 });
private readonly IServiceProvider _services = Substitute.For<IServiceProvider>();
private readonly ScriptAnalysisService _svc;
public ScriptAnalysisAsyncResolveTests()
{
_catalog.GetShapesAsync().Returns(Array.Empty<ScriptShape>());
_svc = new ScriptAnalysisService(_catalog, _cache, _services);
}
private static ScriptShape Shape(string name, params ParameterShape[] ps) => new(name, ps, null);
private static ParameterShape Param(string name, string type) => new(name, type, true);
[Fact]
public async Task HoverAsync_OnSharedCallName_AwaitsCatalog_AndResolvesShape()
{
// The catalog only completes after yielding — a truly asynchronous
// source. The fixed Hover awaits it instead of blocking.
_catalog.GetShapesAsync().Returns(async _ =>
{
await Task.Yield();
return (IReadOnlyList<ScriptShape>)new[]
{
Shape("Aggregate", Param("window", "Integer")),
};
});
var resp = await _svc.Hover(new HoverRequest(
CodeText: "var r = Scripts.CallShared(\"Aggregate\");",
Line: 1,
Column: 30));
Assert.NotNull(resp.Markdown);
Assert.Contains("shared script", resp.Markdown);
Assert.Contains("Aggregate", resp.Markdown);
}
[Fact]
public async Task SignatureHelpAsync_InsideSharedCall_AwaitsCatalog_AndResolvesParameters()
{
_catalog.GetShapesAsync().Returns(async _ =>
{
await Task.Yield();
return (IReadOnlyList<ScriptShape>)new[]
{
Shape("Aggregate", Param("window", "Integer"), Param("mode", "String")),
};
});
var resp = await _svc.SignatureHelp(new SignatureHelpRequest(
CodeText: "var r = Scripts.CallShared(\"Aggregate\", ",
Line: 1,
Column: 41));
Assert.NotNull(resp.Label);
Assert.Contains("Aggregate", resp.Label!);
Assert.Equal(2, resp.Parameters!.Count);
}
[Fact]
public void HoverAndSignatureHelp_AreAsync_NotSyncOverAsync()
{
// Structural guard: the methods must return Task so the catalog can be
// awaited rather than blocked with .GetAwaiter().GetResult().
var hover = typeof(ScriptAnalysisService).GetMethod(nameof(ScriptAnalysisService.Hover));
var sigHelp = typeof(ScriptAnalysisService).GetMethod(nameof(ScriptAnalysisService.SignatureHelp));
Assert.NotNull(hover);
Assert.NotNull(sigHelp);
Assert.Equal(typeof(Task<HoverResponse>), hover!.ReturnType);
Assert.Equal(typeof(Task<SignatureHelpResponse>), sigHelp!.ReturnType);
}
}