mithril_aggregator/http_server/routes/
signer_routes.rs

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