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 pub fn sanitize_js_identifier(name: &str) -> String {
154 let mut result = String::new();
155 let mut chars = name.chars().peekable();
156
157 if let Some(&first) = chars.peek() {
159 if first.is_ascii_digit() {
160 result.push('_');
161 }
162 }
163
164 for ch in chars {
165 if ch.is_ascii_alphanumeric() || ch == '_' {
166 result.push(ch);
167 } else {
168 if !result.ends_with('_') {
171 result.push('_');
172 }
173 }
174 }
175
176 result = result.trim_end_matches('_').to_string();
178
179 if result.is_empty() {
181 result = "operation".to_string();
182 }
183
184 result
185 }
186
187 fn build_template_data(&self) -> Result<K6ScriptTemplateData> {
189 let stages = self
190 .config
191 .scenario
192 .generate_stages(self.config.duration_secs, self.config.max_vus);
193
194 let base_path = self.config.base_path.as_deref().unwrap_or("");
196
197 let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
199
200 let operations = self
201 .templates
202 .iter()
203 .enumerate()
204 .map(|(idx, template)| {
205 let display_name = template.operation.display_name();
206 let sanitized_name = Self::sanitize_js_identifier(&display_name);
207 let metric_name = sanitized_name.clone();
210 let k6_method = match template.operation.method.to_lowercase().as_str() {
212 "delete" => "del".to_string(),
213 m => m.to_string(),
214 };
215 let is_get_or_head = matches!(k6_method.as_str(), "get" | "head");
218
219 let raw_path = template.generate_path();
222 let full_path = if base_path.is_empty() {
223 raw_path
224 } else {
225 format!("{}{}", base_path, raw_path)
226 };
227 let processed_path = DynamicParamProcessor::process_path(&full_path);
228 all_placeholders.extend(processed_path.placeholders.clone());
229
230 let (body_value, body_is_dynamic) = if let Some(body) = &template.body {
232 let processed_body = DynamicParamProcessor::process_json_body(body);
233 all_placeholders.extend(processed_body.placeholders.clone());
234 (Some(processed_body.value), processed_body.is_dynamic)
235 } else {
236 (None, false)
237 };
238
239 let path_value = if processed_path.is_dynamic {
240 processed_path.value
241 } else {
242 full_path
243 };
244
245 K6OperationData {
246 index: idx,
247 name: sanitized_name,
248 metric_name,
249 display_name,
250 method: k6_method,
251 path: Value::String(path_value),
252 path_is_dynamic: processed_path.is_dynamic,
253 headers: Value::String(self.build_headers_json(template)),
254 body: body_value.map(Value::String),
255 body_is_dynamic,
256 has_body: template.body.is_some(),
257 is_get_or_head,
258 }
259 })
260 .collect::<Vec<_>>();
261
262 let required_imports: Vec<String> =
264 DynamicParamProcessor::get_required_imports(&all_placeholders)
265 .into_iter()
266 .map(String::from)
267 .collect();
268 let required_globals: Vec<String> =
269 DynamicParamProcessor::get_required_globals(&all_placeholders)
270 .into_iter()
271 .map(String::from)
272 .collect();
273 let has_dynamic_values = !all_placeholders.is_empty();
274
275 Ok(K6ScriptTemplateData {
276 base_url: self.config.target_url.clone(),
277 stages: stages
278 .iter()
279 .map(|s| K6StageData {
280 duration: s.duration.clone(),
281 target: s.target,
282 })
283 .collect(),
284 operations,
285 threshold_percentile: self.config.threshold_percentile.clone(),
286 threshold_ms: self.config.threshold_ms,
287 max_error_rate: self.config.max_error_rate,
288 scenario_name: format!("{:?}", self.config.scenario).to_lowercase(),
289 skip_tls_verify: self.config.skip_tls_verify,
290 has_dynamic_values,
291 dynamic_imports: required_imports,
292 dynamic_globals: required_globals,
293 security_testing_enabled: self.config.security_testing_enabled,
294 has_custom_headers: !self.config.custom_headers.is_empty(),
295 chunked_request_bodies: self.config.chunked_request_bodies,
296 })
297 }
298
299 fn build_headers_json(&self, template: &RequestTemplate) -> String {
301 let mut headers = template.get_headers();
302
303 if let Some(auth) = &self.config.auth_header {
305 headers.insert("Authorization".to_string(), auth.clone());
306 }
307
308 for (key, value) in &self.config.custom_headers {
310 headers.insert(key.clone(), value.clone());
311 }
312
313 if self.config.chunked_request_bodies && template.body.is_some() {
318 headers.insert("Transfer-Encoding".to_string(), "chunked".to_string());
319 }
320
321 serde_json::to_string(&headers).unwrap_or_else(|_| "{}".to_string())
323 }
324
325 pub fn validate_script(script: &str) -> Vec<String> {
334 let mut errors = Vec::new();
335
336 if !script.contains("import http from 'k6/http'") {
338 errors.push("Missing required import: 'k6/http'".to_string());
339 }
340 if !script.contains("import { check") && !script.contains("import {check") {
341 errors.push("Missing required import: 'check' from 'k6'".to_string());
342 }
343 if !script.contains("import { Rate, Trend") && !script.contains("import {Rate, Trend") {
344 errors.push("Missing required import: 'Rate, Trend' from 'k6/metrics'".to_string());
345 }
346
347 let lines: Vec<&str> = script.lines().collect();
351 for (line_num, line) in lines.iter().enumerate() {
352 let trimmed = line.trim();
353
354 if trimmed.contains("new Trend(") || trimmed.contains("new Rate(") {
356 if let Some(start) = trimmed.find('\'') {
359 if let Some(end) = trimmed[start + 1..].find('\'') {
360 let metric_name = &trimmed[start + 1..start + 1 + end];
361 if !Self::is_valid_k6_metric_name(metric_name) {
362 errors.push(format!(
363 "Line {}: Invalid k6 metric name '{}'. Metric names must only contain ASCII letters, numbers, or underscores and start with a letter or underscore.",
364 line_num + 1,
365 metric_name
366 ));
367 }
368 }
369 } else if let Some(start) = trimmed.find('"') {
370 if let Some(end) = trimmed[start + 1..].find('"') {
371 let metric_name = &trimmed[start + 1..start + 1 + end];
372 if !Self::is_valid_k6_metric_name(metric_name) {
373 errors.push(format!(
374 "Line {}: Invalid k6 metric name '{}'. Metric names must only contain ASCII letters, numbers, or underscores and start with a letter or underscore.",
375 line_num + 1,
376 metric_name
377 ));
378 }
379 }
380 }
381 }
382
383 if trimmed.starts_with("const ") || trimmed.starts_with("let ") {
385 if let Some(equals_pos) = trimmed.find('=') {
386 let var_decl = &trimmed[..equals_pos];
387 if var_decl.contains('.')
390 && !var_decl.contains("'")
391 && !var_decl.contains("\"")
392 && !var_decl.trim().starts_with("//")
393 {
394 errors.push(format!(
395 "Line {}: Invalid JavaScript variable name with dot: {}. Variable names cannot contain dots.",
396 line_num + 1,
397 var_decl.trim()
398 ));
399 }
400 }
401 }
402 }
403
404 errors
405 }
406
407 fn is_valid_k6_metric_name(name: &str) -> bool {
414 if name.is_empty() || name.len() > 128 {
415 return false;
416 }
417
418 let mut chars = name.chars();
419
420 if let Some(first) = chars.next() {
422 if !first.is_ascii_alphabetic() && first != '_' {
423 return false;
424 }
425 }
426
427 for ch in chars {
429 if !ch.is_ascii_alphanumeric() && ch != '_' {
430 return false;
431 }
432 }
433
434 true
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 #[test]
443 fn test_k6_config_creation() {
444 let config = K6Config {
445 target_url: "https://api.example.com".to_string(),
446 base_path: None,
447 scenario: LoadScenario::RampUp,
448 duration_secs: 60,
449 max_vus: 10,
450 threshold_percentile: "p(95)".to_string(),
451 threshold_ms: 500,
452 max_error_rate: 0.05,
453 auth_header: None,
454 custom_headers: HashMap::new(),
455 skip_tls_verify: false,
456 security_testing_enabled: false,
457 chunked_request_bodies: false,
458 };
459
460 assert_eq!(config.duration_secs, 60);
461 assert_eq!(config.max_vus, 10);
462 }
463
464 #[test]
465 fn test_script_generator_creation() {
466 let config = K6Config {
467 target_url: "https://api.example.com".to_string(),
468 base_path: None,
469 scenario: LoadScenario::Constant,
470 duration_secs: 30,
471 max_vus: 5,
472 threshold_percentile: "p(95)".to_string(),
473 threshold_ms: 500,
474 max_error_rate: 0.05,
475 auth_header: None,
476 custom_headers: HashMap::new(),
477 skip_tls_verify: false,
478 security_testing_enabled: false,
479 chunked_request_bodies: false,
480 };
481
482 let templates = vec![];
483 let generator = K6ScriptGenerator::new(config, templates);
484
485 assert_eq!(generator.templates.len(), 0);
486 }
487
488 #[test]
489 fn test_sanitize_js_identifier() {
490 assert_eq!(
492 K6ScriptGenerator::sanitize_js_identifier("billing.subscriptions.v1"),
493 "billing_subscriptions_v1"
494 );
495
496 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("get user"), "get_user");
498
499 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("123invalid"), "_123invalid");
501
502 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("getUsers"), "getUsers");
504
505 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("test...name"), "test_name");
507
508 assert_eq!(K6ScriptGenerator::sanitize_js_identifier(""), "operation");
510
511 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("test@name#value"), "test_name_value");
513
514 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("plans.list"), "plans_list");
516 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("plans.create"), "plans_create");
517 assert_eq!(
518 K6ScriptGenerator::sanitize_js_identifier("plans.update-pricing-schemes"),
519 "plans_update_pricing_schemes"
520 );
521 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("users CRUD"), "users_CRUD");
522 }
523
524 #[test]
525 fn test_script_generation_with_dots_in_name() {
526 use crate::spec_parser::ApiOperation;
527 use openapiv3::Operation;
528
529 let operation = ApiOperation {
531 method: "get".to_string(),
532 path: "/billing/subscriptions".to_string(),
533 operation: Operation::default(),
534 operation_id: Some("billing.subscriptions.v1".to_string()),
535 };
536
537 let template = RequestTemplate {
538 operation,
539 path_params: HashMap::new(),
540 query_params: HashMap::new(),
541 headers: HashMap::new(),
542 body: None,
543 };
544
545 let config = K6Config {
546 target_url: "https://api.example.com".to_string(),
547 base_path: None,
548 scenario: LoadScenario::Constant,
549 duration_secs: 30,
550 max_vus: 5,
551 threshold_percentile: "p(95)".to_string(),
552 threshold_ms: 500,
553 max_error_rate: 0.05,
554 auth_header: None,
555 custom_headers: HashMap::new(),
556 skip_tls_verify: false,
557 security_testing_enabled: false,
558 chunked_request_bodies: false,
559 };
560
561 let generator = K6ScriptGenerator::new(config, vec![template]);
562 let script = generator.generate().expect("Should generate script");
563
564 assert!(
566 script.contains("const billing_subscriptions_v1_latency"),
567 "Script should contain sanitized variable name for latency"
568 );
569 assert!(
570 script.contains("const billing_subscriptions_v1_errors"),
571 "Script should contain sanitized variable name for errors"
572 );
573
574 assert!(
577 !script.contains("const billing.subscriptions"),
578 "Script should not contain variable names with dots - this would cause 'Unexpected token .' error"
579 );
580
581 assert!(
584 script.contains("'billing_subscriptions_v1_latency'"),
585 "Metric name strings should be sanitized (no dots) - k6 validation requires valid metric names"
586 );
587 assert!(
588 script.contains("'billing_subscriptions_v1_errors'"),
589 "Metric name strings should be sanitized (no dots) - k6 validation requires valid metric names"
590 );
591
592 assert!(
594 script.contains("billing.subscriptions.v1"),
595 "Script should contain original name in comments/strings for readability"
596 );
597
598 assert!(
600 script.contains("billing_subscriptions_v1_latency.add"),
601 "Variable usage should use sanitized name"
602 );
603 assert!(
604 script.contains("billing_subscriptions_v1_errors.add"),
605 "Variable usage should use sanitized name"
606 );
607 }
608
609 #[test]
610 fn test_validate_script_valid() {
611 let valid_script = r#"
612import http from 'k6/http';
613import { check, sleep } from 'k6';
614import { Rate, Trend } from 'k6/metrics';
615
616const test_latency = new Trend('test_latency');
617const test_errors = new Rate('test_errors');
618
619export default function() {
620 const res = http.get('https://example.com');
621 test_latency.add(res.timings.duration);
622 test_errors.add(res.status !== 200);
623}
624"#;
625
626 let errors = K6ScriptGenerator::validate_script(valid_script);
627 assert!(errors.is_empty(), "Valid script should have no validation errors");
628 }
629
630 #[test]
631 fn test_validate_script_invalid_metric_name() {
632 let invalid_script = r#"
633import http from 'k6/http';
634import { check, sleep } from 'k6';
635import { Rate, Trend } from 'k6/metrics';
636
637const test_latency = new Trend('test.latency');
638const test_errors = new Rate('test_errors');
639
640export default function() {
641 const res = http.get('https://example.com');
642 test_latency.add(res.timings.duration);
643}
644"#;
645
646 let errors = K6ScriptGenerator::validate_script(invalid_script);
647 assert!(
648 !errors.is_empty(),
649 "Script with invalid metric name should have validation errors"
650 );
651 assert!(
652 errors.iter().any(|e| e.contains("Invalid k6 metric name")),
653 "Should detect invalid metric name with dot"
654 );
655 }
656
657 #[test]
658 fn test_validate_script_missing_imports() {
659 let invalid_script = r#"
660const test_latency = new Trend('test_latency');
661export default function() {}
662"#;
663
664 let errors = K6ScriptGenerator::validate_script(invalid_script);
665 assert!(!errors.is_empty(), "Script missing imports should have validation errors");
666 }
667
668 #[test]
669 fn test_validate_script_metric_name_validation() {
670 let valid_script = r#"
673import http from 'k6/http';
674import { check, sleep } from 'k6';
675import { Rate, Trend } from 'k6/metrics';
676const test_latency = new Trend('test_latency');
677const test_errors = new Rate('test_errors');
678export default function() {}
679"#;
680 let errors = K6ScriptGenerator::validate_script(valid_script);
681 assert!(errors.is_empty(), "Valid metric names should pass validation");
682
683 let invalid_cases = vec![
685 ("test.latency", "dot in metric name"),
686 ("123test", "starts with number"),
687 ("test-latency", "hyphen in metric name"),
688 ("test@latency", "special character"),
689 ];
690
691 for (invalid_name, description) in invalid_cases {
692 let script = format!(
693 r#"
694import http from 'k6/http';
695import {{ check, sleep }} from 'k6';
696import {{ Rate, Trend }} from 'k6/metrics';
697const test_latency = new Trend('{}');
698export default function() {{}}
699"#,
700 invalid_name
701 );
702 let errors = K6ScriptGenerator::validate_script(&script);
703 assert!(
704 !errors.is_empty(),
705 "Metric name '{}' ({}) should fail validation",
706 invalid_name,
707 description
708 );
709 }
710 }
711
712 #[test]
713 fn test_skip_tls_verify_with_body() {
714 use crate::spec_parser::ApiOperation;
715 use openapiv3::Operation;
716 use serde_json::json;
717
718 let operation = ApiOperation {
720 method: "post".to_string(),
721 path: "/api/users".to_string(),
722 operation: Operation::default(),
723 operation_id: Some("createUser".to_string()),
724 };
725
726 let template = RequestTemplate {
727 operation,
728 path_params: HashMap::new(),
729 query_params: HashMap::new(),
730 headers: HashMap::new(),
731 body: Some(json!({"name": "test"})),
732 };
733
734 let config = K6Config {
735 target_url: "https://api.example.com".to_string(),
736 base_path: None,
737 scenario: LoadScenario::Constant,
738 duration_secs: 30,
739 max_vus: 5,
740 threshold_percentile: "p(95)".to_string(),
741 threshold_ms: 500,
742 max_error_rate: 0.05,
743 auth_header: None,
744 custom_headers: HashMap::new(),
745 skip_tls_verify: true,
746 security_testing_enabled: false,
747 chunked_request_bodies: false,
748 };
749
750 let generator = K6ScriptGenerator::new(config, vec![template]);
751 let script = generator.generate().expect("Should generate script");
752
753 assert!(
755 script.contains("insecureSkipTLSVerify: true"),
756 "Script should include insecureSkipTLSVerify option when skip_tls_verify is true"
757 );
758 }
759
760 #[test]
761 fn test_skip_tls_verify_without_body() {
762 use crate::spec_parser::ApiOperation;
763 use openapiv3::Operation;
764
765 let operation = ApiOperation {
767 method: "get".to_string(),
768 path: "/api/users".to_string(),
769 operation: Operation::default(),
770 operation_id: Some("getUsers".to_string()),
771 };
772
773 let template = RequestTemplate {
774 operation,
775 path_params: HashMap::new(),
776 query_params: HashMap::new(),
777 headers: HashMap::new(),
778 body: None,
779 };
780
781 let config = K6Config {
782 target_url: "https://api.example.com".to_string(),
783 base_path: None,
784 scenario: LoadScenario::Constant,
785 duration_secs: 30,
786 max_vus: 5,
787 threshold_percentile: "p(95)".to_string(),
788 threshold_ms: 500,
789 max_error_rate: 0.05,
790 auth_header: None,
791 custom_headers: HashMap::new(),
792 skip_tls_verify: true,
793 security_testing_enabled: false,
794 chunked_request_bodies: false,
795 };
796
797 let generator = K6ScriptGenerator::new(config, vec![template]);
798 let script = generator.generate().expect("Should generate script");
799
800 assert!(
802 script.contains("insecureSkipTLSVerify: true"),
803 "Script should include insecureSkipTLSVerify option when skip_tls_verify is true (no body)"
804 );
805 }
806
807 #[test]
808 fn test_no_skip_tls_verify() {
809 use crate::spec_parser::ApiOperation;
810 use openapiv3::Operation;
811
812 let operation = ApiOperation {
814 method: "get".to_string(),
815 path: "/api/users".to_string(),
816 operation: Operation::default(),
817 operation_id: Some("getUsers".to_string()),
818 };
819
820 let template = RequestTemplate {
821 operation,
822 path_params: HashMap::new(),
823 query_params: HashMap::new(),
824 headers: HashMap::new(),
825 body: None,
826 };
827
828 let config = K6Config {
829 target_url: "https://api.example.com".to_string(),
830 base_path: None,
831 scenario: LoadScenario::Constant,
832 duration_secs: 30,
833 max_vus: 5,
834 threshold_percentile: "p(95)".to_string(),
835 threshold_ms: 500,
836 max_error_rate: 0.05,
837 auth_header: None,
838 custom_headers: HashMap::new(),
839 skip_tls_verify: false,
840 security_testing_enabled: false,
841 chunked_request_bodies: false,
842 };
843
844 let generator = K6ScriptGenerator::new(config, vec![template]);
845 let script = generator.generate().expect("Should generate script");
846
847 assert!(
849 !script.contains("insecureSkipTLSVerify"),
850 "Script should NOT include insecureSkipTLSVerify option when skip_tls_verify is false"
851 );
852 }
853
854 #[test]
855 fn test_skip_tls_verify_multiple_operations() {
856 use crate::spec_parser::ApiOperation;
857 use openapiv3::Operation;
858 use serde_json::json;
859
860 let operation1 = ApiOperation {
862 method: "get".to_string(),
863 path: "/api/users".to_string(),
864 operation: Operation::default(),
865 operation_id: Some("getUsers".to_string()),
866 };
867
868 let operation2 = ApiOperation {
869 method: "post".to_string(),
870 path: "/api/users".to_string(),
871 operation: Operation::default(),
872 operation_id: Some("createUser".to_string()),
873 };
874
875 let template1 = RequestTemplate {
876 operation: operation1,
877 path_params: HashMap::new(),
878 query_params: HashMap::new(),
879 headers: HashMap::new(),
880 body: None,
881 };
882
883 let template2 = RequestTemplate {
884 operation: operation2,
885 path_params: HashMap::new(),
886 query_params: HashMap::new(),
887 headers: HashMap::new(),
888 body: Some(json!({"name": "test"})),
889 };
890
891 let config = K6Config {
892 target_url: "https://api.example.com".to_string(),
893 base_path: None,
894 scenario: LoadScenario::Constant,
895 duration_secs: 30,
896 max_vus: 5,
897 threshold_percentile: "p(95)".to_string(),
898 threshold_ms: 500,
899 max_error_rate: 0.05,
900 auth_header: None,
901 custom_headers: HashMap::new(),
902 skip_tls_verify: true,
903 security_testing_enabled: false,
904 chunked_request_bodies: false,
905 };
906
907 let generator = K6ScriptGenerator::new(config, vec![template1, template2]);
908 let script = generator.generate().expect("Should generate script");
909
910 let skip_count = script.matches("insecureSkipTLSVerify: true").count();
913 assert_eq!(
914 skip_count, 1,
915 "Script should include insecureSkipTLSVerify exactly once in global options (not per-request)"
916 );
917
918 let options_start = script.find("export const options = {").expect("Should have options");
920 let scenarios_start = script.find("scenarios:").expect("Should have scenarios");
921 let options_prefix = &script[options_start..scenarios_start];
922 assert!(
923 options_prefix.contains("insecureSkipTLSVerify: true"),
924 "insecureSkipTLSVerify should be in global options block"
925 );
926 }
927
928 #[test]
929 fn test_dynamic_params_in_body() {
930 use crate::spec_parser::ApiOperation;
931 use openapiv3::Operation;
932 use serde_json::json;
933
934 let operation = ApiOperation {
936 method: "post".to_string(),
937 path: "/api/resources".to_string(),
938 operation: Operation::default(),
939 operation_id: Some("createResource".to_string()),
940 };
941
942 let template = RequestTemplate {
943 operation,
944 path_params: HashMap::new(),
945 query_params: HashMap::new(),
946 headers: HashMap::new(),
947 body: Some(json!({
948 "name": "load-test-${__VU}",
949 "iteration": "${__ITER}"
950 })),
951 };
952
953 let config = K6Config {
954 target_url: "https://api.example.com".to_string(),
955 base_path: None,
956 scenario: LoadScenario::Constant,
957 duration_secs: 30,
958 max_vus: 5,
959 threshold_percentile: "p(95)".to_string(),
960 threshold_ms: 500,
961 max_error_rate: 0.05,
962 auth_header: None,
963 custom_headers: HashMap::new(),
964 skip_tls_verify: false,
965 security_testing_enabled: false,
966 chunked_request_bodies: false,
967 };
968
969 let generator = K6ScriptGenerator::new(config, vec![template]);
970 let script = generator.generate().expect("Should generate script");
971
972 assert!(
974 script.contains("Dynamic body with runtime placeholders"),
975 "Script should contain comment about dynamic body"
976 );
977
978 assert!(
980 script.contains("__VU"),
981 "Script should contain __VU reference for dynamic VU-based values"
982 );
983
984 assert!(
986 script.contains("__ITER"),
987 "Script should contain __ITER reference for dynamic iteration values"
988 );
989 }
990
991 #[test]
992 fn test_dynamic_params_with_uuid() {
993 use crate::spec_parser::ApiOperation;
994 use openapiv3::Operation;
995 use serde_json::json;
996
997 let operation = ApiOperation {
999 method: "post".to_string(),
1000 path: "/api/resources".to_string(),
1001 operation: Operation::default(),
1002 operation_id: Some("createResource".to_string()),
1003 };
1004
1005 let template = RequestTemplate {
1006 operation,
1007 path_params: HashMap::new(),
1008 query_params: HashMap::new(),
1009 headers: HashMap::new(),
1010 body: Some(json!({
1011 "id": "${__UUID}"
1012 })),
1013 };
1014
1015 let config = K6Config {
1016 target_url: "https://api.example.com".to_string(),
1017 base_path: None,
1018 scenario: LoadScenario::Constant,
1019 duration_secs: 30,
1020 max_vus: 5,
1021 threshold_percentile: "p(95)".to_string(),
1022 threshold_ms: 500,
1023 max_error_rate: 0.05,
1024 auth_header: None,
1025 custom_headers: HashMap::new(),
1026 skip_tls_verify: false,
1027 security_testing_enabled: false,
1028 chunked_request_bodies: false,
1029 };
1030
1031 let generator = K6ScriptGenerator::new(config, vec![template]);
1032 let script = generator.generate().expect("Should generate script");
1033
1034 assert!(
1037 !script.contains("k6/experimental/webcrypto"),
1038 "Script should NOT include deprecated k6/experimental/webcrypto import"
1039 );
1040
1041 assert!(
1043 script.contains("crypto.randomUUID()"),
1044 "Script should contain crypto.randomUUID() for UUID placeholder"
1045 );
1046 }
1047
1048 #[test]
1049 fn test_dynamic_params_with_counter() {
1050 use crate::spec_parser::ApiOperation;
1051 use openapiv3::Operation;
1052 use serde_json::json;
1053
1054 let operation = ApiOperation {
1056 method: "post".to_string(),
1057 path: "/api/resources".to_string(),
1058 operation: Operation::default(),
1059 operation_id: Some("createResource".to_string()),
1060 };
1061
1062 let template = RequestTemplate {
1063 operation,
1064 path_params: HashMap::new(),
1065 query_params: HashMap::new(),
1066 headers: HashMap::new(),
1067 body: Some(json!({
1068 "sequence": "${__COUNTER}"
1069 })),
1070 };
1071
1072 let config = K6Config {
1073 target_url: "https://api.example.com".to_string(),
1074 base_path: None,
1075 scenario: LoadScenario::Constant,
1076 duration_secs: 30,
1077 max_vus: 5,
1078 threshold_percentile: "p(95)".to_string(),
1079 threshold_ms: 500,
1080 max_error_rate: 0.05,
1081 auth_header: None,
1082 custom_headers: HashMap::new(),
1083 skip_tls_verify: false,
1084 security_testing_enabled: false,
1085 chunked_request_bodies: false,
1086 };
1087
1088 let generator = K6ScriptGenerator::new(config, vec![template]);
1089 let script = generator.generate().expect("Should generate script");
1090
1091 assert!(
1093 script.contains("let globalCounter = 0"),
1094 "Script should include globalCounter initialization when COUNTER placeholder is used"
1095 );
1096
1097 assert!(
1099 script.contains("globalCounter++"),
1100 "Script should contain globalCounter++ for COUNTER placeholder"
1101 );
1102 }
1103
1104 #[test]
1105 fn test_static_body_no_dynamic_marker() {
1106 use crate::spec_parser::ApiOperation;
1107 use openapiv3::Operation;
1108 use serde_json::json;
1109
1110 let operation = ApiOperation {
1112 method: "post".to_string(),
1113 path: "/api/resources".to_string(),
1114 operation: Operation::default(),
1115 operation_id: Some("createResource".to_string()),
1116 };
1117
1118 let template = RequestTemplate {
1119 operation,
1120 path_params: HashMap::new(),
1121 query_params: HashMap::new(),
1122 headers: HashMap::new(),
1123 body: Some(json!({
1124 "name": "static-value",
1125 "count": 42
1126 })),
1127 };
1128
1129 let config = K6Config {
1130 target_url: "https://api.example.com".to_string(),
1131 base_path: None,
1132 scenario: LoadScenario::Constant,
1133 duration_secs: 30,
1134 max_vus: 5,
1135 threshold_percentile: "p(95)".to_string(),
1136 threshold_ms: 500,
1137 max_error_rate: 0.05,
1138 auth_header: None,
1139 custom_headers: HashMap::new(),
1140 skip_tls_verify: false,
1141 security_testing_enabled: false,
1142 chunked_request_bodies: false,
1143 };
1144
1145 let generator = K6ScriptGenerator::new(config, vec![template]);
1146 let script = generator.generate().expect("Should generate script");
1147
1148 assert!(
1150 !script.contains("Dynamic body with runtime placeholders"),
1151 "Script should NOT contain dynamic body comment for static body"
1152 );
1153
1154 assert!(
1156 !script.contains("webcrypto"),
1157 "Script should NOT include webcrypto import for static body"
1158 );
1159
1160 assert!(
1162 !script.contains("let globalCounter"),
1163 "Script should NOT include globalCounter for static body"
1164 );
1165 }
1166
1167 #[test]
1168 fn test_security_testing_enabled_generates_calling_code() {
1169 use crate::spec_parser::ApiOperation;
1170 use openapiv3::Operation;
1171 use serde_json::json;
1172
1173 let operation = ApiOperation {
1174 method: "post".to_string(),
1175 path: "/api/users".to_string(),
1176 operation: Operation::default(),
1177 operation_id: Some("createUser".to_string()),
1178 };
1179
1180 let template = RequestTemplate {
1181 operation,
1182 path_params: HashMap::new(),
1183 query_params: HashMap::new(),
1184 headers: HashMap::new(),
1185 body: Some(json!({"name": "test"})),
1186 };
1187
1188 let config = K6Config {
1189 target_url: "https://api.example.com".to_string(),
1190 base_path: None,
1191 scenario: LoadScenario::Constant,
1192 duration_secs: 30,
1193 max_vus: 5,
1194 threshold_percentile: "p(95)".to_string(),
1195 threshold_ms: 500,
1196 max_error_rate: 0.05,
1197 auth_header: None,
1198 custom_headers: HashMap::new(),
1199 skip_tls_verify: false,
1200 security_testing_enabled: true,
1201 chunked_request_bodies: false,
1202 };
1203
1204 let generator = K6ScriptGenerator::new(config, vec![template]);
1205 let script = generator.generate().expect("Should generate script");
1206
1207 assert!(
1209 script.contains("getNextSecurityPayload"),
1210 "Script should contain getNextSecurityPayload() call when security_testing_enabled is true"
1211 );
1212 assert!(
1213 script.contains("applySecurityPayload"),
1214 "Script should contain applySecurityPayload() call when security_testing_enabled is true"
1215 );
1216 assert!(
1217 script.contains("secPayloadGroup"),
1218 "Script should contain secPayloadGroup variable when security_testing_enabled is true"
1219 );
1220 assert!(
1221 script.contains("secBodyPayload"),
1222 "Script should contain secBodyPayload variable when security_testing_enabled is true"
1223 );
1224 assert!(
1226 script.contains("hasSecCookie"),
1227 "Script should track hasSecCookie for CookieJar conflict avoidance"
1228 );
1229 assert!(
1230 script.contains("secRequestOpts"),
1231 "Script should use secRequestOpts to conditionally skip CookieJar"
1232 );
1233 assert!(
1235 script.contains("const requestHeaders = { ..."),
1236 "Script should spread headers into mutable copy for security payload injection"
1237 );
1238 assert!(
1240 script.contains("secPayload.injectAsPath"),
1241 "Script should check injectAsPath for path-based URI injection"
1242 );
1243 assert!(
1245 script.contains("secBodyPayload.formBody"),
1246 "Script should check formBody for form-encoded body delivery"
1247 );
1248 assert!(
1249 script.contains("application/x-www-form-urlencoded"),
1250 "Script should set Content-Type for form-encoded body"
1251 );
1252 let op_comment_pos =
1254 script.find("// Operation 0:").expect("Should have Operation 0 comment");
1255 let sec_payload_pos = script
1256 .find("const secPayloadGroup = typeof getNextSecurityPayload")
1257 .expect("Should have secPayloadGroup assignment");
1258 assert!(
1259 sec_payload_pos > op_comment_pos,
1260 "secPayloadGroup should be fetched inside operation block (per-operation), not before it (per-iteration)"
1261 );
1262 }
1263
1264 #[test]
1265 fn test_security_testing_disabled_no_calling_code() {
1266 use crate::spec_parser::ApiOperation;
1267 use openapiv3::Operation;
1268 use serde_json::json;
1269
1270 let operation = ApiOperation {
1271 method: "post".to_string(),
1272 path: "/api/users".to_string(),
1273 operation: Operation::default(),
1274 operation_id: Some("createUser".to_string()),
1275 };
1276
1277 let template = RequestTemplate {
1278 operation,
1279 path_params: HashMap::new(),
1280 query_params: HashMap::new(),
1281 headers: HashMap::new(),
1282 body: Some(json!({"name": "test"})),
1283 };
1284
1285 let config = K6Config {
1286 target_url: "https://api.example.com".to_string(),
1287 base_path: None,
1288 scenario: LoadScenario::Constant,
1289 duration_secs: 30,
1290 max_vus: 5,
1291 threshold_percentile: "p(95)".to_string(),
1292 threshold_ms: 500,
1293 max_error_rate: 0.05,
1294 auth_header: None,
1295 custom_headers: HashMap::new(),
1296 skip_tls_verify: false,
1297 security_testing_enabled: false,
1298 chunked_request_bodies: false,
1299 };
1300
1301 let generator = K6ScriptGenerator::new(config, vec![template]);
1302 let script = generator.generate().expect("Should generate script");
1303
1304 assert!(
1306 !script.contains("getNextSecurityPayload"),
1307 "Script should NOT contain getNextSecurityPayload() when security_testing_enabled is false"
1308 );
1309 assert!(
1310 !script.contains("applySecurityPayload"),
1311 "Script should NOT contain applySecurityPayload() when security_testing_enabled is false"
1312 );
1313 assert!(
1314 !script.contains("secPayloadGroup"),
1315 "Script should NOT contain secPayloadGroup variable when security_testing_enabled is false"
1316 );
1317 assert!(
1318 !script.contains("secBodyPayload"),
1319 "Script should NOT contain secBodyPayload variable when security_testing_enabled is false"
1320 );
1321 assert!(
1322 !script.contains("hasSecCookie"),
1323 "Script should NOT contain hasSecCookie when security_testing_enabled is false"
1324 );
1325 assert!(
1326 !script.contains("secRequestOpts"),
1327 "Script should NOT contain secRequestOpts when security_testing_enabled is false"
1328 );
1329 assert!(
1330 !script.contains("injectAsPath"),
1331 "Script should NOT contain injectAsPath when security_testing_enabled is false"
1332 );
1333 assert!(
1334 !script.contains("formBody"),
1335 "Script should NOT contain formBody when security_testing_enabled is false"
1336 );
1337 }
1338
1339 #[test]
1343 fn test_security_e2e_definitions_and_calls_both_present() {
1344 use crate::security_payloads::{
1345 SecurityPayloads, SecurityTestConfig, SecurityTestGenerator,
1346 };
1347 use crate::spec_parser::ApiOperation;
1348 use openapiv3::Operation;
1349 use serde_json::json;
1350
1351 let operation = ApiOperation {
1353 method: "post".to_string(),
1354 path: "/api/users".to_string(),
1355 operation: Operation::default(),
1356 operation_id: Some("createUser".to_string()),
1357 };
1358
1359 let template = RequestTemplate {
1360 operation,
1361 path_params: HashMap::new(),
1362 query_params: HashMap::new(),
1363 headers: HashMap::new(),
1364 body: Some(json!({"name": "test"})),
1365 };
1366
1367 let config = K6Config {
1368 target_url: "https://api.example.com".to_string(),
1369 base_path: None,
1370 scenario: LoadScenario::Constant,
1371 duration_secs: 30,
1372 max_vus: 5,
1373 threshold_percentile: "p(95)".to_string(),
1374 threshold_ms: 500,
1375 max_error_rate: 0.05,
1376 auth_header: None,
1377 custom_headers: HashMap::new(),
1378 skip_tls_verify: false,
1379 security_testing_enabled: true,
1380 chunked_request_bodies: false,
1381 };
1382
1383 let generator = K6ScriptGenerator::new(config, vec![template]);
1384 let mut script = generator.generate().expect("Should generate base script");
1385
1386 let security_config = SecurityTestConfig::default().enable();
1388 let payloads = SecurityPayloads::get_payloads(&security_config);
1389 assert!(!payloads.is_empty(), "Should have built-in payloads");
1390
1391 let mut additional_code = String::new();
1392 additional_code
1393 .push_str(&SecurityTestGenerator::generate_payload_selection(&payloads, false));
1394 additional_code.push('\n');
1395 additional_code.push_str(&SecurityTestGenerator::generate_apply_payload(&[]));
1396 additional_code.push('\n');
1397
1398 if let Some(pos) = script.find("export const options") {
1400 script.insert_str(
1401 pos,
1402 &format!("\n// === Advanced Testing Features ===\n{}\n", additional_code),
1403 );
1404 }
1405
1406 assert!(
1409 script.contains("function getNextSecurityPayload()"),
1410 "Final script must contain getNextSecurityPayload function DEFINITION"
1411 );
1412 assert!(
1413 script.contains("function applySecurityPayload("),
1414 "Final script must contain applySecurityPayload function DEFINITION"
1415 );
1416 assert!(
1417 script.contains("securityPayloads"),
1418 "Final script must contain securityPayloads array"
1419 );
1420
1421 assert!(
1423 script.contains("const secPayloadGroup = typeof getNextSecurityPayload"),
1424 "Final script must contain secPayloadGroup assignment (template calling code)"
1425 );
1426 assert!(
1427 script.contains("applySecurityPayload(payload, [], secBodyPayload)"),
1428 "Final script must contain applySecurityPayload CALL with secBodyPayload"
1429 );
1430 assert!(
1431 script.contains("const requestHeaders = { ..."),
1432 "Final script must spread headers for security payload header injection"
1433 );
1434 assert!(
1435 script.contains("for (const secPayload of secPayloadGroup)"),
1436 "Final script must loop over secPayloadGroup"
1437 );
1438 assert!(
1439 script.contains("secPayload.injectAsPath"),
1440 "Final script must check injectAsPath for path-based URI injection"
1441 );
1442 assert!(
1443 script.contains("secBodyPayload.formBody"),
1444 "Final script must check formBody for form-encoded body delivery"
1445 );
1446
1447 let def_pos = script.find("function getNextSecurityPayload()").unwrap();
1449 let call_pos =
1450 script.find("const secPayloadGroup = typeof getNextSecurityPayload").unwrap();
1451 let options_pos = script.find("export const options").unwrap();
1452 let default_fn_pos = script.find("export default function").unwrap();
1453
1454 assert!(
1455 def_pos < options_pos,
1456 "Function definitions must appear before export const options"
1457 );
1458 assert!(
1459 call_pos > default_fn_pos,
1460 "Calling code must appear inside export default function"
1461 );
1462 }
1463
1464 #[test]
1466 fn test_security_uri_injection_for_get_requests() {
1467 use crate::spec_parser::ApiOperation;
1468 use openapiv3::Operation;
1469
1470 let operation = ApiOperation {
1471 method: "get".to_string(),
1472 path: "/api/users".to_string(),
1473 operation: Operation::default(),
1474 operation_id: Some("listUsers".to_string()),
1475 };
1476
1477 let template = RequestTemplate {
1478 operation,
1479 path_params: HashMap::new(),
1480 query_params: HashMap::new(),
1481 headers: HashMap::new(),
1482 body: None,
1483 };
1484
1485 let config = K6Config {
1486 target_url: "https://api.example.com".to_string(),
1487 base_path: None,
1488 scenario: LoadScenario::Constant,
1489 duration_secs: 30,
1490 max_vus: 5,
1491 threshold_percentile: "p(95)".to_string(),
1492 threshold_ms: 500,
1493 max_error_rate: 0.05,
1494 auth_header: None,
1495 custom_headers: HashMap::new(),
1496 skip_tls_verify: false,
1497 security_testing_enabled: true,
1498 chunked_request_bodies: false,
1499 };
1500
1501 let generator = K6ScriptGenerator::new(config, vec![template]);
1502 let script = generator.generate().expect("Should generate script");
1503
1504 assert!(
1506 script.contains("requestUrl"),
1507 "Script should build requestUrl variable for URI payload injection"
1508 );
1509 assert!(
1510 script.contains("secPayload.location === 'uri'"),
1511 "Script should check for URI-location payloads"
1512 );
1513 assert!(
1515 script.contains("'test=' + encodeURIComponent(secPayload.payload)"),
1516 "Script should URL-encode security payload in query string for valid HTTP"
1517 );
1518 assert!(
1520 script.contains("secPayload.injectAsPath"),
1521 "Script should check injectAsPath for path-based URI injection"
1522 );
1523 assert!(
1524 script.contains("encodeURI(secPayload.payload)"),
1525 "Script should use encodeURI for path-based injection"
1526 );
1527 assert!(
1529 script.contains("http.get(requestUrl,"),
1530 "GET request should use requestUrl (with URI injection) instead of inline URL"
1531 );
1532 }
1533
1534 #[test]
1536 fn test_security_uri_injection_for_post_requests() {
1537 use crate::spec_parser::ApiOperation;
1538 use openapiv3::Operation;
1539 use serde_json::json;
1540
1541 let operation = ApiOperation {
1542 method: "post".to_string(),
1543 path: "/api/users".to_string(),
1544 operation: Operation::default(),
1545 operation_id: Some("createUser".to_string()),
1546 };
1547
1548 let template = RequestTemplate {
1549 operation,
1550 path_params: HashMap::new(),
1551 query_params: HashMap::new(),
1552 headers: HashMap::new(),
1553 body: Some(json!({"name": "test"})),
1554 };
1555
1556 let config = K6Config {
1557 target_url: "https://api.example.com".to_string(),
1558 base_path: None,
1559 scenario: LoadScenario::Constant,
1560 duration_secs: 30,
1561 max_vus: 5,
1562 threshold_percentile: "p(95)".to_string(),
1563 threshold_ms: 500,
1564 max_error_rate: 0.05,
1565 auth_header: None,
1566 custom_headers: HashMap::new(),
1567 skip_tls_verify: false,
1568 security_testing_enabled: true,
1569 chunked_request_bodies: false,
1570 };
1571
1572 let generator = K6ScriptGenerator::new(config, vec![template]);
1573 let script = generator.generate().expect("Should generate script");
1574
1575 assert!(
1577 script.contains("requestUrl"),
1578 "POST script should build requestUrl for URI payload injection"
1579 );
1580 assert!(
1581 script.contains("secPayload.location === 'uri'"),
1582 "POST script should check for URI-location payloads"
1583 );
1584 assert!(
1585 script.contains("applySecurityPayload(payload, [], secBodyPayload)"),
1586 "POST script should apply security body payload to request body"
1587 );
1588 assert!(
1590 script.contains("http.post(requestUrl,"),
1591 "POST request should use requestUrl (with URI injection) instead of inline URL"
1592 );
1593 }
1594
1595 #[test]
1597 fn test_no_uri_injection_when_security_disabled() {
1598 use crate::spec_parser::ApiOperation;
1599 use openapiv3::Operation;
1600
1601 let operation = ApiOperation {
1602 method: "get".to_string(),
1603 path: "/api/users".to_string(),
1604 operation: Operation::default(),
1605 operation_id: Some("listUsers".to_string()),
1606 };
1607
1608 let template = RequestTemplate {
1609 operation,
1610 path_params: HashMap::new(),
1611 query_params: HashMap::new(),
1612 headers: HashMap::new(),
1613 body: None,
1614 };
1615
1616 let config = K6Config {
1617 target_url: "https://api.example.com".to_string(),
1618 base_path: None,
1619 scenario: LoadScenario::Constant,
1620 duration_secs: 30,
1621 max_vus: 5,
1622 threshold_percentile: "p(95)".to_string(),
1623 threshold_ms: 500,
1624 max_error_rate: 0.05,
1625 auth_header: None,
1626 custom_headers: HashMap::new(),
1627 skip_tls_verify: false,
1628 security_testing_enabled: false,
1629 chunked_request_bodies: false,
1630 };
1631
1632 let generator = K6ScriptGenerator::new(config, vec![template]);
1633 let script = generator.generate().expect("Should generate script");
1634
1635 assert!(
1637 !script.contains("requestUrl"),
1638 "Script should NOT have requestUrl when security is disabled"
1639 );
1640 assert!(
1641 !script.contains("secPayloadGroup"),
1642 "Script should NOT have secPayloadGroup when security is disabled"
1643 );
1644 assert!(
1645 !script.contains("secBodyPayload"),
1646 "Script should NOT have secBodyPayload when security is disabled"
1647 );
1648 }
1649
1650 #[test]
1652 fn test_uses_per_request_cookie_jar() {
1653 use crate::spec_parser::ApiOperation;
1654 use openapiv3::Operation;
1655
1656 let operation = ApiOperation {
1657 method: "get".to_string(),
1658 path: "/api/users".to_string(),
1659 operation: Operation::default(),
1660 operation_id: Some("listUsers".to_string()),
1661 };
1662
1663 let template = RequestTemplate {
1664 operation,
1665 path_params: HashMap::new(),
1666 query_params: HashMap::new(),
1667 headers: HashMap::new(),
1668 body: None,
1669 };
1670
1671 let config = K6Config {
1672 target_url: "https://api.example.com".to_string(),
1673 base_path: None,
1674 scenario: LoadScenario::Constant,
1675 duration_secs: 30,
1676 max_vus: 5,
1677 threshold_percentile: "p(95)".to_string(),
1678 threshold_ms: 500,
1679 max_error_rate: 0.05,
1680 auth_header: None,
1681 custom_headers: HashMap::new(),
1682 skip_tls_verify: false,
1683 security_testing_enabled: false,
1684 chunked_request_bodies: false,
1685 };
1686
1687 let generator = K6ScriptGenerator::new(config, vec![template]);
1688 let script = generator.generate().expect("Should generate script");
1689
1690 assert!(
1692 script.contains("jar: new http.CookieJar()"),
1693 "Script should create fresh CookieJar per request"
1694 );
1695 assert!(
1696 !script.contains("jar: null"),
1697 "Script should NOT use jar: null (does not disable default VU cookie jar in k6)"
1698 );
1699 assert!(
1700 !script.contains("EMPTY_JAR"),
1701 "Script should NOT use shared EMPTY_JAR (accumulates Set-Cookie responses)"
1702 );
1703 }
1704}