openzeppelin_relayer/api/controllers/
health.rs1use actix_web::HttpResponse;
7
8use crate::jobs::JobProducerTrait;
9use crate::models::ThinDataAppState;
10use crate::models::{
11 NetworkRepoModel, NotificationRepoModel, RelayerRepoModel, SignerRepoModel,
12 TransactionRepoModel,
13};
14use crate::repositories::{
15 ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository, Repository,
16 TransactionCounterTrait, TransactionRepository,
17};
18use crate::services::health::get_readiness;
19
20pub async fn health() -> Result<HttpResponse, actix_web::Error> {
24 Ok(HttpResponse::Ok().body("OK"))
25}
26
27pub async fn readiness<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
34 data: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
35) -> Result<HttpResponse, actix_web::Error>
36where
37 J: JobProducerTrait + Send + Sync + 'static,
38 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
39 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
40 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
41 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
42 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
43 TCR: TransactionCounterTrait + Send + Sync + 'static,
44 PR: PluginRepositoryTrait + Send + Sync + 'static,
45 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
46{
47 let response = get_readiness(data).await;
48
49 if response.ready {
50 Ok(HttpResponse::Ok().json(response))
51 } else {
52 Ok(HttpResponse::ServiceUnavailable().json(response))
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use crate::models::health::ComponentStatus;
60 use crate::utils::mocks::mockutils::create_mock_app_state;
61 use actix_web::body::to_bytes;
62 use actix_web::web::ThinData;
63 use std::sync::Arc;
64
65 #[actix_web::test]
70 async fn test_health_returns_ok() {
71 let result = health().await;
72
73 assert!(result.is_ok());
74 let response = result.unwrap();
75 assert_eq!(response.status().as_u16(), 200);
76 }
77
78 #[actix_web::test]
79 async fn test_health_response_body() {
80 let result = health().await;
81
82 let response = result.unwrap();
83 let body = to_bytes(response.into_body()).await.unwrap();
84 assert_eq!(body, "OK");
85 }
86
87 #[actix_web::test]
88 async fn test_health_is_idempotent() {
89 for _ in 0..3 {
91 let result = health().await;
92 assert!(result.is_ok());
93 let response = result.unwrap();
94 assert_eq!(response.status().as_u16(), 200);
95 }
96 }
97
98 #[actix_web::test]
103 async fn test_readiness_returns_503_when_queue_unavailable() {
104 let mut app_state = create_mock_app_state(None, None, None, None, None, None).await;
105
106 Arc::get_mut(&mut app_state.job_producer)
107 .unwrap()
108 .expect_get_queue_backend()
109 .return_const(None);
110
111 let result = readiness(ThinData(app_state)).await;
112
113 assert!(result.is_ok());
114 let response = result.unwrap();
115 assert_eq!(response.status().as_u16(), 503);
116 }
117
118 #[actix_web::test]
119 async fn test_readiness_returns_json_when_unhealthy() {
120 let mut app_state = create_mock_app_state(None, None, None, None, None, None).await;
121
122 Arc::get_mut(&mut app_state.job_producer)
123 .unwrap()
124 .expect_get_queue_backend()
125 .return_const(None);
126
127 let result = readiness(ThinData(app_state)).await;
128 let response = result.unwrap();
129 let body = to_bytes(response.into_body()).await.unwrap();
130 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
131
132 assert_eq!(json["ready"], false);
134 assert_eq!(json["status"], "unhealthy");
135 assert!(json.get("components").is_some());
136 assert!(json.get("timestamp").is_some());
137 }
138
139 #[actix_web::test]
140 async fn test_readiness_includes_reason_when_unhealthy() {
141 let mut app_state = create_mock_app_state(None, None, None, None, None, None).await;
142
143 Arc::get_mut(&mut app_state.job_producer)
144 .unwrap()
145 .expect_get_queue_backend()
146 .return_const(None);
147
148 let result = readiness(ThinData(app_state)).await;
149 let response = result.unwrap();
150 let body = to_bytes(response.into_body()).await.unwrap();
151 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
152
153 assert!(json.get("reason").is_some());
155 let reason = json["reason"].as_str().unwrap();
156 assert!(!reason.is_empty());
157 }
158
159 #[actix_web::test]
160 async fn test_readiness_components_show_unhealthy_state() {
161 let mut app_state = create_mock_app_state(None, None, None, None, None, None).await;
162
163 Arc::get_mut(&mut app_state.job_producer)
164 .unwrap()
165 .expect_get_queue_backend()
166 .return_const(None);
167
168 let result = readiness(ThinData(app_state)).await;
169 let response = result.unwrap();
170 let body = to_bytes(response.into_body()).await.unwrap();
171 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
172
173 let redis_status = json["components"]["redis"]["status"].as_str().unwrap();
176 let queue_status = json["components"]["queue"]["status"].as_str().unwrap();
177
178 assert_eq!(redis_status, "healthy");
179 assert_eq!(queue_status, "unhealthy");
180 }
181
182 #[actix_web::test]
183 async fn test_readiness_system_health_still_checked_when_queue_unavailable() {
184 let mut app_state = create_mock_app_state(None, None, None, None, None, None).await;
185
186 Arc::get_mut(&mut app_state.job_producer)
187 .unwrap()
188 .expect_get_queue_backend()
189 .return_const(None);
190
191 let result = readiness(ThinData(app_state)).await;
192 let response = result.unwrap();
193 let body = to_bytes(response.into_body()).await.unwrap();
194 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
195
196 let system = &json["components"]["system"];
198 assert!(system.get("status").is_some());
199 assert!(system.get("fd_count").is_some());
200 assert!(system.get("fd_limit").is_some());
201 assert!(system.get("fd_usage_percent").is_some());
202 assert!(system.get("close_wait_count").is_some());
203
204 let system_status = system["status"].as_str().unwrap();
206 assert!(
207 system_status == "healthy"
208 || system_status == "degraded"
209 || system_status == "unhealthy"
210 );
211 }
212
213 #[actix_web::test]
218 async fn test_readiness_status_code_matches_ready_field() {
219 let mut app_state = create_mock_app_state(None, None, None, None, None, None).await;
220
221 Arc::get_mut(&mut app_state.job_producer)
223 .unwrap()
224 .expect_get_queue_backend()
225 .return_const(None);
226
227 let result = readiness(ThinData(app_state)).await;
228 let response = result.unwrap();
229 let status_code = response.status().as_u16();
230 let body = to_bytes(response.into_body()).await.unwrap();
231 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
232
233 let ready = json["ready"].as_bool().unwrap();
234
235 assert!(!ready);
237 assert_eq!(status_code, 503);
238 }
239
240 #[actix_web::test]
241 async fn test_readiness_timestamp_is_valid_rfc3339() {
242 let mut app_state = create_mock_app_state(None, None, None, None, None, None).await;
243
244 Arc::get_mut(&mut app_state.job_producer)
245 .unwrap()
246 .expect_get_queue_backend()
247 .return_const(None);
248
249 let result = readiness(ThinData(app_state)).await;
250 let response = result.unwrap();
251 let body = to_bytes(response.into_body()).await.unwrap();
252 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
253
254 let timestamp = json["timestamp"].as_str().unwrap();
255 chrono::DateTime::parse_from_rfc3339(timestamp)
257 .expect("Timestamp should be valid RFC3339 format");
258 }
259
260 #[test]
265 fn test_component_status_serializes_to_lowercase() {
266 assert_eq!(
267 serde_json::to_string(&ComponentStatus::Healthy).unwrap(),
268 "\"healthy\""
269 );
270 assert_eq!(
271 serde_json::to_string(&ComponentStatus::Degraded).unwrap(),
272 "\"degraded\""
273 );
274 assert_eq!(
275 serde_json::to_string(&ComponentStatus::Unhealthy).unwrap(),
276 "\"unhealthy\""
277 );
278 }
279}