Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Validation/ScriptCompilerTests.cs
T

139 lines
5.5 KiB
C#

using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Validation;
/// <summary>
/// M3.2: the design-time deploy gate now delegates to the shared
/// <c>ZB.MOM.WW.ScadaBridge.ScriptAnalysis</c> analyzer — a real Roslyn semantic
/// forbidden-API verdict plus a real CSharpScript compile against the
/// runtime-mirroring <c>ScriptCompileSurface</c>. These tests assert the
/// authoritative behavior: bypasses the old substring scan missed are now caught,
/// and undefined symbols (which a structural scan could never see) fail compile.
/// </summary>
public class ScriptCompilerTests
{
private readonly ScriptCompiler _sut = new();
[Fact]
public void TryCompile_EmptyCode_ReturnsFailure()
{
var result = _sut.TryCompile("", "Test");
Assert.True(result.IsFailure);
Assert.Contains("empty", result.Error, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void TryCompile_WhitespaceOnly_ReturnsFailure()
{
var result = _sut.TryCompile(" \n\t ", "Test");
Assert.True(result.IsFailure);
Assert.Contains("empty", result.Error, StringComparison.OrdinalIgnoreCase);
}
// --- Forbidden-API gate (authoritative) ---
[Fact]
public void TryCompile_ForbiddenApi_ReturnsFailure()
{
var result = _sut.TryCompile("using System.IO; File.ReadAllText(\"x\");", "Test");
Assert.True(result.IsFailure);
Assert.Contains("forbidden", result.Error, StringComparison.OrdinalIgnoreCase);
}
[Theory]
// Namespace alias — the old substring scan never saw "System.IO." spelled out.
[InlineData("using IO = System.IO; IO.File.ReadAllText(\"x\");")]
// using static — the call site is a bare ReadAllText(...) with no namespace text.
[InlineData("using static System.IO.File; ReadAllText(\"x\");")]
// global::-qualified reference.
[InlineData("global::System.IO.File.ReadAllText(\"x\");")]
// Reflection gateway — never spells a forbidden namespace at all.
[InlineData("typeof(string).Assembly.GetType(\"System.IO.File\");")]
public void TryCompile_AdversarialForbiddenApiBypass_StillRejected(string code)
{
var result = _sut.TryCompile(code, "Test");
Assert.True(result.IsFailure);
Assert.Contains("forbidden", result.Error, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void TryCompile_ForbiddenTypeInAllowedNamespace_RejectedAsForbidden()
{
// System.Diagnostics is an ALLOWED namespace (Stopwatch/Debug ok), so the
// `using` directive can't be flagged; Process is a forbidden TYPE reached
// as a bare identifier. The validator's full-framework semantic resolution
// must catch it authoritatively as a forbidden API (not merely as an
// undefined-symbol compile error).
var result = _sut.TryCompile(
"using System.Diagnostics; var p = Process.Start(\"x\");", "Test");
Assert.True(result.IsFailure);
Assert.Contains("forbidden", result.Error, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void TryCompile_StopwatchInAllowedDiagnostics_ReturnsSuccess()
{
// The companion to the Process case: Stopwatch lives in the same allowed
// System.Diagnostics namespace and must NOT be flagged.
var result = _sut.TryCompile(
"using System.Diagnostics; var sw = Stopwatch.StartNew(); var e = sw.ElapsedMilliseconds;",
"Test");
Assert.True(result.IsSuccess, result.IsFailure ? result.Error : null);
}
// --- Real-compile gate (the win over the old structural-only scan) ---
[Fact]
public void TryCompile_UndefinedSymbol_ReturnsFailure()
{
// A brace-balanced, forbidden-API-free script that references an undefined
// symbol. The old substring + brace scan accepted this; the real compile
// rejects it.
var result = _sut.TryCompile("var x = NoSuchThing.Foo();", "Test");
Assert.True(result.IsFailure);
Assert.Contains("compile", result.Error, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void TryCompile_SyntaxError_ReturnsFailure()
{
// Genuinely malformed — must fail the real compile.
var result = _sut.TryCompile("if (true) { var x = 1;", "Test");
Assert.True(result.IsFailure);
}
// --- Valid real script against the script API surface ---
[Fact]
public void TryCompile_ValidScriptUsingApiSurface_ReturnsSuccess()
{
const string code = """
var t = Attributes["Temperature"];
Attributes["Setpoint"] = 42;
var r = await ExternalSystem.Call("erp", "sync");
await Notify.To("ops").Send("s", "m");
""";
var result = _sut.TryCompile(code, "Test");
Assert.True(result.IsSuccess, result.IsFailure ? result.Error : null);
}
// --- Trigger-expression gate via ValidationService.CheckExpressionSyntax ---
[Fact]
public void CheckExpressionSyntax_ValidExpression_ReturnsNull()
{
// A bare boolean expression binding against the trigger globals surface.
var error = ValidationService.CheckExpressionSyntax("Attributes[\"Temp\"] != null");
Assert.Null(error);
}
[Fact]
public void CheckExpressionSyntax_ForbiddenApi_ReturnsMessage()
{
var error = ValidationService.CheckExpressionSyntax("System.IO.File.Exists(\"x\")");
Assert.NotNull(error);
Assert.Contains("forbidden", error, StringComparison.OrdinalIgnoreCase);
}
}