1use crate::crud_flow::{CrudFlowConfig, CrudFlowDetector};
4use crate::data_driven::{DataDistribution, DataDrivenConfig, DataDrivenGenerator, DataMapping};
5use crate::dynamic_params::{DynamicParamProcessor, DynamicPlaceholder};
6use crate::error::{BenchError, Result};
7use crate::executor::K6Executor;
8use crate::invalid_data::{InvalidDataConfig, InvalidDataGenerator};
9use crate::k6_gen::{K6Config, K6ScriptGenerator};
10use crate::mock_integration::{
11 MockIntegrationConfig, MockIntegrationGenerator, MockServerDetector,
12};
13use crate::owasp_api::{OwaspApiConfig, OwaspApiGenerator, OwaspCategory, ReportFormat};
14use crate::parallel_executor::{AggregatedResults, ParallelExecutor};
15use crate::parallel_requests::{ParallelConfig, ParallelRequestGenerator};
16use crate::param_overrides::ParameterOverrides;
17use crate::reporter::TerminalReporter;
18use crate::request_gen::RequestGenerator;
19use crate::scenarios::LoadScenario;
20use crate::security_payloads::{
21 SecurityCategory, SecurityPayload, SecurityPayloads, SecurityTestConfig, SecurityTestGenerator,
22};
23use crate::spec_dependencies::{
24 topological_sort, DependencyDetector, ExtractedValues, SpecDependencyConfig,
25};
26use crate::spec_parser::SpecParser;
27use crate::target_parser::parse_targets_file;
28use crate::wafbench::WafBenchLoader;
29use mockforge_core::openapi::multi_spec::{
30 load_specs_from_directory, load_specs_from_files, merge_specs, ConflictStrategy,
31};
32use mockforge_core::openapi::spec::OpenApiSpec;
33use std::collections::{HashMap, HashSet};
34use std::path::{Path, PathBuf};
35use std::str::FromStr;
36
37pub struct BenchCommand {
39 pub spec: Vec<PathBuf>,
41 pub spec_dir: Option<PathBuf>,
43 pub merge_conflicts: String,
45 pub spec_mode: String,
47 pub dependency_config: Option<PathBuf>,
49 pub target: String,
50 pub base_path: Option<String>,
53 pub duration: String,
54 pub vus: u32,
55 pub scenario: String,
56 pub operations: Option<String>,
57 pub exclude_operations: Option<String>,
61 pub auth: Option<String>,
62 pub headers: Option<String>,
63 pub output: PathBuf,
64 pub generate_only: bool,
65 pub script_output: Option<PathBuf>,
66 pub threshold_percentile: String,
67 pub threshold_ms: u64,
68 pub max_error_rate: f64,
69 pub verbose: bool,
70 pub skip_tls_verify: bool,
71 pub targets_file: Option<PathBuf>,
73 pub max_concurrency: Option<u32>,
75 pub results_format: String,
77 pub params_file: Option<PathBuf>,
82
83 pub crud_flow: bool,
86 pub flow_config: Option<PathBuf>,
88 pub extract_fields: Option<String>,
90
91 pub parallel_create: Option<u32>,
94
95 pub data_file: Option<PathBuf>,
98 pub data_distribution: String,
100 pub data_mappings: Option<String>,
102 pub per_uri_control: bool,
104
105 pub error_rate: Option<f64>,
108 pub error_types: Option<String>,
110
111 pub security_test: bool,
114 pub security_payloads: Option<PathBuf>,
116 pub security_categories: Option<String>,
118 pub security_target_fields: Option<String>,
120
121 pub wafbench_dir: Option<String>,
124 pub wafbench_cycle_all: bool,
126
127 pub conformance: bool,
130 pub conformance_api_key: Option<String>,
132 pub conformance_basic_auth: Option<String>,
134 pub conformance_report: PathBuf,
136 pub conformance_categories: Option<String>,
138 pub conformance_report_format: String,
140 pub conformance_headers: Vec<String>,
143 pub conformance_all_operations: bool,
146 pub conformance_custom: Option<PathBuf>,
148 pub conformance_delay_ms: u64,
151 pub use_k6: bool,
153
154 pub owasp_api_top10: bool,
157 pub owasp_categories: Option<String>,
159 pub owasp_auth_header: String,
161 pub owasp_auth_token: Option<String>,
163 pub owasp_admin_paths: Option<PathBuf>,
165 pub owasp_id_fields: Option<String>,
167 pub owasp_report: Option<PathBuf>,
169 pub owasp_report_format: String,
171 pub owasp_iterations: u32,
173}
174
175impl BenchCommand {
176 pub async fn load_and_merge_specs(&self) -> Result<OpenApiSpec> {
178 let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
179
180 if !self.spec.is_empty() {
182 let specs = load_specs_from_files(self.spec.clone())
183 .await
184 .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
185 all_specs.extend(specs);
186 }
187
188 if let Some(spec_dir) = &self.spec_dir {
190 let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
191 BenchError::Other(format!("Failed to load specs from directory: {}", e))
192 })?;
193 all_specs.extend(dir_specs);
194 }
195
196 if all_specs.is_empty() {
197 return Err(BenchError::Other(
198 "No spec files provided. Use --spec or --spec-dir.".to_string(),
199 ));
200 }
201
202 if all_specs.len() == 1 {
204 return Ok(all_specs.into_iter().next().expect("checked len() == 1 above").1);
206 }
207
208 let conflict_strategy = match self.merge_conflicts.as_str() {
210 "first" => ConflictStrategy::First,
211 "last" => ConflictStrategy::Last,
212 _ => ConflictStrategy::Error,
213 };
214
215 merge_specs(all_specs, conflict_strategy)
216 .map_err(|e| BenchError::Other(format!("Failed to merge specs: {}", e)))
217 }
218
219 fn get_spec_display_name(&self) -> String {
221 if self.spec.len() == 1 {
222 self.spec[0].to_string_lossy().to_string()
223 } else if !self.spec.is_empty() {
224 format!("{} spec files", self.spec.len())
225 } else if let Some(dir) = &self.spec_dir {
226 format!("specs from {}", dir.display())
227 } else {
228 "no specs".to_string()
229 }
230 }
231
232 pub async fn execute(&self) -> Result<()> {
234 if let Some(targets_file) = &self.targets_file {
236 return self.execute_multi_target(targets_file).await;
237 }
238
239 if self.spec_mode == "sequential" && (self.spec.len() > 1 || self.spec_dir.is_some()) {
241 return self.execute_sequential_specs().await;
242 }
243
244 TerminalReporter::print_header(
247 &self.get_spec_display_name(),
248 &self.target,
249 0, &self.scenario,
251 Self::parse_duration(&self.duration)?,
252 );
253
254 if !K6Executor::is_k6_installed() {
256 TerminalReporter::print_error("k6 is not installed");
257 TerminalReporter::print_warning(
258 "Install k6 from: https://k6.io/docs/get-started/installation/",
259 );
260 return Err(BenchError::K6NotFound);
261 }
262
263 if self.conformance {
265 return self.execute_conformance_test().await;
266 }
267
268 TerminalReporter::print_progress("Loading OpenAPI specification(s)...");
270 let merged_spec = self.load_and_merge_specs().await?;
271 let parser = SpecParser::from_spec(merged_spec);
272 if self.spec.len() > 1 || self.spec_dir.is_some() {
273 TerminalReporter::print_success(&format!(
274 "Loaded and merged {} specification(s)",
275 self.spec.len() + self.spec_dir.as_ref().map(|_| 1).unwrap_or(0)
276 ));
277 } else {
278 TerminalReporter::print_success("Specification loaded");
279 }
280
281 let mock_config = self.build_mock_config().await;
283 if mock_config.is_mock_server {
284 TerminalReporter::print_progress("Mock server integration enabled");
285 }
286
287 if self.crud_flow {
289 return self.execute_crud_flow(&parser).await;
290 }
291
292 if self.owasp_api_top10 {
294 return self.execute_owasp_test(&parser).await;
295 }
296
297 TerminalReporter::print_progress("Extracting API operations...");
299 let mut operations = if let Some(filter) = &self.operations {
300 parser.filter_operations(filter)?
301 } else {
302 parser.get_operations()
303 };
304
305 if let Some(exclude) = &self.exclude_operations {
307 let before_count = operations.len();
308 operations = parser.exclude_operations(operations, exclude)?;
309 let excluded_count = before_count - operations.len();
310 if excluded_count > 0 {
311 TerminalReporter::print_progress(&format!(
312 "Excluded {} operations matching '{}'",
313 excluded_count, exclude
314 ));
315 }
316 }
317
318 if operations.is_empty() {
319 return Err(BenchError::Other("No operations found in spec".to_string()));
320 }
321
322 TerminalReporter::print_success(&format!("Found {} operations", operations.len()));
323
324 let param_overrides = if let Some(params_file) = &self.params_file {
326 TerminalReporter::print_progress("Loading parameter overrides...");
327 let overrides = ParameterOverrides::from_file(params_file)?;
328 TerminalReporter::print_success(&format!(
329 "Loaded parameter overrides ({} operation-specific, {} defaults)",
330 overrides.operations.len(),
331 if overrides.defaults.is_empty() { 0 } else { 1 }
332 ));
333 Some(overrides)
334 } else {
335 None
336 };
337
338 TerminalReporter::print_progress("Generating request templates...");
340 let templates: Vec<_> = operations
341 .iter()
342 .map(|op| {
343 let op_overrides = param_overrides.as_ref().map(|po| {
344 po.get_for_operation(op.operation_id.as_deref(), &op.method, &op.path)
345 });
346 RequestGenerator::generate_template_with_overrides(op, op_overrides.as_ref())
347 })
348 .collect::<Result<Vec<_>>>()?;
349 TerminalReporter::print_success("Request templates generated");
350
351 let custom_headers = self.parse_headers()?;
353
354 let base_path = self.resolve_base_path(&parser);
356 if let Some(ref bp) = base_path {
357 TerminalReporter::print_progress(&format!("Using base path: {}", bp));
358 }
359
360 TerminalReporter::print_progress("Generating k6 load test script...");
362 let scenario =
363 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
364
365 let security_testing_enabled = self.security_test || self.wafbench_dir.is_some();
366
367 let k6_config = K6Config {
368 target_url: self.target.clone(),
369 base_path,
370 scenario,
371 duration_secs: Self::parse_duration(&self.duration)?,
372 max_vus: self.vus,
373 threshold_percentile: self.threshold_percentile.clone(),
374 threshold_ms: self.threshold_ms,
375 max_error_rate: self.max_error_rate,
376 auth_header: self.auth.clone(),
377 custom_headers,
378 skip_tls_verify: self.skip_tls_verify,
379 security_testing_enabled,
380 };
381
382 let generator = K6ScriptGenerator::new(k6_config, templates);
383 let mut script = generator.generate()?;
384 TerminalReporter::print_success("k6 script generated");
385
386 let has_advanced_features = self.data_file.is_some()
388 || self.error_rate.is_some()
389 || self.security_test
390 || self.parallel_create.is_some()
391 || self.wafbench_dir.is_some();
392
393 if has_advanced_features {
395 script = self.generate_enhanced_script(&script)?;
396 }
397
398 if mock_config.is_mock_server {
400 let setup_code = MockIntegrationGenerator::generate_setup(&mock_config);
401 let teardown_code = MockIntegrationGenerator::generate_teardown(&mock_config);
402 let helper_code = MockIntegrationGenerator::generate_vu_id_helper();
403
404 if let Some(import_end) = script.find("export const options") {
406 script.insert_str(
407 import_end,
408 &format!(
409 "\n// === Mock Server Integration ===\n{}\n{}\n{}\n",
410 helper_code, setup_code, teardown_code
411 ),
412 );
413 }
414 }
415
416 TerminalReporter::print_progress("Validating k6 script...");
418 let validation_errors = K6ScriptGenerator::validate_script(&script);
419 if !validation_errors.is_empty() {
420 TerminalReporter::print_error("Script validation failed");
421 for error in &validation_errors {
422 eprintln!(" {}", error);
423 }
424 return Err(BenchError::Other(format!(
425 "Generated k6 script has {} validation error(s). Please check the output above.",
426 validation_errors.len()
427 )));
428 }
429 TerminalReporter::print_success("Script validation passed");
430
431 let script_path = if let Some(output) = &self.script_output {
433 output.clone()
434 } else {
435 self.output.join("k6-script.js")
436 };
437
438 if let Some(parent) = script_path.parent() {
439 std::fs::create_dir_all(parent)?;
440 }
441 std::fs::write(&script_path, &script)?;
442 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
443
444 if self.generate_only {
446 println!("\nScript generated successfully. Run it with:");
447 println!(" k6 run {}", script_path.display());
448 return Ok(());
449 }
450
451 TerminalReporter::print_progress("Executing load test...");
453 let executor = K6Executor::new()?;
454
455 std::fs::create_dir_all(&self.output)?;
456
457 let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
458
459 let duration_secs = Self::parse_duration(&self.duration)?;
461 TerminalReporter::print_summary(&results, duration_secs);
462
463 println!("\nResults saved to: {}", self.output.display());
464
465 Ok(())
466 }
467
468 async fn execute_multi_target(&self, targets_file: &Path) -> Result<()> {
470 TerminalReporter::print_progress("Parsing targets file...");
471 let targets = parse_targets_file(targets_file)?;
472 let num_targets = targets.len();
473 TerminalReporter::print_success(&format!("Loaded {} targets", num_targets));
474
475 if targets.is_empty() {
476 return Err(BenchError::Other("No targets found in file".to_string()));
477 }
478
479 let max_concurrency = self.max_concurrency.unwrap_or(10) as usize;
481 let max_concurrency = max_concurrency.min(num_targets); TerminalReporter::print_header(
485 &self.get_spec_display_name(),
486 &format!("{} targets", num_targets),
487 0,
488 &self.scenario,
489 Self::parse_duration(&self.duration)?,
490 );
491
492 let executor = ParallelExecutor::new(
494 BenchCommand {
495 spec: self.spec.clone(),
497 spec_dir: self.spec_dir.clone(),
498 merge_conflicts: self.merge_conflicts.clone(),
499 spec_mode: self.spec_mode.clone(),
500 dependency_config: self.dependency_config.clone(),
501 target: self.target.clone(), base_path: self.base_path.clone(),
503 duration: self.duration.clone(),
504 vus: self.vus,
505 scenario: self.scenario.clone(),
506 operations: self.operations.clone(),
507 exclude_operations: self.exclude_operations.clone(),
508 auth: self.auth.clone(),
509 headers: self.headers.clone(),
510 output: self.output.clone(),
511 generate_only: self.generate_only,
512 script_output: self.script_output.clone(),
513 threshold_percentile: self.threshold_percentile.clone(),
514 threshold_ms: self.threshold_ms,
515 max_error_rate: self.max_error_rate,
516 verbose: self.verbose,
517 skip_tls_verify: self.skip_tls_verify,
518 targets_file: None,
519 max_concurrency: None,
520 results_format: self.results_format.clone(),
521 params_file: self.params_file.clone(),
522 crud_flow: self.crud_flow,
523 flow_config: self.flow_config.clone(),
524 extract_fields: self.extract_fields.clone(),
525 parallel_create: self.parallel_create,
526 data_file: self.data_file.clone(),
527 data_distribution: self.data_distribution.clone(),
528 data_mappings: self.data_mappings.clone(),
529 per_uri_control: self.per_uri_control,
530 error_rate: self.error_rate,
531 error_types: self.error_types.clone(),
532 security_test: self.security_test,
533 security_payloads: self.security_payloads.clone(),
534 security_categories: self.security_categories.clone(),
535 security_target_fields: self.security_target_fields.clone(),
536 wafbench_dir: self.wafbench_dir.clone(),
537 wafbench_cycle_all: self.wafbench_cycle_all,
538 owasp_api_top10: self.owasp_api_top10,
539 owasp_categories: self.owasp_categories.clone(),
540 owasp_auth_header: self.owasp_auth_header.clone(),
541 owasp_auth_token: self.owasp_auth_token.clone(),
542 owasp_admin_paths: self.owasp_admin_paths.clone(),
543 owasp_id_fields: self.owasp_id_fields.clone(),
544 owasp_report: self.owasp_report.clone(),
545 owasp_report_format: self.owasp_report_format.clone(),
546 owasp_iterations: self.owasp_iterations,
547 conformance: false,
548 conformance_api_key: None,
549 conformance_basic_auth: None,
550 conformance_report: PathBuf::from("conformance-report.json"),
551 conformance_categories: None,
552 conformance_report_format: "json".to_string(),
553 conformance_headers: vec![],
554 conformance_all_operations: false,
555 conformance_custom: None,
556 conformance_delay_ms: 0,
557 use_k6: false,
558 },
559 targets,
560 max_concurrency,
561 );
562
563 let aggregated_results = executor.execute_all().await?;
565
566 self.report_multi_target_results(&aggregated_results)?;
568
569 Ok(())
570 }
571
572 fn report_multi_target_results(&self, results: &AggregatedResults) -> Result<()> {
574 TerminalReporter::print_multi_target_summary(results);
576
577 if self.results_format == "aggregated" || self.results_format == "both" {
579 let summary_path = self.output.join("aggregated_summary.json");
580 let summary_json = serde_json::json!({
581 "total_targets": results.total_targets,
582 "successful_targets": results.successful_targets,
583 "failed_targets": results.failed_targets,
584 "aggregated_metrics": {
585 "total_requests": results.aggregated_metrics.total_requests,
586 "total_failed_requests": results.aggregated_metrics.total_failed_requests,
587 "avg_duration_ms": results.aggregated_metrics.avg_duration_ms,
588 "p95_duration_ms": results.aggregated_metrics.p95_duration_ms,
589 "p99_duration_ms": results.aggregated_metrics.p99_duration_ms,
590 "error_rate": results.aggregated_metrics.error_rate,
591 "total_rps": results.aggregated_metrics.total_rps,
592 "avg_rps": results.aggregated_metrics.avg_rps,
593 "total_vus_max": results.aggregated_metrics.total_vus_max,
594 },
595 "target_results": results.target_results.iter().map(|r| {
596 serde_json::json!({
597 "target_url": r.target_url,
598 "target_index": r.target_index,
599 "success": r.success,
600 "error": r.error,
601 "total_requests": r.results.total_requests,
602 "failed_requests": r.results.failed_requests,
603 "avg_duration_ms": r.results.avg_duration_ms,
604 "min_duration_ms": r.results.min_duration_ms,
605 "med_duration_ms": r.results.med_duration_ms,
606 "p90_duration_ms": r.results.p90_duration_ms,
607 "p95_duration_ms": r.results.p95_duration_ms,
608 "p99_duration_ms": r.results.p99_duration_ms,
609 "max_duration_ms": r.results.max_duration_ms,
610 "rps": r.results.rps,
611 "vus_max": r.results.vus_max,
612 "output_dir": r.output_dir.to_string_lossy(),
613 })
614 }).collect::<Vec<_>>(),
615 });
616
617 std::fs::write(&summary_path, serde_json::to_string_pretty(&summary_json)?)?;
618 TerminalReporter::print_success(&format!(
619 "Aggregated summary saved to: {}",
620 summary_path.display()
621 ));
622 }
623
624 println!("\nResults saved to: {}", self.output.display());
625 println!(" - Per-target results: {}", self.output.join("target_*").display());
626 if self.results_format == "aggregated" || self.results_format == "both" {
627 println!(
628 " - Aggregated summary: {}",
629 self.output.join("aggregated_summary.json").display()
630 );
631 }
632
633 Ok(())
634 }
635
636 pub fn parse_duration(duration: &str) -> Result<u64> {
638 let duration = duration.trim();
639
640 if let Some(secs) = duration.strip_suffix('s') {
641 secs.parse::<u64>()
642 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
643 } else if let Some(mins) = duration.strip_suffix('m') {
644 mins.parse::<u64>()
645 .map(|m| m * 60)
646 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
647 } else if let Some(hours) = duration.strip_suffix('h') {
648 hours
649 .parse::<u64>()
650 .map(|h| h * 3600)
651 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
652 } else {
653 duration
655 .parse::<u64>()
656 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
657 }
658 }
659
660 pub fn parse_headers(&self) -> Result<HashMap<String, String>> {
662 let mut headers = HashMap::new();
663
664 if let Some(header_str) = &self.headers {
665 for pair in header_str.split(',') {
666 let parts: Vec<&str> = pair.splitn(2, ':').collect();
667 if parts.len() != 2 {
668 return Err(BenchError::Other(format!(
669 "Invalid header format: '{}'. Expected 'Key:Value'",
670 pair
671 )));
672 }
673 headers.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
674 }
675 }
676
677 Ok(headers)
678 }
679
680 fn parse_extracted_values(output_dir: &Path) -> Result<ExtractedValues> {
681 let extracted_path = output_dir.join("extracted_values.json");
682 if !extracted_path.exists() {
683 return Ok(ExtractedValues::new());
684 }
685
686 let content = std::fs::read_to_string(&extracted_path)
687 .map_err(|e| BenchError::ResultsParseError(e.to_string()))?;
688 let parsed: serde_json::Value = serde_json::from_str(&content)
689 .map_err(|e| BenchError::ResultsParseError(e.to_string()))?;
690
691 let mut extracted = ExtractedValues::new();
692 if let Some(values) = parsed.as_object() {
693 for (key, value) in values {
694 extracted.set(key.clone(), value.clone());
695 }
696 }
697
698 Ok(extracted)
699 }
700
701 fn resolve_base_path(&self, parser: &SpecParser) -> Option<String> {
710 if let Some(cli_base_path) = &self.base_path {
712 if cli_base_path.is_empty() {
713 return None;
715 }
716 return Some(cli_base_path.clone());
717 }
718
719 parser.get_base_path()
721 }
722
723 async fn build_mock_config(&self) -> MockIntegrationConfig {
725 if MockServerDetector::looks_like_mock_server(&self.target) {
727 if let Ok(info) = MockServerDetector::detect(&self.target).await {
729 if info.is_mockforge {
730 TerminalReporter::print_success(&format!(
731 "Detected MockForge server (version: {})",
732 info.version.as_deref().unwrap_or("unknown")
733 ));
734 return MockIntegrationConfig::mock_server();
735 }
736 }
737 }
738 MockIntegrationConfig::real_api()
739 }
740
741 fn build_crud_flow_config(&self) -> Option<CrudFlowConfig> {
743 if !self.crud_flow {
744 return None;
745 }
746
747 if let Some(config_path) = &self.flow_config {
749 match CrudFlowConfig::from_file(config_path) {
750 Ok(config) => return Some(config),
751 Err(e) => {
752 TerminalReporter::print_warning(&format!(
753 "Failed to load flow config: {}. Using auto-detection.",
754 e
755 ));
756 }
757 }
758 }
759
760 let extract_fields = self
762 .extract_fields
763 .as_ref()
764 .map(|f| f.split(',').map(|s| s.trim().to_string()).collect())
765 .unwrap_or_else(|| vec!["id".to_string(), "uuid".to_string()]);
766
767 Some(CrudFlowConfig {
768 flows: Vec::new(), default_extract_fields: extract_fields,
770 })
771 }
772
773 fn build_data_driven_config(&self) -> Option<DataDrivenConfig> {
775 let data_file = self.data_file.as_ref()?;
776
777 let distribution = DataDistribution::from_str(&self.data_distribution)
778 .unwrap_or(DataDistribution::UniquePerVu);
779
780 let mappings = self
781 .data_mappings
782 .as_ref()
783 .map(|m| DataMapping::parse_mappings(m).unwrap_or_default())
784 .unwrap_or_default();
785
786 Some(DataDrivenConfig {
787 file_path: data_file.to_string_lossy().to_string(),
788 distribution,
789 mappings,
790 csv_has_header: true,
791 per_uri_control: self.per_uri_control,
792 per_uri_columns: crate::data_driven::PerUriColumns::default(),
793 })
794 }
795
796 fn build_invalid_data_config(&self) -> Option<InvalidDataConfig> {
798 let error_rate = self.error_rate?;
799
800 let error_types = self
801 .error_types
802 .as_ref()
803 .map(|types| InvalidDataConfig::parse_error_types(types).unwrap_or_default())
804 .unwrap_or_default();
805
806 Some(InvalidDataConfig {
807 error_rate,
808 error_types,
809 target_fields: Vec::new(),
810 })
811 }
812
813 fn build_security_config(&self) -> Option<SecurityTestConfig> {
815 if !self.security_test {
816 return None;
817 }
818
819 let categories = self
820 .security_categories
821 .as_ref()
822 .map(|cats| SecurityTestConfig::parse_categories(cats).unwrap_or_default())
823 .unwrap_or_else(|| {
824 let mut default = HashSet::new();
825 default.insert(SecurityCategory::SqlInjection);
826 default.insert(SecurityCategory::Xss);
827 default
828 });
829
830 let target_fields = self
831 .security_target_fields
832 .as_ref()
833 .map(|fields| fields.split(',').map(|f| f.trim().to_string()).collect())
834 .unwrap_or_default();
835
836 let custom_payloads_file =
837 self.security_payloads.as_ref().map(|p| p.to_string_lossy().to_string());
838
839 Some(SecurityTestConfig {
840 enabled: true,
841 categories,
842 target_fields,
843 custom_payloads_file,
844 include_high_risk: false,
845 })
846 }
847
848 fn build_parallel_config(&self) -> Option<ParallelConfig> {
850 let count = self.parallel_create?;
851
852 Some(ParallelConfig::new(count))
853 }
854
855 fn load_wafbench_payloads(&self) -> Vec<SecurityPayload> {
857 let Some(ref wafbench_dir) = self.wafbench_dir else {
858 return Vec::new();
859 };
860
861 let mut loader = WafBenchLoader::new();
862
863 if let Err(e) = loader.load_from_pattern(wafbench_dir) {
864 TerminalReporter::print_warning(&format!("Failed to load WAFBench tests: {}", e));
865 return Vec::new();
866 }
867
868 let stats = loader.stats();
869
870 if stats.files_processed == 0 {
871 TerminalReporter::print_warning(&format!(
872 "No WAFBench YAML files found matching '{}'",
873 wafbench_dir
874 ));
875 if !stats.parse_errors.is_empty() {
877 TerminalReporter::print_warning("Some files were found but failed to parse:");
878 for error in &stats.parse_errors {
879 TerminalReporter::print_warning(&format!(" - {}", error));
880 }
881 }
882 return Vec::new();
883 }
884
885 TerminalReporter::print_progress(&format!(
886 "Loaded {} WAFBench files, {} test cases, {} payloads",
887 stats.files_processed, stats.test_cases_loaded, stats.payloads_extracted
888 ));
889
890 for (category, count) in &stats.by_category {
892 TerminalReporter::print_progress(&format!(" - {}: {} tests", category, count));
893 }
894
895 for error in &stats.parse_errors {
897 TerminalReporter::print_warning(&format!(" Parse error: {}", error));
898 }
899
900 loader.to_security_payloads()
901 }
902
903 pub(crate) fn generate_enhanced_script(&self, base_script: &str) -> Result<String> {
905 let mut enhanced_script = base_script.to_string();
906 let mut additional_code = String::new();
907
908 if let Some(config) = self.build_data_driven_config() {
910 TerminalReporter::print_progress("Adding data-driven testing support...");
911 additional_code.push_str(&DataDrivenGenerator::generate_setup(&config));
912 additional_code.push('\n');
913 TerminalReporter::print_success("Data-driven testing enabled");
914 }
915
916 if let Some(config) = self.build_invalid_data_config() {
918 TerminalReporter::print_progress("Adding invalid data testing support...");
919 additional_code.push_str(&InvalidDataGenerator::generate_invalidation_logic());
920 additional_code.push('\n');
921 additional_code
922 .push_str(&InvalidDataGenerator::generate_should_invalidate(config.error_rate));
923 additional_code.push('\n');
924 additional_code
925 .push_str(&InvalidDataGenerator::generate_type_selection(&config.error_types));
926 additional_code.push('\n');
927 TerminalReporter::print_success(&format!(
928 "Invalid data testing enabled ({}% error rate)",
929 (self.error_rate.unwrap_or(0.0) * 100.0) as u32
930 ));
931 }
932
933 let security_config = self.build_security_config();
935 let wafbench_payloads = self.load_wafbench_payloads();
936 let security_requested = security_config.is_some() || self.wafbench_dir.is_some();
937
938 if security_config.is_some() || !wafbench_payloads.is_empty() {
939 TerminalReporter::print_progress("Adding security testing support...");
940
941 let mut payload_list: Vec<SecurityPayload> = Vec::new();
943
944 if let Some(ref config) = security_config {
945 payload_list.extend(SecurityPayloads::get_payloads(config));
946 }
947
948 if !wafbench_payloads.is_empty() {
950 TerminalReporter::print_progress(&format!(
951 "Loading {} WAFBench attack patterns...",
952 wafbench_payloads.len()
953 ));
954 payload_list.extend(wafbench_payloads);
955 }
956
957 let target_fields =
958 security_config.as_ref().map(|c| c.target_fields.clone()).unwrap_or_default();
959
960 additional_code.push_str(&SecurityTestGenerator::generate_payload_selection(
961 &payload_list,
962 self.wafbench_cycle_all,
963 ));
964 additional_code.push('\n');
965 additional_code
966 .push_str(&SecurityTestGenerator::generate_apply_payload(&target_fields));
967 additional_code.push('\n');
968 additional_code.push_str(&SecurityTestGenerator::generate_security_checks());
969 additional_code.push('\n');
970
971 let mode = if self.wafbench_cycle_all {
972 "cycle-all"
973 } else {
974 "random"
975 };
976 TerminalReporter::print_success(&format!(
977 "Security testing enabled ({} payloads, {} mode)",
978 payload_list.len(),
979 mode
980 ));
981 } else if security_requested {
982 TerminalReporter::print_warning(
986 "Security testing was requested but no payloads were loaded. \
987 Ensure --wafbench-dir points to valid CRS YAML files or add --security-test.",
988 );
989 additional_code
990 .push_str(&SecurityTestGenerator::generate_payload_selection(&[], false));
991 additional_code.push('\n');
992 additional_code.push_str(&SecurityTestGenerator::generate_apply_payload(&[]));
993 additional_code.push('\n');
994 }
995
996 if let Some(config) = self.build_parallel_config() {
998 TerminalReporter::print_progress("Adding parallel execution support...");
999 additional_code.push_str(&ParallelRequestGenerator::generate_batch_helper(&config));
1000 additional_code.push('\n');
1001 TerminalReporter::print_success(&format!(
1002 "Parallel execution enabled (count: {})",
1003 config.count
1004 ));
1005 }
1006
1007 if !additional_code.is_empty() {
1009 if let Some(import_end) = enhanced_script.find("export const options") {
1011 enhanced_script.insert_str(
1012 import_end,
1013 &format!("\n// === Advanced Testing Features ===\n{}\n", additional_code),
1014 );
1015 }
1016 }
1017
1018 Ok(enhanced_script)
1019 }
1020
1021 async fn execute_sequential_specs(&self) -> Result<()> {
1023 TerminalReporter::print_progress("Sequential spec mode: Loading specs individually...");
1024
1025 let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
1027
1028 if !self.spec.is_empty() {
1029 let specs = load_specs_from_files(self.spec.clone())
1030 .await
1031 .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
1032 all_specs.extend(specs);
1033 }
1034
1035 if let Some(spec_dir) = &self.spec_dir {
1036 let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
1037 BenchError::Other(format!("Failed to load specs from directory: {}", e))
1038 })?;
1039 all_specs.extend(dir_specs);
1040 }
1041
1042 if all_specs.is_empty() {
1043 return Err(BenchError::Other(
1044 "No spec files found for sequential execution".to_string(),
1045 ));
1046 }
1047
1048 TerminalReporter::print_success(&format!("Loaded {} spec(s)", all_specs.len()));
1049
1050 let execution_order = if let Some(config_path) = &self.dependency_config {
1052 TerminalReporter::print_progress("Loading dependency configuration...");
1053 let config = SpecDependencyConfig::from_file(config_path)?;
1054
1055 if !config.disable_auto_detect && config.execution_order.is_empty() {
1056 self.detect_and_sort_specs(&all_specs)?
1058 } else {
1059 config.execution_order.iter().flat_map(|g| g.specs.clone()).collect()
1061 }
1062 } else {
1063 self.detect_and_sort_specs(&all_specs)?
1065 };
1066
1067 TerminalReporter::print_success(&format!(
1068 "Execution order: {}",
1069 execution_order
1070 .iter()
1071 .map(|p| p.file_name().unwrap_or_default().to_string_lossy().to_string())
1072 .collect::<Vec<_>>()
1073 .join(" → ")
1074 ));
1075
1076 let mut extracted_values = ExtractedValues::new();
1078 let total_specs = execution_order.len();
1079
1080 for (index, spec_path) in execution_order.iter().enumerate() {
1081 let spec_name = spec_path.file_name().unwrap_or_default().to_string_lossy().to_string();
1082
1083 TerminalReporter::print_progress(&format!(
1084 "[{}/{}] Executing spec: {}",
1085 index + 1,
1086 total_specs,
1087 spec_name
1088 ));
1089
1090 let spec = all_specs
1092 .iter()
1093 .find(|(p, _)| {
1094 p == spec_path
1095 || p.file_name() == spec_path.file_name()
1096 || p.file_name() == Some(spec_path.as_os_str())
1097 })
1098 .map(|(_, s)| s.clone())
1099 .ok_or_else(|| {
1100 BenchError::Other(format!("Spec not found: {}", spec_path.display()))
1101 })?;
1102
1103 let new_values = self.execute_single_spec(&spec, &spec_name, &extracted_values).await?;
1105
1106 extracted_values.merge(&new_values);
1108
1109 TerminalReporter::print_success(&format!(
1110 "[{}/{}] Completed: {} (extracted {} values)",
1111 index + 1,
1112 total_specs,
1113 spec_name,
1114 new_values.values.len()
1115 ));
1116 }
1117
1118 TerminalReporter::print_success(&format!(
1119 "Sequential execution complete: {} specs executed",
1120 total_specs
1121 ));
1122
1123 Ok(())
1124 }
1125
1126 fn detect_and_sort_specs(&self, specs: &[(PathBuf, OpenApiSpec)]) -> Result<Vec<PathBuf>> {
1128 TerminalReporter::print_progress("Auto-detecting spec dependencies...");
1129
1130 let mut detector = DependencyDetector::new();
1131 let dependencies = detector.detect_dependencies(specs);
1132
1133 if dependencies.is_empty() {
1134 TerminalReporter::print_progress("No dependencies detected, using file order");
1135 return Ok(specs.iter().map(|(p, _)| p.clone()).collect());
1136 }
1137
1138 TerminalReporter::print_progress(&format!(
1139 "Detected {} cross-spec dependencies",
1140 dependencies.len()
1141 ));
1142
1143 for dep in &dependencies {
1144 TerminalReporter::print_progress(&format!(
1145 " {} → {} (via field '{}')",
1146 dep.dependency_spec.file_name().unwrap_or_default().to_string_lossy(),
1147 dep.dependent_spec.file_name().unwrap_or_default().to_string_lossy(),
1148 dep.field_name
1149 ));
1150 }
1151
1152 topological_sort(specs, &dependencies)
1153 }
1154
1155 async fn execute_single_spec(
1157 &self,
1158 spec: &OpenApiSpec,
1159 spec_name: &str,
1160 _external_values: &ExtractedValues,
1161 ) -> Result<ExtractedValues> {
1162 let parser = SpecParser::from_spec(spec.clone());
1163
1164 if self.crud_flow {
1166 self.execute_crud_flow_with_extraction(&parser, spec_name).await
1168 } else {
1169 self.execute_standard_spec(&parser, spec_name).await?;
1171 Ok(ExtractedValues::new())
1172 }
1173 }
1174
1175 async fn execute_crud_flow_with_extraction(
1177 &self,
1178 parser: &SpecParser,
1179 spec_name: &str,
1180 ) -> Result<ExtractedValues> {
1181 let operations = parser.get_operations();
1182 let flows = CrudFlowDetector::detect_flows(&operations);
1183
1184 if flows.is_empty() {
1185 TerminalReporter::print_warning(&format!("No CRUD flows detected in {}", spec_name));
1186 return Ok(ExtractedValues::new());
1187 }
1188
1189 TerminalReporter::print_progress(&format!(
1190 " {} CRUD flow(s) in {}",
1191 flows.len(),
1192 spec_name
1193 ));
1194
1195 let mut handlebars = handlebars::Handlebars::new();
1197 handlebars.register_helper(
1199 "json",
1200 Box::new(
1201 |h: &handlebars::Helper,
1202 _: &handlebars::Handlebars,
1203 _: &handlebars::Context,
1204 _: &mut handlebars::RenderContext,
1205 out: &mut dyn handlebars::Output|
1206 -> handlebars::HelperResult {
1207 let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1208 out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1209 Ok(())
1210 },
1211 ),
1212 );
1213 let template = include_str!("templates/k6_crud_flow.hbs");
1214 let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1215
1216 let custom_headers = self.parse_headers()?;
1217 let config = self.build_crud_flow_config().unwrap_or_default();
1218
1219 let param_overrides = if let Some(params_file) = &self.params_file {
1221 let overrides = ParameterOverrides::from_file(params_file)?;
1222 Some(overrides)
1223 } else {
1224 None
1225 };
1226
1227 let duration_secs = Self::parse_duration(&self.duration)?;
1229 let scenario =
1230 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1231 let stages = scenario.generate_stages(duration_secs, self.vus);
1232
1233 let api_base_path = self.resolve_base_path(parser);
1235
1236 let mut all_headers = custom_headers.clone();
1238 if let Some(auth) = &self.auth {
1239 all_headers.insert("Authorization".to_string(), auth.clone());
1240 }
1241 let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1242
1243 let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1245
1246 let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1247 let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1248 serde_json::json!({
1249 "name": sanitized_name.clone(),
1250 "display_name": f.name,
1251 "base_path": f.base_path,
1252 "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1253 let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1255 let method_raw = if !parts.is_empty() {
1256 parts[0].to_uppercase()
1257 } else {
1258 "GET".to_string()
1259 };
1260 let method = if !parts.is_empty() {
1261 let m = parts[0].to_lowercase();
1262 if m == "delete" { "del".to_string() } else { m }
1264 } else {
1265 "get".to_string()
1266 };
1267 let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1268 let path = if let Some(ref bp) = api_base_path {
1270 format!("{}{}", bp, raw_path)
1271 } else {
1272 raw_path.to_string()
1273 };
1274 let is_get_or_head = method == "get" || method == "head";
1275 let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1277
1278 let body_value = if has_body {
1280 param_overrides.as_ref()
1281 .map(|po| po.get_for_operation(None, &method_raw, raw_path))
1282 .and_then(|oo| oo.body)
1283 .unwrap_or_else(|| serde_json::json!({}))
1284 } else {
1285 serde_json::json!({})
1286 };
1287
1288 let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1290
1291 let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1293 let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1294
1295 serde_json::json!({
1296 "operation": s.operation,
1297 "method": method,
1298 "path": path,
1299 "extract": s.extract,
1300 "use_values": s.use_values,
1301 "use_body": s.use_body,
1302 "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1303 "inject_attacks": s.inject_attacks,
1304 "attack_types": s.attack_types,
1305 "description": s.description,
1306 "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1307 "is_get_or_head": is_get_or_head,
1308 "has_body": has_body,
1309 "body": processed_body.value,
1310 "body_is_dynamic": body_is_dynamic,
1311 "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1312 })
1313 }).collect::<Vec<_>>(),
1314 })
1315 }).collect();
1316
1317 for flow_data in &flows_data {
1319 if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1320 for step in steps {
1321 if let Some(placeholders_arr) =
1322 step.get("_placeholders").and_then(|p| p.as_array())
1323 {
1324 for p_str in placeholders_arr {
1325 if let Some(p_name) = p_str.as_str() {
1326 match p_name {
1327 "VU" => {
1328 all_placeholders.insert(DynamicPlaceholder::VU);
1329 }
1330 "Iteration" => {
1331 all_placeholders.insert(DynamicPlaceholder::Iteration);
1332 }
1333 "Timestamp" => {
1334 all_placeholders.insert(DynamicPlaceholder::Timestamp);
1335 }
1336 "UUID" => {
1337 all_placeholders.insert(DynamicPlaceholder::UUID);
1338 }
1339 "Random" => {
1340 all_placeholders.insert(DynamicPlaceholder::Random);
1341 }
1342 "Counter" => {
1343 all_placeholders.insert(DynamicPlaceholder::Counter);
1344 }
1345 "Date" => {
1346 all_placeholders.insert(DynamicPlaceholder::Date);
1347 }
1348 "VuIter" => {
1349 all_placeholders.insert(DynamicPlaceholder::VuIter);
1350 }
1351 _ => {}
1352 }
1353 }
1354 }
1355 }
1356 }
1357 }
1358 }
1359
1360 let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1362 let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1363
1364 let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1366
1367 let data = serde_json::json!({
1368 "base_url": self.target,
1369 "flows": flows_data,
1370 "extract_fields": config.default_extract_fields,
1371 "duration_secs": duration_secs,
1372 "max_vus": self.vus,
1373 "auth_header": self.auth,
1374 "custom_headers": custom_headers,
1375 "skip_tls_verify": self.skip_tls_verify,
1376 "stages": stages.iter().map(|s| serde_json::json!({
1378 "duration": s.duration,
1379 "target": s.target,
1380 })).collect::<Vec<_>>(),
1381 "threshold_percentile": self.threshold_percentile,
1382 "threshold_ms": self.threshold_ms,
1383 "max_error_rate": self.max_error_rate,
1384 "headers": headers_json,
1385 "dynamic_imports": required_imports,
1386 "dynamic_globals": required_globals,
1387 "extracted_values_output_path": output_dir.join("extracted_values.json").to_string_lossy(),
1388 "security_testing_enabled": security_testing_enabled,
1390 "has_custom_headers": !custom_headers.is_empty(),
1391 });
1392
1393 let mut script = handlebars
1394 .render_template(template, &data)
1395 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1396
1397 if security_testing_enabled {
1399 script = self.generate_enhanced_script(&script)?;
1400 }
1401
1402 let script_path =
1404 self.output.join(format!("k6-{}-crud-flow.js", spec_name.replace('.', "_")));
1405
1406 std::fs::create_dir_all(self.output.clone())?;
1407 std::fs::write(&script_path, &script)?;
1408
1409 if !self.generate_only {
1410 let executor = K6Executor::new()?;
1411 std::fs::create_dir_all(&output_dir)?;
1412
1413 executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1414
1415 let extracted = Self::parse_extracted_values(&output_dir)?;
1416 TerminalReporter::print_progress(&format!(
1417 " Extracted {} value(s) from {}",
1418 extracted.values.len(),
1419 spec_name
1420 ));
1421 return Ok(extracted);
1422 }
1423
1424 Ok(ExtractedValues::new())
1425 }
1426
1427 async fn execute_standard_spec(&self, parser: &SpecParser, spec_name: &str) -> Result<()> {
1429 let mut operations = if let Some(filter) = &self.operations {
1430 parser.filter_operations(filter)?
1431 } else {
1432 parser.get_operations()
1433 };
1434
1435 if let Some(exclude) = &self.exclude_operations {
1436 operations = parser.exclude_operations(operations, exclude)?;
1437 }
1438
1439 if operations.is_empty() {
1440 TerminalReporter::print_warning(&format!("No operations found in {}", spec_name));
1441 return Ok(());
1442 }
1443
1444 TerminalReporter::print_progress(&format!(
1445 " {} operations in {}",
1446 operations.len(),
1447 spec_name
1448 ));
1449
1450 let templates: Vec<_> = operations
1452 .iter()
1453 .map(RequestGenerator::generate_template)
1454 .collect::<Result<Vec<_>>>()?;
1455
1456 let custom_headers = self.parse_headers()?;
1458
1459 let base_path = self.resolve_base_path(parser);
1461
1462 let scenario =
1464 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1465
1466 let security_testing_enabled = self.security_test || self.wafbench_dir.is_some();
1467
1468 let k6_config = K6Config {
1469 target_url: self.target.clone(),
1470 base_path,
1471 scenario,
1472 duration_secs: Self::parse_duration(&self.duration)?,
1473 max_vus: self.vus,
1474 threshold_percentile: self.threshold_percentile.clone(),
1475 threshold_ms: self.threshold_ms,
1476 max_error_rate: self.max_error_rate,
1477 auth_header: self.auth.clone(),
1478 custom_headers,
1479 skip_tls_verify: self.skip_tls_verify,
1480 security_testing_enabled,
1481 };
1482
1483 let generator = K6ScriptGenerator::new(k6_config, templates);
1484 let mut script = generator.generate()?;
1485
1486 let has_advanced_features = self.data_file.is_some()
1488 || self.error_rate.is_some()
1489 || self.security_test
1490 || self.parallel_create.is_some()
1491 || self.wafbench_dir.is_some();
1492
1493 if has_advanced_features {
1494 script = self.generate_enhanced_script(&script)?;
1495 }
1496
1497 let script_path = self.output.join(format!("k6-{}.js", spec_name.replace('.', "_")));
1499
1500 std::fs::create_dir_all(self.output.clone())?;
1501 std::fs::write(&script_path, &script)?;
1502
1503 if !self.generate_only {
1504 let executor = K6Executor::new()?;
1505 let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1506 std::fs::create_dir_all(&output_dir)?;
1507
1508 executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1509 }
1510
1511 Ok(())
1512 }
1513
1514 async fn execute_crud_flow(&self, parser: &SpecParser) -> Result<()> {
1516 let config = self.build_crud_flow_config().unwrap_or_default();
1518
1519 let flows = if !config.flows.is_empty() {
1521 TerminalReporter::print_progress("Using custom flow configuration...");
1522 config.flows.clone()
1523 } else {
1524 TerminalReporter::print_progress("Detecting CRUD operations...");
1525 let operations = parser.get_operations();
1526 CrudFlowDetector::detect_flows(&operations)
1527 };
1528
1529 if flows.is_empty() {
1530 return Err(BenchError::Other(
1531 "No CRUD flows detected in spec. Ensure spec has POST/GET/PUT/DELETE operations on related paths.".to_string(),
1532 ));
1533 }
1534
1535 if config.flows.is_empty() {
1536 TerminalReporter::print_success(&format!("Detected {} CRUD flow(s)", flows.len()));
1537 } else {
1538 TerminalReporter::print_success(&format!("Loaded {} custom flow(s)", flows.len()));
1539 }
1540
1541 for flow in &flows {
1542 TerminalReporter::print_progress(&format!(
1543 " - {}: {} steps",
1544 flow.name,
1545 flow.steps.len()
1546 ));
1547 }
1548
1549 let mut handlebars = handlebars::Handlebars::new();
1551 handlebars.register_helper(
1553 "json",
1554 Box::new(
1555 |h: &handlebars::Helper,
1556 _: &handlebars::Handlebars,
1557 _: &handlebars::Context,
1558 _: &mut handlebars::RenderContext,
1559 out: &mut dyn handlebars::Output|
1560 -> handlebars::HelperResult {
1561 let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1562 out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1563 Ok(())
1564 },
1565 ),
1566 );
1567 let template = include_str!("templates/k6_crud_flow.hbs");
1568
1569 let custom_headers = self.parse_headers()?;
1570
1571 let param_overrides = if let Some(params_file) = &self.params_file {
1573 TerminalReporter::print_progress("Loading parameter overrides...");
1574 let overrides = ParameterOverrides::from_file(params_file)?;
1575 TerminalReporter::print_success(&format!(
1576 "Loaded parameter overrides ({} operation-specific, {} defaults)",
1577 overrides.operations.len(),
1578 if overrides.defaults.is_empty() { 0 } else { 1 }
1579 ));
1580 Some(overrides)
1581 } else {
1582 None
1583 };
1584
1585 let duration_secs = Self::parse_duration(&self.duration)?;
1587 let scenario =
1588 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1589 let stages = scenario.generate_stages(duration_secs, self.vus);
1590
1591 let api_base_path = self.resolve_base_path(parser);
1593 if let Some(ref bp) = api_base_path {
1594 TerminalReporter::print_progress(&format!("Using base path: {}", bp));
1595 }
1596
1597 let mut all_headers = custom_headers.clone();
1599 if let Some(auth) = &self.auth {
1600 all_headers.insert("Authorization".to_string(), auth.clone());
1601 }
1602 let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1603
1604 let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1606
1607 let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1608 let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1610 serde_json::json!({
1611 "name": sanitized_name.clone(), "display_name": f.name, "base_path": f.base_path,
1614 "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1615 let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1617 let method_raw = if !parts.is_empty() {
1618 parts[0].to_uppercase()
1619 } else {
1620 "GET".to_string()
1621 };
1622 let method = if !parts.is_empty() {
1623 let m = parts[0].to_lowercase();
1624 if m == "delete" { "del".to_string() } else { m }
1626 } else {
1627 "get".to_string()
1628 };
1629 let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1630 let path = if let Some(ref bp) = api_base_path {
1632 format!("{}{}", bp, raw_path)
1633 } else {
1634 raw_path.to_string()
1635 };
1636 let is_get_or_head = method == "get" || method == "head";
1637 let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1639
1640 let body_value = if has_body {
1642 param_overrides.as_ref()
1643 .map(|po| po.get_for_operation(None, &method_raw, raw_path))
1644 .and_then(|oo| oo.body)
1645 .unwrap_or_else(|| serde_json::json!({}))
1646 } else {
1647 serde_json::json!({})
1648 };
1649
1650 let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1652 let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1657 let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1658
1659 serde_json::json!({
1660 "operation": s.operation,
1661 "method": method,
1662 "path": path,
1663 "extract": s.extract,
1664 "use_values": s.use_values,
1665 "use_body": s.use_body,
1666 "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1667 "inject_attacks": s.inject_attacks,
1668 "attack_types": s.attack_types,
1669 "description": s.description,
1670 "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1671 "is_get_or_head": is_get_or_head,
1672 "has_body": has_body,
1673 "body": processed_body.value,
1674 "body_is_dynamic": body_is_dynamic,
1675 "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1676 })
1677 }).collect::<Vec<_>>(),
1678 })
1679 }).collect();
1680
1681 for flow_data in &flows_data {
1683 if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1684 for step in steps {
1685 if let Some(placeholders_arr) =
1686 step.get("_placeholders").and_then(|p| p.as_array())
1687 {
1688 for p_str in placeholders_arr {
1689 if let Some(p_name) = p_str.as_str() {
1690 match p_name {
1692 "VU" => {
1693 all_placeholders.insert(DynamicPlaceholder::VU);
1694 }
1695 "Iteration" => {
1696 all_placeholders.insert(DynamicPlaceholder::Iteration);
1697 }
1698 "Timestamp" => {
1699 all_placeholders.insert(DynamicPlaceholder::Timestamp);
1700 }
1701 "UUID" => {
1702 all_placeholders.insert(DynamicPlaceholder::UUID);
1703 }
1704 "Random" => {
1705 all_placeholders.insert(DynamicPlaceholder::Random);
1706 }
1707 "Counter" => {
1708 all_placeholders.insert(DynamicPlaceholder::Counter);
1709 }
1710 "Date" => {
1711 all_placeholders.insert(DynamicPlaceholder::Date);
1712 }
1713 "VuIter" => {
1714 all_placeholders.insert(DynamicPlaceholder::VuIter);
1715 }
1716 _ => {}
1717 }
1718 }
1719 }
1720 }
1721 }
1722 }
1723 }
1724
1725 let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1727 let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1728
1729 let invalid_data_config = self.build_invalid_data_config();
1731 let error_injection_enabled = invalid_data_config.is_some();
1732 let error_rate = self.error_rate.unwrap_or(0.0);
1733 let error_types: Vec<String> = invalid_data_config
1734 .as_ref()
1735 .map(|c| c.error_types.iter().map(|t| format!("{:?}", t)).collect())
1736 .unwrap_or_default();
1737
1738 if error_injection_enabled {
1739 TerminalReporter::print_progress(&format!(
1740 "Error injection enabled ({}% rate)",
1741 (error_rate * 100.0) as u32
1742 ));
1743 }
1744
1745 let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1747
1748 let data = serde_json::json!({
1749 "base_url": self.target,
1750 "flows": flows_data,
1751 "extract_fields": config.default_extract_fields,
1752 "duration_secs": duration_secs,
1753 "max_vus": self.vus,
1754 "auth_header": self.auth,
1755 "custom_headers": custom_headers,
1756 "skip_tls_verify": self.skip_tls_verify,
1757 "stages": stages.iter().map(|s| serde_json::json!({
1759 "duration": s.duration,
1760 "target": s.target,
1761 })).collect::<Vec<_>>(),
1762 "threshold_percentile": self.threshold_percentile,
1763 "threshold_ms": self.threshold_ms,
1764 "max_error_rate": self.max_error_rate,
1765 "headers": headers_json,
1766 "dynamic_imports": required_imports,
1767 "dynamic_globals": required_globals,
1768 "extracted_values_output_path": self
1769 .output
1770 .join("crud_flow_extracted_values.json")
1771 .to_string_lossy(),
1772 "error_injection_enabled": error_injection_enabled,
1774 "error_rate": error_rate,
1775 "error_types": error_types,
1776 "security_testing_enabled": security_testing_enabled,
1778 "has_custom_headers": !custom_headers.is_empty(),
1779 });
1780
1781 let mut script = handlebars
1782 .render_template(template, &data)
1783 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1784
1785 if security_testing_enabled {
1787 script = self.generate_enhanced_script(&script)?;
1788 }
1789
1790 TerminalReporter::print_progress("Validating CRUD flow script...");
1792 let validation_errors = K6ScriptGenerator::validate_script(&script);
1793 if !validation_errors.is_empty() {
1794 TerminalReporter::print_error("CRUD flow script validation failed");
1795 for error in &validation_errors {
1796 eprintln!(" {}", error);
1797 }
1798 return Err(BenchError::Other(format!(
1799 "CRUD flow script validation failed with {} error(s)",
1800 validation_errors.len()
1801 )));
1802 }
1803
1804 TerminalReporter::print_success("CRUD flow script generated");
1805
1806 let script_path = if let Some(output) = &self.script_output {
1808 output.clone()
1809 } else {
1810 self.output.join("k6-crud-flow-script.js")
1811 };
1812
1813 if let Some(parent) = script_path.parent() {
1814 std::fs::create_dir_all(parent)?;
1815 }
1816 std::fs::write(&script_path, &script)?;
1817 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
1818
1819 if self.generate_only {
1820 println!("\nScript generated successfully. Run it with:");
1821 println!(" k6 run {}", script_path.display());
1822 return Ok(());
1823 }
1824
1825 TerminalReporter::print_progress("Executing CRUD flow test...");
1827 let executor = K6Executor::new()?;
1828 std::fs::create_dir_all(&self.output)?;
1829
1830 let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1831
1832 let duration_secs = Self::parse_duration(&self.duration)?;
1833 TerminalReporter::print_summary(&results, duration_secs);
1834
1835 Ok(())
1836 }
1837
1838 async fn execute_conformance_test(&self) -> Result<()> {
1840 use crate::conformance::generator::{ConformanceConfig, ConformanceGenerator};
1841 use crate::conformance::report::ConformanceReport;
1842 use crate::conformance::spec::ConformanceFeature;
1843
1844 TerminalReporter::print_progress("OpenAPI 3.0.0 Conformance Testing Mode");
1845
1846 TerminalReporter::print_progress(
1849 "Conformance mode runs 1 VU, 1 iteration per endpoint (--vus and -d are ignored)",
1850 );
1851
1852 let categories = self.conformance_categories.as_ref().map(|cats_str| {
1854 cats_str
1855 .split(',')
1856 .filter_map(|s| {
1857 let trimmed = s.trim();
1858 if let Some(canonical) = ConformanceFeature::category_from_cli_name(trimmed) {
1859 Some(canonical.to_string())
1860 } else {
1861 TerminalReporter::print_warning(&format!(
1862 "Unknown conformance category: '{}'. Valid categories: {}",
1863 trimmed,
1864 ConformanceFeature::cli_category_names()
1865 .iter()
1866 .map(|(cli, _)| *cli)
1867 .collect::<Vec<_>>()
1868 .join(", ")
1869 ));
1870 None
1871 }
1872 })
1873 .collect::<Vec<String>>()
1874 });
1875
1876 let custom_headers: Vec<(String, String)> = self
1878 .conformance_headers
1879 .iter()
1880 .filter_map(|h| {
1881 let (name, value) = h.split_once(':')?;
1882 Some((name.trim().to_string(), value.trim().to_string()))
1883 })
1884 .collect();
1885
1886 if !custom_headers.is_empty() {
1887 TerminalReporter::print_progress(&format!(
1888 "Using {} custom header(s) for authentication",
1889 custom_headers.len()
1890 ));
1891 }
1892
1893 if self.conformance_delay_ms > 0 {
1894 TerminalReporter::print_progress(&format!(
1895 "Using {}ms delay between conformance requests",
1896 self.conformance_delay_ms
1897 ));
1898 }
1899
1900 std::fs::create_dir_all(&self.output)?;
1902
1903 let config = ConformanceConfig {
1904 target_url: self.target.clone(),
1905 api_key: self.conformance_api_key.clone(),
1906 basic_auth: self.conformance_basic_auth.clone(),
1907 skip_tls_verify: self.skip_tls_verify,
1908 categories,
1909 base_path: self.base_path.clone(),
1910 custom_headers,
1911 output_dir: Some(self.output.clone()),
1912 all_operations: self.conformance_all_operations,
1913 custom_checks_file: self.conformance_custom.clone(),
1914 request_delay_ms: self.conformance_delay_ms,
1915 };
1916
1917 let annotated_ops = if !self.spec.is_empty() {
1920 TerminalReporter::print_progress("Spec-driven conformance mode: analyzing spec...");
1921 let parser = SpecParser::from_file(&self.spec[0]).await?;
1922 let operations = parser.get_operations();
1923
1924 let annotated =
1925 crate::conformance::spec_driven::SpecDrivenConformanceGenerator::annotate_operations(
1926 &operations,
1927 parser.spec(),
1928 );
1929 TerminalReporter::print_success(&format!(
1930 "Analyzed {} operations, found {} feature annotations",
1931 operations.len(),
1932 annotated.iter().map(|a| a.features.len()).sum::<usize>()
1933 ));
1934 Some(annotated)
1935 } else {
1936 None
1937 };
1938
1939 if self.generate_only || self.use_k6 {
1941 let script = if let Some(annotated) = &annotated_ops {
1942 let gen = crate::conformance::spec_driven::SpecDrivenConformanceGenerator::new(
1943 config,
1944 annotated.clone(),
1945 );
1946 let op_count = gen.operation_count();
1947 let (script, check_count) = gen.generate()?;
1948 TerminalReporter::print_success(&format!(
1949 "Conformance: {} operations analyzed, {} unique checks generated",
1950 op_count, check_count
1951 ));
1952 script
1953 } else {
1954 let generator = ConformanceGenerator::new(config);
1955 generator.generate()?
1956 };
1957
1958 let script_path = self.output.join("k6-conformance.js");
1959 std::fs::write(&script_path, &script).map_err(|e| {
1960 BenchError::Other(format!("Failed to write conformance script: {}", e))
1961 })?;
1962 TerminalReporter::print_success(&format!(
1963 "Conformance script generated: {}",
1964 script_path.display()
1965 ));
1966
1967 if self.generate_only {
1968 println!("\nScript generated. Run with:");
1969 println!(" k6 run {}", script_path.display());
1970 return Ok(());
1971 }
1972
1973 if !K6Executor::is_k6_installed() {
1975 TerminalReporter::print_error("k6 is not installed");
1976 TerminalReporter::print_warning(
1977 "Install k6 from: https://k6.io/docs/get-started/installation/",
1978 );
1979 return Err(BenchError::K6NotFound);
1980 }
1981
1982 TerminalReporter::print_progress("Running conformance tests via k6...");
1983 let executor = K6Executor::new()?;
1984 executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1985
1986 let report_path = self.output.join("conformance-report.json");
1987 if report_path.exists() {
1988 let report = ConformanceReport::from_file(&report_path)?;
1989 report.print_report_with_options(self.conformance_all_operations);
1990 self.save_conformance_report(&report, &report_path)?;
1991 } else {
1992 TerminalReporter::print_warning(
1993 "Conformance report not generated (k6 handleSummary may not have run)",
1994 );
1995 }
1996
1997 return Ok(());
1998 }
1999
2000 TerminalReporter::print_progress("Running conformance tests (native executor)...");
2002
2003 let mut executor = crate::conformance::executor::NativeConformanceExecutor::new(config)?;
2004
2005 executor = if let Some(annotated) = &annotated_ops {
2006 executor.with_spec_driven_checks(annotated)
2007 } else {
2008 executor.with_reference_checks()
2009 };
2010 executor = executor.with_custom_checks()?;
2011
2012 TerminalReporter::print_success(&format!(
2013 "Executing {} conformance checks...",
2014 executor.check_count()
2015 ));
2016
2017 let report = executor.execute().await?;
2018 report.print_report_with_options(self.conformance_all_operations);
2019
2020 let failure_details = report.failure_details();
2022 if !failure_details.is_empty() {
2023 let details_path = self.output.join("conformance-failure-details.json");
2024 if let Ok(json) = serde_json::to_string_pretty(&failure_details) {
2025 let _ = std::fs::write(&details_path, json);
2026 TerminalReporter::print_success(&format!(
2027 "Failure details saved to: {}",
2028 details_path.display()
2029 ));
2030 }
2031 }
2032
2033 let report_path = self.output.join("conformance-report.json");
2035 let report_json = serde_json::to_string_pretty(&report.to_json())
2036 .map_err(|e| BenchError::Other(format!("Failed to serialize report: {}", e)))?;
2037 std::fs::write(&report_path, &report_json)
2038 .map_err(|e| BenchError::Other(format!("Failed to write report: {}", e)))?;
2039 TerminalReporter::print_success(&format!("Report saved to: {}", report_path.display()));
2040
2041 self.save_conformance_report(&report, &report_path)?;
2042
2043 Ok(())
2044 }
2045
2046 fn save_conformance_report(
2048 &self,
2049 report: &crate::conformance::report::ConformanceReport,
2050 report_path: &Path,
2051 ) -> Result<()> {
2052 if self.conformance_report_format == "sarif" {
2053 use crate::conformance::sarif::ConformanceSarifReport;
2054 ConformanceSarifReport::write(report, &self.target, &self.conformance_report)?;
2055 TerminalReporter::print_success(&format!(
2056 "SARIF report saved to: {}",
2057 self.conformance_report.display()
2058 ));
2059 } else if self.conformance_report != *report_path {
2060 std::fs::copy(report_path, &self.conformance_report)?;
2061 TerminalReporter::print_success(&format!(
2062 "Report saved to: {}",
2063 self.conformance_report.display()
2064 ));
2065 }
2066 Ok(())
2067 }
2068
2069 async fn execute_owasp_test(&self, parser: &SpecParser) -> Result<()> {
2071 TerminalReporter::print_progress("OWASP API Security Top 10 Testing Mode");
2072
2073 let custom_headers = self.parse_headers()?;
2075
2076 let mut config = OwaspApiConfig::new()
2078 .with_auth_header(&self.owasp_auth_header)
2079 .with_verbose(self.verbose)
2080 .with_insecure(self.skip_tls_verify)
2081 .with_concurrency(self.vus as usize)
2082 .with_iterations(self.owasp_iterations as usize)
2083 .with_base_path(self.base_path.clone())
2084 .with_custom_headers(custom_headers);
2085
2086 if let Some(ref token) = self.owasp_auth_token {
2088 config = config.with_valid_auth_token(token);
2089 }
2090
2091 if let Some(ref cats_str) = self.owasp_categories {
2093 let categories: Vec<OwaspCategory> = cats_str
2094 .split(',')
2095 .filter_map(|s| {
2096 let trimmed = s.trim();
2097 match trimmed.parse::<OwaspCategory>() {
2098 Ok(cat) => Some(cat),
2099 Err(e) => {
2100 TerminalReporter::print_warning(&e);
2101 None
2102 }
2103 }
2104 })
2105 .collect();
2106
2107 if !categories.is_empty() {
2108 config = config.with_categories(categories);
2109 }
2110 }
2111
2112 if let Some(ref admin_paths_file) = self.owasp_admin_paths {
2114 config.admin_paths_file = Some(admin_paths_file.clone());
2115 if let Err(e) = config.load_admin_paths() {
2116 TerminalReporter::print_warning(&format!("Failed to load admin paths file: {}", e));
2117 }
2118 }
2119
2120 if let Some(ref id_fields_str) = self.owasp_id_fields {
2122 let id_fields: Vec<String> = id_fields_str
2123 .split(',')
2124 .map(|s| s.trim().to_string())
2125 .filter(|s| !s.is_empty())
2126 .collect();
2127 if !id_fields.is_empty() {
2128 config = config.with_id_fields(id_fields);
2129 }
2130 }
2131
2132 if let Some(ref report_path) = self.owasp_report {
2134 config = config.with_report_path(report_path);
2135 }
2136 if let Ok(format) = self.owasp_report_format.parse::<ReportFormat>() {
2137 config = config.with_report_format(format);
2138 }
2139
2140 let categories = config.categories_to_test();
2142 TerminalReporter::print_success(&format!(
2143 "Testing {} OWASP categories: {}",
2144 categories.len(),
2145 categories.iter().map(|c| c.cli_name()).collect::<Vec<_>>().join(", ")
2146 ));
2147
2148 if config.valid_auth_token.is_some() {
2149 TerminalReporter::print_progress("Using provided auth token for baseline requests");
2150 }
2151
2152 TerminalReporter::print_progress("Generating OWASP security test script...");
2154 let generator = OwaspApiGenerator::new(config, self.target.clone(), parser);
2155
2156 let script = generator.generate()?;
2158 TerminalReporter::print_success("OWASP security test script generated");
2159
2160 let script_path = if let Some(output) = &self.script_output {
2162 output.clone()
2163 } else {
2164 self.output.join("k6-owasp-security-test.js")
2165 };
2166
2167 if let Some(parent) = script_path.parent() {
2168 std::fs::create_dir_all(parent)?;
2169 }
2170 std::fs::write(&script_path, &script)?;
2171 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
2172
2173 if self.generate_only {
2175 println!("\nOWASP security test script generated. Run it with:");
2176 println!(" k6 run {}", script_path.display());
2177 return Ok(());
2178 }
2179
2180 TerminalReporter::print_progress("Executing OWASP security tests...");
2182 let executor = K6Executor::new()?;
2183 std::fs::create_dir_all(&self.output)?;
2184
2185 let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
2186
2187 let duration_secs = Self::parse_duration(&self.duration)?;
2188 TerminalReporter::print_summary(&results, duration_secs);
2189
2190 println!("\nOWASP security test results saved to: {}", self.output.display());
2191
2192 Ok(())
2193 }
2194}
2195
2196#[cfg(test)]
2197mod tests {
2198 use super::*;
2199 use tempfile::tempdir;
2200
2201 #[test]
2202 fn test_parse_duration() {
2203 assert_eq!(BenchCommand::parse_duration("30s").unwrap(), 30);
2204 assert_eq!(BenchCommand::parse_duration("5m").unwrap(), 300);
2205 assert_eq!(BenchCommand::parse_duration("1h").unwrap(), 3600);
2206 assert_eq!(BenchCommand::parse_duration("60").unwrap(), 60);
2207 }
2208
2209 #[test]
2210 fn test_parse_duration_invalid() {
2211 assert!(BenchCommand::parse_duration("invalid").is_err());
2212 assert!(BenchCommand::parse_duration("30x").is_err());
2213 }
2214
2215 #[test]
2216 fn test_parse_headers() {
2217 let cmd = BenchCommand {
2218 spec: vec![PathBuf::from("test.yaml")],
2219 spec_dir: None,
2220 merge_conflicts: "error".to_string(),
2221 spec_mode: "merge".to_string(),
2222 dependency_config: None,
2223 target: "http://localhost".to_string(),
2224 base_path: None,
2225 duration: "1m".to_string(),
2226 vus: 10,
2227 scenario: "ramp-up".to_string(),
2228 operations: None,
2229 exclude_operations: None,
2230 auth: None,
2231 headers: Some("X-API-Key:test123,X-Client-ID:client456".to_string()),
2232 output: PathBuf::from("output"),
2233 generate_only: false,
2234 script_output: None,
2235 threshold_percentile: "p(95)".to_string(),
2236 threshold_ms: 500,
2237 max_error_rate: 0.05,
2238 verbose: false,
2239 skip_tls_verify: false,
2240 targets_file: None,
2241 max_concurrency: None,
2242 results_format: "both".to_string(),
2243 params_file: None,
2244 crud_flow: false,
2245 flow_config: None,
2246 extract_fields: None,
2247 parallel_create: None,
2248 data_file: None,
2249 data_distribution: "unique-per-vu".to_string(),
2250 data_mappings: None,
2251 per_uri_control: false,
2252 error_rate: None,
2253 error_types: None,
2254 security_test: false,
2255 security_payloads: None,
2256 security_categories: None,
2257 security_target_fields: None,
2258 wafbench_dir: None,
2259 wafbench_cycle_all: false,
2260 owasp_api_top10: false,
2261 owasp_categories: None,
2262 owasp_auth_header: "Authorization".to_string(),
2263 owasp_auth_token: None,
2264 owasp_admin_paths: None,
2265 owasp_id_fields: None,
2266 owasp_report: None,
2267 owasp_report_format: "json".to_string(),
2268 owasp_iterations: 1,
2269 conformance: false,
2270 conformance_api_key: None,
2271 conformance_basic_auth: None,
2272 conformance_report: PathBuf::from("conformance-report.json"),
2273 conformance_categories: None,
2274 conformance_report_format: "json".to_string(),
2275 conformance_headers: vec![],
2276 conformance_all_operations: false,
2277 conformance_custom: None,
2278 conformance_delay_ms: 0,
2279 use_k6: false,
2280 };
2281
2282 let headers = cmd.parse_headers().unwrap();
2283 assert_eq!(headers.get("X-API-Key"), Some(&"test123".to_string()));
2284 assert_eq!(headers.get("X-Client-ID"), Some(&"client456".to_string()));
2285 }
2286
2287 #[test]
2288 fn test_get_spec_display_name() {
2289 let cmd = BenchCommand {
2290 spec: vec![PathBuf::from("test.yaml")],
2291 spec_dir: None,
2292 merge_conflicts: "error".to_string(),
2293 spec_mode: "merge".to_string(),
2294 dependency_config: None,
2295 target: "http://localhost".to_string(),
2296 base_path: None,
2297 duration: "1m".to_string(),
2298 vus: 10,
2299 scenario: "ramp-up".to_string(),
2300 operations: None,
2301 exclude_operations: None,
2302 auth: None,
2303 headers: None,
2304 output: PathBuf::from("output"),
2305 generate_only: false,
2306 script_output: None,
2307 threshold_percentile: "p(95)".to_string(),
2308 threshold_ms: 500,
2309 max_error_rate: 0.05,
2310 verbose: false,
2311 skip_tls_verify: false,
2312 targets_file: None,
2313 max_concurrency: None,
2314 results_format: "both".to_string(),
2315 params_file: None,
2316 crud_flow: false,
2317 flow_config: None,
2318 extract_fields: None,
2319 parallel_create: None,
2320 data_file: None,
2321 data_distribution: "unique-per-vu".to_string(),
2322 data_mappings: None,
2323 per_uri_control: false,
2324 error_rate: None,
2325 error_types: None,
2326 security_test: false,
2327 security_payloads: None,
2328 security_categories: None,
2329 security_target_fields: None,
2330 wafbench_dir: None,
2331 wafbench_cycle_all: false,
2332 owasp_api_top10: false,
2333 owasp_categories: None,
2334 owasp_auth_header: "Authorization".to_string(),
2335 owasp_auth_token: None,
2336 owasp_admin_paths: None,
2337 owasp_id_fields: None,
2338 owasp_report: None,
2339 owasp_report_format: "json".to_string(),
2340 owasp_iterations: 1,
2341 conformance: false,
2342 conformance_api_key: None,
2343 conformance_basic_auth: None,
2344 conformance_report: PathBuf::from("conformance-report.json"),
2345 conformance_categories: None,
2346 conformance_report_format: "json".to_string(),
2347 conformance_headers: vec![],
2348 conformance_all_operations: false,
2349 conformance_custom: None,
2350 conformance_delay_ms: 0,
2351 use_k6: false,
2352 };
2353
2354 assert_eq!(cmd.get_spec_display_name(), "test.yaml");
2355
2356 let cmd_multi = BenchCommand {
2358 spec: vec![PathBuf::from("a.yaml"), PathBuf::from("b.yaml")],
2359 spec_dir: None,
2360 merge_conflicts: "error".to_string(),
2361 spec_mode: "merge".to_string(),
2362 dependency_config: None,
2363 target: "http://localhost".to_string(),
2364 base_path: None,
2365 duration: "1m".to_string(),
2366 vus: 10,
2367 scenario: "ramp-up".to_string(),
2368 operations: None,
2369 exclude_operations: None,
2370 auth: None,
2371 headers: None,
2372 output: PathBuf::from("output"),
2373 generate_only: false,
2374 script_output: None,
2375 threshold_percentile: "p(95)".to_string(),
2376 threshold_ms: 500,
2377 max_error_rate: 0.05,
2378 verbose: false,
2379 skip_tls_verify: false,
2380 targets_file: None,
2381 max_concurrency: None,
2382 results_format: "both".to_string(),
2383 params_file: None,
2384 crud_flow: false,
2385 flow_config: None,
2386 extract_fields: None,
2387 parallel_create: None,
2388 data_file: None,
2389 data_distribution: "unique-per-vu".to_string(),
2390 data_mappings: None,
2391 per_uri_control: false,
2392 error_rate: None,
2393 error_types: None,
2394 security_test: false,
2395 security_payloads: None,
2396 security_categories: None,
2397 security_target_fields: None,
2398 wafbench_dir: None,
2399 wafbench_cycle_all: false,
2400 owasp_api_top10: false,
2401 owasp_categories: None,
2402 owasp_auth_header: "Authorization".to_string(),
2403 owasp_auth_token: None,
2404 owasp_admin_paths: None,
2405 owasp_id_fields: None,
2406 owasp_report: None,
2407 owasp_report_format: "json".to_string(),
2408 owasp_iterations: 1,
2409 conformance: false,
2410 conformance_api_key: None,
2411 conformance_basic_auth: None,
2412 conformance_report: PathBuf::from("conformance-report.json"),
2413 conformance_categories: None,
2414 conformance_report_format: "json".to_string(),
2415 conformance_headers: vec![],
2416 conformance_all_operations: false,
2417 conformance_custom: None,
2418 conformance_delay_ms: 0,
2419 use_k6: false,
2420 };
2421
2422 assert_eq!(cmd_multi.get_spec_display_name(), "2 spec files");
2423 }
2424
2425 #[test]
2426 fn test_parse_extracted_values_from_output_dir() {
2427 let dir = tempdir().unwrap();
2428 let path = dir.path().join("extracted_values.json");
2429 std::fs::write(
2430 &path,
2431 r#"{
2432 "pool_id": "abc123",
2433 "count": 0,
2434 "enabled": false,
2435 "metadata": { "owner": "team-a" }
2436}"#,
2437 )
2438 .unwrap();
2439
2440 let extracted = BenchCommand::parse_extracted_values(dir.path()).unwrap();
2441 assert_eq!(extracted.get("pool_id"), Some(&serde_json::json!("abc123")));
2442 assert_eq!(extracted.get("count"), Some(&serde_json::json!(0)));
2443 assert_eq!(extracted.get("enabled"), Some(&serde_json::json!(false)));
2444 assert_eq!(extracted.get("metadata"), Some(&serde_json::json!({"owner": "team-a"})));
2445 }
2446
2447 #[test]
2448 fn test_parse_extracted_values_missing_file() {
2449 let dir = tempdir().unwrap();
2450 let extracted = BenchCommand::parse_extracted_values(dir.path()).unwrap();
2451 assert!(extracted.values.is_empty());
2452 }
2453}