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    /// OpenAPI 3.2: querystring parameter (entire query string as single value).
305    querystring_param: Option<CompiledParam>,
306    /// Request body configuration.
307    request_body: Option<CompiledRequestBody>,
308}
309
310struct CompiledParam {
311    name: String,
312    required: bool,
313    schema: Option<jsonschema::Validator>,
314}
315
316struct CompiledRequestBody {
317    required: bool,
318    /// Content type -> compiled schema
319    content: HashMap<String, Option<jsonschema::Validator>>,
320}
321
322/// Validate a set of compiled parameters against a value lookup.
323///
324/// Shared logic for path, query, and header parameter validation.
325fn validate_params(
326    params: &[CompiledParam],
327    lookup: impl Fn(&str) -> Option<String>,
328    location: &str,
329) -> Result<(), Vec<ValidationError2>> {
330    let mut errors = Vec::new();
331
332    for param in params {
333        match lookup(&param.name) {
334            Some(value) => {
335                if let Some(schema) = &param.schema {
336                    let json_value = Value::String(value);
337                    let validation_errors: Vec<_> = schema.iter_errors(&json_value).collect();
338                    if !validation_errors.is_empty() {
339                        let reasons: Vec<String> =
340                            validation_errors.iter().map(|e| e.to_string()).collect();
341                        errors.push(ValidationError2::InvalidParameter {
342                            name: param.name.clone(),
343                            location: location.into(),
344                            reason: reasons.join("; "),
345                        });
346                    }
347                }
348            }
349            None if param.required => {
350                errors.push(ValidationError2::MissingRequiredParameter {
351                    name: param.name.clone(),
352                    location: location.into(),
353                });
354            }
355            None => {}
356        }
357    }
358
359    if errors.is_empty() {
360        Ok(())
361    } else {
362        Err(errors)
363    }
364}
365
366impl OperationValidator {
367    /// Create a new validator from parsed operation metadata.
368    pub fn new(parameters: &[Parameter], request_body: Option<&RequestBody>) -> Self {
369        let mut path_params = Vec::new();
370        let mut query_params = Vec::new();
371        let mut header_params = Vec::new();
372        let mut querystring_param = None;
373
374        for param in parameters {
375            let compiled = CompiledParam {
376                name: param.name.clone(),
377                required: param.required || param.location == "path", // Path params always required
378                schema: param.schema.as_ref().and_then(compile_schema_with_formats),
379            };
380
381            match param.location.as_str() {
382                "path" => path_params.push(compiled),
383                "query" => query_params.push(compiled),
384                "header" => header_params.push(compiled),
385                "querystring" => querystring_param = Some(compiled),
386                _ => {} // Ignore cookie params for now
387            }
388        }
389
390        let compiled_body = request_body.map(|rb| {
391            let mut content = HashMap::new();
392            for (media_type, content_schema) in &rb.content {
393                let schema = content_schema
394                    .schema
395                    .as_ref()
396                    .and_then(compile_schema_with_formats);
397                content.insert(media_type.clone(), schema);
398            }
399            CompiledRequestBody {
400                required: rb.required,
401                content,
402            }
403        });
404
405        Self {
406            path_params,
407            query_params,
408            header_params,
409            querystring_param,
410            request_body: compiled_body,
411        }
412    }
413
414    /// Validate path parameters extracted by the router.
415    pub fn validate_path_params(
416        &self,
417        params: &[(String, String)],
418    ) -> Result<(), Vec<ValidationError2>> {
419        let param_map: HashMap<_, _> = params.iter().cloned().collect();
420        validate_params(
421            &self.path_params,
422            |name| param_map.get(name).cloned(),
423            "path",
424        )
425    }
426
427    /// Validate query parameters.
428    pub fn validate_query_params(
429        &self,
430        query_string: Option<&str>,
431    ) -> Result<(), Vec<ValidationError2>> {
432        let param_map: HashMap<String, String> = query_string
433            .unwrap_or("")
434            .split('&')
435            .filter(|s| !s.is_empty())
436            .filter_map(|pair| {
437                let mut parts = pair.splitn(2, '=');
438                let key = parts.next()?;
439                let value = parts.next().unwrap_or("");
440                Some((urlencoding_decode(key), urlencoding_decode(value)))
441            })
442            .collect();
443
444        validate_params(
445            &self.query_params,
446            |name| param_map.get(name).cloned(),
447            "query",
448        )
449    }
450
451    /// OpenAPI 3.2: validate the entire query string as a single value.
452    pub fn validate_querystring(
453        &self,
454        query_string: Option<&str>,
455    ) -> Result<(), Vec<ValidationError2>> {
456        let Some(param) = &self.querystring_param else {
457            return Ok(());
458        };
459
460        let qs = query_string.unwrap_or("");
461
462        if qs.is_empty() && param.required {
463            return Err(vec![ValidationError2::MissingRequiredParameter {
464                name: param.name.clone(),
465                location: "querystring".into(),
466            }]);
467        }
468
469        if !qs.is_empty() {
470            if let Some(schema) = &param.schema {
471                let json_value = Value::String(qs.to_string());
472                let validation_errors: Vec<_> = schema.iter_errors(&json_value).collect();
473                if !validation_errors.is_empty() {
474                    let reasons: Vec<String> =
475                        validation_errors.iter().map(|e| e.to_string()).collect();
476                    return Err(vec![ValidationError2::InvalidParameter {
477                        name: param.name.clone(),
478                        location: "querystring".into(),
479                        reason: reasons.join("; "),
480                    }]);
481                }
482            }
483        }
484
485        Ok(())
486    }
487
488    /// Validate request headers (case-insensitive matching).
489    pub fn validate_headers(
490        &self,
491        headers: &HashMap<String, String>,
492    ) -> Result<(), Vec<ValidationError2>> {
493        let headers_lower: HashMap<String, String> = headers
494            .iter()
495            .map(|(k, v)| (k.to_lowercase(), v.clone()))
496            .collect();
497
498        validate_params(
499            &self.header_params,
500            |name| headers_lower.get(&name.to_lowercase()).cloned(),
501            "header",
502        )
503    }
504
505    /// Validate request body.
506    pub fn validate_body(
507        &self,
508        content_type: Option<&str>,
509        body: &[u8],
510    ) -> Result<(), Vec<ValidationError2>> {
511        let Some(body_spec) = &self.request_body else {
512            // No body spec, nothing to validate
513            return Ok(());
514        };
515
516        // Check if body is required but missing
517        if body_spec.required && body.is_empty() {
518            return Err(vec![ValidationError2::MissingRequiredBody]);
519        }
520
521        // If body is empty and not required, skip validation
522        if body.is_empty() {
523            return Ok(());
524        }
525
526        // Check content type
527        let ct = content_type.unwrap_or("application/octet-stream");
528        let base_ct = ct.split(';').next().unwrap_or(ct).trim();
529
530        // Find matching content type (with wildcard support)
531        let schema = if let Some(schema) = body_spec.content.get(base_ct) {
532            schema
533        } else if let Some(schema) = body_spec.content.get("*/*") {
534            schema
535        } else {
536            return Err(vec![ValidationError2::UnsupportedContentType(
537                base_ct.to_string(),
538            )]);
539        };
540
541        // Validate JSON body against schema
542        if let Some(schema) = schema {
543            if base_ct.contains("json") {
544                let json_body: Value = match serde_json::from_slice(body) {
545                    Ok(v) => v,
546                    Err(e) => {
547                        return Err(vec![ValidationError2::InvalidBody(format!(
548                            "invalid JSON: {}",
549                            e
550                        ))]);
551                    }
552                };
553
554                let validation_errors: Vec<_> = schema.iter_errors(&json_body).collect();
555                if !validation_errors.is_empty() {
556                    let reasons: Vec<String> =
557                        validation_errors.iter().map(|e| e.to_string()).collect();
558                    return Err(vec![ValidationError2::InvalidBody(reasons.join("; "))]);
559                }
560            }
561        }
562
563        Ok(())
564    }
565
566    /// Validate entire request (fail-fast: stops at first error category).
567    pub fn validate_request(
568        &self,
569        path_params: &[(String, String)],
570        query_string: Option<&str>,
571        headers: &HashMap<String, String>,
572        content_type: Option<&str>,
573        body: &[u8],
574    ) -> Result<(), Vec<ValidationError2>> {
575        // Validate in order: path -> query -> querystring -> headers -> body
576        self.validate_path_params(path_params)?;
577        self.validate_query_params(query_string)?;
578        self.validate_querystring(query_string)?;
579        self.validate_headers(headers)?;
580        self.validate_body(content_type, body)?;
581        Ok(())
582    }
583}
584
585/// Simple URL decoding (handles %XX escapes).
586fn urlencoding_decode(input: &str) -> String {
587    let mut result = String::with_capacity(input.len());
588    let mut chars = input.chars().peekable();
589
590    while let Some(c) = chars.next() {
591        if c == '%' {
592            let hex: String = chars.by_ref().take(2).collect();
593            if let Ok(byte) = u8::from_str_radix(&hex, 16) {
594                result.push(byte as char);
595            } else {
596                result.push('%');
597                result.push_str(&hex);
598            }
599        } else if c == '+' {
600            result.push(' ');
601        } else {
602            result.push(c);
603        }
604    }
605
606    result
607}
608
609#[cfg(test)]
610mod tests {
611    use super::compile_schema_with_formats;
612    use super::*;
613
614    fn make_param(name: &str, location: &str, required: bool, schema: Option<Value>) -> Parameter {
615        Parameter {
616            name: name.to_string(),
617            location: location.to_string(),
618            required,
619            schema,
620        }
621    }
622
623    #[test]
624    fn validate_required_path_param() {
625        let params = vec![make_param("id", "path", true, None)];
626        let validator = OperationValidator::new(&params, None);
627
628        // Missing required param
629        let result = validator.validate_path_params(&[]);
630        assert!(result.is_err());
631
632        // Present param
633        let result = validator.validate_path_params(&[("id".into(), "123".into())]);
634        assert!(result.is_ok());
635    }
636
637    #[test]
638    fn validate_path_param_schema() {
639        // Path parameters are always strings. Use a pattern to validate format.
640        let schema = serde_json::json!({
641            "type": "string",
642            "pattern": "^[0-9]+$"
643        });
644        let params = vec![make_param("id", "path", true, Some(schema))];
645        let validator = OperationValidator::new(&params, None);
646
647        // Valid: numeric string
648        let result = validator.validate_path_params(&[("id".into(), "123".into())]);
649        assert!(result.is_ok());
650
651        // Invalid: not matching the numeric pattern
652        let result = validator.validate_path_params(&[("id".into(), "abc".into())]);
653        assert!(result.is_err());
654    }
655
656    #[test]
657    fn validate_required_query_param() {
658        let params = vec![make_param("page", "query", true, None)];
659        let validator = OperationValidator::new(&params, None);
660
661        // Missing required
662        let result = validator.validate_query_params(Some(""));
663        assert!(result.is_err());
664
665        // Present
666        let result = validator.validate_query_params(Some("page=1"));
667        assert!(result.is_ok());
668    }
669
670    #[test]
671    fn validate_optional_query_param() {
672        let params = vec![make_param("limit", "query", false, None)];
673        let validator = OperationValidator::new(&params, None);
674
675        // Missing optional is OK
676        let result = validator.validate_query_params(Some(""));
677        assert!(result.is_ok());
678    }
679
680    #[test]
681    fn validate_required_body() {
682        use barbacane_compiler::ContentSchema;
683        use std::collections::BTreeMap;
684
685        let mut content = BTreeMap::new();
686        content.insert(
687            "application/json".to_string(),
688            ContentSchema { schema: None },
689        );
690
691        let request_body = RequestBody {
692            required: true,
693            content,
694        };
695
696        let validator = OperationValidator::new(&[], Some(&request_body));
697
698        // Missing required body
699        let result = validator.validate_body(Some("application/json"), &[]);
700        assert!(result.is_err());
701
702        // Present body
703        let result = validator.validate_body(Some("application/json"), b"{}");
704        assert!(result.is_ok());
705    }
706
707    #[test]
708    fn validate_body_schema() {
709        use barbacane_compiler::ContentSchema;
710        use std::collections::BTreeMap;
711
712        let schema = serde_json::json!({
713            "type": "object",
714            "required": ["name"],
715            "properties": {
716                "name": { "type": "string" }
717            }
718        });
719
720        let mut content = BTreeMap::new();
721        content.insert(
722            "application/json".to_string(),
723            ContentSchema {
724                schema: Some(schema),
725            },
726        );
727
728        let request_body = RequestBody {
729            required: true,
730            content,
731        };
732
733        let validator = OperationValidator::new(&[], Some(&request_body));
734
735        // Valid body
736        let result = validator.validate_body(Some("application/json"), br#"{"name":"test"}"#);
737        assert!(result.is_ok());
738
739        // Invalid: missing required field
740        let result = validator.validate_body(Some("application/json"), b"{}");
741        assert!(result.is_err());
742    }
743
744    #[test]
745    fn validate_unsupported_content_type() {
746        use barbacane_compiler::ContentSchema;
747        use std::collections::BTreeMap;
748
749        let mut content = BTreeMap::new();
750        content.insert(
751            "application/json".to_string(),
752            ContentSchema { schema: None },
753        );
754
755        let request_body = RequestBody {
756            required: true,
757            content,
758        };
759
760        let validator = OperationValidator::new(&[], Some(&request_body));
761
762        let result = validator.validate_body(Some("text/plain"), b"hello");
763        assert!(result.is_err());
764
765        if let Err(errors) = result {
766            assert!(matches!(
767                errors[0],
768                ValidationError2::UnsupportedContentType(_)
769            ));
770        }
771    }
772
773    #[test]
774    fn problem_details_format() {
775        let errors = vec![ValidationError2::MissingRequiredParameter {
776            name: "id".into(),
777            location: "path".into(),
778        }];
779
780        let problem = ProblemDetails::validation_error(&errors, false);
781        assert_eq!(problem.status, 400);
782        assert_eq!(problem.error_type, "urn:barbacane:error:validation-failed");
783
784        let json = problem.to_json();
785        assert!(json.contains("validation-failed"));
786    }
787
788    #[test]
789    fn problem_details_dev_mode() {
790        let errors = vec![ValidationError2::MissingRequiredParameter {
791            name: "id".into(),
792            location: "path".into(),
793        }];
794
795        let problem = ProblemDetails::validation_error(&errors, true);
796        let json = problem.to_json();
797
798        // Dev mode should include error details
799        assert!(json.contains("errors"));
800        assert!(json.contains("field"));
801    }
802
803    // ========================
804    // Request Limits Tests
805    // ========================
806
807    #[test]
808    fn validate_uri_length_ok() {
809        let limits = RequestLimits::default();
810        let uri = "/api/users/123";
811        assert!(limits.validate_uri(uri).is_ok());
812    }
813
814    #[test]
815    fn validate_uri_length_too_long() {
816        let limits = RequestLimits {
817            max_uri_length: 10,
818            ..Default::default()
819        };
820        let uri = "/api/users/123456789";
821        let result = limits.validate_uri(uri);
822        assert!(result.is_err());
823        assert!(matches!(
824            result.unwrap_err(),
825            ValidationError2::UriTooLong { .. }
826        ));
827    }
828
829    #[test]
830    fn validate_header_count_ok() {
831        let limits = RequestLimits::default();
832        let headers: HashMap<String, String> = (0..10)
833            .map(|i| (format!("Header-{}", i), "value".to_string()))
834            .collect();
835        assert!(limits.validate_headers(&headers).is_ok());
836    }
837
838    #[test]
839    fn validate_header_count_too_many() {
840        let limits = RequestLimits {
841            max_headers: 5,
842            ..Default::default()
843        };
844        let headers: HashMap<String, String> = (0..10)
845            .map(|i| (format!("Header-{}", i), "value".to_string()))
846            .collect();
847        let result = limits.validate_headers(&headers);
848        assert!(result.is_err());
849        assert!(matches!(
850            result.unwrap_err(),
851            ValidationError2::TooManyHeaders { .. }
852        ));
853    }
854
855    #[test]
856    fn validate_header_size_ok() {
857        let limits = RequestLimits::default();
858        let mut headers = HashMap::new();
859        headers.insert("Content-Type".to_string(), "application/json".to_string());
860        assert!(limits.validate_headers(&headers).is_ok());
861    }
862
863    #[test]
864    fn validate_header_size_too_large() {
865        let limits = RequestLimits {
866            max_header_size: 20,
867            ..Default::default()
868        };
869        let mut headers = HashMap::new();
870        headers.insert("X-Very-Long-Header".to_string(), "a".repeat(100));
871        let result = limits.validate_headers(&headers);
872        assert!(result.is_err());
873        assert!(matches!(
874            result.unwrap_err(),
875            ValidationError2::HeaderTooLarge { .. }
876        ));
877    }
878
879    #[test]
880    fn validate_body_size_ok() {
881        let limits = RequestLimits::default();
882        assert!(limits.validate_body_size(1000).is_ok());
883    }
884
885    #[test]
886    fn validate_body_size_too_large() {
887        let limits = RequestLimits {
888            max_body_size: 100,
889            ..Default::default()
890        };
891        let result = limits.validate_body_size(1000);
892        assert!(result.is_err());
893        assert!(matches!(
894            result.unwrap_err(),
895            ValidationError2::BodyTooLarge { .. }
896        ));
897    }
898
899    // ========================
900    // Format Validation Tests
901    // ========================
902
903    #[test]
904    fn format_validation_email() {
905        let schema = serde_json::json!({
906            "type": "object",
907            "properties": {
908                "email": { "type": "string", "format": "email" }
909            }
910        });
911        let validator = compile_schema_with_formats(&schema).unwrap();
912
913        // Valid email
914        let valid = serde_json::json!({"email": "user@example.com"});
915        assert!(validator.is_valid(&valid));
916
917        // Invalid email
918        let invalid = serde_json::json!({"email": "not-an-email"});
919        assert!(!validator.is_valid(&invalid));
920    }
921
922    #[test]
923    fn format_validation_uuid() {
924        let schema = serde_json::json!({
925            "type": "string",
926            "format": "uuid"
927        });
928        let validator = compile_schema_with_formats(&schema).unwrap();
929
930        // Valid UUID
931        let valid = serde_json::json!("550e8400-e29b-41d4-a716-446655440000");
932        assert!(validator.is_valid(&valid));
933
934        // Invalid UUID
935        let invalid = serde_json::json!("not-a-uuid");
936        assert!(!validator.is_valid(&invalid));
937    }
938
939    #[test]
940    fn format_validation_date_time() {
941        let schema = serde_json::json!({
942            "type": "string",
943            "format": "date-time"
944        });
945        let validator = compile_schema_with_formats(&schema).unwrap();
946
947        // Valid date-time (RFC 3339)
948        let valid = serde_json::json!("2024-01-29T12:30:00Z");
949        assert!(validator.is_valid(&valid));
950
951        // Invalid date-time
952        let invalid = serde_json::json!("not-a-date");
953        assert!(!validator.is_valid(&invalid));
954    }
955
956    #[test]
957    fn format_validation_uri() {
958        let schema = serde_json::json!({
959            "type": "string",
960            "format": "uri"
961        });
962        let validator = compile_schema_with_formats(&schema).unwrap();
963
964        // Valid URI
965        let valid = serde_json::json!("https://example.com/path?query=1");
966        assert!(validator.is_valid(&valid));
967
968        // Invalid URI (relative path)
969        let invalid = serde_json::json!("not a uri");
970        assert!(!validator.is_valid(&invalid));
971    }
972
973    #[test]
974    fn format_validation_ipv4() {
975        let schema = serde_json::json!({
976            "type": "string",
977            "format": "ipv4"
978        });
979        let validator = compile_schema_with_formats(&schema).unwrap();
980
981        // Valid IPv4
982        let valid = serde_json::json!("192.168.1.1");
983        assert!(validator.is_valid(&valid));
984
985        // Invalid IPv4
986        let invalid = serde_json::json!("999.999.999.999");
987        assert!(!validator.is_valid(&invalid));
988    }
989
990    #[test]
991    fn format_validation_ipv6() {
992        let schema = serde_json::json!({
993            "type": "string",
994            "format": "ipv6"
995        });
996        let validator = compile_schema_with_formats(&schema).unwrap();
997
998        // Valid IPv6
999        let valid = serde_json::json!("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
1000        assert!(validator.is_valid(&valid));
1001
1002        // Invalid IPv6
1003        let invalid = serde_json::json!("not-ipv6");
1004        assert!(!validator.is_valid(&invalid));
1005    }
1006
1007    // ========================
1008    // Request Limits Tests
1009    // ========================
1010
1011    #[test]
1012    fn validate_all_limits() {
1013        let limits = RequestLimits {
1014            max_uri_length: 10,
1015            max_headers: 2,
1016            max_body_size: 50,
1017            ..Default::default()
1018        };
1019
1020        let mut headers = HashMap::new();
1021        headers.insert("A".to_string(), "1".to_string());
1022
1023        // All within limits
1024        assert!(limits.validate_all("/short", &headers, 10).is_ok());
1025
1026        // URI too long
1027        let result = limits.validate_all("/this/is/a/very/long/uri", &headers, 10);
1028        assert!(result.is_err());
1029        assert_eq!(result.unwrap_err().len(), 1);
1030
1031        // Multiple violations
1032        let many_headers: HashMap<String, String> = (0..5)
1033            .map(|i| (format!("H{}", i), "v".to_string()))
1034            .collect();
1035        let result = limits.validate_all("/this/is/a/very/long/uri", &many_headers, 100);
1036        assert!(result.is_err());
1037        assert_eq!(result.unwrap_err().len(), 3); // URI + headers + body
1038    }
1039
1040    // ========================
1041    // AsyncAPI Message Validation Tests
1042    // ========================
1043
1044    #[test]
1045    fn validate_asyncapi_message_payload() {
1046        // Simulates how the parser creates request_body from AsyncAPI message payload
1047        use barbacane_compiler::ContentSchema;
1048        use std::collections::BTreeMap;
1049
1050        // Message schema with required fields (typical event payload)
1051        let message_schema = serde_json::json!({
1052            "type": "object",
1053            "required": ["eventId", "userId", "timestamp"],
1054            "properties": {
1055                "eventId": { "type": "string", "format": "uuid" },
1056                "userId": { "type": "string" },
1057                "timestamp": { "type": "string", "format": "date-time" },
1058                "metadata": {
1059                    "type": "object",
1060                    "additionalProperties": true
1061                }
1062            }
1063        });
1064
1065        let mut content = BTreeMap::new();
1066        content.insert(
1067            "application/json".to_string(),
1068            ContentSchema {
1069                schema: Some(message_schema),
1070            },
1071        );
1072
1073        let request_body = RequestBody {
1074            required: true,
1075            content,
1076        };
1077
1078        let validator = OperationValidator::new(&[], Some(&request_body));
1079
1080        // Valid message payload
1081        let valid_payload = br#"{
1082            "eventId": "550e8400-e29b-41d4-a716-446655440000",
1083            "userId": "user-123",
1084            "timestamp": "2024-01-29T12:30:00Z"
1085        }"#;
1086        let result = validator.validate_body(Some("application/json"), valid_payload);
1087        assert!(result.is_ok(), "Valid message should pass: {:?}", result);
1088
1089        // Invalid: missing required field
1090        let missing_field = br#"{
1091            "eventId": "550e8400-e29b-41d4-a716-446655440000",
1092            "userId": "user-123"
1093        }"#;
1094        let result = validator.validate_body(Some("application/json"), missing_field);
1095        assert!(result.is_err(), "Missing timestamp should fail");
1096
1097        // Invalid: wrong format for eventId
1098        let wrong_format = br#"{
1099            "eventId": "not-a-uuid",
1100            "userId": "user-123",
1101            "timestamp": "2024-01-29T12:30:00Z"
1102        }"#;
1103        let result = validator.validate_body(Some("application/json"), wrong_format);
1104        assert!(result.is_err(), "Invalid UUID format should fail");
1105    }
1106
1107    #[test]
1108    fn validate_asyncapi_message_with_avro_content_type() {
1109        // AsyncAPI can use different content types (avro, protobuf, etc.)
1110        use barbacane_compiler::ContentSchema;
1111        use std::collections::BTreeMap;
1112
1113        let message_schema = serde_json::json!({
1114            "type": "object",
1115            "required": ["key"],
1116            "properties": {
1117                "key": { "type": "string" }
1118            }
1119        });
1120
1121        let mut content = BTreeMap::new();
1122        content.insert(
1123            "application/vnd.apache.avro+json".to_string(),
1124            ContentSchema {
1125                schema: Some(message_schema),
1126            },
1127        );
1128
1129        let request_body = RequestBody {
1130            required: true,
1131            content,
1132        };
1133
1134        let validator = OperationValidator::new(&[], Some(&request_body));
1135
1136        // JSON-encoded Avro message
1137        let result = validator.validate_body(
1138            Some("application/vnd.apache.avro+json"),
1139            br#"{"key": "value"}"#,
1140        );
1141        assert!(result.is_ok());
1142
1143        // Unsupported content type should fail
1144        let result =
1145            validator.validate_body(Some("application/octet-stream"), br#"{"key": "value"}"#);
1146        assert!(result.is_err());
1147    }
1148
1149    // ========================
1150    // Header Validation Tests
1151    // ========================
1152
1153    #[test]
1154    fn validate_required_header_param() {
1155        let params = vec![make_param("X-Request-Id", "header", true, None)];
1156        let validator = OperationValidator::new(&params, None);
1157
1158        // Missing required header
1159        let headers = HashMap::new();
1160        let result = validator.validate_headers(&headers);
1161        assert!(result.is_err());
1162        let errors = result.unwrap_err();
1163        assert!(matches!(
1164            &errors[0],
1165            ValidationError2::MissingRequiredParameter { name, location }
1166            if name == "X-Request-Id" && location == "header"
1167        ));
1168
1169        // Present header
1170        let mut headers = HashMap::new();
1171        headers.insert("X-Request-Id".to_string(), "abc-123".to_string());
1172        let result = validator.validate_headers(&headers);
1173        assert!(result.is_ok());
1174    }
1175
1176    #[test]
1177    fn validate_optional_header_param() {
1178        let params = vec![make_param("X-Trace-Id", "header", false, None)];
1179        let validator = OperationValidator::new(&params, None);
1180
1181        // Missing optional header is OK
1182        let headers = HashMap::new();
1183        let result = validator.validate_headers(&headers);
1184        assert!(result.is_ok());
1185    }
1186
1187    #[test]
1188    fn validate_header_param_schema() {
1189        let schema = serde_json::json!({
1190            "type": "string",
1191            "pattern": "^Bearer .+$"
1192        });
1193        let params = vec![make_param("Authorization", "header", true, Some(schema))];
1194        let validator = OperationValidator::new(&params, None);
1195
1196        // Valid: matches pattern
1197        let mut headers = HashMap::new();
1198        headers.insert("Authorization".to_string(), "Bearer token123".to_string());
1199        let result = validator.validate_headers(&headers);
1200        assert!(result.is_ok());
1201
1202        // Invalid: doesn't match pattern
1203        headers.insert(
1204            "Authorization".to_string(),
1205            "Basic dXNlcjpwYXNz".to_string(),
1206        );
1207        let result = validator.validate_headers(&headers);
1208        assert!(result.is_err());
1209        let errors = result.unwrap_err();
1210        assert!(matches!(
1211            &errors[0],
1212            ValidationError2::InvalidParameter { name, location, .. }
1213            if name == "Authorization" && location == "header"
1214        ));
1215    }
1216
1217    #[test]
1218    fn validate_header_param_case_insensitive() {
1219        let params = vec![make_param("X-Request-Id", "header", true, None)];
1220        let validator = OperationValidator::new(&params, None);
1221
1222        // Header provided with different casing should match
1223        let mut headers = HashMap::new();
1224        headers.insert("x-request-id".to_string(), "abc-123".to_string());
1225        let result = validator.validate_headers(&headers);
1226        assert!(result.is_ok());
1227
1228        // Uppercase variant
1229        let mut headers = HashMap::new();
1230        headers.insert("X-REQUEST-ID".to_string(), "abc-123".to_string());
1231        let result = validator.validate_headers(&headers);
1232        assert!(result.is_ok());
1233    }
1234
1235    #[test]
1236    fn validate_asyncapi_channel_parameter() {
1237        // Channel parameters (e.g., notifications/{userId}) are path params
1238        let schema = serde_json::json!({
1239            "type": "string",
1240            "pattern": "^user-[a-z0-9]+$"
1241        });
1242
1243        let params = vec![make_param("userId", "path", true, Some(schema))];
1244        let validator = OperationValidator::new(&params, None);
1245
1246        // Valid parameter matching pattern
1247        let result = validator.validate_path_params(&[("userId".into(), "user-abc123".into())]);
1248        assert!(result.is_ok());
1249
1250        // Invalid parameter not matching pattern
1251        let result = validator.validate_path_params(&[("userId".into(), "invalid".into())]);
1252        assert!(result.is_err());
1253    }
1254
1255    // ========================
1256    // OpenAPI 3.2 Querystring Tests
1257    // ========================
1258
1259    #[test]
1260    fn validate_querystring_param_present() {
1261        let schema = serde_json::json!({
1262            "type": "string",
1263            "minLength": 1
1264        });
1265        let params = vec![make_param("q", "querystring", true, Some(schema))];
1266        let validator = OperationValidator::new(&params, None);
1267
1268        // Valid: non-empty query string
1269        let result = validator.validate_querystring(Some("filter=active&sort=name"));
1270        assert!(result.is_ok());
1271    }
1272
1273    #[test]
1274    fn validate_querystring_param_missing_required() {
1275        let params = vec![make_param("q", "querystring", true, None)];
1276        let validator = OperationValidator::new(&params, None);
1277
1278        // Missing required querystring
1279        let result = validator.validate_querystring(Some(""));
1280        assert!(result.is_err());
1281        let errors = result.unwrap_err();
1282        assert!(matches!(
1283            &errors[0],
1284            ValidationError2::MissingRequiredParameter { location, .. }
1285            if location == "querystring"
1286        ));
1287    }
1288
1289    #[test]
1290    fn validate_querystring_param_schema() {
1291        let schema = serde_json::json!({
1292            "type": "string",
1293            "pattern": "^[a-z]+=\\w+$"
1294        });
1295        let params = vec![make_param("q", "querystring", false, Some(schema))];
1296        let validator = OperationValidator::new(&params, None);
1297
1298        // Valid: matches pattern
1299        let result = validator.validate_querystring(Some("key=value"));
1300        assert!(result.is_ok());
1301
1302        // Invalid: doesn't match pattern
1303        let result = validator.validate_querystring(Some("KEY=value&other=123"));
1304        assert!(result.is_err());
1305    }
1306
1307    #[test]
1308    fn validate_querystring_not_set() {
1309        // No querystring parameter defined — should pass through
1310        let params = vec![make_param("name", "query", false, None)];
1311        let validator = OperationValidator::new(&params, None);
1312
1313        let result = validator.validate_querystring(Some("anything"));
1314        assert!(result.is_ok());
1315    }
1316}