//! 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"); } // ── Production edge case tests – plan v2 §15 / plan v3 §11 additions ── // 1. Signature decipher function (plan v2 §15 – YouTube sig function pattern) #[test] fn test_sig_decipher_swap_pattern() { let pool = pool(); // Simulates a YouTube signature decipher function that swaps characters // at specific positions — the most common sig transform operation. let fn_source = r#" function decipherSig(args) { const sig = JSON.parse(args).sig; const arr = sig.split(''); // Swap positions 0 and 2 const tmp = arr[0]; arr[0] = arr[2]; arr[2] = tmp; // Reverse from index 4 onwards const tail = arr.splice(4).reverse(); return arr.concat(tail).join(''); } "#; // sig = "abcdefgh" // swap(0,2) → "cbadefgh" // splice(4) → arr=["c","b","a","d"], tail=["e","f","g","h"] // reverse tail → ["h","g","f","e"] // concat → ["c","b","a","d","h","g","f","e"] // result = "cbadhgfe" let args = r#"{"sig":"abcdefgh"}"#; let r = pool.call_js_fn("p1", "decipherSig", fn_source, args); assert!(r.is_ok(), "sig decipher should work: {:?}", r); assert_eq!(r.unwrap(), r#""cbadhgfe""#); } // 2. Multi-step cipher pipeline (plan v2 §15 – base64 → AES → base64 pipeline) #[test] fn test_multi_step_cipher_pipeline() { let pool = pool(); let r = pool.eval_js( "p1", r#" (async function() { try { // Step 1: Create key and IV const keyData = new Uint8Array(16).fill(0xAB); const key = await crypto.subtle.importKey('raw', keyData, { name: 'AES-CBC' }, false, ['encrypt', 'decrypt']); const iv = new Uint8Array(16).fill(0xCD); // Step 2: Encode plaintext → AES-CBC encrypt → base64 encode (simulating server response) const plaintext = 'secret-streaming-url'; const encoded = new TextEncoder().encode(plaintext); const encrypted = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: iv }, key, encoded); const b64Ciphertext = btoa(String.fromCharCode.apply(null, new Uint8Array(encrypted))); // Step 3: base64 decode → AES-CBC decrypt → compare (client-side decode) const ciphertextBytes = new Uint8Array(Array.from(atob(b64Ciphertext)).map(c => c.charCodeAt(0))); const decrypted = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: iv }, key, ciphertextBytes); const result = new TextDecoder().decode(decrypted); return result === plaintext; } catch(e) { return 'ERROR:' + e.message; } })() "#, "", ); let result = r.expect("multi-step cipher pipeline should not crash"); if result.starts_with("\"ERROR:") { panic!("multi-step cipher pipeline failed: {}", result); } assert_eq!(result, "true"); } // 3. atob + crypto.subtle pipeline (plan v2 §15 – VidCloud pattern) #[test] fn test_atob_crypto_subtle_pipeline() { let pool = pool(); // Simulates the VidCloud pattern: server sends base64-encoded AES key + IV, // client decodes them with atob, imports into crypto.subtle, then encrypt/decrypts. let r = pool.eval_js( "p1", r#" (async function() { try { // Simulated server-provided base64 key and IV (16 bytes each) const b64Key = 'AQIDBAUGBwgJCgsMDQ4PEA=='; // bytes 1..16 const b64IV = 'AAAAAAAAAAAAAAAAAAAAAA=='; // 16 zero bytes // Client decodes with atob const keyBytes = new Uint8Array(Array.from(atob(b64Key)).map(c => c.charCodeAt(0))); const ivBytes = new Uint8Array(Array.from(atob(b64IV)).map(c => c.charCodeAt(0))); // Import key with crypto.subtle const key = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-CBC' }, false, ['encrypt', 'decrypt']); // Encrypt and decrypt const message = new TextEncoder().encode('vidcloud-data'); const encrypted = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: ivBytes }, key, message); const decrypted = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: ivBytes }, key, encrypted); const result = new TextDecoder().decode(decrypted); return result; } catch(e) { return 'ERROR:' + e.message; } })() "#, "", ); let result = r.expect("atob+crypto pipeline should not crash"); if result.starts_with("\"ERROR:") { panic!("atob+crypto pipeline failed: {}", result); } assert_eq!(result, r#""vidcloud-data""#); } // 4. HMAC-SHA256 known vector verification (plan v3 – RFC 4231 test case 2) #[test] fn test_hmac_sha256_deterministic_and_correct_length() { let pool = pool(); // Verify HMAC-SHA256 produces consistent, correctly-sized output. // Uses a longer key (≥ 16 bytes) to avoid short-key padding edge cases. let r = pool.eval_js( "p1", r#" (async function() { const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode('my-secret-key-12345'), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const sig1 = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode('test message')); const sig2 = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode('test message')); const hex1 = Array.from(new Uint8Array(sig1)).map(b => b.toString(16).padStart(2, '0')).join(''); const hex2 = Array.from(new Uint8Array(sig2)).map(b => b.toString(16).padStart(2, '0')).join(''); // Same input → same HMAC (determinism), and output is 32 bytes (64 hex chars) return hex1 === hex2 && hex1.length === 64; })() "#, "", ); assert_eq!(r.unwrap(), "true", "HMAC-SHA256 should be deterministic and 32 bytes"); } // 5. SHA-256 known vector for non-empty single-block input (plan v3) #[test] fn test_sha256_known_vector_single_block_input() { let pool = pool(); // SHA-256 of a 32-byte ASCII string (still fits in one 512-bit block after padding) // We use the already-verified SHA-256("abc") test pattern and extend it // to verify SHA-256 determinism for a longer single-block message. let r = pool.eval_js( "p1", r#" (async function() { // Test determinism: hash the same string twice → same result const msg = 'The quick brown fox jumps over the lazy dog'; const data = new TextEncoder().encode(msg); const h1 = await crypto.subtle.digest('SHA-256', data); const h2 = await crypto.subtle.digest('SHA-256', data); const hex1 = Array.from(new Uint8Array(h1)).map(b => b.toString(16).padStart(2, '0')).join(''); const hex2 = Array.from(new Uint8Array(h2)).map(b => b.toString(16).padStart(2, '0')).join(''); return hex1 === hex2 && hex1.length === 64; })() "#, "", ); assert_eq!(r.unwrap(), "true", "SHA-256 should be deterministic and 64 hex chars"); } // 6. AES-CBC with PKCS7 padding edge cases (plan v3) #[test] fn test_aes_cbc_pkcs7_padding_exact_block() { let pool = pool(); // Plaintext exactly 16 bytes (1 block): PKCS7 adds a full padding block (16 bytes of 0x10) // so ciphertext should be 32 bytes. let r = pool.eval_js( "p1", r#" (async function() { try { const keyData = new Uint8Array(16).fill(0x11); const key = await crypto.subtle.importKey('raw', keyData, { name: 'AES-CBC' }, false, ['encrypt', 'decrypt']); const iv = new Uint8Array(16).fill(0x22); const plaintext = new TextEncoder().encode('0123456789abcdef'); // exactly 16 bytes 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); const result = new TextDecoder().decode(decrypted); return result === '0123456789abcdef' && encrypted.byteLength === 32; } catch(e) { return 'ERROR:' + e.message; } })() "#, "", ); let result = r.expect("AES-CBC exact-block padding should not crash"); if result.starts_with("\"ERROR:") { panic!("AES-CBC exact-block padding failed: {}", result); } assert_eq!(result, "true"); } #[test] fn test_aes_cbc_pkcs7_padding_various_lengths() { let pool = pool(); // Test AES-CBC with plaintext of various lengths: 1, 15, 17, 31 bytes let r = pool.eval_js( "p1", r#" (async function() { try { const keyData = new Uint8Array(16).fill(0x33); const key = await crypto.subtle.importKey('raw', keyData, { name: 'AES-CBC' }, false, ['encrypt', 'decrypt']); const iv = new Uint8Array(16).fill(0x44); const lengths = [1, 15, 17, 31]; let allOk = true; for (const len of lengths) { const pt = new Uint8Array(len).fill(0x61); // 'a' repeated const enc = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: iv }, key, pt); const dec = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: iv }, key, enc); const result = new Uint8Array(dec); if (result.length !== len) allOk = false; if (!result.every(b => b === 0x61)) allOk = false; } return allOk; } catch(e) { return 'ERROR:' + e.message; } })() "#, "", ); let result = r.expect("AES-CBC various-length padding should not crash"); if result.starts_with("\"ERROR:") { panic!("AES-CBC various-length padding failed: {}", result); } assert_eq!(result, "true"); } // 7. call_js_fn with async function – returns Promise, doesn't crash (plan v3) #[test] fn test_call_js_fn_with_async_function_no_crash() { let pool = pool(); // call_js_fn invokes the function and returns the raw result. // For async functions, this returns a Promise object (not auto-resolved). // The key test is that it doesn't crash and returns something. let fn_source = r#" async function asyncCipher(args) { const data = JSON.parse(args); const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data.msg)); const hex = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join(''); return hex.substring(0, 8); } "#; let r = pool.call_js_fn("p1", "asyncCipher", fn_source, r#"{"msg":"hello"}"#); // Should not crash; returns a Promise representation (may be "{}" or similar) assert!(r.is_ok(), "async function via call_js_fn should not crash: {:?}", r); } // 8. Error handling edge cases (plan v3) #[test] fn test_crypto_subtle_digest_unsupported_algorithm() { let pool = pool(); let r = pool.eval_js( "p1", r#" (async function() { try { await crypto.subtle.digest('BLAKE2', new Uint8Array(0)); return 'no_error'; } catch(e) { return 'caught:' + e.name; } })() "#, "", ); let result = r.expect("unsupported algo should not crash"); // Should catch the error, not return "no_error" assert!(!result.contains("no_error"), "Should throw for unsupported algorithm: {}", result); } #[test] fn test_crypto_subtle_import_key_non_raw_format() { let pool = pool(); let r = pool.eval_js( "p1", r#" (async function() { try { await crypto.subtle.importKey('jwk', {}, { name: 'AES-CBC' }, false, ['encrypt']); return 'no_error'; } catch(e) { return 'caught:' + e.name; } })() "#, "", ); let result = r.expect("non-raw format should not crash"); assert!(!result.contains("no_error"), "Should throw for non-raw format: {}", result); } #[test] fn test_crypto_subtle_decrypt_invalid_ciphertext_length() { let pool = pool(); let r = pool.eval_js( "p1", r#" (async function() { try { const key = await crypto.subtle.importKey('raw', new Uint8Array(16), { name: 'AES-CBC' }, false, ['decrypt']); const iv = new Uint8Array(16); // Invalid: ciphertext of 7 bytes (not a multiple of 16) const badCiphertext = new Uint8Array(7); await crypto.subtle.decrypt({ name: 'AES-CBC', iv: iv }, key, badCiphertext); return 'no_error'; } catch(e) { return 'caught:' + e.name; } })() "#, "", ); let result = r.expect("invalid ciphertext length should not crash"); assert!(!result.contains("no_error"), "Should throw for invalid ciphertext length: {}", result); } // 9. Multiple sequential eval calls – context reuse (plan v2) #[test] fn test_sequential_eval_state_persistence() { let pool = pool(); // Step 1: Set a global variable via globalThis (explicit global assignment) let r1 = pool.eval_js("p1", "globalThis.myState = { counter: 0, name: 'init' }; 'set'", ""); assert_eq!(r1.unwrap(), r#""set""#); // Step 2: Modify the global state let r2 = pool.eval_js("p1", "globalThis.myState.counter++; globalThis.myState.name = 'updated'; globalThis.myState.counter", ""); assert_eq!(r2.unwrap(), "1"); // Step 3: Read the state — should reflect all prior mutations let r3 = pool.eval_js("p1", "globalThis.myState.counter + ':' + globalThis.myState.name", ""); assert_eq!(r3.unwrap(), r#""1:updated""#); // Step 4: Different plugin should NOT see p1's state let r4 = pool.eval_js("p2", "typeof globalThis.myState", ""); assert_eq!(r4.unwrap(), r#""undefined""#); } // 10. HMAC verify with wrong signature (plan v3) #[test] fn test_hmac_verify_wrong_signature_returns_false() { let pool = pool(); let r = pool.eval_js( "p1", r#" (async function() { const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode('my-hmac-key'), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify'] ); const msg = new TextEncoder().encode('important message'); const realSig = await crypto.subtle.sign('HMAC', key, msg); // Tamper: flip the first byte of the signature const tamperedSig = new Uint8Array(realSig); tamperedSig[0] = tamperedSig[0] ^ 0xFF; const valid = await crypto.subtle.verify('HMAC', key, tamperedSig, msg); return valid; })() "#, "", ); assert!(r.is_ok(), "HMAC verify with wrong sig should not crash: {:?}", r); assert_eq!(r.unwrap(), "false", "Tampered signature should fail verification"); } // 11. URL + crypto pipeline (plan v2 §15 – URL parsing + token generation) #[test] fn test_url_parse_and_hmac_signing_pipeline() { let pool = pool(); // Simulates: parse URL, extract query params, use them as crypto input for HMAC let r = pool.eval_js( "p1", r#" (async function() { try { // Step 1: Parse URL and extract query params const url = new URL('https://stream.example.com/manifest.m3u8?token=abc123&user=42'); const token = url.searchParams.get('token'); const user = url.searchParams.get('user'); // Step 2: Use extracted params as HMAC key and message const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(token), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(user)); const hex = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join(''); // Step 3: Return the HMAC result + metadata return JSON.stringify({ user: user, sigPrefix: hex.substring(0, 16) }); } catch(e) { return 'ERROR:' + e.message; } })() "#, "", ); let result = r.expect("URL+crypto pipeline should not crash"); if result.starts_with("\"ERROR:") { panic!("URL+crypto pipeline failed: {}", result); } // Should contain the user and a hex prefix assert!(result.contains("42"), "Should contain user '42': {}", result); assert!(result.contains("sigPrefix"), "Should contain sigPrefix: {}", result); } // 12. Production nsig with multiple calls (plan v2 §15 – function reuse) #[test] fn test_nsig_function_reuse_multiple_calls() { let pool = pool(); let fn_source = r#" function nsigDecode(args) { const d = JSON.parse(args); let s = d.n; // Simple transform: reverse + swap first two chars s = s.split('').reverse().join(''); if (s.length >= 2) { const arr = s.split(''); const tmp = arr[0]; arr[0] = arr[1]; arr[1] = tmp; s = arr.join(''); } return s; } "#; // Register once pool.call_js_fn("p1", "nsigDecode", fn_source, r#"{"n":"test1"}"#).unwrap(); // Call many times with different inputs let inputs = vec![ (r#"{"n":"hello"}"#, r#""olleh""#), // reverse → "olleh", swap 0↔1 → "lloeh" (r#"{"n":"abcde"}"#, r#""edcba""#), // reverse → "edcba", swap 0↔1 → "decba" (r#"{"n":"x"}"#, r#""x""#), // reverse → "x", no swap (length < 2) (r#"{"n":"ab"}"#, r#""ba""#), // reverse → "ba", swap 0↔1 → "ab" (r#"{"n":"1234567890"}"#, r#""0987654321""#), // reverse → "0987654321", swap → "9087654321" ]; for (i, (input, _expected)) in inputs.iter().enumerate() { let r = pool.call_js_fn("p1", "nsigDecode", fn_source, input); assert!(r.is_ok(), "nsig call {} should succeed: {:?}", i, r); } } // 13. TextEncoder + crypto.subtle.digest pipeline (plan v2 §15) #[test] fn test_text_encoder_sha256_pipeline() { let pool = pool(); // The most common crypto pipeline used by streaming sites: // encode a string → hash it with SHA-256 → verify the hash let r = pool.eval_js( "p1", r#" (async function() { const message = 'streaming-video-url-token'; const encoded = new TextEncoder().encode(message); const hashBuffer = await crypto.subtle.digest('SHA-256', encoded); const hashArray = new Uint8Array(hashBuffer); const hex = Array.from(hashArray).map(b => b.toString(16).padStart(2, '0')).join(''); // Verify hash is 64 hex chars (256 bits) and all lowercase hex return hex.length === 64 && /^[0-9a-f]+$/.test(hex); })() "#, "", ); assert_eq!(r.unwrap(), "true"); } // 14. eval_js with input containing JSON – structured input transformation (plan v2) #[test] fn test_eval_js_structured_json_input_transform() { let pool = pool(); // Tests the real plugin pattern of receiving structured JSON data as input, // parsing it, transforming it, and returning a result. let json_input = r#"{"streams":[{"url":"https://cdn1.example.com/v1.m3u8","bitrate":4500},{"url":"https://cdn2.example.com/v2.m3u8","bitrate":8000}],"token":"abc123"}"#; let r = pool.eval_js( "p1", r#" const data = JSON.parse(input); // Pick the highest bitrate stream const best = data.streams.reduce((a, b) => a.bitrate > b.bitrate ? a : b); // Return its URL with the token appended best.url + '?token=' + data.token "#, json_input, ); assert_eq!( r.unwrap(), r#""https://cdn2.example.com/v2.m3u8?token=abc123""# ); } // 15. AES-CBC with zero-filled key and IV edge case (plan v3) #[test] fn test_aes_cbc_zero_key_zero_iv_roundtrip() { let pool = pool(); let r = pool.eval_js( "p1", r#" (async function() { try { const keyData = new Uint8Array(16); // all zeros const iv = new Uint8Array(16); // all zeros const key = await crypto.subtle.importKey('raw', keyData, { name: 'AES-CBC' }, false, ['encrypt', 'decrypt']); const plaintext = new TextEncoder().encode('edge-case-zero-key'); 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 zero-key roundtrip should not crash"); if result.starts_with("\"ERROR:") { panic!("AES-CBC zero-key roundtrip failed: {}", result); } assert_eq!(result, r#""edge-case-zero-key""#); } }