use crate::{Error, Result};
use rquickjs::{Context, Ctx, Function, Object, Runtime};
use tracing::debug;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Semaphore;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptResult {
pub return_value: Option<Value>,
pub modified_variables: HashMap<String, Value>,
pub errors: Vec<String>,
pub execution_time_ms: u64,
}
#[derive(Debug, Clone)]
pub struct ScriptContext {
pub request: Option<crate::request_chaining::ChainRequest>,
pub response: Option<crate::request_chaining::ChainResponse>,
pub chain_context: HashMap<String, Value>,
pub variables: HashMap<String, Value>,
pub env_vars: HashMap<String, String>,
}
pub struct ScriptEngine {
semaphore: Arc<Semaphore>,
}
impl std::fmt::Debug for ScriptEngine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ScriptEngine")
.field("semaphore", &format!("Semaphore({})", self.semaphore.available_permits()))
.finish()
}
}
impl ScriptEngine {
pub fn new() -> Self {
let semaphore = Arc::new(Semaphore::new(10));
Self { semaphore }
}
pub async fn execute_script(
&self,
script: &str,
script_context: &ScriptContext,
timeout_ms: u64,
) -> Result<ScriptResult> {
let _permit =
self.semaphore.acquire().await.map_err(|e| {
Error::internal(format!("Failed to acquire execution permit: {}", e))
})?;
let script = script.to_string();
let script_context = script_context.clone();
let start_time = std::time::Instant::now();
let script_clone = script.clone();
let script_context_clone = script_context.clone();
let timeout_duration = std::time::Duration::from_millis(timeout_ms);
let timeout_result = tokio::time::timeout(
timeout_duration,
tokio::task::spawn_blocking(move || {
execute_script_in_runtime(&script_clone, &script_context_clone)
}),
)
.await;
let execution_time_ms = start_time.elapsed().as_millis() as u64;
match timeout_result {
Ok(join_result) => match join_result {
Ok(Ok(mut script_result)) => {
script_result.execution_time_ms = execution_time_ms;
Ok(script_result)
}
Ok(Err(e)) => Err(e),
Err(e) => Err(Error::internal(format!("Script execution task failed: {}", e))),
},
Err(_) => {
Err(Error::internal(format!("Script execution timed out after {}ms", timeout_ms)))
}
}
}
}
fn execute_script_in_runtime(script: &str, script_context: &ScriptContext) -> Result<ScriptResult> {
let runtime = Runtime::new()
.map_err(|e| Error::internal(format!("Failed to create JavaScript runtime: {:?}", e)))?;
let context = Context::full(&runtime)
.map_err(|e| Error::internal(format!("Failed to create JavaScript context: {:?}", e)))?;
context.with(|ctx| {
let global = ctx.globals();
let mockforge_obj = Object::new(ctx.clone())
.map_err(|e| Error::internal(format!("Failed to create mockforge object: {:?}", e)))?;
expose_script_context(ctx.clone(), &mockforge_obj, script_context)
.map_err(|e| Error::internal(format!("Failed to expose script context: {:?}", e)))?;
global.set("mockforge", mockforge_obj).map_err(|e| {
Error::internal(format!("Failed to set global mockforge object: {:?}", e))
})?;
add_global_functions(ctx.clone(), &global, script_context)
.map_err(|e| Error::internal(format!("Failed to add global functions: {:?}", e)))?;
let result = ctx
.eval(script)
.map_err(|e| Error::internal(format!("Script execution failed: {:?}", e)))?;
let modified_vars = extract_modified_variables(&ctx, script_context).map_err(|e| {
Error::internal(format!("Failed to extract modified variables: {:?}", e))
})?;
let return_value = extract_return_value(&ctx, &result)
.map_err(|e| Error::internal(format!("Failed to extract return value: {:?}", e)))?;
Ok(ScriptResult {
return_value,
modified_variables: modified_vars,
errors: vec![], execution_time_ms: 0, })
})
}
fn extract_return_value<'js>(
_ctx: &Ctx<'js>,
result: &rquickjs::Value<'js>,
) -> Result<Option<Value>> {
match result.type_of() {
rquickjs::Type::String => {
if let Some(string_val) = result.as_string() {
Ok(Some(Value::String(string_val.to_string()?)))
} else {
Ok(None)
}
}
rquickjs::Type::Float => {
if let Some(num) = result.as_number() {
if let Some(f64_val) = serde_json::Number::from_f64(num) {
Ok(Some(Value::Number(f64_val)))
} else {
Ok(Some(Value::Number(serde_json::Number::from(result.as_int().unwrap_or(0)))))
}
} else {
Ok(Some(Value::Number(serde_json::Number::from(result.as_int().unwrap_or(0)))))
}
}
rquickjs::Type::Bool => {
if let Some(bool_val) = result.as_bool() {
Ok(Some(Value::Bool(bool_val)))
} else {
Ok(None)
}
}
rquickjs::Type::Object => {
if let Some(obj) = result.as_object() {
if let Some(string_val) = obj.as_string() {
let json_str = string_val.to_string()?;
Ok(Some(Value::String(json_str)))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
_ => Ok(None),
}
}
fn extract_modified_variables<'js>(
ctx: &Ctx<'js>,
original_context: &ScriptContext,
) -> Result<HashMap<String, Value>> {
let mut modified = HashMap::new();
let global = ctx.globals();
let mockforge_obj: Object = global.get("mockforge")?;
let vars_obj: Object = mockforge_obj.get("variables")?;
let keys = vars_obj.keys::<String>();
for key_result in keys {
let key = key_result?;
let js_value: rquickjs::Value = vars_obj.get(&key)?;
if let Some(value) = js_value_to_json_value(&js_value) {
let original_value = original_context.variables.get(&key);
if original_value != Some(&value) {
modified.insert(key, value);
}
}
}
Ok(modified)
}
fn js_value_to_json_value(js_value: &rquickjs::Value) -> Option<Value> {
match js_value.type_of() {
rquickjs::Type::String => {
js_value.as_string().and_then(|s| s.to_string().ok()).map(Value::String)
}
rquickjs::Type::Int => {
js_value.as_int().map(|i| Value::Number(serde_json::Number::from(i)))
}
rquickjs::Type::Float => {
js_value.as_number().and_then(serde_json::Number::from_f64).map(Value::Number)
}
rquickjs::Type::Bool => js_value.as_bool().map(Value::Bool),
rquickjs::Type::Object | rquickjs::Type::Array => {
if let Some(obj) = js_value.as_object() {
if let Some(str_val) = obj.as_string() {
str_val
.to_string()
.ok()
.and_then(|json_str| serde_json::from_str(&json_str).ok())
} else {
None
}
} else {
None
}
}
_ => None, }
}
impl Default for ScriptEngine {
fn default() -> Self {
Self::new()
}
}
fn expose_script_context<'js>(
ctx: Ctx<'js>,
mockforge_obj: &Object<'js>,
script_context: &ScriptContext,
) -> Result<()> {
if let Some(request) = &script_context.request {
let request_obj = Object::new(ctx.clone())?;
request_obj.set("id", &request.id)?;
request_obj.set("method", &request.method)?;
request_obj.set("url", &request.url)?;
let headers_obj = Object::new(ctx.clone())?;
for (key, value) in &request.headers {
headers_obj.set(key.as_str(), value.as_str())?;
}
request_obj.set("headers", headers_obj)?;
if let Some(body) = &request.body {
let body_json = serde_json::to_string(body)
.map_err(|e| Error::internal(format!("Failed to serialize request body: {}", e)))?;
request_obj.set("body", body_json)?;
}
mockforge_obj.set("request", request_obj)?;
}
if let Some(response) = &script_context.response {
let response_obj = Object::new(ctx.clone())?;
response_obj.set("status", response.status as i32)?;
response_obj.set("duration_ms", response.duration_ms as i32)?;
let headers_obj = Object::new(ctx.clone())?;
for (key, value) in &response.headers {
headers_obj.set(key.as_str(), value.as_str())?;
}
response_obj.set("headers", headers_obj)?;
if let Some(body) = &response.body {
let body_json = serde_json::to_string(body).map_err(|e| {
Error::internal(format!("Failed to serialize response body: {}", e))
})?;
response_obj.set("body", body_json)?;
}
mockforge_obj.set("response", response_obj)?;
}
let chain_obj = Object::new(ctx.clone())?;
for (key, value) in &script_context.chain_context {
match value {
Value::String(s) => chain_obj.set(key.as_str(), s.as_str())?,
Value::Number(n) => {
if let Some(i) = n.as_i64() {
chain_obj.set(key.as_str(), i as i32)?;
} else if let Some(f) = n.as_f64() {
chain_obj.set(key.as_str(), f)?;
}
}
Value::Bool(b) => chain_obj.set(key.as_str(), *b)?,
Value::Object(obj) => {
let json_str = serde_json::to_string(&obj)
.map_err(|e| Error::internal(format!("Failed to serialize object: {}", e)))?;
chain_obj.set(key.as_str(), json_str)?;
}
Value::Array(arr) => {
let json_str = serde_json::to_string(&arr)
.map_err(|e| Error::internal(format!("Failed to serialize array: {}", e)))?;
chain_obj.set(key.as_str(), json_str)?;
}
_ => {} }
}
mockforge_obj.set("chain", chain_obj)?;
let vars_obj = Object::new(ctx.clone())?;
for (key, value) in &script_context.variables {
match value {
Value::String(s) => vars_obj.set(key.as_str(), s.as_str())?,
Value::Number(n) => {
if let Some(i) = n.as_i64() {
vars_obj.set(key.as_str(), i as i32)?;
} else if let Some(f) = n.as_f64() {
vars_obj.set(key.as_str(), f)?;
}
}
Value::Bool(b) => vars_obj.set(key.as_str(), *b)?,
_ => {
let json_str = serde_json::to_string(&value).map_err(|e| {
Error::internal(format!("Failed to serialize variable {}: {}", key, e))
})?;
vars_obj.set(key.as_str(), json_str)?;
}
}
}
mockforge_obj.set("variables", vars_obj)?;
let env_obj = Object::new(ctx.clone())?;
for (key, value) in &script_context.env_vars {
env_obj.set(key.as_str(), value.as_str())?;
}
mockforge_obj.set("env", env_obj)?;
Ok(())
}
fn add_global_functions<'js>(
ctx: Ctx<'js>,
global: &Object<'js>,
_script_context: &ScriptContext,
) -> Result<()> {
let console_obj = Object::new(ctx.clone())?;
let log_func = Function::new(ctx.clone(), || {
debug!("Script log called");
})?;
console_obj.set("log", log_func)?;
global.set("console", console_obj)?;
let log_func = Function::new(ctx.clone(), |msg: String| {
debug!("Script log: {}", msg);
})?;
global.set("log", log_func)?;
let stringify_func = Function::new(ctx.clone(), |value: rquickjs::Value| {
if let Some(obj) = value.as_object() {
if let Some(str_val) = obj.as_string() {
str_val.to_string().unwrap_or_else(|_| "undefined".to_string())
} else {
"object".to_string()
}
} else if value.is_string() {
value
.as_string()
.unwrap()
.to_string()
.unwrap_or_else(|_| "undefined".to_string())
} else {
format!("{:?}", value)
}
})?;
global.set("stringify", stringify_func)?;
let crypto_obj = Object::new(ctx.clone())?;
let base64_encode_func = Function::new(ctx.clone(), |input: String| -> String {
use base64::{engine::general_purpose, Engine as _};
general_purpose::STANDARD.encode(input)
})?;
crypto_obj.set("base64Encode", base64_encode_func)?;
let base64_decode_func = Function::new(ctx.clone(), |input: String| -> String {
use base64::{engine::general_purpose, Engine as _};
general_purpose::STANDARD
.decode(input)
.map(|bytes| String::from_utf8_lossy(&bytes).to_string())
.unwrap_or_else(|_| "".to_string())
})?;
crypto_obj.set("base64Decode", base64_decode_func)?;
let sha256_func = Function::new(ctx.clone(), |input: String| -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(input);
hex::encode(hasher.finalize())
})?;
crypto_obj.set("sha256", sha256_func)?;
let random_bytes_func = Function::new(ctx.clone(), |length: usize| -> String {
use rand::Rng;
let mut rng = rand::thread_rng();
let bytes: Vec<u8> = (0..length).map(|_| rng.random()).collect();
hex::encode(bytes)
})?;
crypto_obj.set("randomBytes", random_bytes_func)?;
global.set("crypto", crypto_obj)?;
let date_obj = Object::new(ctx.clone())?;
let now_func = Function::new(ctx.clone(), || -> String { chrono::Utc::now().to_rfc3339() })?;
date_obj.set("now", now_func)?;
let format_func = Function::new(ctx.clone(), |timestamp: String, format: String| -> String {
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(×tamp) {
dt.format(&format).to_string()
} else {
"".to_string()
}
})?;
date_obj.set("format", format_func)?;
let parse_func = Function::new(ctx.clone(), |date_str: String, format: String| -> String {
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&date_str, &format) {
dt.and_utc().to_rfc3339()
} else {
"".to_string()
}
})?;
date_obj.set("parse", parse_func)?;
let add_days_func = Function::new(ctx.clone(), |timestamp: String, days: i64| -> String {
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(×tamp) {
(dt + chrono::Duration::days(days)).to_rfc3339()
} else {
"".to_string()
}
})?;
date_obj.set("addDays", add_days_func)?;
global.set("date", date_obj)?;
let validate_obj = Object::new(ctx.clone())?;
let email_func = Function::new(ctx.clone(), |email: String| -> bool {
regex::Regex::new(r"^[^@]+@[^@]+\.[^@]+$")
.map(|re| re.is_match(&email))
.unwrap_or_else(|_| {
email.contains('@') && email.contains('.') && email.len() > 5
})
})?;
validate_obj.set("email", email_func)?;
let url_func = Function::new(ctx.clone(), |url_str: String| -> bool {
url::Url::parse(&url_str).is_ok()
})?;
validate_obj.set("url", url_func)?;
let regex_func = Function::new(ctx.clone(), |pattern: String, text: String| -> bool {
regex::Regex::new(&pattern).map(|re| re.is_match(&text)).unwrap_or(false)
})?;
validate_obj.set("regex", regex_func)?;
global.set("validate", validate_obj)?;
let json_obj = Object::new(ctx.clone())?;
let json_parse_func = Function::new(ctx.clone(), |json_str: String| -> String {
match serde_json::from_str::<Value>(&json_str) {
Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
Err(_) => "null".to_string(),
}
})?;
json_obj.set("parse", json_parse_func)?;
let json_stringify_func = Function::new(ctx.clone(), |value: String| -> String {
value
})?;
json_obj.set("stringify", json_stringify_func)?;
let json_validate_func = Function::new(ctx.clone(), |json_str: String| -> bool {
serde_json::from_str::<Value>(&json_str).is_ok()
})?;
json_obj.set("validate", json_validate_func)?;
global.set("JSON", json_obj)?;
let http_obj = Object::new(ctx.clone())?;
let http_get_func = Function::new(ctx.clone(), |url: String| -> String {
tokio::task::block_in_place(|| {
reqwest::blocking::get(&url)
.and_then(|resp| resp.text())
.unwrap_or_else(|_| "".to_string())
})
})?;
http_obj.set("get", http_get_func)?;
let http_post_func = Function::new(ctx.clone(), |url: String, body: String| -> String {
tokio::task::block_in_place(|| {
reqwest::blocking::Client::new()
.post(&url)
.body(body)
.send()
.and_then(|resp| resp.text())
.unwrap_or_else(|_| "".to_string())
})
})?;
http_obj.set("post", http_post_func)?;
let url_encode_func = Function::new(ctx.clone(), |input: String| -> String {
urlencoding::encode(&input).to_string()
})?;
http_obj.set("urlEncode", url_encode_func)?;
let url_decode_func = Function::new(ctx.clone(), |input: String| -> String {
urlencoding::decode(&input)
.unwrap_or(std::borrow::Cow::Borrowed(""))
.to_string()
})?;
http_obj.set("urlDecode", url_decode_func)?;
global.set("http", http_obj)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn create_empty_script_context() -> ScriptContext {
ScriptContext {
request: None,
response: None,
chain_context: HashMap::new(),
variables: HashMap::new(),
env_vars: HashMap::new(),
}
}
fn create_full_script_context() -> ScriptContext {
ScriptContext {
request: Some(crate::request_chaining::ChainRequest {
id: "test-request".to_string(),
method: "GET".to_string(),
url: "https://api.example.com/test".to_string(),
headers: [("Content-Type".to_string(), "application/json".to_string())].into(),
body: Some(crate::request_chaining::RequestBody::Json(json!({"key": "value"}))),
depends_on: vec![],
timeout_secs: Some(30),
expected_status: Some(vec![200]),
scripting: None,
}),
response: Some(crate::request_chaining::ChainResponse {
status: 200,
headers: [("Content-Type".to_string(), "application/json".to_string())].into(),
body: Some(json!({"result": "success"})),
duration_ms: 150,
executed_at: chrono::Utc::now().to_rfc3339(),
error: None,
}),
chain_context: {
let mut ctx = HashMap::new();
ctx.insert("login_token".to_string(), json!("abc123"));
ctx.insert("user_id".to_string(), json!(42));
ctx.insert("is_admin".to_string(), json!(true));
ctx.insert("items".to_string(), json!(["a", "b", "c"]));
ctx.insert("config".to_string(), json!({"timeout": 30}));
ctx
},
variables: {
let mut vars = HashMap::new();
vars.insert("counter".to_string(), json!(0));
vars.insert("name".to_string(), json!("test"));
vars
},
env_vars: [
("NODE_ENV".to_string(), "test".to_string()),
("API_KEY".to_string(), "secret123".to_string()),
]
.into(),
}
}
#[test]
fn test_script_result_clone() {
let result = ScriptResult {
return_value: Some(json!("test")),
modified_variables: {
let mut vars = HashMap::new();
vars.insert("key".to_string(), json!("value"));
vars
},
errors: vec!["error1".to_string()],
execution_time_ms: 100,
};
let cloned = result.clone();
assert_eq!(cloned.return_value, result.return_value);
assert_eq!(cloned.modified_variables, result.modified_variables);
assert_eq!(cloned.errors, result.errors);
assert_eq!(cloned.execution_time_ms, result.execution_time_ms);
}
#[test]
fn test_script_result_debug() {
let result = ScriptResult {
return_value: Some(json!("test")),
modified_variables: HashMap::new(),
errors: vec![],
execution_time_ms: 50,
};
let debug = format!("{:?}", result);
assert!(debug.contains("ScriptResult"));
assert!(debug.contains("return_value"));
}
#[test]
fn test_script_result_serialize() {
let result = ScriptResult {
return_value: Some(json!("test")),
modified_variables: HashMap::new(),
errors: vec![],
execution_time_ms: 50,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("return_value"));
assert!(json.contains("execution_time_ms"));
}
#[test]
fn test_script_result_deserialize() {
let json =
r#"{"return_value":"test","modified_variables":{},"errors":[],"execution_time_ms":50}"#;
let result: ScriptResult = serde_json::from_str(json).unwrap();
assert_eq!(result.return_value, Some(json!("test")));
assert_eq!(result.execution_time_ms, 50);
}
#[test]
fn test_script_context_clone() {
let ctx = create_full_script_context();
let cloned = ctx.clone();
assert_eq!(cloned.request.is_some(), ctx.request.is_some());
assert_eq!(cloned.response.is_some(), ctx.response.is_some());
assert_eq!(cloned.chain_context.len(), ctx.chain_context.len());
assert_eq!(cloned.variables.len(), ctx.variables.len());
assert_eq!(cloned.env_vars.len(), ctx.env_vars.len());
}
#[test]
fn test_script_context_debug() {
let ctx = create_empty_script_context();
let debug = format!("{:?}", ctx);
assert!(debug.contains("ScriptContext"));
}
#[test]
fn test_script_engine_new() {
let engine = ScriptEngine::new();
let debug = format!("{:?}", engine);
assert!(debug.contains("ScriptEngine"));
assert!(debug.contains("Semaphore"));
}
#[test]
fn test_script_engine_default() {
let engine = ScriptEngine::default();
let debug = format!("{:?}", engine);
assert!(debug.contains("ScriptEngine"));
}
#[test]
fn test_script_engine_debug() {
let engine = ScriptEngine::new();
let debug = format!("{:?}", engine);
assert!(debug.contains("ScriptEngine"));
assert!(debug.contains("10")); }
#[tokio::test]
async fn test_script_execution() {
let engine = ScriptEngine::new();
let script_context = ScriptContext {
request: Some(crate::request_chaining::ChainRequest {
id: "test-request".to_string(),
method: "GET".to_string(),
url: "https://api.example.com/test".to_string(),
headers: [("Content-Type".to_string(), "application/json".to_string())].into(),
body: None,
depends_on: vec![],
timeout_secs: None,
expected_status: None,
scripting: None,
}),
response: None,
chain_context: {
let mut ctx = HashMap::new();
ctx.insert("login_token".to_string(), json!("abc123"));
ctx
},
variables: HashMap::new(),
env_vars: [("NODE_ENV".to_string(), "test".to_string())].into(),
};
let script = r#"
for (let i = 0; i < 1000000; i++) {
// Loop to ensure measurable execution time
}
"script executed successfully";
"#;
let result = engine.execute_script(script, &script_context, 5000).await;
assert!(result.is_ok(), "Script execution should succeed");
let script_result = result.unwrap();
assert_eq!(script_result.return_value, Some(json!("script executed successfully")));
assert!(script_result.execution_time_ms > 0);
assert!(script_result.errors.is_empty());
}
#[tokio::test]
async fn test_script_with_error() {
let engine = ScriptEngine::new();
let script_context = ScriptContext {
request: None,
response: None,
chain_context: HashMap::new(),
variables: HashMap::new(),
env_vars: HashMap::new(),
};
let script = r#"throw new Error("Intentional test error");"#;
let result = engine.execute_script(script, &script_context, 1000).await;
assert!(result.is_ok() || result.is_err(), "script execution should not panic");
}
#[tokio::test]
async fn test_simple_script_string_return() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let result = engine.execute_script(r#""hello world""#, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("hello world")));
}
#[tokio::test]
async fn test_simple_script_number_return() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let result = engine.execute_script("42", &ctx, 1000).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_simple_script_boolean_return() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let result = engine.execute_script("true", &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!(true)));
}
#[tokio::test]
async fn test_script_timeout() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"
let count = 0;
while (count < 100000000) {
count++;
}
count;
"#;
let result = engine.execute_script(script, &ctx, 10).await;
assert!(result.is_ok() || result.is_err());
}
#[tokio::test]
async fn test_script_with_request_context() {
let engine = ScriptEngine::new();
let ctx = create_full_script_context();
let script = r#"
mockforge.request.method;
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("GET")));
}
#[tokio::test]
async fn test_script_with_response_context() {
let engine = ScriptEngine::new();
let ctx = create_full_script_context();
let script = r#"
mockforge.response.status;
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_script_with_chain_context() {
let engine = ScriptEngine::new();
let ctx = create_full_script_context();
let script = r#"
mockforge.chain.login_token;
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("abc123")));
}
#[tokio::test]
async fn test_script_with_env_vars() {
let engine = ScriptEngine::new();
let ctx = create_full_script_context();
let script = r#"
mockforge.env.NODE_ENV;
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("test")));
}
#[tokio::test]
async fn test_script_modify_variables() {
let engine = ScriptEngine::new();
let mut ctx = create_empty_script_context();
ctx.variables.insert("counter".to_string(), json!(0));
let script = r#"
mockforge.variables.counter = 10;
mockforge.variables.new_var = "created";
mockforge.variables.counter;
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
let script_result = result.unwrap();
assert!(
script_result.modified_variables.contains_key("counter")
|| script_result.modified_variables.contains_key("new_var")
);
}
#[tokio::test]
async fn test_script_console_log() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"
console.log("test message");
"logged";
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_script_log_function() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"
log("test log");
"logged";
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_script_crypto_base64() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"
crypto.base64Encode("hello");
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("aGVsbG8=")));
}
#[tokio::test]
async fn test_script_crypto_base64_decode() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"
crypto.base64Decode("aGVsbG8=");
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("hello")));
}
#[tokio::test]
async fn test_script_crypto_sha256() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"
crypto.sha256("hello");
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
let return_val = result.unwrap().return_value;
assert!(return_val.is_some());
let hash = return_val.unwrap();
assert!(hash.as_str().unwrap().len() == 64); }
#[tokio::test]
async fn test_script_crypto_random_bytes() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"
crypto.randomBytes(16);
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
let return_val = result.unwrap().return_value;
assert!(return_val.is_some());
let hex = return_val.unwrap();
assert!(hex.as_str().unwrap().len() == 32); }
#[tokio::test]
async fn test_script_date_now() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"
date.now();
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
let return_val = result.unwrap().return_value;
assert!(return_val.is_some());
let timestamp = return_val.unwrap();
assert!(timestamp.as_str().unwrap().contains("T"));
}
#[tokio::test]
async fn test_script_date_add_days() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"
date.addDays("2024-01-01T00:00:00+00:00", 1);
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
let return_val = result.unwrap().return_value;
assert!(return_val.is_some());
let new_date = return_val.unwrap();
assert!(new_date.as_str().unwrap().contains("2024-01-02"));
}
#[tokio::test]
async fn test_script_validate_email() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"validate.email("test@example.com");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!(true)));
let script = r#"validate.email("not-an-email");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!(false)));
}
#[tokio::test]
async fn test_script_validate_url() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"validate.url("https://example.com");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!(true)));
let script = r#"validate.url("not-a-url");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!(false)));
}
#[tokio::test]
async fn test_script_validate_regex() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"validate.regex("^hello", "hello world");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!(true)));
let script = r#"validate.regex("^world", "hello world");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!(false)));
}
#[tokio::test]
async fn test_script_json_parse() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"JSON.parse('{"key": "value"}');"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_script_json_validate() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"JSON.validate('{"key": "value"}');"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!(true)));
let script = r#"JSON.validate('not json');"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!(false)));
}
#[tokio::test]
async fn test_script_http_url_encode() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"http.urlEncode("hello world");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("hello%20world")));
}
#[tokio::test]
async fn test_script_http_url_decode() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"http.urlDecode("hello%20world");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("hello world")));
}
#[tokio::test]
async fn test_script_with_syntax_error() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"function { broken"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_execute_simple_string() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let result = engine.execute_script(r#""test""#, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("test")));
}
#[tokio::test]
async fn test_script_with_no_request() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#""no request needed""#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_script_with_no_response() {
let engine = ScriptEngine::new();
let mut ctx = create_empty_script_context();
ctx.request = Some(crate::request_chaining::ChainRequest {
id: "test".to_string(),
method: "GET".to_string(),
url: "http://example.com".to_string(),
headers: HashMap::new(),
body: None,
depends_on: vec![],
timeout_secs: None,
expected_status: None,
scripting: None,
});
let script = r#"mockforge.request.method"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_concurrent_script_execution() {
let engine = Arc::new(ScriptEngine::new());
let ctx = create_empty_script_context();
let mut handles = vec![];
for i in 0..5 {
let engine = engine.clone();
let ctx = ctx.clone();
let handle = tokio::spawn(async move {
let script = format!("{}", i);
engine.execute_script(&script, &ctx, 1000).await
});
handles.push(handle);
}
for handle in handles {
let result = handle.await.unwrap();
assert!(result.is_ok());
}
}
#[test]
fn test_execute_script_in_runtime_success() {
let ctx = create_empty_script_context();
let result = execute_script_in_runtime(r#""hello""#, &ctx);
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("hello")));
}
#[test]
fn test_execute_script_in_runtime_with_context() {
let ctx = create_full_script_context();
let result = execute_script_in_runtime(r#"mockforge.request.method"#, &ctx);
assert!(result.is_ok());
}
#[test]
fn test_execute_script_in_runtime_error() {
let ctx = create_empty_script_context();
let result = execute_script_in_runtime(r#"throw new Error("test");"#, &ctx);
assert!(result.is_err());
}
#[tokio::test]
async fn test_script_chain_context_number() {
let engine = ScriptEngine::new();
let ctx = create_full_script_context();
let script = r#"mockforge.chain.user_id;"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_script_chain_context_boolean() {
let engine = ScriptEngine::new();
let ctx = create_full_script_context();
let script = r#"mockforge.chain.is_admin;"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!(true)));
}
#[tokio::test]
async fn test_script_variables_number() {
let engine = ScriptEngine::new();
let ctx = create_full_script_context();
let script = r#"mockforge.variables.counter;"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_script_variables_string() {
let engine = ScriptEngine::new();
let ctx = create_full_script_context();
let script = r#"mockforge.variables.name;"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("test")));
}
#[tokio::test]
async fn test_script_arithmetic() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"1 + 2 + 3"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_script_string_concatenation() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#""hello" + " " + "world""#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("hello world")));
}
#[tokio::test]
async fn test_script_conditional() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"true ? "yes" : "no""#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("yes")));
}
#[tokio::test]
async fn test_script_function_definition_and_call() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"
function add(a, b) {
return a + b;
}
add(1, 2);
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_script_arrow_function() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"
const multiply = (a, b) => a * b;
multiply(3, 4);
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_script_array_operations() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"
const arr = [1, 2, 3];
arr.length;
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_script_object_access() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"
const obj = {key: "value"};
obj.key;
"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("value")));
}
#[tokio::test]
async fn test_date_format() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"date.format("2024-01-15T10:30:00+00:00", "%Y-%m-%d");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("2024-01-15")));
}
#[tokio::test]
async fn test_date_parse() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"date.parse("2024-01-15 10:30:00", "%Y-%m-%d %H:%M:%S");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
let return_val = result.unwrap().return_value;
assert!(return_val.is_some());
assert!(return_val.unwrap().as_str().unwrap().contains("2024-01-15"));
}
#[tokio::test]
async fn test_date_parse_invalid() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"date.parse("invalid", "%Y-%m-%d");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("")));
}
#[tokio::test]
async fn test_validate_regex_invalid_pattern() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"validate.regex("[invalid", "test");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!(false)));
}
#[tokio::test]
async fn test_script_stringify_function() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"stringify("test");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_crypto_base64_decode_invalid() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"crypto.base64Decode("!!invalid!!");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("")));
}
#[tokio::test]
async fn test_date_add_days_invalid() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"date.addDays("invalid", 1);"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("")));
}
#[tokio::test]
async fn test_date_format_invalid() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"date.format("invalid", "%Y-%m-%d");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("")));
}
#[tokio::test]
async fn test_http_url_encode_special_chars() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"http.urlEncode("a=b&c=d");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
let encoded = result.unwrap().return_value.unwrap();
assert!(encoded.as_str().unwrap().contains("%3D")); assert!(encoded.as_str().unwrap().contains("%26")); }
#[tokio::test]
async fn test_json_parse_invalid() {
let engine = ScriptEngine::new();
let ctx = create_empty_script_context();
let script = r#"JSON.parse("invalid json");"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!("null")));
}
#[tokio::test]
async fn test_script_with_complex_chain_context() {
let engine = ScriptEngine::new();
let mut ctx = create_empty_script_context();
ctx.chain_context.insert("float_val".to_string(), json!(3.125));
ctx.chain_context.insert("bool_val".to_string(), json!(false));
let script = r#"mockforge.chain.bool_val;"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().return_value, Some(json!(false)));
}
#[tokio::test]
async fn test_script_with_complex_variables() {
let engine = ScriptEngine::new();
let mut ctx = create_empty_script_context();
ctx.variables.insert("obj".to_string(), json!({"nested": "value"}));
let script = r#""executed";"#;
let result = engine.execute_script(script, &ctx, 1000).await;
assert!(result.is_ok());
}
}