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
15fn 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
26fn 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
41fn 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
54fn 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 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 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 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 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 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 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}