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