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 {status}"
160        );
161    }
162
163    #[actix_web::test]
164    async fn test_ready_response_is_valid_json() {
165        let app = test::init_service(App::new().configure(init)).await;
166
167        let req = test::TestRequest::get().uri("/ready").to_request();
168        let resp = test::call_service(&app, req).await;
169
170        let status = resp.status().as_u16();
171        // Only validate JSON for non-500 responses
172        if status == 200 || status == 503 {
173            let body = test::read_body(resp).await;
174            let body_str = String::from_utf8(body.to_vec()).unwrap();
175            let json: serde_json::Value =
176                serde_json::from_str(&body_str).expect("Response should be valid JSON");
177
178            // Verify required fields exist
179            assert!(json.get("ready").is_some(), "Should have 'ready' field");
180            assert!(json.get("status").is_some(), "Should have 'status' field");
181            assert!(
182                json.get("components").is_some(),
183                "Should have 'components' field"
184            );
185            assert!(
186                json.get("timestamp").is_some(),
187                "Should have 'timestamp' field"
188            );
189        }
190    }
191
192    #[actix_web::test]
193    async fn test_ready_status_code_correlates_with_ready_field() {
194        let app = test::init_service(App::new().configure(init)).await;
195
196        let req = test::TestRequest::get().uri("/ready").to_request();
197        let resp = test::call_service(&app, req).await;
198
199        let status_code = resp.status().as_u16();
200
201        // Only validate for non-500 responses
202        if status_code == 200 || status_code == 503 {
203            let body = test::read_body(resp).await;
204            let body_str = String::from_utf8(body.to_vec()).unwrap();
205            let json: serde_json::Value = serde_json::from_str(&body_str).unwrap();
206
207            let ready = json["ready"].as_bool().unwrap();
208
209            if ready {
210                assert_eq!(status_code, 200, "ready=true should return 200");
211            } else {
212                assert_eq!(status_code, 503, "ready=false should return 503");
213            }
214        }
215    }
216
217    #[actix_web::test]
218    async fn test_ready_components_have_required_fields() {
219        let app = test::init_service(App::new().configure(init)).await;
220
221        let req = test::TestRequest::get().uri("/ready").to_request();
222        let resp = test::call_service(&app, req).await;
223
224        let status = resp.status().as_u16();
225        if status == 200 || status == 503 {
226            let body = test::read_body(resp).await;
227            let body_str = String::from_utf8(body.to_vec()).unwrap();
228            let json: serde_json::Value = serde_json::from_str(&body_str).unwrap();
229
230            let components = &json["components"];
231
232            // System health fields
233            assert!(components.get("system").is_some());
234            let system = &components["system"];
235            assert!(system.get("status").is_some());
236            assert!(system.get("fd_count").is_some());
237            assert!(system.get("fd_limit").is_some());
238
239            // Redis health fields
240            assert!(components.get("redis").is_some());
241            let redis = &components["redis"];
242            assert!(redis.get("status").is_some());
243            assert!(redis.get("primary_pool").is_some());
244            assert!(redis.get("reader_pool").is_some());
245
246            // Queue health fields
247            assert!(components.get("queue").is_some());
248            let queue = &components["queue"];
249            assert!(queue.get("status").is_some());
250        }
251    }
252
253    #[actix_web::test]
254    async fn test_ready_timestamp_is_rfc3339() {
255        let app = test::init_service(App::new().configure(init)).await;
256
257        let req = test::TestRequest::get().uri("/ready").to_request();
258        let resp = test::call_service(&app, req).await;
259
260        let status = resp.status().as_u16();
261        if status == 200 || status == 503 {
262            let body = test::read_body(resp).await;
263            let body_str = String::from_utf8(body.to_vec()).unwrap();
264            let json: serde_json::Value = serde_json::from_str(&body_str).unwrap();
265
266            let timestamp = json["timestamp"].as_str().unwrap();
267            chrono::DateTime::parse_from_rfc3339(timestamp)
268                .expect("Timestamp should be valid RFC3339");
269        }
270    }
271
272    #[actix_web::test]
273    async fn test_ready_status_values_are_valid() {
274        let app = test::init_service(App::new().configure(init)).await;
275
276        let req = test::TestRequest::get().uri("/ready").to_request();
277        let resp = test::call_service(&app, req).await;
278
279        let status = resp.status().as_u16();
280        if status == 200 || status == 503 {
281            let body = test::read_body(resp).await;
282            let body_str = String::from_utf8(body.to_vec()).unwrap();
283            let json: serde_json::Value = serde_json::from_str(&body_str).unwrap();
284
285            let valid_statuses = ["healthy", "degraded", "unhealthy"];
286
287            // Check overall status
288            let overall_status = json["status"].as_str().unwrap();
289            assert!(
290                valid_statuses.contains(&overall_status),
291                "Overall status '{overall_status}' should be valid"
292            );
293
294            // Check component statuses
295            let system_status = json["components"]["system"]["status"].as_str().unwrap();
296            let redis_status = json["components"]["redis"]["status"].as_str().unwrap();
297            let queue_status = json["components"]["queue"]["status"].as_str().unwrap();
298
299            assert!(valid_statuses.contains(&system_status));
300            assert!(valid_statuses.contains(&redis_status));
301            assert!(valid_statuses.contains(&queue_status));
302        }
303    }
304
305    #[actix_web::test]
306    async fn test_ready_plugins_field_is_optional() {
307        let app = test::init_service(App::new().configure(init)).await;
308
309        let req = test::TestRequest::get().uri("/ready").to_request();
310        let resp = test::call_service(&app, req).await;
311
312        let status = resp.status().as_u16();
313        if status == 200 || status == 503 {
314            let body = test::read_body(resp).await;
315            let body_str = String::from_utf8(body.to_vec()).unwrap();
316            let json: serde_json::Value = serde_json::from_str(&body_str).unwrap();
317
318            // Plugins is optional - may or may not be present
319            if let Some(plugins) = json["components"].get("plugins") {
320                if !plugins.is_null() {
321                    // If present, should have required fields
322                    assert!(plugins.get("status").is_some());
323                    assert!(plugins.get("enabled").is_some());
324                }
325            }
326        }
327    }
328}