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