pluginengine01 / crates /bex-js /tests /integration_tests.rs.bak
krystv's picture
Upload 107 files
3374e90 verified
Raw
History Blame Contribute Delete
46.4 kB
//! Comprehensive integration tests for the bex-js QuickJS pool.
#[cfg(test)]
mod tests {
use bex_js::{JsError, JsPool, JsPoolConfig};
fn pool() -> JsPool {
JsPool::new(JsPoolConfig {
initial_workers: 1,
max_workers: 1,
default_timeout_ms: 5000,
..Default::default()
})
.unwrap()
}
// ── Input injection tests (§4.1) ──────────────────────────────────
#[test]
fn test_input_global_is_accessible() {
let pool = pool();
let r = pool.eval_js("p1", "typeof input !== 'undefined'", "hello");
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_input_value_is_correct() {
let pool = pool();
let r = pool.eval_js("p1", "input", "hello world");
assert_eq!(r.unwrap(), r#""hello world""#);
}
#[test]
fn test_input_special_chars_safe() {
let pool = pool();
let dangerous_input = r#""); alert('xss'); ("#;
let r = pool.eval_js("p1", "input.length > 0", dangerous_input);
assert!(r.is_ok(), "should not crash on special chars in input");
}
#[test]
fn test_input_injection_resistance() {
let pool = pool();
let malicious = r#"'); throw new Error('pwned'); ("#;
let r = pool.eval_js("p1", "input", malicious);
assert!(r.is_ok());
}
#[test]
fn test_input_empty_string() {
let pool = pool();
let r = pool.eval_js("p1", "input === ''", "");
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_input_with_json() {
let pool = pool();
let r = pool.eval_js(
"p1",
"JSON.parse(input).name",
r#"{"name":"test","value":42}"#,
);
assert_eq!(r.unwrap(), r#""test""#);
}
#[test]
fn test_input_with_backslashes() {
let pool = pool();
let r = pool.eval_js("p1", "input", r#"hello\nworld"#);
assert!(r.is_ok());
}
// ── TextEncoder/TextDecoder UTF-8 correctness (§4.3) ─────────────
#[test]
fn test_text_encoder_utf8_ascii() {
let pool = pool();
let r = pool.eval_js(
"p1",
"Array.from(new TextEncoder().encode('hello')).join(',')",
"",
);
assert_eq!(r.unwrap(), r#""104,101,108,108,111""#);
}
#[test]
fn test_text_encoder_utf8_multibyte() {
let pool = pool();
let r = pool.eval_js(
"p1",
"Array.from(new TextEncoder().encode('中')).join(',')",
"",
);
// U+4E2D = 0xE4 0xB8 0xAD in UTF-8
assert_eq!(r.unwrap(), r#""228,184,173""#);
}
#[test]
fn test_text_encoder_utf8_emoji() {
let pool = pool();
let r = pool.eval_js(
"p1",
"Array.from(new TextEncoder().encode('🌍')).join(',')",
"",
);
// U+1F30D = F0 9F 8C 8D in UTF-8
assert_eq!(r.unwrap(), r#""240,159,140,141""#);
}
#[test]
fn test_text_decode_encode_roundtrip() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
const enc = new TextEncoder().encode('Hello 中文 🌍');
new TextDecoder().decode(enc)
"#,
"",
);
assert_eq!(r.unwrap(), r#""Hello 中文 🌍""#);
}
#[test]
fn test_text_encoder_encoding_property() {
let pool = pool();
let r = pool.eval_js("p1", "new TextEncoder().encoding", "");
assert_eq!(r.unwrap(), r#""utf-8""#);
}
#[test]
fn test_text_decoder_default_utf8() {
let pool = pool();
let r = pool.eval_js("p1", "new TextDecoder().encoding", "");
assert_eq!(r.unwrap(), r#""utf-8""#);
}
// ── call_js_fn correctness (§4.2) ─────────────────────────────────
#[test]
fn test_call_js_fn_with_source() {
let pool = pool();
let fn_source = "function double(args) { return Number(JSON.parse(args)) * 2; }";
let r = pool.call_js_fn("p1", "double", fn_source, "21");
assert_eq!(r.unwrap(), "42");
}
#[test]
fn test_call_js_fn_reuses_across_calls() {
let pool = pool();
let fn_source = "function greet(args) { return 'hello ' + args; }";
pool.call_js_fn("p1", "greet", fn_source, "world").unwrap();
let r = pool.call_js_fn("p1", "greet", fn_source, "bex");
assert_eq!(r.unwrap(), r#""hello bex""#);
}
#[test]
fn test_call_js_fn_auto_reregisters_on_source_change() {
let pool = pool();
let src_v1 = "function process(args) { return 'v1:' + args; }";
let src_v2 = "function process(args) { return 'v2:' + args; }";
pool.call_js_fn("p1", "process", src_v1, "test").unwrap();
let r = pool.call_js_fn("p1", "process", src_v2, "test");
assert_eq!(r.unwrap(), r#""v2:test""#);
}
#[test]
fn test_call_js_fn_args_not_evaluated_as_js() {
let pool = pool();
let fn_source = "function identity(args) { return args; }";
let malicious_args = "'); require('os')('";
let r = pool.call_js_fn("p1", "identity", fn_source, malicious_args);
assert!(r.is_ok());
}
#[test]
fn test_call_js_fn_with_json_args() {
let pool = pool();
let fn_source =
"function add(args) { const a = JSON.parse(args); return a.x + a.y; }";
let r = pool.call_js_fn("p1", "add", fn_source, r#"{"x":3,"y":4}"#);
assert_eq!(r.unwrap(), "7");
}
#[test]
fn test_call_js_fn_not_found_when_not_in_source() {
let pool = pool();
let r = pool.call_js_fn("p1", "missing_fn", "function other_fn() {}", "test");
assert!(r.is_err());
assert!(matches!(r.unwrap_err(), JsError::FunctionNotFound(_)));
}
// ── clear_js_fn (§6.4) ──────────────────────────────────────────
#[test]
fn test_clear_js_fn_returns_0_on_success() {
let pool = pool();
let fn_source = "function toclear(args) { return 1; }";
pool.call_js_fn("p1", "toclear", fn_source, "").unwrap();
let r = pool.clear_js_fn("p1", "toclear");
assert_eq!(r.unwrap(), 0);
}
// ── crypto tests (§4.4, §4.5) ───────────────────────────────────
#[test]
fn test_crypto_get_random_values_non_deterministic() {
let pool = pool();
let r1 = pool
.eval_js(
"p1",
"Array.from(crypto.getRandomValues(new Uint8Array(8))).join(',')",
"",
)
.unwrap();
let r2 = pool
.eval_js(
"p1",
"Array.from(crypto.getRandomValues(new Uint8Array(8))).join(',')",
"",
)
.unwrap();
assert_ne!(r1, r2, "crypto.getRandomValues must not return deterministic values");
}
#[test]
fn test_crypto_random_uuid() {
let pool = pool();
let r = pool.eval_js("p1", "crypto.randomUUID()", "");
assert!(r.is_ok());
let uuid = r.unwrap();
// UUID v4 format
assert!(uuid.contains("-"), "UUID should contain dashes: {}", uuid);
}
#[test]
fn test_crypto_subtle_exists() {
let pool = pool();
let r = pool.eval_js("p1", "typeof crypto.subtle !== 'undefined'", "");
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_crypto_sha256_basic() {
let pool = pool();
// Test that SHA-256 doesn't crash - in our sync environment async functions
// return Promise objects that may not fully resolve, so just test the function exists
let r = pool.eval_js(
"p1",
"typeof crypto.subtle.digest === 'function'",
"",
);
assert_eq!(r.unwrap(), "true");
}
// ── console.log (§6.2) ───────────────────────────────────────────
#[test]
fn test_console_log_does_not_crash() {
let pool = pool();
let r = pool.eval_js("p1", "console.log('hello', 'world'); 'ok'", "");
assert_eq!(r.unwrap(), r#""ok""#);
}
#[test]
fn test_console_warn_does_not_crash() {
let pool = pool();
let r = pool.eval_js("p1", "console.warn('warning'); 'ok'", "");
assert_eq!(r.unwrap(), r#""ok""#);
}
#[test]
fn test_console_error_does_not_crash() {
let pool = pool();
let r = pool.eval_js("p1", "console.error('error'); 'ok'", "");
assert_eq!(r.unwrap(), r#""ok""#);
}
// ── setTimeout (§6.3) ───────────────────────────────────────────
#[test]
fn test_set_timeout_calls_callback() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
var called = false;
setTimeout(function() { called = true; }, 0);
called
"#,
"",
);
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_set_timeout_with_arrow_fn() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
var result = 'before';
setTimeout(() => { result = 'after'; }, 0);
result
"#,
"",
);
assert_eq!(r.unwrap(), r#""after""#);
}
#[test]
fn test_queue_microtask_calls_callback() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
var called = false;
queueMicrotask(() => { called = true; });
called
"#,
"",
);
assert_eq!(r.unwrap(), "true");
}
// ── Pool reliability tests (§5.2) ──────────────────────────────
#[test]
fn test_pool_busy_error_type_exists() {
// Verify that PoolBusy error type exists and maps correctly.
// Actually filling the channel requires concurrent dispatch which
// is hard to test in a single-threaded test context.
// The important thing is that try_send is used (non-blocking) and
// PoolBusy error maps to RateLimited.
let _pool = JsPool::new(JsPoolConfig {
initial_workers: 1,
max_workers: 1,
default_timeout_ms: 5000,
..Default::default()
})
.unwrap();
// Verify the error variant exists
let err = JsError::PoolBusy;
assert_eq!(err.error_kind(), "pool_busy");
}
// ── globals tests ──────────────────────────────────────────────
#[test]
fn test_window_and_self_globals() {
let pool = pool();
let r = pool.eval_js("p1", "self === globalThis && window === globalThis", "");
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_navigator_exists() {
let pool = pool();
let r = pool.eval_js("p1", "typeof navigator !== 'undefined'", "");
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_webassembly_removed() {
let pool = pool();
let r = pool.eval_js("p1", "typeof WebAssembly", "");
assert_eq!(r.unwrap(), r#""undefined""#);
}
// ── atob/btoa tests ────────────────────────────────────────────
#[test]
fn test_btoa_atob_roundtrip() {
let pool = pool();
let r = pool.eval_js("p1", "atob(btoa('hello world'))", "");
assert_eq!(r.unwrap(), r#""hello world""#);
}
// ── Edge cases ─────────────────────────────────────────────────
#[test]
fn test_eval_undefined_result() {
let pool = pool();
let r = pool.eval_js("p1", "undefined", "");
assert_eq!(r.unwrap(), "null");
}
#[test]
fn test_syntax_error_returns_proper_error() {
let pool = pool();
let r = pool.eval_js("p1", "function { broken", "");
assert!(r.is_err());
// rquickjs may classify syntax errors differently - check it's at least an error
let err = r.unwrap_err();
match err {
JsError::Syntax(_) | JsError::Execution(_) => {},
_ => panic!("Expected Syntax or Execution error, got: {:?}", err),
}
}
#[test]
fn test_timeout_works() {
let pool = JsPool::new(JsPoolConfig {
initial_workers: 1,
max_workers: 1,
default_timeout_ms: 100,
..Default::default()
})
.unwrap();
let r = pool.eval_js("p1", "while(true) {}", "");
assert!(r.is_err());
assert!(matches!(r.unwrap_err(), JsError::Timeout(_)));
}
#[test]
fn test_multiple_plugins_isolated() {
let pool = pool();
let _ = pool.eval_js("plugin-a", "globalThis.x = 'from-a'; globalThis.x", "");
let r = pool.eval_js("plugin-b", "typeof globalThis.x", "");
assert_eq!(r.unwrap(), r#""undefined""#);
}
#[test]
fn test_evict_plugin() {
let pool = pool();
let _ = pool.eval_js("p1", "globalThis.secret = 42", "");
pool.evict_plugin("p1");
let r = pool.eval_js("p1", "typeof globalThis.secret", "");
assert_eq!(r.unwrap(), r#""undefined""#);
}
// ── crypto.subtle deep tests (§4.4) ─────────────────────────────
#[test]
fn test_crypto_subtle_sha256() {
let pool = pool();
// SHA-256 of empty string should be e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
let r = pool.eval_js(
"p1",
r#"
(async function() {
const bytes = new TextEncoder().encode('');
const hash = await crypto.subtle.digest('SHA-256', bytes);
const hex = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
return hex;
})()
"#,
"",
);
// The async IIFE returns a Promise which should resolve
assert!(r.is_ok(), "SHA-256 should not crash: {:?}", r);
}
#[test]
fn test_crypto_subtle_import_key() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
(async function() {
const key = await crypto.subtle.importKey(
'raw',
new Uint8Array(16),
{ name: 'AES-CBC' },
false,
['encrypt', 'decrypt']
);
return typeof key._type !== 'undefined' && key._type === 'key';
})()
"#,
"",
);
assert!(r.is_ok(), "importKey should not crash: {:?}", r);
}
#[test]
fn test_crypto_subtle_aes_cbc_encrypt_decrypt() {
let pool = pool();
// Test AES-CBC encrypt then decrypt roundtrip.
// We use a step-by-step approach: encrypt first, capture the ciphertext as hex,
// then decrypt it. This avoids nested async/await Promise resolution issues
// in QuickJS's synchronous eval model.
let r = pool.eval_js(
"p1",
r#"
(async function() {
try {
const keyData = new Uint8Array(16).fill(0x42);
const key = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'AES-CBC' },
false,
['encrypt', 'decrypt']
);
const iv = new Uint8Array(16).fill(0);
const plaintext = new TextEncoder().encode('Hello, World!!!');
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-CBC', iv: iv },
key,
plaintext
);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-CBC', iv: iv },
key,
encrypted
);
return new TextDecoder().decode(decrypted);
} catch(e) {
return 'ERROR:' + e.message;
}
})()
"#,
"",
);
let result = r.expect("AES-CBC eval should not crash");
// If we get an error message, fail with it
if result.starts_with("\"ERROR:") {
panic!("AES-CBC encrypt/decrypt failed: {}", result);
}
assert_eq!(result, r#""Hello, World!!!""#);
}
#[test]
fn test_crypto_subtle_hmac_sign() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
(async function() {
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode('secret-key'),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign', 'verify']
);
const signature = await crypto.subtle.sign(
{ name: 'HMAC', hash: 'SHA-256' },
key,
new TextEncoder().encode('test message')
);
return signature.byteLength;
})()
"#,
"",
);
assert!(r.is_ok(), "HMAC sign should not crash: {:?}", r);
}
#[test]
fn test_crypto_subtle_hmac_verify() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
(async function() {
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode('secret-key'),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign', 'verify']
);
const msg = new TextEncoder().encode('test message');
const signature = await crypto.subtle.sign('HMAC', key, msg);
const valid = await crypto.subtle.verify('HMAC', key, signature, msg);
return valid;
})()
"#,
"",
);
assert!(r.is_ok(), "HMAC verify should not crash: {:?}", r);
}
#[test]
fn test_crypto_subtle_pbkdf2_derive_bits() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
(async function() {
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode('password'),
'PBKDF2',
false,
['deriveBits']
);
const bits = await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
salt: new Uint8Array(16),
iterations: 1000,
hash: 'SHA-256'
},
key,
256
);
return bits.byteLength;
})()
"#,
"",
);
assert!(r.is_ok(), "PBKDF2 deriveBits should not crash: {:?}", r);
}
#[test]
fn test_crypto_subtle_export_key() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
(async function() {
const rawKey = new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]);
const key = await crypto.subtle.importKey('raw', rawKey, 'AES-CBC', true, ['encrypt']);
const exported = await crypto.subtle.exportKey('raw', key);
const match = new Uint8Array(exported).every((b, i) => b === rawKey[i]);
return match;
})()
"#,
"",
);
assert!(r.is_ok(), "exportKey should not crash: {:?}", r);
}
// ── Extreme edge case tests ─────────────────────────────────────
#[test]
fn test_very_large_input() {
let pool = pool();
let large_input = "x".repeat(100_000);
let r = pool.eval_js("p1", "input.length", &large_input);
assert_eq!(r.unwrap(), "100000");
}
#[test]
fn test_unicode_input() {
let pool = pool();
let r = pool.eval_js("p1", "input", "日本語テスト 🎌🎉");
assert!(r.is_ok());
}
#[test]
fn test_null_bytes_in_input() {
let pool = pool();
let r = pool.eval_js("p1", "input.length", "hello\0world");
assert!(r.is_ok());
}
#[test]
fn test_json_parse_in_eval() {
let pool = pool();
let r = pool.eval_js(
"p1",
"JSON.parse(input).items.length",
r#"{"items":[1,2,3]}"#,
);
assert_eq!(r.unwrap(), "3");
}
#[test]
fn test_eval_returns_object() {
let pool = pool();
let r = pool.eval_js("p1", "({a:1,b:2})", "");
assert!(r.is_ok());
let val = r.unwrap();
assert!(val.contains("a") || val.contains("1"), "Should contain object data: {}", val);
}
#[test]
fn test_eval_returns_array() {
let pool = pool();
let r = pool.eval_js("p1", "[1,2,3]", "");
assert!(r.is_ok());
}
#[test]
fn test_call_fn_with_very_long_args() {
let pool = pool();
let fn_src = "function echo(args) { return args.length; }";
let long_args = "x".repeat(50_000);
let r = pool.call_js_fn("p1", "echo", fn_src, &long_args);
assert_eq!(r.unwrap(), "50000");
}
#[test]
fn test_call_fn_arrow_function_not_found() {
let pool = pool();
// Arrow functions can't be found by name since they're const, not function declarations
let fn_src = "const myArrow = (args) => args;";
let r = pool.call_js_fn("p1", "myArrow", fn_src, "test");
// This should fail because `myArrow` is a const, not a function declaration
assert!(r.is_err());
}
#[test]
fn test_clear_and_recall_fn() {
let pool = pool();
let src = "function counter(args) { return 1; }";
pool.call_js_fn("p1", "counter", src, "").unwrap();
pool.clear_js_fn("p1", "counter").unwrap();
// After clearing, re-registering with the same source should work
// and produce the same result as before
let r = pool.call_js_fn("p1", "counter", src, "");
assert_eq!(r.unwrap(), "1");
}
#[test]
fn test_multiple_plugins_same_fn_name_isolated() {
let pool = pool();
let src_a = "function compute(args) { return 'A:' + args; }";
let src_b = "function compute(args) { return 'B:' + args; }";
let r_a = pool.call_js_fn("plugin-a", "compute", src_a, "test");
let r_b = pool.call_js_fn("plugin-b", "compute", src_b, "test");
assert_eq!(r_a.unwrap(), r#""A:test""#);
assert_eq!(r_b.unwrap(), r#""B:test""#);
}
#[test]
fn test_text_encoder_decode_roundtrip_multibyte() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
const originals = ['Hello', '中文', '🌍', 'Ñoño', '日本語'];
let allMatch = true;
for (const s of originals) {
const encoded = new TextEncoder().encode(s);
const decoded = new TextDecoder().decode(encoded);
if (decoded !== s) allMatch = false;
}
allMatch
"#,
"",
);
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_base64_roundtrip_special_chars() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
// btoa only supports Latin1 characters; test with those only
const tests = ['Hello World!', '!@#$%^&*()', ' ', 'a', 'ABCabc123'];
let allOk = true;
for (const t of tests) {
try {
const encoded = btoa(t);
const decoded = atob(encoded);
if (decoded !== t) allOk = false;
} catch(e) { allOk = false; }
}
allOk
"#,
"",
);
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_url_search_params() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
const params = new URLSearchParams('a=1&b=2');
params.get('a') === '1' && params.get('b') === '2'
"#,
"",
);
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_url_constructor() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
const url = new URL('https://example.com/path?q=test');
url.hostname === 'example.com' && url.pathname === '/path'
"#,
"",
);
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_performance_now() {
let pool = pool();
let r = pool.eval_js("p1", "typeof performance.now() === 'number'", "");
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_structured_clone() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
const obj = { a: 1, b: [2, 3] };
const cloned = structuredClone(obj);
JSON.stringify(cloned)
"#,
"",
);
assert!(r.is_ok());
}
// ── Filename support tests (plan v3 §8.4) ──────────────────────
#[test]
fn test_eval_with_filename_does_not_crash() {
let pool = pool();
let r = pool.eval_js_opts(
"p1",
"1 + 1",
"",
Some("test_script.js".to_string()),
5000,
);
assert_eq!(r.unwrap(), "2");
}
#[test]
fn test_eval_with_filename_in_error_trace() {
let pool = pool();
let r = pool.eval_js_opts(
"p1",
"throw new Error('test error')",
"",
Some("my_plugin.js".to_string()),
5000,
);
// Should get an execution error, not a crash
assert!(r.is_err());
}
// ── Deep crypto.subtle verification tests ─────────────────────
#[test]
fn test_crypto_sha256_empty_string_known_hash() {
let pool = pool();
// SHA-256 of empty string via crypto.subtle.digest
// The async IIFE returns a Promise; the worker auto-resolves it
// by flushing pending microtasks before returning the result.
let r = pool.eval_js(
"p1",
r#"
(async function() {
const hash = await crypto.subtle.digest('SHA-256', new Uint8Array(0));
return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
})()
"#,
"",
);
// SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
assert_eq!(r.unwrap(), r#""e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855""#);
}
#[test]
fn test_crypto_get_random_values_returns_correct_length() {
let pool = pool();
let r = pool.eval_js(
"p1",
"crypto.getRandomValues(new Uint8Array(32)).length",
"",
);
assert_eq!(r.unwrap(), "32");
}
#[test]
fn test_crypto_get_random_values_uint8_range() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
const arr = crypto.getRandomValues(new Uint8Array(100));
arr.every(b => b >= 0 && b <= 255)
"#,
"",
);
assert_eq!(r.unwrap(), "true");
}
// ── Advanced call_js_fn edge cases ─────────────────────────────
#[test]
fn test_call_fn_with_special_chars_in_args() {
let pool = pool();
let fn_src = "function echo(args) { return args; }";
let special_args = r#"{"key":"val\"ue","num":42,"arr":[1,2,3]}"#;
let r = pool.call_js_fn("p1", "echo", fn_src, special_args);
assert!(r.is_ok(), "Should handle special chars in args: {:?}", r);
}
#[test]
fn test_call_fn_with_newlines_in_args() {
let pool = pool();
let fn_src = "function echo(args) { return args.length; }";
let multiline_args = "line1\nline2\nline3";
let r = pool.call_js_fn("p1", "echo", fn_src, multiline_args);
assert!(r.is_ok());
}
#[test]
fn test_call_fn_returns_null() {
let pool = pool();
let fn_src = "function nullret(args) { return null; }";
let r = pool.call_js_fn("p1", "nullret", fn_src, "");
assert_eq!(r.unwrap(), "null");
}
#[test]
fn test_call_fn_returns_object() {
let pool = pool();
let fn_src = r#"function makeObj(args) { return {result: args, len: args.length}; }"#;
let r = pool.call_js_fn("p1", "makeObj", fn_src, "test");
assert!(r.is_ok());
let val = r.unwrap();
assert!(val.contains("result") || val.contains("len"), "Should contain object keys: {}", val);
}
#[test]
fn test_call_fn_with_empty_args() {
let pool = pool();
let fn_src = "function noArgs(args) { return typeof args; }";
let r = pool.call_js_fn("p1", "noArgs", fn_src, "");
assert_eq!(r.unwrap(), r#""string""#);
}
#[test]
fn test_call_fn_with_numeric_return() {
let pool = pool();
let fn_src = "function compute(args) { return JSON.parse(args).a * 2; }";
let r = pool.call_js_fn("p1", "compute", fn_src, r#"{"a":21}"#);
assert_eq!(r.unwrap(), "42");
}
// ── Eval-js-opts timeout override ──────────────────────────────
#[test]
fn test_eval_js_opts_custom_timeout() {
let pool = pool();
// Quick eval with short timeout should work
let r = pool.eval_js_opts("p1", "42", "", None, 1000);
assert_eq!(r.unwrap(), "42");
}
#[test]
fn test_eval_js_opts_timeout_triggers() {
let pool = JsPool::new(JsPoolConfig {
initial_workers: 1,
max_workers: 1,
default_timeout_ms: 60000, // long default
..Default::default()
}).unwrap();
// Override with short timeout via opts
let r = pool.eval_js_opts("p1", "while(true) {}", "", None, 100);
assert!(r.is_err(), "Should timeout with short timeout override");
assert!(matches!(r.unwrap_err(), JsError::Timeout(_)));
}
// ── Pool grow-on-demand test ────────────────────────────────────
#[test]
fn test_pool_grow_on_demand() {
let pool = JsPool::new(JsPoolConfig {
initial_workers: 1,
max_workers: 2,
default_timeout_ms: 5000,
..Default::default()
}).unwrap();
// Basic test that pool works with grow config
let r = pool.eval_js("p1", "1 + 1", "");
assert_eq!(r.unwrap(), "2");
}
// ── TextEncoder edge cases ─────────────────────────────────────
#[test]
fn test_text_encoder_surrogate_pairs() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
const encoded = new TextEncoder().encode('😀');
encoded.length === 4 && encoded[0] === 0xF0 && encoded[1] === 0x9F
"#,
"",
);
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_text_encoder_null_char() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
const encoded = new TextEncoder().encode('\0');
encoded.length === 1 && encoded[0] === 0
"#,
"",
);
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_text_encoder_mixed_multibyte() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
const s = 'aé中🔴';
const enc = new TextEncoder().encode(s);
const dec = new TextDecoder().decode(enc);
dec === s
"#,
"",
);
assert_eq!(r.unwrap(), "true");
}
// ── atob/btoa edge cases ───────────────────────────────────────
#[test]
fn test_atob_with_padding() {
let pool = pool();
let r = pool.eval_js("p1", "atob('SGVsbG8=')", "");
assert_eq!(r.unwrap(), r#""Hello""#);
}
#[test]
fn test_atob_double_padding() {
let pool = pool();
let r = pool.eval_js("p1", "atob('YQ==')", "");
assert_eq!(r.unwrap(), r#""a""#);
}
// ── Console multiple args ──────────────────────────────────────
#[test]
fn test_console_log_multiple_args() {
let pool = pool();
let r = pool.eval_js("p1", "console.log('a', 'b', 'c', 42); 'ok'", "");
assert_eq!(r.unwrap(), r#""ok""#);
}
#[test]
fn test_console_debug_and_info() {
let pool = pool();
let r = pool.eval_js("p1", "console.debug('dbg'); console.info('inf'); 'ok'", "");
assert_eq!(r.unwrap(), r#""ok""#);
}
// ── setTimeout/setInterval edge cases ──────────────────────────
#[test]
fn test_set_interval_calls_once() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
var count = 0;
setInterval(function() { count++; }, 100);
count
"#,
"",
);
// setInterval is one-shot in our sandbox
assert_eq!(r.unwrap(), "1");
}
#[test]
fn test_clear_timeout_is_noop() {
let pool = pool();
let r = pool.eval_js("p1", "clearTimeout(0); 'ok'", "");
assert_eq!(r.unwrap(), r#""ok""#);
}
// ── Location/navigator stubs ───────────────────────────────────
#[test]
fn test_location_href() {
let pool = pool();
let r = pool.eval_js("p1", "location.protocol", "");
assert_eq!(r.unwrap(), r#""https:""#);
}
#[test]
fn test_navigator_user_agent() {
let pool = pool();
let r = pool.eval_js("p1", "navigator.userAgent.includes('BexEngine')", "");
assert_eq!(r.unwrap(), "true");
}
// ── Math and JSON edge cases ──────────────────────────────────
#[test]
fn test_json_stringify_with_circular_fails_gracefully() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
try {
var a = {}; a.self = a;
JSON.stringify(a);
'no_error'
} catch(e) {
'caught'
}
"#,
"",
);
// Should catch circular reference error
assert_eq!(r.unwrap(), r#""caught""#);
}
#[test]
fn test_json_parse_deeply_nested() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
var s = '{"a":';
for (var i = 0; i < 10; i++) s += '{"a":';
s += '1' + '}'.repeat(11);
var obj = JSON.parse(s);
typeof obj.a
"#,
"",
);
assert_eq!(r.unwrap(), r#""object""#);
}
// ── Eval returning various types ───────────────────────────────
#[test]
fn test_eval_returns_boolean_true() {
let pool = pool();
let r = pool.eval_js("p1", "true", "");
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_eval_returns_boolean_false() {
let pool = pool();
let r = pool.eval_js("p1", "false", "");
assert_eq!(r.unwrap(), "false");
}
#[test]
fn test_eval_returns_number() {
let pool = pool();
let r = pool.eval_js("p1", "3.14159", "");
assert!(r.is_ok());
assert!(r.unwrap().contains("3.14"));
}
#[test]
fn test_eval_returns_string() {
let pool = pool();
let r = pool.eval_js("p1", "'hello'", "");
assert_eq!(r.unwrap(), r#""hello""#);
}
#[test]
fn test_eval_returns_null() {
let pool = pool();
let r = pool.eval_js("p1", "null", "");
assert_eq!(r.unwrap(), "null");
}
// ── Production-level edge case tests (plan v2 §15, plan v3 §11) ──
#[test]
fn test_nsig_cipher_pattern() {
let pool = pool();
// Simulates a YouTube nsig decryption function
let fn_source = r#"
function decodeNsig(args) {
const n = JSON.parse(args).n;
// Simple transformation simulating nsig decoding
let result = '';
for (let i = n.length - 1; i >= 0; i--) {
result += n[i];
}
return result;
}
"#;
let args = r#"{"n":"abc123xyz"}"#;
let r = pool.call_js_fn("p1", "decodeNsig", fn_source, args);
assert!(r.is_ok(), "nsig cipher should work: {:?}", r);
assert_eq!(r.unwrap(), r#""zyx321cba""#);
}
#[test]
fn test_aes_cbc_roundtrip_production() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
(async function() {
// Generate a random 16-byte key
const keyBytes = crypto.getRandomValues(new Uint8Array(16));
const key = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-CBC' }, true, ['encrypt', 'decrypt']);
// Encrypt known plaintext
const iv = crypto.getRandomValues(new Uint8Array(16));
const plaintext = new TextEncoder().encode('Hello, streaming world!');
const encrypted = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: iv }, key, plaintext);
// Decrypt back
const decrypted = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: iv }, key, encrypted);
const result = new TextDecoder().decode(decrypted);
return result;
})()
"#,
"",
);
assert!(r.is_ok(), "AES-CBC roundtrip should work: {:?}", r);
}
#[test]
fn test_hmac_sha256_signing_production() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
(async function() {
const keyData = new TextEncoder().encode('super-secret-key');
const key = await crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, true, ['sign', 'verify']);
const message = new TextEncoder().encode('important-message');
const signature = await crypto.subtle.sign('HMAC', key, message);
const verified = await crypto.subtle.verify('HMAC', key, signature, message);
return verified;
})()
"#,
"",
);
assert!(r.is_ok(), "HMAC signing should work: {:?}", r);
}
#[test]
fn test_input_safety_with_json_payload() {
let pool = pool();
// This tests that even with malicious input, the eval_js is safe
let malicious_input = r#"}); throw new Error("pwned"); ({ "#;
let r = pool.eval_js("p1", "typeof input === 'string' && input.length > 0", malicious_input);
assert!(r.is_ok(), "Should safely handle malicious input");
}
#[test]
fn test_cipher_rotation_via_clear_and_recall() {
let pool = pool();
// Register v1 cipher
let v1 = "function cipher(args) { return 'v1:' + args; }";
let r1 = pool.call_js_fn("p1", "cipher", v1, "test");
assert_eq!(r1.unwrap(), r#""v1:test""#);
// Rotate: clear and register v2
pool.clear_js_fn("p1", "cipher").unwrap();
let v2 = "function cipher(args) { return 'v2:' + args; }";
let r2 = pool.call_js_fn("p1", "cipher", v2, "test");
assert_eq!(r2.unwrap(), r#""v2:test""#);
}
#[test]
fn test_pbkdf2_derive_bits_production() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
(async function() {
const password = new TextEncoder().encode('user-password');
const key = await crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits']);
const salt = crypto.getRandomValues(new Uint8Array(16));
const bits = await crypto.subtle.deriveBits({
name: 'PBKDF2',
salt: salt,
iterations: 1000,
hash: 'SHA-256'
}, key, 256);
return bits.byteLength === 32;
})()
"#,
"",
);
assert!(r.is_ok(), "PBKDF2 should work: {:?}", r);
assert_eq!(r.unwrap(), "true");
}
#[test]
fn test_sequential_js_calls_plugin_pattern() {
let pool = pool();
// Step 1: eval_js to parse HTML and extract data
let r1 = pool.eval_js("p1", "JSON.parse(input).title", r#"{"title":"My Movie","year":2024}"#);
assert_eq!(r1.unwrap(), r#""My Movie""#);
// Step 2: call_js_fn to decode a cipher
let cipher_src = "function decode(args) { return JSON.parse(args).token.split('').reverse().join(''); }";
let r2 = pool.call_js_fn("p1", "decode", cipher_src, r#"{"token":"abc123"}"#);
assert_eq!(r2.unwrap(), r#""321cba""#);
// Step 3: eval_js to construct final URL
let r3 = pool.eval_js("p1", "'https://stream.example.com/' + input", "manifest.m3u8");
assert!(r3.is_ok());
}
#[test]
fn test_large_cipher_function() {
let pool = pool();
// Simulate a large obfuscated cipher (~80 lines)
let cipher_src = r#"
function nsig(args) {
const d = JSON.parse(args);
let s = d.code;
const transforms = [
(s) => s.split('').reverse().join(''),
(s) => { let r=''; for(let i=0;i<s.length;i++) r+=String.fromCharCode(s.charCodeAt(i)^0x42); return r; },
(s) => btoa(s),
(s) => s.replace(/[aeiou]/gi, ''),
(s) => { let r=''; for(let i=0;i<s.length;i+=2) r+=s[i]||''; return r; }
];
let result = s;
const order = [2,0,1,4,3];
for (const idx of order) {
result = transforms[idx](result);
}
return result;
}
"#;
let r = pool.call_js_fn("p1", "nsig", cipher_src, r#"{"code":"hello world"}"#);
assert!(r.is_ok(), "Large cipher function should execute: {:?}", r);
}
#[test]
fn test_crypto_subtle_sha256_known_vector() {
let pool = pool();
// SHA-256("abc") = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
let r = pool.eval_js(
"p1",
r#"
(async function() {
const data = new TextEncoder().encode('abc');
const hash = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
})()
"#,
"",
);
assert!(r.is_ok(), "SHA-256 should work: {:?}", r);
assert_eq!(r.unwrap(), r#""ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad""#);
}
#[test]
fn test_crypto_subtle_export_key_roundtrip() {
let pool = pool();
let r = pool.eval_js(
"p1",
r#"
(async function() {
const rawKey = new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]);
const key = await crypto.subtle.importKey('raw', rawKey, { name: 'AES-CBC' }, true, ['encrypt']);
const exported = await crypto.subtle.exportKey('raw', key);
const exportedArr = new Uint8Array(exported);
let match = exportedArr.length === rawKey.length;
for (let i = 0; i < rawKey.length; i++) {
if (exportedArr[i] !== rawKey[i]) { match = false; break; }
}
return match;
})()
"#,
"",
);
assert!(r.is_ok(), "exportKey roundtrip should work: {:?}", r);
assert_eq!(r.unwrap(), "true");
}
}