use crate::constants;
#[allow(unused_imports)]
use crate::error::{Error, ErrorKind};
use openapiv3::{OpenAPI, Operation, Parameter, ReferenceOr, RequestBody, SecurityScheme};
use std::collections::HashMap;
#[derive(Debug, Default)]
pub struct ValidationResult {
pub warnings: Vec<ValidationWarning>,
pub errors: Vec<Error>,
}
impl ValidationResult {
#[must_use]
pub const fn new() -> Self {
Self {
warnings: Vec::new(),
errors: Vec::new(),
}
}
pub fn into_result(self) -> Result<(), Error> {
self.errors.into_iter().next().map_or_else(|| Ok(()), Err)
}
#[must_use]
pub const fn is_valid(&self) -> bool {
self.errors.is_empty()
}
pub fn add_error(&mut self, error: Error) {
self.errors.push(error);
}
pub fn add_warning(&mut self, warning: ValidationWarning) {
self.warnings.push(warning);
}
}
#[derive(Debug, Clone)]
pub struct ValidationWarning {
pub endpoint: UnsupportedEndpoint,
pub reason: String,
}
impl ValidationWarning {
#[must_use]
pub fn should_skip_endpoint(&self) -> bool {
self.reason.contains("no supported content types")
|| self.reason.contains("unsupported authentication")
}
#[must_use]
pub fn to_skip_endpoint(&self) -> Option<(String, String)> {
if self.should_skip_endpoint() {
Some((self.endpoint.path.clone(), self.endpoint.method.clone()))
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct UnsupportedEndpoint {
pub path: String,
pub method: String,
pub content_type: String,
}
pub struct SpecValidator;
impl SpecValidator {
#[must_use]
pub const fn new() -> Self {
Self
}
fn get_unsupported_content_type_reason(content_type: &str) -> &'static str {
match content_type {
constants::CONTENT_TYPE_MULTIPART => "file uploads are not supported",
constants::CONTENT_TYPE_OCTET_STREAM => "binary data uploads are not supported",
ct if ct.starts_with(constants::CONTENT_TYPE_PREFIX_IMAGE) => {
"image uploads are not supported"
}
constants::CONTENT_TYPE_PDF => "PDF uploads are not supported",
constants::CONTENT_TYPE_XML | constants::CONTENT_TYPE_TEXT_XML => {
"XML content is not supported"
}
constants::CONTENT_TYPE_FORM => "form-encoded data is not supported",
constants::CONTENT_TYPE_TEXT => "plain text content is not supported",
constants::CONTENT_TYPE_CSV => "CSV content is not supported",
constants::CONTENT_TYPE_NDJSON => "newline-delimited JSON is not supported",
constants::CONTENT_TYPE_GRAPHQL => "GraphQL content is not supported",
_ => "is not supported",
}
}
#[deprecated(
since = "0.1.2",
note = "Use `validate_with_mode()` instead. This method defaults to strict mode which may not be desired."
)]
pub fn validate(&self, spec: &OpenAPI) -> Result<(), Error> {
self.validate_with_mode(spec, true).into_result()
}
#[must_use]
pub fn validate_with_mode(&self, spec: &OpenAPI, strict: bool) -> ValidationResult {
let mut result = ValidationResult::new();
let mut unsupported_schemes = HashMap::new();
if let Some(components) = &spec.components {
for (name, scheme_ref) in &components.security_schemes {
let ReferenceOr::Item(scheme) = scheme_ref else {
result.add_error(Error::validation_error(format!(
"Security scheme references are not supported: '{name}'"
)));
continue;
};
let Err(e) = Self::validate_security_scheme(name, scheme) else {
continue;
};
Self::handle_security_scheme_error(
e,
strict,
name,
&mut result,
&mut unsupported_schemes,
);
}
}
for (path, path_item_ref) in spec.paths.iter() {
let ReferenceOr::Item(path_item) = path_item_ref else {
continue;
};
for (method, operation_opt) in crate::spec::http_methods_iter(path_item) {
let Some(operation) = operation_opt else {
continue;
};
Self::validate_operation(
path,
&method.to_lowercase(),
operation,
&mut result,
strict,
&unsupported_schemes,
spec,
);
}
}
result
}
fn validate_security_scheme(
name: &str,
scheme: &SecurityScheme,
) -> Result<Option<String>, Error> {
let unsupported_reason = match scheme {
SecurityScheme::APIKey { .. } => None, SecurityScheme::HTTP {
scheme: http_scheme,
..
} => {
let unsupported_complex_schemes = ["negotiate", "oauth", "oauth2", "openidconnect"];
if unsupported_complex_schemes.contains(&http_scheme.to_lowercase().as_str()) {
Some(format!(
"HTTP scheme '{http_scheme}' requires complex authentication flows"
))
} else {
None }
}
SecurityScheme::OAuth2 { .. } => {
Some("OAuth2 authentication is not supported".to_string())
}
SecurityScheme::OpenIDConnect { .. } => {
Some("OpenID Connect authentication is not supported".to_string())
}
};
if let Some(reason) = unsupported_reason {
return Err(Error::validation_error(format!(
"Security scheme '{name}' uses unsupported authentication: {reason}"
)));
}
let (SecurityScheme::APIKey { extensions, .. } | SecurityScheme::HTTP { extensions, .. }) =
scheme
else {
return Ok(None);
};
let Some(aperture_secret) = extensions.get(crate::constants::EXT_APERTURE_SECRET) else {
return Ok(None);
};
let secret_obj = aperture_secret.as_object().ok_or_else(|| {
Error::validation_error(format!(
"Invalid x-aperture-secret in security scheme '{name}': must be an object"
))
})?;
let source = secret_obj
.get(crate::constants::EXT_KEY_SOURCE)
.ok_or_else(|| {
Error::validation_error(format!(
"Missing 'source' field in x-aperture-secret for security scheme '{name}'"
))
})?
.as_str()
.ok_or_else(|| {
Error::validation_error(format!(
"Invalid 'source' field in x-aperture-secret for security scheme '{name}': must be a string"
))
})?;
if source != crate::constants::SOURCE_ENV {
return Err(Error::validation_error(format!(
"Unsupported source '{source}' in x-aperture-secret for security scheme '{name}'. Only 'env' is supported."
)));
}
let env_name = secret_obj
.get(crate::constants::EXT_KEY_NAME)
.ok_or_else(|| {
Error::validation_error(format!(
"Missing 'name' field in x-aperture-secret for security scheme '{name}'"
))
})?
.as_str()
.ok_or_else(|| {
Error::validation_error(format!(
"Invalid 'name' field in x-aperture-secret for security scheme '{name}': must be a string"
))
})?;
if env_name.is_empty() {
return Err(Error::validation_error(format!(
"Empty 'name' field in x-aperture-secret for security scheme '{name}'"
)));
}
if !env_name.chars().all(|c| c.is_alphanumeric() || c == '_')
|| env_name.chars().next().is_some_and(char::is_numeric)
{
return Err(Error::validation_error(format!(
"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."
)));
}
Ok(None)
}
fn handle_security_scheme_error(
error: Error,
strict: bool,
scheme_name: &str,
result: &mut ValidationResult,
unsupported_schemes: &mut HashMap<String, String>,
) {
if strict {
result.add_error(error);
return;
}
match error {
Error::Internal {
kind: crate::error::ErrorKind::Validation,
ref message,
..
} if message.contains("unsupported authentication") => {
unsupported_schemes.insert(scheme_name.to_string(), message.to_string());
}
_ => {
result.add_error(error);
}
}
}
fn should_skip_operation_for_auth(
path: &str,
method: &str,
operation: &Operation,
spec: &OpenAPI,
strict: bool,
unsupported_schemes: &HashMap<String, String>,
result: &mut ValidationResult,
) -> bool {
if strict || unsupported_schemes.is_empty() {
return false;
}
let Some(reqs) = operation.security.as_ref().or(spec.security.as_ref()) else {
return false;
};
if reqs.is_empty() {
return false;
}
if !Self::should_skip_due_to_auth(reqs, unsupported_schemes) {
return false;
}
let scheme_details = Self::format_unsupported_scheme_details(reqs, unsupported_schemes);
let reason = Self::format_auth_skip_reason(reqs, &scheme_details);
result.add_warning(ValidationWarning {
endpoint: UnsupportedEndpoint {
path: path.to_string(),
method: method.to_uppercase(),
content_type: String::new(),
},
reason,
});
true
}
fn format_unsupported_scheme_details(
reqs: &[openapiv3::SecurityRequirement],
unsupported_schemes: &HashMap<String, String>,
) -> Vec<String> {
reqs.iter()
.flat_map(|req| req.keys())
.filter_map(|scheme_name| {
unsupported_schemes.get(scheme_name).map(|msg| {
match () {
() if msg.contains("OAuth2") => format!("{scheme_name} (OAuth2)"),
() if msg.contains("OpenID Connect") => {
format!("{scheme_name} (OpenID Connect)")
}
() if msg.contains("complex authentication flows") => {
format!("{scheme_name} (requires complex flow)")
}
() => scheme_name.clone(),
}
})
})
.collect()
}
fn format_auth_skip_reason(
reqs: &[openapiv3::SecurityRequirement],
scheme_details: &[String],
) -> String {
if scheme_details.is_empty() {
format!(
"endpoint requires unsupported authentication schemes: {}",
reqs.iter()
.flat_map(|req| req.keys())
.cloned()
.collect::<Vec<_>>()
.join(", ")
)
} else {
format!(
"endpoint requires unsupported authentication: {}",
scheme_details.join(", ")
)
}
}
fn should_skip_due_to_auth(
security_reqs: &[openapiv3::SecurityRequirement],
unsupported_schemes: &HashMap<String, String>,
) -> bool {
security_reqs.iter().all(|req| {
req.keys()
.all(|scheme| unsupported_schemes.contains_key(scheme))
})
}
fn validate_operation(
path: &str,
method: &str,
operation: &Operation,
result: &mut ValidationResult,
strict: bool,
unsupported_schemes: &HashMap<String, String>,
spec: &OpenAPI,
) {
if Self::should_skip_operation_for_auth(
path,
method,
operation,
spec,
strict,
unsupported_schemes,
result,
) {
return;
}
for param_ref in &operation.parameters {
match param_ref {
ReferenceOr::Item(param) => {
if let Err(e) = Self::validate_parameter(path, method, param) {
result.add_error(e);
}
}
ReferenceOr::Reference { .. } => {
}
}
}
if let Some(request_body_ref) = &operation.request_body {
match request_body_ref {
ReferenceOr::Item(request_body) => {
Self::validate_request_body(path, method, request_body, result, strict);
}
ReferenceOr::Reference { .. } => {
result.add_error(Error::validation_error(format!(
"Request body references are not supported in {method} {path}."
)));
}
}
}
}
fn validate_parameter(path: &str, method: &str, param: &Parameter) -> Result<(), Error> {
let param_data = match param {
Parameter::Query { parameter_data, .. }
| Parameter::Header { parameter_data, .. }
| Parameter::Path { parameter_data, .. }
| Parameter::Cookie { parameter_data, .. } => parameter_data,
};
match ¶m_data.format {
openapiv3::ParameterSchemaOrContent::Schema(_) => Ok(()),
openapiv3::ParameterSchemaOrContent::Content(_) => {
Err(Error::validation_error(format!(
"Parameter '{}' in {method} {path} uses unsupported content-based serialization. Only schema-based parameters are supported.",
param_data.name
)))
}
}
}
fn is_json_content_type(content_type: &str) -> bool {
let base_type = content_type
.split(';')
.next()
.unwrap_or(content_type)
.trim();
base_type.eq_ignore_ascii_case(constants::CONTENT_TYPE_JSON)
|| base_type.to_lowercase().ends_with("+json")
}
fn validate_request_body(
path: &str,
method: &str,
request_body: &RequestBody,
result: &mut ValidationResult,
strict: bool,
) {
let (has_json, unsupported_types) = Self::categorize_content_types(request_body);
if unsupported_types.is_empty() {
return;
}
if strict {
Self::add_strict_mode_errors(path, method, &unsupported_types, result);
} else {
Self::add_non_strict_warning(path, method, has_json, &unsupported_types, result);
}
}
fn categorize_content_types(request_body: &RequestBody) -> (bool, Vec<&String>) {
let mut has_json = false;
let mut unsupported_types = Vec::new();
for content_type in request_body.content.keys() {
if Self::is_json_content_type(content_type) {
has_json = true;
} else {
unsupported_types.push(content_type);
}
}
(has_json, unsupported_types)
}
fn add_strict_mode_errors(
path: &str,
method: &str,
unsupported_types: &[&String],
result: &mut ValidationResult,
) {
for content_type in unsupported_types {
let error = Error::validation_error(format!(
"Unsupported request body content type '{content_type}' in {method} {path}. Only 'application/json' is supported in v1.0."
));
result.add_error(error);
}
}
fn add_non_strict_warning(
path: &str,
method: &str,
has_json: bool,
unsupported_types: &[&String],
result: &mut ValidationResult,
) {
let content_types: Vec<String> = unsupported_types
.iter()
.map(|ct| {
let reason = Self::get_unsupported_content_type_reason(ct);
format!("{ct} ({reason})")
})
.collect();
let reason = if has_json {
"endpoint has unsupported content types alongside JSON"
} else {
"endpoint has no supported content types"
};
let warning = ValidationWarning {
endpoint: UnsupportedEndpoint {
path: path.to_string(),
method: method.to_uppercase(),
content_type: content_types.join(", "),
},
reason: reason.to_string(),
};
result.add_warning(warning);
}
}
impl Default for SpecValidator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::default_trait_access)]
#[allow(clippy::field_reassign_with_default)]
#[allow(clippy::too_many_lines)]
mod tests {
use super::*;
use openapiv3::{
Components, Info, MediaType, OpenAPI, Operation, PathItem, ReferenceOr as PathRef,
RequestBody, Responses,
};
fn create_test_spec() -> OpenAPI {
OpenAPI {
openapi: "3.0.0".to_string(),
info: Info {
title: "Test API".to_string(),
version: "1.0.0".to_string(),
..Default::default()
},
..Default::default()
}
}
#[test]
fn test_validate_empty_spec() {
let validator = SpecValidator::new();
let spec = create_test_spec();
assert!(validator
.validate_with_mode(&spec, true)
.into_result()
.is_ok());
}
#[test]
fn test_validate_oauth2_scheme_rejected() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut components = Components::default();
components.security_schemes.insert(
"oauth".to_string(),
ReferenceOr::Item(SecurityScheme::OAuth2 {
flows: Default::default(),
description: None,
extensions: Default::default(),
}),
);
spec.components = Some(components);
let result = validator.validate_with_mode(&spec, true).into_result();
assert!(result.is_err());
match result.unwrap_err() {
Error::Internal {
kind: ErrorKind::Validation,
message,
..
} => {
assert!(message.contains("OAuth2"));
assert!(message.contains("not supported"));
}
_ => panic!("Expected Validation error"),
}
}
#[test]
fn test_validate_reference_rejected() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut components = Components::default();
components.security_schemes.insert(
"auth".to_string(),
ReferenceOr::Reference {
reference: "#/components/securitySchemes/BasicAuth".to_string(),
},
);
spec.components = Some(components);
let result = validator.validate_with_mode(&spec, true).into_result();
assert!(result.is_err());
match result.unwrap_err() {
Error::Internal {
kind: ErrorKind::Validation,
message: msg,
..
} => {
assert!(msg.contains("references are not supported"));
}
_ => panic!("Expected Validation error"),
}
}
#[test]
fn test_validate_supported_schemes() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut components = Components::default();
components.security_schemes.insert(
constants::AUTH_SCHEME_APIKEY.to_string(),
ReferenceOr::Item(SecurityScheme::APIKey {
location: openapiv3::APIKeyLocation::Header,
name: "X-API-Key".to_string(),
description: None,
extensions: Default::default(),
}),
);
components.security_schemes.insert(
constants::AUTH_SCHEME_BEARER.to_string(),
ReferenceOr::Item(SecurityScheme::HTTP {
scheme: constants::AUTH_SCHEME_BEARER.to_string(),
bearer_format: Some("JWT".to_string()),
description: None,
extensions: Default::default(),
}),
);
components.security_schemes.insert(
constants::AUTH_SCHEME_BASIC.to_string(),
ReferenceOr::Item(SecurityScheme::HTTP {
scheme: constants::AUTH_SCHEME_BASIC.to_string(),
bearer_format: None,
description: None,
extensions: Default::default(),
}),
);
spec.components = Some(components);
assert!(validator
.validate_with_mode(&spec, true)
.into_result()
.is_ok());
}
#[test]
fn test_validate_with_mode_non_strict_mixed_content() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut request_body = RequestBody::default();
request_body
.content
.insert("multipart/form-data".to_string(), MediaType::default());
request_body.content.insert(
constants::CONTENT_TYPE_JSON.to_string(),
MediaType::default(),
);
request_body.required = true;
let mut path_item = PathItem::default();
path_item.post = Some(Operation {
operation_id: Some("uploadFile".to_string()),
tags: vec!["files".to_string()],
request_body: Some(ReferenceOr::Item(request_body)),
responses: Responses::default(),
..Default::default()
});
spec.paths
.paths
.insert("/upload".to_string(), PathRef::Item(path_item));
let result = validator.validate_with_mode(&spec, false);
assert!(result.is_valid(), "Non-strict mode should be valid");
assert_eq!(
result.warnings.len(),
1,
"Should have one warning for mixed content types"
);
assert_eq!(result.errors.len(), 0, "Should have no errors");
let warning = &result.warnings[0];
assert_eq!(warning.endpoint.path, "/upload");
assert_eq!(warning.endpoint.method, constants::HTTP_METHOD_POST);
assert!(warning
.endpoint
.content_type
.contains("multipart/form-data"));
assert!(warning
.reason
.contains("unsupported content types alongside JSON"));
}
#[test]
fn test_validate_with_mode_non_strict_only_unsupported() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut request_body = RequestBody::default();
request_body
.content
.insert("multipart/form-data".to_string(), MediaType::default());
request_body.required = true;
let mut path_item = PathItem::default();
path_item.post = Some(Operation {
operation_id: Some("uploadFile".to_string()),
tags: vec!["files".to_string()],
request_body: Some(ReferenceOr::Item(request_body)),
responses: Responses::default(),
..Default::default()
});
spec.paths
.paths
.insert("/upload".to_string(), PathRef::Item(path_item));
let result = validator.validate_with_mode(&spec, false);
assert!(result.is_valid(), "Non-strict mode should be valid");
assert_eq!(result.warnings.len(), 1, "Should have one warning");
assert_eq!(result.errors.len(), 0, "Should have no errors");
let warning = &result.warnings[0];
assert_eq!(warning.endpoint.path, "/upload");
assert_eq!(warning.endpoint.method, constants::HTTP_METHOD_POST);
assert!(warning
.endpoint
.content_type
.contains("multipart/form-data"));
assert!(warning.reason.contains("no supported content types"));
}
#[test]
fn test_validate_with_mode_strict() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut request_body = RequestBody::default();
request_body
.content
.insert("multipart/form-data".to_string(), MediaType::default());
request_body.required = true;
let mut path_item = PathItem::default();
path_item.post = Some(Operation {
operation_id: Some("uploadFile".to_string()),
tags: vec!["files".to_string()],
request_body: Some(ReferenceOr::Item(request_body)),
responses: Responses::default(),
..Default::default()
});
spec.paths
.paths
.insert("/upload".to_string(), PathRef::Item(path_item));
let result = validator.validate_with_mode(&spec, true);
assert!(!result.is_valid(), "Strict mode should be invalid");
assert_eq!(result.warnings.len(), 0, "Should have no warnings");
assert_eq!(result.errors.len(), 1, "Should have one error");
match &result.errors[0] {
Error::Internal {
kind: ErrorKind::Validation,
message: msg,
..
} => {
assert!(msg.contains("multipart/form-data"));
assert!(msg.contains("v1.0"));
}
_ => panic!("Expected Validation error"),
}
}
#[test]
fn test_validate_with_mode_multiple_content_types() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut path_item1 = PathItem::default();
let mut request_body1 = RequestBody::default();
request_body1.content.insert(
constants::CONTENT_TYPE_XML.to_string(),
MediaType::default(),
);
path_item1.post = Some(Operation {
operation_id: Some("postXml".to_string()),
tags: vec!["data".to_string()],
request_body: Some(ReferenceOr::Item(request_body1)),
responses: Responses::default(),
..Default::default()
});
spec.paths
.paths
.insert("/xml".to_string(), PathRef::Item(path_item1));
let mut path_item2 = PathItem::default();
let mut request_body2 = RequestBody::default();
request_body2.content.insert(
constants::CONTENT_TYPE_TEXT.to_string(),
MediaType::default(),
);
path_item2.put = Some(Operation {
operation_id: Some("putText".to_string()),
tags: vec!["data".to_string()],
request_body: Some(ReferenceOr::Item(request_body2)),
responses: Responses::default(),
..Default::default()
});
spec.paths
.paths
.insert("/text".to_string(), PathRef::Item(path_item2));
let result = validator.validate_with_mode(&spec, false);
assert!(result.is_valid());
assert_eq!(result.warnings.len(), 2);
let warning_paths: Vec<&str> = result
.warnings
.iter()
.map(|w| w.endpoint.path.as_str())
.collect();
assert!(warning_paths.contains(&"/xml"));
assert!(warning_paths.contains(&"/text"));
}
#[test]
fn test_validate_with_mode_multiple_unsupported_types_single_endpoint() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut request_body = RequestBody::default();
request_body
.content
.insert("multipart/form-data".to_string(), MediaType::default());
request_body.content.insert(
constants::CONTENT_TYPE_XML.to_string(),
MediaType::default(),
);
request_body.content.insert(
constants::CONTENT_TYPE_TEXT.to_string(),
MediaType::default(),
);
request_body.required = true;
let mut path_item = PathItem::default();
path_item.post = Some(Operation {
operation_id: Some("uploadData".to_string()),
tags: vec!["data".to_string()],
request_body: Some(ReferenceOr::Item(request_body)),
responses: Responses::default(),
..Default::default()
});
spec.paths
.paths
.insert("/data".to_string(), PathRef::Item(path_item));
let result = validator.validate_with_mode(&spec, false);
assert!(result.is_valid(), "Non-strict mode should be valid");
assert_eq!(result.warnings.len(), 1, "Should have exactly one warning");
assert_eq!(result.errors.len(), 0, "Should have no errors");
let warning = &result.warnings[0];
assert_eq!(warning.endpoint.path, "/data");
assert_eq!(warning.endpoint.method, constants::HTTP_METHOD_POST);
assert!(warning
.endpoint
.content_type
.contains("multipart/form-data"));
assert!(warning
.endpoint
.content_type
.contains(constants::CONTENT_TYPE_XML));
assert!(warning
.endpoint
.content_type
.contains(constants::CONTENT_TYPE_TEXT));
assert!(warning.reason.contains("no supported content types"));
}
#[test]
fn test_validate_unsupported_http_scheme() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut components = Components::default();
components.security_schemes.insert(
"negotiate".to_string(),
ReferenceOr::Item(SecurityScheme::HTTP {
scheme: "negotiate".to_string(),
bearer_format: None,
description: None,
extensions: Default::default(),
}),
);
spec.components = Some(components);
let result = validator.validate_with_mode(&spec, true).into_result();
assert!(result.is_err());
match result.unwrap_err() {
Error::Internal {
kind: ErrorKind::Validation,
message: msg,
..
} => {
assert!(msg.contains("requires complex authentication flows"));
}
_ => panic!("Expected Validation error"),
}
}
#[test]
fn test_validate_custom_http_schemes_allowed() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut components = Components::default();
let custom_schemes = vec!["digest", "token", "apikey", "dsn", "custom-auth"];
for scheme in custom_schemes {
components.security_schemes.insert(
format!("{scheme}_auth"),
ReferenceOr::Item(SecurityScheme::HTTP {
scheme: scheme.to_string(),
bearer_format: None,
description: None,
extensions: Default::default(),
}),
);
}
spec.components = Some(components);
let result = validator.validate_with_mode(&spec, true);
assert!(result.is_valid(), "Custom HTTP schemes should be allowed");
}
#[test]
fn test_validate_parameter_reference_allowed() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut path_item = PathItem::default();
path_item.get = Some(Operation {
parameters: vec![ReferenceOr::Reference {
reference: "#/components/parameters/UserId".to_string(),
}],
responses: Responses::default(),
..Default::default()
});
spec.paths
.paths
.insert("/users/{id}".to_string(), PathRef::Item(path_item));
let result = validator.validate_with_mode(&spec, true).into_result();
assert!(result.is_ok());
}
#[test]
fn test_validate_request_body_non_json_rejected() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut request_body = RequestBody::default();
request_body.content.insert(
constants::CONTENT_TYPE_XML.to_string(),
MediaType::default(),
);
request_body.required = true;
let mut path_item = PathItem::default();
path_item.post = Some(Operation {
request_body: Some(ReferenceOr::Item(request_body)),
responses: Responses::default(),
..Default::default()
});
spec.paths
.paths
.insert("/users".to_string(), PathRef::Item(path_item));
let result = validator.validate_with_mode(&spec, true).into_result();
assert!(result.is_err());
match result.unwrap_err() {
Error::Internal {
kind: ErrorKind::Validation,
message: msg,
..
} => {
assert!(msg.contains("Unsupported request body content type 'application/xml'"));
}
_ => panic!("Expected Validation error"),
}
}
#[test]
fn test_validate_x_aperture_secret_valid() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut components = Components::default();
let mut extensions = serde_json::Map::new();
extensions.insert(
crate::constants::EXT_APERTURE_SECRET.to_string(),
serde_json::json!({
"source": "env",
"name": "API_TOKEN"
}),
);
components.security_schemes.insert(
"bearerAuth".to_string(),
ReferenceOr::Item(SecurityScheme::HTTP {
scheme: constants::AUTH_SCHEME_BEARER.to_string(),
bearer_format: None,
description: None,
extensions: extensions.into_iter().collect(),
}),
);
spec.components = Some(components);
assert!(validator
.validate_with_mode(&spec, true)
.into_result()
.is_ok());
}
#[test]
fn test_validate_x_aperture_secret_missing_source() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut components = Components::default();
let mut extensions = serde_json::Map::new();
extensions.insert(
crate::constants::EXT_APERTURE_SECRET.to_string(),
serde_json::json!({
"name": "API_TOKEN"
}),
);
components.security_schemes.insert(
"bearerAuth".to_string(),
ReferenceOr::Item(SecurityScheme::HTTP {
scheme: constants::AUTH_SCHEME_BEARER.to_string(),
bearer_format: None,
description: None,
extensions: extensions.into_iter().collect(),
}),
);
spec.components = Some(components);
let result = validator.validate_with_mode(&spec, true).into_result();
assert!(result.is_err());
match result.unwrap_err() {
Error::Internal {
kind: ErrorKind::Validation,
message: msg,
..
} => {
assert!(msg.contains("Missing 'source' field"));
}
_ => panic!("Expected Validation error"),
}
}
#[test]
fn test_validate_x_aperture_secret_missing_name() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut components = Components::default();
let mut extensions = serde_json::Map::new();
extensions.insert(
crate::constants::EXT_APERTURE_SECRET.to_string(),
serde_json::json!({
"source": "env"
}),
);
components.security_schemes.insert(
"bearerAuth".to_string(),
ReferenceOr::Item(SecurityScheme::HTTP {
scheme: constants::AUTH_SCHEME_BEARER.to_string(),
bearer_format: None,
description: None,
extensions: extensions.into_iter().collect(),
}),
);
spec.components = Some(components);
let result = validator.validate_with_mode(&spec, true).into_result();
assert!(result.is_err());
match result.unwrap_err() {
Error::Internal {
kind: ErrorKind::Validation,
message: msg,
..
} => {
assert!(msg.contains("Missing 'name' field"));
}
_ => panic!("Expected Validation error"),
}
}
#[test]
fn test_validate_x_aperture_secret_invalid_env_name() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut components = Components::default();
let mut extensions = serde_json::Map::new();
extensions.insert(
crate::constants::EXT_APERTURE_SECRET.to_string(),
serde_json::json!({
"source": "env",
"name": "123_INVALID" }),
);
components.security_schemes.insert(
"bearerAuth".to_string(),
ReferenceOr::Item(SecurityScheme::HTTP {
scheme: constants::AUTH_SCHEME_BEARER.to_string(),
bearer_format: None,
description: None,
extensions: extensions.into_iter().collect(),
}),
);
spec.components = Some(components);
let result = validator.validate_with_mode(&spec, true).into_result();
assert!(result.is_err());
match result.unwrap_err() {
Error::Internal {
kind: ErrorKind::Validation,
message: msg,
..
} => {
assert!(msg.contains("Invalid environment variable name"));
}
_ => panic!("Expected Validation error"),
}
}
#[test]
fn test_validate_x_aperture_secret_unsupported_source() {
let validator = SpecValidator::new();
let mut spec = create_test_spec();
let mut components = Components::default();
let mut extensions = serde_json::Map::new();
extensions.insert(
crate::constants::EXT_APERTURE_SECRET.to_string(),
serde_json::json!({
"source": "file", "name": "API_TOKEN"
}),
);
components.security_schemes.insert(
"bearerAuth".to_string(),
ReferenceOr::Item(SecurityScheme::HTTP {
scheme: constants::AUTH_SCHEME_BEARER.to_string(),
bearer_format: None,
description: None,
extensions: extensions.into_iter().collect(),
}),
);
spec.components = Some(components);
let result = validator.validate_with_mode(&spec, true).into_result();
assert!(result.is_err());
match result.unwrap_err() {
Error::Internal {
kind: ErrorKind::Validation,
message: msg,
..
} => {
assert!(msg.contains("Unsupported source 'file'"));
}
_ => panic!("Expected Validation error"),
}
}
}