1use crate::error::Error;
2use openapiv3::{OpenAPI, Operation, Parameter, ReferenceOr, RequestBody, SecurityScheme};
3
4#[derive(Debug, Default)]
6pub struct ValidationResult {
7 pub warnings: Vec<ValidationWarning>,
9 pub errors: Vec<Error>,
11}
12
13impl ValidationResult {
14 #[must_use]
16 pub const fn new() -> Self {
17 Self {
18 warnings: Vec::new(),
19 errors: Vec::new(),
20 }
21 }
22
23 pub fn into_result(self) -> Result<(), Error> {
29 self.errors.into_iter().next().map_or_else(|| Ok(()), Err)
30 }
31
32 #[must_use]
34 pub const fn is_valid(&self) -> bool {
35 self.errors.is_empty()
36 }
37
38 pub fn add_error(&mut self, error: Error) {
40 self.errors.push(error);
41 }
42
43 pub fn add_warning(&mut self, warning: ValidationWarning) {
45 self.warnings.push(warning);
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct ValidationWarning {
52 pub endpoint: UnsupportedEndpoint,
54 pub reason: String,
56}
57
58#[derive(Debug, Clone)]
60pub struct UnsupportedEndpoint {
61 pub path: String,
63 pub method: String,
65 pub content_type: String,
67}
68
69pub struct SpecValidator;
71
72impl SpecValidator {
73 #[must_use]
75 pub const fn new() -> Self {
76 Self
77 }
78
79 fn get_unsupported_content_type_reason(content_type: &str) -> &'static str {
81 match content_type {
82 "multipart/form-data" => "file uploads are not supported",
84 "application/octet-stream" => "binary data uploads are not supported",
85 ct if ct.starts_with("image/") => "image uploads are not supported",
86 "application/pdf" => "PDF uploads are not supported",
87
88 "application/xml" | "text/xml" => "XML content is not supported",
90 "application/x-www-form-urlencoded" => "form-encoded data is not supported",
91 "text/plain" => "plain text content is not supported",
92 "text/csv" => "CSV content is not supported",
93
94 "application/x-ndjson" => "newline-delimited JSON is not supported",
96 "application/graphql" => "GraphQL content is not supported",
97
98 _ => "is not supported",
100 }
101 }
102
103 #[deprecated(
114 since = "0.1.2",
115 note = "Use `validate_with_mode()` instead. This method defaults to strict mode which may not be desired."
116 )]
117 pub fn validate(&self, spec: &OpenAPI) -> Result<(), Error> {
118 self.validate_with_mode(spec, true).into_result()
119 }
120
121 #[must_use]
132 pub fn validate_with_mode(&self, spec: &OpenAPI, strict: bool) -> ValidationResult {
133 let mut result = ValidationResult::new();
134
135 if let Some(components) = &spec.components {
137 for (name, scheme_ref) in &components.security_schemes {
138 match scheme_ref {
139 ReferenceOr::Item(scheme) => {
140 if let Err(e) = Self::validate_security_scheme(name, scheme) {
141 result.add_error(e);
142 }
143 }
144 ReferenceOr::Reference { .. } => {
145 result.add_error(Error::Validation(format!(
146 "Security scheme references are not supported: '{name}'"
147 )));
148 }
149 }
150 }
151 }
152
153 for (path, path_item_ref) in spec.paths.iter() {
155 if let ReferenceOr::Item(path_item) = path_item_ref {
156 for (method, operation_opt) in crate::spec::http_methods_iter(path_item) {
157 if let Some(operation) = operation_opt {
158 Self::validate_operation(
159 path,
160 &method.to_lowercase(),
161 operation,
162 &mut result,
163 strict,
164 );
165 }
166 }
167 }
168 }
169
170 result
171 }
172
173 fn validate_security_scheme(name: &str, scheme: &SecurityScheme) -> Result<(), Error> {
175 match scheme {
177 SecurityScheme::APIKey { .. } => {
178 }
180 SecurityScheme::HTTP {
181 scheme: http_scheme,
182 ..
183 } => {
184 if http_scheme != "bearer" && http_scheme != "basic" {
185 return Err(Error::Validation(format!(
186 "Unsupported HTTP scheme '{http_scheme}' in security scheme '{name}'. Only 'bearer' and 'basic' are supported."
187 )));
188 }
189 }
190 SecurityScheme::OAuth2 { .. } => {
191 return Err(Error::Validation(format!(
192 "OAuth2 security scheme '{name}' is not supported in v1.0."
193 )));
194 }
195 SecurityScheme::OpenIDConnect { .. } => {
196 return Err(Error::Validation(format!(
197 "OpenID Connect security scheme '{name}' is not supported in v1.0."
198 )));
199 }
200 }
201
202 let (SecurityScheme::APIKey { extensions, .. } | SecurityScheme::HTTP { extensions, .. }) =
204 scheme
205 else {
206 return Ok(());
207 };
208
209 if let Some(aperture_secret) = extensions.get("x-aperture-secret") {
210 let secret_obj = aperture_secret.as_object().ok_or_else(|| {
212 Error::Validation(format!(
213 "Invalid x-aperture-secret in security scheme '{name}': must be an object"
214 ))
215 })?;
216
217 let source = secret_obj
219 .get("source")
220 .ok_or_else(|| {
221 Error::Validation(format!(
222 "Missing 'source' field in x-aperture-secret for security scheme '{name}'"
223 ))
224 })?
225 .as_str()
226 .ok_or_else(|| {
227 Error::Validation(format!(
228 "Invalid 'source' field in x-aperture-secret for security scheme '{name}': must be a string"
229 ))
230 })?;
231
232 if source != "env" {
234 return Err(Error::Validation(format!(
235 "Unsupported source '{source}' in x-aperture-secret for security scheme '{name}'. Only 'env' is supported."
236 )));
237 }
238
239 let env_name = secret_obj
241 .get("name")
242 .ok_or_else(|| {
243 Error::Validation(format!(
244 "Missing 'name' field in x-aperture-secret for security scheme '{name}'"
245 ))
246 })?
247 .as_str()
248 .ok_or_else(|| {
249 Error::Validation(format!(
250 "Invalid 'name' field in x-aperture-secret for security scheme '{name}': must be a string"
251 ))
252 })?;
253
254 if env_name.is_empty() {
256 return Err(Error::Validation(format!(
257 "Empty 'name' field in x-aperture-secret for security scheme '{name}'"
258 )));
259 }
260
261 if !env_name.chars().all(|c| c.is_alphanumeric() || c == '_')
263 || env_name.chars().next().is_some_and(char::is_numeric)
264 {
265 return Err(Error::Validation(format!(
266 "Invalid environment variable name '{env_name}' in x-aperture-secret for security scheme '{name}'. Must contain only alphanumeric characters and underscores, and not start with a digit."
267 )));
268 }
269 }
270
271 Ok(())
272 }
273
274 fn validate_operation(
276 path: &str,
277 method: &str,
278 operation: &Operation,
279 result: &mut ValidationResult,
280 strict: bool,
281 ) {
282 for param_ref in &operation.parameters {
284 match param_ref {
285 ReferenceOr::Item(param) => {
286 if let Err(e) = Self::validate_parameter(path, method, param) {
287 result.add_error(e);
288 }
289 }
290 ReferenceOr::Reference { .. } => {
291 }
293 }
294 }
295
296 if let Some(request_body_ref) = &operation.request_body {
298 match request_body_ref {
299 ReferenceOr::Item(request_body) => {
300 Self::validate_request_body(path, method, request_body, result, strict);
301 }
302 ReferenceOr::Reference { .. } => {
303 result.add_error(Error::Validation(format!(
304 "Request body references are not supported in {method} {path}."
305 )));
306 }
307 }
308 }
309 }
310
311 fn validate_parameter(path: &str, method: &str, param: &Parameter) -> Result<(), Error> {
313 let param_data = match param {
314 Parameter::Query { parameter_data, .. }
315 | Parameter::Header { parameter_data, .. }
316 | Parameter::Path { parameter_data, .. }
317 | Parameter::Cookie { parameter_data, .. } => parameter_data,
318 };
319
320 match ¶m_data.format {
321 openapiv3::ParameterSchemaOrContent::Schema(_) => Ok(()),
322 openapiv3::ParameterSchemaOrContent::Content(_) => {
323 Err(Error::Validation(format!(
324 "Parameter '{}' in {method} {path} uses unsupported content-based serialization. Only schema-based parameters are supported.",
325 param_data.name
326 )))
327 }
328 }
329 }
330
331 fn is_json_content_type(content_type: &str) -> bool {
333 let base_type = content_type
335 .split(';')
336 .next()
337 .unwrap_or(content_type)
338 .trim();
339
340 base_type.eq_ignore_ascii_case("application/json")
342 || base_type.to_lowercase().ends_with("+json")
343 }
344
345 fn validate_request_body(
347 path: &str,
348 method: &str,
349 request_body: &RequestBody,
350 result: &mut ValidationResult,
351 strict: bool,
352 ) {
353 let (has_json, unsupported_types) = Self::categorize_content_types(request_body);
354
355 if unsupported_types.is_empty() {
356 return;
357 }
358
359 if strict {
360 Self::add_strict_mode_errors(path, method, &unsupported_types, result);
361 } else {
362 Self::add_non_strict_warning(path, method, has_json, &unsupported_types, result);
363 }
364 }
365
366 fn categorize_content_types(request_body: &RequestBody) -> (bool, Vec<&String>) {
368 let mut has_json = false;
369 let mut unsupported_types = Vec::new();
370
371 for content_type in request_body.content.keys() {
372 if Self::is_json_content_type(content_type) {
373 has_json = true;
374 } else {
375 unsupported_types.push(content_type);
376 }
377 }
378
379 (has_json, unsupported_types)
380 }
381
382 fn add_strict_mode_errors(
384 path: &str,
385 method: &str,
386 unsupported_types: &[&String],
387 result: &mut ValidationResult,
388 ) {
389 for content_type in unsupported_types {
390 let error = Error::Validation(format!(
391 "Unsupported request body content type '{content_type}' in {method} {path}. Only 'application/json' is supported in v1.0."
392 ));
393 result.add_error(error);
394 }
395 }
396
397 fn add_non_strict_warning(
399 path: &str,
400 method: &str,
401 has_json: bool,
402 unsupported_types: &[&String],
403 result: &mut ValidationResult,
404 ) {
405 let content_types: Vec<String> = unsupported_types
406 .iter()
407 .map(|ct| {
408 let reason = Self::get_unsupported_content_type_reason(ct);
409 format!("{ct} ({reason})")
410 })
411 .collect();
412
413 let reason = if has_json {
414 "endpoint has unsupported content types alongside JSON"
415 } else {
416 "endpoint has no supported content types"
417 };
418
419 let warning = ValidationWarning {
420 endpoint: UnsupportedEndpoint {
421 path: path.to_string(),
422 method: method.to_uppercase(),
423 content_type: content_types.join(", "),
424 },
425 reason: reason.to_string(),
426 };
427
428 result.add_warning(warning);
429 }
430}
431
432impl Default for SpecValidator {
433 fn default() -> Self {
434 Self::new()
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use openapiv3::{Components, Info, OpenAPI};
442
443 fn create_test_spec() -> OpenAPI {
444 OpenAPI {
445 openapi: "3.0.0".to_string(),
446 info: Info {
447 title: "Test API".to_string(),
448 version: "1.0.0".to_string(),
449 ..Default::default()
450 },
451 ..Default::default()
452 }
453 }
454
455 #[test]
456 fn test_validate_empty_spec() {
457 let validator = SpecValidator::new();
458 let spec = create_test_spec();
459 assert!(validator
460 .validate_with_mode(&spec, true)
461 .into_result()
462 .is_ok());
463 }
464
465 #[test]
466 fn test_validate_oauth2_scheme_rejected() {
467 let validator = SpecValidator::new();
468 let mut spec = create_test_spec();
469 let mut components = Components::default();
470 components.security_schemes.insert(
471 "oauth".to_string(),
472 ReferenceOr::Item(SecurityScheme::OAuth2 {
473 flows: Default::default(),
474 description: None,
475 extensions: Default::default(),
476 }),
477 );
478 spec.components = Some(components);
479
480 let result = validator.validate_with_mode(&spec, true).into_result();
481 assert!(result.is_err());
482 match result.unwrap_err() {
483 Error::Validation(msg) => {
484 assert!(msg.contains("OAuth2"));
485 assert!(msg.contains("not supported"));
486 }
487 _ => panic!("Expected Validation error"),
488 }
489 }
490
491 #[test]
492 fn test_validate_reference_rejected() {
493 let validator = SpecValidator::new();
494 let mut spec = create_test_spec();
495 let mut components = Components::default();
496 components.security_schemes.insert(
497 "auth".to_string(),
498 ReferenceOr::Reference {
499 reference: "#/components/securitySchemes/BasicAuth".to_string(),
500 },
501 );
502 spec.components = Some(components);
503
504 let result = validator.validate_with_mode(&spec, true).into_result();
505 assert!(result.is_err());
506 match result.unwrap_err() {
507 Error::Validation(msg) => {
508 assert!(msg.contains("references are not supported"));
509 }
510 _ => panic!("Expected Validation error"),
511 }
512 }
513
514 #[test]
515 fn test_validate_supported_schemes() {
516 let validator = SpecValidator::new();
517 let mut spec = create_test_spec();
518 let mut components = Components::default();
519
520 components.security_schemes.insert(
522 "apiKey".to_string(),
523 ReferenceOr::Item(SecurityScheme::APIKey {
524 location: openapiv3::APIKeyLocation::Header,
525 name: "X-API-Key".to_string(),
526 description: None,
527 extensions: Default::default(),
528 }),
529 );
530
531 components.security_schemes.insert(
533 "bearer".to_string(),
534 ReferenceOr::Item(SecurityScheme::HTTP {
535 scheme: "bearer".to_string(),
536 bearer_format: Some("JWT".to_string()),
537 description: None,
538 extensions: Default::default(),
539 }),
540 );
541
542 components.security_schemes.insert(
544 "basic".to_string(),
545 ReferenceOr::Item(SecurityScheme::HTTP {
546 scheme: "basic".to_string(),
547 bearer_format: None,
548 description: None,
549 extensions: Default::default(),
550 }),
551 );
552
553 spec.components = Some(components);
554
555 assert!(validator
556 .validate_with_mode(&spec, true)
557 .into_result()
558 .is_ok());
559 }
560
561 #[test]
562 fn test_validate_with_mode_non_strict_mixed_content() {
563 use openapiv3::{
564 MediaType, Operation, PathItem, ReferenceOr as PathRef, RequestBody, Responses,
565 };
566
567 let validator = SpecValidator::new();
568 let mut spec = create_test_spec();
569
570 let mut request_body = RequestBody::default();
572 request_body
573 .content
574 .insert("multipart/form-data".to_string(), MediaType::default());
575 request_body
576 .content
577 .insert("application/json".to_string(), MediaType::default());
578 request_body.required = true;
579
580 let mut path_item = PathItem::default();
581 path_item.post = Some(Operation {
582 operation_id: Some("uploadFile".to_string()),
583 tags: vec!["files".to_string()],
584 request_body: Some(ReferenceOr::Item(request_body)),
585 responses: Responses::default(),
586 ..Default::default()
587 });
588
589 spec.paths
590 .paths
591 .insert("/upload".to_string(), PathRef::Item(path_item));
592
593 let result = validator.validate_with_mode(&spec, false);
595 assert!(result.is_valid(), "Non-strict mode should be valid");
596 assert_eq!(
597 result.warnings.len(),
598 1,
599 "Should have one warning for mixed content types"
600 );
601 assert_eq!(result.errors.len(), 0, "Should have no errors");
602
603 let warning = &result.warnings[0];
605 assert_eq!(warning.endpoint.path, "/upload");
606 assert_eq!(warning.endpoint.method, "POST");
607 assert!(warning
608 .endpoint
609 .content_type
610 .contains("multipart/form-data"));
611 assert!(warning
612 .reason
613 .contains("unsupported content types alongside JSON"));
614 }
615
616 #[test]
617 fn test_validate_with_mode_non_strict_only_unsupported() {
618 use openapiv3::{
619 MediaType, Operation, PathItem, ReferenceOr as PathRef, RequestBody, Responses,
620 };
621
622 let validator = SpecValidator::new();
623 let mut spec = create_test_spec();
624
625 let mut request_body = RequestBody::default();
627 request_body
628 .content
629 .insert("multipart/form-data".to_string(), MediaType::default());
630 request_body.required = true;
631
632 let mut path_item = PathItem::default();
633 path_item.post = Some(Operation {
634 operation_id: Some("uploadFile".to_string()),
635 tags: vec!["files".to_string()],
636 request_body: Some(ReferenceOr::Item(request_body)),
637 responses: Responses::default(),
638 ..Default::default()
639 });
640
641 spec.paths
642 .paths
643 .insert("/upload".to_string(), PathRef::Item(path_item));
644
645 let result = validator.validate_with_mode(&spec, false);
647 assert!(result.is_valid(), "Non-strict mode should be valid");
648 assert_eq!(result.warnings.len(), 1, "Should have one warning");
649 assert_eq!(result.errors.len(), 0, "Should have no errors");
650
651 let warning = &result.warnings[0];
652 assert_eq!(warning.endpoint.path, "/upload");
653 assert_eq!(warning.endpoint.method, "POST");
654 assert!(warning
655 .endpoint
656 .content_type
657 .contains("multipart/form-data"));
658 assert!(warning.reason.contains("no supported content types"));
659 }
660
661 #[test]
662 fn test_validate_with_mode_strict() {
663 use openapiv3::{
664 MediaType, Operation, PathItem, ReferenceOr as PathRef, RequestBody, Responses,
665 };
666
667 let validator = SpecValidator::new();
668 let mut spec = create_test_spec();
669
670 let mut request_body = RequestBody::default();
671 request_body
672 .content
673 .insert("multipart/form-data".to_string(), MediaType::default());
674 request_body.required = true;
675
676 let mut path_item = PathItem::default();
677 path_item.post = Some(Operation {
678 operation_id: Some("uploadFile".to_string()),
679 tags: vec!["files".to_string()],
680 request_body: Some(ReferenceOr::Item(request_body)),
681 responses: Responses::default(),
682 ..Default::default()
683 });
684
685 spec.paths
686 .paths
687 .insert("/upload".to_string(), PathRef::Item(path_item));
688
689 let result = validator.validate_with_mode(&spec, true);
691 assert!(!result.is_valid(), "Strict mode should be invalid");
692 assert_eq!(result.warnings.len(), 0, "Should have no warnings");
693 assert_eq!(result.errors.len(), 1, "Should have one error");
694
695 match &result.errors[0] {
696 Error::Validation(msg) => {
697 assert!(msg.contains("multipart/form-data"));
698 assert!(msg.contains("v1.0"));
699 }
700 _ => panic!("Expected Validation error"),
701 }
702 }
703
704 #[test]
705 fn test_validate_with_mode_multiple_content_types() {
706 use openapiv3::{
707 MediaType, Operation, PathItem, ReferenceOr as PathRef, RequestBody, Responses,
708 };
709
710 let validator = SpecValidator::new();
711 let mut spec = create_test_spec();
712
713 let mut path_item1 = PathItem::default();
715 let mut request_body1 = RequestBody::default();
716 request_body1
717 .content
718 .insert("application/xml".to_string(), MediaType::default());
719 path_item1.post = Some(Operation {
720 operation_id: Some("postXml".to_string()),
721 tags: vec!["data".to_string()],
722 request_body: Some(ReferenceOr::Item(request_body1)),
723 responses: Responses::default(),
724 ..Default::default()
725 });
726 spec.paths
727 .paths
728 .insert("/xml".to_string(), PathRef::Item(path_item1));
729
730 let mut path_item2 = PathItem::default();
731 let mut request_body2 = RequestBody::default();
732 request_body2
733 .content
734 .insert("text/plain".to_string(), MediaType::default());
735 path_item2.put = Some(Operation {
736 operation_id: Some("putText".to_string()),
737 tags: vec!["data".to_string()],
738 request_body: Some(ReferenceOr::Item(request_body2)),
739 responses: Responses::default(),
740 ..Default::default()
741 });
742 spec.paths
743 .paths
744 .insert("/text".to_string(), PathRef::Item(path_item2));
745
746 let result = validator.validate_with_mode(&spec, false);
748 assert!(result.is_valid());
749 assert_eq!(result.warnings.len(), 2);
750
751 let warning_paths: Vec<&str> = result
752 .warnings
753 .iter()
754 .map(|w| w.endpoint.path.as_str())
755 .collect();
756 assert!(warning_paths.contains(&"/xml"));
757 assert!(warning_paths.contains(&"/text"));
758 }
759
760 #[test]
761 fn test_validate_with_mode_multiple_unsupported_types_single_endpoint() {
762 use openapiv3::{
763 MediaType, Operation, PathItem, ReferenceOr as PathRef, RequestBody, Responses,
764 };
765
766 let validator = SpecValidator::new();
767 let mut spec = create_test_spec();
768
769 let mut request_body = RequestBody::default();
771 request_body
772 .content
773 .insert("multipart/form-data".to_string(), MediaType::default());
774 request_body
775 .content
776 .insert("application/xml".to_string(), MediaType::default());
777 request_body
778 .content
779 .insert("text/plain".to_string(), MediaType::default());
780 request_body.required = true;
781
782 let mut path_item = PathItem::default();
783 path_item.post = Some(Operation {
784 operation_id: Some("uploadData".to_string()),
785 tags: vec!["data".to_string()],
786 request_body: Some(ReferenceOr::Item(request_body)),
787 responses: Responses::default(),
788 ..Default::default()
789 });
790
791 spec.paths
792 .paths
793 .insert("/data".to_string(), PathRef::Item(path_item));
794
795 let result = validator.validate_with_mode(&spec, false);
797 assert!(result.is_valid(), "Non-strict mode should be valid");
798 assert_eq!(result.warnings.len(), 1, "Should have exactly one warning");
799 assert_eq!(result.errors.len(), 0, "Should have no errors");
800
801 let warning = &result.warnings[0];
802 assert_eq!(warning.endpoint.path, "/data");
803 assert_eq!(warning.endpoint.method, "POST");
804 assert!(warning
806 .endpoint
807 .content_type
808 .contains("multipart/form-data"));
809 assert!(warning.endpoint.content_type.contains("application/xml"));
810 assert!(warning.endpoint.content_type.contains("text/plain"));
811 assert!(warning.reason.contains("no supported content types"));
812 }
813
814 #[test]
815 fn test_validate_unsupported_http_scheme() {
816 let validator = SpecValidator::new();
817 let mut spec = create_test_spec();
818 let mut components = Components::default();
819
820 components.security_schemes.insert(
821 "digest".to_string(),
822 ReferenceOr::Item(SecurityScheme::HTTP {
823 scheme: "digest".to_string(),
824 bearer_format: None,
825 description: None,
826 extensions: Default::default(),
827 }),
828 );
829
830 spec.components = Some(components);
831
832 let result = validator.validate_with_mode(&spec, true).into_result();
833 assert!(result.is_err());
834 match result.unwrap_err() {
835 Error::Validation(msg) => {
836 assert!(msg.contains("Unsupported HTTP scheme 'digest'"));
837 }
838 _ => panic!("Expected Validation error"),
839 }
840 }
841
842 #[test]
843 fn test_validate_parameter_reference_allowed() {
844 use openapiv3::{Operation, PathItem, ReferenceOr as PathRef, Responses};
845
846 let validator = SpecValidator::new();
847 let mut spec = create_test_spec();
848
849 let mut path_item = PathItem::default();
850 path_item.get = Some(Operation {
851 parameters: vec![ReferenceOr::Reference {
852 reference: "#/components/parameters/UserId".to_string(),
853 }],
854 responses: Responses::default(),
855 ..Default::default()
856 });
857
858 spec.paths
859 .paths
860 .insert("/users/{id}".to_string(), PathRef::Item(path_item));
861
862 let result = validator.validate_with_mode(&spec, true).into_result();
864 assert!(result.is_ok());
865 }
866
867 #[test]
868 fn test_validate_request_body_non_json_rejected() {
869 use openapiv3::{
870 MediaType, Operation, PathItem, ReferenceOr as PathRef, RequestBody, Responses,
871 };
872
873 let validator = SpecValidator::new();
874 let mut spec = create_test_spec();
875
876 let mut request_body = RequestBody::default();
877 request_body
878 .content
879 .insert("application/xml".to_string(), MediaType::default());
880 request_body.required = true;
881
882 let mut path_item = PathItem::default();
883 path_item.post = Some(Operation {
884 request_body: Some(ReferenceOr::Item(request_body)),
885 responses: Responses::default(),
886 ..Default::default()
887 });
888
889 spec.paths
890 .paths
891 .insert("/users".to_string(), PathRef::Item(path_item));
892
893 let result = validator.validate_with_mode(&spec, true).into_result();
894 assert!(result.is_err());
895 match result.unwrap_err() {
896 Error::Validation(msg) => {
897 assert!(msg.contains("Unsupported request body content type 'application/xml'"));
898 }
899 _ => panic!("Expected Validation error"),
900 }
901 }
902
903 #[test]
904 fn test_validate_x_aperture_secret_valid() {
905 let validator = SpecValidator::new();
906 let mut spec = create_test_spec();
907 let mut components = Components::default();
908
909 let mut extensions = serde_json::Map::new();
911 extensions.insert(
912 "x-aperture-secret".to_string(),
913 serde_json::json!({
914 "source": "env",
915 "name": "API_TOKEN"
916 }),
917 );
918
919 components.security_schemes.insert(
920 "bearerAuth".to_string(),
921 ReferenceOr::Item(SecurityScheme::HTTP {
922 scheme: "bearer".to_string(),
923 bearer_format: None,
924 description: None,
925 extensions: extensions.into_iter().collect(),
926 }),
927 );
928 spec.components = Some(components);
929
930 assert!(validator
931 .validate_with_mode(&spec, true)
932 .into_result()
933 .is_ok());
934 }
935
936 #[test]
937 fn test_validate_x_aperture_secret_missing_source() {
938 let validator = SpecValidator::new();
939 let mut spec = create_test_spec();
940 let mut components = Components::default();
941
942 let mut extensions = serde_json::Map::new();
944 extensions.insert(
945 "x-aperture-secret".to_string(),
946 serde_json::json!({
947 "name": "API_TOKEN"
948 }),
949 );
950
951 components.security_schemes.insert(
952 "bearerAuth".to_string(),
953 ReferenceOr::Item(SecurityScheme::HTTP {
954 scheme: "bearer".to_string(),
955 bearer_format: None,
956 description: None,
957 extensions: extensions.into_iter().collect(),
958 }),
959 );
960 spec.components = Some(components);
961
962 let result = validator.validate_with_mode(&spec, true).into_result();
963 assert!(result.is_err());
964 match result.unwrap_err() {
965 Error::Validation(msg) => {
966 assert!(msg.contains("Missing 'source' field"));
967 }
968 _ => panic!("Expected Validation error"),
969 }
970 }
971
972 #[test]
973 fn test_validate_x_aperture_secret_missing_name() {
974 let validator = SpecValidator::new();
975 let mut spec = create_test_spec();
976 let mut components = Components::default();
977
978 let mut extensions = serde_json::Map::new();
980 extensions.insert(
981 "x-aperture-secret".to_string(),
982 serde_json::json!({
983 "source": "env"
984 }),
985 );
986
987 components.security_schemes.insert(
988 "bearerAuth".to_string(),
989 ReferenceOr::Item(SecurityScheme::HTTP {
990 scheme: "bearer".to_string(),
991 bearer_format: None,
992 description: None,
993 extensions: extensions.into_iter().collect(),
994 }),
995 );
996 spec.components = Some(components);
997
998 let result = validator.validate_with_mode(&spec, true).into_result();
999 assert!(result.is_err());
1000 match result.unwrap_err() {
1001 Error::Validation(msg) => {
1002 assert!(msg.contains("Missing 'name' field"));
1003 }
1004 _ => panic!("Expected Validation error"),
1005 }
1006 }
1007
1008 #[test]
1009 fn test_validate_x_aperture_secret_invalid_env_name() {
1010 let validator = SpecValidator::new();
1011 let mut spec = create_test_spec();
1012 let mut components = Components::default();
1013
1014 let mut extensions = serde_json::Map::new();
1016 extensions.insert(
1017 "x-aperture-secret".to_string(),
1018 serde_json::json!({
1019 "source": "env",
1020 "name": "123_INVALID" }),
1022 );
1023
1024 components.security_schemes.insert(
1025 "bearerAuth".to_string(),
1026 ReferenceOr::Item(SecurityScheme::HTTP {
1027 scheme: "bearer".to_string(),
1028 bearer_format: None,
1029 description: None,
1030 extensions: extensions.into_iter().collect(),
1031 }),
1032 );
1033 spec.components = Some(components);
1034
1035 let result = validator.validate_with_mode(&spec, true).into_result();
1036 assert!(result.is_err());
1037 match result.unwrap_err() {
1038 Error::Validation(msg) => {
1039 assert!(msg.contains("Invalid environment variable name"));
1040 }
1041 _ => panic!("Expected Validation error"),
1042 }
1043 }
1044
1045 #[test]
1046 fn test_validate_x_aperture_secret_unsupported_source() {
1047 let validator = SpecValidator::new();
1048 let mut spec = create_test_spec();
1049 let mut components = Components::default();
1050
1051 let mut extensions = serde_json::Map::new();
1053 extensions.insert(
1054 "x-aperture-secret".to_string(),
1055 serde_json::json!({
1056 "source": "file", "name": "API_TOKEN"
1058 }),
1059 );
1060
1061 components.security_schemes.insert(
1062 "bearerAuth".to_string(),
1063 ReferenceOr::Item(SecurityScheme::HTTP {
1064 scheme: "bearer".to_string(),
1065 bearer_format: None,
1066 description: None,
1067 extensions: extensions.into_iter().collect(),
1068 }),
1069 );
1070 spec.components = Some(components);
1071
1072 let result = validator.validate_with_mode(&spec, true).into_result();
1073 assert!(result.is_err());
1074 match result.unwrap_err() {
1075 Error::Validation(msg) => {
1076 assert!(msg.contains("Unsupported source 'file'"));
1077 }
1078 _ => panic!("Expected Validation error"),
1079 }
1080 }
1081}