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