use mockforge_bench::crud_flow::{CrudFlow, CrudFlowDetector, FlowStep};
use mockforge_bench::k6_gen::K6ScriptGenerator;
use mockforge_bench::param_overrides::{OperationOverrides, ParameterOverrides};
use mockforge_bench::spec_parser::SpecParser;
use serde_json::json;
use std::collections::HashMap;
use std::path::PathBuf;
fn generate_crud_flow_script_with_params(
flows: &[CrudFlow],
param_overrides: Option<&ParameterOverrides>,
skip_tls_verify: bool,
) -> String {
let handlebars = handlebars::Handlebars::new();
let template = include_str!("../src/templates/k6_crud_flow.hbs");
let stages = vec![
json!({"duration": "30s", "target": 5}),
json!({"duration": "30s", "target": 5}),
];
let headers_json = serde_json::to_string(&HashMap::<String, String>::new())
.unwrap_or_else(|_| "{}".to_string());
let data = json!({
"base_url": "https://test.example.com",
"flows": flows.iter().map(|f| {
let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
json!({
"name": sanitized_name.clone(),
"display_name": f.name,
"base_path": f.base_path,
"steps": f.steps.iter().enumerate().map(|(idx, s)| {
let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
let method_raw = if !parts.is_empty() {
parts[0].to_uppercase()
} else {
"GET".to_string()
};
let method = if !parts.is_empty() {
let m = parts[0].to_lowercase();
if m == "delete" { "del".to_string() } else { m }
} else {
"get".to_string()
};
let path = if parts.len() >= 2 { parts[1] } else { "/" };
let is_get_or_head = method == "get" || method == "head";
let has_body = matches!(method.as_str(), "post" | "put" | "patch");
let body_value = if has_body {
param_overrides
.map(|po| po.get_for_operation(None, &method_raw, path))
.and_then(|oo| oo.body)
.unwrap_or_else(|| json!({}))
} else {
json!({})
};
let body_json_str = serde_json::to_string(&body_value)
.unwrap_or_else(|_| "{}".to_string());
json!({
"operation": s.operation,
"method": method,
"path": path,
"extract": s.extract,
"use_values": s.use_values,
"description": s.description,
"display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
"is_get_or_head": is_get_or_head,
"has_body": has_body,
"body": body_json_str,
"body_is_dynamic": false,
})
}).collect::<Vec<_>>(),
})
}).collect::<Vec<_>>(),
"extract_fields": vec!["id", "uuid"],
"duration_secs": 60,
"max_vus": 5,
"skip_tls_verify": skip_tls_verify,
"stages": stages,
"threshold_percentile": "p(95)",
"threshold_ms": 500,
"max_error_rate": 0.05,
"headers": headers_json,
"dynamic_imports": Vec::<String>::new(),
"dynamic_globals": Vec::<String>::new(),
});
handlebars
.render_template(template, &data)
.expect("Should render CRUD flow template")
}
#[test]
fn test_crud_flow_without_params_file_has_empty_body() {
let flows = vec![CrudFlow {
name: "virtualservice".to_string(),
base_path: Some("/virtualservice".to_string()),
steps: vec![
FlowStep::new("POST /virtualservice".to_string())
.with_description("Create virtualservice".to_string())
.with_extract(vec!["uuid".to_string()]),
FlowStep::new("GET /virtualservice/{uuid}".to_string())
.with_description("Read virtualservice".to_string())
.with_values([("uuid".to_string(), "uuid".to_string())].into()),
],
}];
let script = generate_crud_flow_script_with_params(&flows, None, false);
assert!(
script.contains("let payload = {};"),
"POST request should have empty body when no params file is provided.\nScript:\n{}",
script
);
assert!(
!script.contains("[object Object]"),
"Script should not contain '[object Object]' (improper serialization).\nScript:\n{}",
script
);
println!("✓ CRUD flow without params file correctly uses empty body");
}
#[test]
fn test_crud_flow_with_params_file_applies_body() {
let flows = vec![CrudFlow {
name: "virtualservice".to_string(),
base_path: Some("/virtualservice".to_string()),
steps: vec![
FlowStep::new("POST /virtualservice".to_string())
.with_description("Create virtualservice".to_string())
.with_extract(vec!["uuid".to_string()]),
FlowStep::new("PUT /virtualservice/{uuid}".to_string())
.with_description("Update virtualservice".to_string())
.with_values([("uuid".to_string(), "uuid".to_string())].into()),
FlowStep::new("GET /virtualservice/{uuid}".to_string())
.with_description("Read virtualservice".to_string())
.with_values([("uuid".to_string(), "uuid".to_string())].into()),
],
}];
let mut operations = HashMap::new();
operations.insert(
"POST /virtualservice".to_string(),
OperationOverrides {
body: Some(json!({
"name": "test-vs",
"pool_ref": "/api/pool/pool-uuid",
"services": [{"port": 80}]
})),
..Default::default()
},
);
operations.insert(
"PUT /virtualservice/{uuid}".to_string(),
OperationOverrides {
body: Some(json!({
"name": "updated-vs",
"enabled": true
})),
..Default::default()
},
);
let param_overrides = ParameterOverrides {
defaults: OperationOverrides::default(),
operations,
};
let script = generate_crud_flow_script_with_params(&flows, Some(¶m_overrides), false);
assert!(
script.contains("\"name\":\"test-vs\""),
"POST request should have the body from params file.\nScript:\n{}",
script
);
assert!(
script.contains("\"pool_ref\":\"/api/pool/pool-uuid\""),
"POST request should have pool_ref from params file.\nScript:\n{}",
script
);
assert!(
script.contains("\"services\":[{\"port\":80}]"),
"POST request should have services array from params file.\nScript:\n{}",
script
);
assert!(
script.contains("\"name\":\"updated-vs\""),
"PUT request should have the body from params file.\nScript:\n{}",
script
);
assert!(
script.contains("\"enabled\":true"),
"PUT request should have enabled flag from params file.\nScript:\n{}",
script
);
assert!(
!script.contains("[object Object]"),
"Script should not contain '[object Object]' (improper serialization).\nScript:\n{}",
script
);
assert!(
!script.contains("= object"),
"Script should not contain '= object' (improper variable reference).\nScript:\n{}",
script
);
println!("✓ CRUD flow with params file correctly applies body configurations");
println!(" - POST body: name=test-vs, pool_ref, services");
println!(" - PUT body: name=updated-vs, enabled=true");
}
#[test]
fn test_crud_flow_body_serialization_is_valid_javascript() {
let flows = vec![CrudFlow {
name: "test".to_string(),
base_path: Some("/test".to_string()),
steps: vec![
FlowStep::new("POST /test".to_string()).with_description("Create test".to_string())
],
}];
let mut operations = HashMap::new();
operations.insert(
"POST /test".to_string(),
OperationOverrides {
body: Some(json!({
"string_field": "hello world",
"number_field": 42,
"boolean_field": true,
"null_field": null,
"array_field": [1, 2, 3],
"nested_object": {
"inner_string": "nested value",
"inner_array": ["a", "b", "c"]
}
})),
..Default::default()
},
);
let param_overrides = ParameterOverrides {
defaults: OperationOverrides::default(),
operations,
};
let script = generate_crud_flow_script_with_params(&flows, Some(¶m_overrides), false);
assert!(script.contains("\"string_field\":\"hello world\""));
assert!(script.contains("\"number_field\":42"));
assert!(script.contains("\"boolean_field\":true"));
assert!(script.contains("\"null_field\":null"));
assert!(script.contains("\"array_field\":[1,2,3]"));
assert!(script.contains("\"nested_object\":{"));
assert!(script.contains("\"inner_string\":\"nested value\""));
assert!(
script.contains("let payload = {") && script.contains("\"string_field\":\"hello world\""),
"Body should be assigned to let payload as valid JSON.\nScript:\n{}",
script
);
println!("✓ Complex nested body is correctly serialized as valid JavaScript");
}
#[test]
fn test_crud_flow_get_request_has_no_body() {
let flows = vec![CrudFlow {
name: "test".to_string(),
base_path: Some("/test".to_string()),
steps: vec![
FlowStep::new("GET /test".to_string()).with_description("List tests".to_string()),
FlowStep::new("GET /test/{id}".to_string())
.with_description("Get test".to_string())
.with_values([("id".to_string(), "id".to_string())].into()),
],
}];
let mut operations = HashMap::new();
operations.insert(
"GET /test".to_string(),
OperationOverrides {
body: Some(json!({"should": "be ignored"})),
..Default::default()
},
);
let param_overrides = ParameterOverrides {
defaults: OperationOverrides::default(),
operations,
};
let script = generate_crud_flow_script_with_params(&flows, Some(¶m_overrides), false);
let get_without_body = script
.matches("http.get(`${BASE_URL}${path}`, { headers, jar: new http.CookieJar() })")
.count();
assert_eq!(
get_without_body, 2,
"Both GET requests should use http.get without body.\nScript:\n{}",
script
);
assert!(
!script.contains("should"),
"GET request body from params should be ignored.\nScript:\n{}",
script
);
println!("✓ GET requests correctly have no body (params file body ignored)");
}
#[tokio::test]
async fn test_crud_flow_detection_and_params_integration() {
let spec_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("billing_subscriptions_v1.json");
if !spec_path.exists() {
println!("Skipping test - fixture file not found");
return;
}
let parser = SpecParser::from_file(&spec_path).await.expect("Should parse spec");
let operations = parser.get_operations();
let flows = CrudFlowDetector::detect_flows(&operations);
if flows.is_empty() {
println!("Skipping test - no CRUD flows detected in fixture");
return;
}
let mut op_overrides = HashMap::new();
for flow in &flows {
for step in &flow.steps {
let parts: Vec<&str> = step.operation.splitn(2, ' ').collect();
if parts.len() >= 2 {
let method = parts[0].to_uppercase();
if method == "POST" || method == "PUT" || method == "PATCH" {
op_overrides.insert(
step.operation.clone(),
OperationOverrides {
body: Some(json!({"test_field": "test_value"})),
..Default::default()
},
);
}
}
}
}
let param_overrides = ParameterOverrides {
defaults: OperationOverrides::default(),
operations: op_overrides,
};
let script = generate_crud_flow_script_with_params(&flows, Some(¶m_overrides), true);
assert!(!script.contains("[object Object]"));
assert!(!script.contains("= object"));
assert!(script.contains("insecureSkipTLSVerify: true"));
if script.contains("http.post") || script.contains("http.put") || script.contains("http.patch")
{
assert!(
script.contains("test_field"),
"Params file body should be applied to POST/PUT/PATCH requests.\nScript:\n{}",
script
);
}
println!("✓ CRUD flow detection with params integration works correctly");
println!(" - Detected {} flows", flows.len());
}