pub mod health;
pub mod scenarios;
use axum::{routing::get, Router};
use crate::state::AppState;
pub fn router(state: AppState) -> Router {
Router::new()
.route("/health", get(health::health))
.route(
"/scenarios",
get(scenarios::list_scenarios).post(scenarios::post_scenario),
)
.route(
"/scenarios/{id}",
get(scenarios::get_scenario).delete(scenarios::delete_scenario),
)
.route("/scenarios/{id}/stats", get(scenarios::get_scenario_stats))
.route(
"/scenarios/{id}/metrics",
get(scenarios::get_scenario_metrics),
)
.with_state(state)
}
#[cfg(test)]
mod tests {
use super::*;
use http_body_util::BodyExt;
use hyper::{Request, StatusCode};
use tower::ServiceExt;
fn test_router() -> Router {
router(AppState::new())
}
#[tokio::test]
async fn get_health_returns_200() {
let app = test_router();
let request = Request::builder()
.uri("/health")
.body(axum::body::Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(
response.status(),
StatusCode::OK,
"GET /health must return 200 OK"
);
}
#[tokio::test]
async fn get_health_returns_status_ok_json() {
let app = test_router();
let request = Request::builder()
.uri("/health")
.body(axum::body::Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
let body_bytes = response.into_body().collect().await.unwrap().to_bytes();
let body: serde_json::Value =
serde_json::from_slice(&body_bytes).expect("body must be valid JSON");
assert_eq!(
body,
serde_json::json!({ "status": "ok" }),
"GET /health must return {{\"status\": \"ok\"}}"
);
}
#[tokio::test]
async fn get_health_sets_json_content_type() {
let app = test_router();
let request = Request::builder()
.uri("/health")
.body(axum::body::Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
let ct = response
.headers()
.get("content-type")
.expect("response must have Content-Type header")
.to_str()
.unwrap();
assert!(
ct.contains("application/json"),
"Content-Type must be application/json, got: {ct}"
);
}
#[tokio::test]
async fn unknown_route_returns_404() {
let app = test_router();
let request = Request::builder()
.uri("/nonexistent")
.body(axum::body::Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(
response.status(),
StatusCode::NOT_FOUND,
"unknown route must return 404"
);
}
#[tokio::test]
async fn post_health_returns_405() {
let app = test_router();
let request = Request::builder()
.method("POST")
.uri("/health")
.body(axum::body::Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(
response.status(),
StatusCode::METHOD_NOT_ALLOWED,
"POST /health must return 405 Method Not Allowed"
);
}
#[tokio::test]
async fn deeply_nested_unknown_path_returns_404() {
let app = test_router();
let request = Request::builder()
.uri("/a/b/c/d/e/f")
.body(axum::body::Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(
response.status(),
StatusCode::NOT_FOUND,
"deeply nested unknown path must return 404"
);
}
}