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 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
378 #[test]
379 fn test_script_generation_with_dots_in_name() {
380 use crate::spec_parser::ApiOperation;
381 use openapiv3::Operation;
382
383 let operation = ApiOperation {
385 method: "get".to_string(),
386 path: "/billing/subscriptions".to_string(),
387 operation: Operation::default(),
388 operation_id: Some("billing.subscriptions.v1".to_string()),
389 };
390
391 let template = RequestTemplate {
392 operation,
393 path_params: HashMap::new(),
394 query_params: HashMap::new(),
395 headers: HashMap::new(),
396 body: None,
397 };
398
399 let config = K6Config {
400 target_url: "https://api.example.com".to_string(),
401 scenario: LoadScenario::Constant,
402 duration_secs: 30,
403 max_vus: 5,
404 threshold_percentile: "p(95)".to_string(),
405 threshold_ms: 500,
406 max_error_rate: 0.05,
407 auth_header: None,
408 custom_headers: HashMap::new(),
409 skip_tls_verify: false,
410 };
411
412 let generator = K6ScriptGenerator::new(config, vec![template]);
413 let script = generator.generate().expect("Should generate script");
414
415 assert!(
417 script.contains("const billing_subscriptions_v1_latency"),
418 "Script should contain sanitized variable name for latency"
419 );
420 assert!(
421 script.contains("const billing_subscriptions_v1_errors"),
422 "Script should contain sanitized variable name for errors"
423 );
424
425 assert!(
428 !script.contains("const billing.subscriptions"),
429 "Script should not contain variable names with dots - this would cause 'Unexpected token .' error"
430 );
431
432 assert!(
435 script.contains("'billing_subscriptions_v1_latency'"),
436 "Metric name strings should be sanitized (no dots) - k6 validation requires valid metric names"
437 );
438 assert!(
439 script.contains("'billing_subscriptions_v1_errors'"),
440 "Metric name strings should be sanitized (no dots) - k6 validation requires valid metric names"
441 );
442
443 assert!(
445 script.contains("billing.subscriptions.v1"),
446 "Script should contain original name in comments/strings for readability"
447 );
448
449 assert!(
451 script.contains("billing_subscriptions_v1_latency.add"),
452 "Variable usage should use sanitized name"
453 );
454 assert!(
455 script.contains("billing_subscriptions_v1_errors.add"),
456 "Variable usage should use sanitized name"
457 );
458 }
459
460 #[test]
461 fn test_validate_script_valid() {
462 let valid_script = r#"
463import http from 'k6/http';
464import { check, sleep } from 'k6';
465import { Rate, Trend } from 'k6/metrics';
466
467const test_latency = new Trend('test_latency');
468const test_errors = new Rate('test_errors');
469
470export default function() {
471 const res = http.get('https://example.com');
472 test_latency.add(res.timings.duration);
473 test_errors.add(res.status !== 200);
474}
475"#;
476
477 let errors = K6ScriptGenerator::validate_script(valid_script);
478 assert!(errors.is_empty(), "Valid script should have no validation errors");
479 }
480
481 #[test]
482 fn test_validate_script_invalid_metric_name() {
483 let invalid_script = r#"
484import http from 'k6/http';
485import { check, sleep } from 'k6';
486import { Rate, Trend } from 'k6/metrics';
487
488const test_latency = new Trend('test.latency');
489const test_errors = new Rate('test_errors');
490
491export default function() {
492 const res = http.get('https://example.com');
493 test_latency.add(res.timings.duration);
494}
495"#;
496
497 let errors = K6ScriptGenerator::validate_script(invalid_script);
498 assert!(
499 !errors.is_empty(),
500 "Script with invalid metric name should have validation errors"
501 );
502 assert!(
503 errors.iter().any(|e| e.contains("Invalid k6 metric name")),
504 "Should detect invalid metric name with dot"
505 );
506 }
507
508 #[test]
509 fn test_validate_script_missing_imports() {
510 let invalid_script = r#"
511const test_latency = new Trend('test_latency');
512export default function() {}
513"#;
514
515 let errors = K6ScriptGenerator::validate_script(invalid_script);
516 assert!(!errors.is_empty(), "Script missing imports should have validation errors");
517 }
518
519 #[test]
520 fn test_validate_script_metric_name_validation() {
521 let valid_script = r#"
524import http from 'k6/http';
525import { check, sleep } from 'k6';
526import { Rate, Trend } from 'k6/metrics';
527const test_latency = new Trend('test_latency');
528const test_errors = new Rate('test_errors');
529export default function() {}
530"#;
531 let errors = K6ScriptGenerator::validate_script(valid_script);
532 assert!(errors.is_empty(), "Valid metric names should pass validation");
533
534 let invalid_cases = vec![
536 ("test.latency", "dot in metric name"),
537 ("123test", "starts with number"),
538 ("test-latency", "hyphen in metric name"),
539 ("test@latency", "special character"),
540 ];
541
542 for (invalid_name, description) in invalid_cases {
543 let script = format!(
544 r#"
545import http from 'k6/http';
546import {{ check, sleep }} from 'k6';
547import {{ Rate, Trend }} from 'k6/metrics';
548const test_latency = new Trend('{}');
549export default function() {{}}
550"#,
551 invalid_name
552 );
553 let errors = K6ScriptGenerator::validate_script(&script);
554 assert!(
555 !errors.is_empty(),
556 "Metric name '{}' ({}) should fail validation",
557 invalid_name,
558 description
559 );
560 }
561 }
562
563 #[test]
564 fn test_skip_tls_verify_with_body() {
565 use crate::spec_parser::ApiOperation;
566 use openapiv3::Operation;
567 use serde_json::json;
568
569 let operation = ApiOperation {
571 method: "post".to_string(),
572 path: "/api/users".to_string(),
573 operation: Operation::default(),
574 operation_id: Some("createUser".to_string()),
575 };
576
577 let template = RequestTemplate {
578 operation,
579 path_params: HashMap::new(),
580 query_params: HashMap::new(),
581 headers: HashMap::new(),
582 body: Some(json!({"name": "test"})),
583 };
584
585 let config = K6Config {
586 target_url: "https://api.example.com".to_string(),
587 scenario: LoadScenario::Constant,
588 duration_secs: 30,
589 max_vus: 5,
590 threshold_percentile: "p(95)".to_string(),
591 threshold_ms: 500,
592 max_error_rate: 0.05,
593 auth_header: None,
594 custom_headers: HashMap::new(),
595 skip_tls_verify: true,
596 };
597
598 let generator = K6ScriptGenerator::new(config, vec![template]);
599 let script = generator.generate().expect("Should generate script");
600
601 assert!(
603 script.contains("insecureSkipTLSVerify: true"),
604 "Script should include insecureSkipTLSVerify option when skip_tls_verify is true"
605 );
606 }
607
608 #[test]
609 fn test_skip_tls_verify_without_body() {
610 use crate::spec_parser::ApiOperation;
611 use openapiv3::Operation;
612
613 let operation = ApiOperation {
615 method: "get".to_string(),
616 path: "/api/users".to_string(),
617 operation: Operation::default(),
618 operation_id: Some("getUsers".to_string()),
619 };
620
621 let template = RequestTemplate {
622 operation,
623 path_params: HashMap::new(),
624 query_params: HashMap::new(),
625 headers: HashMap::new(),
626 body: None,
627 };
628
629 let config = K6Config {
630 target_url: "https://api.example.com".to_string(),
631 scenario: LoadScenario::Constant,
632 duration_secs: 30,
633 max_vus: 5,
634 threshold_percentile: "p(95)".to_string(),
635 threshold_ms: 500,
636 max_error_rate: 0.05,
637 auth_header: None,
638 custom_headers: HashMap::new(),
639 skip_tls_verify: true,
640 };
641
642 let generator = K6ScriptGenerator::new(config, vec![template]);
643 let script = generator.generate().expect("Should generate script");
644
645 assert!(
647 script.contains("insecureSkipTLSVerify: true"),
648 "Script should include insecureSkipTLSVerify option when skip_tls_verify is true (no body)"
649 );
650 }
651
652 #[test]
653 fn test_no_skip_tls_verify() {
654 use crate::spec_parser::ApiOperation;
655 use openapiv3::Operation;
656
657 let operation = ApiOperation {
659 method: "get".to_string(),
660 path: "/api/users".to_string(),
661 operation: Operation::default(),
662 operation_id: Some("getUsers".to_string()),
663 };
664
665 let template = RequestTemplate {
666 operation,
667 path_params: HashMap::new(),
668 query_params: HashMap::new(),
669 headers: HashMap::new(),
670 body: None,
671 };
672
673 let config = K6Config {
674 target_url: "https://api.example.com".to_string(),
675 scenario: LoadScenario::Constant,
676 duration_secs: 30,
677 max_vus: 5,
678 threshold_percentile: "p(95)".to_string(),
679 threshold_ms: 500,
680 max_error_rate: 0.05,
681 auth_header: None,
682 custom_headers: HashMap::new(),
683 skip_tls_verify: false,
684 };
685
686 let generator = K6ScriptGenerator::new(config, vec![template]);
687 let script = generator.generate().expect("Should generate script");
688
689 assert!(
691 !script.contains("insecureSkipTLSVerify"),
692 "Script should NOT include insecureSkipTLSVerify option when skip_tls_verify is false"
693 );
694 }
695
696 #[test]
697 fn test_skip_tls_verify_multiple_operations() {
698 use crate::spec_parser::ApiOperation;
699 use openapiv3::Operation;
700 use serde_json::json;
701
702 let operation1 = ApiOperation {
704 method: "get".to_string(),
705 path: "/api/users".to_string(),
706 operation: Operation::default(),
707 operation_id: Some("getUsers".to_string()),
708 };
709
710 let operation2 = ApiOperation {
711 method: "post".to_string(),
712 path: "/api/users".to_string(),
713 operation: Operation::default(),
714 operation_id: Some("createUser".to_string()),
715 };
716
717 let template1 = RequestTemplate {
718 operation: operation1,
719 path_params: HashMap::new(),
720 query_params: HashMap::new(),
721 headers: HashMap::new(),
722 body: None,
723 };
724
725 let template2 = RequestTemplate {
726 operation: operation2,
727 path_params: HashMap::new(),
728 query_params: HashMap::new(),
729 headers: HashMap::new(),
730 body: Some(json!({"name": "test"})),
731 };
732
733 let config = K6Config {
734 target_url: "https://api.example.com".to_string(),
735 scenario: LoadScenario::Constant,
736 duration_secs: 30,
737 max_vus: 5,
738 threshold_percentile: "p(95)".to_string(),
739 threshold_ms: 500,
740 max_error_rate: 0.05,
741 auth_header: None,
742 custom_headers: HashMap::new(),
743 skip_tls_verify: true,
744 };
745
746 let generator = K6ScriptGenerator::new(config, vec![template1, template2]);
747 let script = generator.generate().expect("Should generate script");
748
749 let skip_count = script.matches("insecureSkipTLSVerify: true").count();
752 assert_eq!(
753 skip_count, 1,
754 "Script should include insecureSkipTLSVerify exactly once in global options (not per-request)"
755 );
756
757 let options_start = script.find("export const options = {").expect("Should have options");
759 let scenarios_start = script.find("scenarios:").expect("Should have scenarios");
760 let options_prefix = &script[options_start..scenarios_start];
761 assert!(
762 options_prefix.contains("insecureSkipTLSVerify: true"),
763 "insecureSkipTLSVerify should be in global options block"
764 );
765 }
766
767 #[test]
768 fn test_dynamic_params_in_body() {
769 use crate::spec_parser::ApiOperation;
770 use openapiv3::Operation;
771 use serde_json::json;
772
773 let operation = ApiOperation {
775 method: "post".to_string(),
776 path: "/api/resources".to_string(),
777 operation: Operation::default(),
778 operation_id: Some("createResource".to_string()),
779 };
780
781 let template = RequestTemplate {
782 operation,
783 path_params: HashMap::new(),
784 query_params: HashMap::new(),
785 headers: HashMap::new(),
786 body: Some(json!({
787 "name": "load-test-${__VU}",
788 "iteration": "${__ITER}"
789 })),
790 };
791
792 let config = K6Config {
793 target_url: "https://api.example.com".to_string(),
794 scenario: LoadScenario::Constant,
795 duration_secs: 30,
796 max_vus: 5,
797 threshold_percentile: "p(95)".to_string(),
798 threshold_ms: 500,
799 max_error_rate: 0.05,
800 auth_header: None,
801 custom_headers: HashMap::new(),
802 skip_tls_verify: false,
803 };
804
805 let generator = K6ScriptGenerator::new(config, vec![template]);
806 let script = generator.generate().expect("Should generate script");
807
808 assert!(
810 script.contains("Dynamic body with runtime placeholders"),
811 "Script should contain comment about dynamic body"
812 );
813
814 assert!(
816 script.contains("__VU"),
817 "Script should contain __VU reference for dynamic VU-based values"
818 );
819
820 assert!(
822 script.contains("__ITER"),
823 "Script should contain __ITER reference for dynamic iteration values"
824 );
825 }
826
827 #[test]
828 fn test_dynamic_params_with_uuid() {
829 use crate::spec_parser::ApiOperation;
830 use openapiv3::Operation;
831 use serde_json::json;
832
833 let operation = ApiOperation {
835 method: "post".to_string(),
836 path: "/api/resources".to_string(),
837 operation: Operation::default(),
838 operation_id: Some("createResource".to_string()),
839 };
840
841 let template = RequestTemplate {
842 operation,
843 path_params: HashMap::new(),
844 query_params: HashMap::new(),
845 headers: HashMap::new(),
846 body: Some(json!({
847 "id": "${__UUID}"
848 })),
849 };
850
851 let config = K6Config {
852 target_url: "https://api.example.com".to_string(),
853 scenario: LoadScenario::Constant,
854 duration_secs: 30,
855 max_vus: 5,
856 threshold_percentile: "p(95)".to_string(),
857 threshold_ms: 500,
858 max_error_rate: 0.05,
859 auth_header: None,
860 custom_headers: HashMap::new(),
861 skip_tls_verify: false,
862 };
863
864 let generator = K6ScriptGenerator::new(config, vec![template]);
865 let script = generator.generate().expect("Should generate script");
866
867 assert!(
869 script.contains("import { crypto }") || script.contains("webcrypto"),
870 "Script should include crypto import when UUID placeholder is used"
871 );
872
873 assert!(
875 script.contains("crypto.randomUUID()"),
876 "Script should contain crypto.randomUUID() for UUID placeholder"
877 );
878 }
879
880 #[test]
881 fn test_dynamic_params_with_counter() {
882 use crate::spec_parser::ApiOperation;
883 use openapiv3::Operation;
884 use serde_json::json;
885
886 let operation = ApiOperation {
888 method: "post".to_string(),
889 path: "/api/resources".to_string(),
890 operation: Operation::default(),
891 operation_id: Some("createResource".to_string()),
892 };
893
894 let template = RequestTemplate {
895 operation,
896 path_params: HashMap::new(),
897 query_params: HashMap::new(),
898 headers: HashMap::new(),
899 body: Some(json!({
900 "sequence": "${__COUNTER}"
901 })),
902 };
903
904 let config = K6Config {
905 target_url: "https://api.example.com".to_string(),
906 scenario: LoadScenario::Constant,
907 duration_secs: 30,
908 max_vus: 5,
909 threshold_percentile: "p(95)".to_string(),
910 threshold_ms: 500,
911 max_error_rate: 0.05,
912 auth_header: None,
913 custom_headers: HashMap::new(),
914 skip_tls_verify: false,
915 };
916
917 let generator = K6ScriptGenerator::new(config, vec![template]);
918 let script = generator.generate().expect("Should generate script");
919
920 assert!(
922 script.contains("let globalCounter = 0"),
923 "Script should include globalCounter initialization when COUNTER placeholder is used"
924 );
925
926 assert!(
928 script.contains("globalCounter++"),
929 "Script should contain globalCounter++ for COUNTER placeholder"
930 );
931 }
932
933 #[test]
934 fn test_static_body_no_dynamic_marker() {
935 use crate::spec_parser::ApiOperation;
936 use openapiv3::Operation;
937 use serde_json::json;
938
939 let operation = ApiOperation {
941 method: "post".to_string(),
942 path: "/api/resources".to_string(),
943 operation: Operation::default(),
944 operation_id: Some("createResource".to_string()),
945 };
946
947 let template = RequestTemplate {
948 operation,
949 path_params: HashMap::new(),
950 query_params: HashMap::new(),
951 headers: HashMap::new(),
952 body: Some(json!({
953 "name": "static-value",
954 "count": 42
955 })),
956 };
957
958 let config = K6Config {
959 target_url: "https://api.example.com".to_string(),
960 scenario: LoadScenario::Constant,
961 duration_secs: 30,
962 max_vus: 5,
963 threshold_percentile: "p(95)".to_string(),
964 threshold_ms: 500,
965 max_error_rate: 0.05,
966 auth_header: None,
967 custom_headers: HashMap::new(),
968 skip_tls_verify: false,
969 };
970
971 let generator = K6ScriptGenerator::new(config, vec![template]);
972 let script = generator.generate().expect("Should generate script");
973
974 assert!(
976 !script.contains("Dynamic body with runtime placeholders"),
977 "Script should NOT contain dynamic body comment for static body"
978 );
979
980 assert!(
982 !script.contains("webcrypto"),
983 "Script should NOT include webcrypto import for static body"
984 );
985
986 assert!(
988 !script.contains("let globalCounter"),
989 "Script should NOT include globalCounter for static body"
990 );
991 }
992}