1use axum::extract::State;
7use axum::http::StatusCode;
8use axum::response::IntoResponse;
9use axum::Json;
10
11use crate::state::AppState;
12
13pub async fn healthz() -> Json<serde_json::Value> {
15 Json(serde_json::json!({ "ok": true }))
16}
17
18pub async fn readyz(State(state): State<AppState>) -> impl IntoResponse {
20 match state.db.ping().await {
21 Ok(()) => {
22 let metrics = state.identity.metrics();
23 (
24 StatusCode::OK,
25 Json(serde_json::json!({
26 "ok": true,
27 "database": "connected",
28 "database_backend": state.db.backend(),
29 "identity_cache": {
30 "hits": metrics.hits,
31 "misses": metrics.misses,
32 "entries": metrics.entry_count,
33 },
34 })),
35 )
36 }
37 Err(e) => {
38 tracing::error!(error = %e, "readiness check failed: database unreachable");
39 (
40 StatusCode::SERVICE_UNAVAILABLE,
41 Json(serde_json::json!({
42 "ok": false,
43 "database": "unreachable",
44 "error": "database connectivity check failed",
45 })),
46 )
47 }
48 }
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54 use crate::config::{AppConfig, AuthConfig, Config, DatabaseConfig};
55 use axum::body::Body;
56 use axum::routing::get;
57 use axum::Router;
58 use http_body_util::BodyExt;
59 use hyper::Request;
60 use std::sync::Arc;
61 use tower::ServiceExt;
62
63 async fn test_state() -> AppState {
64 let db = atrg_db::connect("sqlite::memory:").await.unwrap();
65 atrg_db::run_internal_migrations(&db).await.unwrap();
66
67 let config = Config {
68 app: AppConfig {
69 name: "test-app".into(),
70 host: "127.0.0.1".into(),
71 port: 3000,
72 secret_key: "test-secret-key-for-health-tests".into(),
73 cors_origins: vec![],
74 environment: "development".into(),
75 admin_dids: vec![],
76 },
77 auth: AuthConfig {
78 client_id: "http://localhost:3000/client-metadata.json".into(),
79 redirect_uri: "http://localhost:3000/auth/callback".into(),
80 scope: "atproto transition:generic".into(),
81 post_login_redirect: "/".into(),
82 },
83 database: DatabaseConfig {
84 url: "sqlite::memory:".into(),
85 },
86 jetstream: None,
87 firehose: None,
88 feed_generator: None,
89 labeler: None,
90 rate_limit: None,
91 };
92
93 AppState {
94 config: Arc::new(config),
95 db,
96 http: reqwest::Client::new(),
97 identity: Arc::new(atrg_identity::IdentityResolver::with_defaults(
98 reqwest::Client::new(),
99 )),
100 extensions: Arc::new(crate::state::Extensions::new()),
101 }
102 }
103
104 #[tokio::test]
105 async fn healthz_returns_200_ok() {
106 let app: Router = Router::new().route("/healthz", get(healthz));
107
108 let req = Request::builder()
109 .uri("/healthz")
110 .body(Body::empty())
111 .unwrap();
112
113 let resp = app.oneshot(req).await.unwrap();
114 assert_eq!(resp.status(), StatusCode::OK);
115
116 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
117 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
118 assert_eq!(body["ok"], true);
119 }
120
121 #[tokio::test]
122 async fn readyz_returns_200_when_db_connected() {
123 let state = test_state().await;
124 let app: Router = Router::new()
125 .route("/readyz", get(readyz))
126 .with_state(state);
127
128 let req = Request::builder()
129 .uri("/readyz")
130 .body(Body::empty())
131 .unwrap();
132
133 let resp = app.oneshot(req).await.unwrap();
134 assert_eq!(resp.status(), StatusCode::OK);
135
136 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
137 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
138 assert_eq!(body["ok"], true);
139 assert_eq!(body["database"], "connected");
140 assert!(body["identity_cache"].is_object());
141 }
142}