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