Skip to main content

mockforge_bench/
command.rs

1//! Bench command implementation
2
3use 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::PathBuf;
35use std::str::FromStr;
36
37/// Bench command configuration
38pub struct BenchCommand {
39    /// OpenAPI spec file(s) - can specify multiple
40    pub spec: Vec<PathBuf>,
41    /// Directory containing OpenAPI spec files (discovers .json, .yaml, .yml files)
42    pub spec_dir: Option<PathBuf>,
43    /// Conflict resolution strategy when merging multiple specs: "error" (default), "first", "last"
44    pub merge_conflicts: String,
45    /// Spec mode: "merge" (default) combines all specs, "sequential" runs them in order
46    pub spec_mode: String,
47    /// Dependency configuration file for cross-spec value passing (used with sequential mode)
48    pub dependency_config: Option<PathBuf>,
49    pub target: String,
50    /// API base path prefix (e.g., "/api" or "/v2/api")
51    /// If None, extracts from OpenAPI spec's servers URL
52    pub base_path: Option<String>,
53    pub duration: String,
54    pub vus: u32,
55    pub scenario: String,
56    pub operations: Option<String>,
57    /// Exclude operations from testing (comma-separated)
58    ///
59    /// Supports "METHOD /path" or just "METHOD" to exclude all operations of that type.
60    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    /// Optional file containing multiple targets
72    pub targets_file: Option<PathBuf>,
73    /// Maximum number of parallel executions (for multi-target mode)
74    pub max_concurrency: Option<u32>,
75    /// Results format: "per-target", "aggregated", or "both"
76    pub results_format: String,
77    /// Optional file containing parameter value overrides (JSON or YAML)
78    ///
79    /// Allows users to provide custom values for path parameters, query parameters,
80    /// headers, and request bodies instead of auto-generated placeholder values.
81    pub params_file: Option<PathBuf>,
82
83    // === CRUD Flow Options ===
84    /// Enable CRUD flow mode
85    pub crud_flow: bool,
86    /// Custom CRUD flow configuration file
87    pub flow_config: Option<PathBuf>,
88    /// Fields to extract from responses
89    pub extract_fields: Option<String>,
90
91    // === Parallel Execution Options ===
92    /// Number of resources to create in parallel
93    pub parallel_create: Option<u32>,
94
95    // === Data-Driven Testing Options ===
96    /// Test data file (CSV or JSON)
97    pub data_file: Option<PathBuf>,
98    /// Data distribution strategy
99    pub data_distribution: String,
100    /// Data column to field mappings
101    pub data_mappings: Option<String>,
102    /// Enable per-URI control mode (each row specifies method, uri, body, etc.)
103    pub per_uri_control: bool,
104
105    // === Invalid Data Testing Options ===
106    /// Percentage of requests with invalid data
107    pub error_rate: Option<f64>,
108    /// Types of invalid data to generate
109    pub error_types: Option<String>,
110
111    // === Security Testing Options ===
112    /// Enable security testing
113    pub security_test: bool,
114    /// Custom security payloads file
115    pub security_payloads: Option<PathBuf>,
116    /// Security test categories
117    pub security_categories: Option<String>,
118    /// Fields to target for security injection
119    pub security_target_fields: Option<String>,
120
121    // === WAFBench Integration ===
122    /// WAFBench test directory or glob pattern for loading CRS attack patterns
123    pub wafbench_dir: Option<String>,
124    /// Cycle through ALL WAFBench payloads instead of random sampling
125    pub wafbench_cycle_all: bool,
126
127    // === OWASP API Security Top 10 Testing ===
128    /// Enable OWASP API Security Top 10 testing mode
129    pub owasp_api_top10: bool,
130    /// OWASP API categories to test (comma-separated)
131    pub owasp_categories: Option<String>,
132    /// Authorization header name for OWASP auth tests
133    pub owasp_auth_header: String,
134    /// Valid authorization token for OWASP baseline requests
135    pub owasp_auth_token: Option<String>,
136    /// File containing admin/privileged paths to test
137    pub owasp_admin_paths: Option<PathBuf>,
138    /// Fields containing resource IDs for BOLA testing
139    pub owasp_id_fields: Option<String>,
140    /// OWASP report output file
141    pub owasp_report: Option<PathBuf>,
142    /// OWASP report format (json, sarif)
143    pub owasp_report_format: String,
144    /// Number of iterations per VU for OWASP tests (default: 1)
145    pub owasp_iterations: u32,
146}
147
148impl BenchCommand {
149    /// Load and merge specs from --spec files and --spec-dir
150    pub async fn load_and_merge_specs(&self) -> Result<OpenApiSpec> {
151        let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
152
153        // Load specs from --spec flags
154        if !self.spec.is_empty() {
155            let specs = load_specs_from_files(self.spec.clone())
156                .await
157                .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
158            all_specs.extend(specs);
159        }
160
161        // Load specs from --spec-dir if provided
162        if let Some(spec_dir) = &self.spec_dir {
163            let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
164                BenchError::Other(format!("Failed to load specs from directory: {}", e))
165            })?;
166            all_specs.extend(dir_specs);
167        }
168
169        if all_specs.is_empty() {
170            return Err(BenchError::Other(
171                "No spec files provided. Use --spec or --spec-dir.".to_string(),
172            ));
173        }
174
175        // If only one spec, return it directly (extract just the OpenApiSpec)
176        if all_specs.len() == 1 {
177            // Safe to unwrap because we just checked len() == 1
178            return Ok(all_specs.into_iter().next().expect("checked len() == 1 above").1);
179        }
180
181        // Merge multiple specs
182        let conflict_strategy = match self.merge_conflicts.as_str() {
183            "first" => ConflictStrategy::First,
184            "last" => ConflictStrategy::Last,
185            _ => ConflictStrategy::Error,
186        };
187
188        merge_specs(all_specs, conflict_strategy)
189            .map_err(|e| BenchError::Other(format!("Failed to merge specs: {}", e)))
190    }
191
192    /// Get a display name for the spec(s)
193    fn get_spec_display_name(&self) -> String {
194        if self.spec.len() == 1 {
195            self.spec[0].to_string_lossy().to_string()
196        } else if !self.spec.is_empty() {
197            format!("{} spec files", self.spec.len())
198        } else if let Some(dir) = &self.spec_dir {
199            format!("specs from {}", dir.display())
200        } else {
201            "no specs".to_string()
202        }
203    }
204
205    /// Execute the bench command
206    pub async fn execute(&self) -> Result<()> {
207        // Check if we're in multi-target mode
208        if let Some(targets_file) = &self.targets_file {
209            return self.execute_multi_target(targets_file).await;
210        }
211
212        // Check if we're in sequential spec mode (for dependency handling)
213        if self.spec_mode == "sequential" && (self.spec.len() > 1 || self.spec_dir.is_some()) {
214            return self.execute_sequential_specs().await;
215        }
216
217        // Single target mode (existing behavior)
218        // Print header
219        TerminalReporter::print_header(
220            &self.get_spec_display_name(),
221            &self.target,
222            0, // Will be updated later
223            &self.scenario,
224            Self::parse_duration(&self.duration)?,
225        );
226
227        // Validate k6 installation
228        if !K6Executor::is_k6_installed() {
229            TerminalReporter::print_error("k6 is not installed");
230            TerminalReporter::print_warning(
231                "Install k6 from: https://k6.io/docs/get-started/installation/",
232            );
233            return Err(BenchError::K6NotFound);
234        }
235
236        // Load and parse spec(s)
237        TerminalReporter::print_progress("Loading OpenAPI specification(s)...");
238        let merged_spec = self.load_and_merge_specs().await?;
239        let parser = SpecParser::from_spec(merged_spec);
240        if self.spec.len() > 1 || self.spec_dir.is_some() {
241            TerminalReporter::print_success(&format!(
242                "Loaded and merged {} specification(s)",
243                self.spec.len() + self.spec_dir.as_ref().map(|_| 1).unwrap_or(0)
244            ));
245        } else {
246            TerminalReporter::print_success("Specification loaded");
247        }
248
249        // Check for mock server integration
250        let mock_config = self.build_mock_config().await;
251        if mock_config.is_mock_server {
252            TerminalReporter::print_progress("Mock server integration enabled");
253        }
254
255        // Check for CRUD flow mode
256        if self.crud_flow {
257            return self.execute_crud_flow(&parser).await;
258        }
259
260        // Check for OWASP API Top 10 testing mode
261        if self.owasp_api_top10 {
262            return self.execute_owasp_test(&parser).await;
263        }
264
265        // Get operations
266        TerminalReporter::print_progress("Extracting API operations...");
267        let mut operations = if let Some(filter) = &self.operations {
268            parser.filter_operations(filter)?
269        } else {
270            parser.get_operations()
271        };
272
273        // Apply exclusions if provided
274        if let Some(exclude) = &self.exclude_operations {
275            let before_count = operations.len();
276            operations = parser.exclude_operations(operations, exclude)?;
277            let excluded_count = before_count - operations.len();
278            if excluded_count > 0 {
279                TerminalReporter::print_progress(&format!(
280                    "Excluded {} operations matching '{}'",
281                    excluded_count, exclude
282                ));
283            }
284        }
285
286        if operations.is_empty() {
287            return Err(BenchError::Other("No operations found in spec".to_string()));
288        }
289
290        TerminalReporter::print_success(&format!("Found {} operations", operations.len()));
291
292        // Load parameter overrides if provided
293        let param_overrides = if let Some(params_file) = &self.params_file {
294            TerminalReporter::print_progress("Loading parameter overrides...");
295            let overrides = ParameterOverrides::from_file(params_file)?;
296            TerminalReporter::print_success(&format!(
297                "Loaded parameter overrides ({} operation-specific, {} defaults)",
298                overrides.operations.len(),
299                if overrides.defaults.is_empty() { 0 } else { 1 }
300            ));
301            Some(overrides)
302        } else {
303            None
304        };
305
306        // Generate request templates
307        TerminalReporter::print_progress("Generating request templates...");
308        let templates: Vec<_> = operations
309            .iter()
310            .map(|op| {
311                let op_overrides = param_overrides.as_ref().map(|po| {
312                    po.get_for_operation(op.operation_id.as_deref(), &op.method, &op.path)
313                });
314                RequestGenerator::generate_template_with_overrides(op, op_overrides.as_ref())
315            })
316            .collect::<Result<Vec<_>>>()?;
317        TerminalReporter::print_success("Request templates generated");
318
319        // Parse headers
320        let custom_headers = self.parse_headers()?;
321
322        // Resolve base path (CLI option takes priority over spec's servers URL)
323        let base_path = self.resolve_base_path(&parser);
324        if let Some(ref bp) = base_path {
325            TerminalReporter::print_progress(&format!("Using base path: {}", bp));
326        }
327
328        // Generate k6 script
329        TerminalReporter::print_progress("Generating k6 load test script...");
330        let scenario =
331            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
332
333        let security_testing_enabled = self.security_test || self.wafbench_dir.is_some();
334
335        let k6_config = K6Config {
336            target_url: self.target.clone(),
337            base_path,
338            scenario,
339            duration_secs: Self::parse_duration(&self.duration)?,
340            max_vus: self.vus,
341            threshold_percentile: self.threshold_percentile.clone(),
342            threshold_ms: self.threshold_ms,
343            max_error_rate: self.max_error_rate,
344            auth_header: self.auth.clone(),
345            custom_headers,
346            skip_tls_verify: self.skip_tls_verify,
347            security_testing_enabled,
348        };
349
350        let generator = K6ScriptGenerator::new(k6_config, templates);
351        let mut script = generator.generate()?;
352        TerminalReporter::print_success("k6 script generated");
353
354        // Check if any advanced features are enabled
355        let has_advanced_features = self.data_file.is_some()
356            || self.error_rate.is_some()
357            || self.security_test
358            || self.parallel_create.is_some()
359            || self.wafbench_dir.is_some();
360
361        // Enhance script with advanced features
362        if has_advanced_features {
363            script = self.generate_enhanced_script(&script)?;
364        }
365
366        // Add mock server integration code
367        if mock_config.is_mock_server {
368            let setup_code = MockIntegrationGenerator::generate_setup(&mock_config);
369            let teardown_code = MockIntegrationGenerator::generate_teardown(&mock_config);
370            let helper_code = MockIntegrationGenerator::generate_vu_id_helper();
371
372            // Insert mock server code after imports
373            if let Some(import_end) = script.find("export const options") {
374                script.insert_str(
375                    import_end,
376                    &format!(
377                        "\n// === Mock Server Integration ===\n{}\n{}\n{}\n",
378                        helper_code, setup_code, teardown_code
379                    ),
380                );
381            }
382        }
383
384        // Validate the generated script
385        TerminalReporter::print_progress("Validating k6 script...");
386        let validation_errors = K6ScriptGenerator::validate_script(&script);
387        if !validation_errors.is_empty() {
388            TerminalReporter::print_error("Script validation failed");
389            for error in &validation_errors {
390                eprintln!("  {}", error);
391            }
392            return Err(BenchError::Other(format!(
393                "Generated k6 script has {} validation error(s). Please check the output above.",
394                validation_errors.len()
395            )));
396        }
397        TerminalReporter::print_success("Script validation passed");
398
399        // Write script to file
400        let script_path = if let Some(output) = &self.script_output {
401            output.clone()
402        } else {
403            self.output.join("k6-script.js")
404        };
405
406        if let Some(parent) = script_path.parent() {
407            std::fs::create_dir_all(parent)?;
408        }
409        std::fs::write(&script_path, &script)?;
410        TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
411
412        // If generate-only mode, exit here
413        if self.generate_only {
414            println!("\nScript generated successfully. Run it with:");
415            println!("  k6 run {}", script_path.display());
416            return Ok(());
417        }
418
419        // Execute k6
420        TerminalReporter::print_progress("Executing load test...");
421        let executor = K6Executor::new()?;
422
423        std::fs::create_dir_all(&self.output)?;
424
425        let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
426
427        // Print results
428        let duration_secs = Self::parse_duration(&self.duration)?;
429        TerminalReporter::print_summary(&results, duration_secs);
430
431        println!("\nResults saved to: {}", self.output.display());
432
433        Ok(())
434    }
435
436    /// Execute multi-target bench testing
437    async fn execute_multi_target(&self, targets_file: &PathBuf) -> Result<()> {
438        TerminalReporter::print_progress("Parsing targets file...");
439        let targets = parse_targets_file(targets_file)?;
440        let num_targets = targets.len();
441        TerminalReporter::print_success(&format!("Loaded {} targets", num_targets));
442
443        if targets.is_empty() {
444            return Err(BenchError::Other("No targets found in file".to_string()));
445        }
446
447        // Determine max concurrency
448        let max_concurrency = self.max_concurrency.unwrap_or(10) as usize;
449        let max_concurrency = max_concurrency.min(num_targets); // Don't exceed number of targets
450
451        // Print header for multi-target mode
452        TerminalReporter::print_header(
453            &self.get_spec_display_name(),
454            &format!("{} targets", num_targets),
455            0,
456            &self.scenario,
457            Self::parse_duration(&self.duration)?,
458        );
459
460        // Create parallel executor
461        let executor = ParallelExecutor::new(
462            BenchCommand {
463                // Clone all fields except targets_file (we don't need it in the executor)
464                spec: self.spec.clone(),
465                spec_dir: self.spec_dir.clone(),
466                merge_conflicts: self.merge_conflicts.clone(),
467                spec_mode: self.spec_mode.clone(),
468                dependency_config: self.dependency_config.clone(),
469                target: self.target.clone(), // Not used in multi-target mode, but kept for compatibility
470                base_path: self.base_path.clone(),
471                duration: self.duration.clone(),
472                vus: self.vus,
473                scenario: self.scenario.clone(),
474                operations: self.operations.clone(),
475                exclude_operations: self.exclude_operations.clone(),
476                auth: self.auth.clone(),
477                headers: self.headers.clone(),
478                output: self.output.clone(),
479                generate_only: self.generate_only,
480                script_output: self.script_output.clone(),
481                threshold_percentile: self.threshold_percentile.clone(),
482                threshold_ms: self.threshold_ms,
483                max_error_rate: self.max_error_rate,
484                verbose: self.verbose,
485                skip_tls_verify: self.skip_tls_verify,
486                targets_file: None,
487                max_concurrency: None,
488                results_format: self.results_format.clone(),
489                params_file: self.params_file.clone(),
490                crud_flow: self.crud_flow,
491                flow_config: self.flow_config.clone(),
492                extract_fields: self.extract_fields.clone(),
493                parallel_create: self.parallel_create,
494                data_file: self.data_file.clone(),
495                data_distribution: self.data_distribution.clone(),
496                data_mappings: self.data_mappings.clone(),
497                per_uri_control: self.per_uri_control,
498                error_rate: self.error_rate,
499                error_types: self.error_types.clone(),
500                security_test: self.security_test,
501                security_payloads: self.security_payloads.clone(),
502                security_categories: self.security_categories.clone(),
503                security_target_fields: self.security_target_fields.clone(),
504                wafbench_dir: self.wafbench_dir.clone(),
505                wafbench_cycle_all: self.wafbench_cycle_all,
506                owasp_api_top10: self.owasp_api_top10,
507                owasp_categories: self.owasp_categories.clone(),
508                owasp_auth_header: self.owasp_auth_header.clone(),
509                owasp_auth_token: self.owasp_auth_token.clone(),
510                owasp_admin_paths: self.owasp_admin_paths.clone(),
511                owasp_id_fields: self.owasp_id_fields.clone(),
512                owasp_report: self.owasp_report.clone(),
513                owasp_report_format: self.owasp_report_format.clone(),
514                owasp_iterations: self.owasp_iterations,
515            },
516            targets,
517            max_concurrency,
518        );
519
520        // Execute all targets
521        let aggregated_results = executor.execute_all().await?;
522
523        // Organize and report results
524        self.report_multi_target_results(&aggregated_results)?;
525
526        Ok(())
527    }
528
529    /// Report results for multi-target execution
530    fn report_multi_target_results(&self, results: &AggregatedResults) -> Result<()> {
531        // Print summary
532        TerminalReporter::print_multi_target_summary(results);
533
534        // Save aggregated summary if requested
535        if self.results_format == "aggregated" || self.results_format == "both" {
536            let summary_path = self.output.join("aggregated_summary.json");
537            let summary_json = serde_json::json!({
538                "total_targets": results.total_targets,
539                "successful_targets": results.successful_targets,
540                "failed_targets": results.failed_targets,
541                "aggregated_metrics": {
542                    "total_requests": results.aggregated_metrics.total_requests,
543                    "total_failed_requests": results.aggregated_metrics.total_failed_requests,
544                    "avg_duration_ms": results.aggregated_metrics.avg_duration_ms,
545                    "p95_duration_ms": results.aggregated_metrics.p95_duration_ms,
546                    "p99_duration_ms": results.aggregated_metrics.p99_duration_ms,
547                    "error_rate": results.aggregated_metrics.error_rate,
548                },
549                "target_results": results.target_results.iter().map(|r| {
550                    serde_json::json!({
551                        "target_url": r.target_url,
552                        "target_index": r.target_index,
553                        "success": r.success,
554                        "error": r.error,
555                        "total_requests": r.results.total_requests,
556                        "failed_requests": r.results.failed_requests,
557                        "avg_duration_ms": r.results.avg_duration_ms,
558                        "p95_duration_ms": r.results.p95_duration_ms,
559                        "p99_duration_ms": r.results.p99_duration_ms,
560                        "output_dir": r.output_dir.to_string_lossy(),
561                    })
562                }).collect::<Vec<_>>(),
563            });
564
565            std::fs::write(&summary_path, serde_json::to_string_pretty(&summary_json)?)?;
566            TerminalReporter::print_success(&format!(
567                "Aggregated summary saved to: {}",
568                summary_path.display()
569            ));
570        }
571
572        println!("\nResults saved to: {}", self.output.display());
573        println!("  - Per-target results: {}", self.output.join("target_*").display());
574        if self.results_format == "aggregated" || self.results_format == "both" {
575            println!(
576                "  - Aggregated summary: {}",
577                self.output.join("aggregated_summary.json").display()
578            );
579        }
580
581        Ok(())
582    }
583
584    /// Parse duration string (e.g., "30s", "5m", "1h") to seconds
585    pub fn parse_duration(duration: &str) -> Result<u64> {
586        let duration = duration.trim();
587
588        if let Some(secs) = duration.strip_suffix('s') {
589            secs.parse::<u64>()
590                .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
591        } else if let Some(mins) = duration.strip_suffix('m') {
592            mins.parse::<u64>()
593                .map(|m| m * 60)
594                .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
595        } else if let Some(hours) = duration.strip_suffix('h') {
596            hours
597                .parse::<u64>()
598                .map(|h| h * 3600)
599                .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
600        } else {
601            // Try parsing as seconds without suffix
602            duration
603                .parse::<u64>()
604                .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
605        }
606    }
607
608    /// Parse headers from command line format (Key:Value,Key2:Value2)
609    pub fn parse_headers(&self) -> Result<HashMap<String, String>> {
610        let mut headers = HashMap::new();
611
612        if let Some(header_str) = &self.headers {
613            for pair in header_str.split(',') {
614                let parts: Vec<&str> = pair.splitn(2, ':').collect();
615                if parts.len() != 2 {
616                    return Err(BenchError::Other(format!(
617                        "Invalid header format: '{}'. Expected 'Key:Value'",
618                        pair
619                    )));
620                }
621                headers.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
622            }
623        }
624
625        Ok(headers)
626    }
627
628    /// Resolve the effective base path for API endpoints
629    ///
630    /// Priority:
631    /// 1. CLI --base-path option (if provided, even if empty string)
632    /// 2. Base path extracted from OpenAPI spec's servers URL
633    /// 3. None (no base path)
634    ///
635    /// An empty string from CLI explicitly disables base path.
636    fn resolve_base_path(&self, parser: &SpecParser) -> Option<String> {
637        // CLI option takes priority (including empty string to disable)
638        if let Some(cli_base_path) = &self.base_path {
639            if cli_base_path.is_empty() {
640                // Empty string explicitly means "no base path"
641                return None;
642            }
643            return Some(cli_base_path.clone());
644        }
645
646        // Fall back to spec's base path
647        parser.get_base_path()
648    }
649
650    /// Build mock server integration configuration
651    async fn build_mock_config(&self) -> MockIntegrationConfig {
652        // Check if target looks like a mock server
653        if MockServerDetector::looks_like_mock_server(&self.target) {
654            // Try to detect if it's actually a MockForge server
655            if let Ok(info) = MockServerDetector::detect(&self.target).await {
656                if info.is_mockforge {
657                    TerminalReporter::print_success(&format!(
658                        "Detected MockForge server (version: {})",
659                        info.version.as_deref().unwrap_or("unknown")
660                    ));
661                    return MockIntegrationConfig::mock_server();
662                }
663            }
664        }
665        MockIntegrationConfig::real_api()
666    }
667
668    /// Build CRUD flow configuration
669    fn build_crud_flow_config(&self) -> Option<CrudFlowConfig> {
670        if !self.crud_flow {
671            return None;
672        }
673
674        // If flow_config file is provided, load it
675        if let Some(config_path) = &self.flow_config {
676            match CrudFlowConfig::from_file(config_path) {
677                Ok(config) => return Some(config),
678                Err(e) => {
679                    TerminalReporter::print_warning(&format!(
680                        "Failed to load flow config: {}. Using auto-detection.",
681                        e
682                    ));
683                }
684            }
685        }
686
687        // Parse extract fields
688        let extract_fields = self
689            .extract_fields
690            .as_ref()
691            .map(|f| f.split(',').map(|s| s.trim().to_string()).collect())
692            .unwrap_or_else(|| vec!["id".to_string(), "uuid".to_string()]);
693
694        Some(CrudFlowConfig {
695            flows: Vec::new(), // Will be auto-detected
696            default_extract_fields: extract_fields,
697        })
698    }
699
700    /// Build data-driven testing configuration
701    fn build_data_driven_config(&self) -> Option<DataDrivenConfig> {
702        let data_file = self.data_file.as_ref()?;
703
704        let distribution = DataDistribution::from_str(&self.data_distribution)
705            .unwrap_or(DataDistribution::UniquePerVu);
706
707        let mappings = self
708            .data_mappings
709            .as_ref()
710            .map(|m| DataMapping::parse_mappings(m).unwrap_or_default())
711            .unwrap_or_default();
712
713        Some(DataDrivenConfig {
714            file_path: data_file.to_string_lossy().to_string(),
715            distribution,
716            mappings,
717            csv_has_header: true,
718            per_uri_control: self.per_uri_control,
719            per_uri_columns: crate::data_driven::PerUriColumns::default(),
720        })
721    }
722
723    /// Build invalid data testing configuration
724    fn build_invalid_data_config(&self) -> Option<InvalidDataConfig> {
725        let error_rate = self.error_rate?;
726
727        let error_types = self
728            .error_types
729            .as_ref()
730            .map(|types| InvalidDataConfig::parse_error_types(types).unwrap_or_default())
731            .unwrap_or_default();
732
733        Some(InvalidDataConfig {
734            error_rate,
735            error_types,
736            target_fields: Vec::new(),
737        })
738    }
739
740    /// Build security testing configuration
741    fn build_security_config(&self) -> Option<SecurityTestConfig> {
742        if !self.security_test {
743            return None;
744        }
745
746        let categories = self
747            .security_categories
748            .as_ref()
749            .map(|cats| SecurityTestConfig::parse_categories(cats).unwrap_or_default())
750            .unwrap_or_else(|| {
751                let mut default = std::collections::HashSet::new();
752                default.insert(SecurityCategory::SqlInjection);
753                default.insert(SecurityCategory::Xss);
754                default
755            });
756
757        let target_fields = self
758            .security_target_fields
759            .as_ref()
760            .map(|fields| fields.split(',').map(|f| f.trim().to_string()).collect())
761            .unwrap_or_default();
762
763        let custom_payloads_file =
764            self.security_payloads.as_ref().map(|p| p.to_string_lossy().to_string());
765
766        Some(SecurityTestConfig {
767            enabled: true,
768            categories,
769            target_fields,
770            custom_payloads_file,
771            include_high_risk: false,
772        })
773    }
774
775    /// Build parallel execution configuration
776    fn build_parallel_config(&self) -> Option<ParallelConfig> {
777        let count = self.parallel_create?;
778
779        Some(ParallelConfig::new(count))
780    }
781
782    /// Load WAFBench payloads from the specified directory or pattern
783    fn load_wafbench_payloads(&self) -> Vec<SecurityPayload> {
784        let Some(ref wafbench_dir) = self.wafbench_dir else {
785            return Vec::new();
786        };
787
788        let mut loader = WafBenchLoader::new();
789
790        if let Err(e) = loader.load_from_pattern(wafbench_dir) {
791            TerminalReporter::print_warning(&format!("Failed to load WAFBench tests: {}", e));
792            return Vec::new();
793        }
794
795        let stats = loader.stats();
796
797        if stats.files_processed == 0 {
798            TerminalReporter::print_warning(&format!(
799                "No WAFBench YAML files found matching '{}'",
800                wafbench_dir
801            ));
802            // Also report any parse errors that may explain why no files were processed
803            if !stats.parse_errors.is_empty() {
804                TerminalReporter::print_warning("Some files were found but failed to parse:");
805                for error in &stats.parse_errors {
806                    TerminalReporter::print_warning(&format!("  - {}", error));
807                }
808            }
809            return Vec::new();
810        }
811
812        TerminalReporter::print_progress(&format!(
813            "Loaded {} WAFBench files, {} test cases, {} payloads",
814            stats.files_processed, stats.test_cases_loaded, stats.payloads_extracted
815        ));
816
817        // Print category breakdown
818        for (category, count) in &stats.by_category {
819            TerminalReporter::print_progress(&format!("  - {}: {} tests", category, count));
820        }
821
822        // Report any parse errors
823        for error in &stats.parse_errors {
824            TerminalReporter::print_warning(&format!("  Parse error: {}", error));
825        }
826
827        loader.to_security_payloads()
828    }
829
830    /// Generate enhanced k6 script with advanced features
831    pub(crate) fn generate_enhanced_script(&self, base_script: &str) -> Result<String> {
832        let mut enhanced_script = base_script.to_string();
833        let mut additional_code = String::new();
834
835        // Add data-driven testing code
836        if let Some(config) = self.build_data_driven_config() {
837            TerminalReporter::print_progress("Adding data-driven testing support...");
838            additional_code.push_str(&DataDrivenGenerator::generate_setup(&config));
839            additional_code.push('\n');
840            TerminalReporter::print_success("Data-driven testing enabled");
841        }
842
843        // Add invalid data generation code
844        if let Some(config) = self.build_invalid_data_config() {
845            TerminalReporter::print_progress("Adding invalid data testing support...");
846            additional_code.push_str(&InvalidDataGenerator::generate_invalidation_logic());
847            additional_code.push('\n');
848            additional_code
849                .push_str(&InvalidDataGenerator::generate_should_invalidate(config.error_rate));
850            additional_code.push('\n');
851            additional_code
852                .push_str(&InvalidDataGenerator::generate_type_selection(&config.error_types));
853            additional_code.push('\n');
854            TerminalReporter::print_success(&format!(
855                "Invalid data testing enabled ({}% error rate)",
856                (self.error_rate.unwrap_or(0.0) * 100.0) as u32
857            ));
858        }
859
860        // Add security testing code
861        let security_config = self.build_security_config();
862        let wafbench_payloads = self.load_wafbench_payloads();
863        let security_requested = security_config.is_some() || self.wafbench_dir.is_some();
864
865        if security_config.is_some() || !wafbench_payloads.is_empty() {
866            TerminalReporter::print_progress("Adding security testing support...");
867
868            // Combine built-in payloads with WAFBench payloads
869            let mut payload_list: Vec<SecurityPayload> = Vec::new();
870
871            if let Some(ref config) = security_config {
872                payload_list.extend(SecurityPayloads::get_payloads(config));
873            }
874
875            // Add WAFBench payloads
876            if !wafbench_payloads.is_empty() {
877                TerminalReporter::print_progress(&format!(
878                    "Loading {} WAFBench attack patterns...",
879                    wafbench_payloads.len()
880                ));
881                payload_list.extend(wafbench_payloads);
882            }
883
884            let target_fields =
885                security_config.as_ref().map(|c| c.target_fields.clone()).unwrap_or_default();
886
887            additional_code.push_str(&SecurityTestGenerator::generate_payload_selection(
888                &payload_list,
889                self.wafbench_cycle_all,
890            ));
891            additional_code.push('\n');
892            additional_code
893                .push_str(&SecurityTestGenerator::generate_apply_payload(&target_fields));
894            additional_code.push('\n');
895            additional_code.push_str(&SecurityTestGenerator::generate_security_checks());
896            additional_code.push('\n');
897
898            let mode = if self.wafbench_cycle_all {
899                "cycle-all"
900            } else {
901                "random"
902            };
903            TerminalReporter::print_success(&format!(
904                "Security testing enabled ({} payloads, {} mode)",
905                payload_list.len(),
906                mode
907            ));
908        } else if security_requested {
909            // User requested security testing (e.g., --wafbench-dir) but no payloads were loaded.
910            // The template has security_testing_enabled=true so it renders calling code.
911            // We must inject stub definitions to avoid undefined function references.
912            TerminalReporter::print_warning(
913                "Security testing was requested but no payloads were loaded. \
914                 Ensure --wafbench-dir points to valid CRS YAML files or add --security-test.",
915            );
916            additional_code
917                .push_str(&SecurityTestGenerator::generate_payload_selection(&[], false));
918            additional_code.push('\n');
919            additional_code.push_str(&SecurityTestGenerator::generate_apply_payload(&[]));
920            additional_code.push('\n');
921        }
922
923        // Add parallel execution code
924        if let Some(config) = self.build_parallel_config() {
925            TerminalReporter::print_progress("Adding parallel execution support...");
926            additional_code.push_str(&ParallelRequestGenerator::generate_batch_helper(&config));
927            additional_code.push('\n');
928            TerminalReporter::print_success(&format!(
929                "Parallel execution enabled (count: {})",
930                config.count
931            ));
932        }
933
934        // Insert additional code after the imports section
935        if !additional_code.is_empty() {
936            // Find the end of the import section
937            if let Some(import_end) = enhanced_script.find("export const options") {
938                enhanced_script.insert_str(
939                    import_end,
940                    &format!("\n// === Advanced Testing Features ===\n{}\n", additional_code),
941                );
942            }
943        }
944
945        Ok(enhanced_script)
946    }
947
948    /// Execute specs sequentially with dependency ordering and value passing
949    async fn execute_sequential_specs(&self) -> Result<()> {
950        TerminalReporter::print_progress("Sequential spec mode: Loading specs individually...");
951
952        // Load all specs (without merging)
953        let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
954
955        if !self.spec.is_empty() {
956            let specs = load_specs_from_files(self.spec.clone())
957                .await
958                .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
959            all_specs.extend(specs);
960        }
961
962        if let Some(spec_dir) = &self.spec_dir {
963            let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
964                BenchError::Other(format!("Failed to load specs from directory: {}", e))
965            })?;
966            all_specs.extend(dir_specs);
967        }
968
969        if all_specs.is_empty() {
970            return Err(BenchError::Other(
971                "No spec files found for sequential execution".to_string(),
972            ));
973        }
974
975        TerminalReporter::print_success(&format!("Loaded {} spec(s)", all_specs.len()));
976
977        // Load dependency config or auto-detect
978        let execution_order = if let Some(config_path) = &self.dependency_config {
979            TerminalReporter::print_progress("Loading dependency configuration...");
980            let config = SpecDependencyConfig::from_file(config_path)?;
981
982            if !config.disable_auto_detect && config.execution_order.is_empty() {
983                // Auto-detect if config doesn't specify order
984                self.detect_and_sort_specs(&all_specs)?
985            } else {
986                // Use configured order
987                config.execution_order.iter().flat_map(|g| g.specs.clone()).collect()
988            }
989        } else {
990            // Auto-detect dependencies
991            self.detect_and_sort_specs(&all_specs)?
992        };
993
994        TerminalReporter::print_success(&format!(
995            "Execution order: {}",
996            execution_order
997                .iter()
998                .map(|p| p.file_name().unwrap_or_default().to_string_lossy().to_string())
999                .collect::<Vec<_>>()
1000                .join(" → ")
1001        ));
1002
1003        // Execute each spec in order
1004        let mut extracted_values = ExtractedValues::new();
1005        let total_specs = execution_order.len();
1006
1007        for (index, spec_path) in execution_order.iter().enumerate() {
1008            let spec_name = spec_path.file_name().unwrap_or_default().to_string_lossy().to_string();
1009
1010            TerminalReporter::print_progress(&format!(
1011                "[{}/{}] Executing spec: {}",
1012                index + 1,
1013                total_specs,
1014                spec_name
1015            ));
1016
1017            // Find the spec in our loaded specs (match by full path or filename)
1018            let spec = all_specs
1019                .iter()
1020                .find(|(p, _)| {
1021                    p == spec_path
1022                        || p.file_name() == spec_path.file_name()
1023                        || p.file_name() == Some(spec_path.as_os_str())
1024                })
1025                .map(|(_, s)| s.clone())
1026                .ok_or_else(|| {
1027                    BenchError::Other(format!("Spec not found: {}", spec_path.display()))
1028                })?;
1029
1030            // Execute this spec with any extracted values from previous specs
1031            let new_values = self.execute_single_spec(&spec, &spec_name, &extracted_values).await?;
1032
1033            // Merge extracted values for the next spec
1034            extracted_values.merge(&new_values);
1035
1036            TerminalReporter::print_success(&format!(
1037                "[{}/{}] Completed: {} (extracted {} values)",
1038                index + 1,
1039                total_specs,
1040                spec_name,
1041                new_values.values.len()
1042            ));
1043        }
1044
1045        TerminalReporter::print_success(&format!(
1046            "Sequential execution complete: {} specs executed",
1047            total_specs
1048        ));
1049
1050        Ok(())
1051    }
1052
1053    /// Detect dependencies and return topologically sorted spec paths
1054    fn detect_and_sort_specs(&self, specs: &[(PathBuf, OpenApiSpec)]) -> Result<Vec<PathBuf>> {
1055        TerminalReporter::print_progress("Auto-detecting spec dependencies...");
1056
1057        let mut detector = DependencyDetector::new();
1058        let dependencies = detector.detect_dependencies(specs);
1059
1060        if dependencies.is_empty() {
1061            TerminalReporter::print_progress("No dependencies detected, using file order");
1062            return Ok(specs.iter().map(|(p, _)| p.clone()).collect());
1063        }
1064
1065        TerminalReporter::print_progress(&format!(
1066            "Detected {} cross-spec dependencies",
1067            dependencies.len()
1068        ));
1069
1070        for dep in &dependencies {
1071            TerminalReporter::print_progress(&format!(
1072                "  {} → {} (via field '{}')",
1073                dep.dependency_spec.file_name().unwrap_or_default().to_string_lossy(),
1074                dep.dependent_spec.file_name().unwrap_or_default().to_string_lossy(),
1075                dep.field_name
1076            ));
1077        }
1078
1079        topological_sort(specs, &dependencies)
1080    }
1081
1082    /// Execute a single spec and extract values for dependent specs
1083    async fn execute_single_spec(
1084        &self,
1085        spec: &OpenApiSpec,
1086        spec_name: &str,
1087        _external_values: &ExtractedValues,
1088    ) -> Result<ExtractedValues> {
1089        let parser = SpecParser::from_spec(spec.clone());
1090
1091        // For now, we execute in CRUD flow mode if enabled, otherwise standard mode
1092        if self.crud_flow {
1093            // Execute CRUD flow and extract values
1094            self.execute_crud_flow_with_extraction(&parser, spec_name).await
1095        } else {
1096            // Execute standard benchmark (no value extraction in non-CRUD mode)
1097            self.execute_standard_spec(&parser, spec_name).await?;
1098            Ok(ExtractedValues::new())
1099        }
1100    }
1101
1102    /// Execute CRUD flow with value extraction for sequential mode
1103    async fn execute_crud_flow_with_extraction(
1104        &self,
1105        parser: &SpecParser,
1106        spec_name: &str,
1107    ) -> Result<ExtractedValues> {
1108        let operations = parser.get_operations();
1109        let flows = CrudFlowDetector::detect_flows(&operations);
1110
1111        if flows.is_empty() {
1112            TerminalReporter::print_warning(&format!("No CRUD flows detected in {}", spec_name));
1113            return Ok(ExtractedValues::new());
1114        }
1115
1116        TerminalReporter::print_progress(&format!(
1117            "  {} CRUD flow(s) in {}",
1118            flows.len(),
1119            spec_name
1120        ));
1121
1122        // Generate and execute the CRUD flow script
1123        let mut handlebars = handlebars::Handlebars::new();
1124        // Register json helper for serializing arrays/objects in templates
1125        handlebars.register_helper(
1126            "json",
1127            Box::new(
1128                |h: &handlebars::Helper,
1129                 _: &handlebars::Handlebars,
1130                 _: &handlebars::Context,
1131                 _: &mut handlebars::RenderContext,
1132                 out: &mut dyn handlebars::Output|
1133                 -> handlebars::HelperResult {
1134                    let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1135                    out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1136                    Ok(())
1137                },
1138            ),
1139        );
1140        let template = include_str!("templates/k6_crud_flow.hbs");
1141
1142        let custom_headers = self.parse_headers()?;
1143        let config = self.build_crud_flow_config().unwrap_or_default();
1144
1145        // Load parameter overrides if provided (for body configurations)
1146        let param_overrides = if let Some(params_file) = &self.params_file {
1147            let overrides = ParameterOverrides::from_file(params_file)?;
1148            Some(overrides)
1149        } else {
1150            None
1151        };
1152
1153        // Generate stages from scenario
1154        let duration_secs = Self::parse_duration(&self.duration)?;
1155        let scenario =
1156            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1157        let stages = scenario.generate_stages(duration_secs, self.vus);
1158
1159        // Resolve base path (CLI option takes priority over spec's servers URL)
1160        let api_base_path = self.resolve_base_path(parser);
1161
1162        // Build headers JSON string for the template
1163        let mut all_headers = custom_headers.clone();
1164        if let Some(auth) = &self.auth {
1165            all_headers.insert("Authorization".to_string(), auth.clone());
1166        }
1167        let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1168
1169        // Track all dynamic placeholders across all operations
1170        let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1171
1172        let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1173            let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1174            serde_json::json!({
1175                "name": sanitized_name.clone(),
1176                "display_name": f.name,
1177                "base_path": f.base_path,
1178                "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1179                    // Parse operation to get method and path
1180                    let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1181                    let method_raw = if !parts.is_empty() {
1182                        parts[0].to_uppercase()
1183                    } else {
1184                        "GET".to_string()
1185                    };
1186                    let method = if !parts.is_empty() {
1187                        let m = parts[0].to_lowercase();
1188                        // k6 uses 'del' for DELETE
1189                        if m == "delete" { "del".to_string() } else { m }
1190                    } else {
1191                        "get".to_string()
1192                    };
1193                    let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1194                    // Prepend API base path if configured
1195                    let path = if let Some(ref bp) = api_base_path {
1196                        format!("{}{}", bp, raw_path)
1197                    } else {
1198                        raw_path.to_string()
1199                    };
1200                    let is_get_or_head = method == "get" || method == "head";
1201                    // POST, PUT, PATCH typically have bodies
1202                    let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1203
1204                    // Look up body from params file if available
1205                    let body_value = if has_body {
1206                        param_overrides.as_ref()
1207                            .map(|po| po.get_for_operation(None, &method_raw, raw_path))
1208                            .and_then(|oo| oo.body)
1209                            .unwrap_or_else(|| serde_json::json!({}))
1210                    } else {
1211                        serde_json::json!({})
1212                    };
1213
1214                    // Process body for dynamic placeholders like ${__VU}, ${__ITER}, etc.
1215                    let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1216
1217                    // Also check for ${extracted.xxx} placeholders which need runtime substitution
1218                    let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1219                    let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1220
1221                    serde_json::json!({
1222                        "operation": s.operation,
1223                        "method": method,
1224                        "path": path,
1225                        "extract": s.extract,
1226                        "use_values": s.use_values,
1227                        "use_body": s.use_body,
1228                        "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1229                        "inject_attacks": s.inject_attacks,
1230                        "attack_types": s.attack_types,
1231                        "description": s.description,
1232                        "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1233                        "is_get_or_head": is_get_or_head,
1234                        "has_body": has_body,
1235                        "body": processed_body.value,
1236                        "body_is_dynamic": body_is_dynamic,
1237                        "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1238                    })
1239                }).collect::<Vec<_>>(),
1240            })
1241        }).collect();
1242
1243        // Collect all placeholders from all steps
1244        for flow_data in &flows_data {
1245            if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1246                for step in steps {
1247                    if let Some(placeholders_arr) =
1248                        step.get("_placeholders").and_then(|p| p.as_array())
1249                    {
1250                        for p_str in placeholders_arr {
1251                            if let Some(p_name) = p_str.as_str() {
1252                                match p_name {
1253                                    "VU" => {
1254                                        all_placeholders.insert(DynamicPlaceholder::VU);
1255                                    }
1256                                    "Iteration" => {
1257                                        all_placeholders.insert(DynamicPlaceholder::Iteration);
1258                                    }
1259                                    "Timestamp" => {
1260                                        all_placeholders.insert(DynamicPlaceholder::Timestamp);
1261                                    }
1262                                    "UUID" => {
1263                                        all_placeholders.insert(DynamicPlaceholder::UUID);
1264                                    }
1265                                    "Random" => {
1266                                        all_placeholders.insert(DynamicPlaceholder::Random);
1267                                    }
1268                                    "Counter" => {
1269                                        all_placeholders.insert(DynamicPlaceholder::Counter);
1270                                    }
1271                                    "Date" => {
1272                                        all_placeholders.insert(DynamicPlaceholder::Date);
1273                                    }
1274                                    "VuIter" => {
1275                                        all_placeholders.insert(DynamicPlaceholder::VuIter);
1276                                    }
1277                                    _ => {}
1278                                }
1279                            }
1280                        }
1281                    }
1282                }
1283            }
1284        }
1285
1286        // Get required imports and globals based on placeholders used
1287        let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1288        let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1289
1290        // Check if security testing is enabled
1291        let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1292
1293        let data = serde_json::json!({
1294            "base_url": self.target,
1295            "flows": flows_data,
1296            "extract_fields": config.default_extract_fields,
1297            "duration_secs": duration_secs,
1298            "max_vus": self.vus,
1299            "auth_header": self.auth,
1300            "custom_headers": custom_headers,
1301            "skip_tls_verify": self.skip_tls_verify,
1302            // Add missing template fields
1303            "stages": stages.iter().map(|s| serde_json::json!({
1304                "duration": s.duration,
1305                "target": s.target,
1306            })).collect::<Vec<_>>(),
1307            "threshold_percentile": self.threshold_percentile,
1308            "threshold_ms": self.threshold_ms,
1309            "max_error_rate": self.max_error_rate,
1310            "headers": headers_json,
1311            "dynamic_imports": required_imports,
1312            "dynamic_globals": required_globals,
1313            // Security testing settings
1314            "security_testing_enabled": security_testing_enabled,
1315            "has_custom_headers": !custom_headers.is_empty(),
1316        });
1317
1318        let mut script = handlebars
1319            .render_template(template, &data)
1320            .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1321
1322        // Enhance script with security testing support if enabled
1323        if security_testing_enabled {
1324            script = self.generate_enhanced_script(&script)?;
1325        }
1326
1327        // Write and execute script
1328        let script_path =
1329            self.output.join(format!("k6-{}-crud-flow.js", spec_name.replace('.', "_")));
1330
1331        std::fs::create_dir_all(self.output.clone())?;
1332        std::fs::write(&script_path, &script)?;
1333
1334        if !self.generate_only {
1335            let executor = K6Executor::new()?;
1336            let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1337            std::fs::create_dir_all(&output_dir)?;
1338
1339            executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1340        }
1341
1342        // For now, return empty extracted values
1343        // TODO: Parse k6 output to extract actual values
1344        Ok(ExtractedValues::new())
1345    }
1346
1347    /// Execute standard (non-CRUD) spec benchmark
1348    async fn execute_standard_spec(&self, parser: &SpecParser, spec_name: &str) -> Result<()> {
1349        let mut operations = if let Some(filter) = &self.operations {
1350            parser.filter_operations(filter)?
1351        } else {
1352            parser.get_operations()
1353        };
1354
1355        if let Some(exclude) = &self.exclude_operations {
1356            operations = parser.exclude_operations(operations, exclude)?;
1357        }
1358
1359        if operations.is_empty() {
1360            TerminalReporter::print_warning(&format!("No operations found in {}", spec_name));
1361            return Ok(());
1362        }
1363
1364        TerminalReporter::print_progress(&format!(
1365            "  {} operations in {}",
1366            operations.len(),
1367            spec_name
1368        ));
1369
1370        // Generate request templates
1371        let templates: Vec<_> = operations
1372            .iter()
1373            .map(RequestGenerator::generate_template)
1374            .collect::<Result<Vec<_>>>()?;
1375
1376        // Parse headers
1377        let custom_headers = self.parse_headers()?;
1378
1379        // Resolve base path
1380        let base_path = self.resolve_base_path(parser);
1381
1382        // Generate k6 script
1383        let scenario =
1384            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1385
1386        let security_testing_enabled = self.security_test || self.wafbench_dir.is_some();
1387
1388        let k6_config = K6Config {
1389            target_url: self.target.clone(),
1390            base_path,
1391            scenario,
1392            duration_secs: Self::parse_duration(&self.duration)?,
1393            max_vus: self.vus,
1394            threshold_percentile: self.threshold_percentile.clone(),
1395            threshold_ms: self.threshold_ms,
1396            max_error_rate: self.max_error_rate,
1397            auth_header: self.auth.clone(),
1398            custom_headers,
1399            skip_tls_verify: self.skip_tls_verify,
1400            security_testing_enabled,
1401        };
1402
1403        let generator = K6ScriptGenerator::new(k6_config, templates);
1404        let mut script = generator.generate()?;
1405
1406        // Enhance script with advanced features (security testing, etc.)
1407        let has_advanced_features = self.data_file.is_some()
1408            || self.error_rate.is_some()
1409            || self.security_test
1410            || self.parallel_create.is_some()
1411            || self.wafbench_dir.is_some();
1412
1413        if has_advanced_features {
1414            script = self.generate_enhanced_script(&script)?;
1415        }
1416
1417        // Write and execute script
1418        let script_path = self.output.join(format!("k6-{}.js", spec_name.replace('.', "_")));
1419
1420        std::fs::create_dir_all(self.output.clone())?;
1421        std::fs::write(&script_path, &script)?;
1422
1423        if !self.generate_only {
1424            let executor = K6Executor::new()?;
1425            let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1426            std::fs::create_dir_all(&output_dir)?;
1427
1428            executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1429        }
1430
1431        Ok(())
1432    }
1433
1434    /// Execute CRUD flow testing mode
1435    async fn execute_crud_flow(&self, parser: &SpecParser) -> Result<()> {
1436        // Check if a custom flow config is provided
1437        let config = self.build_crud_flow_config().unwrap_or_default();
1438
1439        // Use flows from config if provided, otherwise auto-detect
1440        let flows = if !config.flows.is_empty() {
1441            TerminalReporter::print_progress("Using custom flow configuration...");
1442            config.flows.clone()
1443        } else {
1444            TerminalReporter::print_progress("Detecting CRUD operations...");
1445            let operations = parser.get_operations();
1446            CrudFlowDetector::detect_flows(&operations)
1447        };
1448
1449        if flows.is_empty() {
1450            return Err(BenchError::Other(
1451                "No CRUD flows detected in spec. Ensure spec has POST/GET/PUT/DELETE operations on related paths.".to_string(),
1452            ));
1453        }
1454
1455        if config.flows.is_empty() {
1456            TerminalReporter::print_success(&format!("Detected {} CRUD flow(s)", flows.len()));
1457        } else {
1458            TerminalReporter::print_success(&format!("Loaded {} custom flow(s)", flows.len()));
1459        }
1460
1461        for flow in &flows {
1462            TerminalReporter::print_progress(&format!(
1463                "  - {}: {} steps",
1464                flow.name,
1465                flow.steps.len()
1466            ));
1467        }
1468
1469        // Generate CRUD flow script
1470        let mut handlebars = handlebars::Handlebars::new();
1471        // Register json helper for serializing arrays/objects in templates
1472        handlebars.register_helper(
1473            "json",
1474            Box::new(
1475                |h: &handlebars::Helper,
1476                 _: &handlebars::Handlebars,
1477                 _: &handlebars::Context,
1478                 _: &mut handlebars::RenderContext,
1479                 out: &mut dyn handlebars::Output|
1480                 -> handlebars::HelperResult {
1481                    let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1482                    out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1483                    Ok(())
1484                },
1485            ),
1486        );
1487        let template = include_str!("templates/k6_crud_flow.hbs");
1488
1489        let custom_headers = self.parse_headers()?;
1490
1491        // Load parameter overrides if provided (for body configurations)
1492        let param_overrides = if let Some(params_file) = &self.params_file {
1493            TerminalReporter::print_progress("Loading parameter overrides...");
1494            let overrides = ParameterOverrides::from_file(params_file)?;
1495            TerminalReporter::print_success(&format!(
1496                "Loaded parameter overrides ({} operation-specific, {} defaults)",
1497                overrides.operations.len(),
1498                if overrides.defaults.is_empty() { 0 } else { 1 }
1499            ));
1500            Some(overrides)
1501        } else {
1502            None
1503        };
1504
1505        // Generate stages from scenario
1506        let duration_secs = Self::parse_duration(&self.duration)?;
1507        let scenario =
1508            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1509        let stages = scenario.generate_stages(duration_secs, self.vus);
1510
1511        // Resolve base path (CLI option takes priority over spec's servers URL)
1512        let api_base_path = self.resolve_base_path(parser);
1513        if let Some(ref bp) = api_base_path {
1514            TerminalReporter::print_progress(&format!("Using base path: {}", bp));
1515        }
1516
1517        // Build headers JSON string for the template
1518        let mut all_headers = custom_headers.clone();
1519        if let Some(auth) = &self.auth {
1520            all_headers.insert("Authorization".to_string(), auth.clone());
1521        }
1522        let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1523
1524        // Track all dynamic placeholders across all operations
1525        let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1526
1527        let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1528            // Sanitize flow name for use as JavaScript variable and k6 metric names
1529            let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1530            serde_json::json!({
1531                "name": sanitized_name.clone(),  // Use sanitized name for variable names
1532                "display_name": f.name,          // Keep original for comments/display
1533                "base_path": f.base_path,
1534                "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1535                    // Parse operation to get method and path
1536                    let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1537                    let method_raw = if !parts.is_empty() {
1538                        parts[0].to_uppercase()
1539                    } else {
1540                        "GET".to_string()
1541                    };
1542                    let method = if !parts.is_empty() {
1543                        let m = parts[0].to_lowercase();
1544                        // k6 uses 'del' for DELETE
1545                        if m == "delete" { "del".to_string() } else { m }
1546                    } else {
1547                        "get".to_string()
1548                    };
1549                    let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1550                    // Prepend API base path if configured
1551                    let path = if let Some(ref bp) = api_base_path {
1552                        format!("{}{}", bp, raw_path)
1553                    } else {
1554                        raw_path.to_string()
1555                    };
1556                    let is_get_or_head = method == "get" || method == "head";
1557                    // POST, PUT, PATCH typically have bodies
1558                    let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1559
1560                    // Look up body from params file if available (use raw_path for matching)
1561                    let body_value = if has_body {
1562                        param_overrides.as_ref()
1563                            .map(|po| po.get_for_operation(None, &method_raw, raw_path))
1564                            .and_then(|oo| oo.body)
1565                            .unwrap_or_else(|| serde_json::json!({}))
1566                    } else {
1567                        serde_json::json!({})
1568                    };
1569
1570                    // Process body for dynamic placeholders like ${__VU}, ${__ITER}, etc.
1571                    let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1572                    // Note: all_placeholders is captured by the closure but we can't mutate it directly
1573                    // We'll collect placeholders separately below
1574
1575                    // Also check for ${extracted.xxx} placeholders which need runtime substitution
1576                    let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1577                    let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1578
1579                    serde_json::json!({
1580                        "operation": s.operation,
1581                        "method": method,
1582                        "path": path,
1583                        "extract": s.extract,
1584                        "use_values": s.use_values,
1585                        "use_body": s.use_body,
1586                        "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1587                        "inject_attacks": s.inject_attacks,
1588                        "attack_types": s.attack_types,
1589                        "description": s.description,
1590                        "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1591                        "is_get_or_head": is_get_or_head,
1592                        "has_body": has_body,
1593                        "body": processed_body.value,
1594                        "body_is_dynamic": body_is_dynamic,
1595                        "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1596                    })
1597                }).collect::<Vec<_>>(),
1598            })
1599        }).collect();
1600
1601        // Collect all placeholders from all steps
1602        for flow_data in &flows_data {
1603            if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1604                for step in steps {
1605                    if let Some(placeholders_arr) =
1606                        step.get("_placeholders").and_then(|p| p.as_array())
1607                    {
1608                        for p_str in placeholders_arr {
1609                            if let Some(p_name) = p_str.as_str() {
1610                                // Parse placeholder from debug string
1611                                match p_name {
1612                                    "VU" => {
1613                                        all_placeholders.insert(DynamicPlaceholder::VU);
1614                                    }
1615                                    "Iteration" => {
1616                                        all_placeholders.insert(DynamicPlaceholder::Iteration);
1617                                    }
1618                                    "Timestamp" => {
1619                                        all_placeholders.insert(DynamicPlaceholder::Timestamp);
1620                                    }
1621                                    "UUID" => {
1622                                        all_placeholders.insert(DynamicPlaceholder::UUID);
1623                                    }
1624                                    "Random" => {
1625                                        all_placeholders.insert(DynamicPlaceholder::Random);
1626                                    }
1627                                    "Counter" => {
1628                                        all_placeholders.insert(DynamicPlaceholder::Counter);
1629                                    }
1630                                    "Date" => {
1631                                        all_placeholders.insert(DynamicPlaceholder::Date);
1632                                    }
1633                                    "VuIter" => {
1634                                        all_placeholders.insert(DynamicPlaceholder::VuIter);
1635                                    }
1636                                    _ => {}
1637                                }
1638                            }
1639                        }
1640                    }
1641                }
1642            }
1643        }
1644
1645        // Get required imports and globals based on placeholders used
1646        let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1647        let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1648
1649        // Build invalid data config if error injection is enabled
1650        let invalid_data_config = self.build_invalid_data_config();
1651        let error_injection_enabled = invalid_data_config.is_some();
1652        let error_rate = self.error_rate.unwrap_or(0.0);
1653        let error_types: Vec<String> = invalid_data_config
1654            .as_ref()
1655            .map(|c| c.error_types.iter().map(|t| format!("{:?}", t)).collect())
1656            .unwrap_or_default();
1657
1658        if error_injection_enabled {
1659            TerminalReporter::print_progress(&format!(
1660                "Error injection enabled ({}% rate)",
1661                (error_rate * 100.0) as u32
1662            ));
1663        }
1664
1665        // Check if security testing is enabled
1666        let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1667
1668        let data = serde_json::json!({
1669            "base_url": self.target,
1670            "flows": flows_data,
1671            "extract_fields": config.default_extract_fields,
1672            "duration_secs": duration_secs,
1673            "max_vus": self.vus,
1674            "auth_header": self.auth,
1675            "custom_headers": custom_headers,
1676            "skip_tls_verify": self.skip_tls_verify,
1677            // Add missing template fields
1678            "stages": stages.iter().map(|s| serde_json::json!({
1679                "duration": s.duration,
1680                "target": s.target,
1681            })).collect::<Vec<_>>(),
1682            "threshold_percentile": self.threshold_percentile,
1683            "threshold_ms": self.threshold_ms,
1684            "max_error_rate": self.max_error_rate,
1685            "headers": headers_json,
1686            "dynamic_imports": required_imports,
1687            "dynamic_globals": required_globals,
1688            // Error injection settings
1689            "error_injection_enabled": error_injection_enabled,
1690            "error_rate": error_rate,
1691            "error_types": error_types,
1692            // Security testing settings
1693            "security_testing_enabled": security_testing_enabled,
1694            "has_custom_headers": !custom_headers.is_empty(),
1695        });
1696
1697        let mut script = handlebars
1698            .render_template(template, &data)
1699            .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1700
1701        // Enhance script with security testing support if enabled
1702        if security_testing_enabled {
1703            script = self.generate_enhanced_script(&script)?;
1704        }
1705
1706        // Validate the generated CRUD flow script
1707        TerminalReporter::print_progress("Validating CRUD flow script...");
1708        let validation_errors = K6ScriptGenerator::validate_script(&script);
1709        if !validation_errors.is_empty() {
1710            TerminalReporter::print_error("CRUD flow script validation failed");
1711            for error in &validation_errors {
1712                eprintln!("  {}", error);
1713            }
1714            return Err(BenchError::Other(format!(
1715                "CRUD flow script validation failed with {} error(s)",
1716                validation_errors.len()
1717            )));
1718        }
1719
1720        TerminalReporter::print_success("CRUD flow script generated");
1721
1722        // Write and execute script
1723        let script_path = if let Some(output) = &self.script_output {
1724            output.clone()
1725        } else {
1726            self.output.join("k6-crud-flow-script.js")
1727        };
1728
1729        if let Some(parent) = script_path.parent() {
1730            std::fs::create_dir_all(parent)?;
1731        }
1732        std::fs::write(&script_path, &script)?;
1733        TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
1734
1735        if self.generate_only {
1736            println!("\nScript generated successfully. Run it with:");
1737            println!("  k6 run {}", script_path.display());
1738            return Ok(());
1739        }
1740
1741        // Execute k6
1742        TerminalReporter::print_progress("Executing CRUD flow test...");
1743        let executor = K6Executor::new()?;
1744        std::fs::create_dir_all(&self.output)?;
1745
1746        let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1747
1748        let duration_secs = Self::parse_duration(&self.duration)?;
1749        TerminalReporter::print_summary(&results, duration_secs);
1750
1751        Ok(())
1752    }
1753
1754    /// Execute OWASP API Security Top 10 testing mode
1755    async fn execute_owasp_test(&self, parser: &SpecParser) -> Result<()> {
1756        TerminalReporter::print_progress("OWASP API Security Top 10 Testing Mode");
1757
1758        // Parse custom headers from CLI
1759        let custom_headers = self.parse_headers()?;
1760
1761        // Build OWASP configuration from CLI options
1762        let mut config = OwaspApiConfig::new()
1763            .with_auth_header(&self.owasp_auth_header)
1764            .with_verbose(self.verbose)
1765            .with_insecure(self.skip_tls_verify)
1766            .with_concurrency(self.vus as usize)
1767            .with_iterations(self.owasp_iterations as usize)
1768            .with_base_path(self.base_path.clone())
1769            .with_custom_headers(custom_headers);
1770
1771        // Set valid auth token if provided
1772        if let Some(ref token) = self.owasp_auth_token {
1773            config = config.with_valid_auth_token(token);
1774        }
1775
1776        // Parse categories if provided
1777        if let Some(ref cats_str) = self.owasp_categories {
1778            let categories: Vec<OwaspCategory> = cats_str
1779                .split(',')
1780                .filter_map(|s| {
1781                    let trimmed = s.trim();
1782                    match trimmed.parse::<OwaspCategory>() {
1783                        Ok(cat) => Some(cat),
1784                        Err(e) => {
1785                            TerminalReporter::print_warning(&e);
1786                            None
1787                        }
1788                    }
1789                })
1790                .collect();
1791
1792            if !categories.is_empty() {
1793                config = config.with_categories(categories);
1794            }
1795        }
1796
1797        // Load admin paths from file if provided
1798        if let Some(ref admin_paths_file) = self.owasp_admin_paths {
1799            config.admin_paths_file = Some(admin_paths_file.clone());
1800            if let Err(e) = config.load_admin_paths() {
1801                TerminalReporter::print_warning(&format!("Failed to load admin paths file: {}", e));
1802            }
1803        }
1804
1805        // Set ID fields if provided
1806        if let Some(ref id_fields_str) = self.owasp_id_fields {
1807            let id_fields: Vec<String> = id_fields_str
1808                .split(',')
1809                .map(|s| s.trim().to_string())
1810                .filter(|s| !s.is_empty())
1811                .collect();
1812            if !id_fields.is_empty() {
1813                config = config.with_id_fields(id_fields);
1814            }
1815        }
1816
1817        // Set report path and format
1818        if let Some(ref report_path) = self.owasp_report {
1819            config = config.with_report_path(report_path);
1820        }
1821        if let Ok(format) = self.owasp_report_format.parse::<ReportFormat>() {
1822            config = config.with_report_format(format);
1823        }
1824
1825        // Print configuration summary
1826        let categories = config.categories_to_test();
1827        TerminalReporter::print_success(&format!(
1828            "Testing {} OWASP categories: {}",
1829            categories.len(),
1830            categories.iter().map(|c| c.cli_name()).collect::<Vec<_>>().join(", ")
1831        ));
1832
1833        if config.valid_auth_token.is_some() {
1834            TerminalReporter::print_progress("Using provided auth token for baseline requests");
1835        }
1836
1837        // Create the OWASP generator
1838        TerminalReporter::print_progress("Generating OWASP security test script...");
1839        let generator = OwaspApiGenerator::new(config, self.target.clone(), parser);
1840
1841        // Generate the script
1842        let script = generator.generate()?;
1843        TerminalReporter::print_success("OWASP security test script generated");
1844
1845        // Write script to file
1846        let script_path = if let Some(output) = &self.script_output {
1847            output.clone()
1848        } else {
1849            self.output.join("k6-owasp-security-test.js")
1850        };
1851
1852        if let Some(parent) = script_path.parent() {
1853            std::fs::create_dir_all(parent)?;
1854        }
1855        std::fs::write(&script_path, &script)?;
1856        TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
1857
1858        // If generate-only mode, exit here
1859        if self.generate_only {
1860            println!("\nOWASP security test script generated. Run it with:");
1861            println!("  k6 run {}", script_path.display());
1862            return Ok(());
1863        }
1864
1865        // Execute k6
1866        TerminalReporter::print_progress("Executing OWASP security tests...");
1867        let executor = K6Executor::new()?;
1868        std::fs::create_dir_all(&self.output)?;
1869
1870        let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1871
1872        let duration_secs = Self::parse_duration(&self.duration)?;
1873        TerminalReporter::print_summary(&results, duration_secs);
1874
1875        println!("\nOWASP security test results saved to: {}", self.output.display());
1876
1877        Ok(())
1878    }
1879}
1880
1881#[cfg(test)]
1882mod tests {
1883    use super::*;
1884
1885    #[test]
1886    fn test_parse_duration() {
1887        assert_eq!(BenchCommand::parse_duration("30s").unwrap(), 30);
1888        assert_eq!(BenchCommand::parse_duration("5m").unwrap(), 300);
1889        assert_eq!(BenchCommand::parse_duration("1h").unwrap(), 3600);
1890        assert_eq!(BenchCommand::parse_duration("60").unwrap(), 60);
1891    }
1892
1893    #[test]
1894    fn test_parse_duration_invalid() {
1895        assert!(BenchCommand::parse_duration("invalid").is_err());
1896        assert!(BenchCommand::parse_duration("30x").is_err());
1897    }
1898
1899    #[test]
1900    fn test_parse_headers() {
1901        let cmd = BenchCommand {
1902            spec: vec![PathBuf::from("test.yaml")],
1903            spec_dir: None,
1904            merge_conflicts: "error".to_string(),
1905            spec_mode: "merge".to_string(),
1906            dependency_config: None,
1907            target: "http://localhost".to_string(),
1908            base_path: None,
1909            duration: "1m".to_string(),
1910            vus: 10,
1911            scenario: "ramp-up".to_string(),
1912            operations: None,
1913            exclude_operations: None,
1914            auth: None,
1915            headers: Some("X-API-Key:test123,X-Client-ID:client456".to_string()),
1916            output: PathBuf::from("output"),
1917            generate_only: false,
1918            script_output: None,
1919            threshold_percentile: "p(95)".to_string(),
1920            threshold_ms: 500,
1921            max_error_rate: 0.05,
1922            verbose: false,
1923            skip_tls_verify: false,
1924            targets_file: None,
1925            max_concurrency: None,
1926            results_format: "both".to_string(),
1927            params_file: None,
1928            crud_flow: false,
1929            flow_config: None,
1930            extract_fields: None,
1931            parallel_create: None,
1932            data_file: None,
1933            data_distribution: "unique-per-vu".to_string(),
1934            data_mappings: None,
1935            per_uri_control: false,
1936            error_rate: None,
1937            error_types: None,
1938            security_test: false,
1939            security_payloads: None,
1940            security_categories: None,
1941            security_target_fields: None,
1942            wafbench_dir: None,
1943            wafbench_cycle_all: false,
1944            owasp_api_top10: false,
1945            owasp_categories: None,
1946            owasp_auth_header: "Authorization".to_string(),
1947            owasp_auth_token: None,
1948            owasp_admin_paths: None,
1949            owasp_id_fields: None,
1950            owasp_report: None,
1951            owasp_report_format: "json".to_string(),
1952            owasp_iterations: 1,
1953        };
1954
1955        let headers = cmd.parse_headers().unwrap();
1956        assert_eq!(headers.get("X-API-Key"), Some(&"test123".to_string()));
1957        assert_eq!(headers.get("X-Client-ID"), Some(&"client456".to_string()));
1958    }
1959
1960    #[test]
1961    fn test_get_spec_display_name() {
1962        let cmd = BenchCommand {
1963            spec: vec![PathBuf::from("test.yaml")],
1964            spec_dir: None,
1965            merge_conflicts: "error".to_string(),
1966            spec_mode: "merge".to_string(),
1967            dependency_config: None,
1968            target: "http://localhost".to_string(),
1969            base_path: None,
1970            duration: "1m".to_string(),
1971            vus: 10,
1972            scenario: "ramp-up".to_string(),
1973            operations: None,
1974            exclude_operations: None,
1975            auth: None,
1976            headers: None,
1977            output: PathBuf::from("output"),
1978            generate_only: false,
1979            script_output: None,
1980            threshold_percentile: "p(95)".to_string(),
1981            threshold_ms: 500,
1982            max_error_rate: 0.05,
1983            verbose: false,
1984            skip_tls_verify: false,
1985            targets_file: None,
1986            max_concurrency: None,
1987            results_format: "both".to_string(),
1988            params_file: None,
1989            crud_flow: false,
1990            flow_config: None,
1991            extract_fields: None,
1992            parallel_create: None,
1993            data_file: None,
1994            data_distribution: "unique-per-vu".to_string(),
1995            data_mappings: None,
1996            per_uri_control: false,
1997            error_rate: None,
1998            error_types: None,
1999            security_test: false,
2000            security_payloads: None,
2001            security_categories: None,
2002            security_target_fields: None,
2003            wafbench_dir: None,
2004            wafbench_cycle_all: false,
2005            owasp_api_top10: false,
2006            owasp_categories: None,
2007            owasp_auth_header: "Authorization".to_string(),
2008            owasp_auth_token: None,
2009            owasp_admin_paths: None,
2010            owasp_id_fields: None,
2011            owasp_report: None,
2012            owasp_report_format: "json".to_string(),
2013            owasp_iterations: 1,
2014        };
2015
2016        assert_eq!(cmd.get_spec_display_name(), "test.yaml");
2017
2018        // Test multiple specs
2019        let cmd_multi = BenchCommand {
2020            spec: vec![PathBuf::from("a.yaml"), PathBuf::from("b.yaml")],
2021            spec_dir: None,
2022            merge_conflicts: "error".to_string(),
2023            spec_mode: "merge".to_string(),
2024            dependency_config: None,
2025            target: "http://localhost".to_string(),
2026            base_path: None,
2027            duration: "1m".to_string(),
2028            vus: 10,
2029            scenario: "ramp-up".to_string(),
2030            operations: None,
2031            exclude_operations: None,
2032            auth: None,
2033            headers: None,
2034            output: PathBuf::from("output"),
2035            generate_only: false,
2036            script_output: None,
2037            threshold_percentile: "p(95)".to_string(),
2038            threshold_ms: 500,
2039            max_error_rate: 0.05,
2040            verbose: false,
2041            skip_tls_verify: false,
2042            targets_file: None,
2043            max_concurrency: None,
2044            results_format: "both".to_string(),
2045            params_file: None,
2046            crud_flow: false,
2047            flow_config: None,
2048            extract_fields: None,
2049            parallel_create: None,
2050            data_file: None,
2051            data_distribution: "unique-per-vu".to_string(),
2052            data_mappings: None,
2053            per_uri_control: false,
2054            error_rate: None,
2055            error_types: None,
2056            security_test: false,
2057            security_payloads: None,
2058            security_categories: None,
2059            security_target_fields: None,
2060            wafbench_dir: None,
2061            wafbench_cycle_all: false,
2062            owasp_api_top10: false,
2063            owasp_categories: None,
2064            owasp_auth_header: "Authorization".to_string(),
2065            owasp_auth_token: None,
2066            owasp_admin_paths: None,
2067            owasp_id_fields: None,
2068            owasp_report: None,
2069            owasp_report_format: "json".to_string(),
2070            owasp_iterations: 1,
2071        };
2072
2073        assert_eq!(cmd_multi.get_spec_display_name(), "2 spec files");
2074    }
2075}