1use crate::dynamic_params::{DynamicParamProcessor, DynamicPlaceholder};
4use crate::error::{BenchError, Result};
5use crate::request_gen::RequestTemplate;
6use crate::scenarios::LoadScenario;
7use handlebars::Handlebars;
8use serde::Serialize;
9#[cfg(test)]
10use serde_json::json;
11use serde_json::Value;
12use std::collections::{HashMap, HashSet};
13
14#[derive(Debug, Clone, Serialize)]
20pub struct K6ScriptTemplateData {
21 pub base_url: String,
22 pub stages: Vec<K6StageData>,
23 pub operations: Vec<K6OperationData>,
24 pub threshold_percentile: String,
25 pub threshold_ms: u64,
26 pub max_error_rate: f64,
27 pub scenario_name: String,
28 pub skip_tls_verify: bool,
29 pub has_dynamic_values: bool,
30 pub dynamic_imports: Vec<String>,
31 pub dynamic_globals: Vec<String>,
32 pub security_testing_enabled: bool,
33 pub has_custom_headers: bool,
34 pub chunked_request_bodies: bool,
42}
43
44#[derive(Debug, Clone, Serialize)]
46pub struct K6CrudFlowTemplateData {
47 pub base_url: String,
48 pub flows: Vec<Value>,
49 pub extract_fields: Vec<String>,
50 pub duration_secs: u64,
51 pub max_vus: u32,
52 pub auth_header: Option<String>,
53 pub custom_headers: HashMap<String, String>,
54 pub skip_tls_verify: bool,
55 pub stages: Vec<K6StageData>,
56 pub threshold_percentile: String,
57 pub threshold_ms: u64,
58 pub max_error_rate: f64,
59 pub headers: String,
61 pub dynamic_imports: Vec<String>,
62 pub dynamic_globals: Vec<String>,
63 pub extracted_values_output_path: String,
64 pub error_injection_enabled: bool,
65 pub error_rate: f64,
66 pub error_types: Vec<String>,
67 pub security_testing_enabled: bool,
68 pub has_custom_headers: bool,
69}
70
71#[derive(Debug, Clone, Serialize)]
73pub struct K6StageData {
74 pub duration: String,
75 pub target: u32,
76}
77
78#[derive(Debug, Clone, Serialize)]
80pub struct K6OperationData {
81 pub index: usize,
82 pub name: String,
83 pub metric_name: String,
84 pub display_name: String,
85 pub method: String,
86 pub path: Value,
87 pub path_is_dynamic: bool,
88 pub headers: Value,
89 pub body: Option<Value>,
90 pub body_is_dynamic: bool,
91 pub has_body: bool,
92 pub is_get_or_head: bool,
93}
94
95pub struct K6Config {
97 pub target_url: String,
98 pub base_path: Option<String>,
101 pub scenario: LoadScenario,
102 pub duration_secs: u64,
103 pub max_vus: u32,
104 pub threshold_percentile: String,
105 pub threshold_ms: u64,
106 pub max_error_rate: f64,
107 pub auth_header: Option<String>,
108 pub custom_headers: HashMap<String, String>,
109 pub skip_tls_verify: bool,
110 pub security_testing_enabled: bool,
111 pub chunked_request_bodies: bool,
114}
115
116pub struct K6ScriptGenerator {
118 config: K6Config,
119 templates: Vec<RequestTemplate>,
120}
121
122impl K6ScriptGenerator {
123 pub fn new(config: K6Config, templates: Vec<RequestTemplate>) -> Self {
125 Self { config, templates }
126 }
127
128 pub fn generate(&self) -> Result<String> {
130 let handlebars = Handlebars::new();
131
132 let template = include_str!("templates/k6_script.hbs");
133
134 let data = self.build_template_data()?;
135
136 let value = serde_json::to_value(&data)
137 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
138
139 handlebars
140 .render_template(template, &value)
141 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))
142 }
143
144 const K6_METRIC_NAME_BASE_MAX_LEN: usize = 112;
150
151 pub fn sanitize_k6_metric_name(name: &str) -> String {
164 let sanitized = Self::sanitize_js_identifier(name);
165 if sanitized.len() <= Self::K6_METRIC_NAME_BASE_MAX_LEN {
166 return sanitized;
167 }
168
169 use std::collections::hash_map::DefaultHasher;
170 use std::hash::{Hash, Hasher};
171 let mut hasher = DefaultHasher::new();
172 name.hash(&mut hasher);
176 let hash_suffix = format!("{:08x}", hasher.finish() as u32);
177
178 let prefix_len = Self::K6_METRIC_NAME_BASE_MAX_LEN - 9;
180 let prefix = &sanitized[..prefix_len];
181 let prefix = prefix.trim_end_matches('_');
183 format!("{}_{}", prefix, hash_suffix)
184 }
185
186 pub fn sanitize_js_identifier(name: &str) -> String {
196 let mut result = String::new();
197 let mut chars = name.chars().peekable();
198
199 if let Some(&first) = chars.peek() {
201 if first.is_ascii_digit() {
202 result.push('_');
203 }
204 }
205
206 for ch in chars {
207 if ch.is_ascii_alphanumeric() || ch == '_' {
208 result.push(ch);
209 } else {
210 if !result.ends_with('_') {
213 result.push('_');
214 }
215 }
216 }
217
218 result = result.trim_end_matches('_').to_string();
220
221 if result.is_empty() {
223 result = "operation".to_string();
224 }
225
226 result
227 }
228
229 fn build_template_data(&self) -> Result<K6ScriptTemplateData> {
231 let stages = self
232 .config
233 .scenario
234 .generate_stages(self.config.duration_secs, self.config.max_vus);
235
236 let base_path = self.config.base_path.as_deref().unwrap_or("");
238
239 let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
241
242 let operations = self
243 .templates
244 .iter()
245 .enumerate()
246 .map(|(idx, template)| {
247 let display_name = template.operation.display_name();
248 let sanitized_name = Self::sanitize_js_identifier(&display_name);
249 let metric_name = Self::sanitize_k6_metric_name(&display_name);
255 let k6_method = match template.operation.method.to_lowercase().as_str() {
257 "delete" => "del".to_string(),
258 m => m.to_string(),
259 };
260 let is_get_or_head = matches!(k6_method.as_str(), "get" | "head");
263
264 let raw_path = template.generate_path();
267 let full_path = if base_path.is_empty() {
268 raw_path
269 } else {
270 format!("{}{}", base_path, raw_path)
271 };
272 let processed_path = DynamicParamProcessor::process_path(&full_path);
273 all_placeholders.extend(processed_path.placeholders.clone());
274
275 let (body_value, body_is_dynamic) = if let Some(body) = &template.body {
277 let processed_body = DynamicParamProcessor::process_json_body(body);
278 all_placeholders.extend(processed_body.placeholders.clone());
279 (Some(processed_body.value), processed_body.is_dynamic)
280 } else {
281 (None, false)
282 };
283
284 let path_value = if processed_path.is_dynamic {
285 processed_path.value
286 } else {
287 full_path
288 };
289
290 K6OperationData {
291 index: idx,
292 name: sanitized_name,
293 metric_name,
294 display_name,
295 method: k6_method,
296 path: Value::String(path_value),
297 path_is_dynamic: processed_path.is_dynamic,
298 headers: Value::String(self.build_headers_json(template)),
299 body: body_value.map(Value::String),
300 body_is_dynamic,
301 has_body: template.body.is_some(),
302 is_get_or_head,
303 }
304 })
305 .collect::<Vec<_>>();
306
307 let required_imports: Vec<String> =
309 DynamicParamProcessor::get_required_imports(&all_placeholders)
310 .into_iter()
311 .map(String::from)
312 .collect();
313 let required_globals: Vec<String> =
314 DynamicParamProcessor::get_required_globals(&all_placeholders)
315 .into_iter()
316 .map(String::from)
317 .collect();
318 let has_dynamic_values = !all_placeholders.is_empty();
319
320 Ok(K6ScriptTemplateData {
321 base_url: self.config.target_url.clone(),
322 stages: stages
323 .iter()
324 .map(|s| K6StageData {
325 duration: s.duration.clone(),
326 target: s.target,
327 })
328 .collect(),
329 operations,
330 threshold_percentile: self.config.threshold_percentile.clone(),
331 threshold_ms: self.config.threshold_ms,
332 max_error_rate: self.config.max_error_rate,
333 scenario_name: format!("{:?}", self.config.scenario).to_lowercase(),
334 skip_tls_verify: self.config.skip_tls_verify,
335 has_dynamic_values,
336 dynamic_imports: required_imports,
337 dynamic_globals: required_globals,
338 security_testing_enabled: self.config.security_testing_enabled,
339 has_custom_headers: !self.config.custom_headers.is_empty(),
340 chunked_request_bodies: self.config.chunked_request_bodies,
341 })
342 }
343
344 fn build_headers_json(&self, template: &RequestTemplate) -> String {
346 let mut headers = template.get_headers();
347
348 if let Some(auth) = &self.config.auth_header {
350 headers.insert("Authorization".to_string(), auth.clone());
351 }
352
353 for (key, value) in &self.config.custom_headers {
355 headers.insert(key.clone(), value.clone());
356 }
357
358 if self.config.chunked_request_bodies && template.body.is_some() {
363 headers.insert("Transfer-Encoding".to_string(), "chunked".to_string());
364 }
365
366 serde_json::to_string(&headers).unwrap_or_else(|_| "{}".to_string())
368 }
369
370 pub fn validate_script(script: &str) -> Vec<String> {
379 let mut errors = Vec::new();
380
381 if !script.contains("import http from 'k6/http'") {
383 errors.push("Missing required import: 'k6/http'".to_string());
384 }
385 if !script.contains("import { check") && !script.contains("import {check") {
386 errors.push("Missing required import: 'check' from 'k6'".to_string());
387 }
388 if !script.contains("import { Rate, Trend") && !script.contains("import {Rate, Trend") {
389 errors.push("Missing required import: 'Rate, Trend' from 'k6/metrics'".to_string());
390 }
391
392 let lines: Vec<&str> = script.lines().collect();
396 for (line_num, line) in lines.iter().enumerate() {
397 let trimmed = line.trim();
398
399 if trimmed.contains("new Trend(") || trimmed.contains("new Rate(") {
401 if let Some(start) = trimmed.find('\'') {
404 if let Some(end) = trimmed[start + 1..].find('\'') {
405 let metric_name = &trimmed[start + 1..start + 1 + end];
406 if !Self::is_valid_k6_metric_name(metric_name) {
407 errors.push(format!(
408 "Line {}: Invalid k6 metric name '{}'. Metric names must only contain ASCII letters, numbers, or underscores and start with a letter or underscore.",
409 line_num + 1,
410 metric_name
411 ));
412 }
413 }
414 } else if let Some(start) = trimmed.find('"') {
415 if let Some(end) = trimmed[start + 1..].find('"') {
416 let metric_name = &trimmed[start + 1..start + 1 + end];
417 if !Self::is_valid_k6_metric_name(metric_name) {
418 errors.push(format!(
419 "Line {}: Invalid k6 metric name '{}'. Metric names must only contain ASCII letters, numbers, or underscores and start with a letter or underscore.",
420 line_num + 1,
421 metric_name
422 ));
423 }
424 }
425 }
426 }
427
428 if trimmed.starts_with("const ") || trimmed.starts_with("let ") {
430 if let Some(equals_pos) = trimmed.find('=') {
431 let var_decl = &trimmed[..equals_pos];
432 if var_decl.contains('.')
435 && !var_decl.contains("'")
436 && !var_decl.contains("\"")
437 && !var_decl.trim().starts_with("//")
438 {
439 errors.push(format!(
440 "Line {}: Invalid JavaScript variable name with dot: {}. Variable names cannot contain dots.",
441 line_num + 1,
442 var_decl.trim()
443 ));
444 }
445 }
446 }
447 }
448
449 errors
450 }
451
452 fn is_valid_k6_metric_name(name: &str) -> bool {
459 if name.is_empty() || name.len() > 128 {
460 return false;
461 }
462
463 let mut chars = name.chars();
464
465 if let Some(first) = chars.next() {
467 if !first.is_ascii_alphabetic() && first != '_' {
468 return false;
469 }
470 }
471
472 for ch in chars {
474 if !ch.is_ascii_alphanumeric() && ch != '_' {
475 return false;
476 }
477 }
478
479 true
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 #[test]
488 fn test_k6_config_creation() {
489 let config = K6Config {
490 target_url: "https://api.example.com".to_string(),
491 base_path: None,
492 scenario: LoadScenario::RampUp,
493 duration_secs: 60,
494 max_vus: 10,
495 threshold_percentile: "p(95)".to_string(),
496 threshold_ms: 500,
497 max_error_rate: 0.05,
498 auth_header: None,
499 custom_headers: HashMap::new(),
500 skip_tls_verify: false,
501 security_testing_enabled: false,
502 chunked_request_bodies: false,
503 };
504
505 assert_eq!(config.duration_secs, 60);
506 assert_eq!(config.max_vus, 10);
507 }
508
509 #[test]
510 fn test_script_generator_creation() {
511 let config = K6Config {
512 target_url: "https://api.example.com".to_string(),
513 base_path: None,
514 scenario: LoadScenario::Constant,
515 duration_secs: 30,
516 max_vus: 5,
517 threshold_percentile: "p(95)".to_string(),
518 threshold_ms: 500,
519 max_error_rate: 0.05,
520 auth_header: None,
521 custom_headers: HashMap::new(),
522 skip_tls_verify: false,
523 security_testing_enabled: false,
524 chunked_request_bodies: false,
525 };
526
527 let templates = vec![];
528 let generator = K6ScriptGenerator::new(config, templates);
529
530 assert_eq!(generator.templates.len(), 0);
531 }
532
533 #[test]
534 fn test_sanitize_js_identifier() {
535 assert_eq!(
537 K6ScriptGenerator::sanitize_js_identifier("billing.subscriptions.v1"),
538 "billing_subscriptions_v1"
539 );
540
541 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("get user"), "get_user");
543
544 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("123invalid"), "_123invalid");
546
547 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("getUsers"), "getUsers");
549
550 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("test...name"), "test_name");
552
553 assert_eq!(K6ScriptGenerator::sanitize_js_identifier(""), "operation");
555
556 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("test@name#value"), "test_name_value");
558
559 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("plans.list"), "plans_list");
561 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("plans.create"), "plans_create");
562 assert_eq!(
563 K6ScriptGenerator::sanitize_js_identifier("plans.update-pricing-schemes"),
564 "plans_update_pricing_schemes"
565 );
566 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("users CRUD"), "users_CRUD");
567 }
568
569 #[test]
570 fn test_sanitize_k6_metric_name_short_passthrough() {
571 let short = "billing_subscriptions_list";
573 let out = K6ScriptGenerator::sanitize_k6_metric_name(short);
574 assert_eq!(out, short);
575 assert!(K6ScriptGenerator::is_valid_k6_metric_name(&format!("{out}_latency")));
576 }
577
578 #[test]
579 fn test_sanitize_k6_metric_name_truncates_long_microsoft_graph_id() {
580 let long = "drives.drive.items.driveItem.workbook.worksheets.workbookWorksheet.\
584 charts.workbookChart.axes.categoryAxis.format.line.clear";
585 let metric = K6ScriptGenerator::sanitize_k6_metric_name(long);
586
587 assert!(
589 metric.len() <= K6ScriptGenerator::K6_METRIC_NAME_BASE_MAX_LEN,
590 "metric base len {} exceeded cap {}",
591 metric.len(),
592 K6ScriptGenerator::K6_METRIC_NAME_BASE_MAX_LEN
593 );
594
595 assert!(K6ScriptGenerator::is_valid_k6_metric_name(&metric));
597 assert!(K6ScriptGenerator::is_valid_k6_metric_name(&format!("{metric}_latency")));
598 assert!(K6ScriptGenerator::is_valid_k6_metric_name(&format!("{metric}_errors")));
599 assert!(K6ScriptGenerator::is_valid_k6_metric_name(&format!("{metric}_step99_latency")));
601 }
602
603 #[test]
604 fn test_sanitize_k6_metric_name_distinct_long_names_get_distinct_metrics() {
605 let prefix = "a".repeat(150);
608 let a = format!("{prefix}.foo");
609 let b = format!("{prefix}.bar");
610 let ma = K6ScriptGenerator::sanitize_k6_metric_name(&a);
611 let mb = K6ScriptGenerator::sanitize_k6_metric_name(&b);
612 assert_ne!(ma, mb, "distinct long names produced the same metric name");
613 }
614
615 #[test]
616 fn test_sanitize_k6_metric_name_truncated_starts_with_letter() {
617 let long = format!("{}123end", "x".repeat(120));
619 let metric = K6ScriptGenerator::sanitize_k6_metric_name(&long);
620 assert!(K6ScriptGenerator::is_valid_k6_metric_name(&metric));
621 }
622
623 #[test]
624 fn test_microsoft_graph_long_operation_id_passes_validation() {
625 use crate::spec_parser::ApiOperation;
628 use openapiv3::Operation;
629
630 let long_op_id = "drives.drive.items.driveItem.workbook.worksheets.\
631 workbookWorksheet.charts.workbookChart.axes.categoryAxis.format.\
632 line.clear";
633
634 let operation = ApiOperation {
635 method: "post".to_string(),
636 path: "/drives/{drive-id}/items/{item-id}/workbook/worksheets/{worksheet-id}/charts/{chart-id}/axes/categoryAxis/format/line/clear".to_string(),
637 operation: Operation::default(),
638 operation_id: Some(long_op_id.to_string()),
639 };
640 let template = RequestTemplate {
641 operation,
642 path_params: HashMap::new(),
643 query_params: HashMap::new(),
644 headers: HashMap::new(),
645 body: None,
646 };
647 let config = K6Config {
648 target_url: "https://api.example.com".to_string(),
649 base_path: Some("/v1.0".to_string()),
650 scenario: LoadScenario::Constant,
651 duration_secs: 30,
652 max_vus: 5,
653 threshold_percentile: "p(95)".to_string(),
654 threshold_ms: 500,
655 max_error_rate: 0.05,
656 auth_header: None,
657 custom_headers: HashMap::new(),
658 skip_tls_verify: false,
659 security_testing_enabled: false,
660 chunked_request_bodies: false,
661 };
662 let generator = K6ScriptGenerator::new(config, vec![template]);
663 let script = generator.generate().expect("script generates");
664
665 let errors = K6ScriptGenerator::validate_script(&script);
666 assert!(
667 errors.is_empty(),
668 "validate_script returned errors for long operationId: {errors:#?}"
669 );
670 }
671
672 #[test]
673 fn test_script_generation_with_dots_in_name() {
674 use crate::spec_parser::ApiOperation;
675 use openapiv3::Operation;
676
677 let operation = ApiOperation {
679 method: "get".to_string(),
680 path: "/billing/subscriptions".to_string(),
681 operation: Operation::default(),
682 operation_id: Some("billing.subscriptions.v1".to_string()),
683 };
684
685 let template = RequestTemplate {
686 operation,
687 path_params: HashMap::new(),
688 query_params: HashMap::new(),
689 headers: HashMap::new(),
690 body: None,
691 };
692
693 let config = K6Config {
694 target_url: "https://api.example.com".to_string(),
695 base_path: None,
696 scenario: LoadScenario::Constant,
697 duration_secs: 30,
698 max_vus: 5,
699 threshold_percentile: "p(95)".to_string(),
700 threshold_ms: 500,
701 max_error_rate: 0.05,
702 auth_header: None,
703 custom_headers: HashMap::new(),
704 skip_tls_verify: false,
705 security_testing_enabled: false,
706 chunked_request_bodies: false,
707 };
708
709 let generator = K6ScriptGenerator::new(config, vec![template]);
710 let script = generator.generate().expect("Should generate script");
711
712 assert!(
714 script.contains("const billing_subscriptions_v1_latency"),
715 "Script should contain sanitized variable name for latency"
716 );
717 assert!(
718 script.contains("const billing_subscriptions_v1_errors"),
719 "Script should contain sanitized variable name for errors"
720 );
721
722 assert!(
725 !script.contains("const billing.subscriptions"),
726 "Script should not contain variable names with dots - this would cause 'Unexpected token .' error"
727 );
728
729 assert!(
732 script.contains("'billing_subscriptions_v1_latency'"),
733 "Metric name strings should be sanitized (no dots) - k6 validation requires valid metric names"
734 );
735 assert!(
736 script.contains("'billing_subscriptions_v1_errors'"),
737 "Metric name strings should be sanitized (no dots) - k6 validation requires valid metric names"
738 );
739
740 assert!(
742 script.contains("billing.subscriptions.v1"),
743 "Script should contain original name in comments/strings for readability"
744 );
745
746 assert!(
748 script.contains("billing_subscriptions_v1_latency.add"),
749 "Variable usage should use sanitized name"
750 );
751 assert!(
752 script.contains("billing_subscriptions_v1_errors.add"),
753 "Variable usage should use sanitized name"
754 );
755 }
756
757 #[test]
758 fn test_validate_script_valid() {
759 let valid_script = r#"
760import http from 'k6/http';
761import { check, sleep } from 'k6';
762import { Rate, Trend } from 'k6/metrics';
763
764const test_latency = new Trend('test_latency');
765const test_errors = new Rate('test_errors');
766
767export default function() {
768 const res = http.get('https://example.com');
769 test_latency.add(res.timings.duration);
770 test_errors.add(res.status !== 200);
771}
772"#;
773
774 let errors = K6ScriptGenerator::validate_script(valid_script);
775 assert!(errors.is_empty(), "Valid script should have no validation errors");
776 }
777
778 #[test]
779 fn test_validate_script_invalid_metric_name() {
780 let invalid_script = r#"
781import http from 'k6/http';
782import { check, sleep } from 'k6';
783import { Rate, Trend } from 'k6/metrics';
784
785const test_latency = new Trend('test.latency');
786const test_errors = new Rate('test_errors');
787
788export default function() {
789 const res = http.get('https://example.com');
790 test_latency.add(res.timings.duration);
791}
792"#;
793
794 let errors = K6ScriptGenerator::validate_script(invalid_script);
795 assert!(
796 !errors.is_empty(),
797 "Script with invalid metric name should have validation errors"
798 );
799 assert!(
800 errors.iter().any(|e| e.contains("Invalid k6 metric name")),
801 "Should detect invalid metric name with dot"
802 );
803 }
804
805 #[test]
806 fn test_validate_script_missing_imports() {
807 let invalid_script = r#"
808const test_latency = new Trend('test_latency');
809export default function() {}
810"#;
811
812 let errors = K6ScriptGenerator::validate_script(invalid_script);
813 assert!(!errors.is_empty(), "Script missing imports should have validation errors");
814 }
815
816 #[test]
817 fn test_validate_script_metric_name_validation() {
818 let valid_script = r#"
821import http from 'k6/http';
822import { check, sleep } from 'k6';
823import { Rate, Trend } from 'k6/metrics';
824const test_latency = new Trend('test_latency');
825const test_errors = new Rate('test_errors');
826export default function() {}
827"#;
828 let errors = K6ScriptGenerator::validate_script(valid_script);
829 assert!(errors.is_empty(), "Valid metric names should pass validation");
830
831 let invalid_cases = vec![
833 ("test.latency", "dot in metric name"),
834 ("123test", "starts with number"),
835 ("test-latency", "hyphen in metric name"),
836 ("test@latency", "special character"),
837 ];
838
839 for (invalid_name, description) in invalid_cases {
840 let script = format!(
841 r#"
842import http from 'k6/http';
843import {{ check, sleep }} from 'k6';
844import {{ Rate, Trend }} from 'k6/metrics';
845const test_latency = new Trend('{}');
846export default function() {{}}
847"#,
848 invalid_name
849 );
850 let errors = K6ScriptGenerator::validate_script(&script);
851 assert!(
852 !errors.is_empty(),
853 "Metric name '{}' ({}) should fail validation",
854 invalid_name,
855 description
856 );
857 }
858 }
859
860 #[test]
861 fn test_skip_tls_verify_with_body() {
862 use crate::spec_parser::ApiOperation;
863 use openapiv3::Operation;
864 use serde_json::json;
865
866 let operation = ApiOperation {
868 method: "post".to_string(),
869 path: "/api/users".to_string(),
870 operation: Operation::default(),
871 operation_id: Some("createUser".to_string()),
872 };
873
874 let template = RequestTemplate {
875 operation,
876 path_params: HashMap::new(),
877 query_params: HashMap::new(),
878 headers: HashMap::new(),
879 body: Some(json!({"name": "test"})),
880 };
881
882 let config = K6Config {
883 target_url: "https://api.example.com".to_string(),
884 base_path: None,
885 scenario: LoadScenario::Constant,
886 duration_secs: 30,
887 max_vus: 5,
888 threshold_percentile: "p(95)".to_string(),
889 threshold_ms: 500,
890 max_error_rate: 0.05,
891 auth_header: None,
892 custom_headers: HashMap::new(),
893 skip_tls_verify: true,
894 security_testing_enabled: false,
895 chunked_request_bodies: false,
896 };
897
898 let generator = K6ScriptGenerator::new(config, vec![template]);
899 let script = generator.generate().expect("Should generate script");
900
901 assert!(
903 script.contains("insecureSkipTLSVerify: true"),
904 "Script should include insecureSkipTLSVerify option when skip_tls_verify is true"
905 );
906 }
907
908 #[test]
909 fn test_skip_tls_verify_without_body() {
910 use crate::spec_parser::ApiOperation;
911 use openapiv3::Operation;
912
913 let operation = ApiOperation {
915 method: "get".to_string(),
916 path: "/api/users".to_string(),
917 operation: Operation::default(),
918 operation_id: Some("getUsers".to_string()),
919 };
920
921 let template = RequestTemplate {
922 operation,
923 path_params: HashMap::new(),
924 query_params: HashMap::new(),
925 headers: HashMap::new(),
926 body: None,
927 };
928
929 let config = K6Config {
930 target_url: "https://api.example.com".to_string(),
931 base_path: None,
932 scenario: LoadScenario::Constant,
933 duration_secs: 30,
934 max_vus: 5,
935 threshold_percentile: "p(95)".to_string(),
936 threshold_ms: 500,
937 max_error_rate: 0.05,
938 auth_header: None,
939 custom_headers: HashMap::new(),
940 skip_tls_verify: true,
941 security_testing_enabled: false,
942 chunked_request_bodies: false,
943 };
944
945 let generator = K6ScriptGenerator::new(config, vec![template]);
946 let script = generator.generate().expect("Should generate script");
947
948 assert!(
950 script.contains("insecureSkipTLSVerify: true"),
951 "Script should include insecureSkipTLSVerify option when skip_tls_verify is true (no body)"
952 );
953 }
954
955 #[test]
956 fn test_no_skip_tls_verify() {
957 use crate::spec_parser::ApiOperation;
958 use openapiv3::Operation;
959
960 let operation = ApiOperation {
962 method: "get".to_string(),
963 path: "/api/users".to_string(),
964 operation: Operation::default(),
965 operation_id: Some("getUsers".to_string()),
966 };
967
968 let template = RequestTemplate {
969 operation,
970 path_params: HashMap::new(),
971 query_params: HashMap::new(),
972 headers: HashMap::new(),
973 body: None,
974 };
975
976 let config = K6Config {
977 target_url: "https://api.example.com".to_string(),
978 base_path: None,
979 scenario: LoadScenario::Constant,
980 duration_secs: 30,
981 max_vus: 5,
982 threshold_percentile: "p(95)".to_string(),
983 threshold_ms: 500,
984 max_error_rate: 0.05,
985 auth_header: None,
986 custom_headers: HashMap::new(),
987 skip_tls_verify: false,
988 security_testing_enabled: false,
989 chunked_request_bodies: false,
990 };
991
992 let generator = K6ScriptGenerator::new(config, vec![template]);
993 let script = generator.generate().expect("Should generate script");
994
995 assert!(
997 !script.contains("insecureSkipTLSVerify"),
998 "Script should NOT include insecureSkipTLSVerify option when skip_tls_verify is false"
999 );
1000 }
1001
1002 #[test]
1003 fn test_skip_tls_verify_multiple_operations() {
1004 use crate::spec_parser::ApiOperation;
1005 use openapiv3::Operation;
1006 use serde_json::json;
1007
1008 let operation1 = ApiOperation {
1010 method: "get".to_string(),
1011 path: "/api/users".to_string(),
1012 operation: Operation::default(),
1013 operation_id: Some("getUsers".to_string()),
1014 };
1015
1016 let operation2 = ApiOperation {
1017 method: "post".to_string(),
1018 path: "/api/users".to_string(),
1019 operation: Operation::default(),
1020 operation_id: Some("createUser".to_string()),
1021 };
1022
1023 let template1 = RequestTemplate {
1024 operation: operation1,
1025 path_params: HashMap::new(),
1026 query_params: HashMap::new(),
1027 headers: HashMap::new(),
1028 body: None,
1029 };
1030
1031 let template2 = RequestTemplate {
1032 operation: operation2,
1033 path_params: HashMap::new(),
1034 query_params: HashMap::new(),
1035 headers: HashMap::new(),
1036 body: Some(json!({"name": "test"})),
1037 };
1038
1039 let config = K6Config {
1040 target_url: "https://api.example.com".to_string(),
1041 base_path: None,
1042 scenario: LoadScenario::Constant,
1043 duration_secs: 30,
1044 max_vus: 5,
1045 threshold_percentile: "p(95)".to_string(),
1046 threshold_ms: 500,
1047 max_error_rate: 0.05,
1048 auth_header: None,
1049 custom_headers: HashMap::new(),
1050 skip_tls_verify: true,
1051 security_testing_enabled: false,
1052 chunked_request_bodies: false,
1053 };
1054
1055 let generator = K6ScriptGenerator::new(config, vec![template1, template2]);
1056 let script = generator.generate().expect("Should generate script");
1057
1058 let skip_count = script.matches("insecureSkipTLSVerify: true").count();
1061 assert_eq!(
1062 skip_count, 1,
1063 "Script should include insecureSkipTLSVerify exactly once in global options (not per-request)"
1064 );
1065
1066 let options_start = script.find("export const options = {").expect("Should have options");
1068 let scenarios_start = script.find("scenarios:").expect("Should have scenarios");
1069 let options_prefix = &script[options_start..scenarios_start];
1070 assert!(
1071 options_prefix.contains("insecureSkipTLSVerify: true"),
1072 "insecureSkipTLSVerify should be in global options block"
1073 );
1074 }
1075
1076 #[test]
1077 fn test_dynamic_params_in_body() {
1078 use crate::spec_parser::ApiOperation;
1079 use openapiv3::Operation;
1080 use serde_json::json;
1081
1082 let operation = ApiOperation {
1084 method: "post".to_string(),
1085 path: "/api/resources".to_string(),
1086 operation: Operation::default(),
1087 operation_id: Some("createResource".to_string()),
1088 };
1089
1090 let template = RequestTemplate {
1091 operation,
1092 path_params: HashMap::new(),
1093 query_params: HashMap::new(),
1094 headers: HashMap::new(),
1095 body: Some(json!({
1096 "name": "load-test-${__VU}",
1097 "iteration": "${__ITER}"
1098 })),
1099 };
1100
1101 let config = K6Config {
1102 target_url: "https://api.example.com".to_string(),
1103 base_path: None,
1104 scenario: LoadScenario::Constant,
1105 duration_secs: 30,
1106 max_vus: 5,
1107 threshold_percentile: "p(95)".to_string(),
1108 threshold_ms: 500,
1109 max_error_rate: 0.05,
1110 auth_header: None,
1111 custom_headers: HashMap::new(),
1112 skip_tls_verify: false,
1113 security_testing_enabled: false,
1114 chunked_request_bodies: false,
1115 };
1116
1117 let generator = K6ScriptGenerator::new(config, vec![template]);
1118 let script = generator.generate().expect("Should generate script");
1119
1120 assert!(
1122 script.contains("Dynamic body with runtime placeholders"),
1123 "Script should contain comment about dynamic body"
1124 );
1125
1126 assert!(
1128 script.contains("__VU"),
1129 "Script should contain __VU reference for dynamic VU-based values"
1130 );
1131
1132 assert!(
1134 script.contains("__ITER"),
1135 "Script should contain __ITER reference for dynamic iteration values"
1136 );
1137 }
1138
1139 #[test]
1140 fn test_dynamic_params_with_uuid() {
1141 use crate::spec_parser::ApiOperation;
1142 use openapiv3::Operation;
1143 use serde_json::json;
1144
1145 let operation = ApiOperation {
1147 method: "post".to_string(),
1148 path: "/api/resources".to_string(),
1149 operation: Operation::default(),
1150 operation_id: Some("createResource".to_string()),
1151 };
1152
1153 let template = RequestTemplate {
1154 operation,
1155 path_params: HashMap::new(),
1156 query_params: HashMap::new(),
1157 headers: HashMap::new(),
1158 body: Some(json!({
1159 "id": "${__UUID}"
1160 })),
1161 };
1162
1163 let config = K6Config {
1164 target_url: "https://api.example.com".to_string(),
1165 base_path: None,
1166 scenario: LoadScenario::Constant,
1167 duration_secs: 30,
1168 max_vus: 5,
1169 threshold_percentile: "p(95)".to_string(),
1170 threshold_ms: 500,
1171 max_error_rate: 0.05,
1172 auth_header: None,
1173 custom_headers: HashMap::new(),
1174 skip_tls_verify: false,
1175 security_testing_enabled: false,
1176 chunked_request_bodies: false,
1177 };
1178
1179 let generator = K6ScriptGenerator::new(config, vec![template]);
1180 let script = generator.generate().expect("Should generate script");
1181
1182 assert!(
1185 !script.contains("k6/experimental/webcrypto"),
1186 "Script should NOT include deprecated k6/experimental/webcrypto import"
1187 );
1188
1189 assert!(
1191 script.contains("crypto.randomUUID()"),
1192 "Script should contain crypto.randomUUID() for UUID placeholder"
1193 );
1194 }
1195
1196 #[test]
1197 fn test_dynamic_params_with_counter() {
1198 use crate::spec_parser::ApiOperation;
1199 use openapiv3::Operation;
1200 use serde_json::json;
1201
1202 let operation = ApiOperation {
1204 method: "post".to_string(),
1205 path: "/api/resources".to_string(),
1206 operation: Operation::default(),
1207 operation_id: Some("createResource".to_string()),
1208 };
1209
1210 let template = RequestTemplate {
1211 operation,
1212 path_params: HashMap::new(),
1213 query_params: HashMap::new(),
1214 headers: HashMap::new(),
1215 body: Some(json!({
1216 "sequence": "${__COUNTER}"
1217 })),
1218 };
1219
1220 let config = K6Config {
1221 target_url: "https://api.example.com".to_string(),
1222 base_path: None,
1223 scenario: LoadScenario::Constant,
1224 duration_secs: 30,
1225 max_vus: 5,
1226 threshold_percentile: "p(95)".to_string(),
1227 threshold_ms: 500,
1228 max_error_rate: 0.05,
1229 auth_header: None,
1230 custom_headers: HashMap::new(),
1231 skip_tls_verify: false,
1232 security_testing_enabled: false,
1233 chunked_request_bodies: false,
1234 };
1235
1236 let generator = K6ScriptGenerator::new(config, vec![template]);
1237 let script = generator.generate().expect("Should generate script");
1238
1239 assert!(
1241 script.contains("let globalCounter = 0"),
1242 "Script should include globalCounter initialization when COUNTER placeholder is used"
1243 );
1244
1245 assert!(
1247 script.contains("globalCounter++"),
1248 "Script should contain globalCounter++ for COUNTER placeholder"
1249 );
1250 }
1251
1252 #[test]
1253 fn test_static_body_no_dynamic_marker() {
1254 use crate::spec_parser::ApiOperation;
1255 use openapiv3::Operation;
1256 use serde_json::json;
1257
1258 let operation = ApiOperation {
1260 method: "post".to_string(),
1261 path: "/api/resources".to_string(),
1262 operation: Operation::default(),
1263 operation_id: Some("createResource".to_string()),
1264 };
1265
1266 let template = RequestTemplate {
1267 operation,
1268 path_params: HashMap::new(),
1269 query_params: HashMap::new(),
1270 headers: HashMap::new(),
1271 body: Some(json!({
1272 "name": "static-value",
1273 "count": 42
1274 })),
1275 };
1276
1277 let config = K6Config {
1278 target_url: "https://api.example.com".to_string(),
1279 base_path: None,
1280 scenario: LoadScenario::Constant,
1281 duration_secs: 30,
1282 max_vus: 5,
1283 threshold_percentile: "p(95)".to_string(),
1284 threshold_ms: 500,
1285 max_error_rate: 0.05,
1286 auth_header: None,
1287 custom_headers: HashMap::new(),
1288 skip_tls_verify: false,
1289 security_testing_enabled: false,
1290 chunked_request_bodies: false,
1291 };
1292
1293 let generator = K6ScriptGenerator::new(config, vec![template]);
1294 let script = generator.generate().expect("Should generate script");
1295
1296 assert!(
1298 !script.contains("Dynamic body with runtime placeholders"),
1299 "Script should NOT contain dynamic body comment for static body"
1300 );
1301
1302 assert!(
1304 !script.contains("webcrypto"),
1305 "Script should NOT include webcrypto import for static body"
1306 );
1307
1308 assert!(
1310 !script.contains("let globalCounter"),
1311 "Script should NOT include globalCounter for static body"
1312 );
1313 }
1314
1315 #[test]
1316 fn test_security_testing_enabled_generates_calling_code() {
1317 use crate::spec_parser::ApiOperation;
1318 use openapiv3::Operation;
1319 use serde_json::json;
1320
1321 let operation = ApiOperation {
1322 method: "post".to_string(),
1323 path: "/api/users".to_string(),
1324 operation: Operation::default(),
1325 operation_id: Some("createUser".to_string()),
1326 };
1327
1328 let template = RequestTemplate {
1329 operation,
1330 path_params: HashMap::new(),
1331 query_params: HashMap::new(),
1332 headers: HashMap::new(),
1333 body: Some(json!({"name": "test"})),
1334 };
1335
1336 let config = K6Config {
1337 target_url: "https://api.example.com".to_string(),
1338 base_path: None,
1339 scenario: LoadScenario::Constant,
1340 duration_secs: 30,
1341 max_vus: 5,
1342 threshold_percentile: "p(95)".to_string(),
1343 threshold_ms: 500,
1344 max_error_rate: 0.05,
1345 auth_header: None,
1346 custom_headers: HashMap::new(),
1347 skip_tls_verify: false,
1348 security_testing_enabled: true,
1349 chunked_request_bodies: false,
1350 };
1351
1352 let generator = K6ScriptGenerator::new(config, vec![template]);
1353 let script = generator.generate().expect("Should generate script");
1354
1355 assert!(
1357 script.contains("getNextSecurityPayload"),
1358 "Script should contain getNextSecurityPayload() call when security_testing_enabled is true"
1359 );
1360 assert!(
1361 script.contains("applySecurityPayload"),
1362 "Script should contain applySecurityPayload() call when security_testing_enabled is true"
1363 );
1364 assert!(
1365 script.contains("secPayloadGroup"),
1366 "Script should contain secPayloadGroup variable when security_testing_enabled is true"
1367 );
1368 assert!(
1369 script.contains("secBodyPayload"),
1370 "Script should contain secBodyPayload variable when security_testing_enabled is true"
1371 );
1372 assert!(
1374 script.contains("hasSecCookie"),
1375 "Script should track hasSecCookie for CookieJar conflict avoidance"
1376 );
1377 assert!(
1378 script.contains("secRequestOpts"),
1379 "Script should use secRequestOpts to conditionally skip CookieJar"
1380 );
1381 assert!(
1383 script.contains("const requestHeaders = { ..."),
1384 "Script should spread headers into mutable copy for security payload injection"
1385 );
1386 assert!(
1388 script.contains("secPayload.injectAsPath"),
1389 "Script should check injectAsPath for path-based URI injection"
1390 );
1391 assert!(
1393 script.contains("secBodyPayload.formBody"),
1394 "Script should check formBody for form-encoded body delivery"
1395 );
1396 assert!(
1397 script.contains("application/x-www-form-urlencoded"),
1398 "Script should set Content-Type for form-encoded body"
1399 );
1400 let op_comment_pos =
1402 script.find("// Operation 0:").expect("Should have Operation 0 comment");
1403 let sec_payload_pos = script
1404 .find("const secPayloadGroup = typeof getNextSecurityPayload")
1405 .expect("Should have secPayloadGroup assignment");
1406 assert!(
1407 sec_payload_pos > op_comment_pos,
1408 "secPayloadGroup should be fetched inside operation block (per-operation), not before it (per-iteration)"
1409 );
1410 }
1411
1412 #[test]
1413 fn test_security_testing_disabled_no_calling_code() {
1414 use crate::spec_parser::ApiOperation;
1415 use openapiv3::Operation;
1416 use serde_json::json;
1417
1418 let operation = ApiOperation {
1419 method: "post".to_string(),
1420 path: "/api/users".to_string(),
1421 operation: Operation::default(),
1422 operation_id: Some("createUser".to_string()),
1423 };
1424
1425 let template = RequestTemplate {
1426 operation,
1427 path_params: HashMap::new(),
1428 query_params: HashMap::new(),
1429 headers: HashMap::new(),
1430 body: Some(json!({"name": "test"})),
1431 };
1432
1433 let config = K6Config {
1434 target_url: "https://api.example.com".to_string(),
1435 base_path: None,
1436 scenario: LoadScenario::Constant,
1437 duration_secs: 30,
1438 max_vus: 5,
1439 threshold_percentile: "p(95)".to_string(),
1440 threshold_ms: 500,
1441 max_error_rate: 0.05,
1442 auth_header: None,
1443 custom_headers: HashMap::new(),
1444 skip_tls_verify: false,
1445 security_testing_enabled: false,
1446 chunked_request_bodies: false,
1447 };
1448
1449 let generator = K6ScriptGenerator::new(config, vec![template]);
1450 let script = generator.generate().expect("Should generate script");
1451
1452 assert!(
1454 !script.contains("getNextSecurityPayload"),
1455 "Script should NOT contain getNextSecurityPayload() when security_testing_enabled is false"
1456 );
1457 assert!(
1458 !script.contains("applySecurityPayload"),
1459 "Script should NOT contain applySecurityPayload() when security_testing_enabled is false"
1460 );
1461 assert!(
1462 !script.contains("secPayloadGroup"),
1463 "Script should NOT contain secPayloadGroup variable when security_testing_enabled is false"
1464 );
1465 assert!(
1466 !script.contains("secBodyPayload"),
1467 "Script should NOT contain secBodyPayload variable when security_testing_enabled is false"
1468 );
1469 assert!(
1470 !script.contains("hasSecCookie"),
1471 "Script should NOT contain hasSecCookie when security_testing_enabled is false"
1472 );
1473 assert!(
1474 !script.contains("secRequestOpts"),
1475 "Script should NOT contain secRequestOpts when security_testing_enabled is false"
1476 );
1477 assert!(
1478 !script.contains("injectAsPath"),
1479 "Script should NOT contain injectAsPath when security_testing_enabled is false"
1480 );
1481 assert!(
1482 !script.contains("formBody"),
1483 "Script should NOT contain formBody when security_testing_enabled is false"
1484 );
1485 }
1486
1487 #[test]
1491 fn test_security_e2e_definitions_and_calls_both_present() {
1492 use crate::security_payloads::{
1493 SecurityPayloads, SecurityTestConfig, SecurityTestGenerator,
1494 };
1495 use crate::spec_parser::ApiOperation;
1496 use openapiv3::Operation;
1497 use serde_json::json;
1498
1499 let operation = ApiOperation {
1501 method: "post".to_string(),
1502 path: "/api/users".to_string(),
1503 operation: Operation::default(),
1504 operation_id: Some("createUser".to_string()),
1505 };
1506
1507 let template = RequestTemplate {
1508 operation,
1509 path_params: HashMap::new(),
1510 query_params: HashMap::new(),
1511 headers: HashMap::new(),
1512 body: Some(json!({"name": "test"})),
1513 };
1514
1515 let config = K6Config {
1516 target_url: "https://api.example.com".to_string(),
1517 base_path: None,
1518 scenario: LoadScenario::Constant,
1519 duration_secs: 30,
1520 max_vus: 5,
1521 threshold_percentile: "p(95)".to_string(),
1522 threshold_ms: 500,
1523 max_error_rate: 0.05,
1524 auth_header: None,
1525 custom_headers: HashMap::new(),
1526 skip_tls_verify: false,
1527 security_testing_enabled: true,
1528 chunked_request_bodies: false,
1529 };
1530
1531 let generator = K6ScriptGenerator::new(config, vec![template]);
1532 let mut script = generator.generate().expect("Should generate base script");
1533
1534 let security_config = SecurityTestConfig::default().enable();
1536 let payloads = SecurityPayloads::get_payloads(&security_config);
1537 assert!(!payloads.is_empty(), "Should have built-in payloads");
1538
1539 let mut additional_code = String::new();
1540 additional_code
1541 .push_str(&SecurityTestGenerator::generate_payload_selection(&payloads, false));
1542 additional_code.push('\n');
1543 additional_code.push_str(&SecurityTestGenerator::generate_apply_payload(&[]));
1544 additional_code.push('\n');
1545
1546 if let Some(pos) = script.find("export const options") {
1548 script.insert_str(
1549 pos,
1550 &format!("\n// === Advanced Testing Features ===\n{}\n", additional_code),
1551 );
1552 }
1553
1554 assert!(
1557 script.contains("function getNextSecurityPayload()"),
1558 "Final script must contain getNextSecurityPayload function DEFINITION"
1559 );
1560 assert!(
1561 script.contains("function applySecurityPayload("),
1562 "Final script must contain applySecurityPayload function DEFINITION"
1563 );
1564 assert!(
1565 script.contains("securityPayloads"),
1566 "Final script must contain securityPayloads array"
1567 );
1568
1569 assert!(
1571 script.contains("const secPayloadGroup = typeof getNextSecurityPayload"),
1572 "Final script must contain secPayloadGroup assignment (template calling code)"
1573 );
1574 assert!(
1575 script.contains("applySecurityPayload(payload, [], secBodyPayload)"),
1576 "Final script must contain applySecurityPayload CALL with secBodyPayload"
1577 );
1578 assert!(
1579 script.contains("const requestHeaders = { ..."),
1580 "Final script must spread headers for security payload header injection"
1581 );
1582 assert!(
1583 script.contains("for (const secPayload of secPayloadGroup)"),
1584 "Final script must loop over secPayloadGroup"
1585 );
1586 assert!(
1587 script.contains("secPayload.injectAsPath"),
1588 "Final script must check injectAsPath for path-based URI injection"
1589 );
1590 assert!(
1591 script.contains("secBodyPayload.formBody"),
1592 "Final script must check formBody for form-encoded body delivery"
1593 );
1594
1595 let def_pos = script.find("function getNextSecurityPayload()").unwrap();
1597 let call_pos =
1598 script.find("const secPayloadGroup = typeof getNextSecurityPayload").unwrap();
1599 let options_pos = script.find("export const options").unwrap();
1600 let default_fn_pos = script.find("export default function").unwrap();
1601
1602 assert!(
1603 def_pos < options_pos,
1604 "Function definitions must appear before export const options"
1605 );
1606 assert!(
1607 call_pos > default_fn_pos,
1608 "Calling code must appear inside export default function"
1609 );
1610 }
1611
1612 #[test]
1614 fn test_security_uri_injection_for_get_requests() {
1615 use crate::spec_parser::ApiOperation;
1616 use openapiv3::Operation;
1617
1618 let operation = ApiOperation {
1619 method: "get".to_string(),
1620 path: "/api/users".to_string(),
1621 operation: Operation::default(),
1622 operation_id: Some("listUsers".to_string()),
1623 };
1624
1625 let template = RequestTemplate {
1626 operation,
1627 path_params: HashMap::new(),
1628 query_params: HashMap::new(),
1629 headers: HashMap::new(),
1630 body: None,
1631 };
1632
1633 let config = K6Config {
1634 target_url: "https://api.example.com".to_string(),
1635 base_path: None,
1636 scenario: LoadScenario::Constant,
1637 duration_secs: 30,
1638 max_vus: 5,
1639 threshold_percentile: "p(95)".to_string(),
1640 threshold_ms: 500,
1641 max_error_rate: 0.05,
1642 auth_header: None,
1643 custom_headers: HashMap::new(),
1644 skip_tls_verify: false,
1645 security_testing_enabled: true,
1646 chunked_request_bodies: false,
1647 };
1648
1649 let generator = K6ScriptGenerator::new(config, vec![template]);
1650 let script = generator.generate().expect("Should generate script");
1651
1652 assert!(
1654 script.contains("requestUrl"),
1655 "Script should build requestUrl variable for URI payload injection"
1656 );
1657 assert!(
1658 script.contains("secPayload.location === 'uri'"),
1659 "Script should check for URI-location payloads"
1660 );
1661 assert!(
1663 script.contains("'test=' + encodeURIComponent(secPayload.payload)"),
1664 "Script should URL-encode security payload in query string for valid HTTP"
1665 );
1666 assert!(
1668 script.contains("secPayload.injectAsPath"),
1669 "Script should check injectAsPath for path-based URI injection"
1670 );
1671 assert!(
1672 script.contains("encodeURI(secPayload.payload)"),
1673 "Script should use encodeURI for path-based injection"
1674 );
1675 assert!(
1677 script.contains("http.get(requestUrl,"),
1678 "GET request should use requestUrl (with URI injection) instead of inline URL"
1679 );
1680 }
1681
1682 #[test]
1684 fn test_security_uri_injection_for_post_requests() {
1685 use crate::spec_parser::ApiOperation;
1686 use openapiv3::Operation;
1687 use serde_json::json;
1688
1689 let operation = ApiOperation {
1690 method: "post".to_string(),
1691 path: "/api/users".to_string(),
1692 operation: Operation::default(),
1693 operation_id: Some("createUser".to_string()),
1694 };
1695
1696 let template = RequestTemplate {
1697 operation,
1698 path_params: HashMap::new(),
1699 query_params: HashMap::new(),
1700 headers: HashMap::new(),
1701 body: Some(json!({"name": "test"})),
1702 };
1703
1704 let config = K6Config {
1705 target_url: "https://api.example.com".to_string(),
1706 base_path: None,
1707 scenario: LoadScenario::Constant,
1708 duration_secs: 30,
1709 max_vus: 5,
1710 threshold_percentile: "p(95)".to_string(),
1711 threshold_ms: 500,
1712 max_error_rate: 0.05,
1713 auth_header: None,
1714 custom_headers: HashMap::new(),
1715 skip_tls_verify: false,
1716 security_testing_enabled: true,
1717 chunked_request_bodies: false,
1718 };
1719
1720 let generator = K6ScriptGenerator::new(config, vec![template]);
1721 let script = generator.generate().expect("Should generate script");
1722
1723 assert!(
1725 script.contains("requestUrl"),
1726 "POST script should build requestUrl for URI payload injection"
1727 );
1728 assert!(
1729 script.contains("secPayload.location === 'uri'"),
1730 "POST script should check for URI-location payloads"
1731 );
1732 assert!(
1733 script.contains("applySecurityPayload(payload, [], secBodyPayload)"),
1734 "POST script should apply security body payload to request body"
1735 );
1736 assert!(
1738 script.contains("http.post(requestUrl,"),
1739 "POST request should use requestUrl (with URI injection) instead of inline URL"
1740 );
1741 }
1742
1743 #[test]
1745 fn test_no_uri_injection_when_security_disabled() {
1746 use crate::spec_parser::ApiOperation;
1747 use openapiv3::Operation;
1748
1749 let operation = ApiOperation {
1750 method: "get".to_string(),
1751 path: "/api/users".to_string(),
1752 operation: Operation::default(),
1753 operation_id: Some("listUsers".to_string()),
1754 };
1755
1756 let template = RequestTemplate {
1757 operation,
1758 path_params: HashMap::new(),
1759 query_params: HashMap::new(),
1760 headers: HashMap::new(),
1761 body: None,
1762 };
1763
1764 let config = K6Config {
1765 target_url: "https://api.example.com".to_string(),
1766 base_path: None,
1767 scenario: LoadScenario::Constant,
1768 duration_secs: 30,
1769 max_vus: 5,
1770 threshold_percentile: "p(95)".to_string(),
1771 threshold_ms: 500,
1772 max_error_rate: 0.05,
1773 auth_header: None,
1774 custom_headers: HashMap::new(),
1775 skip_tls_verify: false,
1776 security_testing_enabled: false,
1777 chunked_request_bodies: false,
1778 };
1779
1780 let generator = K6ScriptGenerator::new(config, vec![template]);
1781 let script = generator.generate().expect("Should generate script");
1782
1783 assert!(
1785 !script.contains("requestUrl"),
1786 "Script should NOT have requestUrl when security is disabled"
1787 );
1788 assert!(
1789 !script.contains("secPayloadGroup"),
1790 "Script should NOT have secPayloadGroup when security is disabled"
1791 );
1792 assert!(
1793 !script.contains("secBodyPayload"),
1794 "Script should NOT have secBodyPayload when security is disabled"
1795 );
1796 }
1797
1798 #[test]
1800 fn test_uses_per_request_cookie_jar() {
1801 use crate::spec_parser::ApiOperation;
1802 use openapiv3::Operation;
1803
1804 let operation = ApiOperation {
1805 method: "get".to_string(),
1806 path: "/api/users".to_string(),
1807 operation: Operation::default(),
1808 operation_id: Some("listUsers".to_string()),
1809 };
1810
1811 let template = RequestTemplate {
1812 operation,
1813 path_params: HashMap::new(),
1814 query_params: HashMap::new(),
1815 headers: HashMap::new(),
1816 body: None,
1817 };
1818
1819 let config = K6Config {
1820 target_url: "https://api.example.com".to_string(),
1821 base_path: None,
1822 scenario: LoadScenario::Constant,
1823 duration_secs: 30,
1824 max_vus: 5,
1825 threshold_percentile: "p(95)".to_string(),
1826 threshold_ms: 500,
1827 max_error_rate: 0.05,
1828 auth_header: None,
1829 custom_headers: HashMap::new(),
1830 skip_tls_verify: false,
1831 security_testing_enabled: false,
1832 chunked_request_bodies: false,
1833 };
1834
1835 let generator = K6ScriptGenerator::new(config, vec![template]);
1836 let script = generator.generate().expect("Should generate script");
1837
1838 assert!(
1840 script.contains("jar: new http.CookieJar()"),
1841 "Script should create fresh CookieJar per request"
1842 );
1843 assert!(
1844 !script.contains("jar: null"),
1845 "Script should NOT use jar: null (does not disable default VU cookie jar in k6)"
1846 );
1847 assert!(
1848 !script.contains("EMPTY_JAR"),
1849 "Script should NOT use shared EMPTY_JAR (accumulates Set-Cookie responses)"
1850 );
1851 }
1852}