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