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_json::{json, Value};
9use std::collections::{HashMap, HashSet};
10
11pub struct K6Config {
13 pub target_url: String,
14 pub base_path: Option<String>,
17 pub scenario: LoadScenario,
18 pub duration_secs: u64,
19 pub max_vus: u32,
20 pub threshold_percentile: String,
21 pub threshold_ms: u64,
22 pub max_error_rate: f64,
23 pub auth_header: Option<String>,
24 pub custom_headers: HashMap<String, String>,
25 pub skip_tls_verify: bool,
26 pub security_testing_enabled: bool,
28}
29
30pub struct K6ScriptGenerator {
32 config: K6Config,
33 templates: Vec<RequestTemplate>,
34}
35
36impl K6ScriptGenerator {
37 pub fn new(config: K6Config, templates: Vec<RequestTemplate>) -> Self {
39 Self { config, templates }
40 }
41
42 pub fn generate(&self) -> Result<String> {
44 let handlebars = Handlebars::new();
45
46 let template = include_str!("templates/k6_script.hbs");
47
48 let data = self.build_template_data()?;
49
50 handlebars
51 .render_template(template, &data)
52 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))
53 }
54
55 pub fn sanitize_js_identifier(name: &str) -> String {
65 let mut result = String::new();
66 let mut chars = name.chars().peekable();
67
68 if let Some(&first) = chars.peek() {
70 if first.is_ascii_digit() {
71 result.push('_');
72 }
73 }
74
75 for ch in chars {
76 if ch.is_ascii_alphanumeric() || ch == '_' {
77 result.push(ch);
78 } else {
79 if !result.ends_with('_') {
82 result.push('_');
83 }
84 }
85 }
86
87 result = result.trim_end_matches('_').to_string();
89
90 if result.is_empty() {
92 result = "operation".to_string();
93 }
94
95 result
96 }
97
98 fn build_template_data(&self) -> Result<Value> {
100 let stages = self
101 .config
102 .scenario
103 .generate_stages(self.config.duration_secs, self.config.max_vus);
104
105 let base_path = self.config.base_path.as_deref().unwrap_or("");
107
108 let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
110
111 let operations = self
112 .templates
113 .iter()
114 .enumerate()
115 .map(|(idx, template)| {
116 let display_name = template.operation.display_name();
117 let sanitized_name = Self::sanitize_js_identifier(&display_name);
118 let metric_name = sanitized_name.clone();
121 let k6_method = match template.operation.method.to_lowercase().as_str() {
123 "delete" => "del".to_string(),
124 m => m.to_string(),
125 };
126 let is_get_or_head = matches!(k6_method.as_str(), "get" | "head");
129
130 let raw_path = template.generate_path();
133 let full_path = if base_path.is_empty() {
134 raw_path
135 } else {
136 format!("{}{}", base_path, raw_path)
137 };
138 let processed_path = DynamicParamProcessor::process_path(&full_path);
139 all_placeholders.extend(processed_path.placeholders.clone());
140
141 let (body_value, body_is_dynamic) = if let Some(body) = &template.body {
143 let processed_body = DynamicParamProcessor::process_json_body(body);
144 all_placeholders.extend(processed_body.placeholders.clone());
145 (Some(processed_body.value), processed_body.is_dynamic)
146 } else {
147 (None, false)
148 };
149
150 json!({
151 "index": idx,
152 "name": sanitized_name, "metric_name": metric_name, "display_name": display_name, "method": k6_method, "path": if processed_path.is_dynamic { processed_path.value } else { full_path },
157 "path_is_dynamic": processed_path.is_dynamic,
158 "headers": self.build_headers_json(template), "body": body_value,
160 "body_is_dynamic": body_is_dynamic,
161 "has_body": template.body.is_some(),
162 "is_get_or_head": is_get_or_head, })
164 })
165 .collect::<Vec<_>>();
166
167 let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
169 let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
170 let has_dynamic_values = !all_placeholders.is_empty();
171
172 Ok(json!({
173 "base_url": self.config.target_url,
174 "stages": stages.iter().map(|s| json!({
175 "duration": s.duration,
176 "target": s.target,
177 })).collect::<Vec<_>>(),
178 "operations": operations,
179 "threshold_percentile": self.config.threshold_percentile,
180 "threshold_ms": self.config.threshold_ms,
181 "max_error_rate": self.config.max_error_rate,
182 "scenario_name": format!("{:?}", self.config.scenario).to_lowercase(),
183 "skip_tls_verify": self.config.skip_tls_verify,
184 "has_dynamic_values": has_dynamic_values,
185 "dynamic_imports": required_imports,
186 "dynamic_globals": required_globals,
187 "security_testing_enabled": self.config.security_testing_enabled,
188 }))
189 }
190
191 fn build_headers_json(&self, template: &RequestTemplate) -> String {
193 let mut headers = template.get_headers();
194
195 if let Some(auth) = &self.config.auth_header {
197 headers.insert("Authorization".to_string(), auth.clone());
198 }
199
200 for (key, value) in &self.config.custom_headers {
202 headers.insert(key.clone(), value.clone());
203 }
204
205 serde_json::to_string(&headers).unwrap_or_else(|_| "{}".to_string())
207 }
208
209 pub fn validate_script(script: &str) -> Vec<String> {
218 let mut errors = Vec::new();
219
220 if !script.contains("import http from 'k6/http'") {
222 errors.push("Missing required import: 'k6/http'".to_string());
223 }
224 if !script.contains("import { check") && !script.contains("import {check") {
225 errors.push("Missing required import: 'check' from 'k6'".to_string());
226 }
227 if !script.contains("import { Rate, Trend") && !script.contains("import {Rate, Trend") {
228 errors.push("Missing required import: 'Rate, Trend' from 'k6/metrics'".to_string());
229 }
230
231 let lines: Vec<&str> = script.lines().collect();
235 for (line_num, line) in lines.iter().enumerate() {
236 let trimmed = line.trim();
237
238 if trimmed.contains("new Trend(") || trimmed.contains("new Rate(") {
240 if let Some(start) = trimmed.find('\'') {
243 if let Some(end) = trimmed[start + 1..].find('\'') {
244 let metric_name = &trimmed[start + 1..start + 1 + end];
245 if !Self::is_valid_k6_metric_name(metric_name) {
246 errors.push(format!(
247 "Line {}: Invalid k6 metric name '{}'. Metric names must only contain ASCII letters, numbers, or underscores and start with a letter or underscore.",
248 line_num + 1,
249 metric_name
250 ));
251 }
252 }
253 } else if let Some(start) = trimmed.find('"') {
254 if let Some(end) = trimmed[start + 1..].find('"') {
255 let metric_name = &trimmed[start + 1..start + 1 + end];
256 if !Self::is_valid_k6_metric_name(metric_name) {
257 errors.push(format!(
258 "Line {}: Invalid k6 metric name '{}'. Metric names must only contain ASCII letters, numbers, or underscores and start with a letter or underscore.",
259 line_num + 1,
260 metric_name
261 ));
262 }
263 }
264 }
265 }
266
267 if trimmed.starts_with("const ") || trimmed.starts_with("let ") {
269 if let Some(equals_pos) = trimmed.find('=') {
270 let var_decl = &trimmed[..equals_pos];
271 if var_decl.contains('.')
274 && !var_decl.contains("'")
275 && !var_decl.contains("\"")
276 && !var_decl.trim().starts_with("//")
277 {
278 errors.push(format!(
279 "Line {}: Invalid JavaScript variable name with dot: {}. Variable names cannot contain dots.",
280 line_num + 1,
281 var_decl.trim()
282 ));
283 }
284 }
285 }
286 }
287
288 errors
289 }
290
291 fn is_valid_k6_metric_name(name: &str) -> bool {
298 if name.is_empty() || name.len() > 128 {
299 return false;
300 }
301
302 let mut chars = name.chars();
303
304 if let Some(first) = chars.next() {
306 if !first.is_ascii_alphabetic() && first != '_' {
307 return false;
308 }
309 }
310
311 for ch in chars {
313 if !ch.is_ascii_alphanumeric() && ch != '_' {
314 return false;
315 }
316 }
317
318 true
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_k6_config_creation() {
328 let config = K6Config {
329 target_url: "https://api.example.com".to_string(),
330 base_path: None,
331 scenario: LoadScenario::RampUp,
332 duration_secs: 60,
333 max_vus: 10,
334 threshold_percentile: "p(95)".to_string(),
335 threshold_ms: 500,
336 max_error_rate: 0.05,
337 auth_header: None,
338 custom_headers: HashMap::new(),
339 skip_tls_verify: false,
340 security_testing_enabled: false,
341 };
342
343 assert_eq!(config.duration_secs, 60);
344 assert_eq!(config.max_vus, 10);
345 }
346
347 #[test]
348 fn test_script_generator_creation() {
349 let config = K6Config {
350 target_url: "https://api.example.com".to_string(),
351 base_path: None,
352 scenario: LoadScenario::Constant,
353 duration_secs: 30,
354 max_vus: 5,
355 threshold_percentile: "p(95)".to_string(),
356 threshold_ms: 500,
357 max_error_rate: 0.05,
358 auth_header: None,
359 custom_headers: HashMap::new(),
360 skip_tls_verify: false,
361 security_testing_enabled: false,
362 };
363
364 let templates = vec![];
365 let generator = K6ScriptGenerator::new(config, templates);
366
367 assert_eq!(generator.templates.len(), 0);
368 }
369
370 #[test]
371 fn test_sanitize_js_identifier() {
372 assert_eq!(
374 K6ScriptGenerator::sanitize_js_identifier("billing.subscriptions.v1"),
375 "billing_subscriptions_v1"
376 );
377
378 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("get user"), "get_user");
380
381 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("123invalid"), "_123invalid");
383
384 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("getUsers"), "getUsers");
386
387 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("test...name"), "test_name");
389
390 assert_eq!(K6ScriptGenerator::sanitize_js_identifier(""), "operation");
392
393 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("test@name#value"), "test_name_value");
395
396 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("plans.list"), "plans_list");
398 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("plans.create"), "plans_create");
399 assert_eq!(
400 K6ScriptGenerator::sanitize_js_identifier("plans.update-pricing-schemes"),
401 "plans_update_pricing_schemes"
402 );
403 assert_eq!(K6ScriptGenerator::sanitize_js_identifier("users CRUD"), "users_CRUD");
404 }
405
406 #[test]
407 fn test_script_generation_with_dots_in_name() {
408 use crate::spec_parser::ApiOperation;
409 use openapiv3::Operation;
410
411 let operation = ApiOperation {
413 method: "get".to_string(),
414 path: "/billing/subscriptions".to_string(),
415 operation: Operation::default(),
416 operation_id: Some("billing.subscriptions.v1".to_string()),
417 };
418
419 let template = RequestTemplate {
420 operation,
421 path_params: HashMap::new(),
422 query_params: HashMap::new(),
423 headers: HashMap::new(),
424 body: None,
425 };
426
427 let config = K6Config {
428 target_url: "https://api.example.com".to_string(),
429 base_path: None,
430 scenario: LoadScenario::Constant,
431 duration_secs: 30,
432 max_vus: 5,
433 threshold_percentile: "p(95)".to_string(),
434 threshold_ms: 500,
435 max_error_rate: 0.05,
436 auth_header: None,
437 custom_headers: HashMap::new(),
438 skip_tls_verify: false,
439 security_testing_enabled: false,
440 };
441
442 let generator = K6ScriptGenerator::new(config, vec![template]);
443 let script = generator.generate().expect("Should generate script");
444
445 assert!(
447 script.contains("const billing_subscriptions_v1_latency"),
448 "Script should contain sanitized variable name for latency"
449 );
450 assert!(
451 script.contains("const billing_subscriptions_v1_errors"),
452 "Script should contain sanitized variable name for errors"
453 );
454
455 assert!(
458 !script.contains("const billing.subscriptions"),
459 "Script should not contain variable names with dots - this would cause 'Unexpected token .' error"
460 );
461
462 assert!(
465 script.contains("'billing_subscriptions_v1_latency'"),
466 "Metric name strings should be sanitized (no dots) - k6 validation requires valid metric names"
467 );
468 assert!(
469 script.contains("'billing_subscriptions_v1_errors'"),
470 "Metric name strings should be sanitized (no dots) - k6 validation requires valid metric names"
471 );
472
473 assert!(
475 script.contains("billing.subscriptions.v1"),
476 "Script should contain original name in comments/strings for readability"
477 );
478
479 assert!(
481 script.contains("billing_subscriptions_v1_latency.add"),
482 "Variable usage should use sanitized name"
483 );
484 assert!(
485 script.contains("billing_subscriptions_v1_errors.add"),
486 "Variable usage should use sanitized name"
487 );
488 }
489
490 #[test]
491 fn test_validate_script_valid() {
492 let valid_script = r#"
493import http from 'k6/http';
494import { check, sleep } from 'k6';
495import { Rate, Trend } from 'k6/metrics';
496
497const test_latency = new Trend('test_latency');
498const test_errors = new Rate('test_errors');
499
500export default function() {
501 const res = http.get('https://example.com');
502 test_latency.add(res.timings.duration);
503 test_errors.add(res.status !== 200);
504}
505"#;
506
507 let errors = K6ScriptGenerator::validate_script(valid_script);
508 assert!(errors.is_empty(), "Valid script should have no validation errors");
509 }
510
511 #[test]
512 fn test_validate_script_invalid_metric_name() {
513 let invalid_script = r#"
514import http from 'k6/http';
515import { check, sleep } from 'k6';
516import { Rate, Trend } from 'k6/metrics';
517
518const test_latency = new Trend('test.latency');
519const test_errors = new Rate('test_errors');
520
521export default function() {
522 const res = http.get('https://example.com');
523 test_latency.add(res.timings.duration);
524}
525"#;
526
527 let errors = K6ScriptGenerator::validate_script(invalid_script);
528 assert!(
529 !errors.is_empty(),
530 "Script with invalid metric name should have validation errors"
531 );
532 assert!(
533 errors.iter().any(|e| e.contains("Invalid k6 metric name")),
534 "Should detect invalid metric name with dot"
535 );
536 }
537
538 #[test]
539 fn test_validate_script_missing_imports() {
540 let invalid_script = r#"
541const test_latency = new Trend('test_latency');
542export default function() {}
543"#;
544
545 let errors = K6ScriptGenerator::validate_script(invalid_script);
546 assert!(!errors.is_empty(), "Script missing imports should have validation errors");
547 }
548
549 #[test]
550 fn test_validate_script_metric_name_validation() {
551 let valid_script = r#"
554import http from 'k6/http';
555import { check, sleep } from 'k6';
556import { Rate, Trend } from 'k6/metrics';
557const test_latency = new Trend('test_latency');
558const test_errors = new Rate('test_errors');
559export default function() {}
560"#;
561 let errors = K6ScriptGenerator::validate_script(valid_script);
562 assert!(errors.is_empty(), "Valid metric names should pass validation");
563
564 let invalid_cases = vec![
566 ("test.latency", "dot in metric name"),
567 ("123test", "starts with number"),
568 ("test-latency", "hyphen in metric name"),
569 ("test@latency", "special character"),
570 ];
571
572 for (invalid_name, description) in invalid_cases {
573 let script = format!(
574 r#"
575import http from 'k6/http';
576import {{ check, sleep }} from 'k6';
577import {{ Rate, Trend }} from 'k6/metrics';
578const test_latency = new Trend('{}');
579export default function() {{}}
580"#,
581 invalid_name
582 );
583 let errors = K6ScriptGenerator::validate_script(&script);
584 assert!(
585 !errors.is_empty(),
586 "Metric name '{}' ({}) should fail validation",
587 invalid_name,
588 description
589 );
590 }
591 }
592
593 #[test]
594 fn test_skip_tls_verify_with_body() {
595 use crate::spec_parser::ApiOperation;
596 use openapiv3::Operation;
597 use serde_json::json;
598
599 let operation = ApiOperation {
601 method: "post".to_string(),
602 path: "/api/users".to_string(),
603 operation: Operation::default(),
604 operation_id: Some("createUser".to_string()),
605 };
606
607 let template = RequestTemplate {
608 operation,
609 path_params: HashMap::new(),
610 query_params: HashMap::new(),
611 headers: HashMap::new(),
612 body: Some(json!({"name": "test"})),
613 };
614
615 let config = K6Config {
616 target_url: "https://api.example.com".to_string(),
617 base_path: None,
618 scenario: LoadScenario::Constant,
619 duration_secs: 30,
620 max_vus: 5,
621 threshold_percentile: "p(95)".to_string(),
622 threshold_ms: 500,
623 max_error_rate: 0.05,
624 auth_header: None,
625 custom_headers: HashMap::new(),
626 skip_tls_verify: true,
627 security_testing_enabled: false,
628 };
629
630 let generator = K6ScriptGenerator::new(config, vec![template]);
631 let script = generator.generate().expect("Should generate script");
632
633 assert!(
635 script.contains("insecureSkipTLSVerify: true"),
636 "Script should include insecureSkipTLSVerify option when skip_tls_verify is true"
637 );
638 }
639
640 #[test]
641 fn test_skip_tls_verify_without_body() {
642 use crate::spec_parser::ApiOperation;
643 use openapiv3::Operation;
644
645 let operation = ApiOperation {
647 method: "get".to_string(),
648 path: "/api/users".to_string(),
649 operation: Operation::default(),
650 operation_id: Some("getUsers".to_string()),
651 };
652
653 let template = RequestTemplate {
654 operation,
655 path_params: HashMap::new(),
656 query_params: HashMap::new(),
657 headers: HashMap::new(),
658 body: None,
659 };
660
661 let config = K6Config {
662 target_url: "https://api.example.com".to_string(),
663 base_path: None,
664 scenario: LoadScenario::Constant,
665 duration_secs: 30,
666 max_vus: 5,
667 threshold_percentile: "p(95)".to_string(),
668 threshold_ms: 500,
669 max_error_rate: 0.05,
670 auth_header: None,
671 custom_headers: HashMap::new(),
672 skip_tls_verify: true,
673 security_testing_enabled: false,
674 };
675
676 let generator = K6ScriptGenerator::new(config, vec![template]);
677 let script = generator.generate().expect("Should generate script");
678
679 assert!(
681 script.contains("insecureSkipTLSVerify: true"),
682 "Script should include insecureSkipTLSVerify option when skip_tls_verify is true (no body)"
683 );
684 }
685
686 #[test]
687 fn test_no_skip_tls_verify() {
688 use crate::spec_parser::ApiOperation;
689 use openapiv3::Operation;
690
691 let operation = ApiOperation {
693 method: "get".to_string(),
694 path: "/api/users".to_string(),
695 operation: Operation::default(),
696 operation_id: Some("getUsers".to_string()),
697 };
698
699 let template = RequestTemplate {
700 operation,
701 path_params: HashMap::new(),
702 query_params: HashMap::new(),
703 headers: HashMap::new(),
704 body: None,
705 };
706
707 let config = K6Config {
708 target_url: "https://api.example.com".to_string(),
709 base_path: None,
710 scenario: LoadScenario::Constant,
711 duration_secs: 30,
712 max_vus: 5,
713 threshold_percentile: "p(95)".to_string(),
714 threshold_ms: 500,
715 max_error_rate: 0.05,
716 auth_header: None,
717 custom_headers: HashMap::new(),
718 skip_tls_verify: false,
719 security_testing_enabled: false,
720 };
721
722 let generator = K6ScriptGenerator::new(config, vec![template]);
723 let script = generator.generate().expect("Should generate script");
724
725 assert!(
727 !script.contains("insecureSkipTLSVerify"),
728 "Script should NOT include insecureSkipTLSVerify option when skip_tls_verify is false"
729 );
730 }
731
732 #[test]
733 fn test_skip_tls_verify_multiple_operations() {
734 use crate::spec_parser::ApiOperation;
735 use openapiv3::Operation;
736 use serde_json::json;
737
738 let operation1 = ApiOperation {
740 method: "get".to_string(),
741 path: "/api/users".to_string(),
742 operation: Operation::default(),
743 operation_id: Some("getUsers".to_string()),
744 };
745
746 let operation2 = ApiOperation {
747 method: "post".to_string(),
748 path: "/api/users".to_string(),
749 operation: Operation::default(),
750 operation_id: Some("createUser".to_string()),
751 };
752
753 let template1 = RequestTemplate {
754 operation: operation1,
755 path_params: HashMap::new(),
756 query_params: HashMap::new(),
757 headers: HashMap::new(),
758 body: None,
759 };
760
761 let template2 = RequestTemplate {
762 operation: operation2,
763 path_params: HashMap::new(),
764 query_params: HashMap::new(),
765 headers: HashMap::new(),
766 body: Some(json!({"name": "test"})),
767 };
768
769 let config = K6Config {
770 target_url: "https://api.example.com".to_string(),
771 base_path: None,
772 scenario: LoadScenario::Constant,
773 duration_secs: 30,
774 max_vus: 5,
775 threshold_percentile: "p(95)".to_string(),
776 threshold_ms: 500,
777 max_error_rate: 0.05,
778 auth_header: None,
779 custom_headers: HashMap::new(),
780 skip_tls_verify: true,
781 security_testing_enabled: false,
782 };
783
784 let generator = K6ScriptGenerator::new(config, vec![template1, template2]);
785 let script = generator.generate().expect("Should generate script");
786
787 let skip_count = script.matches("insecureSkipTLSVerify: true").count();
790 assert_eq!(
791 skip_count, 1,
792 "Script should include insecureSkipTLSVerify exactly once in global options (not per-request)"
793 );
794
795 let options_start = script.find("export const options = {").expect("Should have options");
797 let scenarios_start = script.find("scenarios:").expect("Should have scenarios");
798 let options_prefix = &script[options_start..scenarios_start];
799 assert!(
800 options_prefix.contains("insecureSkipTLSVerify: true"),
801 "insecureSkipTLSVerify should be in global options block"
802 );
803 }
804
805 #[test]
806 fn test_dynamic_params_in_body() {
807 use crate::spec_parser::ApiOperation;
808 use openapiv3::Operation;
809 use serde_json::json;
810
811 let operation = ApiOperation {
813 method: "post".to_string(),
814 path: "/api/resources".to_string(),
815 operation: Operation::default(),
816 operation_id: Some("createResource".to_string()),
817 };
818
819 let template = RequestTemplate {
820 operation,
821 path_params: HashMap::new(),
822 query_params: HashMap::new(),
823 headers: HashMap::new(),
824 body: Some(json!({
825 "name": "load-test-${__VU}",
826 "iteration": "${__ITER}"
827 })),
828 };
829
830 let config = K6Config {
831 target_url: "https://api.example.com".to_string(),
832 base_path: None,
833 scenario: LoadScenario::Constant,
834 duration_secs: 30,
835 max_vus: 5,
836 threshold_percentile: "p(95)".to_string(),
837 threshold_ms: 500,
838 max_error_rate: 0.05,
839 auth_header: None,
840 custom_headers: HashMap::new(),
841 skip_tls_verify: false,
842 security_testing_enabled: false,
843 };
844
845 let generator = K6ScriptGenerator::new(config, vec![template]);
846 let script = generator.generate().expect("Should generate script");
847
848 assert!(
850 script.contains("Dynamic body with runtime placeholders"),
851 "Script should contain comment about dynamic body"
852 );
853
854 assert!(
856 script.contains("__VU"),
857 "Script should contain __VU reference for dynamic VU-based values"
858 );
859
860 assert!(
862 script.contains("__ITER"),
863 "Script should contain __ITER reference for dynamic iteration values"
864 );
865 }
866
867 #[test]
868 fn test_dynamic_params_with_uuid() {
869 use crate::spec_parser::ApiOperation;
870 use openapiv3::Operation;
871 use serde_json::json;
872
873 let operation = ApiOperation {
875 method: "post".to_string(),
876 path: "/api/resources".to_string(),
877 operation: Operation::default(),
878 operation_id: Some("createResource".to_string()),
879 };
880
881 let template = RequestTemplate {
882 operation,
883 path_params: HashMap::new(),
884 query_params: HashMap::new(),
885 headers: HashMap::new(),
886 body: Some(json!({
887 "id": "${__UUID}"
888 })),
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: false,
903 security_testing_enabled: false,
904 };
905
906 let generator = K6ScriptGenerator::new(config, vec![template]);
907 let script = generator.generate().expect("Should generate script");
908
909 assert!(
912 !script.contains("k6/experimental/webcrypto"),
913 "Script should NOT include deprecated k6/experimental/webcrypto import"
914 );
915
916 assert!(
918 script.contains("crypto.randomUUID()"),
919 "Script should contain crypto.randomUUID() for UUID placeholder"
920 );
921 }
922
923 #[test]
924 fn test_dynamic_params_with_counter() {
925 use crate::spec_parser::ApiOperation;
926 use openapiv3::Operation;
927 use serde_json::json;
928
929 let operation = ApiOperation {
931 method: "post".to_string(),
932 path: "/api/resources".to_string(),
933 operation: Operation::default(),
934 operation_id: Some("createResource".to_string()),
935 };
936
937 let template = RequestTemplate {
938 operation,
939 path_params: HashMap::new(),
940 query_params: HashMap::new(),
941 headers: HashMap::new(),
942 body: Some(json!({
943 "sequence": "${__COUNTER}"
944 })),
945 };
946
947 let config = K6Config {
948 target_url: "https://api.example.com".to_string(),
949 base_path: None,
950 scenario: LoadScenario::Constant,
951 duration_secs: 30,
952 max_vus: 5,
953 threshold_percentile: "p(95)".to_string(),
954 threshold_ms: 500,
955 max_error_rate: 0.05,
956 auth_header: None,
957 custom_headers: HashMap::new(),
958 skip_tls_verify: false,
959 security_testing_enabled: false,
960 };
961
962 let generator = K6ScriptGenerator::new(config, vec![template]);
963 let script = generator.generate().expect("Should generate script");
964
965 assert!(
967 script.contains("let globalCounter = 0"),
968 "Script should include globalCounter initialization when COUNTER placeholder is used"
969 );
970
971 assert!(
973 script.contains("globalCounter++"),
974 "Script should contain globalCounter++ for COUNTER placeholder"
975 );
976 }
977
978 #[test]
979 fn test_static_body_no_dynamic_marker() {
980 use crate::spec_parser::ApiOperation;
981 use openapiv3::Operation;
982 use serde_json::json;
983
984 let operation = ApiOperation {
986 method: "post".to_string(),
987 path: "/api/resources".to_string(),
988 operation: Operation::default(),
989 operation_id: Some("createResource".to_string()),
990 };
991
992 let template = RequestTemplate {
993 operation,
994 path_params: HashMap::new(),
995 query_params: HashMap::new(),
996 headers: HashMap::new(),
997 body: Some(json!({
998 "name": "static-value",
999 "count": 42
1000 })),
1001 };
1002
1003 let config = K6Config {
1004 target_url: "https://api.example.com".to_string(),
1005 base_path: None,
1006 scenario: LoadScenario::Constant,
1007 duration_secs: 30,
1008 max_vus: 5,
1009 threshold_percentile: "p(95)".to_string(),
1010 threshold_ms: 500,
1011 max_error_rate: 0.05,
1012 auth_header: None,
1013 custom_headers: HashMap::new(),
1014 skip_tls_verify: false,
1015 security_testing_enabled: false,
1016 };
1017
1018 let generator = K6ScriptGenerator::new(config, vec![template]);
1019 let script = generator.generate().expect("Should generate script");
1020
1021 assert!(
1023 !script.contains("Dynamic body with runtime placeholders"),
1024 "Script should NOT contain dynamic body comment for static body"
1025 );
1026
1027 assert!(
1029 !script.contains("webcrypto"),
1030 "Script should NOT include webcrypto import for static body"
1031 );
1032
1033 assert!(
1035 !script.contains("let globalCounter"),
1036 "Script should NOT include globalCounter for static body"
1037 );
1038 }
1039}