Skip to main content

mockforge_bench/
command.rs

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