openzeppelin_relayer/api/routes/
health.rs

1//! This module provides health check and readiness endpoints for the API.
2//!
3//! The `/health` endpoint can be used to verify that the service is running and responsive.
4//! The `/ready` endpoint checks system resources like file descriptors and socket states,
5//! as well as Redis connectivity, queue health, and plugin status.
6
7use actix_web::{get, web, Responder};
8
9use crate::api::controllers::health;
10use crate::models::DefaultAppState;
11
12/// Handles the `/health` endpoint.
13///
14/// Returns an `HttpResponse` with a status of `200 OK` and a body of `"OK"`.
15///
16/// Note: OpenAPI documentation for this endpoint can be found in `docs/health_docs.rs`
17#[get("/health")]
18async fn health_route() -> impl Responder {
19    health::health().await
20}
21
22/// Readiness endpoint that checks system resources, Redis, Queue, and plugins.
23///
24/// Returns 200 OK if the service is ready to accept traffic, or 503 Service Unavailable if not.
25///
26/// Health check results are cached for 10 seconds to prevent excessive load from frequent
27/// health checks (e.g., from AWS ECS or Kubernetes).
28///
29/// Note: OpenAPI documentation for this endpoint can be found in `docs/health_docs.rs`
30#[get("/ready")]
31async fn readiness_route(data: web::ThinData<DefaultAppState>) -> impl Responder {
32    health::readiness(data).await
33}
34
35/// Initializes the health check service.
36///
37/// Registers the `health` and `ready` endpoints with the provided service configuration.
38pub fn init(cfg: &mut web::ServiceConfig) {
39    cfg.service(health_route);
40    cfg.service(readiness_route);
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use actix_web::{http::StatusCode, test, App};
47
48    // =========================================================================
49    // Route Registration Tests
50    // =========================================================================
51
52    #[actix_web::test]
53    async fn test_health_route_registered() {
54        let app = test::init_service(App::new().configure(init)).await;
55
56        let req = test::TestRequest::get().uri("/health").to_request();
57        let resp = test::call_service(&app, req).await;
58
59        assert_eq!(resp.status(), StatusCode::OK);
60        let body = test::read_body(resp).await;
61        assert_eq!(body, "OK");
62    }
63
64    #[actix_web::test]
65    async fn test_ready_route_registered() {
66        let app = test::init_service(App::new().configure(init)).await;
67
68        let req = test::TestRequest::get().uri("/ready").to_request();
69        let resp = test::call_service(&app, req).await;
70
71        // Should not be 404 - endpoint exists
72        assert_ne!(resp.status(), StatusCode::NOT_FOUND);
73    }
74
75    // =========================================================================
76    // HTTP Method Tests
77    // =========================================================================
78
79    #[actix_web::test]
80    async fn test_health_rejects_post() {
81        let app = test::init_service(App::new().configure(init)).await;
82
83        let req = test::TestRequest::post().uri("/health").to_request();
84        let resp = test::call_service(&app, req).await;
85
86        // POST should return 404 (route only accepts GET)
87        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
88    }
89
90    #[actix_web::test]
91    async fn test_ready_rejects_post() {
92        let app = test::init_service(App::new().configure(init)).await;
93
94        let req = test::TestRequest::post().uri("/ready").to_request();
95        let resp = test::call_service(&app, req).await;
96
97        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
98    }
99
100    // =========================================================================
101    // Content Type Tests
102    // =========================================================================
103
104    #[actix_web::test]
105    async fn test_ready_returns_json_content_type() {
106        let app = test::init_service(App::new().configure(init)).await;
107
108        let req = test::TestRequest::get().uri("/ready").to_request();
109        let resp = test::call_service(&app, req).await;
110
111        // Skip content-type check if endpoint errored (500)
112        if resp.status() != StatusCode::INTERNAL_SERVER_ERROR {
113            let content_type = resp.headers().get("content-type");
114            assert!(content_type.is_some(), "Should have content-type header");
115            assert!(
116                content_type
117                    .unwrap()
118                    .to_str()
119                    .unwrap()
120                    .contains("application/json"),
121                "Content-Type should be application/json"
122            );
123        }
124    }
125
126    // =========================================================================
127    // Health Endpoint Stability Tests
128    // =========================================================================
129
130    #[actix_web::test]
131    async fn test_health_is_stable_across_requests() {
132        let app = test::init_service(App::new().configure(init)).await;
133
134        for _ in 0..5 {
135            let req = test::TestRequest::get().uri("/health").to_request();
136            let resp = test::call_service(&app, req).await;
137
138            assert_eq!(resp.status(), StatusCode::OK);
139            let body = test::read_body(resp).await;
140            assert_eq!(body, "OK");
141        }
142    }
143
144    // =========================================================================
145    // Readiness Response Validation Tests
146    // =========================================================================
147
148    #[actix_web::test]
149    async fn test_ready_returns_valid_status_code() {
150        let app = test::init_service(App::new().configure(init)).await;
151
152        let req = test::TestRequest::get().uri("/ready").to_request();
153        let resp = test::call_service(&app, req).await;
154
155        let status = resp.status().as_u16();
156        // Valid status codes are: 200 (ready), 503 (not ready), or 500 (internal error in test)
157        assert!(
158            status == 200 || status == 503 || status == 500,
159            "Status should be 200, 503, or 500, got {}",
160            status
161        );
162    }
163
164    #[actix_web::test]
165    async fn test_ready_response_is_valid_json() {
166        let app = test::init_service(App::new().configure(init)).await;
167
168        let req = test::TestRequest::get().uri("/ready").to_request();
169        let resp = test::call_service(&app, req).await;
170
171        let status = resp.status().as_u16();
172        // Only validate JSON for non-500 responses
173        if status == 200 || status == 503 {
174            let body = test::read_body(resp).await;
175            let body_str = String::from_utf8(body.to_vec()).unwrap();
176            let json: serde_json::Value =
177                serde_json::from_str(&body_str).expect("Response should be valid JSON");
178
179            // Verify required fields exist
180            assert!(json.get("ready").is_some(), "Should have 'ready' field");
181            assert!(json.get("status").is_some(), "Should have 'status' field");
182            assert!(
183                json.get("components").is_some(),
184                "Should have 'components' field"
185            );
186            assert!(
187                json.get("timestamp").is_some(),
188                "Should have 'timestamp' field"
189            );
190        }
191    }
192
193    #[actix_web::test]
194    async fn test_ready_status_code_correlates_with_ready_field() {
195        let app = test::init_service(App::new().configure(init)).await;
196
197        let req = test::TestRequest::get().uri("/ready").to_request();
198        let resp = test::call_service(&app, req).await;
199
200        let status_code = resp.status().as_u16();
201
202        // Only validate for non-500 responses
203        if status_code == 200 || status_code == 503 {
204            let body = test::read_body(resp).await;
205            let body_str = String::from_utf8(body.to_vec()).unwrap();
206            let json: serde_json::Value = serde_json::from_str(&body_str).unwrap();
207
208            let ready = json["ready"].as_bool().unwrap();
209
210            if ready {
211                assert_eq!(status_code, 200, "ready=true should return 200");
212            } else {
213                assert_eq!(status_code, 503, "ready=false should return 503");
214            }
215        }
216    }
217
218    #[actix_web::test]
219    async fn test_ready_components_have_required_fields() {
220        let app = test::init_service(App::new().configure(init)).await;
221
222        let req = test::TestRequest::get().uri("/ready").to_request();
223        let resp = test::call_service(&app, req).await;
224
225        let status = resp.status().as_u16();
226        if status == 200 || status == 503 {
227            let body = test::read_body(resp).await;
228            let body_str = String::from_utf8(body.to_vec()).unwrap();
229            let json: serde_json::Value = serde_json::from_str(&body_str).unwrap();
230
231            let components = &json["components"];
232
233            // System health fields
234            assert!(components.get("system").is_some());
235            let system = &components["system"];
236            assert!(system.get("status").is_some());
237            assert!(system.get("fd_count").is_some());
238            assert!(system.get("fd_limit").is_some());
239
240            // Redis health fields
241            assert!(components.get("redis").is_some());
242            let redis = &components["redis"];
243            assert!(redis.get("status").is_some());
244            assert!(redis.get("primary_pool").is_some());
245            assert!(redis.get("reader_pool").is_some());
246
247            // Queue health fields
248            assert!(components.get("queue").is_some());
249            let queue = &components["queue"];
250            assert!(queue.get("status").is_some());
251        }
252    }
253
254    #[actix_web::test]
255    async fn test_ready_timestamp_is_rfc3339() {
256        let app = test::init_service(App::new().configure(init)).await;
257
258        let req = test::TestRequest::get().uri("/ready").to_request();
259        let resp = test::call_service(&app, req).await;
260
261        let status = resp.status().as_u16();
262        if status == 200 || status == 503 {
263            let body = test::read_body(resp).await;
264            let body_str = String::from_utf8(body.to_vec()).unwrap();
265            let json: serde_json::Value = serde_json::from_str(&body_str).unwrap();
266
267            let timestamp = json["timestamp"].as_str().unwrap();
268            chrono::DateTime::parse_from_rfc3339(timestamp)
269                .expect("Timestamp should be valid RFC3339");
270        }
271    }
272
273    #[actix_web::test]
274    async fn test_ready_status_values_are_valid() {
275        let app = test::init_service(App::new().configure(init)).await;
276
277        let req = test::TestRequest::get().uri("/ready").to_request();
278        let resp = test::call_service(&app, req).await;
279
280        let status = resp.status().as_u16();
281        if status == 200 || status == 503 {
282            let body = test::read_body(resp).await;
283            let body_str = String::from_utf8(body.to_vec()).unwrap();
284            let json: serde_json::Value = serde_json::from_str(&body_str).unwrap();
285
286            let valid_statuses = ["healthy", "degraded", "unhealthy"];
287
288            // Check overall status
289            let overall_status = json["status"].as_str().unwrap();
290            assert!(
291                valid_statuses.contains(&overall_status),
292                "Overall status '{}' should be valid",
293                overall_status
294            );
295
296            // Check component statuses
297            let system_status = json["components"]["system"]["status"].as_str().unwrap();
298            let redis_status = json["components"]["redis"]["status"].as_str().unwrap();
299            let queue_status = json["components"]["queue"]["status"].as_str().unwrap();
300
301            assert!(valid_statuses.contains(&system_status));
302            assert!(valid_statuses.contains(&redis_status));
303            assert!(valid_statuses.contains(&queue_status));
304        }
305    }
306
307    #[actix_web::test]
308    async fn test_ready_plugins_field_is_optional() {
309        let app = test::init_service(App::new().configure(init)).await;
310
311        let req = test::TestRequest::get().uri("/ready").to_request();
312        let resp = test::call_service(&app, req).await;
313
314        let status = resp.status().as_u16();
315        if status == 200 || status == 503 {
316            let body = test::read_body(resp).await;
317            let body_str = String::from_utf8(body.to_vec()).unwrap();
318            let json: serde_json::Value = serde_json::from_str(&body_str).unwrap();
319
320            // Plugins is optional - may or may not be present
321            if let Some(plugins) = json["components"].get("plugins") {
322                if !plugins.is_null() {
323                    // If present, should have required fields
324                    assert!(plugins.get("status").is_some());
325                    assert!(plugins.get("enabled").is_some());
326                }
327            }
328        }
329    }
330}