Skip to main content

barbacane_lib/
validator.rs

1//! Request validation for Barbacane gateway.
2//!
3//! Validates incoming requests against OpenAPI parameter and body schemas.
4//! Used by the data plane to reject non-conforming requests before dispatch.
5
6use std::collections::HashMap;
7
8use serde_json::Value;
9use thiserror::Error;
10
11use barbacane_compiler::{Parameter, RequestBody};
12
13/// Validation errors returned when a request doesn't conform to the spec.
14#[derive(Debug, Error)]
15pub enum ValidationError2 {
16    #[error("missing required parameter '{name}' in {location}")]
17    MissingRequiredParameter { name: String, location: String },
18
19    #[error("invalid parameter '{name}' in {location}: {reason}")]
20    InvalidParameter {
21        name: String,
22        location: String,
23        reason: String,
24    },
25
26    #[error("missing required request body")]
27    MissingRequiredBody,
28
29    #[error("unsupported content-type: {0}")]
30    UnsupportedContentType(String),
31
32    #[error("invalid request body: {0}")]
33    InvalidBody(String),
34
35    #[error("request body too large: {size} bytes exceeds limit of {limit} bytes")]
36    BodyTooLarge { size: usize, limit: usize },
37
38    #[error("too many headers: {count} exceeds limit of {limit}")]
39    TooManyHeaders { count: usize, limit: usize },
40
41    #[error("URI too long: {length} characters exceeds limit of {limit}")]
42    UriTooLong { length: usize, limit: usize },
43
44    #[error("header '{name}' too large: {size} bytes exceeds limit of {limit} bytes")]
45    HeaderTooLarge {
46        name: String,
47        size: usize,
48        limit: usize,
49    },
50}
51
52/// RFC 9457 problem details for validation errors.
53#[derive(Debug, Clone, serde::Serialize)]
54pub struct ProblemDetails {
55    #[serde(rename = "type")]
56    pub error_type: String,
57    pub title: String,
58    pub status: u16,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub detail: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub instance: Option<String>,
63    /// Extended fields for dev mode
64    #[serde(flatten)]
65    pub extensions: HashMap<String, Value>,
66}
67
68impl ProblemDetails {
69    pub fn validation_error(errors: &[ValidationError2], dev_mode: bool) -> Self {
70        let mut extensions = HashMap::new();
71
72        if dev_mode && !errors.is_empty() {
73            let error_details: Vec<Value> = errors
74                .iter()
75                .map(|e| {
76                    let mut detail = serde_json::Map::new();
77                    match e {
78                        ValidationError2::MissingRequiredParameter { name, location } => {
79                            detail.insert("field".into(), Value::String(name.clone()));
80                            detail.insert("location".into(), Value::String(location.clone()));
81                            detail.insert(
82                                "reason".into(),
83                                Value::String("missing required parameter".into()),
84                            );
85                        }
86                        ValidationError2::InvalidParameter {
87                            name,
88                            location,
89                            reason,
90                        } => {
91                            detail.insert("field".into(), Value::String(name.clone()));
92                            detail.insert("location".into(), Value::String(location.clone()));
93                            detail.insert("reason".into(), Value::String(reason.clone()));
94                        }
95                        ValidationError2::MissingRequiredBody => {
96                            detail.insert("field".into(), Value::String("body".into()));
97                            detail.insert(
98                                "reason".into(),
99                                Value::String("missing required request body".into()),
100                            );
101                        }
102                        ValidationError2::UnsupportedContentType(ct) => {
103                            detail.insert("field".into(), Value::String("content-type".into()));
104                            detail.insert(
105                                "reason".into(),
106                                Value::String(format!("unsupported: {}", ct)),
107                            );
108                        }
109                        ValidationError2::InvalidBody(reason) => {
110                            detail.insert("field".into(), Value::String("body".into()));
111                            detail.insert("reason".into(), Value::String(reason.clone()));
112                        }
113                        ValidationError2::BodyTooLarge { size, limit } => {
114                            detail.insert("field".into(), Value::String("body".into()));
115                            detail.insert(
116                                "reason".into(),
117                                Value::String(format!(
118                                    "body too large: {} bytes exceeds {} byte limit",
119                                    size, limit
120                                )),
121                            );
122                        }
123                        ValidationError2::TooManyHeaders { count, limit } => {
124                            detail.insert("field".into(), Value::String("headers".into()));
125                            detail.insert(
126                                "reason".into(),
127                                Value::String(format!(
128                                    "too many headers: {} exceeds {} limit",
129                                    count, limit
130                                )),
131                            );
132                        }
133                        ValidationError2::UriTooLong { length, limit } => {
134                            detail.insert("field".into(), Value::String("uri".into()));
135                            detail.insert(
136                                "reason".into(),
137                                Value::String(format!(
138                                    "URI too long: {} chars exceeds {} char limit",
139                                    length, limit
140                                )),
141                            );
142                        }
143                        ValidationError2::HeaderTooLarge { name, size, limit } => {
144                            detail
145                                .insert("field".into(), Value::String(format!("header:{}", name)));
146                            detail.insert(
147                                "reason".into(),
148                                Value::String(format!(
149                                    "header too large: {} bytes exceeds {} byte limit",
150                                    size, limit
151                                )),
152                            );
153                        }
154                    }
155                    Value::Object(detail)
156                })
157                .collect();
158            extensions.insert("errors".into(), Value::Array(error_details));
159        }
160
161        let detail = if errors.len() == 1 {
162            Some(errors[0].to_string())
163        } else {
164            Some(format!("{} validation errors", errors.len()))
165        };
166
167        ProblemDetails {
168            error_type: "urn:barbacane:error:validation-failed".into(),
169            title: "Request validation failed".into(),
170            status: 400,
171            detail,
172            instance: None,
173            extensions,
174        }
175    }
176
177    pub fn to_json(&self) -> String {
178        serde_json::to_string(self).unwrap_or_else(|_| {
179            r#"{"type":"urn:barbacane:error:internal","title":"Serialization error","status":500}"#.into()
180        })
181    }
182}
183
184/// Request limits configuration.
185#[derive(Debug, Clone)]
186pub struct RequestLimits {
187    /// Maximum request body size in bytes (default: 1MB).
188    pub max_body_size: usize,
189    /// Maximum number of headers (default: 100).
190    pub max_headers: usize,
191    /// Maximum header size in bytes (default: 8KB).
192    pub max_header_size: usize,
193    /// Maximum URI length in characters (default: 8KB).
194    pub max_uri_length: usize,
195}
196
197impl Default for RequestLimits {
198    fn default() -> Self {
199        Self {
200            max_body_size: 1024 * 1024, // 1 MB
201            max_headers: 100,
202            max_header_size: 8 * 1024, // 8 KB
203            max_uri_length: 8 * 1024,  // 8 KB
204        }
205    }
206}
207
208impl RequestLimits {
209    /// Validate URI length.
210    pub fn validate_uri(&self, uri: &str) -> Result<(), ValidationError2> {
211        if uri.len() > self.max_uri_length {
212            return Err(ValidationError2::UriTooLong {
213                length: uri.len(),
214                limit: self.max_uri_length,
215            });
216        }
217        Ok(())
218    }
219
220    /// Validate header count and individual header sizes.
221    pub fn validate_headers(
222        &self,
223        headers: &HashMap<String, String>,
224    ) -> Result<(), ValidationError2> {
225        if headers.len() > self.max_headers {
226            return Err(ValidationError2::TooManyHeaders {
227                count: headers.len(),
228                limit: self.max_headers,
229            });
230        }
231
232        for (name, value) in headers {
233            let header_size = name.len() + value.len();
234            if header_size > self.max_header_size {
235                return Err(ValidationError2::HeaderTooLarge {
236                    name: name.clone(),
237                    size: header_size,
238                    limit: self.max_header_size,
239                });
240            }
241        }
242
243        Ok(())
244    }
245
246    /// Validate body size.
247    pub fn validate_body_size(&self, body_len: usize) -> Result<(), ValidationError2> {
248        if body_len > self.max_body_size {
249            return Err(ValidationError2::BodyTooLarge {
250                size: body_len,
251                limit: self.max_body_size,
252            });
253        }
254        Ok(())
255    }
256
257    /// Validate all limits at once. Returns errors for all limit violations.
258    pub fn validate_all(
259        &self,
260        uri: &str,
261        headers: &HashMap<String, String>,
262        body_len: usize,
263    ) -> Result<(), Vec<ValidationError2>> {
264        let mut errors = Vec::new();
265
266        if let Err(e) = self.validate_uri(uri) {
267            errors.push(e);
268        }
269
270        if let Err(e) = self.validate_headers(headers) {
271            errors.push(e);
272        }
273
274        if let Err(e) = self.validate_body_size(body_len) {
275            errors.push(e);
276        }
277
278        if errors.is_empty() {
279            Ok(())
280        } else {
281            Err(errors)
282        }
283    }
284}
285
286/// Compile a JSON schema with format validation enabled.
287///
288/// Supports formats: date-time, email, uuid, uri, ipv4, ipv6.
289fn compile_schema_with_formats(schema: &Value) -> Option<jsonschema::Validator> {
290    jsonschema::options()
291        .should_validate_formats(true)
292        .build(schema)
293        .ok()
294}
295
296/// Compiled validator for an operation.
297pub struct OperationValidator {
298    /// Path parameters with their compiled schemas.
299    path_params: Vec<CompiledParam>,
300    /// Query parameters with their compiled schemas.
301    query_params: Vec<CompiledParam>,
302    /// Header parameters with their compiled schemas.
303    header_params: Vec<CompiledParam>,
304    /// Request body configuration.
305    request_body: Option<CompiledRequestBody>,
306}
307
308struct CompiledParam {
309    name: String,
310    required: bool,
311    schema: Option<jsonschema::Validator>,
312}
313
314struct CompiledRequestBody {
315    required: bool,
316    /// Content type -> compiled schema
317    content: HashMap<String, Option<jsonschema::Validator>>,
318}
319
320/// Validate a set of compiled parameters against a value lookup.
321///
322/// Shared logic for path, query, and header parameter validation.
323fn validate_params(
324    params: &[CompiledParam],
325    lookup: impl Fn(&str) -> Option<String>,
326    location: &str,
327) -> Result<(), Vec<ValidationError2>> {
328    let mut errors = Vec::new();
329
330    for param in params {
331        match lookup(&param.name) {
332            Some(value) => {
333                if let Some(schema) = &param.schema {
334                    let json_value = Value::String(value);
335                    let validation_errors: Vec<_> = schema.iter_errors(&json_value).collect();
336                    if !validation_errors.is_empty() {
337                        let reasons: Vec<String> =
338                            validation_errors.iter().map(|e| e.to_string()).collect();
339                        errors.push(ValidationError2::InvalidParameter {
340                            name: param.name.clone(),
341                            location: location.into(),
342                            reason: reasons.join("; "),
343                        });
344                    }
345                }
346            }
347            None if param.required => {
348                errors.push(ValidationError2::MissingRequiredParameter {
349                    name: param.name.clone(),
350                    location: location.into(),
351                });
352            }
353            None => {}
354        }
355    }
356
357    if errors.is_empty() {
358        Ok(())
359    } else {
360        Err(errors)
361    }
362}
363
364impl OperationValidator {
365    /// Create a new validator from parsed operation metadata.
366    pub fn new(parameters: &[Parameter], request_body: Option<&RequestBody>) -> Self {
367        let mut path_params = Vec::new();
368        let mut query_params = Vec::new();
369        let mut header_params = Vec::new();
370
371        for param in parameters {
372            let compiled = CompiledParam {
373                name: param.name.clone(),
374                required: param.required || param.location == "path", // Path params always required
375                schema: param.schema.as_ref().and_then(compile_schema_with_formats),
376            };
377
378            match param.location.as_str() {
379                "path" => path_params.push(compiled),
380                "query" => query_params.push(compiled),
381                "header" => header_params.push(compiled),
382                _ => {} // Ignore cookie params for now
383            }
384        }
385
386        let compiled_body = request_body.map(|rb| {
387            let mut content = HashMap::new();
388            for (media_type, content_schema) in &rb.content {
389                let schema = content_schema
390                    .schema
391                    .as_ref()
392                    .and_then(compile_schema_with_formats);
393                content.insert(media_type.clone(), schema);
394            }
395            CompiledRequestBody {
396                required: rb.required,
397                content,
398            }
399        });
400
401        Self {
402            path_params,
403            query_params,
404            header_params,
405            request_body: compiled_body,
406        }
407    }
408
409    /// Validate path parameters extracted by the router.
410    pub fn validate_path_params(
411        &self,
412        params: &[(String, String)],
413    ) -> Result<(), Vec<ValidationError2>> {
414        let param_map: HashMap<_, _> = params.iter().cloned().collect();
415        validate_params(
416            &self.path_params,
417            |name| param_map.get(name).cloned(),
418            "path",
419        )
420    }
421
422    /// Validate query parameters.
423    pub fn validate_query_params(
424        &self,
425        query_string: Option<&str>,
426    ) -> Result<(), Vec<ValidationError2>> {
427        let param_map: HashMap<String, String> = query_string
428            .unwrap_or("")
429            .split('&')
430            .filter(|s| !s.is_empty())
431            .filter_map(|pair| {
432                let mut parts = pair.splitn(2, '=');
433                let key = parts.next()?;
434                let value = parts.next().unwrap_or("");
435                Some((urlencoding_decode(key), urlencoding_decode(value)))
436            })
437            .collect();
438
439        validate_params(
440            &self.query_params,
441            |name| param_map.get(name).cloned(),
442            "query",
443        )
444    }
445
446    /// Validate request headers (case-insensitive matching).
447    pub fn validate_headers(
448        &self,
449        headers: &HashMap<String, String>,
450    ) -> Result<(), Vec<ValidationError2>> {
451        let headers_lower: HashMap<String, String> = headers
452            .iter()
453            .map(|(k, v)| (k.to_lowercase(), v.clone()))
454            .collect();
455
456        validate_params(
457            &self.header_params,
458            |name| headers_lower.get(&name.to_lowercase()).cloned(),
459            "header",
460        )
461    }
462
463    /// Validate request body.
464    pub fn validate_body(
465        &self,
466        content_type: Option<&str>,
467        body: &[u8],
468    ) -> Result<(), Vec<ValidationError2>> {
469        let Some(body_spec) = &self.request_body else {
470            // No body spec, nothing to validate
471            return Ok(());
472        };
473
474        // Check if body is required but missing
475        if body_spec.required && body.is_empty() {
476            return Err(vec![ValidationError2::MissingRequiredBody]);
477        }
478
479        // If body is empty and not required, skip validation
480        if body.is_empty() {
481            return Ok(());
482        }
483
484        // Check content type
485        let ct = content_type.unwrap_or("application/octet-stream");
486        let base_ct = ct.split(';').next().unwrap_or(ct).trim();
487
488        // Find matching content type (with wildcard support)
489        let schema = if let Some(schema) = body_spec.content.get(base_ct) {
490            schema
491        } else if let Some(schema) = body_spec.content.get("*/*") {
492            schema
493        } else {
494            return Err(vec![ValidationError2::UnsupportedContentType(
495                base_ct.to_string(),
496            )]);
497        };
498
499        // Validate JSON body against schema
500        if let Some(schema) = schema {
501            if base_ct.contains("json") {
502                let json_body: Value = match serde_json::from_slice(body) {
503                    Ok(v) => v,
504                    Err(e) => {
505                        return Err(vec![ValidationError2::InvalidBody(format!(
506                            "invalid JSON: {}",
507                            e
508                        ))]);
509                    }
510                };
511
512                let validation_errors: Vec<_> = schema.iter_errors(&json_body).collect();
513                if !validation_errors.is_empty() {
514                    let reasons: Vec<String> =
515                        validation_errors.iter().map(|e| e.to_string()).collect();
516                    return Err(vec![ValidationError2::InvalidBody(reasons.join("; "))]);
517                }
518            }
519        }
520
521        Ok(())
522    }
523
524    /// Validate entire request (fail-fast: stops at first error category).
525    pub fn validate_request(
526        &self,
527        path_params: &[(String, String)],
528        query_string: Option<&str>,
529        headers: &HashMap<String, String>,
530        content_type: Option<&str>,
531        body: &[u8],
532    ) -> Result<(), Vec<ValidationError2>> {
533        // Validate in order: path -> query -> headers -> body
534        self.validate_path_params(path_params)?;
535        self.validate_query_params(query_string)?;
536        self.validate_headers(headers)?;
537        self.validate_body(content_type, body)?;
538        Ok(())
539    }
540}
541
542/// Simple URL decoding (handles %XX escapes).
543fn urlencoding_decode(input: &str) -> String {
544    let mut result = String::with_capacity(input.len());
545    let mut chars = input.chars().peekable();
546
547    while let Some(c) = chars.next() {
548        if c == '%' {
549            let hex: String = chars.by_ref().take(2).collect();
550            if let Ok(byte) = u8::from_str_radix(&hex, 16) {
551                result.push(byte as char);
552            } else {
553                result.push('%');
554                result.push_str(&hex);
555            }
556        } else if c == '+' {
557            result.push(' ');
558        } else {
559            result.push(c);
560        }
561    }
562
563    result
564}
565
566#[cfg(test)]
567mod tests {
568    use super::compile_schema_with_formats;
569    use super::*;
570
571    fn make_param(name: &str, location: &str, required: bool, schema: Option<Value>) -> Parameter {
572        Parameter {
573            name: name.to_string(),
574            location: location.to_string(),
575            required,
576            schema,
577        }
578    }
579
580    #[test]
581    fn validate_required_path_param() {
582        let params = vec![make_param("id", "path", true, None)];
583        let validator = OperationValidator::new(&params, None);
584
585        // Missing required param
586        let result = validator.validate_path_params(&[]);
587        assert!(result.is_err());
588
589        // Present param
590        let result = validator.validate_path_params(&[("id".into(), "123".into())]);
591        assert!(result.is_ok());
592    }
593
594    #[test]
595    fn validate_path_param_schema() {
596        // Path parameters are always strings. Use a pattern to validate format.
597        let schema = serde_json::json!({
598            "type": "string",
599            "pattern": "^[0-9]+$"
600        });
601        let params = vec![make_param("id", "path", true, Some(schema))];
602        let validator = OperationValidator::new(&params, None);
603
604        // Valid: numeric string
605        let result = validator.validate_path_params(&[("id".into(), "123".into())]);
606        assert!(result.is_ok());
607
608        // Invalid: not matching the numeric pattern
609        let result = validator.validate_path_params(&[("id".into(), "abc".into())]);
610        assert!(result.is_err());
611    }
612
613    #[test]
614    fn validate_required_query_param() {
615        let params = vec![make_param("page", "query", true, None)];
616        let validator = OperationValidator::new(&params, None);
617
618        // Missing required
619        let result = validator.validate_query_params(Some(""));
620        assert!(result.is_err());
621
622        // Present
623        let result = validator.validate_query_params(Some("page=1"));
624        assert!(result.is_ok());
625    }
626
627    #[test]
628    fn validate_optional_query_param() {
629        let params = vec![make_param("limit", "query", false, None)];
630        let validator = OperationValidator::new(&params, None);
631
632        // Missing optional is OK
633        let result = validator.validate_query_params(Some(""));
634        assert!(result.is_ok());
635    }
636
637    #[test]
638    fn validate_required_body() {
639        use barbacane_compiler::ContentSchema;
640        use std::collections::BTreeMap;
641
642        let mut content = BTreeMap::new();
643        content.insert(
644            "application/json".to_string(),
645            ContentSchema { schema: None },
646        );
647
648        let request_body = RequestBody {
649            required: true,
650            content,
651        };
652
653        let validator = OperationValidator::new(&[], Some(&request_body));
654
655        // Missing required body
656        let result = validator.validate_body(Some("application/json"), &[]);
657        assert!(result.is_err());
658
659        // Present body
660        let result = validator.validate_body(Some("application/json"), b"{}");
661        assert!(result.is_ok());
662    }
663
664    #[test]
665    fn validate_body_schema() {
666        use barbacane_compiler::ContentSchema;
667        use std::collections::BTreeMap;
668
669        let schema = serde_json::json!({
670            "type": "object",
671            "required": ["name"],
672            "properties": {
673                "name": { "type": "string" }
674            }
675        });
676
677        let mut content = BTreeMap::new();
678        content.insert(
679            "application/json".to_string(),
680            ContentSchema {
681                schema: Some(schema),
682            },
683        );
684
685        let request_body = RequestBody {
686            required: true,
687            content,
688        };
689
690        let validator = OperationValidator::new(&[], Some(&request_body));
691
692        // Valid body
693        let result = validator.validate_body(Some("application/json"), br#"{"name":"test"}"#);
694        assert!(result.is_ok());
695
696        // Invalid: missing required field
697        let result = validator.validate_body(Some("application/json"), b"{}");
698        assert!(result.is_err());
699    }
700
701    #[test]
702    fn validate_unsupported_content_type() {
703        use barbacane_compiler::ContentSchema;
704        use std::collections::BTreeMap;
705
706        let mut content = BTreeMap::new();
707        content.insert(
708            "application/json".to_string(),
709            ContentSchema { schema: None },
710        );
711
712        let request_body = RequestBody {
713            required: true,
714            content,
715        };
716
717        let validator = OperationValidator::new(&[], Some(&request_body));
718
719        let result = validator.validate_body(Some("text/plain"), b"hello");
720        assert!(result.is_err());
721
722        if let Err(errors) = result {
723            assert!(matches!(
724                errors[0],
725                ValidationError2::UnsupportedContentType(_)
726            ));
727        }
728    }
729
730    #[test]
731    fn problem_details_format() {
732        let errors = vec![ValidationError2::MissingRequiredParameter {
733            name: "id".into(),
734            location: "path".into(),
735        }];
736
737        let problem = ProblemDetails::validation_error(&errors, false);
738        assert_eq!(problem.status, 400);
739        assert_eq!(problem.error_type, "urn:barbacane:error:validation-failed");
740
741        let json = problem.to_json();
742        assert!(json.contains("validation-failed"));
743    }
744
745    #[test]
746    fn problem_details_dev_mode() {
747        let errors = vec![ValidationError2::MissingRequiredParameter {
748            name: "id".into(),
749            location: "path".into(),
750        }];
751
752        let problem = ProblemDetails::validation_error(&errors, true);
753        let json = problem.to_json();
754
755        // Dev mode should include error details
756        assert!(json.contains("errors"));
757        assert!(json.contains("field"));
758    }
759
760    // ========================
761    // Request Limits Tests
762    // ========================
763
764    #[test]
765    fn validate_uri_length_ok() {
766        let limits = RequestLimits::default();
767        let uri = "/api/users/123";
768        assert!(limits.validate_uri(uri).is_ok());
769    }
770
771    #[test]
772    fn validate_uri_length_too_long() {
773        let limits = RequestLimits {
774            max_uri_length: 10,
775            ..Default::default()
776        };
777        let uri = "/api/users/123456789";
778        let result = limits.validate_uri(uri);
779        assert!(result.is_err());
780        assert!(matches!(
781            result.unwrap_err(),
782            ValidationError2::UriTooLong { .. }
783        ));
784    }
785
786    #[test]
787    fn validate_header_count_ok() {
788        let limits = RequestLimits::default();
789        let headers: HashMap<String, String> = (0..10)
790            .map(|i| (format!("Header-{}", i), "value".to_string()))
791            .collect();
792        assert!(limits.validate_headers(&headers).is_ok());
793    }
794
795    #[test]
796    fn validate_header_count_too_many() {
797        let limits = RequestLimits {
798            max_headers: 5,
799            ..Default::default()
800        };
801        let headers: HashMap<String, String> = (0..10)
802            .map(|i| (format!("Header-{}", i), "value".to_string()))
803            .collect();
804        let result = limits.validate_headers(&headers);
805        assert!(result.is_err());
806        assert!(matches!(
807            result.unwrap_err(),
808            ValidationError2::TooManyHeaders { .. }
809        ));
810    }
811
812    #[test]
813    fn validate_header_size_ok() {
814        let limits = RequestLimits::default();
815        let mut headers = HashMap::new();
816        headers.insert("Content-Type".to_string(), "application/json".to_string());
817        assert!(limits.validate_headers(&headers).is_ok());
818    }
819
820    #[test]
821    fn validate_header_size_too_large() {
822        let limits = RequestLimits {
823            max_header_size: 20,
824            ..Default::default()
825        };
826        let mut headers = HashMap::new();
827        headers.insert("X-Very-Long-Header".to_string(), "a".repeat(100));
828        let result = limits.validate_headers(&headers);
829        assert!(result.is_err());
830        assert!(matches!(
831            result.unwrap_err(),
832            ValidationError2::HeaderTooLarge { .. }
833        ));
834    }
835
836    #[test]
837    fn validate_body_size_ok() {
838        let limits = RequestLimits::default();
839        assert!(limits.validate_body_size(1000).is_ok());
840    }
841
842    #[test]
843    fn validate_body_size_too_large() {
844        let limits = RequestLimits {
845            max_body_size: 100,
846            ..Default::default()
847        };
848        let result = limits.validate_body_size(1000);
849        assert!(result.is_err());
850        assert!(matches!(
851            result.unwrap_err(),
852            ValidationError2::BodyTooLarge { .. }
853        ));
854    }
855
856    // ========================
857    // Format Validation Tests
858    // ========================
859
860    #[test]
861    fn format_validation_email() {
862        let schema = serde_json::json!({
863            "type": "object",
864            "properties": {
865                "email": { "type": "string", "format": "email" }
866            }
867        });
868        let validator = compile_schema_with_formats(&schema).unwrap();
869
870        // Valid email
871        let valid = serde_json::json!({"email": "user@example.com"});
872        assert!(validator.is_valid(&valid));
873
874        // Invalid email
875        let invalid = serde_json::json!({"email": "not-an-email"});
876        assert!(!validator.is_valid(&invalid));
877    }
878
879    #[test]
880    fn format_validation_uuid() {
881        let schema = serde_json::json!({
882            "type": "string",
883            "format": "uuid"
884        });
885        let validator = compile_schema_with_formats(&schema).unwrap();
886
887        // Valid UUID
888        let valid = serde_json::json!("550e8400-e29b-41d4-a716-446655440000");
889        assert!(validator.is_valid(&valid));
890
891        // Invalid UUID
892        let invalid = serde_json::json!("not-a-uuid");
893        assert!(!validator.is_valid(&invalid));
894    }
895
896    #[test]
897    fn format_validation_date_time() {
898        let schema = serde_json::json!({
899            "type": "string",
900            "format": "date-time"
901        });
902        let validator = compile_schema_with_formats(&schema).unwrap();
903
904        // Valid date-time (RFC 3339)
905        let valid = serde_json::json!("2024-01-29T12:30:00Z");
906        assert!(validator.is_valid(&valid));
907
908        // Invalid date-time
909        let invalid = serde_json::json!("not-a-date");
910        assert!(!validator.is_valid(&invalid));
911    }
912
913    #[test]
914    fn format_validation_uri() {
915        let schema = serde_json::json!({
916            "type": "string",
917            "format": "uri"
918        });
919        let validator = compile_schema_with_formats(&schema).unwrap();
920
921        // Valid URI
922        let valid = serde_json::json!("https://example.com/path?query=1");
923        assert!(validator.is_valid(&valid));
924
925        // Invalid URI (relative path)
926        let invalid = serde_json::json!("not a uri");
927        assert!(!validator.is_valid(&invalid));
928    }
929
930    #[test]
931    fn format_validation_ipv4() {
932        let schema = serde_json::json!({
933            "type": "string",
934            "format": "ipv4"
935        });
936        let validator = compile_schema_with_formats(&schema).unwrap();
937
938        // Valid IPv4
939        let valid = serde_json::json!("192.168.1.1");
940        assert!(validator.is_valid(&valid));
941
942        // Invalid IPv4
943        let invalid = serde_json::json!("999.999.999.999");
944        assert!(!validator.is_valid(&invalid));
945    }
946
947    #[test]
948    fn format_validation_ipv6() {
949        let schema = serde_json::json!({
950            "type": "string",
951            "format": "ipv6"
952        });
953        let validator = compile_schema_with_formats(&schema).unwrap();
954
955        // Valid IPv6
956        let valid = serde_json::json!("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
957        assert!(validator.is_valid(&valid));
958
959        // Invalid IPv6
960        let invalid = serde_json::json!("not-ipv6");
961        assert!(!validator.is_valid(&invalid));
962    }
963
964    // ========================
965    // Request Limits Tests
966    // ========================
967
968    #[test]
969    fn validate_all_limits() {
970        let limits = RequestLimits {
971            max_uri_length: 10,
972            max_headers: 2,
973            max_body_size: 50,
974            ..Default::default()
975        };
976
977        let mut headers = HashMap::new();
978        headers.insert("A".to_string(), "1".to_string());
979
980        // All within limits
981        assert!(limits.validate_all("/short", &headers, 10).is_ok());
982
983        // URI too long
984        let result = limits.validate_all("/this/is/a/very/long/uri", &headers, 10);
985        assert!(result.is_err());
986        assert_eq!(result.unwrap_err().len(), 1);
987
988        // Multiple violations
989        let many_headers: HashMap<String, String> = (0..5)
990            .map(|i| (format!("H{}", i), "v".to_string()))
991            .collect();
992        let result = limits.validate_all("/this/is/a/very/long/uri", &many_headers, 100);
993        assert!(result.is_err());
994        assert_eq!(result.unwrap_err().len(), 3); // URI + headers + body
995    }
996
997    // ========================
998    // AsyncAPI Message Validation Tests
999    // ========================
1000
1001    #[test]
1002    fn validate_asyncapi_message_payload() {
1003        // Simulates how the parser creates request_body from AsyncAPI message payload
1004        use barbacane_compiler::ContentSchema;
1005        use std::collections::BTreeMap;
1006
1007        // Message schema with required fields (typical event payload)
1008        let message_schema = serde_json::json!({
1009            "type": "object",
1010            "required": ["eventId", "userId", "timestamp"],
1011            "properties": {
1012                "eventId": { "type": "string", "format": "uuid" },
1013                "userId": { "type": "string" },
1014                "timestamp": { "type": "string", "format": "date-time" },
1015                "metadata": {
1016                    "type": "object",
1017                    "additionalProperties": true
1018                }
1019            }
1020        });
1021
1022        let mut content = BTreeMap::new();
1023        content.insert(
1024            "application/json".to_string(),
1025            ContentSchema {
1026                schema: Some(message_schema),
1027            },
1028        );
1029
1030        let request_body = RequestBody {
1031            required: true,
1032            content,
1033        };
1034
1035        let validator = OperationValidator::new(&[], Some(&request_body));
1036
1037        // Valid message payload
1038        let valid_payload = br#"{
1039            "eventId": "550e8400-e29b-41d4-a716-446655440000",
1040            "userId": "user-123",
1041            "timestamp": "2024-01-29T12:30:00Z"
1042        }"#;
1043        let result = validator.validate_body(Some("application/json"), valid_payload);
1044        assert!(result.is_ok(), "Valid message should pass: {:?}", result);
1045
1046        // Invalid: missing required field
1047        let missing_field = br#"{
1048            "eventId": "550e8400-e29b-41d4-a716-446655440000",
1049            "userId": "user-123"
1050        }"#;
1051        let result = validator.validate_body(Some("application/json"), missing_field);
1052        assert!(result.is_err(), "Missing timestamp should fail");
1053
1054        // Invalid: wrong format for eventId
1055        let wrong_format = br#"{
1056            "eventId": "not-a-uuid",
1057            "userId": "user-123",
1058            "timestamp": "2024-01-29T12:30:00Z"
1059        }"#;
1060        let result = validator.validate_body(Some("application/json"), wrong_format);
1061        assert!(result.is_err(), "Invalid UUID format should fail");
1062    }
1063
1064    #[test]
1065    fn validate_asyncapi_message_with_avro_content_type() {
1066        // AsyncAPI can use different content types (avro, protobuf, etc.)
1067        use barbacane_compiler::ContentSchema;
1068        use std::collections::BTreeMap;
1069
1070        let message_schema = serde_json::json!({
1071            "type": "object",
1072            "required": ["key"],
1073            "properties": {
1074                "key": { "type": "string" }
1075            }
1076        });
1077
1078        let mut content = BTreeMap::new();
1079        content.insert(
1080            "application/vnd.apache.avro+json".to_string(),
1081            ContentSchema {
1082                schema: Some(message_schema),
1083            },
1084        );
1085
1086        let request_body = RequestBody {
1087            required: true,
1088            content,
1089        };
1090
1091        let validator = OperationValidator::new(&[], Some(&request_body));
1092
1093        // JSON-encoded Avro message
1094        let result = validator.validate_body(
1095            Some("application/vnd.apache.avro+json"),
1096            br#"{"key": "value"}"#,
1097        );
1098        assert!(result.is_ok());
1099
1100        // Unsupported content type should fail
1101        let result =
1102            validator.validate_body(Some("application/octet-stream"), br#"{"key": "value"}"#);
1103        assert!(result.is_err());
1104    }
1105
1106    // ========================
1107    // Header Validation Tests
1108    // ========================
1109
1110    #[test]
1111    fn validate_required_header_param() {
1112        let params = vec![make_param("X-Request-Id", "header", true, None)];
1113        let validator = OperationValidator::new(&params, None);
1114
1115        // Missing required header
1116        let headers = HashMap::new();
1117        let result = validator.validate_headers(&headers);
1118        assert!(result.is_err());
1119        let errors = result.unwrap_err();
1120        assert!(matches!(
1121            &errors[0],
1122            ValidationError2::MissingRequiredParameter { name, location }
1123            if name == "X-Request-Id" && location == "header"
1124        ));
1125
1126        // Present header
1127        let mut headers = HashMap::new();
1128        headers.insert("X-Request-Id".to_string(), "abc-123".to_string());
1129        let result = validator.validate_headers(&headers);
1130        assert!(result.is_ok());
1131    }
1132
1133    #[test]
1134    fn validate_optional_header_param() {
1135        let params = vec![make_param("X-Trace-Id", "header", false, None)];
1136        let validator = OperationValidator::new(&params, None);
1137
1138        // Missing optional header is OK
1139        let headers = HashMap::new();
1140        let result = validator.validate_headers(&headers);
1141        assert!(result.is_ok());
1142    }
1143
1144    #[test]
1145    fn validate_header_param_schema() {
1146        let schema = serde_json::json!({
1147            "type": "string",
1148            "pattern": "^Bearer .+$"
1149        });
1150        let params = vec![make_param("Authorization", "header", true, Some(schema))];
1151        let validator = OperationValidator::new(&params, None);
1152
1153        // Valid: matches pattern
1154        let mut headers = HashMap::new();
1155        headers.insert("Authorization".to_string(), "Bearer token123".to_string());
1156        let result = validator.validate_headers(&headers);
1157        assert!(result.is_ok());
1158
1159        // Invalid: doesn't match pattern
1160        headers.insert(
1161            "Authorization".to_string(),
1162            "Basic dXNlcjpwYXNz".to_string(),
1163        );
1164        let result = validator.validate_headers(&headers);
1165        assert!(result.is_err());
1166        let errors = result.unwrap_err();
1167        assert!(matches!(
1168            &errors[0],
1169            ValidationError2::InvalidParameter { name, location, .. }
1170            if name == "Authorization" && location == "header"
1171        ));
1172    }
1173
1174    #[test]
1175    fn validate_header_param_case_insensitive() {
1176        let params = vec![make_param("X-Request-Id", "header", true, None)];
1177        let validator = OperationValidator::new(&params, None);
1178
1179        // Header provided with different casing should match
1180        let mut headers = HashMap::new();
1181        headers.insert("x-request-id".to_string(), "abc-123".to_string());
1182        let result = validator.validate_headers(&headers);
1183        assert!(result.is_ok());
1184
1185        // Uppercase variant
1186        let mut headers = HashMap::new();
1187        headers.insert("X-REQUEST-ID".to_string(), "abc-123".to_string());
1188        let result = validator.validate_headers(&headers);
1189        assert!(result.is_ok());
1190    }
1191
1192    #[test]
1193    fn validate_asyncapi_channel_parameter() {
1194        // Channel parameters (e.g., notifications/{userId}) are path params
1195        let schema = serde_json::json!({
1196            "type": "string",
1197            "pattern": "^user-[a-z0-9]+$"
1198        });
1199
1200        let params = vec![make_param("userId", "path", true, Some(schema))];
1201        let validator = OperationValidator::new(&params, None);
1202
1203        // Valid parameter matching pattern
1204        let result = validator.validate_path_params(&[("userId".into(), "user-abc123".into())]);
1205        assert!(result.is_ok());
1206
1207        // Invalid parameter not matching pattern
1208        let result = validator.validate_path_params(&[("userId".into(), "invalid".into())]);
1209        assert!(result.is_err());
1210    }
1211}