Skip to main content

mockforge_bench/
command.rs

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