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