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<ArgumentOutOfRangeException>(() => 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"));
}
}
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<ArgumentOutOfRangeException>(() => 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"));
}
}