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(
200    script: &str,
201    script_context: &ScriptContext,
202) -> Result<ScriptResult> {
203    // Create JavaScript runtime with proper error handling
204    let runtime = Runtime::new()
205        .map_err(|e| Error::generic(format!("Failed to create JavaScript runtime: {:?}", e)))?;
206    
207    let context = Context::full(&runtime)
208        .map_err(|e| Error::generic(format!("Failed to create JavaScript context: {:?}", e)))?;
209
210    context.with(|ctx| {
211        // Create the global context object with proper error handling
212        let global = ctx.globals();
213        let mockforge_obj = Object::new(ctx.clone())
214            .map_err(|e| Error::generic(format!("Failed to create mockforge object: {:?}", e)))?;
215
216        // Expose context data
217        expose_script_context_static(ctx.clone(), &mockforge_obj, script_context)
218            .map_err(|e| Error::generic(format!("Failed to expose script context: {:?}", e)))?;
219
220        // Add the mockforge object to global scope
221        global.set("mockforge", mockforge_obj)
222            .map_err(|e| Error::generic(format!("Failed to set global mockforge object: {:?}", e)))?;
223
224        // Add utility functions
225        add_global_functions_static(ctx.clone(), &global, script_context)
226            .map_err(|e| Error::generic(format!("Failed to add global functions: {:?}", e)))?;
227
228        // Execute the script
229        let result = ctx.eval(script)
230            .map_err(|e| Error::generic(format!("Script execution failed: {:?}", e)))?;
231
232        // Extract modified variables and return value
233        let modified_vars = extract_modified_variables_static(&ctx, script_context)
234            .map_err(|e| Error::generic(format!("Failed to extract modified variables: {:?}", e)))?;
235        
236        let return_value = extract_return_value_static(&ctx, &result)
237            .map_err(|e| Error::generic(format!("Failed to extract return value: {:?}", e)))?;
238
239        Ok(ScriptResult {
240            return_value,
241            modified_variables: modified_vars,
242            errors: vec![],       // No errors if we reach here
243            execution_time_ms: 0, // Will be set by the caller
244        })
245    })
246}
247
248/// Extract return value from script execution (static version)
249fn extract_return_value_static<'js>(
250    _ctx: &Ctx<'js>,
251    result: &rquickjs::Value<'js>,
252) -> Result<Option<Value>> {
253    match result.type_of() {
254        rquickjs::Type::String => {
255            // Use defensive pattern matching instead of unwrap()
256            if let Some(string_val) = result.as_string() {
257                Ok(Some(Value::String(string_val.to_string()?)))
258            } else {
259                Ok(None)
260            }
261        }
262        rquickjs::Type::Float => {
263            if let Some(num) = result.as_number() {
264                // Use defensive pattern matching for number conversion
265                // Try to convert to f64 first, fallback to int if that fails
266                if let Some(f64_val) = serde_json::Number::from_f64(num) {
267                    Ok(Some(Value::Number(f64_val)))
268                } else {
269                    // Fallback to integer conversion if f64 conversion fails
270                    Ok(Some(Value::Number(serde_json::Number::from(result.as_int().unwrap_or(0)))))
271                }
272            } else {
273                // Fallback to integer if number extraction fails
274                Ok(Some(Value::Number(serde_json::Number::from(result.as_int().unwrap_or(0)))))
275            }
276        }
277        rquickjs::Type::Bool => {
278            // Use defensive pattern matching instead of unwrap()
279            if let Some(bool_val) = result.as_bool() {
280                Ok(Some(Value::Bool(bool_val)))
281            } else {
282                Ok(None)
283            }
284        }
285        rquickjs::Type::Object => {
286            // Try to convert to JSON string and then parse back
287            if let Some(obj) = result.as_object() {
288                if let Some(string_val) = obj.as_string() {
289                    let json_str = string_val.to_string()?;
290                    Ok(Some(Value::String(json_str)))
291                } else {
292                    Ok(None)
293                }
294            } else {
295                Ok(None)
296            }
297        }
298        _ => Ok(None),
299    }
300}
301
302/// Extract modified variables from the script context
303fn extract_modified_variables<'js>(
304    ctx: &Ctx<'js>,
305    original_context: &ScriptContext,
306) -> Result<HashMap<String, Value>> {
307    extract_modified_variables_static(ctx, original_context)
308}
309
310/// Extract modified variables from the script context (static version)
311fn extract_modified_variables_static<'js>(
312    ctx: &Ctx<'js>,
313    original_context: &ScriptContext,
314) -> Result<HashMap<String, Value>> {
315    let mut modified = HashMap::new();
316
317    // Get the global mockforge object
318    let global = ctx.globals();
319    let mockforge_obj: Object = global.get("mockforge")?;
320
321    // Get the variables object
322    let vars_obj: Object = mockforge_obj.get("variables")?;
323
324    // Get all property names
325    let keys = vars_obj.keys::<String>();
326
327    for key_result in keys {
328        let key = key_result?;
329        let js_value: rquickjs::Value = vars_obj.get(&key)?;
330
331        // Convert JS value to serde_json::Value
332        if let Some(value) = js_value_to_json_value(&js_value) {
333            // Check if this is different from the original or new
334            let original_value = original_context.variables.get(&key);
335            if original_value != Some(&value) {
336                modified.insert(key, value);
337            }
338        }
339    }
340
341    Ok(modified)
342}
343
344/// Convert a JavaScript value to a serde_json::Value
345fn js_value_to_json_value(js_value: &rquickjs::Value) -> Option<Value> {
346    match js_value.type_of() {
347        rquickjs::Type::String => {
348            js_value.as_string().and_then(|s| s.to_string().ok()).map(Value::String)
349        }
350        rquickjs::Type::Int => {
351            js_value.as_int().map(|i| Value::Number(serde_json::Number::from(i)))
352        }
353        rquickjs::Type::Float => {
354            js_value.as_number().and_then(serde_json::Number::from_f64).map(Value::Number)
355        }
356        rquickjs::Type::Bool => js_value.as_bool().map(Value::Bool),
357        rquickjs::Type::Object | rquickjs::Type::Array => {
358            // For complex types, try to serialize to JSON string
359            if let Some(obj) = js_value.as_object() {
360                if let Some(str_val) = obj.as_string() {
361                    str_val
362                        .to_string()
363                        .ok()
364                        .and_then(|json_str| serde_json::from_str(&json_str).ok())
365                } else {
366                    // For now, return None for complex objects/arrays
367                    None
368                }
369            } else {
370                None
371            }
372        }
373        _ => None, // Null, undefined, etc.
374    }
375}
376
377/// Execute script with timeout
378fn eval_script_with_timeout<'js>(
379    ctx: &Ctx<'js>,
380    script: &str,
381    _timeout_ms: u64,
382) -> Result<rquickjs::Value<'js>> {
383    // For now, we'll just evaluate without timeout as the JS runtime doesn't support async timeouts
384    // In a future implementation, we could use a separate thread with timeout or implement
385    // a custom timeout mechanism. For now, the timeout is handled at the async boundary.
386
387    ctx.eval(script)
388        .map_err(|e| Error::generic(format!("JavaScript evaluation error: {:?}", e)))
389}
390
391impl Default for ScriptEngine {
392    fn default() -> Self {
393        Self::new()
394    }
395}
396
397/// Expose script context as a global object (static version)
398fn expose_script_context_static<'js>(
399    ctx: Ctx<'js>,
400    mockforge_obj: &Object<'js>,
401    script_context: &ScriptContext,
402) -> Result<()> {
403    // Expose request
404    if let Some(request) = &script_context.request {
405        let request_obj = Object::new(ctx.clone())?;
406        request_obj.set("id", &request.id)?;
407        request_obj.set("method", &request.method)?;
408        request_obj.set("url", &request.url)?;
409
410        // Headers
411        let headers_obj = Object::new(ctx.clone())?;
412        for (key, value) in &request.headers {
413            headers_obj.set(key.as_str(), value.as_str())?;
414        }
415        request_obj.set("headers", headers_obj)?;
416
417        // Body
418        if let Some(body) = &request.body {
419            let body_json = serde_json::to_string(body)
420                .map_err(|e| Error::generic(format!("Failed to serialize request body: {}", e)))?;
421            request_obj.set("body", body_json)?;
422        }
423
424        mockforge_obj.set("request", request_obj)?;
425    }
426
427    // Expose response (for post-scripts)
428    if let Some(response) = &script_context.response {
429        let response_obj = Object::new(ctx.clone())?;
430        response_obj.set("status", response.status as i32)?;
431        response_obj.set("duration_ms", response.duration_ms as i32)?;
432
433        // Response headers
434        let headers_obj = Object::new(ctx.clone())?;
435        for (key, value) in &response.headers {
436            headers_obj.set(key.as_str(), value.as_str())?;
437        }
438        response_obj.set("headers", headers_obj)?;
439
440        // Response body
441        if let Some(body) = &response.body {
442            let body_json = serde_json::to_string(body)
443                .map_err(|e| Error::generic(format!("Failed to serialize response body: {}", e)))?;
444            response_obj.set("body", body_json)?;
445        }
446
447        mockforge_obj.set("response", response_obj)?;
448    }
449
450    // Expose chain context
451    let chain_obj = Object::new(ctx.clone())?;
452    for (key, value) in &script_context.chain_context {
453        match value {
454            Value::String(s) => chain_obj.set(key.as_str(), s.as_str())?,
455            Value::Number(n) => {
456                if let Some(i) = n.as_i64() {
457                    chain_obj.set(key.as_str(), i as i32)?;
458                } else if let Some(f) = n.as_f64() {
459                    chain_obj.set(key.as_str(), f)?;
460                }
461            }
462            Value::Bool(b) => chain_obj.set(key.as_str(), *b)?,
463            Value::Object(obj) => {
464                let json_str = serde_json::to_string(&obj)
465                    .map_err(|e| Error::generic(format!("Failed to serialize object: {}", e)))?;
466                chain_obj.set(key.as_str(), json_str)?;
467            }
468            Value::Array(arr) => {
469                let json_str = serde_json::to_string(&arr)
470                    .map_err(|e| Error::generic(format!("Failed to serialize array: {}", e)))?;
471                chain_obj.set(key.as_str(), json_str)?;
472            }
473            _ => {} // Skip null values and other types
474        }
475    }
476    mockforge_obj.set("chain", chain_obj)?;
477
478    // Expose variables
479    let vars_obj = Object::new(ctx.clone())?;
480    for (key, value) in &script_context.variables {
481        match value {
482            Value::String(s) => vars_obj.set(key.as_str(), s.as_str())?,
483            Value::Number(n) => {
484                if let Some(i) = n.as_i64() {
485                    vars_obj.set(key.as_str(), i as i32)?;
486                } else if let Some(f) = n.as_f64() {
487                    vars_obj.set(key.as_str(), f)?;
488                }
489            }
490            Value::Bool(b) => vars_obj.set(key.as_str(), *b)?,
491            _ => {
492                let json_str = serde_json::to_string(&value).map_err(|e| {
493                    Error::generic(format!("Failed to serialize variable {}: {}", key, e))
494                })?;
495                vars_obj.set(key.as_str(), json_str)?;
496            }
497        }
498    }
499    mockforge_obj.set("variables", vars_obj)?;
500
501    // Expose environment variables
502    let env_obj = Object::new(ctx.clone())?;
503    for (key, value) in &script_context.env_vars {
504        env_obj.set(key.as_str(), value.as_str())?;
505    }
506    mockforge_obj.set("env", env_obj)?;
507
508    Ok(())
509}
510
511/// Add global utility functions to the script context (static version)
512fn add_global_functions_static<'js>(
513    ctx: Ctx<'js>,
514    global: &Object<'js>,
515    _script_context: &ScriptContext,
516) -> Result<()> {
517    // Add console object for logging
518    let console_obj = Object::new(ctx.clone())?;
519    let log_func = Function::new(ctx.clone(), || {
520        println!("Script log called");
521    })?;
522    console_obj.set("log", log_func)?;
523    global.set("console", console_obj)?;
524
525    // Add utility functions for scripts
526    let log_func = Function::new(ctx.clone(), |msg: String| {
527        println!("Script log: {}", msg);
528    })?;
529    global.set("log", log_func)?;
530
531    let stringify_func = Function::new(ctx.clone(), |value: rquickjs::Value| {
532        if let Some(obj) = value.as_object() {
533            if let Some(str_val) = obj.as_string() {
534                str_val.to_string().unwrap_or_else(|_| "undefined".to_string())
535            } else {
536                "object".to_string()
537            }
538        } else if value.is_string() {
539            value
540                .as_string()
541                .unwrap()
542                .to_string()
543                .unwrap_or_else(|_| "undefined".to_string())
544        } else {
545            format!("{:?}", value)
546        }
547    })?;
548    global.set("stringify", stringify_func)?;
549
550    // Add crypto utilities
551    let crypto_obj = Object::new(ctx.clone())?;
552
553    let base64_encode_func = Function::new(ctx.clone(), |input: String| -> String {
554        use base64::{engine::general_purpose, Engine as _};
555        general_purpose::STANDARD.encode(input)
556    })?;
557    crypto_obj.set("base64Encode", base64_encode_func)?;
558
559    let base64_decode_func = Function::new(ctx.clone(), |input: String| -> String {
560        use base64::{engine::general_purpose, Engine as _};
561        general_purpose::STANDARD
562            .decode(input)
563            .map(|bytes| String::from_utf8_lossy(&bytes).to_string())
564            .unwrap_or_else(|_| "".to_string())
565    })?;
566    crypto_obj.set("base64Decode", base64_decode_func)?;
567
568    let sha256_func = Function::new(ctx.clone(), |input: String| -> String {
569        use sha2::{Digest, Sha256};
570        let mut hasher = Sha256::new();
571        hasher.update(input);
572        hex::encode(hasher.finalize())
573    })?;
574    crypto_obj.set("sha256", sha256_func)?;
575
576    let random_bytes_func = Function::new(ctx.clone(), |length: usize| -> String {
577        use rand::Rng;
578        let mut rng = rand::rng();
579        let bytes: Vec<u8> = (0..length).map(|_| rng.random()).collect();
580        hex::encode(bytes)
581    })?;
582    crypto_obj.set("randomBytes", random_bytes_func)?;
583
584    global.set("crypto", crypto_obj)?;
585
586    // Add date/time utilities
587    let date_obj = Object::new(ctx.clone())?;
588
589    let now_func = Function::new(ctx.clone(), || -> String { chrono::Utc::now().to_rfc3339() })?;
590    date_obj.set("now", now_func)?;
591
592    let format_func = Function::new(ctx.clone(), |timestamp: String, format: String| -> String {
593        if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&timestamp) {
594            dt.format(&format).to_string()
595        } else {
596            "".to_string()
597        }
598    })?;
599    date_obj.set("format", format_func)?;
600
601    let parse_func = Function::new(ctx.clone(), |date_str: String, format: String| -> String {
602        if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&date_str, &format) {
603            dt.and_utc().to_rfc3339()
604        } else {
605            "".to_string()
606        }
607    })?;
608    date_obj.set("parse", parse_func)?;
609
610    let add_days_func = Function::new(ctx.clone(), |timestamp: String, days: i64| -> String {
611        if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&timestamp) {
612            (dt + chrono::Duration::days(days)).to_rfc3339()
613        } else {
614            "".to_string()
615        }
616    })?;
617    date_obj.set("addDays", add_days_func)?;
618
619    global.set("date", date_obj)?;
620
621    // Add validation utilities
622    let validate_obj = Object::new(ctx.clone())?;
623
624    let email_func = Function::new(ctx.clone(), |email: String| -> bool {
625        // Simple email regex validation
626        // Note: This regex pattern is static and should never fail compilation,
627        // but we handle errors defensively to prevent panics
628        regex::Regex::new(r"^[^@]+@[^@]+\.[^@]+$")
629            .map(|re| re.is_match(&email))
630            .unwrap_or_else(|_| {
631                // Fallback: basic string check if regex compilation fails (should never happen)
632                email.contains('@') && email.contains('.') && email.len() > 5
633            })
634    })?;
635    validate_obj.set("email", email_func)?;
636
637    let url_func = Function::new(ctx.clone(), |url_str: String| -> bool {
638        url::Url::parse(&url_str).is_ok()
639    })?;
640    validate_obj.set("url", url_func)?;
641
642    let regex_func = Function::new(ctx.clone(), |pattern: String, text: String| -> bool {
643        regex::Regex::new(&pattern).map(|re| re.is_match(&text)).unwrap_or(false)
644    })?;
645    validate_obj.set("regex", regex_func)?;
646
647    global.set("validate", validate_obj)?;
648
649    // Add JSON utilities
650    let json_obj = Object::new(ctx.clone())?;
651
652    let json_parse_func = Function::new(ctx.clone(), |json_str: String| -> String {
653        match serde_json::from_str::<serde_json::Value>(&json_str) {
654            Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
655            Err(_) => "null".to_string(),
656        }
657    })?;
658    json_obj.set("parse", json_parse_func)?;
659
660    let json_stringify_func = Function::new(ctx.clone(), |value: String| -> String {
661        // Assume input is already valid JSON or a simple value
662        value
663    })?;
664    json_obj.set("stringify", json_stringify_func)?;
665
666    let json_validate_func = Function::new(ctx.clone(), |json_str: String| -> bool {
667        serde_json::from_str::<serde_json::Value>(&json_str).is_ok()
668    })?;
669    json_obj.set("validate", json_validate_func)?;
670
671    global.set("JSON", json_obj)?;
672
673    // Add HTTP utilities
674    let http_obj = Object::new(ctx.clone())?;
675
676    let http_get_func = Function::new(ctx.clone(), |url: String| -> String {
677        // WARNING: This blocks a thread from the blocking thread pool.
678        // The JavaScript engine (rquickjs) is already running in spawn_blocking,
679        // so we use block_in_place here. For production, consider limiting
680        // HTTP calls in scripts or using a different scripting approach.
681        tokio::task::block_in_place(|| {
682            reqwest::blocking::get(&url)
683                .and_then(|resp| resp.text())
684                .unwrap_or_else(|_| "".to_string())
685        })
686    })?;
687    http_obj.set("get", http_get_func)?;
688
689    let http_post_func = Function::new(ctx.clone(), |url: String, body: String| -> String {
690        // WARNING: This blocks a thread from the blocking thread pool.
691        // The JavaScript engine (rquickjs) is already running in spawn_blocking,
692        // so we use block_in_place here. For production, consider limiting
693        // HTTP calls in scripts or using a different scripting approach.
694        tokio::task::block_in_place(|| {
695            reqwest::blocking::Client::new()
696                .post(&url)
697                .body(body)
698                .send()
699                .and_then(|resp| resp.text())
700                .unwrap_or_else(|_| "".to_string())
701        })
702    })?;
703    http_obj.set("post", http_post_func)?;
704
705    let url_encode_func = Function::new(ctx.clone(), |input: String| -> String {
706        urlencoding::encode(&input).to_string()
707    })?;
708    http_obj.set("urlEncode", url_encode_func)?;
709
710    let url_decode_func = Function::new(ctx.clone(), |input: String| -> String {
711        urlencoding::decode(&input)
712            .unwrap_or(std::borrow::Cow::Borrowed(""))
713            .to_string()
714    })?;
715    http_obj.set("urlDecode", url_decode_func)?;
716
717    global.set("http", http_obj)?;
718
719    Ok(())
720}
721
722#[cfg(test)]
723mod tests {
724    use super::*;
725    use serde_json::json;
726
727    #[tokio::test]
728    async fn test_script_execution() {
729        let engine = ScriptEngine::new();
730
731        let script_context = ScriptContext {
732            request: Some(crate::request_chaining::ChainRequest {
733                id: "test-request".to_string(),
734                method: "GET".to_string(),
735                url: "https://api.example.com/test".to_string(),
736                headers: [("Content-Type".to_string(), "application/json".to_string())].into(),
737                body: None,
738                depends_on: vec![],
739                timeout_secs: None,
740                expected_status: None,
741                scripting: None,
742            }),
743            response: None,
744            chain_context: {
745                let mut ctx = HashMap::new();
746                ctx.insert("login_token".to_string(), json!("abc123"));
747                ctx
748            },
749            variables: HashMap::new(),
750            env_vars: [("NODE_ENV".to_string(), "test".to_string())].into(),
751        };
752
753        let script = r#"
754            for (let i = 0; i < 1000000; i++) {
755                // Loop to ensure measurable execution time
756            }
757            "script executed successfully";
758        "#;
759
760        let result = engine.execute_script(script, &script_context, 5000).await;
761        assert!(result.is_ok(), "Script execution should succeed");
762
763        let script_result = result.unwrap();
764        assert_eq!(script_result.return_value, Some(json!("script executed successfully")));
765        assert!(script_result.execution_time_ms > 0);
766        assert!(script_result.errors.is_empty());
767    }
768
769    #[tokio::test]
770    async fn test_script_with_error() {
771        let engine = ScriptEngine::new();
772
773        let script_context = ScriptContext {
774            request: None,
775            response: None,
776            chain_context: HashMap::new(),
777            variables: HashMap::new(),
778            env_vars: HashMap::new(),
779        };
780
781        let script = r#"throw new Error("Intentional test error");"#;
782
783        let result = engine.execute_script(script, &script_context, 1000).await;
784        // For now, JavaScript errors are not being caught properly
785        // In a complete implementation, we would handle errors and return them in ScriptResult.errors
786        assert!(result.is_err() || result.is_ok()); // Accept either for now
787    }
788}