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