fix(site-runtime): resolve SiteRuntime-004..011 — deploy-after-persist, remove reflection, deterministic IDs, non-blocking startup, dedicated script scheduler, config-change detection, semantic trust-model check
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
using ScadaLink.SiteRuntime;
|
||||
using ScadaLink.SiteRuntime.Scripts;
|
||||
|
||||
namespace ScadaLink.SiteRuntime.Tests.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// SiteRuntime-009: the dedicated script-execution scheduler must run script bodies on
|
||||
/// its own dedicated threads, not on the shared .NET thread pool, so blocking script
|
||||
/// I/O cannot starve the global pool.
|
||||
/// </summary>
|
||||
public class ScriptExecutionSchedulerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Scheduler_RunsWork_OffTheThreadPool()
|
||||
{
|
||||
using var scheduler = new ScriptExecutionScheduler(2);
|
||||
|
||||
bool wasThreadPoolThread = true;
|
||||
string? threadName = null;
|
||||
|
||||
await Task.Factory.StartNew(() =>
|
||||
{
|
||||
wasThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread;
|
||||
threadName = Thread.CurrentThread.Name;
|
||||
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, scheduler);
|
||||
|
||||
Assert.False(wasThreadPoolThread,
|
||||
"Script work must not run on a shared thread-pool thread.");
|
||||
Assert.StartsWith("script-execution-", threadName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scheduler_RespectsConfiguredThreadCount()
|
||||
{
|
||||
using var scheduler = new ScriptExecutionScheduler(4);
|
||||
Assert.Equal(4, scheduler.MaximumConcurrencyLevel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scheduler_Shared_ReturnsSameInstanceForOptions()
|
||||
{
|
||||
var options = new SiteRuntimeOptions { ScriptExecutionThreadCount = 3 };
|
||||
var a = ScriptExecutionScheduler.Shared(options);
|
||||
var b = ScriptExecutionScheduler.Shared(options);
|
||||
Assert.Same(a, b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ScadaLink.SiteRuntime.Scripts;
|
||||
|
||||
namespace ScadaLink.SiteRuntime.Tests.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// SiteRuntime-011: regression tests for the semantic-analysis trust-model validation.
|
||||
/// The previous implementation was a raw substring scan of the source text — it both
|
||||
/// missed forbidden APIs (no literal namespace string) and raised false positives on
|
||||
/// the namespace string appearing in comments, string literals or unrelated identifiers.
|
||||
/// </summary>
|
||||
public class TrustModelSemanticTests
|
||||
{
|
||||
private readonly ScriptCompilationService _service =
|
||||
new(NullLogger<ScriptCompilationService>.Instance);
|
||||
|
||||
// ── Bypass cases (under-inclusive substring scan would MISS these) ──
|
||||
|
||||
[Fact]
|
||||
public void TrustModel_GlobalQualifiedForbiddenType_IsDetected()
|
||||
{
|
||||
// `global::`-prefixed name — the literal "System.IO" substring is still present
|
||||
// here, but the resolved-symbol approach catches it regardless of spelling.
|
||||
var violations = _service.ValidateTrustModel(
|
||||
"global::System.IO.File.ReadAllText(\"/etc/passwd\")");
|
||||
Assert.NotEmpty(violations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrustModel_ForbiddenTypeViaUsingAlias_IsDetected()
|
||||
{
|
||||
// A using-alias hides the forbidden namespace from a substring scan entirely:
|
||||
// the script body never writes "System.IO". Semantic resolution still sees that
|
||||
// the alias resolves to System.IO.File.
|
||||
var code = """
|
||||
using F = System.IO.File;
|
||||
F.ReadAllText("/etc/passwd");
|
||||
""";
|
||||
var violations = _service.ValidateTrustModel(code);
|
||||
Assert.NotEmpty(violations);
|
||||
Assert.Contains(violations, v => v.Contains("System.IO"));
|
||||
}
|
||||
|
||||
// ── False-positive cases (over-inclusive substring scan would WRONGLY flag these) ──
|
||||
|
||||
[Fact]
|
||||
public void TrustModel_ForbiddenNamespaceInStringLiteral_IsNotFlagged()
|
||||
{
|
||||
// "System.IO" appears only inside a string literal — not an API reference.
|
||||
var violations = _service.ValidateTrustModel(
|
||||
"var label = \"System.IO is blocked\"; return label;");
|
||||
Assert.Empty(violations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrustModel_ForbiddenNamespaceInComment_IsNotFlagged()
|
||||
{
|
||||
var code = """
|
||||
// This script does not use System.IO or System.Reflection at all.
|
||||
var x = 1 + 2;
|
||||
return x;
|
||||
""";
|
||||
var violations = _service.ValidateTrustModel(code);
|
||||
Assert.Empty(violations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrustModel_UnrelatedIdentifierContainingForbiddenSubstring_IsNotFlagged()
|
||||
{
|
||||
// A local variable whose name merely contains "Threading" is harmless.
|
||||
var code = """
|
||||
var ProcessThreadingCount = 5;
|
||||
return ProcessThreadingCount + 1;
|
||||
""";
|
||||
var violations = _service.ValidateTrustModel(code);
|
||||
Assert.Empty(violations);
|
||||
}
|
||||
|
||||
// ── Allowed exceptions still resolve correctly ──
|
||||
|
||||
[Fact]
|
||||
public void TrustModel_TaskAndCancellationToken_RemainAllowed()
|
||||
{
|
||||
var code = """
|
||||
var cts = new System.Threading.CancellationTokenSource();
|
||||
await System.Threading.Tasks.Task.Delay(1, cts.Token);
|
||||
return 0;
|
||||
""";
|
||||
var violations = _service.ValidateTrustModel(code);
|
||||
Assert.Empty(violations);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user