use crate::dynamic_params::{DynamicParamProcessor, DynamicPlaceholder};
use crate::error::{BenchError, Result};
use crate::request_gen::RequestTemplate;
use crate::scenarios::LoadScenario;
use handlebars::Handlebars;
use serde::Serialize;
#[cfg(test)]
use serde_json::json;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Serialize)]
pub struct K6ScriptTemplateData {
pub base_url: String,
pub stages: Vec<K6StageData>,
pub operations: Vec<K6OperationData>,
pub threshold_percentile: String,
pub threshold_ms: u64,
pub max_error_rate: f64,
pub scenario_name: String,
pub skip_tls_verify: bool,
pub has_dynamic_values: bool,
pub dynamic_imports: Vec<String>,
pub dynamic_globals: Vec<String>,
pub security_testing_enabled: bool,
pub has_custom_headers: bool,
pub chunked_request_bodies: bool,
pub target_rps: Option<u32>,
pub no_keep_alive: bool,
pub duration_secs: u64,
pub max_vus: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct K6CrudFlowTemplateData {
pub base_url: String,
pub flows: Vec<Value>,
pub extract_fields: Vec<String>,
pub duration_secs: u64,
pub max_vus: u32,
pub auth_header: Option<String>,
pub custom_headers: HashMap<String, String>,
pub skip_tls_verify: bool,
pub stages: Vec<K6StageData>,
pub threshold_percentile: String,
pub threshold_ms: u64,
pub max_error_rate: f64,
pub headers: String,
pub dynamic_imports: Vec<String>,
pub dynamic_globals: Vec<String>,
pub extracted_values_output_path: String,
pub error_injection_enabled: bool,
pub error_rate: f64,
pub error_types: Vec<String>,
pub security_testing_enabled: bool,
pub has_custom_headers: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct K6StageData {
pub duration: String,
pub target: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct K6OperationData {
pub index: usize,
pub name: String,
pub metric_name: String,
pub display_name: String,
pub method: String,
pub path: Value,
pub path_is_dynamic: bool,
pub headers: Value,
pub body: Option<Value>,
pub body_is_dynamic: bool,
pub has_body: bool,
pub is_get_or_head: bool,
}
pub struct K6Config {
pub target_url: String,
pub base_path: Option<String>,
pub scenario: LoadScenario,
pub duration_secs: u64,
pub max_vus: u32,
pub threshold_percentile: String,
pub threshold_ms: u64,
pub max_error_rate: f64,
pub auth_header: Option<String>,
pub custom_headers: HashMap<String, String>,
pub skip_tls_verify: bool,
pub security_testing_enabled: bool,
pub chunked_request_bodies: bool,
pub target_rps: Option<u32>,
pub no_keep_alive: bool,
}
pub struct K6ScriptGenerator {
config: K6Config,
templates: Vec<RequestTemplate>,
}
impl K6ScriptGenerator {
pub fn new(config: K6Config, templates: Vec<RequestTemplate>) -> Self {
Self { config, templates }
}
pub fn generate(&self) -> Result<String> {
let handlebars = Handlebars::new();
let template = include_str!("templates/k6_script.hbs");
let data = self.build_template_data()?;
let value = serde_json::to_value(&data)
.map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
handlebars
.render_template(template, &value)
.map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))
}
const K6_METRIC_NAME_BASE_MAX_LEN: usize = 112;
pub fn sanitize_k6_metric_name(name: &str) -> String {
let sanitized = Self::sanitize_js_identifier(name);
if sanitized.len() <= Self::K6_METRIC_NAME_BASE_MAX_LEN {
return sanitized;
}
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
name.hash(&mut hasher);
let hash_suffix = format!("{:08x}", hasher.finish() as u32);
let prefix_len = Self::K6_METRIC_NAME_BASE_MAX_LEN - 9;
let prefix = &sanitized[..prefix_len];
let prefix = prefix.trim_end_matches('_');
format!("{}_{}", prefix, hash_suffix)
}
pub fn sanitize_js_identifier(name: &str) -> String {
let mut result = String::new();
let mut chars = name.chars().peekable();
if let Some(&first) = chars.peek() {
if first.is_ascii_digit() {
result.push('_');
}
}
for ch in chars {
if ch.is_ascii_alphanumeric() || ch == '_' {
result.push(ch);
} else {
if !result.ends_with('_') {
result.push('_');
}
}
}
result = result.trim_end_matches('_').to_string();
if result.is_empty() {
result = "operation".to_string();
}
result
}
fn build_template_data(&self) -> Result<K6ScriptTemplateData> {
let stages = self
.config
.scenario
.generate_stages(self.config.duration_secs, self.config.max_vus);
let base_path = self.config.base_path.as_deref().unwrap_or("");
let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
let operations = self
.templates
.iter()
.enumerate()
.map(|(idx, template)| {
let display_name = template.operation.display_name();
let sanitized_name = Self::sanitize_js_identifier(&display_name);
let metric_name = Self::sanitize_k6_metric_name(&display_name);
let k6_method = match template.operation.method.to_lowercase().as_str() {
"delete" => "del".to_string(),
m => m.to_string(),
};
let is_get_or_head = matches!(k6_method.as_str(), "get" | "head");
let raw_path = template.generate_path();
let full_path = if base_path.is_empty() {
raw_path
} else {
format!("{}{}", base_path, raw_path)
};
let processed_path = DynamicParamProcessor::process_path(&full_path);
all_placeholders.extend(processed_path.placeholders.clone());
let (body_value, body_is_dynamic) = if let Some(body) = &template.body {
let processed_body = DynamicParamProcessor::process_json_body(body);
all_placeholders.extend(processed_body.placeholders.clone());
(Some(processed_body.value), processed_body.is_dynamic)
} else {
(None, false)
};
let path_value = if processed_path.is_dynamic {
processed_path.value
} else {
full_path
};
K6OperationData {
index: idx,
name: sanitized_name,
metric_name,
display_name,
method: k6_method,
path: Value::String(path_value),
path_is_dynamic: processed_path.is_dynamic,
headers: Value::String(self.build_headers_json(template)),
body: body_value.map(Value::String),
body_is_dynamic,
has_body: template.body.is_some(),
is_get_or_head,
}
})
.collect::<Vec<_>>();
let required_imports: Vec<String> =
DynamicParamProcessor::get_required_imports(&all_placeholders)
.into_iter()
.map(String::from)
.collect();
let required_globals: Vec<String> =
DynamicParamProcessor::get_required_globals(&all_placeholders)
.into_iter()
.map(String::from)
.collect();
let has_dynamic_values = !all_placeholders.is_empty();
Ok(K6ScriptTemplateData {
base_url: self.config.target_url.clone(),
stages: stages
.iter()
.map(|s| K6StageData {
duration: s.duration.clone(),
target: s.target,
})
.collect(),
operations,
threshold_percentile: self.config.threshold_percentile.clone(),
threshold_ms: self.config.threshold_ms,
max_error_rate: self.config.max_error_rate,
scenario_name: format!("{:?}", self.config.scenario).to_lowercase(),
skip_tls_verify: self.config.skip_tls_verify,
has_dynamic_values,
dynamic_imports: required_imports,
dynamic_globals: required_globals,
security_testing_enabled: self.config.security_testing_enabled,
has_custom_headers: !self.config.custom_headers.is_empty(),
chunked_request_bodies: self.config.chunked_request_bodies,
target_rps: self.config.target_rps,
no_keep_alive: self.config.no_keep_alive,
duration_secs: self.config.duration_secs,
max_vus: self.config.max_vus,
})
}
fn build_headers_json(&self, template: &RequestTemplate) -> String {
let mut headers = template.get_headers();
if let Some(auth) = &self.config.auth_header {
headers.insert("Authorization".to_string(), auth.clone());
}
for (key, value) in &self.config.custom_headers {
headers.insert(key.clone(), value.clone());
}
if self.config.chunked_request_bodies && template.body.is_some() {
headers.insert("Transfer-Encoding".to_string(), "chunked".to_string());
}
serde_json::to_string(&headers).unwrap_or_else(|_| "{}".to_string())
}
pub fn validate_script(script: &str) -> Vec<String> {
let mut errors = Vec::new();
if !script.contains("import http from 'k6/http'") {
errors.push("Missing required import: 'k6/http'".to_string());
}
if !script.contains("import { check") && !script.contains("import {check") {
errors.push("Missing required import: 'check' from 'k6'".to_string());
}
if !script.contains("import { Rate, Trend") && !script.contains("import {Rate, Trend") {
errors.push("Missing required import: 'Rate, Trend' from 'k6/metrics'".to_string());
}
let lines: Vec<&str> = script.lines().collect();
for (line_num, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.contains("new Trend(") || trimmed.contains("new Rate(") {
if let Some(start) = trimmed.find('\'') {
if let Some(end) = trimmed[start + 1..].find('\'') {
let metric_name = &trimmed[start + 1..start + 1 + end];
if !Self::is_valid_k6_metric_name(metric_name) {
errors.push(format!(
"Line {}: Invalid k6 metric name '{}'. Metric names must only contain ASCII letters, numbers, or underscores and start with a letter or underscore.",
line_num + 1,
metric_name
));
}
}
} else if let Some(start) = trimmed.find('"') {
if let Some(end) = trimmed[start + 1..].find('"') {
let metric_name = &trimmed[start + 1..start + 1 + end];
if !Self::is_valid_k6_metric_name(metric_name) {
errors.push(format!(
"Line {}: Invalid k6 metric name '{}'. Metric names must only contain ASCII letters, numbers, or underscores and start with a letter or underscore.",
line_num + 1,
metric_name
));
}
}
}
}
if trimmed.starts_with("const ") || trimmed.starts_with("let ") {
if let Some(equals_pos) = trimmed.find('=') {
let var_decl = &trimmed[..equals_pos];
if var_decl.contains('.')
&& !var_decl.contains("'")
&& !var_decl.contains("\"")
&& !var_decl.trim().starts_with("//")
{
errors.push(format!(
"Line {}: Invalid JavaScript variable name with dot: {}. Variable names cannot contain dots.",
line_num + 1,
var_decl.trim()
));
}
}
}
}
errors
}
fn is_valid_k6_metric_name(name: &str) -> bool {
if name.is_empty() || name.len() > 128 {
return false;
}
let mut chars = name.chars();
if let Some(first) = chars.next() {
if !first.is_ascii_alphabetic() && first != '_' {
return false;
}
}
for ch in chars {
if !ch.is_ascii_alphanumeric() && ch != '_' {
return false;
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_k6_config_creation() {
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::RampUp,
duration_secs: 60,
max_vus: 10,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
assert_eq!(config.duration_secs, 60);
assert_eq!(config.max_vus, 10);
}
#[test]
fn test_script_generator_creation() {
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let templates = vec![];
let generator = K6ScriptGenerator::new(config, templates);
assert_eq!(generator.templates.len(), 0);
}
#[test]
fn test_sanitize_js_identifier() {
assert_eq!(
K6ScriptGenerator::sanitize_js_identifier("billing.subscriptions.v1"),
"billing_subscriptions_v1"
);
assert_eq!(K6ScriptGenerator::sanitize_js_identifier("get user"), "get_user");
assert_eq!(K6ScriptGenerator::sanitize_js_identifier("123invalid"), "_123invalid");
assert_eq!(K6ScriptGenerator::sanitize_js_identifier("getUsers"), "getUsers");
assert_eq!(K6ScriptGenerator::sanitize_js_identifier("test...name"), "test_name");
assert_eq!(K6ScriptGenerator::sanitize_js_identifier(""), "operation");
assert_eq!(K6ScriptGenerator::sanitize_js_identifier("test@name#value"), "test_name_value");
assert_eq!(K6ScriptGenerator::sanitize_js_identifier("plans.list"), "plans_list");
assert_eq!(K6ScriptGenerator::sanitize_js_identifier("plans.create"), "plans_create");
assert_eq!(
K6ScriptGenerator::sanitize_js_identifier("plans.update-pricing-schemes"),
"plans_update_pricing_schemes"
);
assert_eq!(K6ScriptGenerator::sanitize_js_identifier("users CRUD"), "users_CRUD");
}
#[test]
fn test_sanitize_k6_metric_name_short_passthrough() {
let short = "billing_subscriptions_list";
let out = K6ScriptGenerator::sanitize_k6_metric_name(short);
assert_eq!(out, short);
assert!(K6ScriptGenerator::is_valid_k6_metric_name(&format!("{out}_latency")));
}
#[test]
fn test_sanitize_k6_metric_name_truncates_long_microsoft_graph_id() {
let long = "drives.drive.items.driveItem.workbook.worksheets.workbookWorksheet.\
charts.workbookChart.axes.categoryAxis.format.line.clear";
let metric = K6ScriptGenerator::sanitize_k6_metric_name(long);
assert!(
metric.len() <= K6ScriptGenerator::K6_METRIC_NAME_BASE_MAX_LEN,
"metric base len {} exceeded cap {}",
metric.len(),
K6ScriptGenerator::K6_METRIC_NAME_BASE_MAX_LEN
);
assert!(K6ScriptGenerator::is_valid_k6_metric_name(&metric));
assert!(K6ScriptGenerator::is_valid_k6_metric_name(&format!("{metric}_latency")));
assert!(K6ScriptGenerator::is_valid_k6_metric_name(&format!("{metric}_errors")));
assert!(K6ScriptGenerator::is_valid_k6_metric_name(&format!("{metric}_step99_latency")));
}
#[test]
fn test_sanitize_k6_metric_name_distinct_long_names_get_distinct_metrics() {
let prefix = "a".repeat(150);
let a = format!("{prefix}.foo");
let b = format!("{prefix}.bar");
let ma = K6ScriptGenerator::sanitize_k6_metric_name(&a);
let mb = K6ScriptGenerator::sanitize_k6_metric_name(&b);
assert_ne!(ma, mb, "distinct long names produced the same metric name");
}
#[test]
fn test_sanitize_k6_metric_name_truncated_starts_with_letter() {
let long = format!("{}123end", "x".repeat(120));
let metric = K6ScriptGenerator::sanitize_k6_metric_name(&long);
assert!(K6ScriptGenerator::is_valid_k6_metric_name(&metric));
}
#[test]
fn test_microsoft_graph_long_operation_id_passes_validation() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
let long_op_id = "drives.drive.items.driveItem.workbook.worksheets.\
workbookWorksheet.charts.workbookChart.axes.categoryAxis.format.\
line.clear";
let operation = ApiOperation {
method: "post".to_string(),
path: "/drives/{drive-id}/items/{item-id}/workbook/worksheets/{worksheet-id}/charts/{chart-id}/axes/categoryAxis/format/line/clear".to_string(),
operation: Operation::default(),
operation_id: Some(long_op_id.to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: None,
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: Some("/v1.0".to_string()),
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("script generates");
let errors = K6ScriptGenerator::validate_script(&script);
assert!(
errors.is_empty(),
"validate_script returned errors for long operationId: {errors:#?}"
);
}
#[test]
fn test_script_generation_with_dots_in_name() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
let operation = ApiOperation {
method: "get".to_string(),
path: "/billing/subscriptions".to_string(),
operation: Operation::default(),
operation_id: Some("billing.subscriptions.v1".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: None,
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
script.contains("const billing_subscriptions_v1_latency"),
"Script should contain sanitized variable name for latency"
);
assert!(
script.contains("const billing_subscriptions_v1_errors"),
"Script should contain sanitized variable name for errors"
);
assert!(
!script.contains("const billing.subscriptions"),
"Script should not contain variable names with dots - this would cause 'Unexpected token .' error"
);
assert!(
script.contains("'billing_subscriptions_v1_latency'"),
"Metric name strings should be sanitized (no dots) - k6 validation requires valid metric names"
);
assert!(
script.contains("'billing_subscriptions_v1_errors'"),
"Metric name strings should be sanitized (no dots) - k6 validation requires valid metric names"
);
assert!(
script.contains("billing.subscriptions.v1"),
"Script should contain original name in comments/strings for readability"
);
assert!(
script.contains("billing_subscriptions_v1_latency.add"),
"Variable usage should use sanitized name"
);
assert!(
script.contains("billing_subscriptions_v1_errors.add"),
"Variable usage should use sanitized name"
);
}
#[test]
fn test_rps_with_ramp_up_uses_full_vu_pool_and_duration() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
let operation = ApiOperation {
method: "get".to_string(),
path: "/users".to_string(),
operation: Operation::default(),
operation_id: Some("listUsers".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: None,
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::RampUp,
duration_secs: 600,
max_vus: 100,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: Some(100),
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
script.contains("constant-arrival-rate"),
"with --rps set, executor must switch to constant-arrival-rate"
);
assert!(
script.contains("rate: 100,"),
"constant-arrival-rate must use the configured --rps as `rate`"
);
assert!(
script.contains("duration: '600s'"),
"duration must come from --duration, not the ramp-down stage; got:\n{}",
script
);
assert!(
script.contains("preAllocatedVUs: 100,"),
"preAllocatedVUs must equal --vus, not the last stage's target=0; got:\n{}",
script
);
assert!(
script.contains("maxVUs: 100,"),
"maxVUs must equal --vus, not the last stage's target=0; got:\n{}",
script
);
for (idx, line) in script.lines().enumerate() {
let trimmed = line.trim_start();
if trimmed.starts_with("//") || trimmed.starts_with("/*") {
continue;
}
assert!(
!trimmed.starts_with("preAllocatedVUs: 0"),
"regression at line {}: preAllocatedVUs is 0 — constant-arrival-rate \
will run no VUs (issue #79 round 5 ramp-up bug). Line: {:?}",
idx + 1,
line,
);
}
}
#[test]
fn test_cps_sets_no_connection_reuse() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
let operation = ApiOperation {
method: "get".to_string(),
path: "/u".to_string(),
operation: Operation::default(),
operation_id: Some("u".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: None,
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: true,
};
let script = K6ScriptGenerator::new(config, vec![template]).generate().unwrap();
assert!(
script.contains("noConnectionReuse: true"),
"--cps must set noConnectionReuse: true on the k6 options block"
);
assert!(
script.contains("Total Connections:"),
"--cps summary must include connection-rate output (Srikanth's round-5 ask)"
);
assert!(
script.contains("Connection Rate:"),
"--cps summary must include 'Connection Rate:' (Srikanth's round-5 ask)"
);
}
#[test]
fn test_validate_script_valid() {
let valid_script = r#"
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const test_latency = new Trend('test_latency');
const test_errors = new Rate('test_errors');
export default function() {
const res = http.get('https://example.com');
test_latency.add(res.timings.duration);
test_errors.add(res.status !== 200);
}
"#;
let errors = K6ScriptGenerator::validate_script(valid_script);
assert!(errors.is_empty(), "Valid script should have no validation errors");
}
#[test]
fn test_validate_script_invalid_metric_name() {
let invalid_script = r#"
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const test_latency = new Trend('test.latency');
const test_errors = new Rate('test_errors');
export default function() {
const res = http.get('https://example.com');
test_latency.add(res.timings.duration);
}
"#;
let errors = K6ScriptGenerator::validate_script(invalid_script);
assert!(
!errors.is_empty(),
"Script with invalid metric name should have validation errors"
);
assert!(
errors.iter().any(|e| e.contains("Invalid k6 metric name")),
"Should detect invalid metric name with dot"
);
}
#[test]
fn test_validate_script_missing_imports() {
let invalid_script = r#"
const test_latency = new Trend('test_latency');
export default function() {}
"#;
let errors = K6ScriptGenerator::validate_script(invalid_script);
assert!(!errors.is_empty(), "Script missing imports should have validation errors");
}
#[test]
fn test_validate_script_metric_name_validation() {
let valid_script = r#"
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const test_latency = new Trend('test_latency');
const test_errors = new Rate('test_errors');
export default function() {}
"#;
let errors = K6ScriptGenerator::validate_script(valid_script);
assert!(errors.is_empty(), "Valid metric names should pass validation");
let invalid_cases = vec![
("test.latency", "dot in metric name"),
("123test", "starts with number"),
("test-latency", "hyphen in metric name"),
("test@latency", "special character"),
];
for (invalid_name, description) in invalid_cases {
let script = format!(
r#"
import http from 'k6/http';
import {{ check, sleep }} from 'k6';
import {{ Rate, Trend }} from 'k6/metrics';
const test_latency = new Trend('{}');
export default function() {{}}
"#,
invalid_name
);
let errors = K6ScriptGenerator::validate_script(&script);
assert!(
!errors.is_empty(),
"Metric name '{}' ({}) should fail validation",
invalid_name,
description
);
}
}
#[test]
fn test_skip_tls_verify_with_body() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
use serde_json::json;
let operation = ApiOperation {
method: "post".to_string(),
path: "/api/users".to_string(),
operation: Operation::default(),
operation_id: Some("createUser".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: Some(json!({"name": "test"})),
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: true,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
script.contains("insecureSkipTLSVerify: true"),
"Script should include insecureSkipTLSVerify option when skip_tls_verify is true"
);
}
#[test]
fn test_skip_tls_verify_without_body() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
let operation = ApiOperation {
method: "get".to_string(),
path: "/api/users".to_string(),
operation: Operation::default(),
operation_id: Some("getUsers".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: None,
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: true,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
script.contains("insecureSkipTLSVerify: true"),
"Script should include insecureSkipTLSVerify option when skip_tls_verify is true (no body)"
);
}
#[test]
fn test_no_skip_tls_verify() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
let operation = ApiOperation {
method: "get".to_string(),
path: "/api/users".to_string(),
operation: Operation::default(),
operation_id: Some("getUsers".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: None,
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
!script.contains("insecureSkipTLSVerify"),
"Script should NOT include insecureSkipTLSVerify option when skip_tls_verify is false"
);
}
#[test]
fn test_skip_tls_verify_multiple_operations() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
use serde_json::json;
let operation1 = ApiOperation {
method: "get".to_string(),
path: "/api/users".to_string(),
operation: Operation::default(),
operation_id: Some("getUsers".to_string()),
};
let operation2 = ApiOperation {
method: "post".to_string(),
path: "/api/users".to_string(),
operation: Operation::default(),
operation_id: Some("createUser".to_string()),
};
let template1 = RequestTemplate {
operation: operation1,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: None,
};
let template2 = RequestTemplate {
operation: operation2,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: Some(json!({"name": "test"})),
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: true,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template1, template2]);
let script = generator.generate().expect("Should generate script");
let skip_count = script.matches("insecureSkipTLSVerify: true").count();
assert_eq!(
skip_count, 1,
"Script should include insecureSkipTLSVerify exactly once in global options (not per-request)"
);
let options_start = script.find("export const options = {").expect("Should have options");
let scenarios_start = script.find("scenarios:").expect("Should have scenarios");
let options_prefix = &script[options_start..scenarios_start];
assert!(
options_prefix.contains("insecureSkipTLSVerify: true"),
"insecureSkipTLSVerify should be in global options block"
);
}
#[test]
fn test_dynamic_params_in_body() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
use serde_json::json;
let operation = ApiOperation {
method: "post".to_string(),
path: "/api/resources".to_string(),
operation: Operation::default(),
operation_id: Some("createResource".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: Some(json!({
"name": "load-test-${__VU}",
"iteration": "${__ITER}"
})),
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
script.contains("Dynamic body with runtime placeholders"),
"Script should contain comment about dynamic body"
);
assert!(
script.contains("__VU"),
"Script should contain __VU reference for dynamic VU-based values"
);
assert!(
script.contains("__ITER"),
"Script should contain __ITER reference for dynamic iteration values"
);
}
#[test]
fn test_dynamic_params_with_uuid() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
use serde_json::json;
let operation = ApiOperation {
method: "post".to_string(),
path: "/api/resources".to_string(),
operation: Operation::default(),
operation_id: Some("createResource".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: Some(json!({
"id": "${__UUID}"
})),
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
!script.contains("k6/experimental/webcrypto"),
"Script should NOT include deprecated k6/experimental/webcrypto import"
);
assert!(
script.contains("crypto.randomUUID()"),
"Script should contain crypto.randomUUID() for UUID placeholder"
);
}
#[test]
fn test_dynamic_params_with_counter() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
use serde_json::json;
let operation = ApiOperation {
method: "post".to_string(),
path: "/api/resources".to_string(),
operation: Operation::default(),
operation_id: Some("createResource".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: Some(json!({
"sequence": "${__COUNTER}"
})),
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
script.contains("let globalCounter = 0"),
"Script should include globalCounter initialization when COUNTER placeholder is used"
);
assert!(
script.contains("globalCounter++"),
"Script should contain globalCounter++ for COUNTER placeholder"
);
}
#[test]
fn test_static_body_no_dynamic_marker() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
use serde_json::json;
let operation = ApiOperation {
method: "post".to_string(),
path: "/api/resources".to_string(),
operation: Operation::default(),
operation_id: Some("createResource".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: Some(json!({
"name": "static-value",
"count": 42
})),
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
!script.contains("Dynamic body with runtime placeholders"),
"Script should NOT contain dynamic body comment for static body"
);
assert!(
!script.contains("webcrypto"),
"Script should NOT include webcrypto import for static body"
);
assert!(
!script.contains("let globalCounter"),
"Script should NOT include globalCounter for static body"
);
}
#[test]
fn test_security_testing_enabled_generates_calling_code() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
use serde_json::json;
let operation = ApiOperation {
method: "post".to_string(),
path: "/api/users".to_string(),
operation: Operation::default(),
operation_id: Some("createUser".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: Some(json!({"name": "test"})),
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: true,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
script.contains("getNextSecurityPayload"),
"Script should contain getNextSecurityPayload() call when security_testing_enabled is true"
);
assert!(
script.contains("applySecurityPayload"),
"Script should contain applySecurityPayload() call when security_testing_enabled is true"
);
assert!(
script.contains("secPayloadGroup"),
"Script should contain secPayloadGroup variable when security_testing_enabled is true"
);
assert!(
script.contains("secBodyPayload"),
"Script should contain secBodyPayload variable when security_testing_enabled is true"
);
assert!(
script.contains("hasSecCookie"),
"Script should track hasSecCookie for CookieJar conflict avoidance"
);
assert!(
script.contains("secRequestOpts"),
"Script should use secRequestOpts to conditionally skip CookieJar"
);
assert!(
script.contains("const requestHeaders = { ..."),
"Script should spread headers into mutable copy for security payload injection"
);
assert!(
script.contains("secPayload.injectAsPath"),
"Script should check injectAsPath for path-based URI injection"
);
assert!(
script.contains("secBodyPayload.formBody"),
"Script should check formBody for form-encoded body delivery"
);
assert!(
script.contains("application/x-www-form-urlencoded"),
"Script should set Content-Type for form-encoded body"
);
let op_comment_pos =
script.find("// Operation 0:").expect("Should have Operation 0 comment");
let sec_payload_pos = script
.find("const secPayloadGroup = typeof getNextSecurityPayload")
.expect("Should have secPayloadGroup assignment");
assert!(
sec_payload_pos > op_comment_pos,
"secPayloadGroup should be fetched inside operation block (per-operation), not before it (per-iteration)"
);
}
#[test]
fn test_security_testing_disabled_no_calling_code() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
use serde_json::json;
let operation = ApiOperation {
method: "post".to_string(),
path: "/api/users".to_string(),
operation: Operation::default(),
operation_id: Some("createUser".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: Some(json!({"name": "test"})),
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
!script.contains("getNextSecurityPayload"),
"Script should NOT contain getNextSecurityPayload() when security_testing_enabled is false"
);
assert!(
!script.contains("applySecurityPayload"),
"Script should NOT contain applySecurityPayload() when security_testing_enabled is false"
);
assert!(
!script.contains("secPayloadGroup"),
"Script should NOT contain secPayloadGroup variable when security_testing_enabled is false"
);
assert!(
!script.contains("secBodyPayload"),
"Script should NOT contain secBodyPayload variable when security_testing_enabled is false"
);
assert!(
!script.contains("hasSecCookie"),
"Script should NOT contain hasSecCookie when security_testing_enabled is false"
);
assert!(
!script.contains("secRequestOpts"),
"Script should NOT contain secRequestOpts when security_testing_enabled is false"
);
assert!(
!script.contains("injectAsPath"),
"Script should NOT contain injectAsPath when security_testing_enabled is false"
);
assert!(
!script.contains("formBody"),
"Script should NOT contain formBody when security_testing_enabled is false"
);
}
#[test]
fn test_security_e2e_definitions_and_calls_both_present() {
use crate::security_payloads::{
SecurityPayloads, SecurityTestConfig, SecurityTestGenerator,
};
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
use serde_json::json;
let operation = ApiOperation {
method: "post".to_string(),
path: "/api/users".to_string(),
operation: Operation::default(),
operation_id: Some("createUser".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: Some(json!({"name": "test"})),
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: true,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let mut script = generator.generate().expect("Should generate base script");
let security_config = SecurityTestConfig::default().enable();
let payloads = SecurityPayloads::get_payloads(&security_config);
assert!(!payloads.is_empty(), "Should have built-in payloads");
let mut additional_code = String::new();
additional_code
.push_str(&SecurityTestGenerator::generate_payload_selection(&payloads, false));
additional_code.push('\n');
additional_code.push_str(&SecurityTestGenerator::generate_apply_payload(&[]));
additional_code.push('\n');
if let Some(pos) = script.find("export const options") {
script.insert_str(
pos,
&format!("\n// === Advanced Testing Features ===\n{}\n", additional_code),
);
}
assert!(
script.contains("function getNextSecurityPayload()"),
"Final script must contain getNextSecurityPayload function DEFINITION"
);
assert!(
script.contains("function applySecurityPayload("),
"Final script must contain applySecurityPayload function DEFINITION"
);
assert!(
script.contains("securityPayloads"),
"Final script must contain securityPayloads array"
);
assert!(
script.contains("const secPayloadGroup = typeof getNextSecurityPayload"),
"Final script must contain secPayloadGroup assignment (template calling code)"
);
assert!(
script.contains("applySecurityPayload(payload, [], secBodyPayload)"),
"Final script must contain applySecurityPayload CALL with secBodyPayload"
);
assert!(
script.contains("const requestHeaders = { ..."),
"Final script must spread headers for security payload header injection"
);
assert!(
script.contains("for (const secPayload of secPayloadGroup)"),
"Final script must loop over secPayloadGroup"
);
assert!(
script.contains("secPayload.injectAsPath"),
"Final script must check injectAsPath for path-based URI injection"
);
assert!(
script.contains("secBodyPayload.formBody"),
"Final script must check formBody for form-encoded body delivery"
);
let def_pos = script.find("function getNextSecurityPayload()").unwrap();
let call_pos =
script.find("const secPayloadGroup = typeof getNextSecurityPayload").unwrap();
let options_pos = script.find("export const options").unwrap();
let default_fn_pos = script.find("export default function").unwrap();
assert!(
def_pos < options_pos,
"Function definitions must appear before export const options"
);
assert!(
call_pos > default_fn_pos,
"Calling code must appear inside export default function"
);
}
#[test]
fn test_security_uri_injection_for_get_requests() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
let operation = ApiOperation {
method: "get".to_string(),
path: "/api/users".to_string(),
operation: Operation::default(),
operation_id: Some("listUsers".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: None,
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: true,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
script.contains("requestUrl"),
"Script should build requestUrl variable for URI payload injection"
);
assert!(
script.contains("secPayload.location === 'uri'"),
"Script should check for URI-location payloads"
);
assert!(
script.contains("'test=' + encodeURIComponent(secPayload.payload)"),
"Script should URL-encode security payload in query string for valid HTTP"
);
assert!(
script.contains("secPayload.injectAsPath"),
"Script should check injectAsPath for path-based URI injection"
);
assert!(
script.contains("encodeURI(secPayload.payload)"),
"Script should use encodeURI for path-based injection"
);
assert!(
script.contains("http.get(requestUrl,"),
"GET request should use requestUrl (with URI injection) instead of inline URL"
);
}
#[test]
fn test_security_uri_injection_for_post_requests() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
use serde_json::json;
let operation = ApiOperation {
method: "post".to_string(),
path: "/api/users".to_string(),
operation: Operation::default(),
operation_id: Some("createUser".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: Some(json!({"name": "test"})),
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: true,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
script.contains("requestUrl"),
"POST script should build requestUrl for URI payload injection"
);
assert!(
script.contains("secPayload.location === 'uri'"),
"POST script should check for URI-location payloads"
);
assert!(
script.contains("applySecurityPayload(payload, [], secBodyPayload)"),
"POST script should apply security body payload to request body"
);
assert!(
script.contains("http.post(requestUrl,"),
"POST request should use requestUrl (with URI injection) instead of inline URL"
);
}
#[test]
fn test_no_uri_injection_when_security_disabled() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
let operation = ApiOperation {
method: "get".to_string(),
path: "/api/users".to_string(),
operation: Operation::default(),
operation_id: Some("listUsers".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: None,
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
!script.contains("requestUrl"),
"Script should NOT have requestUrl when security is disabled"
);
assert!(
!script.contains("secPayloadGroup"),
"Script should NOT have secPayloadGroup when security is disabled"
);
assert!(
!script.contains("secBodyPayload"),
"Script should NOT have secBodyPayload when security is disabled"
);
}
#[test]
fn test_uses_per_request_cookie_jar() {
use crate::spec_parser::ApiOperation;
use openapiv3::Operation;
let operation = ApiOperation {
method: "get".to_string(),
path: "/api/users".to_string(),
operation: Operation::default(),
operation_id: Some("listUsers".to_string()),
};
let template = RequestTemplate {
operation,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: None,
};
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 5,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
};
let generator = K6ScriptGenerator::new(config, vec![template]);
let script = generator.generate().expect("Should generate script");
assert!(
script.contains("jar: new http.CookieJar()"),
"Script should create fresh CookieJar per request"
);
assert!(
!script.contains("jar: null"),
"Script should NOT use jar: null (does not disable default VU cookie jar in k6)"
);
assert!(
!script.contains("EMPTY_JAR"),
"Script should NOT use shared EMPTY_JAR (accumulates Set-Cookie responses)"
);
}
}