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}
19
20impl ConformanceConfig {
21 pub fn should_include_category(&self, category: &str) -> bool {
23 match &self.categories {
24 None => true,
25 Some(cats) => cats.iter().any(|c| c.eq_ignore_ascii_case(category)),
26 }
27 }
28}
29
30pub struct ConformanceGenerator {
32 config: ConformanceConfig,
33}
34
35impl ConformanceGenerator {
36 pub fn new(config: ConformanceConfig) -> Self {
37 Self { config }
38 }
39
40 pub fn generate(&self) -> Result<String> {
42 let mut script = String::with_capacity(16384);
43
44 script.push_str("import http from 'k6/http';\n");
46 script.push_str("import { check, group } from 'k6';\n\n");
47
48 script.push_str("export const options = {\n");
50 script.push_str(" vus: 1,\n");
51 script.push_str(" iterations: 1,\n");
52 if self.config.skip_tls_verify {
53 script.push_str(" insecureSkipTLSVerify: true,\n");
54 }
55 script.push_str(" thresholds: {\n");
56 script.push_str(" checks: ['rate>0'],\n");
57 script.push_str(" },\n");
58 script.push_str("};\n\n");
59
60 script.push_str(&format!("const BASE_URL = '{}';\n\n", self.config.target_url));
62
63 script.push_str("const JSON_HEADERS = { 'Content-Type': 'application/json' };\n\n");
65
66 script.push_str("export default function () {\n");
68
69 if self.config.should_include_category("Parameters") {
70 self.generate_parameters_group(&mut script);
71 }
72 if self.config.should_include_category("Request Bodies") {
73 self.generate_request_bodies_group(&mut script);
74 }
75 if self.config.should_include_category("Schema Types") {
76 self.generate_schema_types_group(&mut script);
77 }
78 if self.config.should_include_category("Composition") {
79 self.generate_composition_group(&mut script);
80 }
81 if self.config.should_include_category("String Formats") {
82 self.generate_string_formats_group(&mut script);
83 }
84 if self.config.should_include_category("Constraints") {
85 self.generate_constraints_group(&mut script);
86 }
87 if self.config.should_include_category("Response Codes") {
88 self.generate_response_codes_group(&mut script);
89 }
90 if self.config.should_include_category("HTTP Methods") {
91 self.generate_http_methods_group(&mut script);
92 }
93 if self.config.should_include_category("Content Types") {
94 self.generate_content_negotiation_group(&mut script);
95 }
96 if self.config.should_include_category("Security") {
97 self.generate_security_group(&mut script);
98 }
99
100 script.push_str("}\n\n");
101
102 self.generate_handle_summary(&mut script);
104
105 Ok(script)
106 }
107
108 pub fn write_script(&self, path: &Path) -> Result<()> {
110 let script = self.generate()?;
111 if let Some(parent) = path.parent() {
112 std::fs::create_dir_all(parent)?;
113 }
114 std::fs::write(path, script)
115 .map_err(|e| BenchError::Other(format!("Failed to write conformance script: {}", e)))
116 }
117
118 fn generate_parameters_group(&self, script: &mut String) {
119 script.push_str(" group('Parameters', function () {\n");
120
121 script.push_str(" {\n");
123 script.push_str(" let res = http.get(`${BASE_URL}/conformance/params/hello`);\n");
124 script.push_str(
125 " check(res, { 'param:path:string': (r) => r.status >= 200 && r.status < 500 });\n",
126 );
127 script.push_str(" }\n");
128
129 script.push_str(" {\n");
131 script.push_str(" let res = http.get(`${BASE_URL}/conformance/params/42`);\n");
132 script.push_str(
133 " check(res, { 'param:path:integer': (r) => r.status >= 200 && r.status < 500 });\n",
134 );
135 script.push_str(" }\n");
136
137 script.push_str(" {\n");
139 script.push_str(
140 " let res = http.get(`${BASE_URL}/conformance/params/query?name=test`);\n",
141 );
142 script.push_str(
143 " check(res, { 'param:query:string': (r) => r.status >= 200 && r.status < 500 });\n",
144 );
145 script.push_str(" }\n");
146
147 script.push_str(" {\n");
149 script.push_str(
150 " let res = http.get(`${BASE_URL}/conformance/params/query?count=10`);\n",
151 );
152 script.push_str(
153 " check(res, { 'param:query:integer': (r) => r.status >= 200 && r.status < 500 });\n",
154 );
155 script.push_str(" }\n");
156
157 script.push_str(" {\n");
159 script.push_str(
160 " let res = http.get(`${BASE_URL}/conformance/params/query?tags=a&tags=b`);\n",
161 );
162 script.push_str(
163 " check(res, { 'param:query:array': (r) => r.status >= 200 && r.status < 500 });\n",
164 );
165 script.push_str(" }\n");
166
167 script.push_str(" {\n");
169 script.push_str(
170 " let res = http.get(`${BASE_URL}/conformance/params/header`, { headers: { 'X-Custom-Param': 'test-value' } });\n",
171 );
172 script.push_str(
173 " check(res, { 'param:header': (r) => r.status >= 200 && r.status < 500 });\n",
174 );
175 script.push_str(" }\n");
176
177 script.push_str(" {\n");
179 script.push_str(" let jar = http.cookieJar();\n");
180 script.push_str(" jar.set(BASE_URL, 'session', 'abc123');\n");
181 script.push_str(" let res = http.get(`${BASE_URL}/conformance/params/cookie`);\n");
182 script.push_str(
183 " check(res, { 'param:cookie': (r) => r.status >= 200 && r.status < 500 });\n",
184 );
185 script.push_str(" }\n");
186
187 script.push_str(" });\n\n");
188 }
189
190 fn generate_request_bodies_group(&self, script: &mut String) {
191 script.push_str(" group('Request Bodies', function () {\n");
192
193 script.push_str(" {\n");
195 script.push_str(
196 " let res = http.post(`${BASE_URL}/conformance/body/json`, JSON.stringify({ name: 'test', value: 42 }), { headers: JSON_HEADERS });\n",
197 );
198 script.push_str(
199 " check(res, { 'body:json': (r) => r.status >= 200 && r.status < 500 });\n",
200 );
201 script.push_str(" }\n");
202
203 script.push_str(" {\n");
205 script.push_str(
206 " let res = http.post(`${BASE_URL}/conformance/body/form`, { field1: 'value1', field2: 'value2' });\n",
207 );
208 script.push_str(
209 " check(res, { 'body:form-urlencoded': (r) => r.status >= 200 && r.status < 500 });\n",
210 );
211 script.push_str(" }\n");
212
213 script.push_str(" {\n");
215 script.push_str(
216 " let data = { field: http.file('test content', 'test.txt', 'text/plain') };\n",
217 );
218 script.push_str(
219 " let res = http.post(`${BASE_URL}/conformance/body/multipart`, data);\n",
220 );
221 script.push_str(
222 " check(res, { 'body:multipart': (r) => r.status >= 200 && r.status < 500 });\n",
223 );
224 script.push_str(" }\n");
225
226 script.push_str(" });\n\n");
227 }
228
229 fn generate_schema_types_group(&self, script: &mut String) {
230 script.push_str(" group('Schema Types', function () {\n");
231
232 let types = [
233 ("string", r#"{ "value": "hello" }"#, "schema:string"),
234 ("integer", r#"{ "value": 42 }"#, "schema:integer"),
235 ("number", r#"{ "value": 3.14 }"#, "schema:number"),
236 ("boolean", r#"{ "value": true }"#, "schema:boolean"),
237 ("array", r#"{ "value": [1, 2, 3] }"#, "schema:array"),
238 ("object", r#"{ "value": { "nested": "data" } }"#, "schema:object"),
239 ];
240
241 for (type_name, body, check_name) in types {
242 script.push_str(" {\n");
243 script.push_str(&format!(
244 " let res = http.post(`${{BASE_URL}}/conformance/schema/{}`, '{}', {{ headers: JSON_HEADERS }});\n",
245 type_name, body
246 ));
247 script.push_str(&format!(
248 " check(res, {{ '{}': (r) => r.status >= 200 && r.status < 500 }});\n",
249 check_name
250 ));
251 script.push_str(" }\n");
252 }
253
254 script.push_str(" });\n\n");
255 }
256
257 fn generate_composition_group(&self, script: &mut String) {
258 script.push_str(" group('Composition', function () {\n");
259
260 let compositions = [
261 ("oneOf", r#"{ "type": "string", "value": "test" }"#, "composition:oneOf"),
262 ("anyOf", r#"{ "value": "test" }"#, "composition:anyOf"),
263 ("allOf", r#"{ "name": "test", "id": 1 }"#, "composition:allOf"),
264 ];
265
266 for (kind, body, check_name) in compositions {
267 script.push_str(" {\n");
268 script.push_str(&format!(
269 " let res = http.post(`${{BASE_URL}}/conformance/composition/{}`, '{}', {{ headers: JSON_HEADERS }});\n",
270 kind, body
271 ));
272 script.push_str(&format!(
273 " check(res, {{ '{}': (r) => r.status >= 200 && r.status < 500 }});\n",
274 check_name
275 ));
276 script.push_str(" }\n");
277 }
278
279 script.push_str(" });\n\n");
280 }
281
282 fn generate_string_formats_group(&self, script: &mut String) {
283 script.push_str(" group('String Formats', function () {\n");
284
285 let formats = [
286 ("date", r#"{ "value": "2024-01-15" }"#, "format:date"),
287 ("date-time", r#"{ "value": "2024-01-15T10:30:00Z" }"#, "format:date-time"),
288 ("email", r#"{ "value": "test@example.com" }"#, "format:email"),
289 ("uuid", r#"{ "value": "550e8400-e29b-41d4-a716-446655440000" }"#, "format:uuid"),
290 ("uri", r#"{ "value": "https://example.com/path" }"#, "format:uri"),
291 ("ipv4", r#"{ "value": "192.168.1.1" }"#, "format:ipv4"),
292 ("ipv6", r#"{ "value": "::1" }"#, "format:ipv6"),
293 ];
294
295 for (fmt, body, check_name) in formats {
296 script.push_str(" {\n");
297 script.push_str(&format!(
298 " let res = http.post(`${{BASE_URL}}/conformance/formats/{}`, '{}', {{ headers: JSON_HEADERS }});\n",
299 fmt, body
300 ));
301 script.push_str(&format!(
302 " check(res, {{ '{}': (r) => r.status >= 200 && r.status < 500 }});\n",
303 check_name
304 ));
305 script.push_str(" }\n");
306 }
307
308 script.push_str(" });\n\n");
309 }
310
311 fn generate_constraints_group(&self, script: &mut String) {
312 script.push_str(" group('Constraints', function () {\n");
313
314 script.push_str(" {\n");
316 script.push_str(
317 " let res = http.post(`${BASE_URL}/conformance/constraints/required`, JSON.stringify({ required_field: 'present' }), { headers: JSON_HEADERS });\n",
318 );
319 script.push_str(
320 " check(res, { 'constraint:required': (r) => r.status >= 200 && r.status < 500 });\n",
321 );
322 script.push_str(" }\n");
323
324 script.push_str(" {\n");
326 script.push_str(
327 " let res = http.post(`${BASE_URL}/conformance/constraints/optional`, JSON.stringify({}), { headers: JSON_HEADERS });\n",
328 );
329 script.push_str(
330 " check(res, { 'constraint:optional': (r) => r.status >= 200 && r.status < 500 });\n",
331 );
332 script.push_str(" }\n");
333
334 script.push_str(" {\n");
336 script.push_str(
337 " let res = http.post(`${BASE_URL}/conformance/constraints/minmax`, JSON.stringify({ value: 50 }), { headers: JSON_HEADERS });\n",
338 );
339 script.push_str(
340 " check(res, { 'constraint:minmax': (r) => r.status >= 200 && r.status < 500 });\n",
341 );
342 script.push_str(" }\n");
343
344 script.push_str(" {\n");
346 script.push_str(
347 " let res = http.post(`${BASE_URL}/conformance/constraints/pattern`, JSON.stringify({ value: 'ABC-123' }), { headers: JSON_HEADERS });\n",
348 );
349 script.push_str(
350 " check(res, { 'constraint:pattern': (r) => r.status >= 200 && r.status < 500 });\n",
351 );
352 script.push_str(" }\n");
353
354 script.push_str(" {\n");
356 script.push_str(
357 " let res = http.post(`${BASE_URL}/conformance/constraints/enum`, JSON.stringify({ status: 'active' }), { headers: JSON_HEADERS });\n",
358 );
359 script.push_str(
360 " check(res, { 'constraint:enum': (r) => r.status >= 200 && r.status < 500 });\n",
361 );
362 script.push_str(" }\n");
363
364 script.push_str(" });\n\n");
365 }
366
367 fn generate_response_codes_group(&self, script: &mut String) {
368 script.push_str(" group('Response Codes', function () {\n");
369
370 let codes = [
371 ("200", "response:200"),
372 ("201", "response:201"),
373 ("204", "response:204"),
374 ("400", "response:400"),
375 ("404", "response:404"),
376 ];
377
378 for (code, check_name) in codes {
379 script.push_str(" {\n");
380 script.push_str(&format!(
381 " let res = http.get(`${{BASE_URL}}/conformance/responses/{}`);\n",
382 code
383 ));
384 script.push_str(&format!(
385 " check(res, {{ '{}': (r) => r.status === {} }});\n",
386 check_name, code
387 ));
388 script.push_str(" }\n");
389 }
390
391 script.push_str(" });\n\n");
392 }
393
394 fn generate_http_methods_group(&self, script: &mut String) {
395 script.push_str(" group('HTTP Methods', function () {\n");
396
397 script.push_str(" {\n");
399 script.push_str(" let res = http.get(`${BASE_URL}/conformance/methods`);\n");
400 script.push_str(
401 " check(res, { 'method:GET': (r) => r.status >= 200 && r.status < 500 });\n",
402 );
403 script.push_str(" }\n");
404
405 script.push_str(" {\n");
407 script.push_str(
408 " let res = http.post(`${BASE_URL}/conformance/methods`, JSON.stringify({ action: 'create' }), { headers: JSON_HEADERS });\n",
409 );
410 script.push_str(
411 " check(res, { 'method:POST': (r) => r.status >= 200 && r.status < 500 });\n",
412 );
413 script.push_str(" }\n");
414
415 script.push_str(" {\n");
417 script.push_str(
418 " let res = http.put(`${BASE_URL}/conformance/methods`, JSON.stringify({ action: 'update' }), { headers: JSON_HEADERS });\n",
419 );
420 script.push_str(
421 " check(res, { 'method:PUT': (r) => r.status >= 200 && r.status < 500 });\n",
422 );
423 script.push_str(" }\n");
424
425 script.push_str(" {\n");
427 script.push_str(
428 " let res = http.patch(`${BASE_URL}/conformance/methods`, JSON.stringify({ action: 'patch' }), { headers: JSON_HEADERS });\n",
429 );
430 script.push_str(
431 " check(res, { 'method:PATCH': (r) => r.status >= 200 && r.status < 500 });\n",
432 );
433 script.push_str(" }\n");
434
435 script.push_str(" {\n");
437 script.push_str(" let res = http.del(`${BASE_URL}/conformance/methods`);\n");
438 script.push_str(
439 " check(res, { 'method:DELETE': (r) => r.status >= 200 && r.status < 500 });\n",
440 );
441 script.push_str(" }\n");
442
443 script.push_str(" {\n");
445 script.push_str(" let res = http.head(`${BASE_URL}/conformance/methods`);\n");
446 script.push_str(
447 " check(res, { 'method:HEAD': (r) => r.status >= 200 && r.status < 500 });\n",
448 );
449 script.push_str(" }\n");
450
451 script.push_str(" {\n");
453 script.push_str(" let res = http.options(`${BASE_URL}/conformance/methods`);\n");
454 script.push_str(
455 " check(res, { 'method:OPTIONS': (r) => r.status >= 200 && r.status < 500 });\n",
456 );
457 script.push_str(" }\n");
458
459 script.push_str(" });\n\n");
460 }
461
462 fn generate_content_negotiation_group(&self, script: &mut String) {
463 script.push_str(" group('Content Types', function () {\n");
464
465 script.push_str(" {\n");
466 script.push_str(
467 " let res = http.get(`${BASE_URL}/conformance/content-types`, { headers: { 'Accept': 'application/json' } });\n",
468 );
469 script.push_str(
470 " check(res, { 'content:negotiation': (r) => r.status >= 200 && r.status < 500 });\n",
471 );
472 script.push_str(" }\n");
473
474 script.push_str(" });\n\n");
475 }
476
477 fn generate_security_group(&self, script: &mut String) {
478 script.push_str(" group('Security', function () {\n");
479
480 script.push_str(" {\n");
482 script.push_str(
483 " let res = http.get(`${BASE_URL}/conformance/security/bearer`, { headers: { 'Authorization': 'Bearer test-token-123' } });\n",
484 );
485 script.push_str(
486 " check(res, { 'security:bearer': (r) => r.status >= 200 && r.status < 500 });\n",
487 );
488 script.push_str(" }\n");
489
490 let api_key = self.config.api_key.as_deref().unwrap_or("test-api-key-123");
492 script.push_str(" {\n");
493 script.push_str(&format!(
494 " let res = http.get(`${{BASE_URL}}/conformance/security/apikey`, {{ headers: {{ 'X-API-Key': '{}' }} }});\n",
495 api_key
496 ));
497 script.push_str(
498 " check(res, { 'security:apikey': (r) => r.status >= 200 && r.status < 500 });\n",
499 );
500 script.push_str(" }\n");
501
502 let basic_creds = self.config.basic_auth.as_deref().unwrap_or("user:pass");
504 let encoded = base64_encode(basic_creds);
505 script.push_str(" {\n");
506 script.push_str(&format!(
507 " let res = http.get(`${{BASE_URL}}/conformance/security/basic`, {{ headers: {{ 'Authorization': 'Basic {}' }} }});\n",
508 encoded
509 ));
510 script.push_str(
511 " check(res, { 'security:basic': (r) => r.status >= 200 && r.status < 500 });\n",
512 );
513 script.push_str(" }\n");
514
515 script.push_str(" });\n\n");
516 }
517
518 fn generate_handle_summary(&self, script: &mut String) {
519 script.push_str("export function handleSummary(data) {\n");
520 script.push_str(" // Extract check results for conformance reporting\n");
521 script.push_str(" let checks = {};\n");
522 script.push_str(" if (data.metrics && data.metrics.checks) {\n");
523 script.push_str(" // Overall check pass rate\n");
524 script.push_str(" checks.overall_pass_rate = data.metrics.checks.values.rate;\n");
525 script.push_str(" }\n");
526 script.push_str(" // Collect per-check results from root_group\n");
527 script.push_str(" let checkResults = {};\n");
528 script.push_str(" function walkGroups(group) {\n");
529 script.push_str(" if (group.checks) {\n");
530 script.push_str(" for (let checkObj of group.checks) {\n");
531 script.push_str(" checkResults[checkObj.name] = {\n");
532 script.push_str(" passes: checkObj.passes,\n");
533 script.push_str(" fails: checkObj.fails,\n");
534 script.push_str(" };\n");
535 script.push_str(" }\n");
536 script.push_str(" }\n");
537 script.push_str(" if (group.groups) {\n");
538 script.push_str(" for (let subGroup of group.groups) {\n");
539 script.push_str(" walkGroups(subGroup);\n");
540 script.push_str(" }\n");
541 script.push_str(" }\n");
542 script.push_str(" }\n");
543 script.push_str(" if (data.root_group) {\n");
544 script.push_str(" walkGroups(data.root_group);\n");
545 script.push_str(" }\n");
546 script.push_str(" return {\n");
547 script.push_str(" 'conformance-report.json': JSON.stringify({ checks: checkResults, overall: checks }, null, 2),\n");
548 script.push_str(" stdout: textSummary(data, { indent: ' ', enableColors: true }),\n");
549 script.push_str(" };\n");
550 script.push_str("}\n\n");
551 script.push_str("// textSummary fallback\n");
552 script.push_str("function textSummary(data, opts) {\n");
553 script.push_str(" return JSON.stringify(data, null, 2);\n");
554 script.push_str("}\n");
555 }
556}
557
558fn base64_encode(input: &str) -> String {
560 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
561 let bytes = input.as_bytes();
562 let mut result = String::with_capacity((bytes.len() + 2) / 3 * 4);
563 for chunk in bytes.chunks(3) {
564 let b0 = chunk[0] as u32;
565 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
566 let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
567 let triple = (b0 << 16) | (b1 << 8) | b2;
568 result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
569 result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
570 if chunk.len() > 1 {
571 result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
572 } else {
573 result.push('=');
574 }
575 if chunk.len() > 2 {
576 result.push(CHARS[(triple & 0x3F) as usize] as char);
577 } else {
578 result.push('=');
579 }
580 }
581 result
582}
583
584#[cfg(test)]
585mod tests {
586 use super::*;
587
588 #[test]
589 fn test_generate_conformance_script() {
590 let config = ConformanceConfig {
591 target_url: "http://localhost:8080".to_string(),
592 api_key: None,
593 basic_auth: None,
594 skip_tls_verify: false,
595 categories: None,
596 };
597 let generator = ConformanceGenerator::new(config);
598 let script = generator.generate().unwrap();
599
600 assert!(script.contains("import http from 'k6/http'"));
601 assert!(script.contains("vus: 1"));
602 assert!(script.contains("iterations: 1"));
603 assert!(script.contains("group('Parameters'"));
604 assert!(script.contains("group('Request Bodies'"));
605 assert!(script.contains("group('Schema Types'"));
606 assert!(script.contains("group('Composition'"));
607 assert!(script.contains("group('String Formats'"));
608 assert!(script.contains("group('Constraints'"));
609 assert!(script.contains("group('Response Codes'"));
610 assert!(script.contains("group('HTTP Methods'"));
611 assert!(script.contains("group('Content Types'"));
612 assert!(script.contains("group('Security'"));
613 assert!(script.contains("handleSummary"));
614 }
615
616 #[test]
617 fn test_base64_encode() {
618 assert_eq!(base64_encode("user:pass"), "dXNlcjpwYXNz");
619 assert_eq!(base64_encode("a"), "YQ==");
620 assert_eq!(base64_encode("ab"), "YWI=");
621 assert_eq!(base64_encode("abc"), "YWJj");
622 }
623
624 #[test]
625 fn test_conformance_script_with_custom_auth() {
626 let config = ConformanceConfig {
627 target_url: "https://api.example.com".to_string(),
628 api_key: Some("my-api-key".to_string()),
629 basic_auth: Some("admin:secret".to_string()),
630 skip_tls_verify: true,
631 categories: None,
632 };
633 let generator = ConformanceGenerator::new(config);
634 let script = generator.generate().unwrap();
635
636 assert!(script.contains("insecureSkipTLSVerify: true"));
637 assert!(script.contains("my-api-key"));
638 assert!(script.contains(&base64_encode("admin:secret")));
639 }
640
641 #[test]
642 fn test_should_include_category_none_includes_all() {
643 let config = ConformanceConfig {
644 target_url: "http://localhost:8080".to_string(),
645 api_key: None,
646 basic_auth: None,
647 skip_tls_verify: false,
648 categories: None,
649 };
650 assert!(config.should_include_category("Parameters"));
651 assert!(config.should_include_category("Security"));
652 assert!(config.should_include_category("Anything"));
653 }
654
655 #[test]
656 fn test_should_include_category_filtered() {
657 let config = ConformanceConfig {
658 target_url: "http://localhost:8080".to_string(),
659 api_key: None,
660 basic_auth: None,
661 skip_tls_verify: false,
662 categories: Some(vec!["Parameters".to_string(), "Security".to_string()]),
663 };
664 assert!(config.should_include_category("Parameters"));
665 assert!(config.should_include_category("Security"));
666 assert!(config.should_include_category("parameters")); assert!(!config.should_include_category("Composition"));
668 assert!(!config.should_include_category("Schema Types"));
669 }
670
671 #[test]
672 fn test_generate_with_category_filter() {
673 let config = ConformanceConfig {
674 target_url: "http://localhost:8080".to_string(),
675 api_key: None,
676 basic_auth: None,
677 skip_tls_verify: false,
678 categories: Some(vec!["Parameters".to_string(), "Security".to_string()]),
679 };
680 let generator = ConformanceGenerator::new(config);
681 let script = generator.generate().unwrap();
682
683 assert!(script.contains("group('Parameters'"));
684 assert!(script.contains("group('Security'"));
685 assert!(!script.contains("group('Request Bodies'"));
686 assert!(!script.contains("group('Schema Types'"));
687 assert!(!script.contains("group('Composition'"));
688 }
689}