//! 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 btoa(s), (s) => s.replace(/[aeiou]/gi, ''), (s) => { let r=''; for(let i=0;i 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"); } }