use crate::CorsConfig;
use axum::body::Body;
use axum::http::{HeaderMap, HeaderValue, Response, StatusCode};
use axum::response::IntoResponse;
fn is_origin_allowed(origin: &str, allowed_origins: &[String]) -> bool {
if origin.is_empty() {
return false;
}
allowed_origins
.iter()
.any(|allowed| allowed == "*" || allowed == origin)
}
fn is_method_allowed(method: &str, allowed_methods: &[String]) -> bool {
allowed_methods
.iter()
.any(|allowed| allowed == "*" || allowed.eq_ignore_ascii_case(method))
}
fn are_headers_allowed(requested: impl IntoIterator<Item = impl AsRef<str>>, allowed: &[String]) -> bool {
if allowed.iter().any(|h| h == "*") {
return true;
}
requested.into_iter().all(|req_header| {
let req_str = req_header.as_ref();
allowed
.iter()
.any(|allowed_header| allowed_header.eq_ignore_ascii_case(req_str))
})
}
pub fn handle_preflight(headers: &HeaderMap, cors_config: &CorsConfig) -> Result<Response<Body>, Box<Response<Body>>> {
let origin = headers.get("origin").and_then(|v| v.to_str().ok()).unwrap_or("");
if origin.is_empty() || !is_origin_allowed(origin, &cors_config.allowed_origins) {
return Err(Box::new(
(
StatusCode::FORBIDDEN,
axum::Json(serde_json::json!({
"detail": format!("CORS request from origin '{}' not allowed", origin)
})),
)
.into_response(),
));
}
let requested_method = headers
.get("access-control-request-method")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if !requested_method.is_empty() && !is_method_allowed(requested_method, &cors_config.allowed_methods) {
return Err(Box::new((StatusCode::FORBIDDEN).into_response()));
}
let requested_headers_str = headers
.get("access-control-request-headers")
.and_then(|v| v.to_str().ok());
if let Some(req_headers) = requested_headers_str {
if !are_headers_allowed(req_headers.split(',').map(|h| h.trim()), &cors_config.allowed_headers) {
return Err(Box::new((StatusCode::FORBIDDEN).into_response()));
}
}
let mut response = Response::builder().status(StatusCode::NO_CONTENT);
let headers_mut = match response.headers_mut() {
Some(headers) => headers,
None => {
return Err(Box::new(
(
StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(serde_json::json!({
"detail": "Failed to construct response headers"
})),
)
.into_response(),
));
}
};
headers_mut.insert(
"access-control-allow-origin",
HeaderValue::from_str(origin).unwrap_or_else(|_| HeaderValue::from_static("*")),
);
let methods = cors_config.allowed_methods_joined();
headers_mut.insert(
"access-control-allow-methods",
HeaderValue::from_str(methods).unwrap_or_else(|_| HeaderValue::from_static("*")),
);
let allowed_headers = cors_config.allowed_headers_joined();
headers_mut.insert(
"access-control-allow-headers",
HeaderValue::from_str(allowed_headers).unwrap_or_else(|_| HeaderValue::from_static("*")),
);
if let Some(max_age) = cors_config.max_age
&& let Ok(header_val) = HeaderValue::from_str(&max_age.to_string())
{
headers_mut.insert("access-control-max-age", header_val);
}
if let Some(true) = cors_config.allow_credentials {
headers_mut.insert("access-control-allow-credentials", HeaderValue::from_static("true"));
}
match response.body(Body::empty()) {
Ok(resp) => Ok(resp),
Err(_) => Err(Box::new(
(
StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(serde_json::json!({
"detail": "Failed to construct response body"
})),
)
.into_response(),
)),
}
}
pub fn add_cors_headers(response: &mut Response<Body>, origin: &str, cors_config: &CorsConfig) {
let headers = response.headers_mut();
if let Ok(origin_value) = HeaderValue::from_str(origin) {
headers.insert("access-control-allow-origin", origin_value);
}
if let Some(ref expose_headers) = cors_config.expose_headers {
let expose = expose_headers.join(", ");
if let Ok(expose_value) = HeaderValue::from_str(&expose) {
headers.insert("access-control-expose-headers", expose_value);
}
}
if let Some(true) = cors_config.allow_credentials {
headers.insert("access-control-allow-credentials", HeaderValue::from_static("true"));
}
}
pub fn validate_cors_request(headers: &HeaderMap, cors_config: &CorsConfig) -> Result<(), Box<Response<Body>>> {
let origin = headers.get("origin").and_then(|v| v.to_str().ok()).unwrap_or("");
if !origin.is_empty() && !is_origin_allowed(origin, &cors_config.allowed_origins) {
return Err(Box::new(
(
StatusCode::FORBIDDEN,
axum::Json(serde_json::json!({
"detail": format!("CORS request from origin '{}' not allowed", origin)
})),
)
.into_response(),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn make_cors_config() -> CorsConfig {
CorsConfig {
allowed_origins: vec!["https://example.com".to_string()],
allowed_methods: vec!["GET".to_string(), "POST".to_string()],
allowed_headers: vec!["content-type".to_string(), "authorization".to_string()],
expose_headers: Some(vec!["x-custom-header".to_string()]),
max_age: Some(3600),
allow_credentials: Some(true),
..Default::default()
}
}
#[test]
fn test_is_origin_allowed_exact_match() {
let allowed = vec!["https://example.com".to_string()];
assert!(is_origin_allowed("https://example.com", &allowed));
assert!(!is_origin_allowed("https://evil.com", &allowed));
}
#[test]
fn test_is_origin_allowed_wildcard() {
let allowed = vec!["*".to_string()];
assert!(is_origin_allowed("https://example.com", &allowed));
assert!(is_origin_allowed("https://any-domain.com", &allowed));
}
#[test]
fn test_is_origin_allowed_empty_origin() {
let allowed = vec!["*".to_string()];
assert!(!is_origin_allowed("", &allowed));
}
#[test]
fn test_is_method_allowed_case_insensitive() {
let allowed = vec!["GET".to_string(), "POST".to_string()];
assert!(is_method_allowed("GET", &allowed));
assert!(is_method_allowed("get", &allowed));
assert!(is_method_allowed("POST", &allowed));
assert!(is_method_allowed("post", &allowed));
assert!(!is_method_allowed("DELETE", &allowed));
}
#[test]
fn test_is_method_allowed_wildcard() {
let allowed = vec!["*".to_string()];
assert!(is_method_allowed("GET", &allowed));
assert!(is_method_allowed("DELETE", &allowed));
assert!(is_method_allowed("PATCH", &allowed));
}
#[test]
fn test_are_headers_allowed_case_insensitive() {
let allowed = vec!["Content-Type".to_string(), "Authorization".to_string()];
assert!(are_headers_allowed(&["content-type"], &allowed));
assert!(are_headers_allowed(&["AUTHORIZATION"], &allowed));
assert!(are_headers_allowed(&["content-type", "authorization"], &allowed));
assert!(!are_headers_allowed(&["x-custom"], &allowed));
}
#[test]
fn test_are_headers_allowed_wildcard() {
let allowed = vec!["*".to_string()];
assert!(are_headers_allowed(&["any-header"], &allowed));
assert!(are_headers_allowed(&["multiple", "headers"], &allowed));
}
#[test]
fn test_handle_preflight_success() {
let config = make_cors_config();
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://example.com"));
headers.insert("access-control-request-method", HeaderValue::from_static("POST"));
headers.insert(
"access-control-request-headers",
HeaderValue::from_static("content-type"),
);
let result = handle_preflight(&headers, &config);
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.status(), StatusCode::NO_CONTENT);
let resp_headers = response.headers();
assert_eq!(
resp_headers.get("access-control-allow-origin").unwrap(),
"https://example.com"
);
assert!(
resp_headers
.get("access-control-allow-methods")
.unwrap()
.to_str()
.unwrap()
.contains("POST")
);
assert_eq!(resp_headers.get("access-control-max-age").unwrap(), "3600");
assert_eq!(resp_headers.get("access-control-allow-credentials").unwrap(), "true");
}
#[test]
fn test_handle_preflight_origin_not_allowed() {
let config = make_cors_config();
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://evil.com"));
headers.insert("access-control-request-method", HeaderValue::from_static("GET"));
let result = handle_preflight(&headers, &config);
assert!(result.is_err());
let response = *result.unwrap_err();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn test_handle_preflight_method_not_allowed() {
let config = make_cors_config();
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://example.com"));
headers.insert("access-control-request-method", HeaderValue::from_static("DELETE"));
let result = handle_preflight(&headers, &config);
assert!(result.is_err());
let response = *result.unwrap_err();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn test_handle_preflight_header_not_allowed() {
let config = make_cors_config();
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://example.com"));
headers.insert("access-control-request-method", HeaderValue::from_static("POST"));
headers.insert(
"access-control-request-headers",
HeaderValue::from_static("x-forbidden-header"),
);
let result = handle_preflight(&headers, &config);
assert!(result.is_err());
let response = *result.unwrap_err();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn test_handle_preflight_empty_origin() {
let config = make_cors_config();
let headers = HeaderMap::new();
let result = handle_preflight(&headers, &config);
assert!(result.is_err());
let response = *result.unwrap_err();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn test_add_cors_headers() {
let config = make_cors_config();
let mut response = Response::new(Body::empty());
add_cors_headers(&mut response, "https://example.com", &config);
let headers = response.headers();
assert_eq!(
headers.get("access-control-allow-origin").unwrap(),
"https://example.com"
);
assert_eq!(headers.get("access-control-expose-headers").unwrap(), "x-custom-header");
assert_eq!(headers.get("access-control-allow-credentials").unwrap(), "true");
}
#[test]
fn test_validate_cors_request_allowed() {
let config = make_cors_config();
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://example.com"));
let result = validate_cors_request(&headers, &config);
assert!(result.is_ok());
}
#[test]
fn test_validate_cors_request_not_allowed() {
let config = make_cors_config();
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://evil.com"));
let result = validate_cors_request(&headers, &config);
assert!(result.is_err());
let response = *result.unwrap_err();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn test_validate_cors_request_no_origin() {
let config = make_cors_config();
let headers = HeaderMap::new();
let result = validate_cors_request(&headers, &config);
assert!(result.is_ok());
}
#[test]
fn test_credentials_with_wildcard_config_is_security_issue() {
let config = CorsConfig {
allowed_origins: vec!["*".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: Some(true), ..Default::default()
};
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://evil.com"));
headers.insert("access-control-request-method", HeaderValue::from_static("GET"));
let result = handle_preflight(&headers, &config);
if let Ok(response) = result {
let resp_headers = response.headers();
let has_credentials = resp_headers
.get("access-control-allow-credentials")
.map(|v| v == "true")
.unwrap_or(false);
let origin_header = resp_headers.get("access-control-allow-origin");
if has_credentials && origin_header.is_some() {
let origin_val = origin_header.unwrap().to_str().unwrap_or("");
if origin_val == "*" {
panic!("SECURITY VULNERABILITY: credentials=true with origin=* allowed");
}
}
}
}
#[test]
fn test_subdomain_bypass_blocked() {
let config = CorsConfig {
allowed_origins: vec!["https://example.com".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
assert!(!is_origin_allowed("https://api.example.com", &config.allowed_origins));
assert!(!is_origin_allowed("https://evil.example.com", &config.allowed_origins));
assert!(!is_origin_allowed(
"https://sub.sub.example.com",
&config.allowed_origins
));
assert!(is_origin_allowed("https://example.com", &config.allowed_origins));
}
#[test]
fn test_port_bypass_blocked() {
let config = CorsConfig {
allowed_origins: vec!["http://localhost:3000".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
assert!(!is_origin_allowed("http://localhost:3001", &config.allowed_origins));
assert!(!is_origin_allowed("http://localhost:8080", &config.allowed_origins));
assert!(!is_origin_allowed("http://localhost:443", &config.allowed_origins));
assert!(is_origin_allowed("http://localhost:3000", &config.allowed_origins));
}
#[test]
fn test_protocol_downgrade_attack_blocked() {
let config = CorsConfig {
allowed_origins: vec!["https://example.com".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
assert!(!is_origin_allowed("http://example.com", &config.allowed_origins));
assert!(!is_origin_allowed("ws://example.com", &config.allowed_origins));
assert!(!is_origin_allowed("wss://example.com", &config.allowed_origins));
assert!(is_origin_allowed("https://example.com", &config.allowed_origins));
}
#[test]
fn test_case_sensitive_origin_matching() {
let config = CorsConfig {
allowed_origins: vec!["https://Example.Com".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
assert!(!is_origin_allowed("https://example.com", &config.allowed_origins));
assert!(!is_origin_allowed("https://EXAMPLE.COM", &config.allowed_origins));
assert!(is_origin_allowed("https://Example.Com", &config.allowed_origins));
}
#[test]
fn test_trailing_slash_origin_not_normalized() {
let config = CorsConfig {
allowed_origins: vec!["https://example.com".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
assert!(!is_origin_allowed("https://example.com/", &config.allowed_origins));
assert!(is_origin_allowed("https://example.com", &config.allowed_origins));
}
#[test]
fn test_null_origin_with_wildcard() {
let config = CorsConfig {
allowed_origins: vec!["*".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
assert!(is_origin_allowed("null", &config.allowed_origins));
let with_explicit_null = CorsConfig {
allowed_origins: vec!["null".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
assert!(is_origin_allowed("null", &with_explicit_null.allowed_origins));
}
#[test]
fn test_empty_origin_always_rejected() {
let config_with_wildcard = CorsConfig {
allowed_origins: vec!["*".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
assert!(!is_origin_allowed("", &config_with_wildcard.allowed_origins));
let config_with_explicit = CorsConfig {
allowed_origins: vec!["https://example.com".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
assert!(!is_origin_allowed("", &config_with_explicit.allowed_origins));
}
#[test]
fn test_preflight_rejects_invalid_origin() {
let config = make_cors_config();
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://untrusted.com"));
headers.insert("access-control-request-method", HeaderValue::from_static("POST"));
let result = handle_preflight(&headers, &config);
assert!(result.is_err());
let response = *result.unwrap_err();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn test_multiple_origins_exact_matching() {
let config = CorsConfig {
allowed_origins: vec!["https://trusted1.com".to_string(), "https://trusted2.com".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
assert!(is_origin_allowed("https://trusted1.com", &config.allowed_origins));
assert!(is_origin_allowed("https://trusted2.com", &config.allowed_origins));
assert!(!is_origin_allowed(
"https://trusted1.com.evil.com",
&config.allowed_origins
));
assert!(!is_origin_allowed("https://trusted3.com", &config.allowed_origins));
assert!(!is_origin_allowed("https://trusted.com", &config.allowed_origins));
}
#[test]
fn test_wildcard_allows_all_but_check_credentials() {
let config = CorsConfig {
allowed_origins: vec!["*".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
assert!(is_origin_allowed("https://example.com", &config.allowed_origins));
assert!(is_origin_allowed("https://evil.com", &config.allowed_origins));
assert!(is_origin_allowed("http://localhost:3000", &config.allowed_origins));
assert!(!is_origin_allowed("", &config.allowed_origins));
}
#[test]
fn test_preflight_response_has_correct_allowed_origins() {
let config = CorsConfig {
allowed_origins: vec!["https://trusted.com".to_string()],
allowed_methods: vec!["GET".to_string(), "POST".to_string()],
allowed_headers: vec!["content-type".to_string()],
expose_headers: None,
max_age: Some(3600),
allow_credentials: Some(false),
..Default::default()
};
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://trusted.com"));
headers.insert("access-control-request-method", HeaderValue::from_static("POST"));
headers.insert(
"access-control-request-headers",
HeaderValue::from_static("content-type"),
);
let result = handle_preflight(&headers, &config);
assert!(result.is_ok());
let response = result.unwrap();
let resp_headers = response.headers();
assert_eq!(
resp_headers.get("access-control-allow-origin").unwrap(),
"https://trusted.com"
);
assert!(
resp_headers
.get("access-control-allow-methods")
.unwrap()
.to_str()
.unwrap()
.contains("GET")
);
assert!(
resp_headers
.get("access-control-allow-methods")
.unwrap()
.to_str()
.unwrap()
.contains("POST")
);
assert!(resp_headers.get("access-control-allow-credentials").is_none());
}
#[test]
fn test_preflight_all_origins_require_validation() {
let config = CorsConfig {
allowed_origins: vec!["https://trusted.com".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
let test_cases = vec![
"https://trusted.com",
"https://evil.com",
"https://trusted.com.evil",
"http://trusted.com",
"",
];
for origin in test_cases {
let mut headers = HeaderMap::new();
headers.insert(
"origin",
HeaderValue::from_str(origin).unwrap_or_else(|_| HeaderValue::from_static("")),
);
headers.insert("access-control-request-method", HeaderValue::from_static("GET"));
let result = handle_preflight(&headers, &config);
if origin == "https://trusted.com" {
assert!(result.is_ok(), "Valid origin {} should be allowed", origin);
} else {
assert!(result.is_err(), "Invalid origin {} should be rejected", origin);
}
}
}
#[test]
fn test_preflight_validates_all_requested_headers() {
let config = CorsConfig {
allowed_origins: vec!["https://trusted.com".to_string()],
allowed_methods: vec!["POST".to_string()],
allowed_headers: vec!["content-type".to_string(), "authorization".to_string()],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
let test_cases = vec![
("content-type", true),
("authorization", true),
("content-type, authorization", true),
("x-custom-header", false),
("content-type, x-custom", false),
];
for (headers_str, should_pass) in test_cases {
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://trusted.com"));
headers.insert("access-control-request-method", HeaderValue::from_static("POST"));
headers.insert(
"access-control-request-headers",
HeaderValue::from_str(headers_str).unwrap(),
);
let result = handle_preflight(&headers, &config);
if should_pass {
assert!(
result.is_ok(),
"Preflight with valid headers '{}' should pass",
headers_str
);
} else {
assert!(
result.is_err(),
"Preflight with invalid headers '{}' should fail",
headers_str
);
}
}
}
#[test]
fn test_add_cors_headers_respects_origin() {
let config = CorsConfig {
allowed_origins: vec!["https://trusted.com".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: Some(vec!["x-custom".to_string()]),
max_age: None,
allow_credentials: Some(true),
..Default::default()
};
let mut response = Response::new(Body::empty());
add_cors_headers(&mut response, "https://trusted.com", &config);
let headers = response.headers();
assert_eq!(
headers.get("access-control-allow-origin").unwrap(),
"https://trusted.com"
);
assert_eq!(headers.get("access-control-expose-headers").unwrap(), "x-custom");
assert_eq!(headers.get("access-control-allow-credentials").unwrap(), "true");
}
#[test]
fn test_validate_cors_request_origin_must_match() {
let config = CorsConfig {
allowed_origins: vec!["https://trusted.com".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://trusted.com"));
assert!(validate_cors_request(&headers, &config).is_ok());
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://evil.com"));
assert!(validate_cors_request(&headers, &config).is_err());
let headers = HeaderMap::new();
assert!(validate_cors_request(&headers, &config).is_ok());
}
#[test]
fn test_preflight_requires_access_control_request_method() {
let config = make_cors_config();
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://example.com"));
let result = handle_preflight(&headers, &config);
assert!(result.is_ok());
}
#[test]
fn test_preflight_method_case_insensitive() {
let config = CorsConfig {
allowed_origins: vec!["https://example.com".to_string()],
allowed_methods: vec!["GET".to_string(), "POST".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
let test_cases = vec!["GET", "get", "Get", "POST", "post"];
for method in test_cases {
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://example.com"));
headers.insert("access-control-request-method", HeaderValue::from_str(method).unwrap());
let result = handle_preflight(&headers, &config);
assert!(
result.is_ok(),
"Method '{}' should be allowed (case-insensitive)",
method
);
}
}
#[test]
fn test_preflight_max_age_header() {
let config = CorsConfig {
allowed_origins: vec!["https://example.com".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: Some(7200),
allow_credentials: None,
..Default::default()
};
let mut headers = HeaderMap::new();
headers.insert("origin", HeaderValue::from_static("https://example.com"));
headers.insert("access-control-request-method", HeaderValue::from_static("GET"));
let result = handle_preflight(&headers, &config);
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.headers().get("access-control-max-age").unwrap(), "7200");
}
#[test]
fn test_wildcard_patterns_not_supported() {
let config = CorsConfig {
allowed_origins: vec!["*.example.com".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
expose_headers: None,
max_age: None,
allow_credentials: None,
..Default::default()
};
assert!(!is_origin_allowed("https://api.example.com", &config.allowed_origins));
assert!(!is_origin_allowed("https://example.com", &config.allowed_origins));
assert!(is_origin_allowed("*.example.com", &config.allowed_origins));
}
}