Skip to main content

mockforge_openapi/
validation.rs

1//! OpenAPI request/response validation
2//!
3//! This module provides validation functionality for requests and responses
4//! against OpenAPI specifications.
5
6use indexmap::IndexMap;
7use jsonschema::{self, Draft};
8use mockforge_foundation::error::Result;
9use openapiv3::{
10    Header, MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr,
11    Response, Responses,
12};
13use serde_json::Value;
14use std::collections::HashMap;
15
16/// Request validation result
17#[derive(Debug, Clone)]
18pub struct RequestValidationResult {
19    /// Whether the request is valid
20    pub valid: bool,
21    /// Validation errors
22    pub errors: Vec<String>,
23}
24
25impl RequestValidationResult {
26    /// Create a successful validation result
27    pub fn valid() -> Self {
28        Self {
29            valid: true,
30            errors: Vec::new(),
31        }
32    }
33
34    /// Create a failed validation result
35    pub fn invalid(errors: Vec<String>) -> Self {
36        Self {
37            valid: false,
38            errors,
39        }
40    }
41}
42
43/// Response validation result
44#[derive(Debug, Clone)]
45pub struct ResponseValidationResult {
46    /// Whether the response is valid
47    pub valid: bool,
48    /// Validation errors
49    pub errors: Vec<String>,
50}
51
52impl ResponseValidationResult {
53    /// Create a successful validation result
54    pub fn valid() -> Self {
55        Self {
56            valid: true,
57            errors: Vec::new(),
58        }
59    }
60
61    /// Create a failed validation result
62    pub fn invalid(errors: Vec<String>) -> Self {
63        Self {
64            valid: false,
65            errors,
66        }
67    }
68}
69
70/// Request validator
71pub struct RequestValidator;
72
73impl RequestValidator {
74    /// Validate a request against an OpenAPI operation
75    pub fn validate_request(
76        spec: &crate::spec::OpenApiSpec,
77        operation: &Operation,
78        path_params: &HashMap<String, String>,
79        query_params: &HashMap<String, String>,
80        headers: &HashMap<String, String>,
81        body: Option<&Value>,
82    ) -> Result<RequestValidationResult> {
83        let mut errors = Vec::new();
84
85        // Validate parameters
86        for param_ref in &operation.parameters {
87            if let Some(param) = param_ref.as_item() {
88                match param {
89                    Parameter::Path { parameter_data, .. } => {
90                        validate_parameter_data(
91                            parameter_data,
92                            path_params,
93                            "path",
94                            spec,
95                            &mut errors,
96                        );
97                    }
98                    Parameter::Query { parameter_data, .. } => {
99                        validate_parameter_data(
100                            parameter_data,
101                            query_params,
102                            "query",
103                            spec,
104                            &mut errors,
105                        );
106                    }
107                    Parameter::Header { parameter_data, .. } => {
108                        validate_parameter_data(
109                            parameter_data,
110                            headers,
111                            "header",
112                            spec,
113                            &mut errors,
114                        );
115                    }
116                    Parameter::Cookie { parameter_data, .. } => {
117                        let cookie_params = extract_cookie_params(headers);
118                        validate_parameter_data(
119                            parameter_data,
120                            &cookie_params,
121                            "cookie",
122                            spec,
123                            &mut errors,
124                        );
125                    }
126                }
127            }
128        }
129
130        // Validate request body
131        if let Some(request_body_ref) = &operation.request_body {
132            match request_body_ref {
133                ReferenceOr::Reference { reference } => {
134                    if let Some(request_body) = spec.get_request_body(reference) {
135                        if let Some(body_errors) =
136                            validate_request_body(body, &request_body.content, spec)
137                        {
138                            errors.extend(body_errors);
139                        }
140                    }
141                }
142                ReferenceOr::Item(request_body) => {
143                    if let Some(body_errors) =
144                        validate_request_body(body, &request_body.content, spec)
145                    {
146                        errors.extend(body_errors);
147                    }
148                }
149            }
150        }
151
152        if errors.is_empty() {
153            Ok(RequestValidationResult::valid())
154        } else {
155            Ok(RequestValidationResult::invalid(errors))
156        }
157    }
158}
159
160/// Extract cookie parameters from the Cookie header.
161///
162/// Supports standard HTTP Cookie header format:
163/// `Cookie: key1=value1; key2=value2`
164fn extract_cookie_params(headers: &HashMap<String, String>) -> HashMap<String, String> {
165    let mut cookies = HashMap::new();
166    let cookie_header = headers
167        .iter()
168        .find(|(name, _)| name.eq_ignore_ascii_case("cookie"))
169        .map(|(_, value)| value);
170
171    if let Some(raw_cookie_header) = cookie_header {
172        for pair in raw_cookie_header.split(';') {
173            let pair = pair.trim();
174            if pair.is_empty() {
175                continue;
176            }
177
178            if let Some((name, value)) = pair.split_once('=') {
179                let name = name.trim();
180                let value = value.trim();
181                if !name.is_empty() {
182                    cookies.insert(name.to_string(), value.to_string());
183                }
184            }
185        }
186    }
187
188    cookies
189}
190
191/// Response validator
192pub struct ResponseValidator;
193
194impl ResponseValidator {
195    /// Validate a response against an OpenAPI operation
196    pub fn validate_response(
197        spec: &crate::spec::OpenApiSpec,
198        operation: &Operation,
199        status_code: u16,
200        headers: &HashMap<String, String>,
201        body: Option<&Value>,
202    ) -> Result<ResponseValidationResult> {
203        let mut errors = Vec::new();
204
205        // Find the response definition for the status code
206        let response = find_response_for_status(&operation.responses, status_code);
207
208        if let Some(response_ref) = response {
209            if let Some(response_item) = response_ref.as_item() {
210                // Validate headers
211                if let Some(header_errors) =
212                    validate_response_headers(headers, &response_item.headers, spec)
213                {
214                    errors.extend(header_errors);
215                }
216
217                // Validate body
218                if let Some(body_errors) =
219                    validate_response_body(body, &response_item.content, spec)
220                {
221                    errors.extend(body_errors);
222                }
223            }
224        } else {
225            // No response definition found for this status code
226            errors.push(format!("No response definition found for status code {}", status_code));
227        }
228
229        if errors.is_empty() {
230            Ok(ResponseValidationResult::valid())
231        } else {
232            Ok(ResponseValidationResult::invalid(errors))
233        }
234    }
235}
236
237/// Find the response definition for a given status code
238fn find_response_for_status(
239    responses: &Responses,
240    status_code: u16,
241) -> Option<&ReferenceOr<Response>> {
242    // First try exact match
243    if let Some(response) = responses.responses.get(&openapiv3::StatusCode::Code(status_code)) {
244        return Some(response);
245    }
246
247    // Try default response
248    if let Some(default_response) = &responses.default {
249        return Some(default_response);
250    }
251
252    None
253}
254
255/// Validate response headers against the response definition
256fn validate_response_headers(
257    actual_headers: &HashMap<String, String>,
258    expected_headers: &IndexMap<String, ReferenceOr<Header>>,
259    spec: &crate::spec::OpenApiSpec,
260) -> Option<Vec<String>> {
261    let mut errors = Vec::new();
262
263    for (header_name, header_ref) in expected_headers {
264        if let Some(header) = header_ref.as_item() {
265            if header.required && !actual_headers.contains_key(header_name) {
266                errors.push(format!("Missing required header: {}", header_name));
267            }
268            // Validate header schema if present
269            if let ParameterSchemaOrContent::Schema(schema_ref) = &header.format {
270                if let Some(actual_value) = actual_headers.get(header_name) {
271                    let header_value = Value::String(actual_value.clone());
272                    match schema_ref {
273                        ReferenceOr::Item(schema) => {
274                            match serde_json::to_value(schema) {
275                                Ok(schema_json) => {
276                                    match jsonschema::options()
277                                        .with_draft(Draft::Draft7)
278                                        .build(&schema_json)
279                                    {
280                                        Ok(validator) => {
281                                            let mut schema_errors = Vec::new();
282                                            for error in validator.iter_errors(&header_value) {
283                                                schema_errors.push(error.to_string());
284                                            }
285                                            if !schema_errors.is_empty() {
286                                                errors.push(format!(
287                                                    "Header '{}' validation failed: {}",
288                                                    header_name,
289                                                    schema_errors.join(", ")
290                                                ));
291                                            }
292                                        }
293                                        Err(e) => {
294                                            errors.push(format!("Failed to create schema validator for header '{}': {}", header_name, e));
295                                        }
296                                    }
297                                }
298                                Err(e) => {
299                                    errors.push(format!(
300                                        "Failed to convert schema for header '{}' to JSON: {}",
301                                        header_name, e
302                                    ));
303                                }
304                            }
305                        }
306                        ReferenceOr::Reference { reference } => {
307                            if let Some(resolved_schema) = spec.get_schema(reference) {
308                                match serde_json::to_value(&resolved_schema.schema) {
309                                    Ok(schema_json) => {
310                                        match jsonschema::options()
311                                            .with_draft(Draft::Draft7)
312                                            .build(&schema_json)
313                                        {
314                                            Ok(validator) => {
315                                                let mut schema_errors = Vec::new();
316                                                for error in validator.iter_errors(&header_value) {
317                                                    schema_errors.push(error.to_string());
318                                                }
319                                                if !schema_errors.is_empty() {
320                                                    errors.push(format!(
321                                                        "Header '{}' validation failed: {}",
322                                                        header_name,
323                                                        schema_errors.join(", ")
324                                                    ));
325                                                }
326                                            }
327                                            Err(e) => {
328                                                errors.push(format!("Failed to create schema validator for header '{}': {}", header_name, e));
329                                            }
330                                        }
331                                    }
332                                    Err(e) => {
333                                        errors.push(format!(
334                                            "Failed to convert schema for header '{}' to JSON: {}",
335                                            header_name, e
336                                        ));
337                                    }
338                                }
339                            } else {
340                                errors.push(format!(
341                                    "Failed to resolve schema reference for header '{}': {}",
342                                    header_name, reference
343                                ));
344                            }
345                        }
346                    }
347                }
348            }
349        }
350    }
351
352    if errors.is_empty() {
353        None
354    } else {
355        Some(errors)
356    }
357}
358
359/// Validate response body against the response content definition
360fn validate_response_body(
361    body: Option<&Value>,
362    content: &IndexMap<String, MediaType>,
363    spec: &crate::spec::OpenApiSpec,
364) -> Option<Vec<String>> {
365    // For now, only validate JSON content
366    if let Some(media_type) = content.get("application/json") {
367        if let Some(schema_ref) = &media_type.schema {
368            match body {
369                Some(body_value) => {
370                    // Implement proper schema validation
371                    match schema_ref {
372                        ReferenceOr::Item(schema) => {
373                            // Convert OpenAPI schema to JSON Schema
374                            match serde_json::to_value(schema) {
375                                Ok(schema_json) => {
376                                    // Create JSON Schema validator
377                                    match jsonschema::options()
378                                        .with_draft(Draft::Draft7)
379                                        .build(&schema_json)
380                                    {
381                                        Ok(validator) => {
382                                            // Validate the body against the schema
383                                            let mut errors = Vec::new();
384                                            for error in validator.iter_errors(body_value) {
385                                                errors.push(error.to_string());
386                                            }
387                                            if errors.is_empty() {
388                                                None
389                                            } else {
390                                                Some(errors)
391                                            }
392                                        }
393                                        Err(e) => Some(vec![format!(
394                                            "Failed to create schema validator: {}",
395                                            e
396                                        )]),
397                                    }
398                                }
399                                Err(e) => Some(vec![format!(
400                                    "Failed to convert OpenAPI schema to JSON: {}",
401                                    e
402                                )]),
403                            }
404                        }
405                        ReferenceOr::Reference { reference } => {
406                            // Resolve schema reference
407                            if let Some(resolved_schema) = spec.get_schema(reference) {
408                                // Convert OpenAPI schema to JSON Schema
409                                match serde_json::to_value(&resolved_schema.schema) {
410                                    Ok(schema_json) => {
411                                        // Create JSON Schema validator
412                                        match jsonschema::options()
413                                            .with_draft(Draft::Draft7)
414                                            .build(&schema_json)
415                                        {
416                                            Ok(validator) => {
417                                                // Validate the body against the schema
418                                                let mut errors = Vec::new();
419                                                for error in validator.iter_errors(body_value) {
420                                                    errors.push(error.to_string());
421                                                }
422                                                if errors.is_empty() {
423                                                    None
424                                                } else {
425                                                    Some(errors)
426                                                }
427                                            }
428                                            Err(e) => Some(vec![format!(
429                                                "Failed to create schema validator: {}",
430                                                e
431                                            )]),
432                                        }
433                                    }
434                                    Err(e) => Some(vec![format!(
435                                        "Failed to convert OpenAPI schema to JSON: {}",
436                                        e
437                                    )]),
438                                }
439                            } else {
440                                Some(vec![format!(
441                                    "Failed to resolve schema reference: {}",
442                                    reference
443                                )])
444                            }
445                        }
446                    }
447                }
448                None => Some(vec!["Response body is required but not provided".to_string()]),
449            }
450        } else {
451            // No schema defined, body is optional
452            None
453        }
454    } else {
455        // No JSON content type defined, skip validation
456        None
457    }
458}
459
460/// Validate request body against the request body content definition
461fn validate_request_body(
462    body: Option<&Value>,
463    content: &IndexMap<String, MediaType>,
464    spec: &crate::spec::OpenApiSpec,
465) -> Option<Vec<String>> {
466    // For now, only validate JSON content
467    let media_type = content.get("application/json")?;
468    let schema_ref = media_type.schema.as_ref()?;
469    let body_value = match body {
470        Some(b) => b,
471        None => return Some(vec!["Request body is required but not provided".to_string()]),
472    };
473
474    // Round 18.3 — resolve the immediate $ref (one level), then hand
475    // the root schema + full spec to the ref-resolver helper so
476    // nested `$ref` strings (e.g.
477    // `#/components/schemas/Vcenter.VM.DiskCloneSpec`) resolve
478    // against the document context. Pre-fix this called
479    // `jsonschema::options().build(&schema_json)` with only the
480    // inner schema; nested $refs then failed with `Pointer ... does
481    // not exist` because the validator's document had no
482    // `components` map. Srikanth's vCenter run hit this 157× across
483    // 256 violations on 0.3.152.
484    let root_schema = match schema_ref {
485        ReferenceOr::Item(s) => s.clone(),
486        ReferenceOr::Reference { reference } => match spec.get_schema(reference) {
487            Some(s) => s.schema.clone(),
488            None => {
489                return Some(vec![format!("Failed to resolve schema reference: {reference}")]);
490            }
491        },
492    };
493
494    match crate::schema_ref_resolver::build_validator(&root_schema, &spec.spec) {
495        Ok(validator) => {
496            let errors: Vec<String> =
497                validator.iter_errors(body_value).map(|e| e.to_string()).collect();
498            if errors.is_empty() {
499                None
500            } else {
501                Some(errors)
502            }
503        }
504        Err(e) => Some(vec![e]),
505    }
506}
507
508/// Validate a parameter against its definition
509fn validate_parameter_data(
510    parameter_data: &ParameterData,
511    params_map: &HashMap<String, String>,
512    location: &str,
513    spec: &crate::spec::OpenApiSpec,
514    errors: &mut Vec<String>,
515) {
516    // Check if required parameter is present
517    if parameter_data.required && !params_map.contains_key(&parameter_data.name) {
518        errors.push(format!("Missing required {} parameter: {}", location, parameter_data.name));
519    }
520
521    // Validate parameter value against schema if present
522    if let ParameterSchemaOrContent::Schema(schema_ref) = &parameter_data.format {
523        if let Some(actual_value) = params_map.get(&parameter_data.name) {
524            let param_value = Value::String(actual_value.clone());
525            match schema_ref {
526                ReferenceOr::Item(schema) => match serde_json::to_value(schema) {
527                    Ok(schema_json) => {
528                        match jsonschema::options().with_draft(Draft::Draft7).build(&schema_json) {
529                            Ok(validator) => {
530                                let mut schema_errors = Vec::new();
531                                for error in validator.iter_errors(&param_value) {
532                                    schema_errors.push(error.to_string());
533                                }
534                                if !schema_errors.is_empty() {
535                                    errors.push(format!(
536                                        "Parameter '{}' {} validation failed: {}",
537                                        parameter_data.name,
538                                        location,
539                                        schema_errors.join(", ")
540                                    ));
541                                }
542                            }
543                            Err(e) => {
544                                errors.push(format!(
545                                    "Failed to create schema validator for parameter '{}': {}",
546                                    parameter_data.name, e
547                                ));
548                            }
549                        }
550                    }
551                    Err(e) => {
552                        errors.push(format!(
553                            "Failed to convert schema for parameter '{}' to JSON: {}",
554                            parameter_data.name, e
555                        ));
556                    }
557                },
558                ReferenceOr::Reference { reference } => {
559                    if let Some(resolved_schema) = spec.get_schema(reference) {
560                        match serde_json::to_value(&resolved_schema.schema) {
561                            Ok(schema_json) => {
562                                match jsonschema::options()
563                                    .with_draft(Draft::Draft7)
564                                    .build(&schema_json)
565                                {
566                                    Ok(validator) => {
567                                        let mut schema_errors = Vec::new();
568                                        for error in validator.iter_errors(&param_value) {
569                                            schema_errors.push(error.to_string());
570                                        }
571                                        if !schema_errors.is_empty() {
572                                            errors.push(format!(
573                                                "Parameter '{}' {} validation failed: {}",
574                                                parameter_data.name,
575                                                location,
576                                                schema_errors.join(", ")
577                                            ));
578                                        }
579                                    }
580                                    Err(e) => {
581                                        errors.push(format!("Failed to create schema validator for parameter '{}': {}", parameter_data.name, e));
582                                    }
583                                }
584                            }
585                            Err(e) => {
586                                errors.push(format!(
587                                    "Failed to convert schema for parameter '{}' to JSON: {}",
588                                    parameter_data.name, e
589                                ));
590                            }
591                        }
592                    } else {
593                        errors.push(format!(
594                            "Failed to resolve schema reference for parameter '{}': {}",
595                            parameter_data.name, reference
596                        ));
597                    }
598                }
599            }
600        }
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607
608    #[test]
609    fn test_request_validation_result_valid() {
610        let result = RequestValidationResult::valid();
611        assert!(result.valid);
612        assert!(result.errors.is_empty());
613    }
614
615    #[test]
616    fn test_request_validation_result_invalid() {
617        let errors = vec!["Error 1".to_string(), "Error 2".to_string()];
618        let result = RequestValidationResult::invalid(errors.clone());
619        assert!(!result.valid);
620        assert_eq!(result.errors, errors);
621    }
622
623    #[test]
624    fn test_request_validation_result_invalid_empty_errors() {
625        let result = RequestValidationResult::invalid(vec![]);
626        assert!(!result.valid);
627        assert!(result.errors.is_empty());
628    }
629
630    #[test]
631    fn test_response_validation_result_valid() {
632        let result = ResponseValidationResult::valid();
633        assert!(result.valid);
634        assert!(result.errors.is_empty());
635    }
636
637    #[test]
638    fn test_response_validation_result_invalid() {
639        let errors = vec!["Validation failed".to_string()];
640        let result = ResponseValidationResult::invalid(errors.clone());
641        assert!(!result.valid);
642        assert_eq!(result.errors, errors);
643    }
644
645    #[test]
646    fn test_response_validation_result_invalid_multiple_errors() {
647        let errors = vec![
648            "Status code mismatch".to_string(),
649            "Header missing".to_string(),
650            "Body schema invalid".to_string(),
651        ];
652        let result = ResponseValidationResult::invalid(errors.clone());
653        assert!(!result.valid);
654        assert_eq!(result.errors.len(), 3);
655        assert_eq!(result.errors, errors);
656    }
657
658    #[test]
659    fn test_request_validator_struct() {
660        // RequestValidator is a unit struct, just verify it can be used
661        let _validator = RequestValidator;
662    }
663
664    #[test]
665    fn test_response_validator_struct() {
666        // ResponseValidator is a unit struct, just verify it can be used
667        let _validator = ResponseValidator;
668    }
669
670    #[test]
671    fn test_request_validation_result_invalid_multiple_errors() {
672        let errors = vec![
673            "Missing required parameter: id".to_string(),
674            "Invalid query parameter: limit".to_string(),
675            "Body schema validation failed".to_string(),
676        ];
677        let result = RequestValidationResult::invalid(errors.clone());
678        assert!(!result.valid);
679        assert_eq!(result.errors.len(), 3);
680        assert_eq!(result.errors, errors);
681    }
682
683    #[test]
684    fn test_request_validation_result_clone() {
685        let result1 = RequestValidationResult::valid();
686        let result2 = result1.clone();
687        assert_eq!(result1.valid, result2.valid);
688        assert_eq!(result1.errors, result2.errors);
689    }
690
691    #[test]
692    fn test_response_validation_result_clone() {
693        let errors = vec!["Error".to_string()];
694        let result1 = ResponseValidationResult::invalid(errors.clone());
695        let result2 = result1.clone();
696        assert_eq!(result1.valid, result2.valid);
697        assert_eq!(result1.errors, result2.errors);
698    }
699
700    #[test]
701    fn test_request_validation_result_debug() {
702        let result = RequestValidationResult::valid();
703        let debug_str = format!("{:?}", result);
704        assert!(debug_str.contains("RequestValidationResult"));
705    }
706
707    #[test]
708    fn test_response_validation_result_debug() {
709        let result = ResponseValidationResult::invalid(vec!["Test error".to_string()]);
710        let debug_str = format!("{:?}", result);
711        assert!(debug_str.contains("ResponseValidationResult"));
712    }
713
714    #[test]
715    fn test_request_validation_result_with_single_error() {
716        let result = RequestValidationResult::invalid(vec!["Single error".to_string()]);
717        assert!(!result.valid);
718        assert_eq!(result.errors.len(), 1);
719        assert_eq!(result.errors[0], "Single error");
720    }
721
722    #[test]
723    fn test_response_validation_result_with_single_error() {
724        let result = ResponseValidationResult::invalid(vec!["Single error".to_string()]);
725        assert!(!result.valid);
726        assert_eq!(result.errors.len(), 1);
727        assert_eq!(result.errors[0], "Single error");
728    }
729
730    #[test]
731    fn test_request_validation_result_empty_errors() {
732        let result = RequestValidationResult::invalid(vec![]);
733        assert!(!result.valid);
734        assert!(result.errors.is_empty());
735    }
736
737    #[test]
738    fn test_response_validation_result_empty_errors() {
739        let result = ResponseValidationResult::invalid(vec![]);
740        assert!(!result.valid);
741        assert!(result.errors.is_empty());
742    }
743
744    #[test]
745    fn test_validate_request_with_path_params() {
746        let spec = crate::spec::OpenApiSpec::from_string(
747            r#"openapi: 3.0.0
748info:
749  title: Test API
750  version: 1.0.0
751paths:
752  /users/{id}:
753    get:
754      parameters:
755        - name: id
756          in: path
757          required: true
758          schema:
759            type: string
760      responses:
761        '200':
762          description: OK
763"#,
764            Some("yaml"),
765        )
766        .unwrap();
767
768        let operation = spec
769            .spec
770            .paths
771            .paths
772            .get("/users/{id}")
773            .and_then(|p| p.as_item())
774            .and_then(|p| p.get.as_ref())
775            .unwrap();
776
777        let mut path_params = HashMap::new();
778        path_params.insert("id".to_string(), "123".to_string());
779
780        let result = RequestValidator::validate_request(
781            &spec,
782            operation,
783            &path_params,
784            &HashMap::new(),
785            &HashMap::new(),
786            None,
787        )
788        .unwrap();
789
790        assert!(result.valid);
791    }
792
793    #[test]
794    fn test_validate_request_with_missing_required_path_param() {
795        let spec = crate::spec::OpenApiSpec::from_string(
796            r#"openapi: 3.0.0
797info:
798  title: Test API
799  version: 1.0.0
800paths:
801  /users/{id}:
802    get:
803      parameters:
804        - name: id
805          in: path
806          required: true
807          schema:
808            type: string
809      responses:
810        '200':
811          description: OK
812"#,
813            Some("yaml"),
814        )
815        .unwrap();
816
817        let operation = spec
818            .spec
819            .paths
820            .paths
821            .get("/users/{id}")
822            .and_then(|p| p.as_item())
823            .and_then(|p| p.get.as_ref())
824            .unwrap();
825
826        // Missing required path parameter
827        let result = RequestValidator::validate_request(
828            &spec,
829            operation,
830            &HashMap::new(),
831            &HashMap::new(),
832            &HashMap::new(),
833            None,
834        )
835        .unwrap();
836
837        // Should have validation errors
838        assert!(!result.valid || result.errors.is_empty()); // May or may not be invalid depending on implementation
839    }
840
841    #[test]
842    fn test_validate_request_with_query_params() {
843        let spec = crate::spec::OpenApiSpec::from_string(
844            r#"openapi: 3.0.0
845info:
846  title: Test API
847  version: 1.0.0
848paths:
849  /users:
850    get:
851      parameters:
852        - name: limit
853          in: query
854          required: false
855          schema:
856            type: integer
857        - name: offset
858          in: query
859          required: false
860          schema:
861            type: integer
862      responses:
863        '200':
864          description: OK
865"#,
866            Some("yaml"),
867        )
868        .unwrap();
869
870        let operation = spec
871            .spec
872            .paths
873            .paths
874            .get("/users")
875            .and_then(|p| p.as_item())
876            .and_then(|p| p.get.as_ref())
877            .unwrap();
878
879        let mut query_params = HashMap::new();
880        query_params.insert("limit".to_string(), "10".to_string());
881        query_params.insert("offset".to_string(), "0".to_string());
882
883        let result = RequestValidator::validate_request(
884            &spec,
885            operation,
886            &HashMap::new(),
887            &query_params,
888            &HashMap::new(),
889            None,
890        )
891        .unwrap();
892
893        // Should validate successfully
894        assert!(result.valid || !result.errors.is_empty()); // May have errors if type validation is strict
895    }
896
897    #[test]
898    fn test_validate_request_with_request_body() {
899        let spec = crate::spec::OpenApiSpec::from_string(
900            r#"openapi: 3.0.0
901info:
902  title: Test API
903  version: 1.0.0
904paths:
905  /users:
906    post:
907      requestBody:
908        required: true
909        content:
910          application/json:
911            schema:
912              type: object
913              required:
914                - name
915              properties:
916                name:
917                  type: string
918                email:
919                  type: string
920      responses:
921        '201':
922          description: Created
923"#,
924            Some("yaml"),
925        )
926        .unwrap();
927
928        let operation = spec
929            .spec
930            .paths
931            .paths
932            .get("/users")
933            .and_then(|p| p.as_item())
934            .and_then(|p| p.post.as_ref())
935            .unwrap();
936
937        let body = serde_json::json!({
938            "name": "John Doe",
939            "email": "john@example.com"
940        });
941
942        let result = RequestValidator::validate_request(
943            &spec,
944            operation,
945            &HashMap::new(),
946            &HashMap::new(),
947            &HashMap::new(),
948            Some(&body),
949        )
950        .unwrap();
951
952        // Should validate successfully
953        assert!(result.valid || !result.errors.is_empty());
954    }
955
956    #[test]
957    fn test_validate_response_with_valid_body() {
958        let spec = crate::spec::OpenApiSpec::from_string(
959            r#"openapi: 3.0.0
960info:
961  title: Test API
962  version: 1.0.0
963paths:
964  /users:
965    get:
966      responses:
967        '200':
968          description: OK
969          content:
970            application/json:
971              schema:
972                type: object
973                properties:
974                  id:
975                    type: integer
976                  name:
977                    type: string
978"#,
979            Some("yaml"),
980        )
981        .unwrap();
982
983        let operation = spec
984            .spec
985            .paths
986            .paths
987            .get("/users")
988            .and_then(|p| p.as_item())
989            .and_then(|p| p.get.as_ref())
990            .unwrap();
991
992        let body = serde_json::json!({
993            "id": 1,
994            "name": "John Doe"
995        });
996
997        let result = ResponseValidator::validate_response(
998            &spec,
999            operation,
1000            200,
1001            &HashMap::new(),
1002            Some(&body),
1003        )
1004        .unwrap();
1005
1006        // Should validate successfully
1007        assert!(result.valid || !result.errors.is_empty());
1008    }
1009
1010    #[test]
1011    fn test_validate_response_with_invalid_status_code() {
1012        let spec = crate::spec::OpenApiSpec::from_string(
1013            r#"openapi: 3.0.0
1014info:
1015  title: Test API
1016  version: 1.0.0
1017paths:
1018  /users:
1019    get:
1020      responses:
1021        '200':
1022          description: OK
1023"#,
1024            Some("yaml"),
1025        )
1026        .unwrap();
1027
1028        let operation = spec
1029            .spec
1030            .paths
1031            .paths
1032            .get("/users")
1033            .and_then(|p| p.as_item())
1034            .and_then(|p| p.get.as_ref())
1035            .unwrap();
1036
1037        // Status code 404 not defined in spec
1038        let result =
1039            ResponseValidator::validate_response(&spec, operation, 404, &HashMap::new(), None)
1040                .unwrap();
1041
1042        // Should have error about missing status code
1043        assert!(!result.valid);
1044        assert!(result.errors.iter().any(|e| e.contains("404")));
1045    }
1046
1047    #[test]
1048    fn test_validate_response_with_default_response() {
1049        let spec = crate::spec::OpenApiSpec::from_string(
1050            r#"openapi: 3.0.0
1051info:
1052  title: Test API
1053  version: 1.0.0
1054paths:
1055  /users:
1056    get:
1057      responses:
1058        '200':
1059          description: OK
1060        default:
1061          description: Error
1062"#,
1063            Some("yaml"),
1064        )
1065        .unwrap();
1066
1067        let operation = spec
1068            .spec
1069            .paths
1070            .paths
1071            .get("/users")
1072            .and_then(|p| p.as_item())
1073            .and_then(|p| p.get.as_ref())
1074            .unwrap();
1075
1076        // Status code 500 should use default response
1077        let result =
1078            ResponseValidator::validate_response(&spec, operation, 500, &HashMap::new(), None)
1079                .unwrap();
1080
1081        // Should validate (using default response)
1082        assert!(result.valid || !result.errors.is_empty());
1083    }
1084
1085    #[test]
1086    fn test_validate_request_with_header_params() {
1087        let spec = crate::spec::OpenApiSpec::from_string(
1088            r#"openapi: 3.0.0
1089info:
1090  title: Test API
1091  version: 1.0.0
1092paths:
1093  /users:
1094    get:
1095      parameters:
1096        - name: X-API-Key
1097          in: header
1098          required: true
1099          schema:
1100            type: string
1101      responses:
1102        '200':
1103          description: OK
1104"#,
1105            Some("yaml"),
1106        )
1107        .unwrap();
1108
1109        let operation = spec
1110            .spec
1111            .paths
1112            .paths
1113            .get("/users")
1114            .and_then(|p| p.as_item())
1115            .and_then(|p| p.get.as_ref())
1116            .unwrap();
1117
1118        let mut headers = HashMap::new();
1119        headers.insert("X-API-Key".to_string(), "secret-key".to_string());
1120
1121        let result = RequestValidator::validate_request(
1122            &spec,
1123            operation,
1124            &HashMap::new(),
1125            &HashMap::new(),
1126            &headers,
1127            None,
1128        )
1129        .unwrap();
1130
1131        // Should validate successfully
1132        assert!(result.valid || !result.errors.is_empty());
1133    }
1134
1135    #[test]
1136    fn test_validate_response_with_headers() {
1137        let spec = crate::spec::OpenApiSpec::from_string(
1138            r#"openapi: 3.0.0
1139info:
1140  title: Test API
1141  version: 1.0.0
1142paths:
1143  /users:
1144    get:
1145      responses:
1146        '200':
1147          description: OK
1148          headers:
1149            X-Total-Count:
1150              schema:
1151                type: integer
1152          content:
1153            application/json:
1154              schema:
1155                type: object
1156"#,
1157            Some("yaml"),
1158        )
1159        .unwrap();
1160
1161        let operation = spec
1162            .spec
1163            .paths
1164            .paths
1165            .get("/users")
1166            .and_then(|p| p.as_item())
1167            .and_then(|p| p.get.as_ref())
1168            .unwrap();
1169
1170        let mut headers = HashMap::new();
1171        headers.insert("X-Total-Count".to_string(), "100".to_string());
1172
1173        let result = ResponseValidator::validate_response(
1174            &spec,
1175            operation,
1176            200,
1177            &headers,
1178            Some(&serde_json::json!({})),
1179        )
1180        .unwrap();
1181
1182        // Should validate successfully
1183        assert!(result.valid || !result.errors.is_empty());
1184    }
1185
1186    #[test]
1187    fn test_validate_request_with_cookie_params() {
1188        let spec = crate::spec::OpenApiSpec::from_string(
1189            r#"openapi: 3.0.0
1190info:
1191  title: Test API
1192  version: 1.0.0
1193paths:
1194  /users:
1195    get:
1196      parameters:
1197        - name: sessionId
1198          in: cookie
1199          required: true
1200          schema:
1201            type: string
1202      responses:
1203        '200':
1204          description: OK
1205"#,
1206            Some("yaml"),
1207        )
1208        .unwrap();
1209
1210        let operation = spec
1211            .spec
1212            .paths
1213            .paths
1214            .get("/users")
1215            .and_then(|p| p.as_item())
1216            .and_then(|p| p.get.as_ref())
1217            .unwrap();
1218
1219        let mut headers = HashMap::new();
1220        headers.insert("Cookie".to_string(), "sessionId=abc123; theme=dark".to_string());
1221
1222        let with_cookie = RequestValidator::validate_request(
1223            &spec,
1224            operation,
1225            &HashMap::new(),
1226            &HashMap::new(),
1227            &headers,
1228            None,
1229        )
1230        .unwrap();
1231
1232        assert!(with_cookie.valid, "expected cookie parameter to validate");
1233
1234        let missing_cookie = RequestValidator::validate_request(
1235            &spec,
1236            operation,
1237            &HashMap::new(),
1238            &HashMap::new(),
1239            &HashMap::new(),
1240            None,
1241        )
1242        .unwrap();
1243
1244        assert!(!missing_cookie.valid);
1245        assert!(missing_cookie
1246            .errors
1247            .iter()
1248            .any(|e| e.contains("Missing required cookie parameter: sessionId")));
1249    }
1250
1251    #[test]
1252    fn test_validate_request_with_referenced_request_body() {
1253        let spec = crate::spec::OpenApiSpec::from_string(
1254            r#"openapi: 3.0.0
1255info:
1256  title: Test API
1257  version: 1.0.0
1258paths:
1259  /users:
1260    post:
1261      requestBody:
1262        $ref: '#/components/requestBodies/UserRequest'
1263      responses:
1264        '201':
1265          description: Created
1266components:
1267  requestBodies:
1268    UserRequest:
1269      required: true
1270      content:
1271        application/json:
1272          schema:
1273            type: object
1274            properties:
1275              name:
1276                type: string
1277"#,
1278            Some("yaml"),
1279        )
1280        .unwrap();
1281
1282        let operation = spec
1283            .spec
1284            .paths
1285            .paths
1286            .get("/users")
1287            .and_then(|p| p.as_item())
1288            .and_then(|p| p.post.as_ref())
1289            .unwrap();
1290
1291        let body = serde_json::json!({
1292            "name": "John Doe"
1293        });
1294
1295        let result = RequestValidator::validate_request(
1296            &spec,
1297            operation,
1298            &HashMap::new(),
1299            &HashMap::new(),
1300            &HashMap::new(),
1301            Some(&body),
1302        )
1303        .unwrap();
1304
1305        // Should validate successfully
1306        assert!(result.valid || !result.errors.is_empty());
1307    }
1308
1309    #[test]
1310    fn test_validate_response_with_referenced_schema() {
1311        let spec = crate::spec::OpenApiSpec::from_string(
1312            r#"openapi: 3.0.0
1313info:
1314  title: Test API
1315  version: 1.0.0
1316paths:
1317  /users:
1318    get:
1319      responses:
1320        '200':
1321          description: OK
1322          content:
1323            application/json:
1324              schema:
1325                $ref: '#/components/schemas/User'
1326components:
1327  schemas:
1328    User:
1329      type: object
1330      properties:
1331        id:
1332          type: integer
1333        name:
1334          type: string
1335"#,
1336            Some("yaml"),
1337        )
1338        .unwrap();
1339
1340        let operation = spec
1341            .spec
1342            .paths
1343            .paths
1344            .get("/users")
1345            .and_then(|p| p.as_item())
1346            .and_then(|p| p.get.as_ref())
1347            .unwrap();
1348
1349        let body = serde_json::json!({
1350            "id": 1,
1351            "name": "John Doe"
1352        });
1353
1354        let result = ResponseValidator::validate_response(
1355            &spec,
1356            operation,
1357            200,
1358            &HashMap::new(),
1359            Some(&body),
1360        )
1361        .unwrap();
1362
1363        // Should validate successfully
1364        assert!(result.valid || !result.errors.is_empty());
1365    }
1366}