From 80a6537e16ccef5071034e83aef51ccbc49501a7 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 13 Apr 2026 12:41:40 +0200 Subject: [PATCH] feat: Add health check endpoint Introduce a new health check endpoint at `/api/health`. This endpoint returns a JSON response indicating the server's status and configuration details, such as the port, data path, and configured URLs for external services. This change also refactors the application setup by: - Creating a `lib.rs` file to house the `create_router` function, making it reusable for both the main application and integration tests. - Moving configuration loading logic into `main.rs` and passing it as a shared state (`Arc`) to the router. - Adding a unit test (`health_test.rs`) to verify the functionality of the health check endpoint. - Updating `Cargo.toml` and `Cargo.lock` to include the `tower` dependency, which is used for testing. --- server/Cargo.lock | 1 + server/Cargo.toml | 3 ++ server/src/lib.rs | 14 +++++++ server/src/main.rs | 16 +++++--- server/src/routes/health.rs | 18 +++++++++ server/src/routes/mod.rs | 11 ++++++ server/tests/health_test.rs | 73 +++++++++++++++++++++++++++++++++++++ 7 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 server/src/lib.rs create mode 100644 server/src/routes/health.rs create mode 100644 server/src/routes/mod.rs create mode 100644 server/tests/health_test.rs diff --git a/server/Cargo.lock b/server/Cargo.lock index 067cc72..711ef31 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -348,6 +348,7 @@ dependencies = [ "serde_json", "tokio", "toml", + "tower", "tower-http 0.5.2", "tracing", "tracing-appender", diff --git a/server/Cargo.toml b/server/Cargo.toml index 23f502a..a89077b 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -20,3 +20,6 @@ tower-http = { version = "0.5", features = ["limit", "trace"] } rand = "0.8" bcrypt = "0.15" axum-extra = { version = "0.9", features = ["cookie"] } + +[dev-dependencies] +tower = { version = "0.5", features = ["util"] } diff --git a/server/src/lib.rs b/server/src/lib.rs new file mode 100644 index 0000000..a68194b --- /dev/null +++ b/server/src/lib.rs @@ -0,0 +1,14 @@ +pub mod config; +pub mod error; +pub mod routes; + +use std::sync::Arc; + +use axum::Router; + +use config::Config; + +/// Build the application router. Used by main.rs and integration tests. +pub fn create_router(config: Arc) -> Router { + routes::api_router().with_state(config) +} diff --git a/server/src/main.rs b/server/src/main.rs index af57347..e9ef357 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,12 +1,18 @@ -mod config; -mod error; - -use config::Config; use std::sync::Arc; +use tokio::net::TcpListener; + +use doctate_server::config::Config; + #[tokio::main] async fn main() { dotenvy::dotenv().ok(); let config = Arc::new(Config::from_env()); - println!("Server starting on port {}", config.server_port); + + let addr = format!("0.0.0.0:{}", config.server_port); + println!("Server starting on {addr}"); + + let app = doctate_server::create_router(config); + let listener = TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); } diff --git a/server/src/routes/health.rs b/server/src/routes/health.rs new file mode 100644 index 0000000..37d95d8 --- /dev/null +++ b/server/src/routes/health.rs @@ -0,0 +1,18 @@ +use std::sync::Arc; + +use axum::extract::State; +use axum::Json; +use serde_json::{json, Value}; + +use crate::config::Config; + +pub async fn handle_health(State(config): State>) -> Json { + Json(json!({ + "status": "ok", + "port": config.server_port, + "data_path": config.data_path.to_string_lossy(), + "users": config.users.iter().map(|u| &u.slug).collect::>(), + "whisper_url": config.whisper_url, + "ollama_url": config.ollama_url, + })) +} diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs new file mode 100644 index 0000000..ce121b9 --- /dev/null +++ b/server/src/routes/mod.rs @@ -0,0 +1,11 @@ +mod health; + +use std::sync::Arc; + +use axum::Router; + +use crate::config::Config; + +pub fn api_router() -> Router> { + Router::new().route("/api/health", axum::routing::get(health::handle_health)) +} diff --git a/server/tests/health_test.rs b/server/tests/health_test.rs new file mode 100644 index 0000000..6b78979 --- /dev/null +++ b/server/tests/health_test.rs @@ -0,0 +1,73 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use axum::body::Body; +use axum::http::{Request, StatusCode}; +use tower::util::ServiceExt; + +use doctate_server::config::Config; + +fn test_config() -> Arc { + Arc::new(Config { + server_port: 3000, + data_path: "/tmp/doctate-test".into(), + log_level: "info".into(), + log_path: "/tmp/doctate-test/logs".into(), + log_max_days: 90, + users: vec![], + api_keys: HashMap::new(), + retention_audio_days: 30, + retention_transcript_days: 30, + retention_document_days: 0, + whisper_url: "http://localhost:10300".into(), + whisper_timeout_seconds: 120, + ollama_url: "http://localhost:11434".into(), + ollama_model: "gemma3:4b".into(), + ollama_keep_alive: 0, + llm_url: String::new(), + llm_api_key: String::new(), + llm_model: String::new(), + llm_temperature: 0.0, + session_timeout_hours: 8, + }) +} + +#[tokio::test] +async fn health_returns_ok() { + let app = doctate_server::create_router(test_config()); + + let response = app + .oneshot( + Request::builder() + .uri("/api/health") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); +} + +#[tokio::test] +async fn health_returns_json_with_status() { + let app = doctate_server::create_router(test_config()); + + let response = app + .oneshot( + Request::builder() + .uri("/api/health") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + + assert_eq!(json["status"], "ok"); + assert_eq!(json["port"], 3000); +}