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