use std::sync::Arc;
use http::{Request, Response, StatusCode, header::CONTENT_TYPE};
use hyper::body::Incoming;
use crate::{
extract::PathParams,
health::config::HealthRegistry,
response::{APPLICATION_JSON, BoxBody},
state::AppState,
};
pub async fn liveness_check(
_req: Request<Incoming>,
_params: PathParams,
_state: Arc<AppState>,
) -> Response<BoxBody> {
let body = serde_json::json!({ "status": "ok" });
Response::builder()
.status(StatusCode::OK)
.header(CONTENT_TYPE, APPLICATION_JSON)
.body(http_body_util::Full::new(bytes::Bytes::from(
serde_json::to_vec(&body).unwrap_or_default(),
)))
.unwrap()
}
pub async fn readiness_check(
_req: Request<Incoming>,
_params: PathParams,
state: Arc<AppState>,
) -> Response<BoxBody> {
let mut checks = serde_json::Map::new();
let mut all_ok = true;
#[cfg(feature = "database")]
{
use sea_orm::{ConnectionTrait, Statement};
if let Some(conn) = state.get::<sea_orm::DatabaseConnection>() {
let backend = conn.get_database_backend();
let db_ok = conn
.execute(Statement::from_string(backend, "SELECT 1"))
.await
.is_ok();
checks.insert(
"db".to_string(),
if db_ok { "ok".into() } else { "error".into() },
);
if !db_ok {
all_ok = false;
}
}
}
if let Some(registry) = state.get::<HealthRegistry>() {
for (name, check_fn) in ®istry.checks {
let ok = check_fn().await;
checks.insert(
name.to_string(),
if ok { "ok".into() } else { "error".into() },
);
if !ok {
all_ok = false;
}
}
}
let mut body = serde_json::json!({ "status": if all_ok { "ok" } else { "error" } });
if !checks.is_empty() {
body["checks"] = serde_json::Value::Object(checks);
}
let status = if all_ok {
StatusCode::OK
} else {
StatusCode::SERVICE_UNAVAILABLE
};
Response::builder()
.status(status)
.header(CONTENT_TYPE, APPLICATION_JSON)
.body(http_body_util::Full::new(bytes::Bytes::from(
serde_json::to_vec(&body).unwrap_or_default(),
)))
.unwrap()
}
pub async fn health_check(
req: Request<Incoming>,
params: PathParams,
state: Arc<AppState>,
) -> Response<BoxBody> {
readiness_check(req, params, state).await
}
#[cfg(test)]
mod tests {
use http::{HeaderValue, StatusCode};
use serde_json::Value;
use crate::{app::Rapina, testing::TestClient};
#[tokio::test]
async fn test_liveness_check_always_returns_200() {
let app = Rapina::new().with_health_check(true);
let client = TestClient::new(app).await;
let response = client.get("/__rapina/health/live").send().await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.json::<Value>()["status"], "ok");
}
#[tokio::test]
async fn test_readiness_check_returns_200_when_no_checks() {
let app = Rapina::new().with_health_check(true);
let client = TestClient::new(app).await;
let response = client.get("/__rapina/health/ready").send().await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.json::<Value>()["status"], "ok");
}
#[tokio::test]
async fn test_readiness_check_returns_503_when_custom_check_fails() {
let app = Rapina::new()
.with_health_check(true)
.add_health_check("redis", || async { false });
let client = TestClient::new(app).await;
let response = client.get("/__rapina/health/ready").send().await;
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
}
#[tokio::test]
async fn test_health_check_returns_200_with_json_content_type() {
let app = Rapina::new().with_health_check(true);
let client = TestClient::new(app).await;
let response = client.get("/__rapina/health").send().await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers().get(http::header::CONTENT_TYPE),
Some(&HeaderValue::from_static("application/json"))
);
}
#[tokio::test]
async fn test_health_check_returns_status_ok() {
let app = Rapina::new().with_health_check(true);
let client = TestClient::new(app).await;
let json = client.get("/__rapina/health").send().await.json::<Value>();
assert_eq!(json["status"], "ok");
}
#[tokio::test]
async fn test_health_check_returns_404_when_disabled() {
let app = Rapina::new().with_health_check(false);
let client = TestClient::new(app).await;
let response = client.get("/__rapina/health").send().await;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_health_check_with_passing_custom_check_returns_200() {
let app = Rapina::new()
.with_health_check(true)
.add_health_check("redis", || async { true });
let client = TestClient::new(app).await;
let json = client.get("/__rapina/health").send().await.json::<Value>();
assert_eq!(json["status"], "ok");
assert_eq!(json["checks"]["redis"], "ok");
}
#[tokio::test]
async fn test_health_check_with_failing_custom_check_returns_503() {
let app = Rapina::new()
.with_health_check(true)
.add_health_check("redis", || async { false });
let client = TestClient::new(app).await;
let response = client.get("/__rapina/health").send().await;
let status = response.status();
let json = response.json::<Value>();
assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(json["status"], "error");
assert_eq!(json["checks"]["redis"], "error");
}
#[tokio::test]
async fn test_health_check_with_mixed_custom_checks_returns_503() {
let app = Rapina::new()
.with_health_check(true)
.add_health_check("redis", || async { true })
.add_health_check("stripe", || async { false });
let client = TestClient::new(app).await;
let response = client.get("/__rapina/health").send().await;
let status = response.status();
let json = response.json::<Value>();
assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(json["status"], "error");
assert_eq!(json["checks"]["redis"], "ok");
assert_eq!(json["checks"]["stripe"], "error");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_health_check_with_database_returns_db_ok() {
use crate::database::DatabaseConfig;
let app = Rapina::new()
.with_health_check(true)
.with_database(DatabaseConfig::new("sqlite::memory:"))
.await
.unwrap();
let client = TestClient::new(app).await;
let json = client.get("/__rapina/health").send().await.json::<Value>();
assert_eq!(json["status"], "ok");
assert_eq!(json["checks"]["db"], "ok");
}
}