namespace XFESystem.Browser.Tests; public sealed class JavaScriptRuntimeTests { [Fact] public void ModernJavaScriptUsesARealInterpreter() { var runtime = new JavaScriptRuntime(); var result = runtime.Evaluate("class Item { constructor(value) { this.value = value; } } const items = [1,2,3].map(x => new Item(x * 2)); items.reduce((sum, x) => sum + x.value, 0)"); Assert.True(result.Success, result.Error); Assert.Equal("12", result.Value); } [Fact] public void ClosuresJsonUnicodeAndTemplatesWork() { var result = new JavaScriptRuntime().Evaluate("const make = x => y => x + y; const value = JSON.parse('{\"Count\": 40}'); `中文 ${make(value.Count)(2)}`"); Assert.True(result.Success, result.Error); Assert.Equal("中文 42", result.Value); } [Fact] public void ConsoleCallbacksCaptureValuesAndFilterTerminalControlCharacters() { var result = new JavaScriptRuntime().Evaluate("console.log('Answer', 42); console.warn('Hello\\u001b[31m'); 'done'"); Assert.True(result.Success, result.Error); Assert.Equal("Answer 42\nHello[31m", result.ConsoleOutput); Assert.Equal("done", result.Value); Assert.DoesNotContain('\u001b', result.Text); } [Fact] public void ScriptStatePersistsOnlyWithinOneRuntime() { var first = new JavaScriptRuntime(); Assert.True(first.Evaluate("var count = 10").Success); Assert.Equal("11", first.Evaluate("++count").Value); Assert.Equal("undefined", new JavaScriptRuntime().Evaluate("typeof count").Value); } [Theory] [InlineData("typeof System", "undefined")] [InlineData("typeof importNamespace", "undefined")] [InlineData("typeof require", "undefined")] [InlineData("typeof fetch", "undefined")] [InlineData("typeof process", "undefined")] [InlineData("typeof console.GetType", "undefined")] public void ScriptsCannotReachClrFilesSocketsOrNativeProcess(string source, string expected) { var result = new JavaScriptRuntime().Evaluate(source); Assert.True(result.Success, result.Error); Assert.Equal(expected, result.Value); } [Theory] [InlineData("eval('40 + 2')")] [InlineData("new Function('return 42')()")] public void DynamicCodeGenerationIsExplicitlyDisabled(string source) { var result = new JavaScriptRuntime().Evaluate(source); Assert.False(result.Success); Assert.NotEmpty(result.Error); } [Fact] public void InfiniteLoopsStopAtTheStatementLimitAndTheEngineCanRunAgain() { var runtime = new JavaScriptRuntime(new BrowserLimits { MaximumStatements = 100 }); var result = runtime.Evaluate("while (true) { }"); Assert.False(result.Success); Assert.Contains("statement", result.Error, StringComparison.OrdinalIgnoreCase); Assert.Equal("42", runtime.Evaluate("6 * 7").Value); } [Fact] public void ExcessiveAllocationFailsAtTheMemoryLimit() { var runtime = new JavaScriptRuntime(new BrowserLimits { MaximumScriptMemoryBytes = 1_000_000, MaximumStatements = 1_000_000, ScriptTimeout = TimeSpan.FromSeconds(2) }); var result = runtime.Evaluate("const storage = []; while (true) storage.push('X'.repeat(1024));"); Assert.False(result.Success); Assert.Contains("allocated", result.Error, StringComparison.OrdinalIgnoreCase); } [Fact] public void RecursiveCallsAndOutputAreBounded() { var recursion = new JavaScriptRuntime(new BrowserLimits { MaximumRecursionDepth = 16 }) .Evaluate("function f() { return f(); } f()"); Assert.False(recursion.Success); var output = new JavaScriptRuntime(new BrowserLimits { MaximumOutputCharacters = 40 }) .Evaluate("console.log('X'.repeat(41))"); Assert.False(output.Success); Assert.True(output.ConsoleOutput.Length <= 40); } [Fact] public void ScriptsResultsAndInvalidLimitsAreRejected() { var runtime = new JavaScriptRuntime(new BrowserLimits { MaximumScriptCharacters = 10, MaximumOutputCharacters = 8 }); Assert.False(runtime.Evaluate("12345678901").Success); Assert.False(runtime.Evaluate("'123456789'").Success); Assert.Throws(() => new JavaScriptRuntime(new BrowserLimits { MaximumStatements = 0 })); } [Fact] public void ResultConversionDoesNotInvokeAnUnboundedUserToStringCallback() { var result = new JavaScriptRuntime().Evaluate("({toString() { while(true) {} }})"); Assert.True(result.Success, result.Error); Assert.Equal("[object Object]", result.Value); } [Fact] public void AtomicsCannotBlockTheHostThreadOutsideStatementTimeoutChecks() { var result = new JavaScriptRuntime().Evaluate("const shared = new Int32Array(new SharedArrayBuffer(4)); Atomics.wait(shared, 0, 0)"); Assert.False(result.Success); Assert.Contains("Atomics.wait cannot be used", result.Error, StringComparison.Ordinal); } [Fact] public void BudgetClassificationUsesExceptionKindsRatherThanUserErrorWords() { var runtime = new JavaScriptRuntime(new() { MaximumStatements = 100 }); Assert.True(runtime.Evaluate("while(true){}").BudgetExceeded); Assert.False(runtime.Evaluate("throw Error('statement memory timeout recursion')").BudgetExceeded); Assert.False(runtime.Evaluate("nonexistentFunction()").BudgetExceeded); Assert.True(new JavaScriptRuntime(new() { MaximumScriptCharacters = 10 }).Evaluate("12345678901").BudgetExceeded); } [Fact] public void PatchedProofChecksTheAssemblyActuallyLoadedByTheTestHost() { var expected = Environment.GetEnvironmentVariable("XFE_EXPECT_JINT_SHA"); if (string.IsNullOrEmpty(expected)) return; var path = typeof(Jint.Engine).Assembly.Location; var actual = Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(File.ReadAllBytes(path))); Assert.Equal(expected, actual); Assert.NotNull(typeof(Jint.Engine).Assembly.GetType("XFESystem.NativeJintCleanupPatchV2")); } }