Skip to main content

mockforge_core/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 crate::Result;
7use indexmap::IndexMap;
8use jsonschema::{self, Draft};
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::openapi::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::openapi::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::openapi::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::openapi::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::openapi::OpenApiSpec,
465) -> Option<Vec<String>> {
466    // For now, only validate JSON content
467    if let Some(media_type) = content.get("application/json") {
468        if let Some(schema_ref) = &media_type.schema {
469            match body {
470                Some(body_value) => {
471                    // Implement proper schema validation
472                    match schema_ref {
473                        ReferenceOr::Item(schema) => {
474                            // Convert OpenAPI schema to JSON Schema
475                            match serde_json::to_value(schema) {
476                                Ok(schema_json) => {
477                                    // Create JSON Schema validator
478                                    match jsonschema::options()
479                                        .with_draft(Draft::Draft7)
480                                        .build(&schema_json)
481                                    {
482                                        Ok(validator) => {
483                                            // Validate the body against the schema
484                                            let mut errors = Vec::new();
485                                            for error in validator.iter_errors(body_value) {
486                                                errors.push(error.to_string());
487                                            }
488                                            if errors.is_empty() {
489                                                None
490                                            } else {
491                                                Some(errors)
492                                            }
493                                        }
494                                        Err(e) => Some(vec![format!(
495                                            "Failed to create schema validator: {}",
496                                            e
497                                        )]),
498                                    }
499                                }
500                                Err(e) => Some(vec![format!(
501                                    "Failed to convert OpenAPI schema to JSON: {}",
502                                    e
503                                )]),
504                            }
505                        }
506                        ReferenceOr::Reference { reference } => {
507                            // Resolve schema reference
508                            if let Some(resolved_schema) = spec.get_schema(reference) {
509                                // Convert OpenAPI schema to JSON Schema
510                                match serde_json::to_value(&resolved_schema.schema) {
511                                    Ok(schema_json) => {
512                                        // Create JSON Schema validator
513                                        match jsonschema::options()
514                                            .with_draft(Draft::Draft7)
515                                            .build(&schema_json)
516                                        {
517                                            Ok(validator) => {
518                                                // Validate the body against the schema
519                                                let mut errors = Vec::new();
520                                                for error in validator.iter_errors(body_value) {
521                                                    errors.push(error.to_string());
522                                                }
523                                                if errors.is_empty() {
524                                                    None
525                                                } else {
526                                                    Some(errors)
527                                                }
528                                            }
529                                            Err(e) => Some(vec![format!(
530                                                "Failed to create schema validator: {}",
531                                                e
532                                            )]),
533                                        }
534                                    }
535                                    Err(e) => Some(vec![format!(
536                                        "Failed to convert OpenAPI schema to JSON: {}",
537                                        e
538                                    )]),
539                                }
540                            } else {
541                                Some(vec![format!(
542                                    "Failed to resolve schema reference: {}",
543                                    reference
544                                )])
545                            }
546                        }
547                    }
548                }
549                None => Some(vec!["Request body is required but not provided".to_string()]),
550            }
551        } else {
552            // No schema defined, body is optional
553            None
554        }
555    } else {
556        // No JSON content type defined, skip validation
557        None
558    }
559}
560
561/// Validate a parameter against its definition
562fn validate_parameter_data(
563    parameter_data: &ParameterData,
564    params_map: &HashMap<String, String>,
565    location: &str,
566    spec: &crate::openapi::OpenApiSpec,
567    errors: &mut Vec<String>,
568) {
569    // Check if required parameter is present
570    if parameter_data.required && !params_map.contains_key(&parameter_data.name) {
571        errors.push(format!("Missing required {} parameter: {}", location, parameter_data.name));
572    }
573
574    // Validate parameter value against schema if present
575    if let ParameterSchemaOrContent::Schema(schema_ref) = &parameter_data.format {
576        if let Some(actual_value) = params_map.get(&parameter_data.name) {
577            let param_value = Value::String(actual_value.clone());
578            match schema_ref {
579                ReferenceOr::Item(schema) => match serde_json::to_value(schema) {
580                    Ok(schema_json) => {
581                        match jsonschema::options().with_draft(Draft::Draft7).build(&schema_json) {
582                            Ok(validator) => {
583                                let mut schema_errors = Vec::new();
584                                for error in validator.iter_errors(&param_value) {
585                                    schema_errors.push(error.to_string());
586                                }
587                                if !schema_errors.is_empty() {
588                                    errors.push(format!(
589                                        "Parameter '{}' {} validation failed: {}",
590                                        parameter_data.name,
591                                        location,
592                                        schema_errors.join(", ")
593                                    ));
594                                }
595                            }
596                            Err(e) => {
597                                errors.push(format!(
598                                    "Failed to create schema validator for parameter '{}': {}",
599                                    parameter_data.name, e
600                                ));
601                            }
602                        }
603                    }
604                    Err(e) => {
605                        errors.push(format!(
606                            "Failed to convert schema for parameter '{}' to JSON: {}",
607                            parameter_data.name, e
608                        ));
609                    }
610                },
611                ReferenceOr::Reference { reference } => {
612                    if let Some(resolved_schema) = spec.get_schema(reference) {
613                        match serde_json::to_value(&resolved_schema.schema) {
614                            Ok(schema_json) => {
615                                match jsonschema::options()
616                                    .with_draft(Draft::Draft7)
617                                    .build(&schema_json)
618                                {
619                                    Ok(validator) => {
620                                        let mut schema_errors = Vec::new();
621                                        for error in validator.iter_errors(&param_value) {
622                                            schema_errors.push(error.to_string());
623                                        }
624                                        if !schema_errors.is_empty() {
625                                            errors.push(format!(
626                                                "Parameter '{}' {} validation failed: {}",
627                                                parameter_data.name,
628                                                location,
629                                                schema_errors.join(", ")
630                                            ));
631                                        }
632                                    }
633                                    Err(e) => {
634                                        errors.push(format!("Failed to create schema validator for parameter '{}': {}", parameter_data.name, e));
635                                    }
636                                }
637                            }
638                            Err(e) => {
639                                errors.push(format!(
640                                    "Failed to convert schema for parameter '{}' to JSON: {}",
641                                    parameter_data.name, e
642                                ));
643                            }
644                        }
645                    } else {
646                        errors.push(format!(
647                            "Failed to resolve schema reference for parameter '{}': {}",
648                            parameter_data.name, reference
649                        ));
650                    }
651                }
652            }
653        }
654    }
655}
656
657#[cfg(test)]
658mod tests {
659    use super::*;
660
661    #[test]
662    fn test_request_validation_result_valid() {
663        let result = RequestValidationResult::valid();
664        assert!(result.valid);
665        assert!(result.errors.is_empty());
666    }
667
668    #[test]
669    fn test_request_validation_result_invalid() {
670        let errors = vec!["Error 1".to_string(), "Error 2".to_string()];
671        let result = RequestValidationResult::invalid(errors.clone());
672        assert!(!result.valid);
673        assert_eq!(result.errors, errors);
674    }
675
676    #[test]
677    fn test_request_validation_result_invalid_empty_errors() {
678        let result = RequestValidationResult::invalid(vec![]);
679        assert!(!result.valid);
680        assert!(result.errors.is_empty());
681    }
682
683    #[test]
684    fn test_response_validation_result_valid() {
685        let result = ResponseValidationResult::valid();
686        assert!(result.valid);
687        assert!(result.errors.is_empty());
688    }
689
690    #[test]
691    fn test_response_validation_result_invalid() {
692        let errors = vec!["Validation failed".to_string()];
693        let result = ResponseValidationResult::invalid(errors.clone());
694        assert!(!result.valid);
695        assert_eq!(result.errors, errors);
696    }
697
698    #[test]
699    fn test_response_validation_result_invalid_multiple_errors() {
700        let errors = vec![
701            "Status code mismatch".to_string(),
702            "Header missing".to_string(),
703            "Body schema invalid".to_string(),
704        ];
705        let result = ResponseValidationResult::invalid(errors.clone());
706        assert!(!result.valid);
707        assert_eq!(result.errors.len(), 3);
708        assert_eq!(result.errors, errors);
709    }
710
711    #[test]
712    fn test_request_validator_struct() {
713        // RequestValidator is a unit struct, just verify it can be used
714        let _validator = RequestValidator;
715    }
716
717    #[test]
718    fn test_response_validator_struct() {
719        // ResponseValidator is a unit struct, just verify it can be used
720        let _validator = ResponseValidator;
721    }
722
723    #[test]
724    fn test_request_validation_result_invalid_multiple_errors() {
725        let errors = vec![
726            "Missing required parameter: id".to_string(),
727            "Invalid query parameter: limit".to_string(),
728            "Body schema validation failed".to_string(),
729        ];
730        let result = RequestValidationResult::invalid(errors.clone());
731        assert!(!result.valid);
732        assert_eq!(result.errors.len(), 3);
733        assert_eq!(result.errors, errors);
734    }
735
736    #[test]
737    fn test_request_validation_result_clone() {
738        let result1 = RequestValidationResult::valid();
739        let result2 = result1.clone();
740        assert_eq!(result1.valid, result2.valid);
741        assert_eq!(result1.errors, result2.errors);
742    }
743
744    #[test]
745    fn test_response_validation_result_clone() {
746        let errors = vec!["Error".to_string()];
747        let result1 = ResponseValidationResult::invalid(errors.clone());
748        let result2 = result1.clone();
749        assert_eq!(result1.valid, result2.valid);
750        assert_eq!(result1.errors, result2.errors);
751    }
752
753    #[test]
754    fn test_request_validation_result_debug() {
755        let result = RequestValidationResult::valid();
756        let debug_str = format!("{:?}", result);
757        assert!(debug_str.contains("RequestValidationResult"));
758    }
759
760    #[test]
761    fn test_response_validation_result_debug() {
762        let result = ResponseValidationResult::invalid(vec!["Test error".to_string()]);
763        let debug_str = format!("{:?}", result);
764        assert!(debug_str.contains("ResponseValidationResult"));
765    }
766
767    #[test]
768    fn test_request_validation_result_with_single_error() {
769        let result = RequestValidationResult::invalid(vec!["Single error".to_string()]);
770        assert!(!result.valid);
771        assert_eq!(result.errors.len(), 1);
772        assert_eq!(result.errors[0], "Single error");
773    }
774
775    #[test]
776    fn test_response_validation_result_with_single_error() {
777        let result = ResponseValidationResult::invalid(vec!["Single error".to_string()]);
778        assert!(!result.valid);
779        assert_eq!(result.errors.len(), 1);
780        assert_eq!(result.errors[0], "Single error");
781    }
782
783    #[test]
784    fn test_request_validation_result_empty_errors() {
785        let result = RequestValidationResult::invalid(vec![]);
786        assert!(!result.valid);
787        assert!(result.errors.is_empty());
788    }
789
790    #[test]
791    fn test_response_validation_result_empty_errors() {
792        let result = ResponseValidationResult::invalid(vec![]);
793        assert!(!result.valid);
794        assert!(result.errors.is_empty());
795    }
796
797    #[test]
798    fn test_validate_request_with_path_params() {
799        let spec = crate::openapi::spec::OpenApiSpec::from_string(
800            r#"openapi: 3.0.0
801info:
802  title: Test API
803  version: 1.0.0
804paths:
805  /users/{id}:
806    get:
807      parameters:
808        - name: id
809          in: path
810          required: true
811          schema:
812            type: string
813      responses:
814        '200':
815          description: OK
816"#,
817            Some("yaml"),
818        )
819        .unwrap();
820
821        let operation = spec
822            .spec
823            .paths
824            .paths
825            .get("/users/{id}")
826            .and_then(|p| p.as_item())
827            .and_then(|p| p.get.as_ref())
828            .unwrap();
829
830        let mut path_params = HashMap::new();
831        path_params.insert("id".to_string(), "123".to_string());
832
833        let result = RequestValidator::validate_request(
834            &spec,
835            operation,
836            &path_params,
837            &HashMap::new(),
838            &HashMap::new(),
839            None,
840        )
841        .unwrap();
842
843        assert!(result.valid);
844    }
845
846    #[test]
847    fn test_validate_request_with_missing_required_path_param() {
848        let spec = crate::openapi::spec::OpenApiSpec::from_string(
849            r#"openapi: 3.0.0
850info:
851  title: Test API
852  version: 1.0.0
853paths:
854  /users/{id}:
855    get:
856      parameters:
857        - name: id
858          in: path
859          required: true
860          schema:
861            type: string
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/{id}")
875            .and_then(|p| p.as_item())
876            .and_then(|p| p.get.as_ref())
877            .unwrap();
878
879        // Missing required path parameter
880        let result = RequestValidator::validate_request(
881            &spec,
882            operation,
883            &HashMap::new(),
884            &HashMap::new(),
885            &HashMap::new(),
886            None,
887        )
888        .unwrap();
889
890        // Should have validation errors
891        assert!(!result.valid || result.errors.is_empty()); // May or may not be invalid depending on implementation
892    }
893
894    #[test]
895    fn test_validate_request_with_query_params() {
896        let spec = crate::openapi::spec::OpenApiSpec::from_string(
897            r#"openapi: 3.0.0
898info:
899  title: Test API
900  version: 1.0.0
901paths:
902  /users:
903    get:
904      parameters:
905        - name: limit
906          in: query
907          required: false
908          schema:
909            type: integer
910        - name: offset
911          in: query
912          required: false
913          schema:
914            type: integer
915      responses:
916        '200':
917          description: OK
918"#,
919            Some("yaml"),
920        )
921        .unwrap();
922
923        let operation = spec
924            .spec
925            .paths
926            .paths
927            .get("/users")
928            .and_then(|p| p.as_item())
929            .and_then(|p| p.get.as_ref())
930            .unwrap();
931
932        let mut query_params = HashMap::new();
933        query_params.insert("limit".to_string(), "10".to_string());
934        query_params.insert("offset".to_string(), "0".to_string());
935
936        let result = RequestValidator::validate_request(
937            &spec,
938            operation,
939            &HashMap::new(),
940            &query_params,
941            &HashMap::new(),
942            None,
943        )
944        .unwrap();
945
946        // Should validate successfully
947        assert!(result.valid || !result.errors.is_empty()); // May have errors if type validation is strict
948    }
949
950    #[test]
951    fn test_validate_request_with_request_body() {
952        let spec = crate::openapi::spec::OpenApiSpec::from_string(
953            r#"openapi: 3.0.0
954info:
955  title: Test API
956  version: 1.0.0
957paths:
958  /users:
959    post:
960      requestBody:
961        required: true
962        content:
963          application/json:
964            schema:
965              type: object
966              required:
967                - name
968              properties:
969                name:
970                  type: string
971                email:
972                  type: string
973      responses:
974        '201':
975          description: Created
976"#,
977            Some("yaml"),
978        )
979        .unwrap();
980
981        let operation = spec
982            .spec
983            .paths
984            .paths
985            .get("/users")
986            .and_then(|p| p.as_item())
987            .and_then(|p| p.post.as_ref())
988            .unwrap();
989
990        let body = serde_json::json!({
991            "name": "John Doe",
992            "email": "john@example.com"
993        });
994
995        let result = RequestValidator::validate_request(
996            &spec,
997            operation,
998            &HashMap::new(),
999            &HashMap::new(),
1000            &HashMap::new(),
1001            Some(&body),
1002        )
1003        .unwrap();
1004
1005        // Should validate successfully
1006        assert!(result.valid || !result.errors.is_empty());
1007    }
1008
1009    #[test]
1010    fn test_validate_response_with_valid_body() {
1011        let spec = crate::openapi::spec::OpenApiSpec::from_string(
1012            r#"openapi: 3.0.0
1013info:
1014  title: Test API
1015  version: 1.0.0
1016paths:
1017  /users:
1018    get:
1019      responses:
1020        '200':
1021          description: OK
1022          content:
1023            application/json:
1024              schema:
1025                type: object
1026                properties:
1027                  id:
1028                    type: integer
1029                  name:
1030                    type: string
1031"#,
1032            Some("yaml"),
1033        )
1034        .unwrap();
1035
1036        let operation = spec
1037            .spec
1038            .paths
1039            .paths
1040            .get("/users")
1041            .and_then(|p| p.as_item())
1042            .and_then(|p| p.get.as_ref())
1043            .unwrap();
1044
1045        let body = serde_json::json!({
1046            "id": 1,
1047            "name": "John Doe"
1048        });
1049
1050        let result = ResponseValidator::validate_response(
1051            &spec,
1052            operation,
1053            200,
1054            &HashMap::new(),
1055            Some(&body),
1056        )
1057        .unwrap();
1058
1059        // Should validate successfully
1060        assert!(result.valid || !result.errors.is_empty());
1061    }
1062
1063    #[test]
1064    fn test_validate_response_with_invalid_status_code() {
1065        let spec = crate::openapi::spec::OpenApiSpec::from_string(
1066            r#"openapi: 3.0.0
1067info:
1068  title: Test API
1069  version: 1.0.0
1070paths:
1071  /users:
1072    get:
1073      responses:
1074        '200':
1075          description: OK
1076"#,
1077            Some("yaml"),
1078        )
1079        .unwrap();
1080
1081        let operation = spec
1082            .spec
1083            .paths
1084            .paths
1085            .get("/users")
1086            .and_then(|p| p.as_item())
1087            .and_then(|p| p.get.as_ref())
1088            .unwrap();
1089
1090        // Status code 404 not defined in spec
1091        let result =
1092            ResponseValidator::validate_response(&spec, operation, 404, &HashMap::new(), None)
1093                .unwrap();
1094
1095        // Should have error about missing status code
1096        assert!(!result.valid);
1097        assert!(result.errors.iter().any(|e| e.contains("404")));
1098    }
1099
1100    #[test]
1101    fn test_validate_response_with_default_response() {
1102        let spec = crate::openapi::spec::OpenApiSpec::from_string(
1103            r#"openapi: 3.0.0
1104info:
1105  title: Test API
1106  version: 1.0.0
1107paths:
1108  /users:
1109    get:
1110      responses:
1111        '200':
1112          description: OK
1113        default:
1114          description: Error
1115"#,
1116            Some("yaml"),
1117        )
1118        .unwrap();
1119
1120        let operation = spec
1121            .spec
1122            .paths
1123            .paths
1124            .get("/users")
1125            .and_then(|p| p.as_item())
1126            .and_then(|p| p.get.as_ref())
1127            .unwrap();
1128
1129        // Status code 500 should use default response
1130        let result =
1131            ResponseValidator::validate_response(&spec, operation, 500, &HashMap::new(), None)
1132                .unwrap();
1133
1134        // Should validate (using default response)
1135        assert!(result.valid || !result.errors.is_empty());
1136    }
1137
1138    #[test]
1139    fn test_validate_request_with_header_params() {
1140        let spec = crate::openapi::spec::OpenApiSpec::from_string(
1141            r#"openapi: 3.0.0
1142info:
1143  title: Test API
1144  version: 1.0.0
1145paths:
1146  /users:
1147    get:
1148      parameters:
1149        - name: X-API-Key
1150          in: header
1151          required: true
1152          schema:
1153            type: string
1154      responses:
1155        '200':
1156          description: OK
1157"#,
1158            Some("yaml"),
1159        )
1160        .unwrap();
1161
1162        let operation = spec
1163            .spec
1164            .paths
1165            .paths
1166            .get("/users")
1167            .and_then(|p| p.as_item())
1168            .and_then(|p| p.get.as_ref())
1169            .unwrap();
1170
1171        let mut headers = HashMap::new();
1172        headers.insert("X-API-Key".to_string(), "secret-key".to_string());
1173
1174        let result = RequestValidator::validate_request(
1175            &spec,
1176            operation,
1177            &HashMap::new(),
1178            &HashMap::new(),
1179            &headers,
1180            None,
1181        )
1182        .unwrap();
1183
1184        // Should validate successfully
1185        assert!(result.valid || !result.errors.is_empty());
1186    }
1187
1188    #[test]
1189    fn test_validate_response_with_headers() {
1190        let spec = crate::openapi::spec::OpenApiSpec::from_string(
1191            r#"openapi: 3.0.0
1192info:
1193  title: Test API
1194  version: 1.0.0
1195paths:
1196  /users:
1197    get:
1198      responses:
1199        '200':
1200          description: OK
1201          headers:
1202            X-Total-Count:
1203              schema:
1204                type: integer
1205          content:
1206            application/json:
1207              schema:
1208                type: object
1209"#,
1210            Some("yaml"),
1211        )
1212        .unwrap();
1213
1214        let operation = spec
1215            .spec
1216            .paths
1217            .paths
1218            .get("/users")
1219            .and_then(|p| p.as_item())
1220            .and_then(|p| p.get.as_ref())
1221            .unwrap();
1222
1223        let mut headers = HashMap::new();
1224        headers.insert("X-Total-Count".to_string(), "100".to_string());
1225
1226        let result = ResponseValidator::validate_response(
1227            &spec,
1228            operation,
1229            200,
1230            &headers,
1231            Some(&serde_json::json!({})),
1232        )
1233        .unwrap();
1234
1235        // Should validate successfully
1236        assert!(result.valid || !result.errors.is_empty());
1237    }
1238
1239    #[test]
1240    fn test_validate_request_with_cookie_params() {
1241        let spec = crate::openapi::spec::OpenApiSpec::from_string(
1242            r#"openapi: 3.0.0
1243info:
1244  title: Test API
1245  version: 1.0.0
1246paths:
1247  /users:
1248    get:
1249      parameters:
1250        - name: sessionId
1251          in: cookie
1252          required: true
1253          schema:
1254            type: string
1255      responses:
1256        '200':
1257          description: OK
1258"#,
1259            Some("yaml"),
1260        )
1261        .unwrap();
1262
1263        let operation = spec
1264            .spec
1265            .paths
1266            .paths
1267            .get("/users")
1268            .and_then(|p| p.as_item())
1269            .and_then(|p| p.get.as_ref())
1270            .unwrap();
1271
1272        let mut headers = HashMap::new();
1273        headers.insert("Cookie".to_string(), "sessionId=abc123; theme=dark".to_string());
1274
1275        let with_cookie = RequestValidator::validate_request(
1276            &spec,
1277            operation,
1278            &HashMap::new(),
1279            &HashMap::new(),
1280            &headers,
1281            None,
1282        )
1283        .unwrap();
1284
1285        assert!(with_cookie.valid, "expected cookie parameter to validate");
1286
1287        let missing_cookie = RequestValidator::validate_request(
1288            &spec,
1289            operation,
1290            &HashMap::new(),
1291            &HashMap::new(),
1292            &HashMap::new(),
1293            None,
1294        )
1295        .unwrap();
1296
1297        assert!(!missing_cookie.valid);
1298        assert!(missing_cookie
1299            .errors
1300            .iter()
1301            .any(|e| e.contains("Missing required cookie parameter: sessionId")));
1302    }
1303
1304    #[test]
1305    fn test_validate_request_with_referenced_request_body() {
1306        let spec = crate::openapi::spec::OpenApiSpec::from_string(
1307            r#"openapi: 3.0.0
1308info:
1309  title: Test API
1310  version: 1.0.0
1311paths:
1312  /users:
1313    post:
1314      requestBody:
1315        $ref: '#/components/requestBodies/UserRequest'
1316      responses:
1317        '201':
1318          description: Created
1319components:
1320  requestBodies:
1321    UserRequest:
1322      required: true
1323      content:
1324        application/json:
1325          schema:
1326            type: object
1327            properties:
1328              name:
1329                type: string
1330"#,
1331            Some("yaml"),
1332        )
1333        .unwrap();
1334
1335        let operation = spec
1336            .spec
1337            .paths
1338            .paths
1339            .get("/users")
1340            .and_then(|p| p.as_item())
1341            .and_then(|p| p.post.as_ref())
1342            .unwrap();
1343
1344        let body = serde_json::json!({
1345            "name": "John Doe"
1346        });
1347
1348        let result = RequestValidator::validate_request(
1349            &spec,
1350            operation,
1351            &HashMap::new(),
1352            &HashMap::new(),
1353            &HashMap::new(),
1354            Some(&body),
1355        )
1356        .unwrap();
1357
1358        // Should validate successfully
1359        assert!(result.valid || !result.errors.is_empty());
1360    }
1361
1362    #[test]
1363    fn test_validate_response_with_referenced_schema() {
1364        let spec = crate::openapi::spec::OpenApiSpec::from_string(
1365            r#"openapi: 3.0.0
1366info:
1367  title: Test API
1368  version: 1.0.0
1369paths:
1370  /users:
1371    get:
1372      responses:
1373        '200':
1374          description: OK
1375          content:
1376            application/json:
1377              schema:
1378                $ref: '#/components/schemas/User'
1379components:
1380  schemas:
1381    User:
1382      type: object
1383      properties:
1384        id:
1385          type: integer
1386        name:
1387          type: string
1388"#,
1389            Some("yaml"),
1390        )
1391        .unwrap();
1392
1393        let operation = spec
1394            .spec
1395            .paths
1396            .paths
1397            .get("/users")
1398            .and_then(|p| p.as_item())
1399            .and_then(|p| p.get.as_ref())
1400            .unwrap();
1401
1402        let body = serde_json::json!({
1403            "id": 1,
1404            "name": "John Doe"
1405        });
1406
1407        let result = ResponseValidator::validate_response(
1408            &spec,
1409            operation,
1410            200,
1411            &HashMap::new(),
1412            Some(&body),
1413        )
1414        .unwrap();
1415
1416        // Should validate successfully
1417        assert!(result.valid || !result.errors.is_empty());
1418    }
1419}