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