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