mithril_aggregator/http_server/routes/artifact_routes/
cardano_database.rs

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