| |
|
|
| #[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() |
| } |
|
|
| |
|
|
| #[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()); |
| } |
|
|
| |
|
|
| #[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(',')", |
| "", |
| ); |
| |
| 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(',')", |
| "", |
| ); |
| |
| 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""#); |
| } |
|
|
| |
|
|
| #[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(_))); |
| } |
|
|
| |
|
|
| #[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); |
| } |
|
|
| |
|
|
| #[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(); |
| |
| 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(); |
| |
| |
| let r = pool.eval_js( |
| "p1", |
| "typeof crypto.subtle.digest === 'function'", |
| "", |
| ); |
| assert_eq!(r.unwrap(), "true"); |
| } |
|
|
| |
|
|
| #[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""#); |
| } |
|
|
| |
|
|
| #[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"); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_pool_busy_error_type_exists() { |
| |
| |
| |
| |
| |
| let _pool = JsPool::new(JsPoolConfig { |
| initial_workers: 1, |
| max_workers: 1, |
| default_timeout_ms: 5000, |
| ..Default::default() |
| }) |
| .unwrap(); |
| |
| let err = JsError::PoolBusy; |
| assert_eq!(err.error_kind(), "pool_busy"); |
| } |
|
|
| |
|
|
| #[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""#); |
| } |
|
|
| |
|
|
| #[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""#); |
| } |
|
|
| |
|
|
| #[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()); |
| |
| 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""#); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_crypto_subtle_sha256() { |
| let pool = pool(); |
| |
| 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; |
| })() |
| "#, |
| "", |
| ); |
| |
| 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(); |
| |
| |
| |
| |
| 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 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); |
| } |
|
|
| |
|
|
| #[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(); |
| |
| let fn_src = "const myArrow = (args) => args;"; |
| let r = pool.call_js_fn("p1", "myArrow", fn_src, "test"); |
| |
| 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(); |
| |
| |
| 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()); |
| } |
|
|
| |
|
|
| #[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, |
| ); |
| |
| assert!(r.is_err()); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_crypto_sha256_empty_string_known_hash() { |
| let pool = pool(); |
| |
| |
| |
| 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(''); |
| })() |
| "#, |
| "", |
| ); |
| |
| 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"); |
| } |
|
|
| |
|
|
| #[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"); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_eval_js_opts_custom_timeout() { |
| let pool = pool(); |
| |
| 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, |
| ..Default::default() |
| }).unwrap(); |
| |
| 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(_))); |
| } |
|
|
| |
|
|
| #[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(); |
| |
| let r = pool.eval_js("p1", "1 + 1", ""); |
| assert_eq!(r.unwrap(), "2"); |
| } |
|
|
| |
|
|
| #[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"); |
| } |
|
|
| |
|
|
| #[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""#); |
| } |
|
|
| |
|
|
| #[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""#); |
| } |
|
|
| |
|
|
| #[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 |
| "#, |
| "", |
| ); |
| |
| 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""#); |
| } |
|
|
| |
|
|
| #[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"); |
| } |
|
|
| |
|
|
| #[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' |
| } |
| "#, |
| "", |
| ); |
| |
| 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""#); |
| } |
|
|
| |
|
|
| #[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"); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_nsig_cipher_pattern() { |
| let pool = pool(); |
| |
| 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(); |
| |
| 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(); |
| |
| 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""#); |
|
|
| |
| 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(); |
| |
| let r1 = pool.eval_js("p1", "JSON.parse(input).title", r#"{"title":"My Movie","year":2024}"#); |
| assert_eq!(r1.unwrap(), r#""My Movie""#); |
|
|
| |
| 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""#); |
|
|
| |
| 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(); |
| |
| 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(); |
| |
| 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"); |
| } |
|
|
| |
|
|
| |
| #[test] |
| fn test_sig_decipher_swap_pattern() { |
| let pool = pool(); |
| |
| |
| 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(''); |
| } |
| "#; |
| |
| |
| |
| |
| |
| |
| 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""#); |
| } |
|
|
| |
| #[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"); |
| } |
|
|
| |
| #[test] |
| fn test_atob_crypto_subtle_pipeline() { |
| let pool = pool(); |
| |
| |
| 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""#); |
| } |
|
|
| |
| #[test] |
| fn test_hmac_sha256_deterministic_and_correct_length() { |
| let pool = pool(); |
| |
| |
| 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"); |
| } |
|
|
| |
| #[test] |
| fn test_sha256_known_vector_single_block_input() { |
| let pool = pool(); |
| |
| |
| |
| 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"); |
| } |
|
|
| |
| #[test] |
| fn test_aes_cbc_pkcs7_padding_exact_block() { |
| let pool = pool(); |
| |
| |
| 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(); |
| |
| 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"); |
| } |
|
|
| |
| #[test] |
| fn test_call_js_fn_with_async_function_no_crash() { |
| let pool = pool(); |
| |
| |
| |
| 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"}"#); |
| |
| assert!(r.is_ok(), "async function via call_js_fn should not crash: {:?}", r); |
| } |
|
|
| |
| #[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"); |
| |
| 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); |
| } |
|
|
| |
| #[test] |
| fn test_sequential_eval_state_persistence() { |
| let pool = pool(); |
| |
| let r1 = pool.eval_js("p1", "globalThis.myState = { counter: 0, name: 'init' }; 'set'", ""); |
| assert_eq!(r1.unwrap(), r#""set""#); |
|
|
| |
| let r2 = pool.eval_js("p1", "globalThis.myState.counter++; globalThis.myState.name = 'updated'; globalThis.myState.counter", ""); |
| assert_eq!(r2.unwrap(), "1"); |
|
|
| |
| let r3 = pool.eval_js("p1", "globalThis.myState.counter + ':' + globalThis.myState.name", ""); |
| assert_eq!(r3.unwrap(), r#""1:updated""#); |
|
|
| |
| let r4 = pool.eval_js("p2", "typeof globalThis.myState", ""); |
| assert_eq!(r4.unwrap(), r#""undefined""#); |
| } |
|
|
| |
| #[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"); |
| } |
|
|
| |
| #[test] |
| fn test_url_parse_and_hmac_signing_pipeline() { |
| let pool = pool(); |
| |
| 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); |
| } |
| |
| assert!(result.contains("42"), "Should contain user '42': {}", result); |
| assert!(result.contains("sigPrefix"), "Should contain sigPrefix: {}", result); |
| } |
|
|
| |
| #[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; |
| } |
| "#; |
| |
| pool.call_js_fn("p1", "nsigDecode", fn_source, r#"{"n":"test1"}"#).unwrap(); |
|
|
| |
| let inputs = vec![ |
| (r#"{"n":"hello"}"#, r#""olleh""#), |
| (r#"{"n":"abcde"}"#, r#""edcba""#), |
| (r#"{"n":"x"}"#, r#""x""#), |
| (r#"{"n":"ab"}"#, r#""ba""#), |
| (r#"{"n":"1234567890"}"#, r#""0987654321""#), |
| ]; |
|
|
| 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); |
| } |
| } |
|
|
| |
| #[test] |
| fn test_text_encoder_sha256_pipeline() { |
| let pool = pool(); |
| |
| |
| 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"); |
| } |
|
|
| |
| #[test] |
| fn test_eval_js_structured_json_input_transform() { |
| let pool = pool(); |
| |
| |
| 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""# |
| ); |
| } |
|
|
| |
| #[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""#); |
| } |
| } |
|
|