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