mithril_aggregator/http_server/routes/artifact_routes/
cardano_database.rs1use crate::http_server::routes::middlewares;
2use crate::http_server::routes::router::RouterState;
3use warp::Filter;
4
5pub fn routes(
6 router_state: &RouterState,
7) -> impl Filter<Extract = (impl warp::Reply + use<>,), Error = warp::Rejection> + Clone + use<> {
8 artifact_cardano_database_list(router_state)
9 .or(artifact_cardano_database_digest_list(router_state))
10 .or(artifact_cardano_database_by_id(router_state))
11 .or(serve_cardano_database_dir(router_state))
12}
13
14fn artifact_cardano_database_list(
16 router_state: &RouterState,
17) -> impl Filter<Extract = (impl warp::Reply + use<>,), Error = warp::Rejection> + Clone + use<> {
18 warp::path!("artifact" / "cardano-database")
19 .and(warp::get())
20 .and(middlewares::with_logger(router_state))
21 .and(middlewares::with_http_message_service(router_state))
22 .and_then(handlers::list_artifacts)
23}
24
25fn artifact_cardano_database_by_id(
27 dependency_manager: &RouterState,
28) -> impl Filter<Extract = (impl warp::Reply + use<>,), Error = warp::Rejection> + Clone + use<> {
29 warp::path!("artifact" / "cardano-database" / String)
30 .and(warp::get())
31 .and(middlewares::with_client_metadata(dependency_manager))
32 .and(middlewares::with_logger(dependency_manager))
33 .and(middlewares::with_http_message_service(dependency_manager))
34 .and(middlewares::with_metrics_service(dependency_manager))
35 .and_then(handlers::get_artifact_by_signed_entity_id)
36}
37
38fn artifact_cardano_database_digest_list(
40 router_state: &RouterState,
41) -> impl Filter<Extract = (impl warp::Reply + use<>,), Error = warp::Rejection> + Clone + use<> {
42 warp::path!("artifact" / "cardano-database" / "digests")
43 .and(warp::get())
44 .and(middlewares::with_logger(router_state))
45 .and(middlewares::with_http_message_service(router_state))
46 .and_then(handlers::list_digests)
47}
48
49fn serve_cardano_database_dir(
50 router_state: &RouterState,
51) -> impl Filter<Extract = (impl warp::Reply + use<>,), Error = warp::Rejection> + Clone + use<> {
52 warp::path(crate::http_server::CARDANO_DATABASE_DOWNLOAD_PATH)
53 .and(warp::fs::dir(
54 router_state.configuration.cardano_db_artifacts_directory.clone(),
55 ))
56 .and(middlewares::with_logger(router_state))
57 .and(middlewares::extract_config(router_state, |config| {
58 config.allow_http_serve_directory
59 }))
60 .and_then(handlers::ensure_downloaded_file_is_a_cardano_database_artifact)
61}
62
63mod handlers {
64 use slog::{Logger, debug, warn};
65 use std::convert::Infallible;
66 use std::sync::Arc;
67 use warp::http::StatusCode;
68
69 use crate::MetricsService;
70 use crate::http_server::routes::middlewares::ClientMetadata;
71 use crate::http_server::routes::reply;
72 use crate::services::MessageService;
73
74 pub const LIST_MAX_ITEMS: usize = 20;
75
76 pub async fn list_artifacts(
78 logger: Logger,
79 http_message_service: Arc<dyn MessageService>,
80 ) -> Result<impl warp::Reply, Infallible> {
81 match http_message_service
82 .get_cardano_database_list_message(LIST_MAX_ITEMS)
83 .await
84 {
85 Ok(message) => Ok(reply::json(&message, StatusCode::OK)),
86 Err(err) => {
87 warn!(logger,"list_artifacts_cardano_database"; "error" => ?err);
88 Ok(reply::server_error(err))
89 }
90 }
91 }
92
93 pub async fn get_artifact_by_signed_entity_id(
95 signed_entity_id: String,
96 client_metadata: ClientMetadata,
97 logger: Logger,
98 http_message_service: Arc<dyn MessageService>,
99 metrics_service: Arc<MetricsService>,
100 ) -> Result<impl warp::Reply, Infallible> {
101 metrics_service
102 .get_artifact_detail_cardano_database_total_served_since_startup()
103 .increment(&[
104 client_metadata.origin_tag.as_deref().unwrap_or_default(),
105 client_metadata.client_type.as_deref().unwrap_or_default(),
106 ]);
107
108 match http_message_service
109 .get_cardano_database_message(&signed_entity_id)
110 .await
111 {
112 Ok(Some(signed_entity)) => Ok(reply::json(&signed_entity, StatusCode::OK)),
113 Ok(None) => {
114 warn!(logger, "cardano_database_details::not_found");
115 Ok(reply::empty(StatusCode::NOT_FOUND))
116 }
117 Err(err) => {
118 warn!(logger,"cardano_database_details::error"; "error" => ?err);
119 Ok(reply::server_error(err))
120 }
121 }
122 }
123
124 pub async fn ensure_downloaded_file_is_a_cardano_database_artifact(
127 reply: warp::fs::File,
128 logger: Logger,
129 allow_http_serve_directory: bool,
130 ) -> Result<impl warp::Reply, Infallible> {
131 let filepath = reply.path().to_path_buf();
132 debug!(
133 logger,
134 ">> ensure_downloaded_file_is_a_cardano_database / file: `{}`",
135 filepath.display()
136 );
137
138 if !allow_http_serve_directory {
139 warn!(logger, "ensure_downloaded_file_is_a_cardano_database::error"; "error" => "http serve directory is disabled");
140 return Ok(reply::empty(StatusCode::FORBIDDEN));
141 }
142
143 let file_is_a_cardano_database_archive = filepath.to_string_lossy().contains("ancillary")
145 || filepath.to_string_lossy().contains("immutable")
146 || filepath.to_string_lossy().contains("digests");
147 match file_is_a_cardano_database_archive {
148 true => Ok(reply::add_content_disposition_header(reply, &filepath)),
149 false => {
150 warn!(logger,"ensure_downloaded_file_is_a_cardano_database::error"; "error" => "file is not a Cardano database archive");
151 Ok(reply::empty(StatusCode::NOT_FOUND))
152 }
153 }
154 }
155
156 pub async fn list_digests(
158 logger: Logger,
159 http_message_service: Arc<dyn MessageService>,
160 ) -> Result<impl warp::Reply, Infallible> {
161 match http_message_service.get_cardano_database_digest_list_message().await {
162 Ok(message) => Ok(reply::json(&message, StatusCode::OK)),
163 Err(err) => {
164 warn!(logger,"list_digests_cardano_database"; "error" => ?err);
165 Ok(reply::server_error(err))
166 }
167 }
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use serde_json::Value::Null;
174 use std::sync::Arc;
175 use warp::{
176 http::{Method, StatusCode},
177 test::request,
178 };
179
180 use mithril_api_spec::APISpec;
181 use mithril_common::messages::{
182 CardanoDatabaseDigestListItemMessage, CardanoDatabaseSnapshotListItemMessage,
183 CardanoDatabaseSnapshotMessage,
184 };
185 use mithril_common::test::double::Dummy;
186 use mithril_common::{MITHRIL_CLIENT_TYPE_HEADER, MITHRIL_ORIGIN_TAG_HEADER};
187 use mithril_persistence::sqlite::HydrationError;
188
189 use crate::{initialize_dependencies, services::MockMessageService};
190
191 use super::*;
192
193 fn setup_router(
194 state: RouterState,
195 ) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
196 let cors = warp::cors()
197 .allow_any_origin()
198 .allow_headers(vec!["content-type"])
199 .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS]);
200
201 warp::any().and(routes(&state).with(cors))
202 }
203
204 #[tokio::test]
205 async fn test_cardano_database_get_ok() {
206 let mut mock_http_message_service = MockMessageService::new();
207 mock_http_message_service
208 .expect_get_cardano_database_list_message()
209 .return_once(|_| Ok(vec![CardanoDatabaseSnapshotListItemMessage::dummy()]))
210 .once();
211
212 let mut dependency_manager = initialize_dependencies!().await;
213
214 dependency_manager.message_service = Arc::new(mock_http_message_service);
215
216 let method = Method::GET.as_str();
217 let path = "/artifact/cardano-database";
218
219 let response = request()
220 .method(method)
221 .path(path)
222 .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
223 dependency_manager,
224 ))))
225 .await;
226
227 APISpec::verify_conformity(
228 APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
229 method,
230 path,
231 "application/json",
232 &Null,
233 &response,
234 &StatusCode::OK,
235 )
236 .unwrap();
237 }
238
239 #[tokio::test]
240 async fn test_cardano_database_get_ko() {
241 let mut mock_http_message_service = MockMessageService::new();
242 mock_http_message_service
243 .expect_get_cardano_database_list_message()
244 .return_once(|_| Err(HydrationError::InvalidData("invalid data".to_string()).into()))
245 .once();
246 let mut dependency_manager = initialize_dependencies!().await;
247 dependency_manager.message_service = Arc::new(mock_http_message_service);
248
249 let method = Method::GET.as_str();
250 let path = "/artifact/cardano-database";
251
252 let response = request()
253 .method(method)
254 .path(path)
255 .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
256 dependency_manager,
257 ))))
258 .await;
259
260 APISpec::verify_conformity(
261 APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
262 method,
263 path,
264 "application/json",
265 &Null,
266 &response,
267 &StatusCode::INTERNAL_SERVER_ERROR,
268 )
269 .unwrap();
270 }
271
272 #[tokio::test]
273 async fn test_cardano_database_detail_increments_artifact_detail_total_served_since_startup_metric()
274 {
275 let method = Method::GET.as_str();
276 let path = "/artifact/cardano-database/{hash}";
277 let dependency_manager = Arc::new(initialize_dependencies!().await);
278 let initial_counter_value = dependency_manager
279 .metrics_service
280 .get_artifact_detail_cardano_database_total_served_since_startup()
281 .get(&["TEST", "CLI"]);
282
283 request()
284 .method(method)
285 .path(path)
286 .header(MITHRIL_ORIGIN_TAG_HEADER, "TEST")
287 .header(MITHRIL_CLIENT_TYPE_HEADER, "CLI")
288 .reply(&setup_router(RouterState::new_with_origin_tag_white_list(
289 dependency_manager.clone(),
290 &["TEST"],
291 )))
292 .await;
293
294 assert_eq!(
295 initial_counter_value + 1,
296 dependency_manager
297 .metrics_service
298 .get_artifact_detail_cardano_database_total_served_since_startup()
299 .get(&["TEST", "CLI"]),
300 );
301 }
302
303 #[tokio::test]
304 async fn test_cardano_database_detail_get_ok() {
305 let mut mock_http_message_service = MockMessageService::new();
306 mock_http_message_service
307 .expect_get_cardano_database_message()
308 .return_once(|_| Ok(Some(CardanoDatabaseSnapshotMessage::dummy())))
309 .once();
310 let mut dependency_manager = initialize_dependencies!().await;
311 dependency_manager.message_service = Arc::new(mock_http_message_service);
312
313 let method = Method::GET.as_str();
314 let path = "/artifact/cardano-database/{hash}";
315
316 let response = request()
317 .method(method)
318 .path(path)
319 .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
320 dependency_manager,
321 ))))
322 .await;
323
324 APISpec::verify_conformity(
325 APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
326 method,
327 path,
328 "application/json",
329 &Null,
330 &response,
331 &StatusCode::OK,
332 )
333 .unwrap();
334 }
335
336 #[tokio::test]
337 async fn test_cardano_database_detail_returns_404_not_found_when_no_cardano_database_snapshot()
338 {
339 let mut mock_http_message_service = MockMessageService::new();
340 mock_http_message_service
341 .expect_get_cardano_database_message()
342 .return_once(|_| Ok(None))
343 .once();
344 let mut dependency_manager = initialize_dependencies!().await;
345 dependency_manager.message_service = Arc::new(mock_http_message_service);
346
347 let method = Method::GET.as_str();
348 let path = "/artifact/cardano-database/{hash}";
349
350 let response = request()
351 .method(method)
352 .path(path)
353 .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
354 dependency_manager,
355 ))))
356 .await;
357
358 APISpec::verify_conformity(
359 APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
360 method,
361 path,
362 "application/json",
363 &Null,
364 &response,
365 &StatusCode::NOT_FOUND,
366 )
367 .unwrap();
368 }
369
370 #[tokio::test]
371 async fn test_cardano_database_detail_get_ko() {
372 let mut mock_http_message_service = MockMessageService::new();
373 mock_http_message_service
374 .expect_get_cardano_database_message()
375 .return_once(|_| Err(HydrationError::InvalidData("invalid data".to_string()).into()))
376 .once();
377 let mut dependency_manager = initialize_dependencies!().await;
378 dependency_manager.message_service = Arc::new(mock_http_message_service);
379
380 let method = Method::GET.as_str();
381 let path = "/artifact/cardano-database/{hash}";
382
383 let response = request()
384 .method(method)
385 .path(path)
386 .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
387 dependency_manager,
388 ))))
389 .await;
390
391 APISpec::verify_conformity(
392 APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
393 method,
394 path,
395 "application/json",
396 &Null,
397 &response,
398 &StatusCode::INTERNAL_SERVER_ERROR,
399 )
400 .unwrap();
401 }
402
403 #[tokio::test]
404 async fn test_cardano_database_get_digests_ok() {
405 let mut mock_http_message_service = MockMessageService::new();
406 mock_http_message_service
407 .expect_get_cardano_database_digest_list_message()
408 .return_once(|| Ok(vec![CardanoDatabaseDigestListItemMessage::dummy()]))
409 .once();
410 let mut dependency_manager = initialize_dependencies!().await;
411 dependency_manager.message_service = Arc::new(mock_http_message_service);
412
413 let method = Method::GET.as_str();
414 let path = "/artifact/cardano-database/digests";
415
416 let response = request()
417 .method(method)
418 .path(path)
419 .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
420 dependency_manager,
421 ))))
422 .await;
423
424 APISpec::verify_conformity(
425 APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
426 method,
427 path,
428 "application/json",
429 &Null,
430 &response,
431 &StatusCode::OK,
432 )
433 .unwrap();
434 }
435
436 #[tokio::test]
437 async fn test_cardano_database_get_digests_ko() {
438 let mut mock_http_message_service = MockMessageService::new();
439 mock_http_message_service
440 .expect_get_cardano_database_digest_list_message()
441 .return_once(|| Err(HydrationError::InvalidData("invalid data".to_string()).into()))
442 .once();
443 let mut dependency_manager = initialize_dependencies!().await;
444 dependency_manager.message_service = Arc::new(mock_http_message_service);
445
446 let method = Method::GET.as_str();
447 let path = "/artifact/cardano-database/digests";
448
449 let response = request()
450 .method(method)
451 .path(path)
452 .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
453 dependency_manager,
454 ))))
455 .await;
456
457 APISpec::verify_conformity(
458 APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
459 method,
460 path,
461 "application/json",
462 &Null,
463 &response,
464 &StatusCode::INTERNAL_SERVER_ERROR,
465 )
466 .unwrap();
467 }
468}