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            // Combine built-in payloads with WAFBench payloads
869            let mut payload_list: Vec<SecurityPayload> = Vec::new();
870
871            if let Some(ref config) = security_config {
872                payload_list.extend(SecurityPayloads::get_payloads(config));
873            }
874
875            // Add WAFBench payloads
876            if !wafbench_payloads.is_empty() {
877                TerminalReporter::print_progress(&format!(
878                    "Loading {} WAFBench attack patterns...",
879                    wafbench_payloads.len()
880                ));
881                payload_list.extend(wafbench_payloads);
882            }
883
884            let target_fields =
885                security_config.as_ref().map(|c| c.target_fields.clone()).unwrap_or_default();
886
887            additional_code.push_str(&SecurityTestGenerator::generate_payload_selection(
888                &payload_list,
889                self.wafbench_cycle_all,
890            ));
891            additional_code.push('\n');
892            additional_code
893                .push_str(&SecurityTestGenerator::generate_apply_payload(&target_fields));
894            additional_code.push('\n');
895            additional_code.push_str(&SecurityTestGenerator::generate_security_checks());
896            additional_code.push('\n');
897
898            let mode = if self.wafbench_cycle_all {
899                "cycle-all"
900            } else {
901                "random"
902            };
903            TerminalReporter::print_success(&format!(
904                "Security testing enabled ({} payloads, {} mode)",
905                payload_list.len(),
906                mode
907            ));
908        }
909
910        // Add parallel execution code
911        if let Some(config) = self.build_parallel_config() {
912            TerminalReporter::print_progress("Adding parallel execution support...");
913            additional_code.push_str(&ParallelRequestGenerator::generate_batch_helper(&config));
914            additional_code.push('\n');
915            TerminalReporter::print_success(&format!(
916                "Parallel execution enabled (count: {})",
917                config.count
918            ));
919        }
920
921        // Insert additional code after the imports section
922        if !additional_code.is_empty() {
923            // Find the end of the import section
924            if let Some(import_end) = enhanced_script.find("export const options") {
925                enhanced_script.insert_str(
926                    import_end,
927                    &format!("\n// === Advanced Testing Features ===\n{}\n", additional_code),
928                );
929            }
930        }
931
932        Ok(enhanced_script)
933    }
934
935    /// Execute specs sequentially with dependency ordering and value passing
936    async fn execute_sequential_specs(&self) -> Result<()> {
937        TerminalReporter::print_progress("Sequential spec mode: Loading specs individually...");
938
939        // Load all specs (without merging)
940        let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
941
942        if !self.spec.is_empty() {
943            let specs = load_specs_from_files(self.spec.clone())
944                .await
945                .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
946            all_specs.extend(specs);
947        }
948
949        if let Some(spec_dir) = &self.spec_dir {
950            let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
951                BenchError::Other(format!("Failed to load specs from directory: {}", e))
952            })?;
953            all_specs.extend(dir_specs);
954        }
955
956        if all_specs.is_empty() {
957            return Err(BenchError::Other(
958                "No spec files found for sequential execution".to_string(),
959            ));
960        }
961
962        TerminalReporter::print_success(&format!("Loaded {} spec(s)", all_specs.len()));
963
964        // Load dependency config or auto-detect
965        let execution_order = if let Some(config_path) = &self.dependency_config {
966            TerminalReporter::print_progress("Loading dependency configuration...");
967            let config = SpecDependencyConfig::from_file(config_path)?;
968
969            if !config.disable_auto_detect && config.execution_order.is_empty() {
970                // Auto-detect if config doesn't specify order
971                self.detect_and_sort_specs(&all_specs)?
972            } else {
973                // Use configured order
974                config.execution_order.iter().flat_map(|g| g.specs.clone()).collect()
975            }
976        } else {
977            // Auto-detect dependencies
978            self.detect_and_sort_specs(&all_specs)?
979        };
980
981        TerminalReporter::print_success(&format!(
982            "Execution order: {}",
983            execution_order
984                .iter()
985                .map(|p| p.file_name().unwrap_or_default().to_string_lossy().to_string())
986                .collect::<Vec<_>>()
987                .join(" → ")
988        ));
989
990        // Execute each spec in order
991        let mut extracted_values = ExtractedValues::new();
992        let total_specs = execution_order.len();
993
994        for (index, spec_path) in execution_order.iter().enumerate() {
995            let spec_name = spec_path.file_name().unwrap_or_default().to_string_lossy().to_string();
996
997            TerminalReporter::print_progress(&format!(
998                "[{}/{}] Executing spec: {}",
999                index + 1,
1000                total_specs,
1001                spec_name
1002            ));
1003
1004            // Find the spec in our loaded specs (match by full path or filename)
1005            let spec = all_specs
1006                .iter()
1007                .find(|(p, _)| {
1008                    p == spec_path
1009                        || p.file_name() == spec_path.file_name()
1010                        || p.file_name() == Some(spec_path.as_os_str())
1011                })
1012                .map(|(_, s)| s.clone())
1013                .ok_or_else(|| {
1014                    BenchError::Other(format!("Spec not found: {}", spec_path.display()))
1015                })?;
1016
1017            // Execute this spec with any extracted values from previous specs
1018            let new_values = self.execute_single_spec(&spec, &spec_name, &extracted_values).await?;
1019
1020            // Merge extracted values for the next spec
1021            extracted_values.merge(&new_values);
1022
1023            TerminalReporter::print_success(&format!(
1024                "[{}/{}] Completed: {} (extracted {} values)",
1025                index + 1,
1026                total_specs,
1027                spec_name,
1028                new_values.values.len()
1029            ));
1030        }
1031
1032        TerminalReporter::print_success(&format!(
1033            "Sequential execution complete: {} specs executed",
1034            total_specs
1035        ));
1036
1037        Ok(())
1038    }
1039
1040    /// Detect dependencies and return topologically sorted spec paths
1041    fn detect_and_sort_specs(&self, specs: &[(PathBuf, OpenApiSpec)]) -> Result<Vec<PathBuf>> {
1042        TerminalReporter::print_progress("Auto-detecting spec dependencies...");
1043
1044        let mut detector = DependencyDetector::new();
1045        let dependencies = detector.detect_dependencies(specs);
1046
1047        if dependencies.is_empty() {
1048            TerminalReporter::print_progress("No dependencies detected, using file order");
1049            return Ok(specs.iter().map(|(p, _)| p.clone()).collect());
1050        }
1051
1052        TerminalReporter::print_progress(&format!(
1053            "Detected {} cross-spec dependencies",
1054            dependencies.len()
1055        ));
1056
1057        for dep in &dependencies {
1058            TerminalReporter::print_progress(&format!(
1059                "  {} → {} (via field '{}')",
1060                dep.dependency_spec.file_name().unwrap_or_default().to_string_lossy(),
1061                dep.dependent_spec.file_name().unwrap_or_default().to_string_lossy(),
1062                dep.field_name
1063            ));
1064        }
1065
1066        topological_sort(specs, &dependencies)
1067    }
1068
1069    /// Execute a single spec and extract values for dependent specs
1070    async fn execute_single_spec(
1071        &self,
1072        spec: &OpenApiSpec,
1073        spec_name: &str,
1074        _external_values: &ExtractedValues,
1075    ) -> Result<ExtractedValues> {
1076        let parser = SpecParser::from_spec(spec.clone());
1077
1078        // For now, we execute in CRUD flow mode if enabled, otherwise standard mode
1079        if self.crud_flow {
1080            // Execute CRUD flow and extract values
1081            self.execute_crud_flow_with_extraction(&parser, spec_name).await
1082        } else {
1083            // Execute standard benchmark (no value extraction in non-CRUD mode)
1084            self.execute_standard_spec(&parser, spec_name).await?;
1085            Ok(ExtractedValues::new())
1086        }
1087    }
1088
1089    /// Execute CRUD flow with value extraction for sequential mode
1090    async fn execute_crud_flow_with_extraction(
1091        &self,
1092        parser: &SpecParser,
1093        spec_name: &str,
1094    ) -> Result<ExtractedValues> {
1095        let operations = parser.get_operations();
1096        let flows = CrudFlowDetector::detect_flows(&operations);
1097
1098        if flows.is_empty() {
1099            TerminalReporter::print_warning(&format!("No CRUD flows detected in {}", spec_name));
1100            return Ok(ExtractedValues::new());
1101        }
1102
1103        TerminalReporter::print_progress(&format!(
1104            "  {} CRUD flow(s) in {}",
1105            flows.len(),
1106            spec_name
1107        ));
1108
1109        // Generate and execute the CRUD flow script
1110        let mut handlebars = handlebars::Handlebars::new();
1111        // Register json helper for serializing arrays/objects in templates
1112        handlebars.register_helper(
1113            "json",
1114            Box::new(
1115                |h: &handlebars::Helper,
1116                 _: &handlebars::Handlebars,
1117                 _: &handlebars::Context,
1118                 _: &mut handlebars::RenderContext,
1119                 out: &mut dyn handlebars::Output|
1120                 -> handlebars::HelperResult {
1121                    let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1122                    out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1123                    Ok(())
1124                },
1125            ),
1126        );
1127        let template = include_str!("templates/k6_crud_flow.hbs");
1128
1129        let custom_headers = self.parse_headers()?;
1130        let config = self.build_crud_flow_config().unwrap_or_default();
1131
1132        // Load parameter overrides if provided (for body configurations)
1133        let param_overrides = if let Some(params_file) = &self.params_file {
1134            let overrides = ParameterOverrides::from_file(params_file)?;
1135            Some(overrides)
1136        } else {
1137            None
1138        };
1139
1140        // Generate stages from scenario
1141        let duration_secs = Self::parse_duration(&self.duration)?;
1142        let scenario =
1143            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1144        let stages = scenario.generate_stages(duration_secs, self.vus);
1145
1146        // Resolve base path (CLI option takes priority over spec's servers URL)
1147        let api_base_path = self.resolve_base_path(parser);
1148
1149        // Build headers JSON string for the template
1150        let mut all_headers = custom_headers.clone();
1151        if let Some(auth) = &self.auth {
1152            all_headers.insert("Authorization".to_string(), auth.clone());
1153        }
1154        let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1155
1156        // Track all dynamic placeholders across all operations
1157        let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1158
1159        let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1160            let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1161            serde_json::json!({
1162                "name": sanitized_name.clone(),
1163                "display_name": f.name,
1164                "base_path": f.base_path,
1165                "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1166                    // Parse operation to get method and path
1167                    let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1168                    let method_raw = if !parts.is_empty() {
1169                        parts[0].to_uppercase()
1170                    } else {
1171                        "GET".to_string()
1172                    };
1173                    let method = if !parts.is_empty() {
1174                        let m = parts[0].to_lowercase();
1175                        // k6 uses 'del' for DELETE
1176                        if m == "delete" { "del".to_string() } else { m }
1177                    } else {
1178                        "get".to_string()
1179                    };
1180                    let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1181                    // Prepend API base path if configured
1182                    let path = if let Some(ref bp) = api_base_path {
1183                        format!("{}{}", bp, raw_path)
1184                    } else {
1185                        raw_path.to_string()
1186                    };
1187                    let is_get_or_head = method == "get" || method == "head";
1188                    // POST, PUT, PATCH typically have bodies
1189                    let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1190
1191                    // Look up body from params file if available
1192                    let body_value = if has_body {
1193                        param_overrides.as_ref()
1194                            .map(|po| po.get_for_operation(None, &method_raw, &raw_path))
1195                            .and_then(|oo| oo.body)
1196                            .unwrap_or_else(|| serde_json::json!({}))
1197                    } else {
1198                        serde_json::json!({})
1199                    };
1200
1201                    // Process body for dynamic placeholders like ${__VU}, ${__ITER}, etc.
1202                    let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1203
1204                    // Also check for ${extracted.xxx} placeholders which need runtime substitution
1205                    let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1206                    let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1207
1208                    serde_json::json!({
1209                        "operation": s.operation,
1210                        "method": method,
1211                        "path": path,
1212                        "extract": s.extract,
1213                        "use_values": s.use_values,
1214                        "use_body": s.use_body,
1215                        "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1216                        "inject_attacks": s.inject_attacks,
1217                        "attack_types": s.attack_types,
1218                        "description": s.description,
1219                        "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1220                        "is_get_or_head": is_get_or_head,
1221                        "has_body": has_body,
1222                        "body": processed_body.value,
1223                        "body_is_dynamic": body_is_dynamic,
1224                        "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1225                    })
1226                }).collect::<Vec<_>>(),
1227            })
1228        }).collect();
1229
1230        // Collect all placeholders from all steps
1231        for flow_data in &flows_data {
1232            if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1233                for step in steps {
1234                    if let Some(placeholders_arr) =
1235                        step.get("_placeholders").and_then(|p| p.as_array())
1236                    {
1237                        for p_str in placeholders_arr {
1238                            if let Some(p_name) = p_str.as_str() {
1239                                match p_name {
1240                                    "VU" => {
1241                                        all_placeholders.insert(DynamicPlaceholder::VU);
1242                                    }
1243                                    "Iteration" => {
1244                                        all_placeholders.insert(DynamicPlaceholder::Iteration);
1245                                    }
1246                                    "Timestamp" => {
1247                                        all_placeholders.insert(DynamicPlaceholder::Timestamp);
1248                                    }
1249                                    "UUID" => {
1250                                        all_placeholders.insert(DynamicPlaceholder::UUID);
1251                                    }
1252                                    "Random" => {
1253                                        all_placeholders.insert(DynamicPlaceholder::Random);
1254                                    }
1255                                    "Counter" => {
1256                                        all_placeholders.insert(DynamicPlaceholder::Counter);
1257                                    }
1258                                    "Date" => {
1259                                        all_placeholders.insert(DynamicPlaceholder::Date);
1260                                    }
1261                                    "VuIter" => {
1262                                        all_placeholders.insert(DynamicPlaceholder::VuIter);
1263                                    }
1264                                    _ => {}
1265                                }
1266                            }
1267                        }
1268                    }
1269                }
1270            }
1271        }
1272
1273        // Get required imports and globals based on placeholders used
1274        let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1275        let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1276
1277        // Check if security testing is enabled
1278        let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1279
1280        let data = serde_json::json!({
1281            "base_url": self.target,
1282            "flows": flows_data,
1283            "extract_fields": config.default_extract_fields,
1284            "duration_secs": duration_secs,
1285            "max_vus": self.vus,
1286            "auth_header": self.auth,
1287            "custom_headers": custom_headers,
1288            "skip_tls_verify": self.skip_tls_verify,
1289            // Add missing template fields
1290            "stages": stages.iter().map(|s| serde_json::json!({
1291                "duration": s.duration,
1292                "target": s.target,
1293            })).collect::<Vec<_>>(),
1294            "threshold_percentile": self.threshold_percentile,
1295            "threshold_ms": self.threshold_ms,
1296            "max_error_rate": self.max_error_rate,
1297            "headers": headers_json,
1298            "dynamic_imports": required_imports,
1299            "dynamic_globals": required_globals,
1300            // Security testing settings
1301            "security_testing_enabled": security_testing_enabled,
1302        });
1303
1304        let mut script = handlebars
1305            .render_template(template, &data)
1306            .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1307
1308        // Enhance script with security testing support if enabled
1309        if security_testing_enabled {
1310            script = self.generate_enhanced_script(&script)?;
1311        }
1312
1313        // Write and execute script
1314        let script_path =
1315            self.output.join(format!("k6-{}-crud-flow.js", spec_name.replace('.', "_")));
1316
1317        std::fs::create_dir_all(self.output.clone())?;
1318        std::fs::write(&script_path, &script)?;
1319
1320        if !self.generate_only {
1321            let executor = K6Executor::new()?;
1322            let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1323            std::fs::create_dir_all(&output_dir)?;
1324
1325            executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1326        }
1327
1328        // For now, return empty extracted values
1329        // TODO: Parse k6 output to extract actual values
1330        Ok(ExtractedValues::new())
1331    }
1332
1333    /// Execute standard (non-CRUD) spec benchmark
1334    async fn execute_standard_spec(&self, parser: &SpecParser, spec_name: &str) -> Result<()> {
1335        let mut operations = if let Some(filter) = &self.operations {
1336            parser.filter_operations(filter)?
1337        } else {
1338            parser.get_operations()
1339        };
1340
1341        if let Some(exclude) = &self.exclude_operations {
1342            operations = parser.exclude_operations(operations, exclude)?;
1343        }
1344
1345        if operations.is_empty() {
1346            TerminalReporter::print_warning(&format!("No operations found in {}", spec_name));
1347            return Ok(());
1348        }
1349
1350        TerminalReporter::print_progress(&format!(
1351            "  {} operations in {}",
1352            operations.len(),
1353            spec_name
1354        ));
1355
1356        // Generate request templates
1357        let templates: Vec<_> = operations
1358            .iter()
1359            .map(RequestGenerator::generate_template)
1360            .collect::<Result<Vec<_>>>()?;
1361
1362        // Parse headers
1363        let custom_headers = self.parse_headers()?;
1364
1365        // Resolve base path
1366        let base_path = self.resolve_base_path(parser);
1367
1368        // Generate k6 script
1369        let scenario =
1370            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1371
1372        // Check if security testing is enabled
1373        let security_testing_enabled = self.security_test || self.wafbench_dir.is_some();
1374
1375        let k6_config = K6Config {
1376            target_url: self.target.clone(),
1377            base_path,
1378            scenario,
1379            duration_secs: Self::parse_duration(&self.duration)?,
1380            max_vus: self.vus,
1381            threshold_percentile: self.threshold_percentile.clone(),
1382            threshold_ms: self.threshold_ms,
1383            max_error_rate: self.max_error_rate,
1384            auth_header: self.auth.clone(),
1385            custom_headers,
1386            skip_tls_verify: self.skip_tls_verify,
1387            security_testing_enabled,
1388        };
1389
1390        let generator = K6ScriptGenerator::new(k6_config, templates);
1391        let script = generator.generate()?;
1392
1393        // Write and execute script
1394        let script_path = self.output.join(format!("k6-{}.js", spec_name.replace('.', "_")));
1395
1396        std::fs::create_dir_all(self.output.clone())?;
1397        std::fs::write(&script_path, &script)?;
1398
1399        if !self.generate_only {
1400            let executor = K6Executor::new()?;
1401            let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1402            std::fs::create_dir_all(&output_dir)?;
1403
1404            executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1405        }
1406
1407        Ok(())
1408    }
1409
1410    /// Execute CRUD flow testing mode
1411    async fn execute_crud_flow(&self, parser: &SpecParser) -> Result<()> {
1412        // Check if a custom flow config is provided
1413        let config = self.build_crud_flow_config().unwrap_or_default();
1414
1415        // Use flows from config if provided, otherwise auto-detect
1416        let flows = if !config.flows.is_empty() {
1417            TerminalReporter::print_progress("Using custom flow configuration...");
1418            config.flows.clone()
1419        } else {
1420            TerminalReporter::print_progress("Detecting CRUD operations...");
1421            let operations = parser.get_operations();
1422            CrudFlowDetector::detect_flows(&operations)
1423        };
1424
1425        if flows.is_empty() {
1426            return Err(BenchError::Other(
1427                "No CRUD flows detected in spec. Ensure spec has POST/GET/PUT/DELETE operations on related paths.".to_string(),
1428            ));
1429        }
1430
1431        if config.flows.is_empty() {
1432            TerminalReporter::print_success(&format!("Detected {} CRUD flow(s)", flows.len()));
1433        } else {
1434            TerminalReporter::print_success(&format!("Loaded {} custom flow(s)", flows.len()));
1435        }
1436
1437        for flow in &flows {
1438            TerminalReporter::print_progress(&format!(
1439                "  - {}: {} steps",
1440                flow.name,
1441                flow.steps.len()
1442            ));
1443        }
1444
1445        // Generate CRUD flow script
1446        let mut handlebars = handlebars::Handlebars::new();
1447        // Register json helper for serializing arrays/objects in templates
1448        handlebars.register_helper(
1449            "json",
1450            Box::new(
1451                |h: &handlebars::Helper,
1452                 _: &handlebars::Handlebars,
1453                 _: &handlebars::Context,
1454                 _: &mut handlebars::RenderContext,
1455                 out: &mut dyn handlebars::Output|
1456                 -> handlebars::HelperResult {
1457                    let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1458                    out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1459                    Ok(())
1460                },
1461            ),
1462        );
1463        let template = include_str!("templates/k6_crud_flow.hbs");
1464
1465        let custom_headers = self.parse_headers()?;
1466
1467        // Load parameter overrides if provided (for body configurations)
1468        let param_overrides = if let Some(params_file) = &self.params_file {
1469            TerminalReporter::print_progress("Loading parameter overrides...");
1470            let overrides = ParameterOverrides::from_file(params_file)?;
1471            TerminalReporter::print_success(&format!(
1472                "Loaded parameter overrides ({} operation-specific, {} defaults)",
1473                overrides.operations.len(),
1474                if overrides.defaults.is_empty() { 0 } else { 1 }
1475            ));
1476            Some(overrides)
1477        } else {
1478            None
1479        };
1480
1481        // Generate stages from scenario
1482        let duration_secs = Self::parse_duration(&self.duration)?;
1483        let scenario =
1484            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1485        let stages = scenario.generate_stages(duration_secs, self.vus);
1486
1487        // Resolve base path (CLI option takes priority over spec's servers URL)
1488        let api_base_path = self.resolve_base_path(parser);
1489        if let Some(ref bp) = api_base_path {
1490            TerminalReporter::print_progress(&format!("Using base path: {}", bp));
1491        }
1492
1493        // Build headers JSON string for the template
1494        let mut all_headers = custom_headers.clone();
1495        if let Some(auth) = &self.auth {
1496            all_headers.insert("Authorization".to_string(), auth.clone());
1497        }
1498        let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1499
1500        // Track all dynamic placeholders across all operations
1501        let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1502
1503        let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1504            // Sanitize flow name for use as JavaScript variable and k6 metric names
1505            let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1506            serde_json::json!({
1507                "name": sanitized_name.clone(),  // Use sanitized name for variable names
1508                "display_name": f.name,          // Keep original for comments/display
1509                "base_path": f.base_path,
1510                "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1511                    // Parse operation to get method and path
1512                    let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1513                    let method_raw = if !parts.is_empty() {
1514                        parts[0].to_uppercase()
1515                    } else {
1516                        "GET".to_string()
1517                    };
1518                    let method = if !parts.is_empty() {
1519                        let m = parts[0].to_lowercase();
1520                        // k6 uses 'del' for DELETE
1521                        if m == "delete" { "del".to_string() } else { m }
1522                    } else {
1523                        "get".to_string()
1524                    };
1525                    let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1526                    // Prepend API base path if configured
1527                    let path = if let Some(ref bp) = api_base_path {
1528                        format!("{}{}", bp, raw_path)
1529                    } else {
1530                        raw_path.to_string()
1531                    };
1532                    let is_get_or_head = method == "get" || method == "head";
1533                    // POST, PUT, PATCH typically have bodies
1534                    let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1535
1536                    // Look up body from params file if available (use raw_path for matching)
1537                    let body_value = if has_body {
1538                        param_overrides.as_ref()
1539                            .map(|po| po.get_for_operation(None, &method_raw, raw_path))
1540                            .and_then(|oo| oo.body)
1541                            .unwrap_or_else(|| serde_json::json!({}))
1542                    } else {
1543                        serde_json::json!({})
1544                    };
1545
1546                    // Process body for dynamic placeholders like ${__VU}, ${__ITER}, etc.
1547                    let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1548                    // Note: all_placeholders is captured by the closure but we can't mutate it directly
1549                    // We'll collect placeholders separately below
1550
1551                    // Also check for ${extracted.xxx} placeholders which need runtime substitution
1552                    let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1553                    let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1554
1555                    serde_json::json!({
1556                        "operation": s.operation,
1557                        "method": method,
1558                        "path": path,
1559                        "extract": s.extract,
1560                        "use_values": s.use_values,
1561                        "use_body": s.use_body,
1562                        "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1563                        "inject_attacks": s.inject_attacks,
1564                        "attack_types": s.attack_types,
1565                        "description": s.description,
1566                        "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1567                        "is_get_or_head": is_get_or_head,
1568                        "has_body": has_body,
1569                        "body": processed_body.value,
1570                        "body_is_dynamic": body_is_dynamic,
1571                        "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1572                    })
1573                }).collect::<Vec<_>>(),
1574            })
1575        }).collect();
1576
1577        // Collect all placeholders from all steps
1578        for flow_data in &flows_data {
1579            if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1580                for step in steps {
1581                    if let Some(placeholders_arr) =
1582                        step.get("_placeholders").and_then(|p| p.as_array())
1583                    {
1584                        for p_str in placeholders_arr {
1585                            if let Some(p_name) = p_str.as_str() {
1586                                // Parse placeholder from debug string
1587                                match p_name {
1588                                    "VU" => {
1589                                        all_placeholders.insert(DynamicPlaceholder::VU);
1590                                    }
1591                                    "Iteration" => {
1592                                        all_placeholders.insert(DynamicPlaceholder::Iteration);
1593                                    }
1594                                    "Timestamp" => {
1595                                        all_placeholders.insert(DynamicPlaceholder::Timestamp);
1596                                    }
1597                                    "UUID" => {
1598                                        all_placeholders.insert(DynamicPlaceholder::UUID);
1599                                    }
1600                                    "Random" => {
1601                                        all_placeholders.insert(DynamicPlaceholder::Random);
1602                                    }
1603                                    "Counter" => {
1604                                        all_placeholders.insert(DynamicPlaceholder::Counter);
1605                                    }
1606                                    "Date" => {
1607                                        all_placeholders.insert(DynamicPlaceholder::Date);
1608                                    }
1609                                    "VuIter" => {
1610                                        all_placeholders.insert(DynamicPlaceholder::VuIter);
1611                                    }
1612                                    _ => {}
1613                                }
1614                            }
1615                        }
1616                    }
1617                }
1618            }
1619        }
1620
1621        // Get required imports and globals based on placeholders used
1622        let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1623        let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1624
1625        // Build invalid data config if error injection is enabled
1626        let invalid_data_config = self.build_invalid_data_config();
1627        let error_injection_enabled = invalid_data_config.is_some();
1628        let error_rate = self.error_rate.unwrap_or(0.0);
1629        let error_types: Vec<String> = invalid_data_config
1630            .as_ref()
1631            .map(|c| c.error_types.iter().map(|t| format!("{:?}", t)).collect())
1632            .unwrap_or_default();
1633
1634        if error_injection_enabled {
1635            TerminalReporter::print_progress(&format!(
1636                "Error injection enabled ({}% rate)",
1637                (error_rate * 100.0) as u32
1638            ));
1639        }
1640
1641        // Check if security testing is enabled
1642        let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1643
1644        let data = serde_json::json!({
1645            "base_url": self.target,
1646            "flows": flows_data,
1647            "extract_fields": config.default_extract_fields,
1648            "duration_secs": duration_secs,
1649            "max_vus": self.vus,
1650            "auth_header": self.auth,
1651            "custom_headers": custom_headers,
1652            "skip_tls_verify": self.skip_tls_verify,
1653            // Add missing template fields
1654            "stages": stages.iter().map(|s| serde_json::json!({
1655                "duration": s.duration,
1656                "target": s.target,
1657            })).collect::<Vec<_>>(),
1658            "threshold_percentile": self.threshold_percentile,
1659            "threshold_ms": self.threshold_ms,
1660            "max_error_rate": self.max_error_rate,
1661            "headers": headers_json,
1662            "dynamic_imports": required_imports,
1663            "dynamic_globals": required_globals,
1664            // Error injection settings
1665            "error_injection_enabled": error_injection_enabled,
1666            "error_rate": error_rate,
1667            "error_types": error_types,
1668            // Security testing settings
1669            "security_testing_enabled": security_testing_enabled,
1670        });
1671
1672        let mut script = handlebars
1673            .render_template(template, &data)
1674            .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1675
1676        // Enhance script with security testing support if enabled
1677        if security_testing_enabled {
1678            script = self.generate_enhanced_script(&script)?;
1679        }
1680
1681        // Validate the generated CRUD flow script
1682        TerminalReporter::print_progress("Validating CRUD flow script...");
1683        let validation_errors = K6ScriptGenerator::validate_script(&script);
1684        if !validation_errors.is_empty() {
1685            TerminalReporter::print_error("CRUD flow script validation failed");
1686            for error in &validation_errors {
1687                eprintln!("  {}", error);
1688            }
1689            return Err(BenchError::Other(format!(
1690                "CRUD flow script validation failed with {} error(s)",
1691                validation_errors.len()
1692            )));
1693        }
1694
1695        TerminalReporter::print_success("CRUD flow script generated");
1696
1697        // Write and execute script
1698        let script_path = if let Some(output) = &self.script_output {
1699            output.clone()
1700        } else {
1701            self.output.join("k6-crud-flow-script.js")
1702        };
1703
1704        if let Some(parent) = script_path.parent() {
1705            std::fs::create_dir_all(parent)?;
1706        }
1707        std::fs::write(&script_path, &script)?;
1708        TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
1709
1710        if self.generate_only {
1711            println!("\nScript generated successfully. Run it with:");
1712            println!("  k6 run {}", script_path.display());
1713            return Ok(());
1714        }
1715
1716        // Execute k6
1717        TerminalReporter::print_progress("Executing CRUD flow test...");
1718        let executor = K6Executor::new()?;
1719        std::fs::create_dir_all(&self.output)?;
1720
1721        let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1722
1723        let duration_secs = Self::parse_duration(&self.duration)?;
1724        TerminalReporter::print_summary(&results, duration_secs);
1725
1726        Ok(())
1727    }
1728
1729    /// Execute OWASP API Security Top 10 testing mode
1730    async fn execute_owasp_test(&self, parser: &SpecParser) -> Result<()> {
1731        TerminalReporter::print_progress("OWASP API Security Top 10 Testing Mode");
1732
1733        // Parse custom headers from CLI
1734        let custom_headers = self.parse_headers()?;
1735
1736        // Build OWASP configuration from CLI options
1737        let mut config = OwaspApiConfig::new()
1738            .with_auth_header(&self.owasp_auth_header)
1739            .with_verbose(self.verbose)
1740            .with_insecure(self.skip_tls_verify)
1741            .with_concurrency(self.vus as usize)
1742            .with_iterations(self.owasp_iterations as usize)
1743            .with_base_path(self.base_path.clone())
1744            .with_custom_headers(custom_headers);
1745
1746        // Set valid auth token if provided
1747        if let Some(ref token) = self.owasp_auth_token {
1748            config = config.with_valid_auth_token(token);
1749        }
1750
1751        // Parse categories if provided
1752        if let Some(ref cats_str) = self.owasp_categories {
1753            let categories: Vec<OwaspCategory> = cats_str
1754                .split(',')
1755                .filter_map(|s| {
1756                    let trimmed = s.trim();
1757                    match trimmed.parse::<OwaspCategory>() {
1758                        Ok(cat) => Some(cat),
1759                        Err(e) => {
1760                            TerminalReporter::print_warning(&e);
1761                            None
1762                        }
1763                    }
1764                })
1765                .collect();
1766
1767            if !categories.is_empty() {
1768                config = config.with_categories(categories);
1769            }
1770        }
1771
1772        // Load admin paths from file if provided
1773        if let Some(ref admin_paths_file) = self.owasp_admin_paths {
1774            config.admin_paths_file = Some(admin_paths_file.clone());
1775            if let Err(e) = config.load_admin_paths() {
1776                TerminalReporter::print_warning(&format!("Failed to load admin paths file: {}", e));
1777            }
1778        }
1779
1780        // Set ID fields if provided
1781        if let Some(ref id_fields_str) = self.owasp_id_fields {
1782            let id_fields: Vec<String> = id_fields_str
1783                .split(',')
1784                .map(|s| s.trim().to_string())
1785                .filter(|s| !s.is_empty())
1786                .collect();
1787            if !id_fields.is_empty() {
1788                config = config.with_id_fields(id_fields);
1789            }
1790        }
1791
1792        // Set report path and format
1793        if let Some(ref report_path) = self.owasp_report {
1794            config = config.with_report_path(report_path);
1795        }
1796        if let Ok(format) = self.owasp_report_format.parse::<ReportFormat>() {
1797            config = config.with_report_format(format);
1798        }
1799
1800        // Print configuration summary
1801        let categories = config.categories_to_test();
1802        TerminalReporter::print_success(&format!(
1803            "Testing {} OWASP categories: {}",
1804            categories.len(),
1805            categories.iter().map(|c| c.cli_name()).collect::<Vec<_>>().join(", ")
1806        ));
1807
1808        if config.valid_auth_token.is_some() {
1809            TerminalReporter::print_progress("Using provided auth token for baseline requests");
1810        }
1811
1812        // Create the OWASP generator
1813        TerminalReporter::print_progress("Generating OWASP security test script...");
1814        let generator = OwaspApiGenerator::new(config, self.target.clone(), parser);
1815
1816        // Generate the script
1817        let script = generator.generate()?;
1818        TerminalReporter::print_success("OWASP security test script generated");
1819
1820        // Write script to file
1821        let script_path = if let Some(output) = &self.script_output {
1822            output.clone()
1823        } else {
1824            self.output.join("k6-owasp-security-test.js")
1825        };
1826
1827        if let Some(parent) = script_path.parent() {
1828            std::fs::create_dir_all(parent)?;
1829        }
1830        std::fs::write(&script_path, &script)?;
1831        TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
1832
1833        // If generate-only mode, exit here
1834        if self.generate_only {
1835            println!("\nOWASP security test script generated. Run it with:");
1836            println!("  k6 run {}", script_path.display());
1837            return Ok(());
1838        }
1839
1840        // Execute k6
1841        TerminalReporter::print_progress("Executing OWASP security tests...");
1842        let executor = K6Executor::new()?;
1843        std::fs::create_dir_all(&self.output)?;
1844
1845        let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1846
1847        let duration_secs = Self::parse_duration(&self.duration)?;
1848        TerminalReporter::print_summary(&results, duration_secs);
1849
1850        println!("\nOWASP security test results saved to: {}", self.output.display());
1851
1852        Ok(())
1853    }
1854}
1855
1856#[cfg(test)]
1857mod tests {
1858    use super::*;
1859
1860    #[test]
1861    fn test_parse_duration() {
1862        assert_eq!(BenchCommand::parse_duration("30s").unwrap(), 30);
1863        assert_eq!(BenchCommand::parse_duration("5m").unwrap(), 300);
1864        assert_eq!(BenchCommand::parse_duration("1h").unwrap(), 3600);
1865        assert_eq!(BenchCommand::parse_duration("60").unwrap(), 60);
1866    }
1867
1868    #[test]
1869    fn test_parse_duration_invalid() {
1870        assert!(BenchCommand::parse_duration("invalid").is_err());
1871        assert!(BenchCommand::parse_duration("30x").is_err());
1872    }
1873
1874    #[test]
1875    fn test_parse_headers() {
1876        let cmd = BenchCommand {
1877            spec: vec![PathBuf::from("test.yaml")],
1878            spec_dir: None,
1879            merge_conflicts: "error".to_string(),
1880            spec_mode: "merge".to_string(),
1881            dependency_config: None,
1882            target: "http://localhost".to_string(),
1883            base_path: None,
1884            duration: "1m".to_string(),
1885            vus: 10,
1886            scenario: "ramp-up".to_string(),
1887            operations: None,
1888            exclude_operations: None,
1889            auth: None,
1890            headers: Some("X-API-Key:test123,X-Client-ID:client456".to_string()),
1891            output: PathBuf::from("output"),
1892            generate_only: false,
1893            script_output: None,
1894            threshold_percentile: "p(95)".to_string(),
1895            threshold_ms: 500,
1896            max_error_rate: 0.05,
1897            verbose: false,
1898            skip_tls_verify: false,
1899            targets_file: None,
1900            max_concurrency: None,
1901            results_format: "both".to_string(),
1902            params_file: None,
1903            crud_flow: false,
1904            flow_config: None,
1905            extract_fields: None,
1906            parallel_create: None,
1907            data_file: None,
1908            data_distribution: "unique-per-vu".to_string(),
1909            data_mappings: None,
1910            per_uri_control: false,
1911            error_rate: None,
1912            error_types: None,
1913            security_test: false,
1914            security_payloads: None,
1915            security_categories: None,
1916            security_target_fields: None,
1917            wafbench_dir: None,
1918            wafbench_cycle_all: false,
1919            owasp_api_top10: false,
1920            owasp_categories: None,
1921            owasp_auth_header: "Authorization".to_string(),
1922            owasp_auth_token: None,
1923            owasp_admin_paths: None,
1924            owasp_id_fields: None,
1925            owasp_report: None,
1926            owasp_report_format: "json".to_string(),
1927            owasp_iterations: 1,
1928        };
1929
1930        let headers = cmd.parse_headers().unwrap();
1931        assert_eq!(headers.get("X-API-Key"), Some(&"test123".to_string()));
1932        assert_eq!(headers.get("X-Client-ID"), Some(&"client456".to_string()));
1933    }
1934
1935    #[test]
1936    fn test_get_spec_display_name() {
1937        let cmd = BenchCommand {
1938            spec: vec![PathBuf::from("test.yaml")],
1939            spec_dir: None,
1940            merge_conflicts: "error".to_string(),
1941            spec_mode: "merge".to_string(),
1942            dependency_config: None,
1943            target: "http://localhost".to_string(),
1944            base_path: None,
1945            duration: "1m".to_string(),
1946            vus: 10,
1947            scenario: "ramp-up".to_string(),
1948            operations: None,
1949            exclude_operations: None,
1950            auth: None,
1951            headers: None,
1952            output: PathBuf::from("output"),
1953            generate_only: false,
1954            script_output: None,
1955            threshold_percentile: "p(95)".to_string(),
1956            threshold_ms: 500,
1957            max_error_rate: 0.05,
1958            verbose: false,
1959            skip_tls_verify: false,
1960            targets_file: None,
1961            max_concurrency: None,
1962            results_format: "both".to_string(),
1963            params_file: None,
1964            crud_flow: false,
1965            flow_config: None,
1966            extract_fields: None,
1967            parallel_create: None,
1968            data_file: None,
1969            data_distribution: "unique-per-vu".to_string(),
1970            data_mappings: None,
1971            per_uri_control: false,
1972            error_rate: None,
1973            error_types: None,
1974            security_test: false,
1975            security_payloads: None,
1976            security_categories: None,
1977            security_target_fields: None,
1978            wafbench_dir: None,
1979            wafbench_cycle_all: false,
1980            owasp_api_top10: false,
1981            owasp_categories: None,
1982            owasp_auth_header: "Authorization".to_string(),
1983            owasp_auth_token: None,
1984            owasp_admin_paths: None,
1985            owasp_id_fields: None,
1986            owasp_report: None,
1987            owasp_report_format: "json".to_string(),
1988            owasp_iterations: 1,
1989        };
1990
1991        assert_eq!(cmd.get_spec_display_name(), "test.yaml");
1992
1993        // Test multiple specs
1994        let cmd_multi = BenchCommand {
1995            spec: vec![PathBuf::from("a.yaml"), PathBuf::from("b.yaml")],
1996            spec_dir: None,
1997            merge_conflicts: "error".to_string(),
1998            spec_mode: "merge".to_string(),
1999            dependency_config: None,
2000            target: "http://localhost".to_string(),
2001            base_path: None,
2002            duration: "1m".to_string(),
2003            vus: 10,
2004            scenario: "ramp-up".to_string(),
2005            operations: None,
2006            exclude_operations: None,
2007            auth: None,
2008            headers: None,
2009            output: PathBuf::from("output"),
2010            generate_only: false,
2011            script_output: None,
2012            threshold_percentile: "p(95)".to_string(),
2013            threshold_ms: 500,
2014            max_error_rate: 0.05,
2015            verbose: false,
2016            skip_tls_verify: false,
2017            targets_file: None,
2018            max_concurrency: None,
2019            results_format: "both".to_string(),
2020            params_file: None,
2021            crud_flow: false,
2022            flow_config: None,
2023            extract_fields: None,
2024            parallel_create: None,
2025            data_file: None,
2026            data_distribution: "unique-per-vu".to_string(),
2027            data_mappings: None,
2028            per_uri_control: false,
2029            error_rate: None,
2030            error_types: None,
2031            security_test: false,
2032            security_payloads: None,
2033            security_categories: None,
2034            security_target_fields: None,
2035            wafbench_dir: None,
2036            wafbench_cycle_all: false,
2037            owasp_api_top10: false,
2038            owasp_categories: None,
2039            owasp_auth_header: "Authorization".to_string(),
2040            owasp_auth_token: None,
2041            owasp_admin_paths: None,
2042            owasp_id_fields: None,
2043            owasp_report: None,
2044            owasp_report_format: "json".to_string(),
2045            owasp_iterations: 1,
2046        };
2047
2048        assert_eq!(cmd_multi.get_spec_display_name(), "2 spec files");
2049    }
2050}