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