using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation; namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Validation; /// /// M3.2: the design-time deploy gate now delegates to the shared /// ZB.MOM.WW.ScadaBridge.ScriptAnalysis analyzer — a real Roslyn semantic /// forbidden-API verdict plus a real CSharpScript compile against the /// runtime-mirroring ScriptCompileSurface. 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. /// 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); } // --- 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); } }