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 + 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
14/// GET /artifact/cardano-database
15fn 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
25/// GET /artifact/cardano-database/:id
26fn 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
38/// GET /artifact/cardano-database/digests
39fn 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    /// 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        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    /// Download a file if it's a Cardano_database artifact file
125    // TODO: this function should probable be unit tested once the file naming convention is defined
126    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        // TODO: enhance this check with a regular expression once the file naming convention is defined
144        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    /// List digests
157    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}