use crate::state::AppState;
use axum::extract::State;
use axum::response::IntoResponse;
pub async fn handler(State(state): State<AppState>) -> impl IntoResponse {
crate::probe::readiness_response(&state)
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
fn test_state() -> AppState {
AppState {
extensions: std::sync::Arc::new(
std::sync::Mutex::new(std::collections::HashMap::new()),
),
#[cfg(feature = "db")]
pool: None,
profile: Some("dev".into()),
started_at: std::time::Instant::now(),
health_detailed: true,
probes: crate::probe::ProbeState::ready_for_test(),
metrics: crate::middleware::MetricsCollector::new(),
log_levels: crate::actuator::LogLevels::new("info"),
task_registry: crate::actuator::TaskRegistry::new(),
config_props: crate::actuator::ConfigProperties::default(),
#[cfg(feature = "ws")]
channels: crate::channels::Channels::new(32),
#[cfg(feature = "ws")]
shutdown: tokio_util::sync::CancellationToken::new(),
}
}
#[tokio::test]
async fn health_no_database_returns_ok() -> Result<(), Box<dyn std::error::Error>> {
let app = axum::Router::new()
.route("/health", axum::routing::get(handler))
.with_state(test_state());
let response = app
.oneshot(Request::builder().uri("/health").body(Body::empty())?)
.await?;
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await?;
let json: serde_json::Value = serde_json::from_slice(&body)?;
assert_eq!(json["status"], "ok");
assert!(json["version"].is_string());
assert_eq!(json["profile"], "dev");
assert!(json["uptime"].is_string());
assert!(json.get("pool").is_none());
Ok(())
}
#[tokio::test]
async fn health_no_database_returns_json_content_type() -> Result<(), Box<dyn std::error::Error>>
{
let app = axum::Router::new()
.route("/health", axum::routing::get(handler))
.with_state(test_state());
let response = app
.oneshot(Request::builder().uri("/health").body(Body::empty())?)
.await?;
let content_type = response
.headers()
.get("content-type")
.ok_or("missing content type")?
.to_str()?;
assert!(
content_type.contains("application/json"),
"Expected application/json, got {content_type}"
);
Ok(())
}
#[cfg(feature = "db")]
#[tokio::test]
async fn health_with_pool_returns_pool_status() -> Result<(), Box<dyn std::error::Error>> {
let config = crate::config::DatabaseConfig {
url: Some("postgres://localhost/test".into()),
pool_size: 5,
..Default::default()
};
let pool = crate::db::create_pool(&config)?.ok_or("no pool created")?;
let app = axum::Router::new()
.route("/health", axum::routing::get(handler))
.with_state(AppState {
extensions: std::sync::Arc::new(std::sync::Mutex::new(
std::collections::HashMap::new(),
)),
pool: Some(pool),
profile: Some("prod".into()),
started_at: std::time::Instant::now(),
health_detailed: true,
probes: crate::probe::ProbeState::ready_for_test(),
metrics: crate::middleware::MetricsCollector::new(),
log_levels: crate::actuator::LogLevels::new("info"),
task_registry: crate::actuator::TaskRegistry::new(),
config_props: crate::actuator::ConfigProperties::default(),
#[cfg(feature = "ws")]
channels: crate::channels::Channels::new(32),
#[cfg(feature = "ws")]
shutdown: tokio_util::sync::CancellationToken::new(),
});
let response = app
.oneshot(Request::builder().uri("/health").body(Body::empty())?)
.await?;
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await?;
let json: serde_json::Value = serde_json::from_slice(&body)?;
assert_eq!(json["status"], "ok");
assert!(json["version"].is_string());
assert_eq!(json["pool"]["size"], 5);
assert!(json["pool"]["available"].is_number());
Ok(())
}
#[tokio::test]
async fn health_response_includes_version() -> Result<(), Box<dyn std::error::Error>> {
let app = axum::Router::new()
.route("/health", axum::routing::get(handler))
.with_state(test_state());
let response = app
.oneshot(Request::builder().uri("/health").body(Body::empty())?)
.await?;
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await?;
let json: serde_json::Value = serde_json::from_slice(&body)?;
assert_eq!(json["version"], env!("CARGO_PKG_VERSION"));
Ok(())
}
#[tokio::test]
async fn health_detailed_false_omits_details() -> Result<(), Box<dyn std::error::Error>> {
let mut state = test_state();
state.health_detailed = false;
let app = axum::Router::new()
.route("/health", axum::routing::get(handler))
.with_state(state);
let response = app
.oneshot(Request::builder().uri("/health").body(Body::empty())?)
.await?;
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await?;
let json: serde_json::Value = serde_json::from_slice(&body)?;
assert_eq!(json["status"], "ok");
assert!(json.get("version").is_none());
assert!(json.get("profile").is_none());
assert!(json.get("uptime").is_none());
assert!(json.get("pool").is_none());
Ok(())
}
}