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