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