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