Skip to main content

mockforge_bench/conformance/
generator.rs

1//! k6 script generator for OpenAPI 3.0.0 conformance testing
2
3use crate::error::{BenchError, Result};
4use std::path::Path;
5
6/// Configuration for conformance test generation
7pub struct ConformanceConfig {
8    /// Target base URL
9    pub target_url: String,
10    /// API key for security scheme tests
11    pub api_key: Option<String>,
12    /// Basic auth credentials (user:pass) for security scheme tests
13    pub basic_auth: Option<String>,
14    /// Skip TLS verification
15    pub skip_tls_verify: bool,
16    /// Optional category filter — None means all categories
17    pub categories: Option<Vec<String>>,
18}
19
20impl ConformanceConfig {
21    /// Check if a category should be included based on the filter
22    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
30/// Generates k6 scripts for OpenAPI 3.0.0 conformance testing
31pub struct ConformanceGenerator {
32    config: ConformanceConfig,
33}
34
35impl ConformanceGenerator {
36    pub fn new(config: ConformanceConfig) -> Self {
37        Self { config }
38    }
39
40    /// Generate the conformance test k6 script
41    pub fn generate(&self) -> Result<String> {
42        let mut script = String::with_capacity(16384);
43
44        // Imports
45        script.push_str("import http from 'k6/http';\n");
46        script.push_str("import { check, group } from 'k6';\n\n");
47
48        // Options: 1 VU, 1 iteration (functional test, not load test)
49        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        // Base URL
61        script.push_str(&format!("const BASE_URL = '{}';\n\n", self.config.target_url));
62
63        // Helper: JSON headers
64        script.push_str("const JSON_HEADERS = { 'Content-Type': 'application/json' };\n\n");
65
66        // Default function
67        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        // handleSummary for conformance report output
103        self.generate_handle_summary(&mut script);
104
105        Ok(script)
106    }
107
108    /// Write the generated script to a file
109    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        // Path param: string
122        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        // Path param: integer
130        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        // Query param: string
138        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        // Query param: integer
148        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        // Query param: array
158        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        // Header param
168        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        // Cookie param
178        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        // JSON body
194        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        // Form-urlencoded body
204        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        // Multipart body
214        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        // Required field
315        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        // Optional field
325        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        // Min/max
335        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        // Pattern
345        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        // Enum
355        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        // GET
398        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        // POST
406        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        // PUT
416        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        // PATCH
426        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        // DELETE
436        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        // HEAD
444        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        // OPTIONS
452        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        // Bearer token
481        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        // API Key
491        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        // Basic auth
503        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
558/// Simple base64 encoding for basic auth
559fn 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")); // case-insensitive
667        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}