mithril_aggregator/http_server/routes/
signer_routes.rs

1use anyhow::Context;
2use slog::warn;
3use warp::Filter;
4
5use mithril_common::StdResult;
6use mithril_common::entities::Epoch;
7
8use crate::dependency_injection::EpochServiceWrapper;
9use crate::http_server::routes::middlewares;
10use crate::http_server::routes::router::RouterState;
11
12const MITHRIL_SIGNER_VERSION_HEADER: &str = "signer-node-version";
13
14pub fn routes(
15    router_state: &RouterState,
16) -> impl Filter<Extract = (impl warp::Reply + use<>,), Error = warp::Rejection> + Clone + use<> {
17    register_signer(router_state)
18        .or(registered_signers(router_state))
19        .or(signers_tickers(router_state))
20}
21
22/// POST /register-signer
23fn register_signer(
24    router_state: &RouterState,
25) -> impl Filter<Extract = (impl warp::Reply + use<>,), Error = warp::Rejection> + Clone + use<> {
26    warp::path!("register-signer")
27        .and(warp::post())
28        .and(middlewares::with_origin_tag(router_state))
29        .and(warp::header::optional::<String>(
30            MITHRIL_SIGNER_VERSION_HEADER,
31        ))
32        .and(warp::body::json())
33        .and(middlewares::with_logger(router_state))
34        .and(middlewares::with_signer_registerer(router_state))
35        .and(middlewares::with_event_transmitter(router_state))
36        .and(middlewares::with_epoch_service(router_state))
37        .and(middlewares::with_metrics_service(router_state))
38        .and_then(handlers::register_signer)
39}
40
41/// Get /signers/tickers
42fn signers_tickers(
43    router_state: &RouterState,
44) -> impl Filter<Extract = (impl warp::Reply + use<>,), Error = warp::Rejection> + Clone + use<> {
45    warp::path!("signers" / "tickers")
46        .and(warp::get())
47        .and(middlewares::with_logger(router_state))
48        .and(middlewares::extract_config(router_state, |config| {
49            config.network.to_string()
50        }))
51        .and(middlewares::with_signer_getter(router_state))
52        .and_then(handlers::signers_tickers)
53}
54
55/// Get /signers/registered/:epoch
56fn registered_signers(
57    router_state: &RouterState,
58) -> impl Filter<Extract = (impl warp::Reply + use<>,), Error = warp::Rejection> + Clone + use<> {
59    warp::path!("signers" / "registered" / String)
60        .and(warp::get())
61        .and(middlewares::with_logger(router_state))
62        .and(middlewares::with_epoch_service(router_state))
63        .and(middlewares::with_verification_key_store(router_state))
64        .and_then(handlers::registered_signers)
65}
66
67async fn fetch_epoch_header_value(
68    epoch_service: EpochServiceWrapper,
69    logger: &slog::Logger,
70) -> String {
71    match epoch_service.read().await.epoch_of_current_data() {
72        Ok(epoch) => format!("{epoch}"),
73        Err(e) => {
74            warn!(logger, "Could not fetch epoch header value from Epoch service"; "error" => ?e);
75            String::new()
76        }
77    }
78}
79
80async fn compute_registration_epoch(
81    registered_at: &str,
82    epoch_service: EpochServiceWrapper,
83) -> StdResult<Epoch> {
84    if registered_at.to_lowercase() == "latest" {
85        epoch_service.read().await.epoch_of_current_data()
86    } else {
87        registered_at
88            .parse::<u64>()
89            .map(Epoch)
90            .with_context(|| "Invalid epoch: must be a number or 'latest'")
91    }
92}
93
94mod handlers {
95    use crate::database::repository::SignerGetter;
96    use crate::dependency_injection::EpochServiceWrapper;
97    use crate::entities::{
98        SignerRegistrationsMessage, SignerTickerListItemMessage, SignersTickersMessage,
99    };
100    use crate::event_store::{EventMessage, TransmitterService};
101    use crate::http_server::routes::signer_routes::{
102        compute_registration_epoch, fetch_epoch_header_value,
103    };
104    use crate::{FromRegisterSignerAdapter, MetricsService, VerificationKeyStorer};
105    use crate::{SignerRegisterer, SignerRegistrationError, http_server::routes::reply};
106    use mithril_common::messages::{RegisterSignerMessage, TryFromMessageAdapter};
107    use slog::{Logger, debug, warn};
108    use std::convert::Infallible;
109    use std::sync::Arc;
110    use warp::http::StatusCode;
111
112    /// Register Signer
113    #[allow(clippy::too_many_arguments)]
114    pub async fn register_signer(
115        origin_tag: Option<String>,
116        signer_node_version: Option<String>,
117        register_signer_message: RegisterSignerMessage,
118        logger: Logger,
119        signer_registerer: Arc<dyn SignerRegisterer>,
120        event_transmitter: Arc<TransmitterService<EventMessage>>,
121        epoch_service: EpochServiceWrapper,
122        metrics_service: Arc<MetricsService>,
123    ) -> Result<impl warp::Reply, Infallible> {
124        debug!(logger, ">> register_signer"; "payload" => ?register_signer_message);
125
126        metrics_service
127            .get_signer_registration_total_received_since_startup()
128            .increment(&[origin_tag.as_deref().unwrap_or_default()]);
129
130        let registration_epoch = register_signer_message.epoch;
131
132        let signer = match FromRegisterSignerAdapter::try_adapt(register_signer_message) {
133            Ok(signer) => signer,
134            Err(err) => {
135                warn!(logger,"register_signer::payload decoding error"; "error" => ?err);
136                return Ok(reply::bad_request(
137                    "Could not decode signer payload".to_string(),
138                    err.to_string(),
139                ));
140            }
141        };
142
143        let epoch_str = fetch_epoch_header_value(epoch_service, &logger).await;
144
145        match signer_registerer.register_signer(registration_epoch, &signer).await {
146            Ok(signer_with_stake) => {
147                event_transmitter.send(EventMessage::signer_registration(
148                    "HTTP::signer_register",
149                    &signer_with_stake,
150                    signer_node_version,
151                    epoch_str.as_str(),
152                ));
153
154                Ok(reply::empty(StatusCode::CREATED))
155            }
156            Err(SignerRegistrationError::ExistingSigner(signer_with_stake)) => {
157                debug!(logger, "register_signer::already_registered");
158
159                event_transmitter.send(EventMessage::signer_registration(
160                    "HTTP::signer_register",
161                    &signer_with_stake,
162                    signer_node_version,
163                    epoch_str.as_str(),
164                ));
165
166                Ok(reply::empty(StatusCode::CREATED))
167            }
168            Err(SignerRegistrationError::FailedSignerRegistration(err)) => {
169                warn!(logger,"register_signer::failed_signer_registration"; "error" => ?err);
170                Ok(reply::bad_request(
171                    "failed_signer_registration".to_string(),
172                    err.to_string(),
173                ))
174            }
175            Err(SignerRegistrationError::RegistrationRoundNotYetOpened) => {
176                warn!(logger, "register_signer::registration_round_not_yed_opened");
177                Ok(reply::server_error(
178                    SignerRegistrationError::RegistrationRoundNotYetOpened,
179                ))
180            }
181            Err(err) => {
182                warn!(logger,"register_signer::error"; "error" => ?err);
183                Ok(reply::server_error(err))
184            }
185        }
186    }
187
188    /// Get Registered Signers for a given epoch
189    pub async fn registered_signers(
190        registered_at: String,
191        logger: Logger,
192        epoch_service: EpochServiceWrapper,
193        verification_key_store: Arc<dyn VerificationKeyStorer>,
194    ) -> Result<impl warp::Reply, Infallible> {
195        let registered_at_epoch =
196            match compute_registration_epoch(&registered_at, epoch_service).await {
197                Ok(epoch) => epoch,
198                Err(err) => {
199                    warn!(logger,"registered_signers::invalid_epoch"; "error" => ?err);
200                    return Ok(reply::bad_request(
201                        "invalid_epoch".to_string(),
202                        err.to_string(),
203                    ));
204                }
205            };
206
207        // The given epoch is the epoch at which the signer registered, the store works on
208        // the recording epoch so we need to offset.
209        match verification_key_store
210            .get_signers(registered_at_epoch.offset_to_recording_epoch())
211            .await
212        {
213            Ok(Some(signers)) => {
214                let message = SignerRegistrationsMessage::new(registered_at_epoch, signers);
215                Ok(reply::json(&message, StatusCode::OK))
216            }
217            Ok(None) => {
218                warn!(logger, "registered_signers::not_found");
219                Ok(reply::empty(StatusCode::NOT_FOUND))
220            }
221            Err(err) => {
222                warn!(logger,"registered_signers::error"; "error" => ?err);
223                Ok(reply::server_error(err))
224            }
225        }
226    }
227
228    pub async fn signers_tickers(
229        logger: Logger,
230        network: String,
231        signer_getter: Arc<dyn SignerGetter>,
232    ) -> Result<impl warp::Reply, Infallible> {
233        match signer_getter.get_all().await {
234            Ok(signers) => {
235                let signers: Vec<_> = signers
236                    .into_iter()
237                    .map(|s| SignerTickerListItemMessage {
238                        party_id: s.signer_id,
239                        pool_ticker: s.pool_ticker,
240                        has_registered: s.last_registered_at.is_some(),
241                    })
242                    .collect();
243                Ok(reply::json(
244                    &SignersTickersMessage { network, signers },
245                    StatusCode::OK,
246                ))
247            }
248            Err(err) => {
249                warn!(logger,"registered_signers::error"; "error" => ?err);
250                Ok(reply::server_error(err))
251            }
252        }
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use anyhow::anyhow;
259    use mockall::predicate::eq;
260    use serde_json::Value::Null;
261    use std::sync::Arc;
262    use tokio::sync::RwLock;
263    use warp::{
264        http::{Method, StatusCode},
265        test::request,
266    };
267
268    use mithril_api_spec::APISpec;
269    use mithril_common::{
270        MITHRIL_ORIGIN_TAG_HEADER,
271        crypto_helper::ProtocolRegistrationError,
272        entities::Epoch,
273        messages::RegisterSignerMessage,
274        test_utils::{MithrilFixtureBuilder, fake_data},
275    };
276
277    use crate::{
278        SignerRegistrationError,
279        database::{record::SignerRecord, repository::MockSignerGetter},
280        http_server::routes::reply::MithrilStatusCode,
281        initialize_dependencies,
282        services::{FakeEpochService, MockSignerRegisterer},
283        store::MockVerificationKeyStorer,
284        test_tools::TestLogger,
285    };
286
287    use super::*;
288
289    fn setup_router(
290        state: RouterState,
291    ) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
292        let cors = warp::cors()
293            .allow_any_origin()
294            .allow_headers(vec!["content-type"])
295            .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS]);
296
297        warp::any().and(routes(&state).with(cors))
298    }
299
300    #[tokio::test]
301    async fn test_register_signer_post_ok() {
302        let signer_with_stake = fake_data::signers_with_stakes(1).pop().unwrap();
303        let mut mock_signer_registerer = MockSignerRegisterer::new();
304        mock_signer_registerer
305            .expect_register_signer()
306            .return_once(|_, _| Ok(signer_with_stake));
307        let mut dependency_manager = initialize_dependencies!().await;
308        dependency_manager.signer_registerer = Arc::new(mock_signer_registerer);
309
310        let signer: RegisterSignerMessage = RegisterSignerMessage::dummy();
311
312        let method = Method::POST.as_str();
313        let path = "/register-signer";
314
315        let response = request()
316            .method(method)
317            .path(path)
318            .json(&signer)
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            &signer,
330            &response,
331            &StatusCode::CREATED,
332        )
333        .unwrap();
334    }
335
336    #[tokio::test]
337    async fn test_register_signer_post_increments_signer_registration_total_received_since_startup_metric()
338     {
339        let method = Method::POST.as_str();
340        let path = "/register-signer";
341        let dependency_manager = Arc::new(initialize_dependencies!().await);
342        let initial_counter_value = dependency_manager
343            .metrics_service
344            .get_signer_registration_total_received_since_startup()
345            .get(&["TEST"]);
346
347        request()
348            .method(method)
349            .path(path)
350            .json(&RegisterSignerMessage::dummy())
351            .header(MITHRIL_ORIGIN_TAG_HEADER, "TEST")
352            .reply(&setup_router(RouterState::new_with_origin_tag_white_list(
353                dependency_manager.clone(),
354                &["TEST"],
355            )))
356            .await;
357
358        assert_eq!(
359            initial_counter_value + 1,
360            dependency_manager
361                .metrics_service
362                .get_signer_registration_total_received_since_startup()
363                .get(&["TEST"])
364        );
365    }
366
367    #[tokio::test]
368    async fn test_register_signer_post_ok_existing() {
369        let signer_with_stake = fake_data::signers_with_stakes(1).pop().unwrap();
370        let mut mock_signer_registerer = MockSignerRegisterer::new();
371        mock_signer_registerer.expect_register_signer().return_once(|_, _| {
372            Err(SignerRegistrationError::ExistingSigner(Box::new(
373                signer_with_stake,
374            )))
375        });
376        let mut dependency_manager = initialize_dependencies!().await;
377        dependency_manager.signer_registerer = Arc::new(mock_signer_registerer);
378
379        let signer: RegisterSignerMessage = RegisterSignerMessage::dummy();
380
381        let method = Method::POST.as_str();
382        let path = "/register-signer";
383
384        let response = request()
385            .method(method)
386            .path(path)
387            .json(&signer)
388            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
389                dependency_manager,
390            ))))
391            .await;
392
393        APISpec::verify_conformity(
394            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
395            method,
396            path,
397            "application/json",
398            &signer,
399            &response,
400            &StatusCode::CREATED,
401        )
402        .unwrap();
403    }
404
405    #[tokio::test]
406    async fn test_register_signer_post_ko_400() {
407        let mut mock_signer_registerer = MockSignerRegisterer::new();
408        mock_signer_registerer.expect_register_signer().return_once(|_, _| {
409            Err(SignerRegistrationError::FailedSignerRegistration(anyhow!(
410                ProtocolRegistrationError::OpCertInvalid
411            )))
412        });
413        let mut dependency_manager = initialize_dependencies!().await;
414        dependency_manager.signer_registerer = Arc::new(mock_signer_registerer);
415
416        let signer: RegisterSignerMessage = RegisterSignerMessage::dummy();
417
418        let method = Method::POST.as_str();
419        let path = "/register-signer";
420
421        let response = request()
422            .method(method)
423            .path(path)
424            .json(&signer)
425            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
426                dependency_manager,
427            ))))
428            .await;
429
430        APISpec::verify_conformity(
431            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
432            method,
433            path,
434            "application/json",
435            &signer,
436            &response,
437            &StatusCode::BAD_REQUEST,
438        )
439        .unwrap();
440    }
441
442    #[tokio::test]
443    async fn test_register_signer_post_ko_500() {
444        let mut mock_signer_registerer = MockSignerRegisterer::new();
445        mock_signer_registerer.expect_register_signer().return_once(|_, _| {
446            Err(SignerRegistrationError::FailedSignerRecorder(
447                "an error occurred".to_string(),
448            ))
449        });
450        let mut dependency_manager = initialize_dependencies!().await;
451        dependency_manager.signer_registerer = Arc::new(mock_signer_registerer);
452
453        let signer: RegisterSignerMessage = RegisterSignerMessage::dummy();
454        let method = Method::POST.as_str();
455        let path = "/register-signer";
456
457        let response = request()
458            .method(method)
459            .path(path)
460            .json(&signer)
461            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
462                dependency_manager,
463            ))))
464            .await;
465
466        APISpec::verify_conformity(
467            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
468            method,
469            path,
470            "application/json",
471            &signer,
472            &response,
473            &StatusCode::INTERNAL_SERVER_ERROR,
474        )
475        .unwrap();
476    }
477
478    #[tokio::test]
479    async fn test_register_signer_post_ko_550() {
480        let mut mock_signer_registerer = MockSignerRegisterer::new();
481        mock_signer_registerer
482            .expect_register_signer()
483            .return_once(|_, _| Err(SignerRegistrationError::RegistrationRoundNotYetOpened));
484        let mut dependency_manager = initialize_dependencies!().await;
485        dependency_manager.signer_registerer = Arc::new(mock_signer_registerer);
486
487        let signer: RegisterSignerMessage = RegisterSignerMessage::dummy();
488        let method = Method::POST.as_str();
489        let path = "/register-signer";
490
491        let response = request()
492            .method(method)
493            .path(path)
494            .json(&signer)
495            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
496                dependency_manager,
497            ))))
498            .await;
499
500        APISpec::verify_conformity(
501            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
502            method,
503            path,
504            "application/json",
505            &signer,
506            &response,
507            &MithrilStatusCode::registration_round_not_yet_opened(),
508        )
509        .unwrap();
510    }
511
512    #[tokio::test]
513    async fn test_registered_signers_get_offset_given_epoch_to_registration_epoch() {
514        let asked_epoch = Epoch(1);
515        let expected_retrieval_epoch = asked_epoch.offset_to_recording_epoch();
516        let mut mock_verification_key_store = MockVerificationKeyStorer::new();
517        mock_verification_key_store
518            .expect_get_signers()
519            .with(eq(expected_retrieval_epoch))
520            .return_once(|_| Ok(Some(fake_data::signers_with_stakes(3))))
521            .once();
522        let mut dependency_manager = initialize_dependencies!().await;
523        dependency_manager.verification_key_store = Arc::new(mock_verification_key_store);
524
525        let method = Method::GET.as_str();
526        let base_path = "/signers/registered";
527
528        let response = request()
529            .method(method)
530            .path(&format!("{base_path}/{asked_epoch}"))
531            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
532                dependency_manager,
533            ))))
534            .await;
535
536        assert!(
537            response.status().is_success(),
538            "expected the response to succeed, was: {response:#?}"
539        );
540    }
541
542    #[tokio::test]
543    async fn test_registered_signers_get_ok() {
544        let mut mock_verification_key_store = MockVerificationKeyStorer::new();
545        mock_verification_key_store
546            .expect_get_signers()
547            .return_once(|_| Ok(Some(fake_data::signers_with_stakes(3))))
548            .once();
549        let mut dependency_manager = initialize_dependencies!().await;
550        dependency_manager.verification_key_store = Arc::new(mock_verification_key_store);
551
552        let base_path = "/signers/registered";
553        let method = Method::GET.as_str();
554
555        let response = request()
556            .method(method)
557            .path(&format!("{base_path}/1"))
558            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
559                dependency_manager,
560            ))))
561            .await;
562
563        APISpec::verify_conformity(
564            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
565            method,
566            &format!("{base_path}/{{epoch}}"),
567            "application/json",
568            &Null,
569            &response,
570            &StatusCode::OK,
571        )
572        .unwrap();
573    }
574
575    #[tokio::test]
576    async fn test_registered_signers_returns_404_not_found_when_no_registration() {
577        let mut mock_verification_key_store = MockVerificationKeyStorer::new();
578        mock_verification_key_store
579            .expect_get_signers()
580            .return_once(|_| Ok(None))
581            .once();
582        let mut dependency_manager = initialize_dependencies!().await;
583        dependency_manager.verification_key_store = Arc::new(mock_verification_key_store);
584
585        let method = Method::GET.as_str();
586        let base_path = "/signers/registered";
587
588        let response = request()
589            .method(method)
590            .path(&format!("{base_path}/3"))
591            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
592                dependency_manager,
593            ))))
594            .await;
595
596        APISpec::verify_conformity(
597            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
598            method,
599            &format!("{base_path}/{{epoch}}"),
600            "application/json",
601            &Null,
602            &response,
603            &StatusCode::NOT_FOUND,
604        )
605        .unwrap();
606    }
607
608    #[tokio::test]
609    async fn test_registered_signers_get_ko() {
610        let mut mock_verification_key_store = MockVerificationKeyStorer::new();
611        mock_verification_key_store
612            .expect_get_signers()
613            .return_once(|_| Err(anyhow!("invalid query")));
614        let mut dependency_manager = initialize_dependencies!().await;
615        dependency_manager.verification_key_store = Arc::new(mock_verification_key_store);
616
617        let method = Method::GET.as_str();
618        let base_path = "/signers/registered";
619
620        let response = request()
621            .method(method)
622            .path(&format!("{base_path}/1"))
623            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
624                dependency_manager,
625            ))))
626            .await;
627
628        APISpec::verify_conformity(
629            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
630            method,
631            &format!("{base_path}/{{epoch}}"),
632            "application/json",
633            &Null,
634            &response,
635            &StatusCode::INTERNAL_SERVER_ERROR,
636        )
637        .unwrap();
638    }
639
640    #[tokio::test]
641    async fn test_signers_tickers_get_ok() {
642        let mut mock_signer_getter = MockSignerGetter::new();
643        mock_signer_getter
644            .expect_get_all()
645            .return_once(|| {
646                Ok(vec![
647                    SignerRecord {
648                        signer_id: "pool_without_ticker".to_string(),
649                        pool_ticker: None,
650                        created_at: Default::default(),
651                        updated_at: Default::default(),
652                        last_registered_at: None,
653                    },
654                    SignerRecord {
655                        signer_id: "pool_with_ticker".to_string(),
656                        pool_ticker: Some("pool_ticker".to_string()),
657                        created_at: Default::default(),
658                        updated_at: Default::default(),
659                        last_registered_at: None,
660                    },
661                ])
662            })
663            .once();
664        let mut dependency_manager = initialize_dependencies!().await;
665        dependency_manager.signer_getter = Arc::new(mock_signer_getter);
666
667        let method = Method::GET.as_str();
668        let path = "/signers/tickers";
669
670        let response = request()
671            .method(method)
672            .path(path)
673            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
674                dependency_manager,
675            ))))
676            .await;
677
678        APISpec::verify_conformity(
679            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
680            method,
681            path,
682            "application/json",
683            &Null,
684            &response,
685            &StatusCode::OK,
686        )
687        .unwrap();
688    }
689
690    #[tokio::test]
691    async fn test_signers_tickers_get_ko() {
692        let mut mock_signer_getter = MockSignerGetter::new();
693        mock_signer_getter
694            .expect_get_all()
695            .return_once(|| Err(anyhow!("an error")))
696            .once();
697        let mut dependency_manager = initialize_dependencies!().await;
698        dependency_manager.signer_getter = Arc::new(mock_signer_getter);
699
700        let method = Method::GET.as_str();
701        let path = "/signers/tickers";
702
703        let response = request()
704            .method(method)
705            .path(path)
706            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
707                dependency_manager,
708            ))))
709            .await;
710
711        APISpec::verify_conformity(
712            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
713            method,
714            path,
715            "application/json",
716            &Null,
717            &response,
718            &StatusCode::INTERNAL_SERVER_ERROR,
719        )
720        .unwrap();
721    }
722
723    #[tokio::test]
724    async fn test_fetch_epoch_header_value_when_epoch_service_return_epoch() {
725        let fixture = MithrilFixtureBuilder::default().build();
726        let epoch_service = Arc::new(RwLock::new(FakeEpochService::from_fixture(
727            Epoch(84),
728            &fixture,
729        )));
730
731        let epoch_str = fetch_epoch_header_value(epoch_service, &TestLogger::stdout()).await;
732
733        assert_eq!(epoch_str, "84".to_string());
734    }
735
736    #[tokio::test]
737    async fn test_fetch_epoch_header_value_when_epoch_service_error_return_empty_string() {
738        let epoch_service = Arc::new(RwLock::new(FakeEpochService::without_data()));
739
740        let epoch_str = fetch_epoch_header_value(epoch_service, &TestLogger::stdout()).await;
741
742        assert_eq!(epoch_str, "".to_string());
743    }
744
745    mod registered_signers_registration_epoch {
746        use crate::services::FakeEpochServiceBuilder;
747
748        use super::*;
749
750        #[tokio::test]
751        async fn use_given_epoch_if_valid_number() {
752            let epoch_service = Arc::new(RwLock::new(
753                FakeEpochServiceBuilder::dummy(Epoch(89)).build(),
754            ));
755            let epoch = compute_registration_epoch("456", epoch_service).await.unwrap();
756
757            assert_eq!(epoch, Epoch(456));
758        }
759
760        #[tokio::test]
761        async fn use_epoch_service_current_epoch_if_latest() {
762            let epoch_service = Arc::new(RwLock::new(
763                FakeEpochServiceBuilder::dummy(Epoch(89)).build(),
764            ));
765            let epoch = compute_registration_epoch("latest", epoch_service).await.unwrap();
766
767            assert_eq!(epoch, Epoch(89));
768        }
769
770        #[tokio::test]
771        async fn error_if_given_epoch_is_not_a_number_nor_latest() {
772            let epoch_service = Arc::new(RwLock::new(
773                FakeEpochServiceBuilder::dummy(Epoch(89)).build(),
774            ));
775
776            compute_registration_epoch("invalid", epoch_service)
777                .await
778                .expect_err("Should fail if epoch is not a number nor 'latest'");
779        }
780    }
781}