Skip to main content

mockforge_bench/
command.rs

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