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