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