use axum::body::Body;
use axum::Router;
use crate::config::{AppConfig, AuthConfig, ServerConfig};
use crate::http::{build_with_config, X_REQUEST_ID};
#[tokio::test]
async fn test_request_id_middleware_generates_uuid_when_absent() {
let config = AppConfig {
server: ServerConfig {
host: "127.0.0.1".to_string(),
port: 8080,
request_timeout_secs: 30,
cors: None,
},
authentication: AuthConfig::None,
timeout: None,
};
let router = build_with_config(&config).unwrap();
let response = tower::ServiceExt::oneshot(
router,
axum::http::Request::builder()
.uri("/cov-test-route")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::OK);
let request_id = response
.headers()
.get(X_REQUEST_ID)
.expect("response should have x-request-id header");
let id_str = request_id.to_str().unwrap();
assert_eq!(id_str.len(), 36, "generated request ID should be a UUID");
}
#[tokio::test]
async fn test_request_id_middleware_preserves_custom_id() {
let config = AppConfig {
server: ServerConfig {
host: "127.0.0.1".to_string(),
port: 8080,
request_timeout_secs: 30,
cors: None,
},
authentication: AuthConfig::None,
timeout: None,
};
let router = build_with_config(&config).unwrap();
let response = tower::ServiceExt::oneshot(
router,
axum::http::Request::builder()
.uri("/cov-test-route")
.header(X_REQUEST_ID, "my-custom-id-789")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::OK);
let request_id = response.headers().get(X_REQUEST_ID).unwrap();
assert_eq!(request_id.to_str().unwrap(), "my-custom-id-789");
}
#[tokio::test]
async fn test_request_id_middleware_non_utf8_header_generates_uuid() {
let config = AppConfig {
server: ServerConfig {
host: "127.0.0.1".to_string(),
port: 8080,
request_timeout_secs: 30,
cors: None,
},
authentication: AuthConfig::None,
timeout: None,
};
let router = build_with_config(&config).unwrap();
let response = tower::ServiceExt::oneshot(
router,
axum::http::Request::builder()
.uri("/cov-test-route")
.header(
X_REQUEST_ID,
axum::http::HeaderValue::from_bytes(b"\xff\xfe").unwrap(),
)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::OK);
let request_id = response.headers().get(X_REQUEST_ID).unwrap();
let id_str = request_id.to_str().unwrap();
assert_eq!(id_str.len(), 36, "should generate UUID for non-UTF8 header");
}
#[cfg(feature = "security")]
fn create_test_jwt(secret: &[u8], payload: &serde_json::Value) -> String {
use base64::Engine;
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
let header = serde_json::json!({"alg": "HS256", "typ": "JWT"});
let header_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD
.encode(serde_json::to_string(&header).unwrap());
let payload_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD
.encode(serde_json::to_string(payload).unwrap());
let signing_input = format!("{}.{}", header_b64, payload_b64);
let mut mac = Hmac::<Sha256>::new_from_slice(secret).expect("HMAC can take key of any size");
mac.update(signing_input.as_bytes());
let signature = mac.finalize().into_bytes();
let signature_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signature);
format!("{}.{}", signing_input, signature_b64)
}
#[cfg(feature = "security")]
fn build_jwt_test_router(secret: &str) -> Router {
let config = AppConfig {
server: ServerConfig {
host: "127.0.0.1".to_string(),
port: 8080,
request_timeout_secs: 30,
cors: None,
},
authentication: AuthConfig::Jwt {
secret: secret.to_string(),
},
timeout: None,
};
build_with_config(&config).unwrap()
}
#[cfg(feature = "security")]
#[tokio::test]
async fn test_jwt_auth_missing_header_returns_401() {
let router = build_jwt_test_router("MySecureSecret123!@#ABCDEFGHIJKLM");
let response = tower::ServiceExt::oneshot(
router,
axum::http::Request::builder()
.uri("/cov-test-route")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::UNAUTHORIZED);
}
#[cfg(feature = "security")]
#[tokio::test]
async fn test_jwt_auth_non_bearer_token_returns_401() {
let router = build_jwt_test_router("MySecureSecret123!@#ABCDEFGHIJKLM");
let response = tower::ServiceExt::oneshot(
router,
axum::http::Request::builder()
.uri("/cov-test-route")
.header("authorization", "Basic dXNlcjpwYXNz")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::UNAUTHORIZED);
}
#[cfg(feature = "security")]
#[tokio::test]
async fn test_jwt_auth_empty_bearer_token_returns_401() {
let router = build_jwt_test_router("MySecureSecret123!@#ABCDEFGHIJKLM");
let response = tower::ServiceExt::oneshot(
router,
axum::http::Request::builder()
.uri("/cov-test-route")
.header("authorization", "Bearer ")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::UNAUTHORIZED);
}
#[cfg(feature = "security")]
#[tokio::test]
async fn test_jwt_auth_invalid_token_returns_401() {
let router = build_jwt_test_router("MySecureSecret123!@#ABCDEFGHIJKLM");
let response = tower::ServiceExt::oneshot(
router,
axum::http::Request::builder()
.uri("/cov-test-route")
.header("authorization", "Bearer invalid.token.here")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::UNAUTHORIZED);
}
#[cfg(feature = "security")]
#[tokio::test]
async fn test_jwt_auth_valid_token_returns_200() {
let secret = "MySecureSecret123!@#ABCDEFGHIJKLM";
let router = build_jwt_test_router(secret);
let payload = serde_json::json!({
"sub": "test-user",
"permissions": ["read"],
"iat": chrono::Utc::now().timestamp(),
"exp": chrono::Utc::now().timestamp() + 3600
});
let token = create_test_jwt(secret.as_bytes(), &payload);
let response = tower::ServiceExt::oneshot(
router,
axum::http::Request::builder()
.uri("/cov-test-route")
.header("authorization", format!("Bearer {}", token))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::OK);
}
#[cfg(feature = "security")]
fn build_apikey_test_router(header_name: &str, prefix: &str) -> Router {
let config = AppConfig {
server: ServerConfig {
host: "127.0.0.1".to_string(),
port: 8080,
request_timeout_secs: 30,
cors: None,
},
authentication: AuthConfig::ApiKey {
header_name: header_name.to_string(),
prefix: prefix.to_string(),
},
timeout: None,
};
build_with_config(&config).unwrap()
}
#[cfg(feature = "security")]
#[tokio::test]
async fn test_apikey_auth_missing_header_returns_401() {
let router = build_apikey_test_router("X-API-Key", "key-");
let response = tower::ServiceExt::oneshot(
router,
axum::http::Request::builder()
.uri("/cov-test-route")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::UNAUTHORIZED);
}
#[cfg(feature = "security")]
#[tokio::test]
async fn test_apikey_auth_wrong_prefix_returns_401() {
let router = build_apikey_test_router("X-API-Key", "key-");
let response = tower::ServiceExt::oneshot(
router,
axum::http::Request::builder()
.uri("/cov-test-route")
.header("X-API-Key", "Bearer-something")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::UNAUTHORIZED);
}
#[cfg(feature = "security")]
#[tokio::test]
async fn test_apikey_auth_empty_prefix_returns_401() {
let router = build_apikey_test_router("X-API-Key", "");
let response = tower::ServiceExt::oneshot(
router,
axum::http::Request::builder()
.uri("/cov-test-route")
.header("X-API-Key", "some-key-value")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::UNAUTHORIZED);
}
#[cfg(feature = "security")]
#[tokio::test]
async fn test_apikey_auth_unregistered_key_returns_401() {
let router = build_apikey_test_router("X-API-Key", "key-");
let response = tower::ServiceExt::oneshot(
router,
axum::http::Request::builder()
.uri("/cov-test-route")
.header("X-API-Key", "key-unregistered-key-123")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::UNAUTHORIZED);
}
#[cfg(feature = "security")]
#[tokio::test]
async fn test_apikey_auth_with_x_forwarded_for_header() {
let router = build_apikey_test_router("X-API-Key", "key-");
let response = tower::ServiceExt::oneshot(
router,
axum::http::Request::builder()
.uri("/cov-test-route")
.header("X-API-Key", "key-test")
.header("x-forwarded-for", "203.0.113.50")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::UNAUTHORIZED);
}
#[cfg(feature = "security")]
#[tokio::test]
async fn test_apikey_auth_with_x_real_ip_header() {
let router = build_apikey_test_router("X-API-Key", "key-");
let response = tower::ServiceExt::oneshot(
router,
axum::http::Request::builder()
.uri("/cov-test-route")
.header("X-API-Key", "key-test")
.header("x-real-ip", "203.0.113.99")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::UNAUTHORIZED);
}