1use std::collections::HashMap;
7
8use serde_json::Value;
9use thiserror::Error;
10
11use barbacane_compiler::{Parameter, RequestBody};
12
13#[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#[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 #[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#[derive(Debug, Clone)]
186pub struct RequestLimits {
187 pub max_body_size: usize,
189 pub max_headers: usize,
191 pub max_header_size: usize,
193 pub max_uri_length: usize,
195}
196
197impl Default for RequestLimits {
198 fn default() -> Self {
199 Self {
200 max_body_size: 1024 * 1024, max_headers: 100,
202 max_header_size: 8 * 1024, max_uri_length: 8 * 1024, }
205 }
206}
207
208impl RequestLimits {
209 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 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 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 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 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 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
312fn compile_schema_with_formats(schema: &Value) -> Option<jsonschema::Validator> {
316 jsonschema::options()
317 .should_validate_formats(true)
318 .build(schema)
319 .ok()
320}
321
322pub struct OperationValidator {
324 path_params: Vec<CompiledParam>,
326 query_params: Vec<CompiledParam>,
328 header_params: Vec<CompiledParam>,
330 querystring_param: Option<CompiledParam>,
332 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: HashMap<String, Option<jsonschema::Validator>>,
346}
347
348fn 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(¶m.name) {
360 Some(value) => {
361 if let Some(schema) = ¶m.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 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", 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 _ => {} }
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 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 pub fn validate_query_params(
455 &self,
456 query_string: Option<&str>,
457 ) -> Result<(), Vec<ValidationError2>> {
458 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 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) = ¶m.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 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 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 return Ok(());
565 };
566
567 if body_spec.required && body.is_empty() {
569 return Err(vec![ValidationError2::MissingRequiredBody]);
570 }
571
572 if body.is_empty() {
574 return Ok(());
575 }
576
577 let ct = content_type.unwrap_or("application/octet-stream");
579 let base_ct = ct.split(';').next().unwrap_or(ct).trim();
580
581 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 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 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 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
636pub 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
671fn 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(¶ms, None);
699
700 let result = validator.validate_path_params(&[]);
702 assert!(result.is_err());
703
704 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 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(¶ms, None);
718
719 let result = validator.validate_path_params(&[("id".into(), "123".into())]);
721 assert!(result.is_ok());
722
723 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(¶ms, None);
732
733 let result = validator.validate_query_params(Some(""));
735 assert!(result.is_err());
736
737 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(¶ms, None);
746
747 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 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(¶ms, 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 assert!(validator.validate_query_params(Some("role=user")).is_ok());
768
769 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 let result = validator.validate_body(Some("application/json"), &[]);
796 assert!(result.is_err());
797
798 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 let result = validator.validate_body(Some("application/json"), br#"{"name":"test"}"#);
833 assert!(result.is_ok());
834
835 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 assert!(json.contains("errors"));
896 assert!(json.contains("field"));
897 }
898
899 #[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 #[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 let valid = serde_json::json!({"email": "user@example.com"});
1011 assert!(validator.is_valid(&valid));
1012
1013 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 let valid = serde_json::json!("550e8400-e29b-41d4-a716-446655440000");
1028 assert!(validator.is_valid(&valid));
1029
1030 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 let valid = serde_json::json!("2024-01-29T12:30:00Z");
1045 assert!(validator.is_valid(&valid));
1046
1047 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 let valid = serde_json::json!("https://example.com/path?query=1");
1062 assert!(validator.is_valid(&valid));
1063
1064 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 let valid = serde_json::json!("192.168.1.1");
1079 assert!(validator.is_valid(&valid));
1080
1081 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 let valid = serde_json::json!("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
1096 assert!(validator.is_valid(&valid));
1097
1098 let invalid = serde_json::json!("not-ipv6");
1100 assert!(!validator.is_valid(&invalid));
1101 }
1102
1103 #[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 assert!(limits.validate_all("/short", &headers, 10).is_ok());
1121
1122 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 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); }
1135
1136 #[test]
1141 fn validate_asyncapi_message_payload() {
1142 use barbacane_compiler::ContentSchema;
1144 use std::collections::BTreeMap;
1145
1146 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 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 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 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 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 let result = validator.validate_body(
1234 Some("application/vnd.apache.avro+json"),
1235 br#"{"key": "value"}"#,
1236 );
1237 assert!(result.is_ok());
1238
1239 let result =
1241 validator.validate_body(Some("application/octet-stream"), br#"{"key": "value"}"#);
1242 assert!(result.is_err());
1243 }
1244
1245 #[test]
1250 fn validate_required_header_param() {
1251 let params = vec![make_param("X-Request-Id", "header", true, None)];
1252 let validator = OperationValidator::new(¶ms, None);
1253
1254 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 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(¶ms, None);
1276
1277 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(¶ms, None);
1291
1292 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 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(¶ms, None);
1317
1318 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 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 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(¶ms, None);
1341
1342 let result = validator.validate_path_params(&[("userId".into(), "user-abc123".into())]);
1344 assert!(result.is_ok());
1345
1346 let result = validator.validate_path_params(&[("userId".into(), "invalid".into())]);
1348 assert!(result.is_err());
1349 }
1350
1351 #[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(¶ms, None);
1363
1364 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(¶ms, None);
1373
1374 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(¶ms, None);
1393
1394 let result = validator.validate_querystring(Some("key=value"));
1396 assert!(result.is_ok());
1397
1398 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 let params = vec![make_param("name", "query", false, None)];
1407 let validator = OperationValidator::new(¶ms, 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 assert_eq!(percent_decode("caf%C3%A9"), "café");
1417 assert_eq!(percent_decode("%E2%82%AC"), "€");
1419 assert_eq!(percent_decode("a%20b+c"), "a b c");
1421 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 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}