1use axum::{
2 Json, Router,
3 extract::State,
4 http::StatusCode,
5 response::{IntoResponse, Response},
6 routing::{get, post},
7};
8use serde_json::json;
9use sqlx::PgPool;
10use tower_http::trace::TraceLayer;
11
12use crate::ingest;
13
14#[derive(Clone)]
15pub struct AppState {
16 pub pool: PgPool,
17}
18
19pub fn router(pool: PgPool) -> Router {
20 let api_v1 = Router::new().route("/days", post(ingest::upload_day));
24
25 Router::new()
26 .route("/health", get(health))
27 .nest("/api/v1", api_v1)
28 .with_state(AppState { pool })
29 .layer(TraceLayer::new_for_http())
30}
31
32async fn health(State(state): State<AppState>) -> Response {
35 match sqlx::query("SELECT 1").execute(&state.pool).await {
36 Ok(_) => (
37 StatusCode::OK,
38 Json(json!({
39 "status": "ok",
40 "version": env!("CARGO_PKG_VERSION"),
41 "database": "ok",
42 })),
43 )
44 .into_response(),
45 Err(error) => {
46 tracing::error!(%error, "health check: database unreachable");
47 (
48 StatusCode::SERVICE_UNAVAILABLE,
49 Json(json!({
50 "status": "degraded",
51 "version": env!("CARGO_PKG_VERSION"),
52 "database": "unavailable",
53 })),
54 )
55 .into_response()
56 }
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use axum::{body::Body, http::Request};
63 use http_body_util::BodyExt;
64 use sqlx::postgres::PgPoolOptions;
65 use tower::ServiceExt;
66
67 use super::*;
68
69 fn dead_pool() -> PgPool {
72 PgPoolOptions::new()
73 .acquire_timeout(std::time::Duration::from_secs(1))
75 .connect_lazy("postgres://nobody:nowhere@127.0.0.1:1/kasl")
76 .expect("lazy pool creation does not touch the network")
77 }
78
79 #[tokio::test]
80 async fn health_reports_degraded_without_a_database() {
81 let response = router(dead_pool()).oneshot(Request::get("/health").body(Body::empty()).unwrap()).await.unwrap();
82 assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
83
84 let body = response.into_body().collect().await.unwrap().to_bytes();
85 let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
86 assert_eq!(body["status"], "degraded");
87 assert_eq!(body["database"], "unavailable");
88 assert_eq!(body["version"], env!("CARGO_PKG_VERSION"));
89 }
90
91 #[tokio::test]
92 async fn unknown_routes_return_404() {
93 let response = router(dead_pool()).oneshot(Request::get("/nope").body(Body::empty()).unwrap()).await.unwrap();
94 assert_eq!(response.status(), StatusCode::NOT_FOUND);
95 }
96}