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