use crate::{configs::Config, server::router};
use std::sync::Arc;
use axum::{http::{header, HeaderValue, Method, StatusCode}, routing::get, Json, Router, body::{Body, to_bytes}, routing::post};
use tower::ServiceExt;
use super::support::{app, request};
#[tokio::test]
async fn successful_and_error_responses_have_ids_and_security_headers() {
let app = app(&Config::default());
for (path, status) in [("/items", StatusCode::OK), ("/missing?secret=private", StatusCode::NOT_FOUND)] {
let response = app.clone().oneshot(request(path, "127.0.0.1")).await.unwrap();
assert_eq!(response.status(), status);
let id = response.headers()["x-request-id"].to_str().unwrap().to_owned();
assert!(uuid::Uuid::parse_str(&id).is_ok());
assert_eq!(response.headers()["x-content-type-options"], "nosniff");
if status.is_client_error() {
let body = to_bytes(response.into_body(), 4096).await.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(body["error"]["request_id"], id);
assert_eq!(body["error"]["code"], "not_found");
}
}
}
#[tokio::test]
async fn rate_limits_socket_peers_and_ignores_spoofed_forwarding_headers() {
let app = app(&Config { rate_limit_requests: 1, ..Config::default() });
assert_eq!(app.clone().oneshot(request("/items", "127.0.0.1")).await.unwrap().status(), StatusCode::OK);
let mut spoofed = request("/items", "127.0.0.1");
spoofed.headers_mut().insert("x-forwarded-for", HeaderValue::from_static("203.0.113.9"));
let response = app.clone().oneshot(spoofed).await.unwrap();
assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
assert!(response.headers().contains_key(header::RETRY_AFTER));
assert!(response.headers().contains_key("x-request-id"));
assert_eq!(app.clone().oneshot(request("/items", "127.0.0.2")).await.unwrap().status(), StatusCode::OK);
for path in ["/health/live", "/health/ready"] {
assert_eq!(app.clone().oneshot(request(path, "127.0.0.1")).await.unwrap().status(), StatusCode::OK);
}
}
#[tokio::test]
async fn timeout_and_panic_return_safe_json_errors() {
let config = Config { request_timeout: std::time::Duration::from_millis(5), ..Config::default() };
let app = router(Router::new()
.route("/slow", get(|| async { tokio::time::sleep(std::time::Duration::from_secs(1)).await; StatusCode::OK }))
.route("/panic", get(|| async { panic!("private internal details"); #[allow(unreachable_code)] StatusCode::OK })), &config);
for (path, status, code) in [("/slow", StatusCode::GATEWAY_TIMEOUT, "request_timeout"), ("/panic", StatusCode::INTERNAL_SERVER_ERROR, "internal_error")] {
let response = app.clone().oneshot(request(path, "127.0.0.1")).await.unwrap();
assert_eq!(response.status(), status);
let body = to_bytes(response.into_body(), 4096).await.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(body["error"]["code"], code);
assert!(!body.to_string().contains("private internal details"));
}
}
#[tokio::test]
async fn rejects_declared_and_streamed_oversized_bodies() {
let app = router(Router::new().route("/echo", post(|body: axum::body::Bytes| async move { body })),
&Config { body_limit: 4, ..Config::default() });
for declared in [false, true] {
let mut req = request("/echo", "127.0.0.1");
*req.method_mut() = Method::POST;
*req.body_mut() = Body::from("12345");
if declared { req.headers_mut().insert(header::CONTENT_LENGTH, HeaderValue::from_static("5")); }
let response = app.clone().oneshot(req).await.unwrap();
assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
assert_eq!(response.headers()[header::CONTENT_TYPE], "application/json");
}
}
#[tokio::test]
async fn cors_allows_only_configured_origins() {
let app = app(&Config { cors_origins: vec![HeaderValue::from_static("https://example.com")], ..Config::default() });
for origin in ["https://example.com", "https://untrusted.example"] {
let mut req = request("/items", "127.0.0.1");
*req.method_mut() = Method::OPTIONS;
req.headers_mut().insert(header::ORIGIN, origin.parse().unwrap());
req.headers_mut().insert(header::ACCESS_CONTROL_REQUEST_METHOD, HeaderValue::from_static("GET"));
let response = app.clone().oneshot(req).await.unwrap();
assert_eq!(response.headers().contains_key(header::ACCESS_CONTROL_ALLOW_ORIGIN), origin == "https://example.com");
assert!(response.headers().contains_key("x-request-id"));
}
let app = self::app(&Config::default());
let mut req = request("/items", "127.0.0.1");
req.headers_mut().insert(header::ORIGIN, HeaderValue::from_static("https://example.com"));
assert!(!app.oneshot(req).await.unwrap().headers().contains_key(header::ACCESS_CONTROL_ALLOW_ORIGIN));
}
#[tokio::test]
async fn concurrency_rejection_preserves_health_and_permits_are_released() {
let entered = Arc::new(tokio::sync::Notify::new());
let release = Arc::new(tokio::sync::Notify::new());
let handler_entered = entered.clone();
let handler_release = release.clone();
let app = router(Router::new().route("/work", get(move || {
let entered = handler_entered.clone();
let release = handler_release.clone();
async move { entered.notify_one(); release.notified().await; StatusCode::OK }
})), &Config { max_in_flight: 1, ..Config::default() });
let first = tokio::spawn(app.clone().oneshot(request("/work", "127.0.0.1")));
tokio::time::timeout(std::time::Duration::from_secs(1), entered.notified()).await.unwrap();
let response = app.clone().oneshot(request("/work", "127.0.0.2")).await.unwrap();
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
assert!(response.headers().contains_key(header::RETRY_AFTER));
assert_eq!(app.clone().oneshot(request("/health/live", "127.0.0.2")).await.unwrap().status(), StatusCode::OK);
release.notify_one();
assert_eq!(first.await.unwrap().unwrap().status(), StatusCode::OK);
release.notify_one();
assert_eq!(app.oneshot(request("/work", "127.0.0.2")).await.unwrap().status(), StatusCode::OK);
}
#[tokio::test]
async fn json_rejections_and_method_errors_use_the_error_envelope() {
let app = router(Router::new().route("/json", post(|Json(value): Json<serde_json::Value>| async move { Json(value) })), &Config::default());
let mut req = request("/json", "127.0.0.1");
*req.method_mut() = Method::POST;
*req.body_mut() = Body::from("{broken");
req.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("application/json"));
let response = app.clone().oneshot(req).await.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = to_bytes(response.into_body(), 4096).await.unwrap();
assert_eq!(serde_json::from_slice::<serde_json::Value>(&body).unwrap()["error"]["code"], "bad_request");
let response = app.oneshot(request("/json", "127.0.0.1")).await.unwrap();
assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
assert!(response.headers().contains_key(header::ALLOW));
}