diff --git a/API/README.md b/API/README.md index 6db9b03..91f5b66 100644 --- a/API/README.md +++ b/API/README.md @@ -8,15 +8,18 @@ Requests that may change the state (PUT & DELETE) will require authenticaiton if In such case the authorization header must be sent with the proper API key. Example: `authorization: ApiKey SECRET_API_KEY` -## GET `/flags/{namespace}` +## GET `/api/flags/{namespace}` Get all the enabled feature flags in a namespace. -## HEAD `/flags/{namespace}` +## HEAD `/api/flags/{namespace}` Check if the feature flags have been updated for a namespace. The response will contain the [etag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) header. If the etag header value is the same as in the most recent GET or HEAD request, the namespace's feature flags have not changed. -## PUT `/flags/{namespace}/{flag}` +## PUT `/api/flags/{namespace}/{flag}` Enable a feature flag in a namespace. -## DELETE `/flags/{namespace}/{flag}` -Disable a feature flag in a namespace. \ No newline at end of file +## DELETE `/api/flags/{namespace}/{flag}` +Disable a feature flag in a namespace. + +## GET `/api/health` +Health check endpoint that can be used to verify the service is running in a healthy state. diff --git a/open_api.yaml b/open_api.yaml index 08ca26f..8d77b1e 100644 --- a/open_api.yaml +++ b/open_api.yaml @@ -80,6 +80,16 @@ paths: '401': description: Unauthorized + /api/health: + get: + summary: Health check endpoint + description: Check the health status of the service and its database connection (if any) + responses: + '200': + description: Service is healthy + '503': + description: Service is unavailable + components: schemas: Response: diff --git a/src/main.rs b/src/main.rs index 414b79d..7649094 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,7 +31,7 @@ async fn main() { api_key: cfg.api_key(), }; let router = Router::new() - .route("/health", get(health_check)) + .route("/api/health", get(health_check)) .route("/api/flags/:namespace", get(get_ns).head(head_ns)) .route("/api/flags/:namespace/:flag", put(put_flag).delete(delete_flag)) .with_state(state); diff --git a/tests/health_check.rs b/tests/health_check.rs deleted file mode 100644 index 6e1e2f0..0000000 --- a/tests/health_check.rs +++ /dev/null @@ -1,101 +0,0 @@ -use axum::Router; -use axum::body::Body; -use axum::http::{Request, StatusCode}; -use axum::routing::get; -use std::sync::{Arc, RwLock}; -use tower::ServiceExt; - -// Import the Database trait -use flagpole::db::Database; - -#[cfg(not(feature = "redis"))] -use flagpole::db::mem::InMemoryDb; - -#[cfg(feature = "redis")] -use flagpole::db::redis::RedisDb; - -#[derive(Clone)] -struct AppState -where - T: Database, -{ - db: Arc>, - api_key: Option, -} - -async fn health_check_handler( - axum::extract::State(state): axum::extract::State>, -) -> StatusCode { - let db = state.db.read().unwrap(); - match db.health_check() { - Ok(_) => StatusCode::OK, - Err(_) => StatusCode::SERVICE_UNAVAILABLE, - } -} - -#[cfg(not(feature = "redis"))] -#[tokio::test] -async fn test_health_check_in_memory() { - let state = AppState { - db: Arc::new(RwLock::new(InMemoryDb::new())), - api_key: None, - }; - - let app = Router::new().route("/health", get(health_check_handler)).with_state(state); - - let response = app - .oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap()) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); -} - -#[cfg(feature = "redis")] -#[tokio::test] -async fn test_health_check_redis_not_connected() { - // Use an invalid Redis URI to simulate connection failure - let state = AppState { - db: Arc::new(RwLock::new(RedisDb::new("redis://invalid-host:6379".to_string()))), - api_key: None, - }; - - let app = Router::new().route("/health", get(health_check_handler)).with_state(state); - - let response = app - .oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap()) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); -} - -#[cfg(feature = "redis")] -#[tokio::test] -async fn test_health_check_redis_connected() { - // This test requires a running Redis instance at localhost:6379 - // Skip if Redis is not available - let redis_uri = - std::env::var("REDIS_URI").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string()); - - let state = AppState { - db: Arc::new(RwLock::new(RedisDb::new(redis_uri))), - api_key: None, - }; - - let app = Router::new().route("/health", get(health_check_handler)).with_state(state); - - let response = app - .oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap()) - .await - .unwrap(); - - // This will pass if Redis is available, otherwise it will fail - // In a real CI/CD setup, you'd ensure Redis is running or skip this test - let status = response.status(); - assert!( - status == StatusCode::OK || status == StatusCode::SERVICE_UNAVAILABLE, - "Expected OK or SERVICE_UNAVAILABLE, got {:?}", - status - ); -}