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_list_by_epoch(router_state))
10        .or(artifact_cardano_database_digest_list(router_state))
11        .or(artifact_cardano_database_by_id(router_state))
12        .or(serve_cardano_database_dir(router_state))
13}
14
15/// GET /artifact/cardano-database
16fn artifact_cardano_database_list(
17    router_state: &RouterState,
18) -> impl Filter<Extract = (impl warp::Reply + use<>,), Error = warp::Rejection> + Clone + use<> {
19    warp::path!("artifact" / "cardano-database")
20        .and(warp::get())
21        .and(middlewares::with_logger(router_state))
22        .and(middlewares::with_http_message_service(router_state))
23        .and_then(handlers::list_artifacts)
24}
25
26/// GET /artifact/cardano-database/epoch/:epoch
27fn artifact_cardano_database_list_by_epoch(
28    router_state: &RouterState,
29) -> impl Filter<Extract = (impl warp::Reply + use<>,), Error = warp::Rejection> + Clone + use<> {
30    warp::path!("artifact" / "cardano-database" / "epoch" / String)
31        .and(warp::get())
32        .and(middlewares::with_logger(router_state))
33        .and(middlewares::with_epoch_service(router_state))
34        .and(middlewares::with_http_message_service(router_state))
35        .and(middlewares::extract_config(router_state, |config| {
36            config.max_artifact_epoch_offset
37        }))
38        .and_then(handlers::list_artifacts_by_epoch)
39}
40
41/// GET /artifact/cardano-database/:id
42fn artifact_cardano_database_by_id(
43    dependency_manager: &RouterState,
44) -> impl Filter<Extract = (impl warp::Reply + use<>,), Error = warp::Rejection> + Clone + use<> {
45    warp::path!("artifact" / "cardano-database" / String)
46        .and(warp::get())
47        .and(middlewares::with_client_metadata(dependency_manager))
48        .and(middlewares::with_logger(dependency_manager))
49        .and(middlewares::with_http_message_service(dependency_manager))
50        .and(middlewares::with_metrics_service(dependency_manager))
51        .and_then(handlers::get_artifact_by_signed_entity_id)
52}
53
54/// GET /artifact/cardano-database/digests
55fn artifact_cardano_database_digest_list(
56    router_state: &RouterState,
57) -> impl Filter<Extract = (impl warp::Reply + use<>,), Error = warp::Rejection> + Clone + use<> {
58    warp::path!("artifact" / "cardano-database" / "digests")
59        .and(warp::get())
60        .and(middlewares::with_logger(router_state))
61        .and(middlewares::with_http_message_service(router_state))
62        .and_then(handlers::list_digests)
63}
64
65fn serve_cardano_database_dir(
66    router_state: &RouterState,
67) -> impl Filter<Extract = (impl warp::Reply + use<>,), Error = warp::Rejection> + Clone + use<> {
68    warp::path(crate::http_server::CARDANO_DATABASE_DOWNLOAD_PATH)
69        .and(warp::fs::dir(
70            router_state.configuration.cardano_db_artifacts_directory.clone(),
71        ))
72        .and(middlewares::with_logger(router_state))
73        .and(middlewares::extract_config(router_state, |config| {
74            config.allow_http_serve_directory
75        }))
76        .and_then(handlers::ensure_downloaded_file_is_a_cardano_database_artifact)
77}
78
79mod handlers {
80    use slog::{Logger, debug, warn};
81    use std::convert::Infallible;
82    use std::sync::Arc;
83    use warp::http::StatusCode;
84
85    use crate::MetricsService;
86    use crate::dependency_injection::EpochServiceWrapper;
87    use crate::http_server::{parameters, routes::middlewares::ClientMetadata, routes::reply};
88    use crate::services::MessageService;
89
90    pub const LIST_MAX_ITEMS: usize = 20;
91
92    /// List artifacts
93    pub async fn list_artifacts(
94        logger: Logger,
95        http_message_service: Arc<dyn MessageService>,
96    ) -> Result<impl warp::Reply, Infallible> {
97        match http_message_service
98            .get_cardano_database_list_message(LIST_MAX_ITEMS)
99            .await
100        {
101            Ok(message) => Ok(reply::json(&message, StatusCode::OK)),
102            Err(err) => {
103                warn!(logger,"list_artifacts_cardano_database"; "error" => ?err);
104                Ok(reply::server_error(err))
105            }
106        }
107    }
108
109    /// List artifacts for an epoch
110    pub async fn list_artifacts_by_epoch(
111        epoch: String,
112        logger: Logger,
113        epoch_service: EpochServiceWrapper,
114        http_message_service: Arc<dyn MessageService>,
115        max_artifact_epoch_offset: u64,
116    ) -> Result<impl warp::Reply, Infallible> {
117        let expanded_epoch = match parameters::expand_epoch(&epoch, epoch_service).await {
118            Ok(epoch) => epoch,
119            Err(err) => {
120                warn!(logger,"list_by_epoch_artifacts_cardano_database::invalid_epoch"; "error" => ?err);
121                return Ok(reply::bad_request(
122                    "invalid_epoch".to_string(),
123                    err.to_string(),
124                ));
125            }
126        };
127
128        if expanded_epoch.has_offset_greater_than(max_artifact_epoch_offset) {
129            return Ok(reply::bad_request(
130                "invalid_epoch".to_string(),
131                format!(
132                    "offset greater than maximum allowed value: epoch:`{epoch}`, max offset:`{max_artifact_epoch_offset}`"
133                ),
134            ));
135        }
136
137        match http_message_service
138            .get_cardano_database_list_message_by_epoch(LIST_MAX_ITEMS, *expanded_epoch)
139            .await
140        {
141            Ok(message) => Ok(reply::json(&message, StatusCode::OK)),
142            Err(err) => {
143                warn!(logger,"list_by_epoch_artifacts_cardano_database"; "error" => ?err);
144                Ok(reply::server_error(err))
145            }
146        }
147    }
148
149    /// Get artifact by signed entity id
150    pub async fn get_artifact_by_signed_entity_id(
151        signed_entity_id: String,
152        client_metadata: ClientMetadata,
153        logger: Logger,
154        http_message_service: Arc<dyn MessageService>,
155        metrics_service: Arc<MetricsService>,
156    ) -> Result<impl warp::Reply, Infallible> {
157        metrics_service
158            .get_artifact_detail_cardano_database_total_served_since_startup()
159            .increment(&[
160                client_metadata.origin_tag.as_deref().unwrap_or_default(),
161                client_metadata.client_type.as_deref().unwrap_or_default(),
162            ]);
163
164        match http_message_service
165            .get_cardano_database_message(&signed_entity_id)
166            .await
167        {
168            Ok(Some(signed_entity)) => Ok(reply::json(&signed_entity, StatusCode::OK)),
169            Ok(None) => {
170                warn!(logger, "cardano_database_details::not_found");
171                Ok(reply::empty(StatusCode::NOT_FOUND))
172            }
173            Err(err) => {
174                warn!(logger,"cardano_database_details::error"; "error" => ?err);
175                Ok(reply::server_error(err))
176            }
177        }
178    }
179
180    /// Download a file if it's a Cardano_database artifact file
181    // TODO: this function should probable be unit tested once the file naming convention is defined
182    pub async fn ensure_downloaded_file_is_a_cardano_database_artifact(
183        reply: warp::fs::File,
184        logger: Logger,
185        allow_http_serve_directory: bool,
186    ) -> Result<impl warp::Reply, Infallible> {
187        let filepath = reply.path().to_path_buf();
188        debug!(
189            logger,
190            ">> ensure_downloaded_file_is_a_cardano_database / file: `{}`",
191            filepath.display()
192        );
193
194        if !allow_http_serve_directory {
195            warn!(logger, "ensure_downloaded_file_is_a_cardano_database::error"; "error" => "http serve directory is disabled");
196            return Ok(reply::empty(StatusCode::FORBIDDEN));
197        }
198
199        // TODO: enhance this check with a regular expression once the file naming convention is defined
200        let file_is_a_cardano_database_archive = filepath.to_string_lossy().contains("ancillary")
201            || filepath.to_string_lossy().contains("immutable")
202            || filepath.to_string_lossy().contains("digests");
203        match file_is_a_cardano_database_archive {
204            true => Ok(reply::add_content_disposition_header(reply, &filepath)),
205            false => {
206                warn!(logger,"ensure_downloaded_file_is_a_cardano_database::error"; "error" => "file is not a Cardano database archive");
207                Ok(reply::empty(StatusCode::NOT_FOUND))
208            }
209        }
210    }
211
212    /// List digests
213    pub async fn list_digests(
214        logger: Logger,
215        http_message_service: Arc<dyn MessageService>,
216    ) -> Result<impl warp::Reply, Infallible> {
217        match http_message_service.get_cardano_database_digest_list_message().await {
218            Ok(message) => Ok(reply::json(&message, StatusCode::OK)),
219            Err(err) => {
220                warn!(logger,"list_digests_cardano_database"; "error" => ?err);
221                Ok(reply::server_error(err))
222            }
223        }
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use mockall::predicate::{always, eq};
230    use serde_json::Value::Null;
231    use std::sync::Arc;
232    use tokio::sync::RwLock;
233    use warp::{
234        http::{Method, StatusCode},
235        test::request,
236    };
237
238    use mithril_api_spec::APISpec;
239    use mithril_common::entities::Epoch;
240    use mithril_common::messages::{
241        CardanoDatabaseDigestListItemMessage, CardanoDatabaseSnapshotListItemMessage,
242        CardanoDatabaseSnapshotMessage,
243    };
244    use mithril_common::test::double::Dummy;
245    use mithril_common::{MITHRIL_CLIENT_TYPE_HEADER, MITHRIL_ORIGIN_TAG_HEADER};
246    use mithril_persistence::sqlite::HydrationError;
247
248    use crate::{
249        http_server::routes::router::RouterConfig,
250        initialize_dependencies,
251        services::{FakeEpochServiceBuilder, MockMessageService},
252    };
253
254    use super::*;
255
256    fn setup_router(
257        state: RouterState,
258    ) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
259        let cors = warp::cors()
260            .allow_any_origin()
261            .allow_headers(vec!["content-type"])
262            .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS]);
263
264        warp::any().and(routes(&state).with(cors))
265    }
266
267    #[tokio::test]
268    async fn test_cardano_database_get_ok() {
269        let mut mock_http_message_service = MockMessageService::new();
270        mock_http_message_service
271            .expect_get_cardano_database_list_message()
272            .return_once(|_| Ok(vec![CardanoDatabaseSnapshotListItemMessage::dummy()]))
273            .once();
274
275        let mut dependency_manager = initialize_dependencies!().await;
276
277        dependency_manager.message_service = Arc::new(mock_http_message_service);
278
279        let method = Method::GET.as_str();
280        let path = "/artifact/cardano-database";
281
282        let response = request()
283            .method(method)
284            .path(path)
285            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
286                dependency_manager,
287            ))))
288            .await;
289
290        APISpec::verify_conformity(
291            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
292            method,
293            path,
294            "application/json",
295            &Null,
296            &response,
297            &StatusCode::OK,
298        )
299        .unwrap();
300    }
301
302    #[tokio::test]
303    async fn test_cardano_database_get_ko() {
304        let mut mock_http_message_service = MockMessageService::new();
305        mock_http_message_service
306            .expect_get_cardano_database_list_message()
307            .return_once(|_| Err(HydrationError::InvalidData("invalid data".to_string()).into()))
308            .once();
309        let mut dependency_manager = initialize_dependencies!().await;
310        dependency_manager.message_service = Arc::new(mock_http_message_service);
311
312        let method = Method::GET.as_str();
313        let path = "/artifact/cardano-database";
314
315        let response = request()
316            .method(method)
317            .path(path)
318            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
319                dependency_manager,
320            ))))
321            .await;
322
323        APISpec::verify_conformity(
324            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
325            method,
326            path,
327            "application/json",
328            &Null,
329            &response,
330            &StatusCode::INTERNAL_SERVER_ERROR,
331        )
332        .unwrap();
333    }
334
335    #[tokio::test]
336    async fn test_cardano_database_get_by_epoch_ok() {
337        let epoch_service = FakeEpochServiceBuilder::dummy(Epoch(100)).build();
338        let mut mock_http_message_service = MockMessageService::new();
339        mock_http_message_service
340            .expect_get_cardano_database_list_message_by_epoch()
341            .with(always(), eq(&Epoch(85)))
342            .return_once(|_, _| Ok(vec![CardanoDatabaseSnapshotListItemMessage::dummy()]))
343            .once();
344
345        let mut dependency_manager = initialize_dependencies!().await;
346        dependency_manager.epoch_service = Arc::new(RwLock::new(epoch_service));
347        dependency_manager.message_service = Arc::new(mock_http_message_service);
348
349        let method = Method::GET.as_str();
350        let base_path = "/artifact/cardano-database/epoch";
351
352        let response = request()
353            .method(method)
354            .path(&format!("{base_path}/latest-15"))
355            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
356                dependency_manager,
357            ))))
358            .await;
359
360        APISpec::verify_conformity(
361            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
362            method,
363            &format!("{base_path}/{{epoch}}"),
364            "application/json",
365            &Null,
366            &response,
367            &StatusCode::OK,
368        )
369        .unwrap();
370    }
371
372    #[tokio::test]
373    async fn test_cardano_database_get_by_epoch_reject_query_with_offset_greater_than_max_configured()
374     {
375        let epoch_service = FakeEpochServiceBuilder::dummy(Epoch(100)).build();
376        let mut mock_http_message_service = MockMessageService::new();
377        mock_http_message_service
378            .expect_get_cardano_database_list_message_by_epoch()
379            .returning(|_, _| Ok(vec![CardanoDatabaseSnapshotListItemMessage::dummy()]));
380
381        let mut dependency_manager = initialize_dependencies!().await;
382        dependency_manager.epoch_service = Arc::new(RwLock::new(epoch_service));
383        dependency_manager.message_service = Arc::new(mock_http_message_service);
384
385        let router = setup_router(RouterState::new(
386            Arc::new(dependency_manager),
387            RouterConfig {
388                max_artifact_epoch_offset: 10,
389                ..RouterConfig::dummy()
390            },
391        ));
392
393        let method = Method::GET.as_str();
394        let base_path = "/artifact/cardano-database/epoch";
395
396        let response = request()
397            .method(method)
398            .path(&format!("{base_path}/latest-10"))
399            .reply(&router)
400            .await;
401
402        assert_eq!(response.status(), StatusCode::OK);
403
404        let response = request()
405            .method(method)
406            .path(&format!("{base_path}/latest-11"))
407            .reply(&router)
408            .await;
409
410        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
411    }
412
413    #[tokio::test]
414    async fn test_cardano_database_get_by_epoch_ko() {
415        let mut mock_http_message_service = MockMessageService::new();
416        mock_http_message_service
417            .expect_get_cardano_database_list_message_by_epoch()
418            .return_once(|_, _| Err(HydrationError::InvalidData("invalid data".to_string()).into()))
419            .once();
420
421        let mut dependency_manager = initialize_dependencies!().await;
422        dependency_manager.message_service = Arc::new(mock_http_message_service);
423
424        let method = Method::GET.as_str();
425        let base_path = "/artifact/cardano-database/epoch";
426
427        let response = request()
428            .method(method)
429            .path(&format!("{base_path}/120"))
430            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
431                dependency_manager,
432            ))))
433            .await;
434
435        APISpec::verify_conformity(
436            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
437            method,
438            &format!("{base_path}/{{epoch}}"),
439            "application/json",
440            &Null,
441            &response,
442            &StatusCode::INTERNAL_SERVER_ERROR,
443        )
444        .unwrap();
445    }
446
447    #[tokio::test]
448    async fn test_cardano_database_detail_increments_artifact_detail_total_served_since_startup_metric()
449     {
450        let method = Method::GET.as_str();
451        let path = "/artifact/cardano-database/{hash}";
452        let dependency_manager = Arc::new(initialize_dependencies!().await);
453        let initial_counter_value = dependency_manager
454            .metrics_service
455            .get_artifact_detail_cardano_database_total_served_since_startup()
456            .get(&["TEST", "CLI"]);
457
458        request()
459            .method(method)
460            .path(path)
461            .header(MITHRIL_ORIGIN_TAG_HEADER, "TEST")
462            .header(MITHRIL_CLIENT_TYPE_HEADER, "CLI")
463            .reply(&setup_router(RouterState::new_with_origin_tag_white_list(
464                dependency_manager.clone(),
465                &["TEST"],
466            )))
467            .await;
468
469        assert_eq!(
470            initial_counter_value + 1,
471            dependency_manager
472                .metrics_service
473                .get_artifact_detail_cardano_database_total_served_since_startup()
474                .get(&["TEST", "CLI"]),
475        );
476    }
477
478    #[tokio::test]
479    async fn test_cardano_database_detail_get_ok() {
480        let mut mock_http_message_service = MockMessageService::new();
481        mock_http_message_service
482            .expect_get_cardano_database_message()
483            .return_once(|_| Ok(Some(CardanoDatabaseSnapshotMessage::dummy())))
484            .once();
485        let mut dependency_manager = initialize_dependencies!().await;
486        dependency_manager.message_service = Arc::new(mock_http_message_service);
487
488        let method = Method::GET.as_str();
489        let path = "/artifact/cardano-database/{hash}";
490
491        let response = request()
492            .method(method)
493            .path(path)
494            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
495                dependency_manager,
496            ))))
497            .await;
498
499        APISpec::verify_conformity(
500            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
501            method,
502            path,
503            "application/json",
504            &Null,
505            &response,
506            &StatusCode::OK,
507        )
508        .unwrap();
509    }
510
511    #[tokio::test]
512    async fn test_cardano_database_detail_returns_404_not_found_when_no_cardano_database_snapshot()
513    {
514        let mut mock_http_message_service = MockMessageService::new();
515        mock_http_message_service
516            .expect_get_cardano_database_message()
517            .return_once(|_| Ok(None))
518            .once();
519        let mut dependency_manager = initialize_dependencies!().await;
520        dependency_manager.message_service = Arc::new(mock_http_message_service);
521
522        let method = Method::GET.as_str();
523        let path = "/artifact/cardano-database/{hash}";
524
525        let response = request()
526            .method(method)
527            .path(path)
528            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
529                dependency_manager,
530            ))))
531            .await;
532
533        APISpec::verify_conformity(
534            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
535            method,
536            path,
537            "application/json",
538            &Null,
539            &response,
540            &StatusCode::NOT_FOUND,
541        )
542        .unwrap();
543    }
544
545    #[tokio::test]
546    async fn test_cardano_database_detail_get_ko() {
547        let mut mock_http_message_service = MockMessageService::new();
548        mock_http_message_service
549            .expect_get_cardano_database_message()
550            .return_once(|_| Err(HydrationError::InvalidData("invalid data".to_string()).into()))
551            .once();
552        let mut dependency_manager = initialize_dependencies!().await;
553        dependency_manager.message_service = Arc::new(mock_http_message_service);
554
555        let method = Method::GET.as_str();
556        let path = "/artifact/cardano-database/{hash}";
557
558        let response = request()
559            .method(method)
560            .path(path)
561            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
562                dependency_manager,
563            ))))
564            .await;
565
566        APISpec::verify_conformity(
567            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
568            method,
569            path,
570            "application/json",
571            &Null,
572            &response,
573            &StatusCode::INTERNAL_SERVER_ERROR,
574        )
575        .unwrap();
576    }
577
578    #[tokio::test]
579    async fn test_cardano_database_get_digests_ok() {
580        let mut mock_http_message_service = MockMessageService::new();
581        mock_http_message_service
582            .expect_get_cardano_database_digest_list_message()
583            .return_once(|| Ok(vec![CardanoDatabaseDigestListItemMessage::dummy()]))
584            .once();
585        let mut dependency_manager = initialize_dependencies!().await;
586        dependency_manager.message_service = Arc::new(mock_http_message_service);
587
588        let method = Method::GET.as_str();
589        let path = "/artifact/cardano-database/digests";
590
591        let response = request()
592            .method(method)
593            .path(path)
594            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
595                dependency_manager,
596            ))))
597            .await;
598
599        APISpec::verify_conformity(
600            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
601            method,
602            path,
603            "application/json",
604            &Null,
605            &response,
606            &StatusCode::OK,
607        )
608        .unwrap();
609    }
610
611    #[tokio::test]
612    async fn test_cardano_database_get_digests_ko() {
613        let mut mock_http_message_service = MockMessageService::new();
614        mock_http_message_service
615            .expect_get_cardano_database_digest_list_message()
616            .return_once(|| Err(HydrationError::InvalidData("invalid data".to_string()).into()))
617            .once();
618        let mut dependency_manager = initialize_dependencies!().await;
619        dependency_manager.message_service = Arc::new(mock_http_message_service);
620
621        let method = Method::GET.as_str();
622        let path = "/artifact/cardano-database/digests";
623
624        let response = request()
625            .method(method)
626            .path(path)
627            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
628                dependency_manager,
629            ))))
630            .await;
631
632        APISpec::verify_conformity(
633            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
634            method,
635            path,
636            "application/json",
637            &Null,
638            &response,
639            &StatusCode::INTERNAL_SERVER_ERROR,
640        )
641        .unwrap();
642    }
643}