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