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::jobs::JobProducerError;
60 use crate::models::health::ComponentStatus;
61 use crate::utils::mocks::mockutils::create_mock_app_state;
62 use actix_web::body::to_bytes;
63 use actix_web::web::ThinData;
64 use std::sync::Arc;
65
66 #[actix_web::test]
71 async fn test_health_returns_ok() {
72 let result = health().await;
73
74 assert!(result.is_ok());
75 let response = result.unwrap();
76 assert_eq!(response.status().as_u16(), 200);
77 }
78
79 #[actix_web::test]
80 async fn test_health_response_body() {
81 let result = health().await;
82
83 let response = result.unwrap();
84 let body = to_bytes(response.into_body()).await.unwrap();
85 assert_eq!(body, "OK");
86 }
87
88 #[actix_web::test]
89 async fn test_health_is_idempotent() {
90 for _ in 0..3 {
92 let result = health().await;
93 assert!(result.is_ok());
94 let response = result.unwrap();
95 assert_eq!(response.status().as_u16(), 200);
96 }
97 }
98
99 #[actix_web::test]
104 async fn test_readiness_returns_503_when_queue_unavailable() {
105 let mut app_state = create_mock_app_state(None, None, None, None, None, None).await;
106
107 Arc::get_mut(&mut app_state.job_producer)
108 .unwrap()
109 .expect_get_queue()
110 .returning(|| {
111 Box::pin(async {
112 Err(JobProducerError::QueueError(
113 "Queue not available".to_string(),
114 ))
115 })
116 });
117
118 let result = readiness(ThinData(app_state)).await;
119
120 assert!(result.is_ok());
121 let response = result.unwrap();
122 assert_eq!(response.status().as_u16(), 503);
123 }
124
125 #[actix_web::test]
126 async fn test_readiness_returns_json_when_unhealthy() {
127 let mut app_state = create_mock_app_state(None, None, None, None, None, None).await;
128
129 Arc::get_mut(&mut app_state.job_producer)
130 .unwrap()
131 .expect_get_queue()
132 .returning(|| {
133 Box::pin(async {
134 Err(JobProducerError::QueueError(
135 "Queue not available".to_string(),
136 ))
137 })
138 });
139
140 let result = readiness(ThinData(app_state)).await;
141 let response = result.unwrap();
142 let body = to_bytes(response.into_body()).await.unwrap();
143 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
144
145 assert_eq!(json["ready"], false);
147 assert_eq!(json["status"], "unhealthy");
148 assert!(json.get("components").is_some());
149 assert!(json.get("timestamp").is_some());
150 }
151
152 #[actix_web::test]
153 async fn test_readiness_includes_reason_when_unhealthy() {
154 let mut app_state = create_mock_app_state(None, None, None, None, None, None).await;
155
156 Arc::get_mut(&mut app_state.job_producer)
157 .unwrap()
158 .expect_get_queue()
159 .returning(|| {
160 Box::pin(async {
161 Err(JobProducerError::QueueError(
162 "Queue not available".to_string(),
163 ))
164 })
165 });
166
167 let result = readiness(ThinData(app_state)).await;
168 let response = result.unwrap();
169 let body = to_bytes(response.into_body()).await.unwrap();
170 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
171
172 assert!(json.get("reason").is_some());
174 let reason = json["reason"].as_str().unwrap();
175 assert!(!reason.is_empty());
176 }
177
178 #[actix_web::test]
179 async fn test_readiness_components_show_unhealthy_state() {
180 let mut app_state = create_mock_app_state(None, None, None, None, None, None).await;
181
182 Arc::get_mut(&mut app_state.job_producer)
183 .unwrap()
184 .expect_get_queue()
185 .returning(|| {
186 Box::pin(async {
187 Err(JobProducerError::QueueError(
188 "Queue not available".to_string(),
189 ))
190 })
191 });
192
193 let result = readiness(ThinData(app_state)).await;
194 let response = result.unwrap();
195 let body = to_bytes(response.into_body()).await.unwrap();
196 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
197
198 let redis_status = json["components"]["redis"]["status"].as_str().unwrap();
200 let queue_status = json["components"]["queue"]["status"].as_str().unwrap();
201
202 assert_eq!(redis_status, "unhealthy");
203 assert_eq!(queue_status, "unhealthy");
204 }
205
206 #[actix_web::test]
207 async fn test_readiness_system_health_still_checked_when_queue_unavailable() {
208 let mut app_state = create_mock_app_state(None, None, None, None, None, None).await;
209
210 Arc::get_mut(&mut app_state.job_producer)
211 .unwrap()
212 .expect_get_queue()
213 .returning(|| {
214 Box::pin(async {
215 Err(JobProducerError::QueueError(
216 "Queue not available".to_string(),
217 ))
218 })
219 });
220
221 let result = readiness(ThinData(app_state)).await;
222 let response = result.unwrap();
223 let body = to_bytes(response.into_body()).await.unwrap();
224 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
225
226 let system = &json["components"]["system"];
228 assert!(system.get("status").is_some());
229 assert!(system.get("fd_count").is_some());
230 assert!(system.get("fd_limit").is_some());
231 assert!(system.get("fd_usage_percent").is_some());
232 assert!(system.get("close_wait_count").is_some());
233
234 let system_status = system["status"].as_str().unwrap();
236 assert!(
237 system_status == "healthy"
238 || system_status == "degraded"
239 || system_status == "unhealthy"
240 );
241 }
242
243 #[actix_web::test]
248 async fn test_readiness_status_code_matches_ready_field() {
249 let mut app_state = create_mock_app_state(None, None, None, None, None, None).await;
250
251 Arc::get_mut(&mut app_state.job_producer)
253 .unwrap()
254 .expect_get_queue()
255 .returning(|| {
256 Box::pin(async { Err(JobProducerError::QueueError("Unavailable".to_string())) })
257 });
258
259 let result = readiness(ThinData(app_state)).await;
260 let response = result.unwrap();
261 let status_code = response.status().as_u16();
262 let body = to_bytes(response.into_body()).await.unwrap();
263 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
264
265 let ready = json["ready"].as_bool().unwrap();
266
267 assert!(!ready);
269 assert_eq!(status_code, 503);
270 }
271
272 #[actix_web::test]
273 async fn test_readiness_timestamp_is_valid_rfc3339() {
274 let mut app_state = create_mock_app_state(None, None, None, None, None, None).await;
275
276 Arc::get_mut(&mut app_state.job_producer)
277 .unwrap()
278 .expect_get_queue()
279 .returning(|| {
280 Box::pin(async { Err(JobProducerError::QueueError("Unavailable".to_string())) })
281 });
282
283 let result = readiness(ThinData(app_state)).await;
284 let response = result.unwrap();
285 let body = to_bytes(response.into_body()).await.unwrap();
286 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
287
288 let timestamp = json["timestamp"].as_str().unwrap();
289 chrono::DateTime::parse_from_rfc3339(timestamp)
291 .expect("Timestamp should be valid RFC3339 format");
292 }
293
294 #[test]
299 fn test_component_status_serializes_to_lowercase() {
300 assert_eq!(
301 serde_json::to_string(&ComponentStatus::Healthy).unwrap(),
302 "\"healthy\""
303 );
304 assert_eq!(
305 serde_json::to_string(&ComponentStatus::Degraded).unwrap(),
306 "\"degraded\""
307 );
308 assert_eq!(
309 serde_json::to_string(&ComponentStatus::Unhealthy).unwrap(),
310 "\"unhealthy\""
311 );
312 }
313}