using Microsoft.Extensions.Logging.Abstractions; using ScadaLink.SiteRuntime.Scripts; namespace ScadaLink.SiteRuntime.Tests.Scripts; /// /// 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. /// public class TrustModelSemanticTests { private readonly ScriptCompilationService _service = new(NullLogger.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); } }