1use crate::crud_flow::{CrudFlowConfig, CrudFlowDetector};
4use crate::data_driven::{DataDistribution, DataDrivenConfig, DataDrivenGenerator, DataMapping};
5use crate::error::{BenchError, Result};
6use crate::executor::K6Executor;
7use crate::invalid_data::{InvalidDataConfig, InvalidDataGenerator, InvalidDataType};
8use crate::k6_gen::{K6Config, K6ScriptGenerator};
9use crate::mock_integration::{MockIntegrationConfig, MockIntegrationGenerator, MockServerDetector};
10use crate::parallel_executor::{AggregatedResults, ParallelExecutor};
11use crate::parallel_requests::{ParallelConfig, ParallelRequestGenerator};
12use crate::param_overrides::ParameterOverrides;
13use crate::reporter::TerminalReporter;
14use crate::request_gen::RequestGenerator;
15use crate::scenarios::LoadScenario;
16use crate::security_payloads::{SecurityCategory, SecurityPayloads, SecurityTestConfig, SecurityTestGenerator};
17use crate::spec_parser::SpecParser;
18use crate::target_parser::parse_targets_file;
19use std::collections::HashMap;
20use std::path::PathBuf;
21use std::str::FromStr;
22
23pub struct BenchCommand {
25 pub spec: PathBuf,
26 pub target: String,
27 pub duration: String,
28 pub vus: u32,
29 pub scenario: String,
30 pub operations: Option<String>,
31 pub exclude_operations: Option<String>,
35 pub auth: Option<String>,
36 pub headers: Option<String>,
37 pub output: PathBuf,
38 pub generate_only: bool,
39 pub script_output: Option<PathBuf>,
40 pub threshold_percentile: String,
41 pub threshold_ms: u64,
42 pub max_error_rate: f64,
43 pub verbose: bool,
44 pub skip_tls_verify: bool,
45 pub targets_file: Option<PathBuf>,
47 pub max_concurrency: Option<u32>,
49 pub results_format: String,
51 pub params_file: Option<PathBuf>,
56
57 pub crud_flow: bool,
60 pub flow_config: Option<PathBuf>,
62 pub extract_fields: Option<String>,
64
65 pub parallel_create: Option<u32>,
68
69 pub data_file: Option<PathBuf>,
72 pub data_distribution: String,
74 pub data_mappings: Option<String>,
76
77 pub error_rate: Option<f64>,
80 pub error_types: Option<String>,
82
83 pub security_test: bool,
86 pub security_payloads: Option<PathBuf>,
88 pub security_categories: Option<String>,
90 pub security_target_fields: Option<String>,
92}
93
94impl BenchCommand {
95 pub async fn execute(&self) -> Result<()> {
97 if let Some(targets_file) = &self.targets_file {
99 return self.execute_multi_target(targets_file).await;
100 }
101
102 TerminalReporter::print_header(
105 self.spec.to_str().unwrap(),
106 &self.target,
107 0, &self.scenario,
109 Self::parse_duration(&self.duration)?,
110 );
111
112 if !K6Executor::is_k6_installed() {
114 TerminalReporter::print_error("k6 is not installed");
115 TerminalReporter::print_warning(
116 "Install k6 from: https://k6.io/docs/get-started/installation/",
117 );
118 return Err(BenchError::K6NotFound);
119 }
120
121 TerminalReporter::print_progress("Loading OpenAPI specification...");
123 let parser = SpecParser::from_file(&self.spec).await?;
124 TerminalReporter::print_success("Specification loaded");
125
126 let mock_config = self.build_mock_config().await;
128 if mock_config.is_mock_server {
129 TerminalReporter::print_progress("Mock server integration enabled");
130 }
131
132 if self.crud_flow {
134 return self.execute_crud_flow(&parser).await;
135 }
136
137 TerminalReporter::print_progress("Extracting API operations...");
139 let mut operations = if let Some(filter) = &self.operations {
140 parser.filter_operations(filter)?
141 } else {
142 parser.get_operations()
143 };
144
145 if let Some(exclude) = &self.exclude_operations {
147 let before_count = operations.len();
148 operations = parser.exclude_operations(operations, exclude)?;
149 let excluded_count = before_count - operations.len();
150 if excluded_count > 0 {
151 TerminalReporter::print_progress(&format!(
152 "Excluded {} operations matching '{}'",
153 excluded_count, exclude
154 ));
155 }
156 }
157
158 if operations.is_empty() {
159 return Err(BenchError::Other("No operations found in spec".to_string()));
160 }
161
162 TerminalReporter::print_success(&format!("Found {} operations", operations.len()));
163
164 let param_overrides = if let Some(params_file) = &self.params_file {
166 TerminalReporter::print_progress("Loading parameter overrides...");
167 let overrides = ParameterOverrides::from_file(params_file)?;
168 TerminalReporter::print_success(&format!(
169 "Loaded parameter overrides ({} operation-specific, {} defaults)",
170 overrides.operations.len(),
171 if overrides.defaults.is_empty() { 0 } else { 1 }
172 ));
173 Some(overrides)
174 } else {
175 None
176 };
177
178 TerminalReporter::print_progress("Generating request templates...");
180 let templates: Vec<_> = operations
181 .iter()
182 .map(|op| {
183 let op_overrides = param_overrides.as_ref().map(|po| {
184 po.get_for_operation(op.operation_id.as_deref(), &op.method, &op.path)
185 });
186 RequestGenerator::generate_template_with_overrides(op, op_overrides.as_ref())
187 })
188 .collect::<Result<Vec<_>>>()?;
189 TerminalReporter::print_success("Request templates generated");
190
191 let custom_headers = self.parse_headers()?;
193
194 TerminalReporter::print_progress("Generating k6 load test script...");
196 let scenario =
197 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
198
199 let k6_config = K6Config {
200 target_url: self.target.clone(),
201 scenario,
202 duration_secs: Self::parse_duration(&self.duration)?,
203 max_vus: self.vus,
204 threshold_percentile: self.threshold_percentile.clone(),
205 threshold_ms: self.threshold_ms,
206 max_error_rate: self.max_error_rate,
207 auth_header: self.auth.clone(),
208 custom_headers,
209 skip_tls_verify: self.skip_tls_verify,
210 };
211
212 let generator = K6ScriptGenerator::new(k6_config, templates);
213 let mut script = generator.generate()?;
214 TerminalReporter::print_success("k6 script generated");
215
216 let has_advanced_features = self.data_file.is_some()
218 || self.error_rate.is_some()
219 || self.security_test
220 || self.parallel_create.is_some();
221
222 if has_advanced_features {
224 script = self.generate_enhanced_script(&script)?;
225 }
226
227 if mock_config.is_mock_server {
229 let setup_code = MockIntegrationGenerator::generate_setup(&mock_config);
230 let teardown_code = MockIntegrationGenerator::generate_teardown(&mock_config);
231 let helper_code = MockIntegrationGenerator::generate_vu_id_helper();
232
233 if let Some(import_end) = script.find("export const options") {
235 script.insert_str(
236 import_end,
237 &format!("\n// === Mock Server Integration ===\n{}\n{}\n{}\n",
238 helper_code, setup_code, teardown_code),
239 );
240 }
241 }
242
243 TerminalReporter::print_progress("Validating k6 script...");
245 let validation_errors = K6ScriptGenerator::validate_script(&script);
246 if !validation_errors.is_empty() {
247 TerminalReporter::print_error("Script validation failed");
248 for error in &validation_errors {
249 eprintln!(" {}", error);
250 }
251 return Err(BenchError::Other(format!(
252 "Generated k6 script has {} validation error(s). Please check the output above.",
253 validation_errors.len()
254 )));
255 }
256 TerminalReporter::print_success("Script validation passed");
257
258 let script_path = if let Some(output) = &self.script_output {
260 output.clone()
261 } else {
262 self.output.join("k6-script.js")
263 };
264
265 std::fs::create_dir_all(script_path.parent().unwrap())?;
266 std::fs::write(&script_path, &script)?;
267 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
268
269 if self.generate_only {
271 println!("\nScript generated successfully. Run it with:");
272 println!(" k6 run {}", script_path.display());
273 return Ok(());
274 }
275
276 TerminalReporter::print_progress("Executing load test...");
278 let executor = K6Executor::new()?;
279
280 std::fs::create_dir_all(&self.output)?;
281
282 let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
283
284 let duration_secs = Self::parse_duration(&self.duration)?;
286 TerminalReporter::print_summary(&results, duration_secs);
287
288 println!("\nResults saved to: {}", self.output.display());
289
290 Ok(())
291 }
292
293 async fn execute_multi_target(&self, targets_file: &PathBuf) -> Result<()> {
295 TerminalReporter::print_progress("Parsing targets file...");
296 let targets = parse_targets_file(targets_file)?;
297 let num_targets = targets.len();
298 TerminalReporter::print_success(&format!("Loaded {} targets", num_targets));
299
300 if targets.is_empty() {
301 return Err(BenchError::Other("No targets found in file".to_string()));
302 }
303
304 let max_concurrency = self.max_concurrency.unwrap_or(10) as usize;
306 let max_concurrency = max_concurrency.min(num_targets); TerminalReporter::print_header(
310 self.spec.to_str().unwrap(),
311 &format!("{} targets", num_targets),
312 0,
313 &self.scenario,
314 Self::parse_duration(&self.duration)?,
315 );
316
317 let executor = ParallelExecutor::new(
319 BenchCommand {
320 spec: self.spec.clone(),
322 target: self.target.clone(), duration: self.duration.clone(),
324 vus: self.vus,
325 scenario: self.scenario.clone(),
326 operations: self.operations.clone(),
327 exclude_operations: self.exclude_operations.clone(),
328 auth: self.auth.clone(),
329 headers: self.headers.clone(),
330 output: self.output.clone(),
331 generate_only: self.generate_only,
332 script_output: self.script_output.clone(),
333 threshold_percentile: self.threshold_percentile.clone(),
334 threshold_ms: self.threshold_ms,
335 max_error_rate: self.max_error_rate,
336 verbose: self.verbose,
337 skip_tls_verify: self.skip_tls_verify,
338 targets_file: None,
339 max_concurrency: None,
340 results_format: self.results_format.clone(),
341 params_file: self.params_file.clone(),
342 crud_flow: self.crud_flow,
343 flow_config: self.flow_config.clone(),
344 extract_fields: self.extract_fields.clone(),
345 parallel_create: self.parallel_create,
346 data_file: self.data_file.clone(),
347 data_distribution: self.data_distribution.clone(),
348 data_mappings: self.data_mappings.clone(),
349 error_rate: self.error_rate,
350 error_types: self.error_types.clone(),
351 security_test: self.security_test,
352 security_payloads: self.security_payloads.clone(),
353 security_categories: self.security_categories.clone(),
354 security_target_fields: self.security_target_fields.clone(),
355 },
356 targets,
357 max_concurrency,
358 );
359
360 let aggregated_results = executor.execute_all().await?;
362
363 self.report_multi_target_results(&aggregated_results)?;
365
366 Ok(())
367 }
368
369 fn report_multi_target_results(&self, results: &AggregatedResults) -> Result<()> {
371 TerminalReporter::print_multi_target_summary(results);
373
374 if self.results_format == "aggregated" || self.results_format == "both" {
376 let summary_path = self.output.join("aggregated_summary.json");
377 let summary_json = serde_json::json!({
378 "total_targets": results.total_targets,
379 "successful_targets": results.successful_targets,
380 "failed_targets": results.failed_targets,
381 "aggregated_metrics": {
382 "total_requests": results.aggregated_metrics.total_requests,
383 "total_failed_requests": results.aggregated_metrics.total_failed_requests,
384 "avg_duration_ms": results.aggregated_metrics.avg_duration_ms,
385 "p95_duration_ms": results.aggregated_metrics.p95_duration_ms,
386 "p99_duration_ms": results.aggregated_metrics.p99_duration_ms,
387 "error_rate": results.aggregated_metrics.error_rate,
388 },
389 "target_results": results.target_results.iter().map(|r| {
390 serde_json::json!({
391 "target_url": r.target_url,
392 "target_index": r.target_index,
393 "success": r.success,
394 "error": r.error,
395 "total_requests": r.results.total_requests,
396 "failed_requests": r.results.failed_requests,
397 "avg_duration_ms": r.results.avg_duration_ms,
398 "p95_duration_ms": r.results.p95_duration_ms,
399 "p99_duration_ms": r.results.p99_duration_ms,
400 "output_dir": r.output_dir.to_string_lossy(),
401 })
402 }).collect::<Vec<_>>(),
403 });
404
405 std::fs::write(&summary_path, serde_json::to_string_pretty(&summary_json)?)?;
406 TerminalReporter::print_success(&format!(
407 "Aggregated summary saved to: {}",
408 summary_path.display()
409 ));
410 }
411
412 println!("\nResults saved to: {}", self.output.display());
413 println!(" - Per-target results: {}", self.output.join("target_*").display());
414 if self.results_format == "aggregated" || self.results_format == "both" {
415 println!(
416 " - Aggregated summary: {}",
417 self.output.join("aggregated_summary.json").display()
418 );
419 }
420
421 Ok(())
422 }
423
424 pub fn parse_duration(duration: &str) -> Result<u64> {
426 let duration = duration.trim();
427
428 if let Some(secs) = duration.strip_suffix('s') {
429 secs.parse::<u64>()
430 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
431 } else if let Some(mins) = duration.strip_suffix('m') {
432 mins.parse::<u64>()
433 .map(|m| m * 60)
434 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
435 } else if let Some(hours) = duration.strip_suffix('h') {
436 hours
437 .parse::<u64>()
438 .map(|h| h * 3600)
439 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
440 } else {
441 duration
443 .parse::<u64>()
444 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
445 }
446 }
447
448 pub fn parse_headers(&self) -> Result<HashMap<String, String>> {
450 let mut headers = HashMap::new();
451
452 if let Some(header_str) = &self.headers {
453 for pair in header_str.split(',') {
454 let parts: Vec<&str> = pair.splitn(2, ':').collect();
455 if parts.len() != 2 {
456 return Err(BenchError::Other(format!(
457 "Invalid header format: '{}'. Expected 'Key:Value'",
458 pair
459 )));
460 }
461 headers.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
462 }
463 }
464
465 Ok(headers)
466 }
467
468 async fn build_mock_config(&self) -> MockIntegrationConfig {
470 if MockServerDetector::looks_like_mock_server(&self.target) {
472 if let Ok(info) = MockServerDetector::detect(&self.target).await {
474 if info.is_mockforge {
475 TerminalReporter::print_success(&format!(
476 "Detected MockForge server (version: {})",
477 info.version.as_deref().unwrap_or("unknown")
478 ));
479 return MockIntegrationConfig::mock_server();
480 }
481 }
482 }
483 MockIntegrationConfig::real_api()
484 }
485
486 fn build_crud_flow_config(&self) -> Option<CrudFlowConfig> {
488 if !self.crud_flow {
489 return None;
490 }
491
492 if let Some(config_path) = &self.flow_config {
494 match CrudFlowConfig::from_file(config_path) {
495 Ok(config) => return Some(config),
496 Err(e) => {
497 TerminalReporter::print_warning(&format!(
498 "Failed to load flow config: {}. Using auto-detection.",
499 e
500 ));
501 }
502 }
503 }
504
505 let extract_fields = self
507 .extract_fields
508 .as_ref()
509 .map(|f| f.split(',').map(|s| s.trim().to_string()).collect())
510 .unwrap_or_else(|| vec!["id".to_string(), "uuid".to_string()]);
511
512 Some(CrudFlowConfig {
513 flows: Vec::new(), default_extract_fields: extract_fields,
515 })
516 }
517
518 fn build_data_driven_config(&self) -> Option<DataDrivenConfig> {
520 let data_file = self.data_file.as_ref()?;
521
522 let distribution = DataDistribution::from_str(&self.data_distribution)
523 .unwrap_or(DataDistribution::UniquePerVu);
524
525 let mappings = self.data_mappings.as_ref().map(|m| {
526 DataMapping::parse_mappings(m).unwrap_or_default()
527 }).unwrap_or_default();
528
529 Some(DataDrivenConfig {
530 file_path: data_file.to_string_lossy().to_string(),
531 distribution,
532 mappings,
533 csv_has_header: true,
534 })
535 }
536
537 fn build_invalid_data_config(&self) -> Option<InvalidDataConfig> {
539 let error_rate = self.error_rate?;
540
541 let error_types = self.error_types.as_ref()
542 .map(|types| InvalidDataConfig::parse_error_types(types).unwrap_or_default())
543 .unwrap_or_default();
544
545 Some(InvalidDataConfig {
546 error_rate,
547 error_types,
548 target_fields: Vec::new(),
549 })
550 }
551
552 fn build_security_config(&self) -> Option<SecurityTestConfig> {
554 if !self.security_test {
555 return None;
556 }
557
558 let categories = self.security_categories.as_ref()
559 .map(|cats| SecurityTestConfig::parse_categories(cats).unwrap_or_default())
560 .unwrap_or_else(|| {
561 let mut default = std::collections::HashSet::new();
562 default.insert(SecurityCategory::SqlInjection);
563 default.insert(SecurityCategory::Xss);
564 default
565 });
566
567 let target_fields = self.security_target_fields.as_ref()
568 .map(|fields| fields.split(',').map(|f| f.trim().to_string()).collect())
569 .unwrap_or_default();
570
571 let custom_payloads_file = self.security_payloads.as_ref()
572 .map(|p| p.to_string_lossy().to_string());
573
574 Some(SecurityTestConfig {
575 enabled: true,
576 categories,
577 target_fields,
578 custom_payloads_file,
579 include_high_risk: false,
580 })
581 }
582
583 fn build_parallel_config(&self) -> Option<ParallelConfig> {
585 let count = self.parallel_create?;
586
587 Some(ParallelConfig::new(count))
588 }
589
590 fn generate_enhanced_script(&self, base_script: &str) -> Result<String> {
592 let mut enhanced_script = base_script.to_string();
593 let mut additional_code = String::new();
594
595 if let Some(config) = self.build_data_driven_config() {
597 TerminalReporter::print_progress("Adding data-driven testing support...");
598 additional_code.push_str(&DataDrivenGenerator::generate_setup(&config));
599 additional_code.push('\n');
600 TerminalReporter::print_success("Data-driven testing enabled");
601 }
602
603 if let Some(config) = self.build_invalid_data_config() {
605 TerminalReporter::print_progress("Adding invalid data testing support...");
606 additional_code.push_str(&InvalidDataGenerator::generate_invalidation_logic());
607 additional_code.push('\n');
608 additional_code.push_str(&InvalidDataGenerator::generate_should_invalidate(config.error_rate));
609 additional_code.push('\n');
610 additional_code.push_str(&InvalidDataGenerator::generate_type_selection(&config.error_types));
611 additional_code.push('\n');
612 TerminalReporter::print_success(&format!(
613 "Invalid data testing enabled ({}% error rate)",
614 (self.error_rate.unwrap_or(0.0) * 100.0) as u32
615 ));
616 }
617
618 if let Some(config) = self.build_security_config() {
620 TerminalReporter::print_progress("Adding security testing support...");
621 let payload_list = SecurityPayloads::get_payloads(&config);
622 additional_code.push_str(&SecurityTestGenerator::generate_payload_selection(&payload_list));
623 additional_code.push('\n');
624 additional_code.push_str(&SecurityTestGenerator::generate_apply_payload(&config.target_fields));
625 additional_code.push('\n');
626 additional_code.push_str(&SecurityTestGenerator::generate_security_checks());
627 additional_code.push('\n');
628 TerminalReporter::print_success(&format!(
629 "Security testing enabled ({} payloads)",
630 payload_list.len()
631 ));
632 }
633
634 if let Some(config) = self.build_parallel_config() {
636 TerminalReporter::print_progress("Adding parallel execution support...");
637 additional_code.push_str(&ParallelRequestGenerator::generate_batch_helper(&config));
638 additional_code.push('\n');
639 TerminalReporter::print_success(&format!(
640 "Parallel execution enabled (count: {})",
641 config.count
642 ));
643 }
644
645 if !additional_code.is_empty() {
647 if let Some(import_end) = enhanced_script.find("export const options") {
649 enhanced_script.insert_str(
650 import_end,
651 &format!("\n// === Advanced Testing Features ===\n{}\n", additional_code),
652 );
653 }
654 }
655
656 Ok(enhanced_script)
657 }
658
659 async fn execute_crud_flow(&self, parser: &SpecParser) -> Result<()> {
661 TerminalReporter::print_progress("Detecting CRUD operations...");
662
663 let operations = parser.get_operations();
664 let flows = CrudFlowDetector::detect_flows(&operations);
665
666 if flows.is_empty() {
667 return Err(BenchError::Other(
668 "No CRUD flows detected in spec. Ensure spec has POST/GET/PUT/DELETE operations on related paths.".to_string(),
669 ));
670 }
671
672 TerminalReporter::print_success(&format!("Detected {} CRUD flow(s)", flows.len()));
673
674 for flow in &flows {
675 TerminalReporter::print_progress(&format!(
676 " - {}: {} steps",
677 flow.name,
678 flow.steps.len()
679 ));
680 }
681
682 let handlebars = handlebars::Handlebars::new();
684 let template = include_str!("templates/k6_crud_flow.hbs");
685
686 let custom_headers = self.parse_headers()?;
687 let config = self.build_crud_flow_config().unwrap_or_default();
688
689 let data = serde_json::json!({
690 "base_url": self.target,
691 "flows": flows.iter().map(|f| {
692 serde_json::json!({
693 "name": f.name,
694 "base_path": f.base_path,
695 "steps": f.steps.iter().map(|s| {
696 serde_json::json!({
697 "operation": s.operation,
698 "extract": s.extract,
699 "use_values": s.use_values,
700 "description": s.description,
701 })
702 }).collect::<Vec<_>>(),
703 })
704 }).collect::<Vec<_>>(),
705 "extract_fields": config.default_extract_fields,
706 "duration_secs": Self::parse_duration(&self.duration)?,
707 "max_vus": self.vus,
708 "auth_header": self.auth,
709 "custom_headers": custom_headers,
710 "skip_tls_verify": self.skip_tls_verify,
711 });
712
713 let script = handlebars
714 .render_template(template, &data)
715 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
716
717 TerminalReporter::print_success("CRUD flow script generated");
718
719 let script_path = if let Some(output) = &self.script_output {
721 output.clone()
722 } else {
723 self.output.join("k6-crud-flow-script.js")
724 };
725
726 std::fs::create_dir_all(script_path.parent().unwrap())?;
727 std::fs::write(&script_path, &script)?;
728 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
729
730 if self.generate_only {
731 println!("\nScript generated successfully. Run it with:");
732 println!(" k6 run {}", script_path.display());
733 return Ok(());
734 }
735
736 TerminalReporter::print_progress("Executing CRUD flow test...");
738 let executor = K6Executor::new()?;
739 std::fs::create_dir_all(&self.output)?;
740
741 let results = executor
742 .execute(&script_path, Some(&self.output), self.verbose)
743 .await?;
744
745 let duration_secs = Self::parse_duration(&self.duration)?;
746 TerminalReporter::print_summary(&results, duration_secs);
747
748 Ok(())
749 }
750}
751
752#[cfg(test)]
753mod tests {
754 use super::*;
755
756 #[test]
757 fn test_parse_duration() {
758 assert_eq!(BenchCommand::parse_duration("30s").unwrap(), 30);
759 assert_eq!(BenchCommand::parse_duration("5m").unwrap(), 300);
760 assert_eq!(BenchCommand::parse_duration("1h").unwrap(), 3600);
761 assert_eq!(BenchCommand::parse_duration("60").unwrap(), 60);
762 }
763
764 #[test]
765 fn test_parse_duration_invalid() {
766 assert!(BenchCommand::parse_duration("invalid").is_err());
767 assert!(BenchCommand::parse_duration("30x").is_err());
768 }
769
770 #[test]
771 fn test_parse_headers() {
772 let cmd = BenchCommand {
773 spec: PathBuf::from("test.yaml"),
774 target: "http://localhost".to_string(),
775 duration: "1m".to_string(),
776 vus: 10,
777 scenario: "ramp-up".to_string(),
778 operations: None,
779 exclude_operations: None,
780 auth: None,
781 headers: Some("X-API-Key:test123,X-Client-ID:client456".to_string()),
782 output: PathBuf::from("output"),
783 generate_only: false,
784 script_output: None,
785 threshold_percentile: "p(95)".to_string(),
786 threshold_ms: 500,
787 max_error_rate: 0.05,
788 verbose: false,
789 skip_tls_verify: false,
790 targets_file: None,
791 max_concurrency: None,
792 results_format: "both".to_string(),
793 params_file: None,
794 crud_flow: false,
795 flow_config: None,
796 extract_fields: None,
797 parallel_create: None,
798 data_file: None,
799 data_distribution: "unique-per-vu".to_string(),
800 data_mappings: None,
801 error_rate: None,
802 error_types: None,
803 security_test: false,
804 security_payloads: None,
805 security_categories: None,
806 security_target_fields: None,
807 };
808
809 let headers = cmd.parse_headers().unwrap();
810 assert_eq!(headers.get("X-API-Key"), Some(&"test123".to_string()));
811 assert_eq!(headers.get("X-Client-ID"), Some(&"client456".to_string()));
812 }
813}