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_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 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
286fn compile_schema_with_formats(schema: &Value) -> Option<jsonschema::Validator> {
290 jsonschema::options()
291 .should_validate_formats(true)
292 .build(schema)
293 .ok()
294}
295
296pub struct OperationValidator {
298 path_params: Vec<CompiledParam>,
300 query_params: Vec<CompiledParam>,
302 header_params: Vec<CompiledParam>,
304 request_body: Option<CompiledRequestBody>,
306}
307
308struct CompiledParam {
309 name: String,
310 required: bool,
311 schema: Option<jsonschema::Validator>,
312}
313
314struct CompiledRequestBody {
315 required: bool,
316 content: HashMap<String, Option<jsonschema::Validator>>,
318}
319
320fn validate_params(
324 params: &[CompiledParam],
325 lookup: impl Fn(&str) -> Option<String>,
326 location: &str,
327) -> Result<(), Vec<ValidationError2>> {
328 let mut errors = Vec::new();
329
330 for param in params {
331 match lookup(¶m.name) {
332 Some(value) => {
333 if let Some(schema) = ¶m.schema {
334 let json_value = Value::String(value);
335 let validation_errors: Vec<_> = schema.iter_errors(&json_value).collect();
336 if !validation_errors.is_empty() {
337 let reasons: Vec<String> =
338 validation_errors.iter().map(|e| e.to_string()).collect();
339 errors.push(ValidationError2::InvalidParameter {
340 name: param.name.clone(),
341 location: location.into(),
342 reason: reasons.join("; "),
343 });
344 }
345 }
346 }
347 None if param.required => {
348 errors.push(ValidationError2::MissingRequiredParameter {
349 name: param.name.clone(),
350 location: location.into(),
351 });
352 }
353 None => {}
354 }
355 }
356
357 if errors.is_empty() {
358 Ok(())
359 } else {
360 Err(errors)
361 }
362}
363
364impl OperationValidator {
365 pub fn new(parameters: &[Parameter], request_body: Option<&RequestBody>) -> Self {
367 let mut path_params = Vec::new();
368 let mut query_params = Vec::new();
369 let mut header_params = Vec::new();
370
371 for param in parameters {
372 let compiled = CompiledParam {
373 name: param.name.clone(),
374 required: param.required || param.location == "path", schema: param.schema.as_ref().and_then(compile_schema_with_formats),
376 };
377
378 match param.location.as_str() {
379 "path" => path_params.push(compiled),
380 "query" => query_params.push(compiled),
381 "header" => header_params.push(compiled),
382 _ => {} }
384 }
385
386 let compiled_body = request_body.map(|rb| {
387 let mut content = HashMap::new();
388 for (media_type, content_schema) in &rb.content {
389 let schema = content_schema
390 .schema
391 .as_ref()
392 .and_then(compile_schema_with_formats);
393 content.insert(media_type.clone(), schema);
394 }
395 CompiledRequestBody {
396 required: rb.required,
397 content,
398 }
399 });
400
401 Self {
402 path_params,
403 query_params,
404 header_params,
405 request_body: compiled_body,
406 }
407 }
408
409 pub fn validate_path_params(
411 &self,
412 params: &[(String, String)],
413 ) -> Result<(), Vec<ValidationError2>> {
414 let param_map: HashMap<_, _> = params.iter().cloned().collect();
415 validate_params(
416 &self.path_params,
417 |name| param_map.get(name).cloned(),
418 "path",
419 )
420 }
421
422 pub fn validate_query_params(
424 &self,
425 query_string: Option<&str>,
426 ) -> Result<(), Vec<ValidationError2>> {
427 let param_map: HashMap<String, String> = query_string
428 .unwrap_or("")
429 .split('&')
430 .filter(|s| !s.is_empty())
431 .filter_map(|pair| {
432 let mut parts = pair.splitn(2, '=');
433 let key = parts.next()?;
434 let value = parts.next().unwrap_or("");
435 Some((urlencoding_decode(key), urlencoding_decode(value)))
436 })
437 .collect();
438
439 validate_params(
440 &self.query_params,
441 |name| param_map.get(name).cloned(),
442 "query",
443 )
444 }
445
446 pub fn validate_headers(
448 &self,
449 headers: &HashMap<String, String>,
450 ) -> Result<(), Vec<ValidationError2>> {
451 let headers_lower: HashMap<String, String> = headers
452 .iter()
453 .map(|(k, v)| (k.to_lowercase(), v.clone()))
454 .collect();
455
456 validate_params(
457 &self.header_params,
458 |name| headers_lower.get(&name.to_lowercase()).cloned(),
459 "header",
460 )
461 }
462
463 pub fn validate_body(
465 &self,
466 content_type: Option<&str>,
467 body: &[u8],
468 ) -> Result<(), Vec<ValidationError2>> {
469 let Some(body_spec) = &self.request_body else {
470 return Ok(());
472 };
473
474 if body_spec.required && body.is_empty() {
476 return Err(vec![ValidationError2::MissingRequiredBody]);
477 }
478
479 if body.is_empty() {
481 return Ok(());
482 }
483
484 let ct = content_type.unwrap_or("application/octet-stream");
486 let base_ct = ct.split(';').next().unwrap_or(ct).trim();
487
488 let schema = if let Some(schema) = body_spec.content.get(base_ct) {
490 schema
491 } else if let Some(schema) = body_spec.content.get("*/*") {
492 schema
493 } else {
494 return Err(vec![ValidationError2::UnsupportedContentType(
495 base_ct.to_string(),
496 )]);
497 };
498
499 if let Some(schema) = schema {
501 if base_ct.contains("json") {
502 let json_body: Value = match serde_json::from_slice(body) {
503 Ok(v) => v,
504 Err(e) => {
505 return Err(vec![ValidationError2::InvalidBody(format!(
506 "invalid JSON: {}",
507 e
508 ))]);
509 }
510 };
511
512 let validation_errors: Vec<_> = schema.iter_errors(&json_body).collect();
513 if !validation_errors.is_empty() {
514 let reasons: Vec<String> =
515 validation_errors.iter().map(|e| e.to_string()).collect();
516 return Err(vec![ValidationError2::InvalidBody(reasons.join("; "))]);
517 }
518 }
519 }
520
521 Ok(())
522 }
523
524 pub fn validate_request(
526 &self,
527 path_params: &[(String, String)],
528 query_string: Option<&str>,
529 headers: &HashMap<String, String>,
530 content_type: Option<&str>,
531 body: &[u8],
532 ) -> Result<(), Vec<ValidationError2>> {
533 self.validate_path_params(path_params)?;
535 self.validate_query_params(query_string)?;
536 self.validate_headers(headers)?;
537 self.validate_body(content_type, body)?;
538 Ok(())
539 }
540}
541
542fn urlencoding_decode(input: &str) -> String {
544 let mut result = String::with_capacity(input.len());
545 let mut chars = input.chars().peekable();
546
547 while let Some(c) = chars.next() {
548 if c == '%' {
549 let hex: String = chars.by_ref().take(2).collect();
550 if let Ok(byte) = u8::from_str_radix(&hex, 16) {
551 result.push(byte as char);
552 } else {
553 result.push('%');
554 result.push_str(&hex);
555 }
556 } else if c == '+' {
557 result.push(' ');
558 } else {
559 result.push(c);
560 }
561 }
562
563 result
564}
565
566#[cfg(test)]
567mod tests {
568 use super::compile_schema_with_formats;
569 use super::*;
570
571 fn make_param(name: &str, location: &str, required: bool, schema: Option<Value>) -> Parameter {
572 Parameter {
573 name: name.to_string(),
574 location: location.to_string(),
575 required,
576 schema,
577 }
578 }
579
580 #[test]
581 fn validate_required_path_param() {
582 let params = vec![make_param("id", "path", true, None)];
583 let validator = OperationValidator::new(¶ms, None);
584
585 let result = validator.validate_path_params(&[]);
587 assert!(result.is_err());
588
589 let result = validator.validate_path_params(&[("id".into(), "123".into())]);
591 assert!(result.is_ok());
592 }
593
594 #[test]
595 fn validate_path_param_schema() {
596 let schema = serde_json::json!({
598 "type": "string",
599 "pattern": "^[0-9]+$"
600 });
601 let params = vec![make_param("id", "path", true, Some(schema))];
602 let validator = OperationValidator::new(¶ms, None);
603
604 let result = validator.validate_path_params(&[("id".into(), "123".into())]);
606 assert!(result.is_ok());
607
608 let result = validator.validate_path_params(&[("id".into(), "abc".into())]);
610 assert!(result.is_err());
611 }
612
613 #[test]
614 fn validate_required_query_param() {
615 let params = vec![make_param("page", "query", true, None)];
616 let validator = OperationValidator::new(¶ms, None);
617
618 let result = validator.validate_query_params(Some(""));
620 assert!(result.is_err());
621
622 let result = validator.validate_query_params(Some("page=1"));
624 assert!(result.is_ok());
625 }
626
627 #[test]
628 fn validate_optional_query_param() {
629 let params = vec![make_param("limit", "query", false, None)];
630 let validator = OperationValidator::new(¶ms, None);
631
632 let result = validator.validate_query_params(Some(""));
634 assert!(result.is_ok());
635 }
636
637 #[test]
638 fn validate_required_body() {
639 use barbacane_compiler::ContentSchema;
640 use std::collections::BTreeMap;
641
642 let mut content = BTreeMap::new();
643 content.insert(
644 "application/json".to_string(),
645 ContentSchema { schema: None },
646 );
647
648 let request_body = RequestBody {
649 required: true,
650 content,
651 };
652
653 let validator = OperationValidator::new(&[], Some(&request_body));
654
655 let result = validator.validate_body(Some("application/json"), &[]);
657 assert!(result.is_err());
658
659 let result = validator.validate_body(Some("application/json"), b"{}");
661 assert!(result.is_ok());
662 }
663
664 #[test]
665 fn validate_body_schema() {
666 use barbacane_compiler::ContentSchema;
667 use std::collections::BTreeMap;
668
669 let schema = serde_json::json!({
670 "type": "object",
671 "required": ["name"],
672 "properties": {
673 "name": { "type": "string" }
674 }
675 });
676
677 let mut content = BTreeMap::new();
678 content.insert(
679 "application/json".to_string(),
680 ContentSchema {
681 schema: Some(schema),
682 },
683 );
684
685 let request_body = RequestBody {
686 required: true,
687 content,
688 };
689
690 let validator = OperationValidator::new(&[], Some(&request_body));
691
692 let result = validator.validate_body(Some("application/json"), br#"{"name":"test"}"#);
694 assert!(result.is_ok());
695
696 let result = validator.validate_body(Some("application/json"), b"{}");
698 assert!(result.is_err());
699 }
700
701 #[test]
702 fn validate_unsupported_content_type() {
703 use barbacane_compiler::ContentSchema;
704 use std::collections::BTreeMap;
705
706 let mut content = BTreeMap::new();
707 content.insert(
708 "application/json".to_string(),
709 ContentSchema { schema: None },
710 );
711
712 let request_body = RequestBody {
713 required: true,
714 content,
715 };
716
717 let validator = OperationValidator::new(&[], Some(&request_body));
718
719 let result = validator.validate_body(Some("text/plain"), b"hello");
720 assert!(result.is_err());
721
722 if let Err(errors) = result {
723 assert!(matches!(
724 errors[0],
725 ValidationError2::UnsupportedContentType(_)
726 ));
727 }
728 }
729
730 #[test]
731 fn problem_details_format() {
732 let errors = vec![ValidationError2::MissingRequiredParameter {
733 name: "id".into(),
734 location: "path".into(),
735 }];
736
737 let problem = ProblemDetails::validation_error(&errors, false);
738 assert_eq!(problem.status, 400);
739 assert_eq!(problem.error_type, "urn:barbacane:error:validation-failed");
740
741 let json = problem.to_json();
742 assert!(json.contains("validation-failed"));
743 }
744
745 #[test]
746 fn problem_details_dev_mode() {
747 let errors = vec![ValidationError2::MissingRequiredParameter {
748 name: "id".into(),
749 location: "path".into(),
750 }];
751
752 let problem = ProblemDetails::validation_error(&errors, true);
753 let json = problem.to_json();
754
755 assert!(json.contains("errors"));
757 assert!(json.contains("field"));
758 }
759
760 #[test]
765 fn validate_uri_length_ok() {
766 let limits = RequestLimits::default();
767 let uri = "/api/users/123";
768 assert!(limits.validate_uri(uri).is_ok());
769 }
770
771 #[test]
772 fn validate_uri_length_too_long() {
773 let limits = RequestLimits {
774 max_uri_length: 10,
775 ..Default::default()
776 };
777 let uri = "/api/users/123456789";
778 let result = limits.validate_uri(uri);
779 assert!(result.is_err());
780 assert!(matches!(
781 result.unwrap_err(),
782 ValidationError2::UriTooLong { .. }
783 ));
784 }
785
786 #[test]
787 fn validate_header_count_ok() {
788 let limits = RequestLimits::default();
789 let headers: HashMap<String, String> = (0..10)
790 .map(|i| (format!("Header-{}", i), "value".to_string()))
791 .collect();
792 assert!(limits.validate_headers(&headers).is_ok());
793 }
794
795 #[test]
796 fn validate_header_count_too_many() {
797 let limits = RequestLimits {
798 max_headers: 5,
799 ..Default::default()
800 };
801 let headers: HashMap<String, String> = (0..10)
802 .map(|i| (format!("Header-{}", i), "value".to_string()))
803 .collect();
804 let result = limits.validate_headers(&headers);
805 assert!(result.is_err());
806 assert!(matches!(
807 result.unwrap_err(),
808 ValidationError2::TooManyHeaders { .. }
809 ));
810 }
811
812 #[test]
813 fn validate_header_size_ok() {
814 let limits = RequestLimits::default();
815 let mut headers = HashMap::new();
816 headers.insert("Content-Type".to_string(), "application/json".to_string());
817 assert!(limits.validate_headers(&headers).is_ok());
818 }
819
820 #[test]
821 fn validate_header_size_too_large() {
822 let limits = RequestLimits {
823 max_header_size: 20,
824 ..Default::default()
825 };
826 let mut headers = HashMap::new();
827 headers.insert("X-Very-Long-Header".to_string(), "a".repeat(100));
828 let result = limits.validate_headers(&headers);
829 assert!(result.is_err());
830 assert!(matches!(
831 result.unwrap_err(),
832 ValidationError2::HeaderTooLarge { .. }
833 ));
834 }
835
836 #[test]
837 fn validate_body_size_ok() {
838 let limits = RequestLimits::default();
839 assert!(limits.validate_body_size(1000).is_ok());
840 }
841
842 #[test]
843 fn validate_body_size_too_large() {
844 let limits = RequestLimits {
845 max_body_size: 100,
846 ..Default::default()
847 };
848 let result = limits.validate_body_size(1000);
849 assert!(result.is_err());
850 assert!(matches!(
851 result.unwrap_err(),
852 ValidationError2::BodyTooLarge { .. }
853 ));
854 }
855
856 #[test]
861 fn format_validation_email() {
862 let schema = serde_json::json!({
863 "type": "object",
864 "properties": {
865 "email": { "type": "string", "format": "email" }
866 }
867 });
868 let validator = compile_schema_with_formats(&schema).unwrap();
869
870 let valid = serde_json::json!({"email": "user@example.com"});
872 assert!(validator.is_valid(&valid));
873
874 let invalid = serde_json::json!({"email": "not-an-email"});
876 assert!(!validator.is_valid(&invalid));
877 }
878
879 #[test]
880 fn format_validation_uuid() {
881 let schema = serde_json::json!({
882 "type": "string",
883 "format": "uuid"
884 });
885 let validator = compile_schema_with_formats(&schema).unwrap();
886
887 let valid = serde_json::json!("550e8400-e29b-41d4-a716-446655440000");
889 assert!(validator.is_valid(&valid));
890
891 let invalid = serde_json::json!("not-a-uuid");
893 assert!(!validator.is_valid(&invalid));
894 }
895
896 #[test]
897 fn format_validation_date_time() {
898 let schema = serde_json::json!({
899 "type": "string",
900 "format": "date-time"
901 });
902 let validator = compile_schema_with_formats(&schema).unwrap();
903
904 let valid = serde_json::json!("2024-01-29T12:30:00Z");
906 assert!(validator.is_valid(&valid));
907
908 let invalid = serde_json::json!("not-a-date");
910 assert!(!validator.is_valid(&invalid));
911 }
912
913 #[test]
914 fn format_validation_uri() {
915 let schema = serde_json::json!({
916 "type": "string",
917 "format": "uri"
918 });
919 let validator = compile_schema_with_formats(&schema).unwrap();
920
921 let valid = serde_json::json!("https://example.com/path?query=1");
923 assert!(validator.is_valid(&valid));
924
925 let invalid = serde_json::json!("not a uri");
927 assert!(!validator.is_valid(&invalid));
928 }
929
930 #[test]
931 fn format_validation_ipv4() {
932 let schema = serde_json::json!({
933 "type": "string",
934 "format": "ipv4"
935 });
936 let validator = compile_schema_with_formats(&schema).unwrap();
937
938 let valid = serde_json::json!("192.168.1.1");
940 assert!(validator.is_valid(&valid));
941
942 let invalid = serde_json::json!("999.999.999.999");
944 assert!(!validator.is_valid(&invalid));
945 }
946
947 #[test]
948 fn format_validation_ipv6() {
949 let schema = serde_json::json!({
950 "type": "string",
951 "format": "ipv6"
952 });
953 let validator = compile_schema_with_formats(&schema).unwrap();
954
955 let valid = serde_json::json!("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
957 assert!(validator.is_valid(&valid));
958
959 let invalid = serde_json::json!("not-ipv6");
961 assert!(!validator.is_valid(&invalid));
962 }
963
964 #[test]
969 fn validate_all_limits() {
970 let limits = RequestLimits {
971 max_uri_length: 10,
972 max_headers: 2,
973 max_body_size: 50,
974 ..Default::default()
975 };
976
977 let mut headers = HashMap::new();
978 headers.insert("A".to_string(), "1".to_string());
979
980 assert!(limits.validate_all("/short", &headers, 10).is_ok());
982
983 let result = limits.validate_all("/this/is/a/very/long/uri", &headers, 10);
985 assert!(result.is_err());
986 assert_eq!(result.unwrap_err().len(), 1);
987
988 let many_headers: HashMap<String, String> = (0..5)
990 .map(|i| (format!("H{}", i), "v".to_string()))
991 .collect();
992 let result = limits.validate_all("/this/is/a/very/long/uri", &many_headers, 100);
993 assert!(result.is_err());
994 assert_eq!(result.unwrap_err().len(), 3); }
996
997 #[test]
1002 fn validate_asyncapi_message_payload() {
1003 use barbacane_compiler::ContentSchema;
1005 use std::collections::BTreeMap;
1006
1007 let message_schema = serde_json::json!({
1009 "type": "object",
1010 "required": ["eventId", "userId", "timestamp"],
1011 "properties": {
1012 "eventId": { "type": "string", "format": "uuid" },
1013 "userId": { "type": "string" },
1014 "timestamp": { "type": "string", "format": "date-time" },
1015 "metadata": {
1016 "type": "object",
1017 "additionalProperties": true
1018 }
1019 }
1020 });
1021
1022 let mut content = BTreeMap::new();
1023 content.insert(
1024 "application/json".to_string(),
1025 ContentSchema {
1026 schema: Some(message_schema),
1027 },
1028 );
1029
1030 let request_body = RequestBody {
1031 required: true,
1032 content,
1033 };
1034
1035 let validator = OperationValidator::new(&[], Some(&request_body));
1036
1037 let valid_payload = br#"{
1039 "eventId": "550e8400-e29b-41d4-a716-446655440000",
1040 "userId": "user-123",
1041 "timestamp": "2024-01-29T12:30:00Z"
1042 }"#;
1043 let result = validator.validate_body(Some("application/json"), valid_payload);
1044 assert!(result.is_ok(), "Valid message should pass: {:?}", result);
1045
1046 let missing_field = br#"{
1048 "eventId": "550e8400-e29b-41d4-a716-446655440000",
1049 "userId": "user-123"
1050 }"#;
1051 let result = validator.validate_body(Some("application/json"), missing_field);
1052 assert!(result.is_err(), "Missing timestamp should fail");
1053
1054 let wrong_format = br#"{
1056 "eventId": "not-a-uuid",
1057 "userId": "user-123",
1058 "timestamp": "2024-01-29T12:30:00Z"
1059 }"#;
1060 let result = validator.validate_body(Some("application/json"), wrong_format);
1061 assert!(result.is_err(), "Invalid UUID format should fail");
1062 }
1063
1064 #[test]
1065 fn validate_asyncapi_message_with_avro_content_type() {
1066 use barbacane_compiler::ContentSchema;
1068 use std::collections::BTreeMap;
1069
1070 let message_schema = serde_json::json!({
1071 "type": "object",
1072 "required": ["key"],
1073 "properties": {
1074 "key": { "type": "string" }
1075 }
1076 });
1077
1078 let mut content = BTreeMap::new();
1079 content.insert(
1080 "application/vnd.apache.avro+json".to_string(),
1081 ContentSchema {
1082 schema: Some(message_schema),
1083 },
1084 );
1085
1086 let request_body = RequestBody {
1087 required: true,
1088 content,
1089 };
1090
1091 let validator = OperationValidator::new(&[], Some(&request_body));
1092
1093 let result = validator.validate_body(
1095 Some("application/vnd.apache.avro+json"),
1096 br#"{"key": "value"}"#,
1097 );
1098 assert!(result.is_ok());
1099
1100 let result =
1102 validator.validate_body(Some("application/octet-stream"), br#"{"key": "value"}"#);
1103 assert!(result.is_err());
1104 }
1105
1106 #[test]
1111 fn validate_required_header_param() {
1112 let params = vec![make_param("X-Request-Id", "header", true, None)];
1113 let validator = OperationValidator::new(¶ms, None);
1114
1115 let headers = HashMap::new();
1117 let result = validator.validate_headers(&headers);
1118 assert!(result.is_err());
1119 let errors = result.unwrap_err();
1120 assert!(matches!(
1121 &errors[0],
1122 ValidationError2::MissingRequiredParameter { name, location }
1123 if name == "X-Request-Id" && location == "header"
1124 ));
1125
1126 let mut headers = HashMap::new();
1128 headers.insert("X-Request-Id".to_string(), "abc-123".to_string());
1129 let result = validator.validate_headers(&headers);
1130 assert!(result.is_ok());
1131 }
1132
1133 #[test]
1134 fn validate_optional_header_param() {
1135 let params = vec![make_param("X-Trace-Id", "header", false, None)];
1136 let validator = OperationValidator::new(¶ms, None);
1137
1138 let headers = HashMap::new();
1140 let result = validator.validate_headers(&headers);
1141 assert!(result.is_ok());
1142 }
1143
1144 #[test]
1145 fn validate_header_param_schema() {
1146 let schema = serde_json::json!({
1147 "type": "string",
1148 "pattern": "^Bearer .+$"
1149 });
1150 let params = vec![make_param("Authorization", "header", true, Some(schema))];
1151 let validator = OperationValidator::new(¶ms, None);
1152
1153 let mut headers = HashMap::new();
1155 headers.insert("Authorization".to_string(), "Bearer token123".to_string());
1156 let result = validator.validate_headers(&headers);
1157 assert!(result.is_ok());
1158
1159 headers.insert(
1161 "Authorization".to_string(),
1162 "Basic dXNlcjpwYXNz".to_string(),
1163 );
1164 let result = validator.validate_headers(&headers);
1165 assert!(result.is_err());
1166 let errors = result.unwrap_err();
1167 assert!(matches!(
1168 &errors[0],
1169 ValidationError2::InvalidParameter { name, location, .. }
1170 if name == "Authorization" && location == "header"
1171 ));
1172 }
1173
1174 #[test]
1175 fn validate_header_param_case_insensitive() {
1176 let params = vec![make_param("X-Request-Id", "header", true, None)];
1177 let validator = OperationValidator::new(¶ms, None);
1178
1179 let mut headers = HashMap::new();
1181 headers.insert("x-request-id".to_string(), "abc-123".to_string());
1182 let result = validator.validate_headers(&headers);
1183 assert!(result.is_ok());
1184
1185 let mut headers = HashMap::new();
1187 headers.insert("X-REQUEST-ID".to_string(), "abc-123".to_string());
1188 let result = validator.validate_headers(&headers);
1189 assert!(result.is_ok());
1190 }
1191
1192 #[test]
1193 fn validate_asyncapi_channel_parameter() {
1194 let schema = serde_json::json!({
1196 "type": "string",
1197 "pattern": "^user-[a-z0-9]+$"
1198 });
1199
1200 let params = vec![make_param("userId", "path", true, Some(schema))];
1201 let validator = OperationValidator::new(¶ms, None);
1202
1203 let result = validator.validate_path_params(&[("userId".into(), "user-abc123".into())]);
1205 assert!(result.is_ok());
1206
1207 let result = validator.validate_path_params(&[("userId".into(), "invalid".into())]);
1209 assert!(result.is_err());
1210 }
1211}