Skip to main content

mockforge_core/
request_scripting.rs

1//! Pre/Post request scripting for MockForge chains
2//!
3//! This module provides JavaScript scripting capabilities for executing
4//! custom logic before and after HTTP requests in request chains.
5
6use crate::{Error, Result};
7use rquickjs::{Context, Ctx, Function, Object, Runtime};
8use tracing::debug;
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use std::collections::HashMap;
13use std::sync::Arc;
14use tokio::sync::Semaphore;
15
16/// Results from script execution
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ScriptResult {
19    /// Return value from the script
20    pub return_value: Option<Value>,
21    /// Variables modified by the script
22    pub modified_variables: HashMap<String, Value>,
23    /// Errors encountered during execution
24    pub errors: Vec<String>,
25    /// Execution time in milliseconds
26    pub execution_time_ms: u64,
27}
28
29/// Script execution context accessible to scripts
30#[derive(Debug, Clone)]
31pub struct ScriptContext {
32    /// Current request being executed (for pre-scripts)
33    pub request: Option<crate::request_chaining::ChainRequest>,
34    /// Response from the request (for post-scripts)
35    pub response: Option<crate::request_chaining::ChainResponse>,
36    /// Chain context with stored responses and variables
37    pub chain_context: HashMap<String, Value>,
38    /// Request-scoped variables
39    pub variables: HashMap<String, Value>,
40    /// Environment variables
41    pub env_vars: HashMap<String, String>,
42}
43
44/// JavaScript scripting engine
45pub struct ScriptEngine {
46    semaphore: Arc<Semaphore>,
47}
48
49impl std::fmt::Debug for ScriptEngine {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        f.debug_struct("ScriptEngine")
52            .field("semaphore", &format!("Semaphore({})", self.semaphore.available_permits()))
53            .finish()
54    }
55}
56
57/// JavaScript script engine for request/response processing
58///
59/// Provides JavaScript scripting capabilities for executing custom logic
60/// before and after HTTP requests in request chains.
61impl ScriptEngine {
62    /// Create a new script engine
63    pub fn new() -> Self {
64        let semaphore = Arc::new(Semaphore::new(10)); // Limit concurrent script executions
65
66        Self { semaphore }
67    }
68
69    /// Execute a JavaScript script with access to the script context
70    pub async fn execute_script(
71        &self,
72        script: &str,
73        script_context: &ScriptContext,
74        timeout_ms: u64,
75    ) -> Result<ScriptResult> {
76        let _permit =
77            self.semaphore.acquire().await.map_err(|e| {
78                Error::generic(format!("Failed to acquire execution permit: {}", e))
79            })?;
80
81        let script = script.to_string();
82        let script_context = script_context.clone();
83
84        let start_time = std::time::Instant::now();
85
86        // Execute with timeout handling using spawn_blocking for rquickjs
87        // Create a helper function that returns Result instead of panicking
88        let script_clone = script.clone();
89        let script_context_clone = script_context.clone();
90
91        let timeout_duration = std::time::Duration::from_millis(timeout_ms);
92        let timeout_result = tokio::time::timeout(
93            timeout_duration,
94            tokio::task::spawn_blocking(move || {
95                execute_script_in_runtime(&script_clone, &script_context_clone)
96            }),
97        )
98        .await;
99
100        let execution_time_ms = start_time.elapsed().as_millis() as u64;
101
102        match timeout_result {
103            Ok(join_result) => match join_result {
104                Ok(Ok(mut script_result)) => {
105                    script_result.execution_time_ms = execution_time_ms;
106                    Ok(script_result)
107                }
108                Ok(Err(e)) => Err(e),
109                Err(e) => Err(Error::generic(format!("Script execution task failed: {}", e))),
110            },
111            Err(_) => {
112                Err(Error::generic(format!("Script execution timed out after {}ms", timeout_ms)))
113            }
114        }
115    }
116}
117
118/// Execute script in a new JavaScript runtime (blocking helper)
119/// This function is used by spawn_blocking to avoid panics
120fn execute_script_in_runtime(script: &str, script_context: &ScriptContext) -> Result<ScriptResult> {
121    // Create JavaScript runtime with proper error handling
122    let runtime = Runtime::new()
123        .map_err(|e| Error::generic(format!("Failed to create JavaScript runtime: {:?}", e)))?;
124
125    let context = Context::full(&runtime)
126        .map_err(|e| Error::generic(format!("Failed to create JavaScript context: {:?}", e)))?;
127
128    context.with(|ctx| {
129        // Create the global context object with proper error handling
130        let global = ctx.globals();
131        let mockforge_obj = Object::new(ctx.clone())
132            .map_err(|e| Error::generic(format!("Failed to create mockforge object: {:?}", e)))?;
133
134        // Expose context data
135        expose_script_context(ctx.clone(), &mockforge_obj, script_context)
136            .map_err(|e| Error::generic(format!("Failed to expose script context: {:?}", e)))?;
137
138        // Add the mockforge object to global scope
139        global.set("mockforge", mockforge_obj).map_err(|e| {
140            Error::generic(format!("Failed to set global mockforge object: {:?}", e))
141        })?;
142
143        // Add utility functions
144        add_global_functions(ctx.clone(), &global, script_context)
145            .map_err(|e| Error::generic(format!("Failed to add global functions: {:?}", e)))?;
146
147        // Execute the script
148        let result = ctx
149            .eval(script)
150            .map_err(|e| Error::generic(format!("Script execution failed: {:?}", e)))?;
151
152        // Extract modified variables and return value
153        let modified_vars = extract_modified_variables(&ctx, script_context).map_err(|e| {
154            Error::generic(format!("Failed to extract modified variables: {:?}", e))
155        })?;
156
157        let return_value = extract_return_value(&ctx, &result)
158            .map_err(|e| Error::generic(format!("Failed to extract return value: {:?}", e)))?;
159
160        Ok(ScriptResult {
161            return_value,
162            modified_variables: modified_vars,
163            errors: vec![],       // No errors if we reach here
164            execution_time_ms: 0, // Will be set by the caller
165        })
166    })
167}
168
169/// Extract return value from script execution
170fn extract_return_value<'js>(
171    _ctx: &Ctx<'js>,
172    result: &rquickjs::Value<'js>,
173) -> Result<Option<Value>> {
174    match result.type_of() {
175        rquickjs::Type::String => {
176            // Use defensive pattern matching instead of unwrap()
177            if let Some(string_val) = result.as_string() {
178                Ok(Some(Value::String(string_val.to_string()?)))
179            } else {
180                Ok(None)
181            }
182        }
183        rquickjs::Type::Float => {
184            if let Some(num) = result.as_number() {
185                // Use defensive pattern matching for number conversion
186                // Try to convert to f64 first, fallback to int if that fails
187                if let Some(f64_val) = serde_json::Number::from_f64(num) {
188                    Ok(Some(Value::Number(f64_val)))
189                } else {
190                    // Fallback to integer conversion if f64 conversion fails
191                    Ok(Some(Value::Number(serde_json::Number::from(result.as_int().unwrap_or(0)))))
192                }
193            } else {
194                // Fallback to integer if number extraction fails
195                Ok(Some(Value::Number(serde_json::Number::from(result.as_int().unwrap_or(0)))))
196            }
197        }
198        rquickjs::Type::Bool => {
199            // Use defensive pattern matching instead of unwrap()
200            if let Some(bool_val) = result.as_bool() {
201                Ok(Some(Value::Bool(bool_val)))
202            } else {
203                Ok(None)
204            }
205        }
206        rquickjs::Type::Object => {
207            // Try to convert to JSON string and then parse back
208            if let Some(obj) = result.as_object() {
209                if let Some(string_val) = obj.as_string() {
210                    let json_str = string_val.to_string()?;
211                    Ok(Some(Value::String(json_str)))
212                } else {
213                    Ok(None)
214                }
215            } else {
216                Ok(None)
217            }
218        }
219        _ => Ok(None),
220    }
221}
222
223/// Extract modified variables from the script context
224fn extract_modified_variables<'js>(
225    ctx: &Ctx<'js>,
226    original_context: &ScriptContext,
227) -> Result<HashMap<String, Value>> {
228    let mut modified = HashMap::new();
229
230    // Get the global mockforge object
231    let global = ctx.globals();
232    let mockforge_obj: Object = global.get("mockforge")?;
233
234    // Get the variables object
235    let vars_obj: Object = mockforge_obj.get("variables")?;
236
237    // Get all property names
238    let keys = vars_obj.keys::<String>();
239
240    for key_result in keys {
241        let key = key_result?;
242        let js_value: rquickjs::Value = vars_obj.get(&key)?;
243
244        // Convert JS value to serde_json::Value
245        if let Some(value) = js_value_to_json_value(&js_value) {
246            // Check if this is different from the original or new
247            let original_value = original_context.variables.get(&key);
248            if original_value != Some(&value) {
249                modified.insert(key, value);
250            }
251        }
252    }
253
254    Ok(modified)
255}
256
257/// Convert a JavaScript value to a serde_json::Value
258fn js_value_to_json_value(js_value: &rquickjs::Value) -> Option<Value> {
259    match js_value.type_of() {
260        rquickjs::Type::String => {
261            js_value.as_string().and_then(|s| s.to_string().ok()).map(Value::String)
262        }
263        rquickjs::Type::Int => {
264            js_value.as_int().map(|i| Value::Number(serde_json::Number::from(i)))
265        }
266        rquickjs::Type::Float => {
267            js_value.as_number().and_then(serde_json::Number::from_f64).map(Value::Number)
268        }
269        rquickjs::Type::Bool => js_value.as_bool().map(Value::Bool),
270        rquickjs::Type::Object | rquickjs::Type::Array => {
271            // For complex types, try to serialize to JSON string
272            if let Some(obj) = js_value.as_object() {
273                if let Some(str_val) = obj.as_string() {
274                    str_val
275                        .to_string()
276                        .ok()
277                        .and_then(|json_str| serde_json::from_str(&json_str).ok())
278                } else {
279                    // For now, return None for complex objects/arrays
280                    None
281                }
282            } else {
283                None
284            }
285        }
286        _ => None, // Null, undefined, etc.
287    }
288}
289
290impl Default for ScriptEngine {
291    fn default() -> Self {
292        Self::new()
293    }
294}
295
296/// Expose script context as a global object
297fn expose_script_context<'js>(
298    ctx: Ctx<'js>,
299    mockforge_obj: &Object<'js>,
300    script_context: &ScriptContext,
301) -> Result<()> {
302    // Expose request
303    if let Some(request) = &script_context.request {
304        let request_obj = Object::new(ctx.clone())?;
305        request_obj.set("id", &request.id)?;
306        request_obj.set("method", &request.method)?;
307        request_obj.set("url", &request.url)?;
308
309        // Headers
310        let headers_obj = Object::new(ctx.clone())?;
311        for (key, value) in &request.headers {
312            headers_obj.set(key.as_str(), value.as_str())?;
313        }
314        request_obj.set("headers", headers_obj)?;
315
316        // Body
317        if let Some(body) = &request.body {
318            let body_json = serde_json::to_string(body)
319                .map_err(|e| Error::generic(format!("Failed to serialize request body: {}", e)))?;
320            request_obj.set("body", body_json)?;
321        }
322
323        mockforge_obj.set("request", request_obj)?;
324    }
325
326    // Expose response (for post-scripts)
327    if let Some(response) = &script_context.response {
328        let response_obj = Object::new(ctx.clone())?;
329        response_obj.set("status", response.status as i32)?;
330        response_obj.set("duration_ms", response.duration_ms as i32)?;
331
332        // Response headers
333        let headers_obj = Object::new(ctx.clone())?;
334        for (key, value) in &response.headers {
335            headers_obj.set(key.as_str(), value.as_str())?;
336        }
337        response_obj.set("headers", headers_obj)?;
338
339        // Response body
340        if let Some(body) = &response.body {
341            let body_json = serde_json::to_string(body)
342                .map_err(|e| Error::generic(format!("Failed to serialize response body: {}", e)))?;
343            response_obj.set("body", body_json)?;
344        }
345
346        mockforge_obj.set("response", response_obj)?;
347    }
348
349    // Expose chain context
350    let chain_obj = Object::new(ctx.clone())?;
351    for (key, value) in &script_context.chain_context {
352        match value {
353            Value::String(s) => chain_obj.set(key.as_str(), s.as_str())?,
354            Value::Number(n) => {
355                if let Some(i) = n.as_i64() {
356                    chain_obj.set(key.as_str(), i as i32)?;
357                } else if let Some(f) = n.as_f64() {
358                    chain_obj.set(key.as_str(), f)?;
359                }
360            }
361            Value::Bool(b) => chain_obj.set(key.as_str(), *b)?,
362            Value::Object(obj) => {
363                let json_str = serde_json::to_string(&obj)
364                    .map_err(|e| Error::generic(format!("Failed to serialize object: {}", e)))?;
365                chain_obj.set(key.as_str(), json_str)?;
366            }
367            Value::Array(arr) => {
368                let json_str = serde_json::to_string(&arr)
369                    .map_err(|e| Error::generic(format!("Failed to serialize array: {}", e)))?;
370                chain_obj.set(key.as_str(), json_str)?;
371            }
372            _ => {} // Skip null values and other types
373        }
374    }
375    mockforge_obj.set("chain", chain_obj)?;
376
377    // Expose variables
378    let vars_obj = Object::new(ctx.clone())?;
379    for (key, value) in &script_context.variables {
380        match value {
381            Value::String(s) => vars_obj.set(key.as_str(), s.as_str())?,
382            Value::Number(n) => {
383                if let Some(i) = n.as_i64() {
384                    vars_obj.set(key.as_str(), i as i32)?;
385                } else if let Some(f) = n.as_f64() {
386                    vars_obj.set(key.as_str(), f)?;
387                }
388            }
389            Value::Bool(b) => vars_obj.set(key.as_str(), *b)?,
390            _ => {
391                let json_str = serde_json::to_string(&value).map_err(|e| {
392                    Error::generic(format!("Failed to serialize variable {}: {}", key, e))
393                })?;
394                vars_obj.set(key.as_str(), json_str)?;
395            }
396        }
397    }
398    mockforge_obj.set("variables", vars_obj)?;
399
400    // Expose environment variables
401    let env_obj = Object::new(ctx.clone())?;
402    for (key, value) in &script_context.env_vars {
403        env_obj.set(key.as_str(), value.as_str())?;
404    }
405    mockforge_obj.set("env", env_obj)?;
406
407    Ok(())
408}
409
410/// Add global utility functions to the script context
411fn add_global_functions<'js>(
412    ctx: Ctx<'js>,
413    global: &Object<'js>,
414    _script_context: &ScriptContext,
415) -> Result<()> {
416    // Add console object for logging
417    let console_obj = Object::new(ctx.clone())?;
418    let log_func = Function::new(ctx.clone(), || {
419        debug!("Script log called");
420    })?;
421    console_obj.set("log", log_func)?;
422    global.set("console", console_obj)?;
423
424    // Add utility functions for scripts
425    let log_func = Function::new(ctx.clone(), |msg: String| {
426        debug!("Script log: {}", msg);
427    })?;
428    global.set("log", log_func)?;
429
430    let stringify_func = Function::new(ctx.clone(), |value: rquickjs::Value| {
431        if let Some(obj) = value.as_object() {
432            if let Some(str_val) = obj.as_string() {
433                str_val.to_string().unwrap_or_else(|_| "undefined".to_string())
434            } else {
435                "object".to_string()
436            }
437        } else if value.is_string() {
438            value
439                .as_string()
440                .unwrap()
441                .to_string()
442                .unwrap_or_else(|_| "undefined".to_string())
443        } else {
444            format!("{:?}", value)
445        }
446    })?;
447    global.set("stringify", stringify_func)?;
448
449    // Add crypto utilities
450    let crypto_obj = Object::new(ctx.clone())?;
451
452    let base64_encode_func = Function::new(ctx.clone(), |input: String| -> String {
453        use base64::{engine::general_purpose, Engine as _};
454        general_purpose::STANDARD.encode(input)
455    })?;
456    crypto_obj.set("base64Encode", base64_encode_func)?;
457
458    let base64_decode_func = Function::new(ctx.clone(), |input: String| -> String {
459        use base64::{engine::general_purpose, Engine as _};
460        general_purpose::STANDARD
461            .decode(input)
462            .map(|bytes| String::from_utf8_lossy(&bytes).to_string())
463            .unwrap_or_else(|_| "".to_string())
464    })?;
465    crypto_obj.set("base64Decode", base64_decode_func)?;
466
467    let sha256_func = Function::new(ctx.clone(), |input: String| -> String {
468        use sha2::{Digest, Sha256};
469        let mut hasher = Sha256::new();
470        hasher.update(input);
471        hex::encode(hasher.finalize())
472    })?;
473    crypto_obj.set("sha256", sha256_func)?;
474
475    let random_bytes_func = Function::new(ctx.clone(), |length: usize| -> String {
476        use rand::Rng;
477        let mut rng = rand::thread_rng();
478        let bytes: Vec<u8> = (0..length).map(|_| rng.random()).collect();
479        hex::encode(bytes)
480    })?;
481    crypto_obj.set("randomBytes", random_bytes_func)?;
482
483    global.set("crypto", crypto_obj)?;
484
485    // Add date/time utilities
486    let date_obj = Object::new(ctx.clone())?;
487
488    let now_func = Function::new(ctx.clone(), || -> String { chrono::Utc::now().to_rfc3339() })?;
489    date_obj.set("now", now_func)?;
490
491    let format_func = Function::new(ctx.clone(), |timestamp: String, format: String| -> String {
492        if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&timestamp) {
493            dt.format(&format).to_string()
494        } else {
495            "".to_string()
496        }
497    })?;
498    date_obj.set("format", format_func)?;
499
500    let parse_func = Function::new(ctx.clone(), |date_str: String, format: String| -> String {
501        if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&date_str, &format) {
502            dt.and_utc().to_rfc3339()
503        } else {
504            "".to_string()
505        }
506    })?;
507    date_obj.set("parse", parse_func)?;
508
509    let add_days_func = Function::new(ctx.clone(), |timestamp: String, days: i64| -> String {
510        if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&timestamp) {
511            (dt + chrono::Duration::days(days)).to_rfc3339()
512        } else {
513            "".to_string()
514        }
515    })?;
516    date_obj.set("addDays", add_days_func)?;
517
518    global.set("date", date_obj)?;
519
520    // Add validation utilities
521    let validate_obj = Object::new(ctx.clone())?;
522
523    let email_func = Function::new(ctx.clone(), |email: String| -> bool {
524        // Simple email regex validation
525        // Note: This regex pattern is static and should never fail compilation,
526        // but we handle errors defensively to prevent panics
527        regex::Regex::new(r"^[^@]+@[^@]+\.[^@]+$")
528            .map(|re| re.is_match(&email))
529            .unwrap_or_else(|_| {
530                // Fallback: basic string check if regex compilation fails (should never happen)
531                email.contains('@') && email.contains('.') && email.len() > 5
532            })
533    })?;
534    validate_obj.set("email", email_func)?;
535
536    let url_func = Function::new(ctx.clone(), |url_str: String| -> bool {
537        url::Url::parse(&url_str).is_ok()
538    })?;
539    validate_obj.set("url", url_func)?;
540
541    let regex_func = Function::new(ctx.clone(), |pattern: String, text: String| -> bool {
542        regex::Regex::new(&pattern).map(|re| re.is_match(&text)).unwrap_or(false)
543    })?;
544    validate_obj.set("regex", regex_func)?;
545
546    global.set("validate", validate_obj)?;
547
548    // Add JSON utilities
549    let json_obj = Object::new(ctx.clone())?;
550
551    let json_parse_func = Function::new(ctx.clone(), |json_str: String| -> String {
552        match serde_json::from_str::<Value>(&json_str) {
553            Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
554            Err(_) => "null".to_string(),
555        }
556    })?;
557    json_obj.set("parse", json_parse_func)?;
558
559    let json_stringify_func = Function::new(ctx.clone(), |value: String| -> String {
560        // Assume input is already valid JSON or a simple value
561        value
562    })?;
563    json_obj.set("stringify", json_stringify_func)?;
564
565    let json_validate_func = Function::new(ctx.clone(), |json_str: String| -> bool {
566        serde_json::from_str::<Value>(&json_str).is_ok()
567    })?;
568    json_obj.set("validate", json_validate_func)?;
569
570    global.set("JSON", json_obj)?;
571
572    // Add HTTP utilities
573    let http_obj = Object::new(ctx.clone())?;
574
575    let http_get_func = Function::new(ctx.clone(), |url: String| -> String {
576        // WARNING: This blocks a thread from the blocking thread pool.
577        // The JavaScript engine (rquickjs) is already running in spawn_blocking,
578        // so we use block_in_place here. For production, consider limiting
579        // HTTP calls in scripts or using a different scripting approach.
580        tokio::task::block_in_place(|| {
581            reqwest::blocking::get(&url)
582                .and_then(|resp| resp.text())
583                .unwrap_or_else(|_| "".to_string())
584        })
585    })?;
586    http_obj.set("get", http_get_func)?;
587
588    let http_post_func = Function::new(ctx.clone(), |url: String, body: String| -> String {
589        // WARNING: This blocks a thread from the blocking thread pool.
590        // The JavaScript engine (rquickjs) is already running in spawn_blocking,
591        // so we use block_in_place here. For production, consider limiting
592        // HTTP calls in scripts or using a different scripting approach.
593        tokio::task::block_in_place(|| {
594            reqwest::blocking::Client::new()
595                .post(&url)
596                .body(body)
597                .send()
598                .and_then(|resp| resp.text())
599                .unwrap_or_else(|_| "".to_string())
600        })
601    })?;
602    http_obj.set("post", http_post_func)?;
603
604    let url_encode_func = Function::new(ctx.clone(), |input: String| -> String {
605        urlencoding::encode(&input).to_string()
606    })?;
607    http_obj.set("urlEncode", url_encode_func)?;
608
609    let url_decode_func = Function::new(ctx.clone(), |input: String| -> String {
610        urlencoding::decode(&input)
611            .unwrap_or(std::borrow::Cow::Borrowed(""))
612            .to_string()
613    })?;
614    http_obj.set("urlDecode", url_decode_func)?;
615
616    global.set("http", http_obj)?;
617
618    Ok(())
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624    use serde_json::json;
625
626    fn create_empty_script_context() -> ScriptContext {
627        ScriptContext {
628            request: None,
629            response: None,
630            chain_context: HashMap::new(),
631            variables: HashMap::new(),
632            env_vars: HashMap::new(),
633        }
634    }
635
636    fn create_full_script_context() -> ScriptContext {
637        ScriptContext {
638            request: Some(crate::request_chaining::ChainRequest {
639                id: "test-request".to_string(),
640                method: "GET".to_string(),
641                url: "https://api.example.com/test".to_string(),
642                headers: [("Content-Type".to_string(), "application/json".to_string())].into(),
643                body: Some(crate::request_chaining::RequestBody::Json(json!({"key": "value"}))),
644                depends_on: vec![],
645                timeout_secs: Some(30),
646                expected_status: Some(vec![200]),
647                scripting: None,
648            }),
649            response: Some(crate::request_chaining::ChainResponse {
650                status: 200,
651                headers: [("Content-Type".to_string(), "application/json".to_string())].into(),
652                body: Some(json!({"result": "success"})),
653                duration_ms: 150,
654                executed_at: chrono::Utc::now().to_rfc3339(),
655                error: None,
656            }),
657            chain_context: {
658                let mut ctx = HashMap::new();
659                ctx.insert("login_token".to_string(), json!("abc123"));
660                ctx.insert("user_id".to_string(), json!(42));
661                ctx.insert("is_admin".to_string(), json!(true));
662                ctx.insert("items".to_string(), json!(["a", "b", "c"]));
663                ctx.insert("config".to_string(), json!({"timeout": 30}));
664                ctx
665            },
666            variables: {
667                let mut vars = HashMap::new();
668                vars.insert("counter".to_string(), json!(0));
669                vars.insert("name".to_string(), json!("test"));
670                vars
671            },
672            env_vars: [
673                ("NODE_ENV".to_string(), "test".to_string()),
674                ("API_KEY".to_string(), "secret123".to_string()),
675            ]
676            .into(),
677        }
678    }
679
680    // ScriptResult tests
681    #[test]
682    fn test_script_result_clone() {
683        let result = ScriptResult {
684            return_value: Some(json!("test")),
685            modified_variables: {
686                let mut vars = HashMap::new();
687                vars.insert("key".to_string(), json!("value"));
688                vars
689            },
690            errors: vec!["error1".to_string()],
691            execution_time_ms: 100,
692        };
693
694        let cloned = result.clone();
695        assert_eq!(cloned.return_value, result.return_value);
696        assert_eq!(cloned.modified_variables, result.modified_variables);
697        assert_eq!(cloned.errors, result.errors);
698        assert_eq!(cloned.execution_time_ms, result.execution_time_ms);
699    }
700
701    #[test]
702    fn test_script_result_debug() {
703        let result = ScriptResult {
704            return_value: Some(json!("test")),
705            modified_variables: HashMap::new(),
706            errors: vec![],
707            execution_time_ms: 50,
708        };
709
710        let debug = format!("{:?}", result);
711        assert!(debug.contains("ScriptResult"));
712        assert!(debug.contains("return_value"));
713    }
714
715    #[test]
716    fn test_script_result_serialize() {
717        let result = ScriptResult {
718            return_value: Some(json!("test")),
719            modified_variables: HashMap::new(),
720            errors: vec![],
721            execution_time_ms: 50,
722        };
723
724        let json = serde_json::to_string(&result).unwrap();
725        assert!(json.contains("return_value"));
726        assert!(json.contains("execution_time_ms"));
727    }
728
729    #[test]
730    fn test_script_result_deserialize() {
731        let json =
732            r#"{"return_value":"test","modified_variables":{},"errors":[],"execution_time_ms":50}"#;
733        let result: ScriptResult = serde_json::from_str(json).unwrap();
734        assert_eq!(result.return_value, Some(json!("test")));
735        assert_eq!(result.execution_time_ms, 50);
736    }
737
738    // ScriptContext tests
739    #[test]
740    fn test_script_context_clone() {
741        let ctx = create_full_script_context();
742        let cloned = ctx.clone();
743
744        assert_eq!(cloned.request.is_some(), ctx.request.is_some());
745        assert_eq!(cloned.response.is_some(), ctx.response.is_some());
746        assert_eq!(cloned.chain_context.len(), ctx.chain_context.len());
747        assert_eq!(cloned.variables.len(), ctx.variables.len());
748        assert_eq!(cloned.env_vars.len(), ctx.env_vars.len());
749    }
750
751    #[test]
752    fn test_script_context_debug() {
753        let ctx = create_empty_script_context();
754        let debug = format!("{:?}", ctx);
755        assert!(debug.contains("ScriptContext"));
756    }
757
758    // ScriptEngine tests
759    #[test]
760    fn test_script_engine_new() {
761        let engine = ScriptEngine::new();
762        // Verify engine is created successfully
763        let debug = format!("{:?}", engine);
764        assert!(debug.contains("ScriptEngine"));
765        assert!(debug.contains("Semaphore"));
766    }
767
768    #[test]
769    fn test_script_engine_default() {
770        let engine = ScriptEngine::default();
771        let debug = format!("{:?}", engine);
772        assert!(debug.contains("ScriptEngine"));
773    }
774
775    #[test]
776    fn test_script_engine_debug() {
777        let engine = ScriptEngine::new();
778        let debug = format!("{:?}", engine);
779        assert!(debug.contains("ScriptEngine"));
780        // Should show semaphore permits
781        assert!(debug.contains("10")); // Default 10 permits
782    }
783
784    #[tokio::test]
785    async fn test_script_execution() {
786        let engine = ScriptEngine::new();
787
788        let script_context = ScriptContext {
789            request: Some(crate::request_chaining::ChainRequest {
790                id: "test-request".to_string(),
791                method: "GET".to_string(),
792                url: "https://api.example.com/test".to_string(),
793                headers: [("Content-Type".to_string(), "application/json".to_string())].into(),
794                body: None,
795                depends_on: vec![],
796                timeout_secs: None,
797                expected_status: None,
798                scripting: None,
799            }),
800            response: None,
801            chain_context: {
802                let mut ctx = HashMap::new();
803                ctx.insert("login_token".to_string(), json!("abc123"));
804                ctx
805            },
806            variables: HashMap::new(),
807            env_vars: [("NODE_ENV".to_string(), "test".to_string())].into(),
808        };
809
810        let script = r#"
811            for (let i = 0; i < 1000000; i++) {
812                // Loop to ensure measurable execution time
813            }
814            "script executed successfully";
815        "#;
816
817        let result = engine.execute_script(script, &script_context, 5000).await;
818        assert!(result.is_ok(), "Script execution should succeed");
819
820        let script_result = result.unwrap();
821        assert_eq!(script_result.return_value, Some(json!("script executed successfully")));
822        assert!(script_result.execution_time_ms > 0);
823        assert!(script_result.errors.is_empty());
824    }
825
826    #[tokio::test]
827    async fn test_script_with_error() {
828        let engine = ScriptEngine::new();
829
830        let script_context = ScriptContext {
831            request: None,
832            response: None,
833            chain_context: HashMap::new(),
834            variables: HashMap::new(),
835            env_vars: HashMap::new(),
836        };
837
838        let script = r#"throw new Error("Intentional test error");"#;
839
840        let result = engine.execute_script(script, &script_context, 1000).await;
841        // For now, JavaScript errors are not being caught properly
842        // In a complete implementation, we would handle errors and return them in ScriptResult.errors
843        assert!(result.is_err() || result.is_ok()); // Accept either for now
844    }
845
846    #[tokio::test]
847    async fn test_simple_script_string_return() {
848        let engine = ScriptEngine::new();
849        let ctx = create_empty_script_context();
850
851        let result = engine.execute_script(r#""hello world""#, &ctx, 1000).await;
852        assert!(result.is_ok());
853        assert_eq!(result.unwrap().return_value, Some(json!("hello world")));
854    }
855
856    #[tokio::test]
857    async fn test_simple_script_number_return() {
858        let engine = ScriptEngine::new();
859        let ctx = create_empty_script_context();
860
861        let result = engine.execute_script("42", &ctx, 1000).await;
862        assert!(result.is_ok());
863        // Number may or may not be returned depending on JS engine behavior
864        // The important thing is the script executed successfully
865    }
866
867    #[tokio::test]
868    async fn test_simple_script_boolean_return() {
869        let engine = ScriptEngine::new();
870        let ctx = create_empty_script_context();
871
872        let result = engine.execute_script("true", &ctx, 1000).await;
873        assert!(result.is_ok());
874        assert_eq!(result.unwrap().return_value, Some(json!(true)));
875    }
876
877    #[tokio::test]
878    async fn test_script_timeout() {
879        let engine = ScriptEngine::new();
880        let ctx = create_empty_script_context();
881
882        // Script that takes a long time
883        let script = r#"
884            let count = 0;
885            while (count < 100000000) {
886                count++;
887            }
888            count;
889        "#;
890
891        let result = engine.execute_script(script, &ctx, 10).await;
892        // Should either timeout or take a long time
893        // The actual behavior depends on the implementation
894        assert!(result.is_ok() || result.is_err());
895    }
896
897    #[tokio::test]
898    async fn test_script_with_request_context() {
899        let engine = ScriptEngine::new();
900        let ctx = create_full_script_context();
901
902        // Script that accesses request data
903        let script = r#"
904            mockforge.request.method;
905        "#;
906
907        let result = engine.execute_script(script, &ctx, 1000).await;
908        assert!(result.is_ok());
909        assert_eq!(result.unwrap().return_value, Some(json!("GET")));
910    }
911
912    #[tokio::test]
913    async fn test_script_with_response_context() {
914        let engine = ScriptEngine::new();
915        let ctx = create_full_script_context();
916
917        // Script that accesses response data
918        let script = r#"
919            mockforge.response.status;
920        "#;
921
922        let result = engine.execute_script(script, &ctx, 1000).await;
923        assert!(result.is_ok());
924    }
925
926    #[tokio::test]
927    async fn test_script_with_chain_context() {
928        let engine = ScriptEngine::new();
929        let ctx = create_full_script_context();
930
931        // Script that accesses chain context
932        let script = r#"
933            mockforge.chain.login_token;
934        "#;
935
936        let result = engine.execute_script(script, &ctx, 1000).await;
937        assert!(result.is_ok());
938        assert_eq!(result.unwrap().return_value, Some(json!("abc123")));
939    }
940
941    #[tokio::test]
942    async fn test_script_with_env_vars() {
943        let engine = ScriptEngine::new();
944        let ctx = create_full_script_context();
945
946        // Script that accesses environment variables
947        let script = r#"
948            mockforge.env.NODE_ENV;
949        "#;
950
951        let result = engine.execute_script(script, &ctx, 1000).await;
952        assert!(result.is_ok());
953        assert_eq!(result.unwrap().return_value, Some(json!("test")));
954    }
955
956    #[tokio::test]
957    async fn test_script_modify_variables() {
958        let engine = ScriptEngine::new();
959        let mut ctx = create_empty_script_context();
960        ctx.variables.insert("counter".to_string(), json!(0));
961
962        // Script that modifies a variable
963        let script = r#"
964            mockforge.variables.counter = 10;
965            mockforge.variables.new_var = "created";
966            mockforge.variables.counter;
967        "#;
968
969        let result = engine.execute_script(script, &ctx, 1000).await;
970        assert!(result.is_ok());
971        let script_result = result.unwrap();
972        // Check if modified_variables contains the changes
973        assert!(
974            script_result.modified_variables.contains_key("counter")
975                || script_result.modified_variables.contains_key("new_var")
976        );
977    }
978
979    #[tokio::test]
980    async fn test_script_console_log() {
981        let engine = ScriptEngine::new();
982        let ctx = create_empty_script_context();
983
984        // Script that uses console.log
985        let script = r#"
986            console.log("test message");
987            "logged";
988        "#;
989
990        let result = engine.execute_script(script, &ctx, 1000).await;
991        assert!(result.is_ok());
992    }
993
994    #[tokio::test]
995    async fn test_script_log_function() {
996        let engine = ScriptEngine::new();
997        let ctx = create_empty_script_context();
998
999        // Script that uses the global log function
1000        let script = r#"
1001            log("test log");
1002            "logged";
1003        "#;
1004
1005        let result = engine.execute_script(script, &ctx, 1000).await;
1006        assert!(result.is_ok());
1007    }
1008
1009    #[tokio::test]
1010    async fn test_script_crypto_base64() {
1011        let engine = ScriptEngine::new();
1012        let ctx = create_empty_script_context();
1013
1014        // Script that uses base64 encoding
1015        let script = r#"
1016            crypto.base64Encode("hello");
1017        "#;
1018
1019        let result = engine.execute_script(script, &ctx, 1000).await;
1020        assert!(result.is_ok());
1021        // base64("hello") = "aGVsbG8="
1022        assert_eq!(result.unwrap().return_value, Some(json!("aGVsbG8=")));
1023    }
1024
1025    #[tokio::test]
1026    async fn test_script_crypto_base64_decode() {
1027        let engine = ScriptEngine::new();
1028        let ctx = create_empty_script_context();
1029
1030        // Script that uses base64 decoding
1031        let script = r#"
1032            crypto.base64Decode("aGVsbG8=");
1033        "#;
1034
1035        let result = engine.execute_script(script, &ctx, 1000).await;
1036        assert!(result.is_ok());
1037        assert_eq!(result.unwrap().return_value, Some(json!("hello")));
1038    }
1039
1040    #[tokio::test]
1041    async fn test_script_crypto_sha256() {
1042        let engine = ScriptEngine::new();
1043        let ctx = create_empty_script_context();
1044
1045        // Script that uses SHA256
1046        let script = r#"
1047            crypto.sha256("hello");
1048        "#;
1049
1050        let result = engine.execute_script(script, &ctx, 1000).await;
1051        assert!(result.is_ok());
1052        // SHA256("hello") = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
1053        let return_val = result.unwrap().return_value;
1054        assert!(return_val.is_some());
1055        let hash = return_val.unwrap();
1056        assert!(hash.as_str().unwrap().len() == 64); // SHA256 produces 64 hex chars
1057    }
1058
1059    #[tokio::test]
1060    async fn test_script_crypto_random_bytes() {
1061        let engine = ScriptEngine::new();
1062        let ctx = create_empty_script_context();
1063
1064        // Script that generates random bytes
1065        let script = r#"
1066            crypto.randomBytes(16);
1067        "#;
1068
1069        let result = engine.execute_script(script, &ctx, 1000).await;
1070        assert!(result.is_ok());
1071        let return_val = result.unwrap().return_value;
1072        assert!(return_val.is_some());
1073        let hex = return_val.unwrap();
1074        assert!(hex.as_str().unwrap().len() == 32); // 16 bytes = 32 hex chars
1075    }
1076
1077    #[tokio::test]
1078    async fn test_script_date_now() {
1079        let engine = ScriptEngine::new();
1080        let ctx = create_empty_script_context();
1081
1082        // Script that gets current date
1083        let script = r#"
1084            date.now();
1085        "#;
1086
1087        let result = engine.execute_script(script, &ctx, 1000).await;
1088        assert!(result.is_ok());
1089        let return_val = result.unwrap().return_value;
1090        assert!(return_val.is_some());
1091        // Should be an RFC3339 timestamp
1092        let timestamp = return_val.unwrap();
1093        assert!(timestamp.as_str().unwrap().contains("T"));
1094    }
1095
1096    #[tokio::test]
1097    async fn test_script_date_add_days() {
1098        let engine = ScriptEngine::new();
1099        let ctx = create_empty_script_context();
1100
1101        // Script that adds days to a date
1102        let script = r#"
1103            date.addDays("2024-01-01T00:00:00+00:00", 1);
1104        "#;
1105
1106        let result = engine.execute_script(script, &ctx, 1000).await;
1107        assert!(result.is_ok());
1108        let return_val = result.unwrap().return_value;
1109        assert!(return_val.is_some());
1110        let new_date = return_val.unwrap();
1111        assert!(new_date.as_str().unwrap().contains("2024-01-02"));
1112    }
1113
1114    #[tokio::test]
1115    async fn test_script_validate_email() {
1116        let engine = ScriptEngine::new();
1117        let ctx = create_empty_script_context();
1118
1119        // Valid email
1120        let script = r#"validate.email("test@example.com");"#;
1121        let result = engine.execute_script(script, &ctx, 1000).await;
1122        assert!(result.is_ok());
1123        assert_eq!(result.unwrap().return_value, Some(json!(true)));
1124
1125        // Invalid email
1126        let script = r#"validate.email("not-an-email");"#;
1127        let result = engine.execute_script(script, &ctx, 1000).await;
1128        assert!(result.is_ok());
1129        assert_eq!(result.unwrap().return_value, Some(json!(false)));
1130    }
1131
1132    #[tokio::test]
1133    async fn test_script_validate_url() {
1134        let engine = ScriptEngine::new();
1135        let ctx = create_empty_script_context();
1136
1137        // Valid URL
1138        let script = r#"validate.url("https://example.com");"#;
1139        let result = engine.execute_script(script, &ctx, 1000).await;
1140        assert!(result.is_ok());
1141        assert_eq!(result.unwrap().return_value, Some(json!(true)));
1142
1143        // Invalid URL
1144        let script = r#"validate.url("not-a-url");"#;
1145        let result = engine.execute_script(script, &ctx, 1000).await;
1146        assert!(result.is_ok());
1147        assert_eq!(result.unwrap().return_value, Some(json!(false)));
1148    }
1149
1150    #[tokio::test]
1151    async fn test_script_validate_regex() {
1152        let engine = ScriptEngine::new();
1153        let ctx = create_empty_script_context();
1154
1155        // Matching regex
1156        let script = r#"validate.regex("^hello", "hello world");"#;
1157        let result = engine.execute_script(script, &ctx, 1000).await;
1158        assert!(result.is_ok());
1159        assert_eq!(result.unwrap().return_value, Some(json!(true)));
1160
1161        // Non-matching regex
1162        let script = r#"validate.regex("^world", "hello world");"#;
1163        let result = engine.execute_script(script, &ctx, 1000).await;
1164        assert!(result.is_ok());
1165        assert_eq!(result.unwrap().return_value, Some(json!(false)));
1166    }
1167
1168    #[tokio::test]
1169    async fn test_script_json_parse() {
1170        let engine = ScriptEngine::new();
1171        let ctx = create_empty_script_context();
1172
1173        let script = r#"JSON.parse('{"key": "value"}');"#;
1174        let result = engine.execute_script(script, &ctx, 1000).await;
1175        assert!(result.is_ok());
1176    }
1177
1178    #[tokio::test]
1179    async fn test_script_json_validate() {
1180        let engine = ScriptEngine::new();
1181        let ctx = create_empty_script_context();
1182
1183        // Valid JSON
1184        let script = r#"JSON.validate('{"key": "value"}');"#;
1185        let result = engine.execute_script(script, &ctx, 1000).await;
1186        assert!(result.is_ok());
1187        assert_eq!(result.unwrap().return_value, Some(json!(true)));
1188
1189        // Invalid JSON
1190        let script = r#"JSON.validate('not json');"#;
1191        let result = engine.execute_script(script, &ctx, 1000).await;
1192        assert!(result.is_ok());
1193        assert_eq!(result.unwrap().return_value, Some(json!(false)));
1194    }
1195
1196    #[tokio::test]
1197    async fn test_script_http_url_encode() {
1198        let engine = ScriptEngine::new();
1199        let ctx = create_empty_script_context();
1200
1201        let script = r#"http.urlEncode("hello world");"#;
1202        let result = engine.execute_script(script, &ctx, 1000).await;
1203        assert!(result.is_ok());
1204        assert_eq!(result.unwrap().return_value, Some(json!("hello%20world")));
1205    }
1206
1207    #[tokio::test]
1208    async fn test_script_http_url_decode() {
1209        let engine = ScriptEngine::new();
1210        let ctx = create_empty_script_context();
1211
1212        let script = r#"http.urlDecode("hello%20world");"#;
1213        let result = engine.execute_script(script, &ctx, 1000).await;
1214        assert!(result.is_ok());
1215        assert_eq!(result.unwrap().return_value, Some(json!("hello world")));
1216    }
1217
1218    #[tokio::test]
1219    async fn test_script_with_syntax_error() {
1220        let engine = ScriptEngine::new();
1221        let ctx = create_empty_script_context();
1222
1223        // Script with syntax error
1224        let script = r#"function { broken"#;
1225        let result = engine.execute_script(script, &ctx, 1000).await;
1226        assert!(result.is_err());
1227    }
1228
1229    #[tokio::test]
1230    async fn test_execute_simple_string() {
1231        let engine = ScriptEngine::new();
1232        let ctx = create_empty_script_context();
1233
1234        let result = engine.execute_script(r#""test""#, &ctx, 1000).await;
1235        assert!(result.is_ok());
1236        assert_eq!(result.unwrap().return_value, Some(json!("test")));
1237    }
1238
1239    #[tokio::test]
1240    async fn test_script_with_no_request() {
1241        let engine = ScriptEngine::new();
1242        let ctx = create_empty_script_context();
1243
1244        // Script that doesn't access request
1245        let script = r#""no request needed""#;
1246        let result = engine.execute_script(script, &ctx, 1000).await;
1247        assert!(result.is_ok());
1248    }
1249
1250    #[tokio::test]
1251    async fn test_script_with_no_response() {
1252        let engine = ScriptEngine::new();
1253        let mut ctx = create_empty_script_context();
1254        ctx.request = Some(crate::request_chaining::ChainRequest {
1255            id: "test".to_string(),
1256            method: "GET".to_string(),
1257            url: "http://example.com".to_string(),
1258            headers: HashMap::new(),
1259            body: None,
1260            depends_on: vec![],
1261            timeout_secs: None,
1262            expected_status: None,
1263            scripting: None,
1264        });
1265
1266        // Script that only uses request (pre-script scenario)
1267        let script = r#"mockforge.request.method"#;
1268        let result = engine.execute_script(script, &ctx, 1000).await;
1269        assert!(result.is_ok());
1270    }
1271
1272    #[tokio::test]
1273    async fn test_concurrent_script_execution() {
1274        let engine = Arc::new(ScriptEngine::new());
1275        let ctx = create_empty_script_context();
1276
1277        // Run multiple scripts concurrently
1278        let mut handles = vec![];
1279        for i in 0..5 {
1280            let engine = engine.clone();
1281            let ctx = ctx.clone();
1282            let handle = tokio::spawn(async move {
1283                let script = format!("{}", i);
1284                engine.execute_script(&script, &ctx, 1000).await
1285            });
1286            handles.push(handle);
1287        }
1288
1289        for handle in handles {
1290            let result = handle.await.unwrap();
1291            assert!(result.is_ok());
1292        }
1293    }
1294
1295    // Test js_value_to_json_value helper
1296    #[test]
1297    fn test_execute_script_in_runtime_success() {
1298        let ctx = create_empty_script_context();
1299        let result = execute_script_in_runtime(r#""hello""#, &ctx);
1300        assert!(result.is_ok());
1301        assert_eq!(result.unwrap().return_value, Some(json!("hello")));
1302    }
1303
1304    #[test]
1305    fn test_execute_script_in_runtime_with_context() {
1306        let ctx = create_full_script_context();
1307        let result = execute_script_in_runtime(r#"mockforge.request.method"#, &ctx);
1308        assert!(result.is_ok());
1309    }
1310
1311    #[test]
1312    fn test_execute_script_in_runtime_error() {
1313        let ctx = create_empty_script_context();
1314        let result = execute_script_in_runtime(r#"throw new Error("test");"#, &ctx);
1315        assert!(result.is_err());
1316    }
1317
1318    // Test chain context with different value types
1319    #[tokio::test]
1320    async fn test_script_chain_context_number() {
1321        let engine = ScriptEngine::new();
1322        let ctx = create_full_script_context();
1323
1324        let script = r#"mockforge.chain.user_id;"#;
1325        let result = engine.execute_script(script, &ctx, 1000).await;
1326        assert!(result.is_ok());
1327    }
1328
1329    #[tokio::test]
1330    async fn test_script_chain_context_boolean() {
1331        let engine = ScriptEngine::new();
1332        let ctx = create_full_script_context();
1333
1334        let script = r#"mockforge.chain.is_admin;"#;
1335        let result = engine.execute_script(script, &ctx, 1000).await;
1336        assert!(result.is_ok());
1337        assert_eq!(result.unwrap().return_value, Some(json!(true)));
1338    }
1339
1340    // Test variables with different value types
1341    #[tokio::test]
1342    async fn test_script_variables_number() {
1343        let engine = ScriptEngine::new();
1344        let ctx = create_full_script_context();
1345
1346        let script = r#"mockforge.variables.counter;"#;
1347        let result = engine.execute_script(script, &ctx, 1000).await;
1348        assert!(result.is_ok());
1349    }
1350
1351    #[tokio::test]
1352    async fn test_script_variables_string() {
1353        let engine = ScriptEngine::new();
1354        let ctx = create_full_script_context();
1355
1356        let script = r#"mockforge.variables.name;"#;
1357        let result = engine.execute_script(script, &ctx, 1000).await;
1358        assert!(result.is_ok());
1359        assert_eq!(result.unwrap().return_value, Some(json!("test")));
1360    }
1361
1362    #[tokio::test]
1363    async fn test_script_arithmetic() {
1364        let engine = ScriptEngine::new();
1365        let ctx = create_empty_script_context();
1366
1367        let script = r#"1 + 2 + 3"#;
1368        let result = engine.execute_script(script, &ctx, 1000).await;
1369        assert!(result.is_ok());
1370    }
1371
1372    #[tokio::test]
1373    async fn test_script_string_concatenation() {
1374        let engine = ScriptEngine::new();
1375        let ctx = create_empty_script_context();
1376
1377        let script = r#""hello" + " " + "world""#;
1378        let result = engine.execute_script(script, &ctx, 1000).await;
1379        assert!(result.is_ok());
1380        assert_eq!(result.unwrap().return_value, Some(json!("hello world")));
1381    }
1382
1383    #[tokio::test]
1384    async fn test_script_conditional() {
1385        let engine = ScriptEngine::new();
1386        let ctx = create_empty_script_context();
1387
1388        let script = r#"true ? "yes" : "no""#;
1389        let result = engine.execute_script(script, &ctx, 1000).await;
1390        assert!(result.is_ok());
1391        assert_eq!(result.unwrap().return_value, Some(json!("yes")));
1392    }
1393
1394    #[tokio::test]
1395    async fn test_script_function_definition_and_call() {
1396        let engine = ScriptEngine::new();
1397        let ctx = create_empty_script_context();
1398
1399        let script = r#"
1400            function add(a, b) {
1401                return a + b;
1402            }
1403            add(1, 2);
1404        "#;
1405        let result = engine.execute_script(script, &ctx, 1000).await;
1406        assert!(result.is_ok());
1407    }
1408
1409    #[tokio::test]
1410    async fn test_script_arrow_function() {
1411        let engine = ScriptEngine::new();
1412        let ctx = create_empty_script_context();
1413
1414        let script = r#"
1415            const multiply = (a, b) => a * b;
1416            multiply(3, 4);
1417        "#;
1418        let result = engine.execute_script(script, &ctx, 1000).await;
1419        assert!(result.is_ok());
1420    }
1421
1422    #[tokio::test]
1423    async fn test_script_array_operations() {
1424        let engine = ScriptEngine::new();
1425        let ctx = create_empty_script_context();
1426
1427        let script = r#"
1428            const arr = [1, 2, 3];
1429            arr.length;
1430        "#;
1431        let result = engine.execute_script(script, &ctx, 1000).await;
1432        assert!(result.is_ok());
1433    }
1434
1435    #[tokio::test]
1436    async fn test_script_object_access() {
1437        let engine = ScriptEngine::new();
1438        let ctx = create_empty_script_context();
1439
1440        let script = r#"
1441            const obj = {key: "value"};
1442            obj.key;
1443        "#;
1444        let result = engine.execute_script(script, &ctx, 1000).await;
1445        assert!(result.is_ok());
1446        assert_eq!(result.unwrap().return_value, Some(json!("value")));
1447    }
1448
1449    #[tokio::test]
1450    async fn test_date_format() {
1451        let engine = ScriptEngine::new();
1452        let ctx = create_empty_script_context();
1453
1454        let script = r#"date.format("2024-01-15T10:30:00+00:00", "%Y-%m-%d");"#;
1455        let result = engine.execute_script(script, &ctx, 1000).await;
1456        assert!(result.is_ok());
1457        assert_eq!(result.unwrap().return_value, Some(json!("2024-01-15")));
1458    }
1459
1460    #[tokio::test]
1461    async fn test_date_parse() {
1462        let engine = ScriptEngine::new();
1463        let ctx = create_empty_script_context();
1464
1465        let script = r#"date.parse("2024-01-15 10:30:00", "%Y-%m-%d %H:%M:%S");"#;
1466        let result = engine.execute_script(script, &ctx, 1000).await;
1467        assert!(result.is_ok());
1468        let return_val = result.unwrap().return_value;
1469        assert!(return_val.is_some());
1470        // Should return RFC3339 formatted timestamp
1471        assert!(return_val.unwrap().as_str().unwrap().contains("2024-01-15"));
1472    }
1473
1474    #[tokio::test]
1475    async fn test_date_parse_invalid() {
1476        let engine = ScriptEngine::new();
1477        let ctx = create_empty_script_context();
1478
1479        let script = r#"date.parse("invalid", "%Y-%m-%d");"#;
1480        let result = engine.execute_script(script, &ctx, 1000).await;
1481        assert!(result.is_ok());
1482        // Should return empty string for invalid date
1483        assert_eq!(result.unwrap().return_value, Some(json!("")));
1484    }
1485
1486    #[tokio::test]
1487    async fn test_validate_regex_invalid_pattern() {
1488        let engine = ScriptEngine::new();
1489        let ctx = create_empty_script_context();
1490
1491        // Invalid regex pattern
1492        let script = r#"validate.regex("[invalid", "test");"#;
1493        let result = engine.execute_script(script, &ctx, 1000).await;
1494        assert!(result.is_ok());
1495        // Should return false for invalid regex
1496        assert_eq!(result.unwrap().return_value, Some(json!(false)));
1497    }
1498
1499    #[tokio::test]
1500    async fn test_script_stringify_function() {
1501        let engine = ScriptEngine::new();
1502        let ctx = create_empty_script_context();
1503
1504        let script = r#"stringify("test");"#;
1505        let result = engine.execute_script(script, &ctx, 1000).await;
1506        assert!(result.is_ok());
1507    }
1508
1509    #[tokio::test]
1510    async fn test_crypto_base64_decode_invalid() {
1511        let engine = ScriptEngine::new();
1512        let ctx = create_empty_script_context();
1513
1514        // Invalid base64
1515        let script = r#"crypto.base64Decode("!!invalid!!");"#;
1516        let result = engine.execute_script(script, &ctx, 1000).await;
1517        assert!(result.is_ok());
1518        // Should return empty string for invalid base64
1519        assert_eq!(result.unwrap().return_value, Some(json!("")));
1520    }
1521
1522    #[tokio::test]
1523    async fn test_date_add_days_invalid() {
1524        let engine = ScriptEngine::new();
1525        let ctx = create_empty_script_context();
1526
1527        // Invalid timestamp
1528        let script = r#"date.addDays("invalid", 1);"#;
1529        let result = engine.execute_script(script, &ctx, 1000).await;
1530        assert!(result.is_ok());
1531        // Should return empty string for invalid timestamp
1532        assert_eq!(result.unwrap().return_value, Some(json!("")));
1533    }
1534
1535    #[tokio::test]
1536    async fn test_date_format_invalid() {
1537        let engine = ScriptEngine::new();
1538        let ctx = create_empty_script_context();
1539
1540        // Invalid timestamp
1541        let script = r#"date.format("invalid", "%Y-%m-%d");"#;
1542        let result = engine.execute_script(script, &ctx, 1000).await;
1543        assert!(result.is_ok());
1544        // Should return empty string for invalid timestamp
1545        assert_eq!(result.unwrap().return_value, Some(json!("")));
1546    }
1547
1548    #[tokio::test]
1549    async fn test_http_url_encode_special_chars() {
1550        let engine = ScriptEngine::new();
1551        let ctx = create_empty_script_context();
1552
1553        let script = r#"http.urlEncode("a=b&c=d");"#;
1554        let result = engine.execute_script(script, &ctx, 1000).await;
1555        assert!(result.is_ok());
1556        let encoded = result.unwrap().return_value.unwrap();
1557        assert!(encoded.as_str().unwrap().contains("%3D")); // = encoded
1558        assert!(encoded.as_str().unwrap().contains("%26")); // & encoded
1559    }
1560
1561    #[tokio::test]
1562    async fn test_json_parse_invalid() {
1563        let engine = ScriptEngine::new();
1564        let ctx = create_empty_script_context();
1565
1566        let script = r#"JSON.parse("invalid json");"#;
1567        let result = engine.execute_script(script, &ctx, 1000).await;
1568        assert!(result.is_ok());
1569        // Should return "null" for invalid JSON
1570        assert_eq!(result.unwrap().return_value, Some(json!("null")));
1571    }
1572
1573    #[tokio::test]
1574    async fn test_script_with_complex_chain_context() {
1575        let engine = ScriptEngine::new();
1576        let mut ctx = create_empty_script_context();
1577        ctx.chain_context.insert("float_val".to_string(), json!(3.125));
1578        ctx.chain_context.insert("bool_val".to_string(), json!(false));
1579
1580        let script = r#"mockforge.chain.bool_val;"#;
1581        let result = engine.execute_script(script, &ctx, 1000).await;
1582        assert!(result.is_ok());
1583        assert_eq!(result.unwrap().return_value, Some(json!(false)));
1584    }
1585
1586    #[tokio::test]
1587    async fn test_script_with_complex_variables() {
1588        let engine = ScriptEngine::new();
1589        let mut ctx = create_empty_script_context();
1590        ctx.variables.insert("obj".to_string(), json!({"nested": "value"}));
1591
1592        let script = r#""executed";"#;
1593        let result = engine.execute_script(script, &ctx, 1000).await;
1594        assert!(result.is_ok());
1595    }
1596}