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