1use crate::error::{BenchError, Result};
4use std::path::{Path, PathBuf};
5
6#[derive(Default)]
8pub struct ConformanceConfig {
9 pub target_url: String,
11 pub api_key: Option<String>,
13 pub basic_auth: Option<String>,
15 pub skip_tls_verify: bool,
17 pub categories: Option<Vec<String>>,
19 pub base_path: Option<String>,
21 pub custom_headers: Vec<(String, String)>,
25 pub output_dir: Option<PathBuf>,
29 pub all_operations: bool,
32}
33
34impl ConformanceConfig {
35 pub fn should_include_category(&self, category: &str) -> bool {
37 match &self.categories {
38 None => true,
39 Some(cats) => cats.iter().any(|c| c.eq_ignore_ascii_case(category)),
40 }
41 }
42
43 pub fn has_custom_headers(&self) -> bool {
45 !self.custom_headers.is_empty()
46 }
47
48 pub fn has_cookie_header(&self) -> bool {
52 self.custom_headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("cookie"))
53 }
54
55 pub fn custom_headers_js_object(&self) -> String {
57 let entries: Vec<String> = self
58 .custom_headers
59 .iter()
60 .map(|(k, v)| format!("'{}': '{}'", k, v.replace('\'', "\\'")))
61 .collect();
62 format!("{{ {} }}", entries.join(", "))
63 }
64
65 pub fn effective_base_url(&self) -> String {
68 match &self.base_path {
69 None => self.target_url.clone(),
70 Some(bp) if bp.is_empty() => self.target_url.clone(),
71 Some(bp) => {
72 let url = self.target_url.trim_end_matches('/');
73 let path = if bp.starts_with('/') {
74 bp.as_str()
75 } else {
76 return format!("{}/{}", url, bp);
77 };
78 format!("{}{}", url, path)
79 }
80 }
81 }
82}
83
84pub struct ConformanceGenerator {
86 config: ConformanceConfig,
87}
88
89impl ConformanceGenerator {
90 pub fn new(config: ConformanceConfig) -> Self {
91 Self { config }
92 }
93
94 pub fn generate(&self) -> Result<String> {
96 let mut script = String::with_capacity(16384);
97
98 script.push_str("import http from 'k6/http';\n");
100 script.push_str("import { check, group } from 'k6';\n\n");
101
102 script.push_str("export const options = {\n");
104 script.push_str(" vus: 1,\n");
105 script.push_str(" iterations: 1,\n");
106 if self.config.skip_tls_verify {
107 script.push_str(" insecureSkipTLSVerify: true,\n");
108 }
109 script.push_str(" thresholds: {\n");
110 script.push_str(" checks: ['rate>0'],\n");
111 script.push_str(" },\n");
112 script.push_str("};\n\n");
113
114 script.push_str(&format!("const BASE_URL = '{}';\n\n", self.config.effective_base_url()));
116
117 script.push_str("const JSON_HEADERS = { 'Content-Type': 'application/json' };\n\n");
119
120 script.push_str("export default function () {\n");
122
123 if self.config.has_cookie_header() {
124 script.push_str(
125 " // Clear cookie jar to prevent server Set-Cookie from duplicating custom Cookie header\n",
126 );
127 script.push_str(" http.cookieJar().clear(BASE_URL);\n\n");
128 }
129
130 if self.config.should_include_category("Parameters") {
131 self.generate_parameters_group(&mut script);
132 }
133 if self.config.should_include_category("Request Bodies") {
134 self.generate_request_bodies_group(&mut script);
135 }
136 if self.config.should_include_category("Schema Types") {
137 self.generate_schema_types_group(&mut script);
138 }
139 if self.config.should_include_category("Composition") {
140 self.generate_composition_group(&mut script);
141 }
142 if self.config.should_include_category("String Formats") {
143 self.generate_string_formats_group(&mut script);
144 }
145 if self.config.should_include_category("Constraints") {
146 self.generate_constraints_group(&mut script);
147 }
148 if self.config.should_include_category("Response Codes") {
149 self.generate_response_codes_group(&mut script);
150 }
151 if self.config.should_include_category("HTTP Methods") {
152 self.generate_http_methods_group(&mut script);
153 }
154 if self.config.should_include_category("Content Types") {
155 self.generate_content_negotiation_group(&mut script);
156 }
157 if self.config.should_include_category("Security") {
158 self.generate_security_group(&mut script);
159 }
160
161 script.push_str("}\n\n");
162
163 self.generate_handle_summary(&mut script);
165
166 Ok(script)
167 }
168
169 pub fn write_script(&self, path: &Path) -> Result<()> {
171 let script = self.generate()?;
172 if let Some(parent) = path.parent() {
173 std::fs::create_dir_all(parent)?;
174 }
175 std::fs::write(path, script)
176 .map_err(|e| BenchError::Other(format!("Failed to write conformance script: {}", e)))
177 }
178
179 fn merge_with_custom_headers(&self, headers_expr: &str) -> String {
183 if self.config.has_custom_headers() {
184 format!(
185 "Object.assign({{}}, {}, {})",
186 headers_expr,
187 self.config.custom_headers_js_object()
188 )
189 } else {
190 headers_expr.to_string()
191 }
192 }
193
194 fn emit_get(&self, script: &mut String, url: &str, extra_headers: Option<&str>) {
196 let has_custom = self.config.has_custom_headers();
197 let custom_obj = self.config.custom_headers_js_object();
198 match (extra_headers, has_custom) {
199 (None, false) => {
200 script.push_str(&format!(" let res = http.get(`{}`);\n", url));
201 }
202 (None, true) => {
203 script.push_str(&format!(
204 " let res = http.get(`{}`, {{ headers: {} }});\n",
205 url, custom_obj
206 ));
207 }
208 (Some(hdrs), false) => {
209 script.push_str(&format!(
210 " let res = http.get(`{}`, {{ headers: {} }});\n",
211 url, hdrs
212 ));
213 }
214 (Some(hdrs), true) => {
215 script.push_str(&format!(
216 " let res = http.get(`{}`, {{ headers: Object.assign({{}}, {}, {}) }});\n",
217 url, hdrs, custom_obj
218 ));
219 }
220 }
221 self.maybe_clear_cookie_jar(script);
222 }
223
224 fn emit_post_like(
226 &self,
227 script: &mut String,
228 method: &str,
229 url: &str,
230 body: &str,
231 headers_expr: &str,
232 ) {
233 let merged = self.merge_with_custom_headers(headers_expr);
234 script.push_str(&format!(
235 " let res = http.{}(`{}`, {}, {{ headers: {} }});\n",
236 method, url, body, merged
237 ));
238 self.maybe_clear_cookie_jar(script);
239 }
240
241 fn emit_no_body(&self, script: &mut String, method: &str, url: &str) {
243 if self.config.has_custom_headers() {
244 script.push_str(&format!(
245 " let res = http.{}(`{}`, {{ headers: {} }});\n",
246 method,
247 url,
248 self.config.custom_headers_js_object()
249 ));
250 } else {
251 script.push_str(&format!(" let res = http.{}(`{}`);\n", method, url));
252 }
253 self.maybe_clear_cookie_jar(script);
254 }
255
256 fn maybe_clear_cookie_jar(&self, script: &mut String) {
260 if self.config.has_cookie_header() {
261 script.push_str(" http.cookieJar().clear(BASE_URL);\n");
262 }
263 }
264
265 fn generate_parameters_group(&self, script: &mut String) {
266 script.push_str(" group('Parameters', function () {\n");
267
268 script.push_str(" {\n");
270 self.emit_get(script, "${BASE_URL}/conformance/params/hello", None);
271 script.push_str(
272 " check(res, { 'param:path:string': (r) => r.status >= 200 && r.status < 500 });\n",
273 );
274 script.push_str(" }\n");
275
276 script.push_str(" {\n");
278 self.emit_get(script, "${BASE_URL}/conformance/params/42", None);
279 script.push_str(
280 " check(res, { 'param:path:integer': (r) => r.status >= 200 && r.status < 500 });\n",
281 );
282 script.push_str(" }\n");
283
284 script.push_str(" {\n");
286 self.emit_get(script, "${BASE_URL}/conformance/params/query?name=test", None);
287 script.push_str(
288 " check(res, { 'param:query:string': (r) => r.status >= 200 && r.status < 500 });\n",
289 );
290 script.push_str(" }\n");
291
292 script.push_str(" {\n");
294 self.emit_get(script, "${BASE_URL}/conformance/params/query?count=10", None);
295 script.push_str(
296 " check(res, { 'param:query:integer': (r) => r.status >= 200 && r.status < 500 });\n",
297 );
298 script.push_str(" }\n");
299
300 script.push_str(" {\n");
302 self.emit_get(script, "${BASE_URL}/conformance/params/query?tags=a&tags=b", None);
303 script.push_str(
304 " check(res, { 'param:query:array': (r) => r.status >= 200 && r.status < 500 });\n",
305 );
306 script.push_str(" }\n");
307
308 script.push_str(" {\n");
310 self.emit_get(
311 script,
312 "${BASE_URL}/conformance/params/header",
313 Some("{ 'X-Custom-Param': 'test-value' }"),
314 );
315 script.push_str(
316 " check(res, { 'param:header': (r) => r.status >= 200 && r.status < 500 });\n",
317 );
318 script.push_str(" }\n");
319
320 script.push_str(" {\n");
322 script.push_str(" let jar = http.cookieJar();\n");
323 script.push_str(" jar.set(BASE_URL, 'session', 'abc123');\n");
324 self.emit_get(script, "${BASE_URL}/conformance/params/cookie", None);
325 script.push_str(
326 " check(res, { 'param:cookie': (r) => r.status >= 200 && r.status < 500 });\n",
327 );
328 script.push_str(" }\n");
329
330 script.push_str(" });\n\n");
331 }
332
333 fn generate_request_bodies_group(&self, script: &mut String) {
334 script.push_str(" group('Request Bodies', function () {\n");
335
336 script.push_str(" {\n");
338 self.emit_post_like(
339 script,
340 "post",
341 "${BASE_URL}/conformance/body/json",
342 "JSON.stringify({ name: 'test', value: 42 })",
343 "JSON_HEADERS",
344 );
345 script.push_str(
346 " check(res, { 'body:json': (r) => r.status >= 200 && r.status < 500 });\n",
347 );
348 script.push_str(" }\n");
349
350 script.push_str(" {\n");
352 if self.config.has_custom_headers() {
353 script.push_str(&format!(
354 " let res = http.post(`${{BASE_URL}}/conformance/body/form`, {{ field1: 'value1', field2: 'value2' }}, {{ headers: {} }});\n",
355 self.config.custom_headers_js_object()
356 ));
357 } else {
358 script.push_str(
359 " let res = http.post(`${BASE_URL}/conformance/body/form`, { field1: 'value1', field2: 'value2' });\n",
360 );
361 }
362 self.maybe_clear_cookie_jar(script);
363 script.push_str(
364 " check(res, { 'body:form-urlencoded': (r) => r.status >= 200 && r.status < 500 });\n",
365 );
366 script.push_str(" }\n");
367
368 script.push_str(" {\n");
370 script.push_str(
371 " let data = { field: http.file('test content', 'test.txt', 'text/plain') };\n",
372 );
373 if self.config.has_custom_headers() {
374 script.push_str(&format!(
375 " let res = http.post(`${{BASE_URL}}/conformance/body/multipart`, data, {{ headers: {} }});\n",
376 self.config.custom_headers_js_object()
377 ));
378 } else {
379 script.push_str(
380 " let res = http.post(`${BASE_URL}/conformance/body/multipart`, data);\n",
381 );
382 }
383 self.maybe_clear_cookie_jar(script);
384 script.push_str(
385 " check(res, { 'body:multipart': (r) => r.status >= 200 && r.status < 500 });\n",
386 );
387 script.push_str(" }\n");
388
389 script.push_str(" });\n\n");
390 }
391
392 fn generate_schema_types_group(&self, script: &mut String) {
393 script.push_str(" group('Schema Types', function () {\n");
394
395 let types = [
396 ("string", r#"{ "value": "hello" }"#, "schema:string"),
397 ("integer", r#"{ "value": 42 }"#, "schema:integer"),
398 ("number", r#"{ "value": 3.14 }"#, "schema:number"),
399 ("boolean", r#"{ "value": true }"#, "schema:boolean"),
400 ("array", r#"{ "value": [1, 2, 3] }"#, "schema:array"),
401 ("object", r#"{ "value": { "nested": "data" } }"#, "schema:object"),
402 ];
403
404 for (type_name, body, check_name) in types {
405 script.push_str(" {\n");
406 let url = format!("${{BASE_URL}}/conformance/schema/{}", type_name);
407 let body_str = format!("'{}'", body);
408 self.emit_post_like(script, "post", &url, &body_str, "JSON_HEADERS");
409 script.push_str(&format!(
410 " check(res, {{ '{}': (r) => r.status >= 200 && r.status < 500 }});\n",
411 check_name
412 ));
413 script.push_str(" }\n");
414 }
415
416 script.push_str(" });\n\n");
417 }
418
419 fn generate_composition_group(&self, script: &mut String) {
420 script.push_str(" group('Composition', function () {\n");
421
422 let compositions = [
423 ("oneOf", r#"{ "type": "string", "value": "test" }"#, "composition:oneOf"),
424 ("anyOf", r#"{ "value": "test" }"#, "composition:anyOf"),
425 ("allOf", r#"{ "name": "test", "id": 1 }"#, "composition:allOf"),
426 ];
427
428 for (kind, body, check_name) in compositions {
429 script.push_str(" {\n");
430 let url = format!("${{BASE_URL}}/conformance/composition/{}", kind);
431 let body_str = format!("'{}'", body);
432 self.emit_post_like(script, "post", &url, &body_str, "JSON_HEADERS");
433 script.push_str(&format!(
434 " check(res, {{ '{}': (r) => r.status >= 200 && r.status < 500 }});\n",
435 check_name
436 ));
437 script.push_str(" }\n");
438 }
439
440 script.push_str(" });\n\n");
441 }
442
443 fn generate_string_formats_group(&self, script: &mut String) {
444 script.push_str(" group('String Formats', function () {\n");
445
446 let formats = [
447 ("date", r#"{ "value": "2024-01-15" }"#, "format:date"),
448 ("date-time", r#"{ "value": "2024-01-15T10:30:00Z" }"#, "format:date-time"),
449 ("email", r#"{ "value": "test@example.com" }"#, "format:email"),
450 ("uuid", r#"{ "value": "550e8400-e29b-41d4-a716-446655440000" }"#, "format:uuid"),
451 ("uri", r#"{ "value": "https://example.com/path" }"#, "format:uri"),
452 ("ipv4", r#"{ "value": "192.168.1.1" }"#, "format:ipv4"),
453 ("ipv6", r#"{ "value": "::1" }"#, "format:ipv6"),
454 ];
455
456 for (fmt, body, check_name) in formats {
457 script.push_str(" {\n");
458 let url = format!("${{BASE_URL}}/conformance/formats/{}", fmt);
459 let body_str = format!("'{}'", body);
460 self.emit_post_like(script, "post", &url, &body_str, "JSON_HEADERS");
461 script.push_str(&format!(
462 " check(res, {{ '{}': (r) => r.status >= 200 && r.status < 500 }});\n",
463 check_name
464 ));
465 script.push_str(" }\n");
466 }
467
468 script.push_str(" });\n\n");
469 }
470
471 fn generate_constraints_group(&self, script: &mut String) {
472 script.push_str(" group('Constraints', function () {\n");
473
474 let constraints = [
475 (
476 "required",
477 "JSON.stringify({ required_field: 'present' })",
478 "constraint:required",
479 ),
480 ("optional", "JSON.stringify({})", "constraint:optional"),
481 ("minmax", "JSON.stringify({ value: 50 })", "constraint:minmax"),
482 ("pattern", "JSON.stringify({ value: 'ABC-123' })", "constraint:pattern"),
483 ("enum", "JSON.stringify({ status: 'active' })", "constraint:enum"),
484 ];
485
486 for (kind, body, check_name) in constraints {
487 script.push_str(" {\n");
488 let url = format!("${{BASE_URL}}/conformance/constraints/{}", kind);
489 self.emit_post_like(script, "post", &url, body, "JSON_HEADERS");
490 script.push_str(&format!(
491 " check(res, {{ '{}': (r) => r.status >= 200 && r.status < 500 }});\n",
492 check_name
493 ));
494 script.push_str(" }\n");
495 }
496
497 script.push_str(" });\n\n");
498 }
499
500 fn generate_response_codes_group(&self, script: &mut String) {
501 script.push_str(" group('Response Codes', function () {\n");
502
503 let codes = [
504 ("200", "response:200"),
505 ("201", "response:201"),
506 ("204", "response:204"),
507 ("400", "response:400"),
508 ("404", "response:404"),
509 ];
510
511 for (code, check_name) in codes {
512 script.push_str(" {\n");
513 let url = format!("${{BASE_URL}}/conformance/responses/{}", code);
514 self.emit_get(script, &url, None);
515 script.push_str(&format!(
516 " check(res, {{ '{}': (r) => r.status === {} }});\n",
517 check_name, code
518 ));
519 script.push_str(" }\n");
520 }
521
522 script.push_str(" });\n\n");
523 }
524
525 fn generate_http_methods_group(&self, script: &mut String) {
526 script.push_str(" group('HTTP Methods', function () {\n");
527
528 script.push_str(" {\n");
530 self.emit_get(script, "${BASE_URL}/conformance/methods", None);
531 script.push_str(
532 " check(res, { 'method:GET': (r) => r.status >= 200 && r.status < 500 });\n",
533 );
534 script.push_str(" }\n");
535
536 script.push_str(" {\n");
538 self.emit_post_like(
539 script,
540 "post",
541 "${BASE_URL}/conformance/methods",
542 "JSON.stringify({ action: 'create' })",
543 "JSON_HEADERS",
544 );
545 script.push_str(
546 " check(res, { 'method:POST': (r) => r.status >= 200 && r.status < 500 });\n",
547 );
548 script.push_str(" }\n");
549
550 script.push_str(" {\n");
552 self.emit_post_like(
553 script,
554 "put",
555 "${BASE_URL}/conformance/methods",
556 "JSON.stringify({ action: 'update' })",
557 "JSON_HEADERS",
558 );
559 script.push_str(
560 " check(res, { 'method:PUT': (r) => r.status >= 200 && r.status < 500 });\n",
561 );
562 script.push_str(" }\n");
563
564 script.push_str(" {\n");
566 self.emit_post_like(
567 script,
568 "patch",
569 "${BASE_URL}/conformance/methods",
570 "JSON.stringify({ action: 'patch' })",
571 "JSON_HEADERS",
572 );
573 script.push_str(
574 " check(res, { 'method:PATCH': (r) => r.status >= 200 && r.status < 500 });\n",
575 );
576 script.push_str(" }\n");
577
578 script.push_str(" {\n");
580 self.emit_no_body(script, "del", "${BASE_URL}/conformance/methods");
581 script.push_str(
582 " check(res, { 'method:DELETE': (r) => r.status >= 200 && r.status < 500 });\n",
583 );
584 script.push_str(" }\n");
585
586 script.push_str(" {\n");
588 self.emit_no_body(script, "head", "${BASE_URL}/conformance/methods");
589 script.push_str(
590 " check(res, { 'method:HEAD': (r) => r.status >= 200 && r.status < 500 });\n",
591 );
592 script.push_str(" }\n");
593
594 script.push_str(" {\n");
596 self.emit_no_body(script, "options", "${BASE_URL}/conformance/methods");
597 script.push_str(
598 " check(res, { 'method:OPTIONS': (r) => r.status >= 200 && r.status < 500 });\n",
599 );
600 script.push_str(" }\n");
601
602 script.push_str(" });\n\n");
603 }
604
605 fn generate_content_negotiation_group(&self, script: &mut String) {
606 script.push_str(" group('Content Types', function () {\n");
607
608 script.push_str(" {\n");
609 self.emit_get(
610 script,
611 "${BASE_URL}/conformance/content-types",
612 Some("{ 'Accept': 'application/json' }"),
613 );
614 script.push_str(
615 " check(res, { 'content:negotiation': (r) => r.status >= 200 && r.status < 500 });\n",
616 );
617 script.push_str(" }\n");
618
619 script.push_str(" });\n\n");
620 }
621
622 fn generate_security_group(&self, script: &mut String) {
623 script.push_str(" group('Security', function () {\n");
624
625 script.push_str(" {\n");
627 self.emit_get(
628 script,
629 "${BASE_URL}/conformance/security/bearer",
630 Some("{ 'Authorization': 'Bearer test-token-123' }"),
631 );
632 script.push_str(
633 " check(res, { 'security:bearer': (r) => r.status >= 200 && r.status < 500 });\n",
634 );
635 script.push_str(" }\n");
636
637 let api_key = self.config.api_key.as_deref().unwrap_or("test-api-key-123");
639 script.push_str(" {\n");
640 let api_key_hdrs = format!("{{ 'X-API-Key': '{}' }}", api_key);
641 self.emit_get(script, "${BASE_URL}/conformance/security/apikey", Some(&api_key_hdrs));
642 script.push_str(
643 " check(res, { 'security:apikey': (r) => r.status >= 200 && r.status < 500 });\n",
644 );
645 script.push_str(" }\n");
646
647 let basic_creds = self.config.basic_auth.as_deref().unwrap_or("user:pass");
649 let encoded = base64_encode(basic_creds);
650 script.push_str(" {\n");
651 let basic_hdrs = format!("{{ 'Authorization': 'Basic {}' }}", encoded);
652 self.emit_get(script, "${BASE_URL}/conformance/security/basic", Some(&basic_hdrs));
653 script.push_str(
654 " check(res, { 'security:basic': (r) => r.status >= 200 && r.status < 500 });\n",
655 );
656 script.push_str(" }\n");
657
658 script.push_str(" });\n\n");
659 }
660
661 fn generate_handle_summary(&self, script: &mut String) {
662 let report_path = match &self.config.output_dir {
665 Some(dir) => {
666 let abs = std::fs::canonicalize(dir)
667 .unwrap_or_else(|_| dir.clone())
668 .join("conformance-report.json");
669 abs.to_string_lossy().to_string()
670 }
671 None => "conformance-report.json".to_string(),
672 };
673
674 script.push_str("export function handleSummary(data) {\n");
675 script.push_str(" // Extract check results for conformance reporting\n");
676 script.push_str(" let checks = {};\n");
677 script.push_str(" if (data.metrics && data.metrics.checks) {\n");
678 script.push_str(" // Overall check pass rate\n");
679 script.push_str(" checks.overall_pass_rate = data.metrics.checks.values.rate;\n");
680 script.push_str(" }\n");
681 script.push_str(" // Collect per-check results from root_group\n");
682 script.push_str(" let checkResults = {};\n");
683 script.push_str(" function walkGroups(group) {\n");
684 script.push_str(" if (group.checks) {\n");
685 script.push_str(" for (let checkObj of group.checks) {\n");
686 script.push_str(" checkResults[checkObj.name] = {\n");
687 script.push_str(" passes: checkObj.passes,\n");
688 script.push_str(" fails: checkObj.fails,\n");
689 script.push_str(" };\n");
690 script.push_str(" }\n");
691 script.push_str(" }\n");
692 script.push_str(" if (group.groups) {\n");
693 script.push_str(" for (let subGroup of group.groups) {\n");
694 script.push_str(" walkGroups(subGroup);\n");
695 script.push_str(" }\n");
696 script.push_str(" }\n");
697 script.push_str(" }\n");
698 script.push_str(" if (data.root_group) {\n");
699 script.push_str(" walkGroups(data.root_group);\n");
700 script.push_str(" }\n");
701 script.push_str(" return {\n");
702 script.push_str(&format!(
703 " '{}': JSON.stringify({{ checks: checkResults, overall: checks }}, null, 2),\n",
704 report_path
705 ));
706 script.push_str(" stdout: textSummary(data, { indent: ' ', enableColors: true }),\n");
707 script.push_str(" };\n");
708 script.push_str("}\n\n");
709 script.push_str("// textSummary fallback\n");
710 script.push_str("function textSummary(data, opts) {\n");
711 script.push_str(" return JSON.stringify(data, null, 2);\n");
712 script.push_str("}\n");
713 }
714}
715
716fn base64_encode(input: &str) -> String {
718 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
719 let bytes = input.as_bytes();
720 let mut result = String::with_capacity(bytes.len().div_ceil(3) * 4);
721 for chunk in bytes.chunks(3) {
722 let b0 = chunk[0] as u32;
723 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
724 let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
725 let triple = (b0 << 16) | (b1 << 8) | b2;
726 result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
727 result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
728 if chunk.len() > 1 {
729 result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
730 } else {
731 result.push('=');
732 }
733 if chunk.len() > 2 {
734 result.push(CHARS[(triple & 0x3F) as usize] as char);
735 } else {
736 result.push('=');
737 }
738 }
739 result
740}
741
742#[cfg(test)]
743mod tests {
744 use super::*;
745
746 #[test]
747 fn test_generate_conformance_script() {
748 let config = ConformanceConfig {
749 target_url: "http://localhost:8080".to_string(),
750 api_key: None,
751 basic_auth: None,
752 skip_tls_verify: false,
753 categories: None,
754 base_path: None,
755 custom_headers: vec![],
756 output_dir: None,
757 all_operations: false,
758 };
759 let generator = ConformanceGenerator::new(config);
760 let script = generator.generate().unwrap();
761
762 assert!(script.contains("import http from 'k6/http'"));
763 assert!(script.contains("vus: 1"));
764 assert!(script.contains("iterations: 1"));
765 assert!(script.contains("group('Parameters'"));
766 assert!(script.contains("group('Request Bodies'"));
767 assert!(script.contains("group('Schema Types'"));
768 assert!(script.contains("group('Composition'"));
769 assert!(script.contains("group('String Formats'"));
770 assert!(script.contains("group('Constraints'"));
771 assert!(script.contains("group('Response Codes'"));
772 assert!(script.contains("group('HTTP Methods'"));
773 assert!(script.contains("group('Content Types'"));
774 assert!(script.contains("group('Security'"));
775 assert!(script.contains("handleSummary"));
776 }
777
778 #[test]
779 fn test_base64_encode() {
780 assert_eq!(base64_encode("user:pass"), "dXNlcjpwYXNz");
781 assert_eq!(base64_encode("a"), "YQ==");
782 assert_eq!(base64_encode("ab"), "YWI=");
783 assert_eq!(base64_encode("abc"), "YWJj");
784 }
785
786 #[test]
787 fn test_conformance_script_with_custom_auth() {
788 let config = ConformanceConfig {
789 target_url: "https://api.example.com".to_string(),
790 api_key: Some("my-api-key".to_string()),
791 basic_auth: Some("admin:secret".to_string()),
792 skip_tls_verify: true,
793 categories: None,
794 base_path: None,
795 custom_headers: vec![],
796 output_dir: None,
797 all_operations: false,
798 };
799 let generator = ConformanceGenerator::new(config);
800 let script = generator.generate().unwrap();
801
802 assert!(script.contains("insecureSkipTLSVerify: true"));
803 assert!(script.contains("my-api-key"));
804 assert!(script.contains(&base64_encode("admin:secret")));
805 }
806
807 #[test]
808 fn test_should_include_category_none_includes_all() {
809 let config = ConformanceConfig {
810 target_url: "http://localhost:8080".to_string(),
811 api_key: None,
812 basic_auth: None,
813 skip_tls_verify: false,
814 categories: None,
815 base_path: None,
816 custom_headers: vec![],
817 output_dir: None,
818 all_operations: false,
819 };
820 assert!(config.should_include_category("Parameters"));
821 assert!(config.should_include_category("Security"));
822 assert!(config.should_include_category("Anything"));
823 }
824
825 #[test]
826 fn test_should_include_category_filtered() {
827 let config = ConformanceConfig {
828 target_url: "http://localhost:8080".to_string(),
829 api_key: None,
830 basic_auth: None,
831 skip_tls_verify: false,
832 categories: Some(vec!["Parameters".to_string(), "Security".to_string()]),
833 base_path: None,
834 custom_headers: vec![],
835 output_dir: None,
836 all_operations: false,
837 };
838 assert!(config.should_include_category("Parameters"));
839 assert!(config.should_include_category("Security"));
840 assert!(config.should_include_category("parameters")); assert!(!config.should_include_category("Composition"));
842 assert!(!config.should_include_category("Schema Types"));
843 }
844
845 #[test]
846 fn test_generate_with_category_filter() {
847 let config = ConformanceConfig {
848 target_url: "http://localhost:8080".to_string(),
849 api_key: None,
850 basic_auth: None,
851 skip_tls_verify: false,
852 categories: Some(vec!["Parameters".to_string(), "Security".to_string()]),
853 base_path: None,
854 custom_headers: vec![],
855 output_dir: None,
856 all_operations: false,
857 };
858 let generator = ConformanceGenerator::new(config);
859 let script = generator.generate().unwrap();
860
861 assert!(script.contains("group('Parameters'"));
862 assert!(script.contains("group('Security'"));
863 assert!(!script.contains("group('Request Bodies'"));
864 assert!(!script.contains("group('Schema Types'"));
865 assert!(!script.contains("group('Composition'"));
866 }
867
868 #[test]
869 fn test_effective_base_url_no_base_path() {
870 let config = ConformanceConfig {
871 target_url: "https://example.com".to_string(),
872 api_key: None,
873 basic_auth: None,
874 skip_tls_verify: false,
875 categories: None,
876 base_path: None,
877 custom_headers: vec![],
878 output_dir: None,
879 all_operations: false,
880 };
881 assert_eq!(config.effective_base_url(), "https://example.com");
882 }
883
884 #[test]
885 fn test_effective_base_url_with_base_path() {
886 let config = ConformanceConfig {
887 target_url: "https://example.com".to_string(),
888 api_key: None,
889 basic_auth: None,
890 skip_tls_verify: false,
891 categories: None,
892 base_path: Some("/api".to_string()),
893 custom_headers: vec![],
894 output_dir: None,
895 all_operations: false,
896 };
897 assert_eq!(config.effective_base_url(), "https://example.com/api");
898 }
899
900 #[test]
901 fn test_effective_base_url_trailing_slash_normalization() {
902 let config = ConformanceConfig {
903 target_url: "https://example.com/".to_string(),
904 api_key: None,
905 basic_auth: None,
906 skip_tls_verify: false,
907 categories: None,
908 base_path: Some("/api".to_string()),
909 custom_headers: vec![],
910 output_dir: None,
911 all_operations: false,
912 };
913 assert_eq!(config.effective_base_url(), "https://example.com/api");
914 }
915
916 #[test]
917 fn test_generate_script_with_base_path() {
918 let config = ConformanceConfig {
919 target_url: "https://192.168.2.86".to_string(),
920 api_key: None,
921 basic_auth: None,
922 skip_tls_verify: true,
923 categories: None,
924 base_path: Some("/api".to_string()),
925 custom_headers: vec![],
926 output_dir: None,
927 all_operations: false,
928 };
929 let generator = ConformanceGenerator::new(config);
930 let script = generator.generate().unwrap();
931
932 assert!(script.contains("const BASE_URL = 'https://192.168.2.86/api'"));
933 assert!(script.contains("${BASE_URL}/conformance/"));
935 }
936
937 #[test]
938 fn test_generate_with_custom_headers() {
939 let config = ConformanceConfig {
940 target_url: "https://192.168.2.86".to_string(),
941 api_key: None,
942 basic_auth: None,
943 skip_tls_verify: true,
944 categories: Some(vec!["Parameters".to_string()]),
945 base_path: Some("/api".to_string()),
946 custom_headers: vec![
947 ("X-Avi-Tenant".to_string(), "admin".to_string()),
948 ("X-CSRFToken".to_string(), "real-token".to_string()),
949 ],
950 output_dir: None,
951 all_operations: false,
952 };
953 let generator = ConformanceGenerator::new(config);
954 let script = generator.generate().unwrap();
955
956 assert!(
958 !script.contains("const CUSTOM_HEADERS"),
959 "Script should NOT declare a CUSTOM_HEADERS const"
960 );
961 assert!(script.contains("'X-Avi-Tenant': 'admin'"));
962 assert!(script.contains("'X-CSRFToken': 'real-token'"));
963 }
964
965 #[test]
966 fn test_custom_headers_js_object() {
967 let config = ConformanceConfig {
968 target_url: "http://localhost".to_string(),
969 api_key: None,
970 basic_auth: None,
971 skip_tls_verify: false,
972 categories: None,
973 base_path: None,
974 custom_headers: vec![
975 ("Authorization".to_string(), "Bearer abc123".to_string()),
976 ("X-Custom".to_string(), "value".to_string()),
977 ],
978 output_dir: None,
979 all_operations: false,
980 };
981 let js = config.custom_headers_js_object();
982 assert!(js.contains("'Authorization': 'Bearer abc123'"));
983 assert!(js.contains("'X-Custom': 'value'"));
984 }
985}