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