use mockforge_bench::k6_gen::{K6Config, K6ScriptGenerator};
use mockforge_bench::request_gen::RequestGenerator;
use mockforge_bench::scenarios::LoadScenario;
use mockforge_bench::security_payloads::{
SecurityPayloads, SecurityTestConfig, SecurityTestGenerator,
};
use mockforge_bench::spec_parser::SpecParser;
use mockforge_bench::target_parser::parse_targets_file;
use mockforge_bench::wafbench::WafBenchLoader;
use std::collections::HashMap;
use std::path::PathBuf;
#[tokio::test]
async fn test_issue_79_comprehensive_summary() {
let spec_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("billing_subscriptions_v1.json");
let parser = SpecParser::from_file(&spec_path)
.await
.expect("Should parse billing subscriptions spec");
let operations = parser.get_operations();
let templates: Result<Vec<_>, _> =
operations.iter().map(RequestGenerator::generate_template).collect();
let templates = templates.expect("Should generate request templates");
let mut headers = HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
headers.insert("Authorization".to_string(), "Bearer test-token".to_string());
let config = K6Config {
target_url: "https://192.168.1.100".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 60,
max_vus: 50,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: None,
custom_headers: headers,
skip_tls_verify: true,
security_testing_enabled: false,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
geo_source_ips: Vec::new(),
geo_source_headers: Vec::new(),
};
let generator = K6ScriptGenerator::new(config, templates);
let script = generator.generate().expect("Should generate k6 script");
assert!(
!script.contains("Unexpected token"),
"Script should not contain JavaScript syntax errors"
);
assert!(
script.contains("insecureSkipTLSVerify: true"),
"Script should include insecureSkipTLSVerify"
);
assert!(script.contains("p(95)<500"), "Thresholds should use correct k6 syntax");
assert!(
script.contains("http.get(") || script.contains("http.post("),
"Script should use lowercase HTTP methods"
);
assert!(!script.contains("[object]"), "Headers should be properly serialized");
assert!(script.contains("Authorization"), "Custom headers should be included");
for line in script.lines() {
if line.contains("new Trend(") || line.contains("new Rate(") {
assert!(
line.matches('.').count() <= 2,
"Metric names should not contain dots: {}",
line
);
}
}
println!("✓ Issue #79: Comprehensive end-to-end test - ALL FIXES VALIDATED");
println!(" - Operation ID sanitization: ✓");
println!(" - Metric name sanitization: ✓");
println!(" - Threshold syntax: ✓");
println!(" - HTTP method case: ✓");
println!(" - Headers serialization: ✓");
println!(" - insecureSkipTLSVerify: ✓");
println!(" - Custom headers: ✓");
}
#[tokio::test]
async fn test_issue_79_swagger_2_0_support() {
let spec_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("billing_subscriptions_v1.json");
let parser = SpecParser::from_file(&spec_path).await.expect("Should parse spec");
let operations = parser.get_operations();
assert!(!operations.is_empty(), "Should find operations in spec");
println!("✓ Issue #79(8): Swagger 2.0 to OpenAPI 3.0 conversion - WORKS");
}
#[tokio::test]
async fn test_issue_79_multi_target_parsing() {
let temp_dir = std::env::temp_dir().join("mockforge_test_multi_target");
std::fs::create_dir_all(&temp_dir).expect("Should create temp dir");
let targets_file = temp_dir.join("targets.txt");
std::fs::write(
&targets_file,
"https://api1.example.com\n\
https://api2.example.com\n\
https://api3.example.com\n\
192.168.1.100:8080\n\
api4.example.com\n",
)
.expect("Should write targets file");
let targets = parse_targets_file(&targets_file).expect("Should parse targets file");
assert_eq!(targets.len(), 5, "Should parse 5 targets");
assert_eq!(targets[0].url, "https://api1.example.com", "First target should be correct");
assert_eq!(
targets[3].url, "http://192.168.1.100:8080",
"IP:port target should be normalized with http://"
);
println!("✓ Issue #79(11): Multi-target parsing - WORKS");
}
#[tokio::test]
async fn test_issue_79_full_security_pipeline_with_real_spec() {
let spec_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("billing_subscriptions_v1.json");
let parser = SpecParser::from_file(&spec_path)
.await
.expect("Should parse billing subscriptions spec");
let operations = parser.get_operations();
assert!(!operations.is_empty(), "Should find operations in spec");
let templates: Vec<_> = operations
.iter()
.map(RequestGenerator::generate_template)
.collect::<mockforge_bench::error::Result<Vec<_>>>()
.expect("Should generate request templates");
let config = K6Config {
target_url: "https://api-m.sandbox.paypal.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
max_vus: 10,
threshold_percentile: "p(95)".to_string(),
threshold_ms: 500,
max_error_rate: 0.05,
auth_header: Some("Bearer test-token-12345".to_string()),
custom_headers: HashMap::new(),
skip_tls_verify: false,
security_testing_enabled: true,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
geo_source_ips: Vec::new(),
geo_source_headers: Vec::new(),
};
let generator = K6ScriptGenerator::new(config, templates);
let mut script = generator.generate().expect("Should generate k6 script");
let security_config = SecurityTestConfig::default().enable();
let payloads = SecurityPayloads::get_payloads(&security_config);
assert!(!payloads.is_empty(), "Should have built-in security 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');
additional_code.push_str(&SecurityTestGenerator::generate_security_checks());
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()"),
"Must contain getNextSecurityPayload() function DEFINITION"
);
assert!(
script.contains("function applySecurityPayload("),
"Must contain applySecurityPayload() function DEFINITION"
);
assert!(
script.contains("function checkSecurityResponse("),
"Must contain checkSecurityResponse() function DEFINITION"
);
assert!(
script.contains("const securityPayloads = ["),
"Must contain securityPayloads array"
);
assert!(
script.contains("const secPayloadGroup = typeof getNextSecurityPayload"),
"Must contain secPayloadGroup = getNextSecurityPayload() CALL"
);
assert!(
script.contains("secPayload.location === 'header'"),
"Must contain header location check for header injection"
);
assert!(
script.contains("const requestHeaders = { ..."),
"Must spread headers into mutable copy for injection"
);
assert!(
script.contains("for (const secPayload of secPayloadGroup)"),
"Must loop over secPayloadGroup"
);
assert!(
script.contains("secPayload.location === 'uri'"),
"Must contain URI location check for query parameter injection"
);
assert!(
script.contains("'test=' + encodeURIComponent(secPayload.payload)"),
"Must URL-encode security payload in query string for valid HTTP"
);
assert!(
script.contains("requestUrl"),
"Must build requestUrl variable for URI injection"
);
assert!(
script.contains("secPayload.injectAsPath"),
"Must check injectAsPath for path-based URI injection (CRS 942101)"
);
assert!(
script.contains("encodeURI(secPayload.payload)"),
"Must use encodeURI for path-based injection"
);
assert!(
script.contains("secBodyPayload.formBody"),
"Must check formBody for form-encoded body delivery (CRS 942432)"
);
assert!(
script.contains("decodeURIComponent"),
"Must decode formBody into object for k6 native form encoding"
);
assert!(
script.contains("applySecurityPayload(payload, [], secBodyPayload)"),
"Must contain applySecurityPayload() CALL with secBodyPayload for body injection"
);
let def_pos = script.find("function getNextSecurityPayload()").unwrap();
let options_pos = script.find("export const options").unwrap();
let default_fn_pos = script.find("export default function").unwrap();
let call_pos = script.find("const secPayloadGroup = typeof getNextSecurityPayload").unwrap();
assert!(def_pos < options_pos, "Definitions must come before export const options");
assert!(call_pos > default_fn_pos, "Calling code must be inside export default function");
let payload_array_start = script.find("const securityPayloads = [").unwrap();
let payload_array_end = script[payload_array_start..].find("];").unwrap();
let payload_array = &script[payload_array_start..payload_array_start + payload_array_end];
assert!(
payload_array.contains("payload:"),
"securityPayloads array must contain actual payload entries, not be empty"
);
let default_fn_section = &script[default_fn_pos..];
for line in default_fn_section.lines() {
let trimmed = line.trim();
if trimmed.starts_with("const res = http.") {
assert!(
trimmed.contains("requestUrl"),
"HTTP call should use requestUrl for URI injection: {}",
trimmed
);
}
}
println!("\n✓ Issue #79: Full security pipeline integration test PASSED");
println!(" - Real spec file: billing_subscriptions_v1.json");
println!(" - {} operations processed", operations.len());
println!(" - {} security payloads loaded", payloads.len());
println!(" - Function definitions: ✓");
println!(" - Calling code (header injection): ✓");
println!(" - Calling code (URI injection): ✓");
println!(" - Calling code (body injection): ✓");
println!(" - Correct ordering: ✓");
println!(" - requestUrl used in all HTTP calls: ✓");
println!(" - injectAsPath for path-based injection: ✓");
println!(" - formBody for form-encoded delivery: ✓");
}
#[tokio::test]
async fn test_issue_79_wafbench_grouped_payloads_e2e() {
let temp_dir = std::env::temp_dir().join("mockforge_test_wafbench_grouping");
std::fs::create_dir_all(&temp_dir).expect("Should create temp dir");
let yaml_content = r#"
meta:
author: test
description: "Tests for SQL injection rule 942290"
enabled: true
name: "942290.yaml"
tests:
- desc: "SQL injection with URI and User-Agent"
test_title: "942290-1"
stages:
- stage:
input:
dest_addr: 127.0.0.1
headers:
Host: localhost
User-Agent: "ModSecurity CRS 3 Tests"
method: GET
port: 80
uri: "/test?id=2"
output:
log_contains: id "942290"
- desc: "SQL injection with body payload"
test_title: "942240-1"
stages:
- stage:
input:
dest_addr: 127.0.0.1
headers:
Host: localhost
Content-Type: "application/x-www-form-urlencoded"
method: POST
port: 80
uri: "/"
data: "%22+WAITFOR+DELAY+%270%3A0%3A5%27"
output:
log_contains: id "942240"
- desc: "Simple SQL injection in URI only"
test_title: "942100-1"
stages:
- stage:
input:
dest_addr: 127.0.0.1
headers: {}
method: GET
port: 80
uri: "/test?param=1+OR+1%3D1"
output:
log_contains: id "942100"
"#;
let yaml_path = temp_dir.join("942290.yaml");
std::fs::write(&yaml_path, yaml_content).expect("Should write test YAML");
let mut loader = WafBenchLoader::new();
loader.load_file(&yaml_path).expect("Should load WAFBench file");
let wafbench_payloads = loader.to_security_payloads();
assert!(!wafbench_payloads.is_empty(), "Should have loaded WAFBench payloads");
let grouped: Vec<_> = wafbench_payloads
.iter()
.filter(|p| p.group_id.as_deref() == Some("942290-1"))
.collect();
assert!(
grouped.len() >= 2,
"942290-1 should have at least 2 grouped payloads (URI + headers), got {}",
grouped.len()
);
let ungrouped: Vec<_> =
wafbench_payloads.iter().filter(|p| p.description.contains("942100")).collect();
assert!(!ungrouped.is_empty(), "Should have 942100 payloads");
assert!(
ungrouped.iter().all(|p| p.group_id.is_none()),
"Single-part test 942100-1 should NOT have group_id"
);
use mockforge_bench::security_payloads::PayloadLocation;
let body_payloads: Vec<_> = wafbench_payloads
.iter()
.filter(|p| p.description.contains("942240") && p.location == PayloadLocation::Body)
.collect();
assert!(!body_payloads.is_empty(), "Should have body payload for 942240");
let body_payload = &body_payloads[0];
assert!(
body_payload.payload.contains('"'),
"Body payload should have %22 decoded to double-quote, got: {}",
body_payload.payload
);
assert!(
!body_payload.payload.contains("%22"),
"Body payload should NOT contain literal %22, got: {}",
body_payload.payload
);
assert!(
body_payload.payload.contains(' '),
"Body payload should have + decoded to space, got: {}",
body_payload.payload
);
assert!(
body_payload.form_encoded_body.is_some(),
"Body payload should have form_encoded_body set for form-encoded delivery"
);
assert_eq!(
body_payload.form_encoded_body.as_deref().unwrap(),
"%22+WAITFOR+DELAY+%270%3A0%3A5%27",
"form_encoded_body should preserve the raw CRS data value"
);
let uri_with_query: Vec<_> = wafbench_payloads
.iter()
.filter(|p| p.description.contains("942100") && p.location == PayloadLocation::Uri)
.collect();
assert!(!uri_with_query.is_empty(), "Should have URI payload for 942100");
assert!(
uri_with_query[0].inject_as_path.is_none(),
"URI payload with query params should NOT have inject_as_path"
);
let spec_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("billing_subscriptions_v1.json");
let parser = SpecParser::from_file(&spec_path).await.expect("Should parse spec");
let operations = parser.get_operations();
let templates: Vec<_> = operations
.iter()
.map(RequestGenerator::generate_template)
.collect::<mockforge_bench::error::Result<Vec<_>>>()
.expect("Should generate templates");
let config = K6Config {
target_url: "https://api.example.com".to_string(),
base_path: None,
scenario: LoadScenario::Constant,
duration_secs: 30,
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: true,
chunked_request_bodies: false,
target_rps: None,
no_keep_alive: false,
geo_source_ips: Vec::new(),
geo_source_headers: Vec::new(),
};
let generator = K6ScriptGenerator::new(config, templates);
let mut script = generator.generate().expect("Should generate base k6 script");
let mut additional_code = String::new();
additional_code.push_str(&SecurityTestGenerator::generate_payload_selection(
&wafbench_payloads,
true, ));
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("const groupedPayloads"),
"Script must contain groupedPayloads array"
);
assert!(
script.contains("groupId: '942290-1'"),
"Script must have groupId: '942290-1' for multi-part test case"
);
assert!(
script.contains("groupId: null"),
"Script must have groupId: null for single-part test cases"
);
assert!(
script.contains("groupedPayloads[__payloadIndex]"),
"getNextSecurityPayload should index into groupedPayloads (cycle-all mode)"
);
assert!(
script.contains("for (const secPayload of secPayloadGroup)"),
"Template must loop over secPayloadGroup"
);
assert!(
script.contains("applySecurityPayload(payload, [], secBodyPayload)"),
"Template must use secBodyPayload (not secPayload) for body injection"
);
assert!(
script.contains("WAITFOR DELAY"),
"Body payload 'payload' field must be decoded - should contain 'WAITFOR DELAY' with spaces"
);
assert!(
script.contains("formBody: '%22+WAITFOR"),
"Body payload should have formBody with raw CRS data for form-encoded delivery"
);
assert!(
script.contains("groupMap[p.groupId]"),
"Script must contain grouping logic that collects by groupId"
);
assert!(
script.contains("secPayload.injectAsPath"),
"Script must check injectAsPath for path-based URI injection"
);
assert!(
script.contains("encodeURI(secPayload.payload)"),
"Script must use encodeURI for path replacement"
);
assert!(
script.contains("secBodyPayload.formBody"),
"Script must check formBody for form-encoded body delivery"
);
assert!(
script.contains("decodeURIComponent"),
"Script must decode formBody into object for k6 native form encoding"
);
let _ = std::fs::remove_dir_all(&temp_dir);
println!("\n✓ Issue #79: WAFBench grouped payloads E2E test PASSED");
println!(" - WAFBench YAML loaded: 3 test cases");
println!(" - Multi-part grouping (942290-1): ✓");
println!(" - Single-part no group (942100-1): ✓");
println!(" - Body URL-decoding (942240-1): ✓");
println!(" - groupedPayloads array: ✓");
println!(" - secPayloadGroup loop in template: ✓");
println!(" - secBodyPayload for body injection: ✓");
println!(" - getNextSecurityPayload returns arrays: ✓");
println!(" - injectAsPath for path-based injection: ✓");
println!(" - formBody for form-encoded delivery: ✓");
}