mithril_client/
client.rs

1use std::collections::HashMap;
2#[cfg(not(target_family = "wasm"))]
3use std::str::FromStr;
4use std::sync::Arc;
5
6use anyhow::{Context, anyhow};
7#[cfg(any(feature = "fs", not(target_family = "wasm")))]
8use chrono::Utc;
9#[cfg(not(target_family = "wasm"))]
10use rand::SeedableRng;
11#[cfg(not(target_family = "wasm"))]
12use rand::rngs::StdRng;
13use serde::{Deserialize, Serialize};
14use slog::{Logger, o};
15
16use mithril_aggregator_client::AggregatorHttpClient;
17#[cfg(not(target_family = "wasm"))]
18use mithril_aggregator_discovery::{
19    AggregatorDiscoverer, AggregatorEndpoint, AggregatorEndpointWithCapabilities,
20    CapableAggregatorDiscoverer, HttpConfigAggregatorDiscoverer, RequiredAggregatorCapabilities,
21    ShuffleAggregatorDiscoverer,
22};
23use mithril_common::{MITHRIL_CLIENT_TYPE_HEADER, MITHRIL_ORIGIN_TAG_HEADER};
24
25use crate::MithrilResult;
26use crate::cardano_database_client::CardanoDatabaseClient;
27use crate::cardano_stake_distribution_client::CardanoStakeDistributionClient;
28use crate::cardano_transaction_client::CardanoTransactionClient;
29#[cfg(feature = "unstable")]
30use crate::certificate_client::CertificateVerifierCache;
31use crate::certificate_client::{
32    CertificateClient, CertificateVerifier, MithrilCertificateVerifier,
33};
34#[cfg(not(target_family = "wasm"))]
35use crate::common::MithrilNetwork;
36use crate::era::{EraFetcher, MithrilEraClient};
37use crate::feedback::{FeedbackReceiver, FeedbackSender};
38#[cfg(feature = "fs")]
39use crate::file_downloader::{
40    FileDownloadRetryPolicy, FileDownloader, HttpFileDownloader, RetryDownloader,
41};
42use crate::mithril_stake_distribution_client::MithrilStakeDistributionClient;
43use crate::snapshot_client::SnapshotClient;
44#[cfg(feature = "fs")]
45use crate::utils::AncillaryVerifier;
46#[cfg(feature = "fs")]
47use crate::utils::TimestampTempDirectoryProvider;
48
49const DEFAULT_CLIENT_TYPE: &str = "LIBRARY";
50
51#[cfg(target_family = "wasm")]
52const fn one_week_in_seconds() -> u32 {
53    604800
54}
55
56/// The type of discovery to use to find the aggregator to connect to.
57pub enum AggregatorDiscoveryType {
58    /// Use a specific URL to connect to the aggregator.
59    Url(String),
60    /// Automatically discover the aggregator.
61    #[cfg(not(target_family = "wasm"))]
62    Automatic(MithrilNetwork),
63}
64
65#[cfg(not(target_family = "wasm"))]
66impl FromStr for AggregatorDiscoveryType {
67    type Err = anyhow::Error;
68
69    fn from_str(s: &str) -> Result<Self, Self::Err> {
70        if let Some(network) = s.strip_prefix("auto:") {
71            Ok(AggregatorDiscoveryType::Automatic(MithrilNetwork::new(
72                network.to_string(),
73            )))
74        } else {
75            Ok(AggregatorDiscoveryType::Url(s.to_string()))
76        }
77    }
78}
79
80/// The genesis verification key.
81pub enum GenesisVerificationKey {
82    /// The verification key is provided as a JSON Hex-encoded string.
83    JsonHex(String),
84}
85
86/// Options that can be used to configure the client.
87#[derive(Debug, Clone, Default, Serialize, Deserialize)]
88pub struct ClientOptions {
89    /// HTTP headers to include in the client requests.
90    pub http_headers: Option<HashMap<String, String>>,
91
92    /// Tag to retrieve the origin of the client requests.
93    #[cfg(target_family = "wasm")]
94    #[cfg_attr(target_family = "wasm", serde(default))]
95    pub origin_tag: Option<String>,
96
97    /// Whether to enable unstable features in the WASM client.
98    #[cfg(target_family = "wasm")]
99    #[cfg_attr(target_family = "wasm", serde(default))]
100    pub unstable: bool,
101
102    /// Whether to enable certificate chain verification caching in the WASM client.
103    ///
104    /// `unstable` must be set to `true` for this option to have any effect.
105    ///
106    /// DANGER: This feature is highly experimental and insecure, and it must not be used in production
107    #[cfg(target_family = "wasm")]
108    #[cfg_attr(target_family = "wasm", serde(default))]
109    pub enable_certificate_chain_verification_cache: bool,
110
111    /// Duration in seconds of certificate chain verification cache in the WASM client.
112    ///
113    /// Default to one week (604800 seconds).
114    ///
115    /// `enable_certificate_chain_verification_cache` and `unstable` must both be set to `true`
116    /// for this option to have any effect.
117    #[cfg(target_family = "wasm")]
118    #[cfg_attr(target_family = "wasm", serde(default = "one_week_in_seconds"))]
119    pub certificate_chain_verification_cache_duration_in_seconds: u32,
120}
121
122impl ClientOptions {
123    /// Instantiate a new [ClientOptions].
124    pub fn new(http_headers: Option<HashMap<String, String>>) -> Self {
125        Self {
126            http_headers,
127            #[cfg(target_family = "wasm")]
128            origin_tag: None,
129            #[cfg(target_family = "wasm")]
130            unstable: false,
131            #[cfg(target_family = "wasm")]
132            enable_certificate_chain_verification_cache: false,
133            #[cfg(target_family = "wasm")]
134            certificate_chain_verification_cache_duration_in_seconds: one_week_in_seconds(),
135        }
136    }
137
138    /// Enable unstable features in the WASM client.
139    #[cfg(target_family = "wasm")]
140    pub fn with_unstable_features(self, unstable: bool) -> Self {
141        Self { unstable, ..self }
142    }
143}
144
145/// Structure that aggregates the available clients for each of the Mithril types of certified data.
146///
147/// Use the [ClientBuilder] to instantiate it easily.
148#[derive(Clone)]
149pub struct Client {
150    certificate_client: Arc<CertificateClient>,
151    mithril_stake_distribution_client: Arc<MithrilStakeDistributionClient>,
152    snapshot_client: Arc<SnapshotClient>,
153    cardano_database_client: Arc<CardanoDatabaseClient>,
154    cardano_transaction_client: Arc<CardanoTransactionClient>,
155    cardano_stake_distribution_client: Arc<CardanoStakeDistributionClient>,
156    mithril_era_client: Arc<MithrilEraClient>,
157}
158
159impl Client {
160    /// Get the client that fetches and verifies Mithril certificates.
161    pub fn certificate(&self) -> Arc<CertificateClient> {
162        self.certificate_client.clone()
163    }
164
165    /// Get the client that fetches Mithril stake distributions.
166    pub fn mithril_stake_distribution(&self) -> Arc<MithrilStakeDistributionClient> {
167        self.mithril_stake_distribution_client.clone()
168    }
169
170    /// Get the client that fetches and downloads Mithril snapshots.
171    #[deprecated(since = "0.12.35", note = "superseded by `cardano_database_v2`")]
172    pub fn cardano_database(&self) -> Arc<SnapshotClient> {
173        self.snapshot_client.clone()
174    }
175
176    /// Get the client that fetches and downloads Cardano database snapshots.
177    pub fn cardano_database_v2(&self) -> Arc<CardanoDatabaseClient> {
178        self.cardano_database_client.clone()
179    }
180
181    /// Get the client that fetches and verifies Mithril Cardano transaction proof.
182    pub fn cardano_transaction(&self) -> Arc<CardanoTransactionClient> {
183        self.cardano_transaction_client.clone()
184    }
185
186    /// Get the client that fetches Cardano stake distributions.
187    pub fn cardano_stake_distribution(&self) -> Arc<CardanoStakeDistributionClient> {
188        self.cardano_stake_distribution_client.clone()
189    }
190
191    /// Get the client that fetches the current Mithril era.
192    pub fn mithril_era_client(&self) -> Arc<MithrilEraClient> {
193        self.mithril_era_client.clone()
194    }
195}
196
197/// Builder that can be used to create a [Client] easily or with custom dependencies.
198pub struct ClientBuilder {
199    aggregator_discovery: AggregatorDiscoveryType,
200    #[cfg(not(target_family = "wasm"))]
201    aggregator_capabilities: Option<RequiredAggregatorCapabilities>,
202    #[cfg(not(target_family = "wasm"))]
203    aggregator_discoverer: Option<Arc<dyn AggregatorDiscoverer<AggregatorEndpoint>>>,
204    genesis_verification_key: Option<GenesisVerificationKey>,
205    origin_tag: Option<String>,
206    client_type: Option<String>,
207    #[cfg(feature = "fs")]
208    ancillary_verification_key: Option<String>,
209    certificate_verifier: Option<Arc<dyn CertificateVerifier>>,
210    #[cfg(feature = "fs")]
211    http_file_downloader: Option<Arc<dyn FileDownloader>>,
212    #[cfg(feature = "unstable")]
213    certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
214    era_fetcher: Option<Arc<dyn EraFetcher>>,
215    logger: Option<Logger>,
216    feedback_receivers: Vec<Arc<dyn FeedbackReceiver>>,
217    options: ClientOptions,
218}
219
220impl ClientBuilder {
221    /// Constructs a new `ClientBuilder` that fetches data from the aggregator at the given
222    /// endpoint and with the given genesis verification key.
223    #[deprecated(
224        since = "0.12.36",
225        note = "Use `new` method instead and set the genesis verification key with `set_genesis_verification_key`"
226    )]
227    pub fn aggregator(endpoint: &str, genesis_verification_key: &str) -> ClientBuilder {
228        Self::new(AggregatorDiscoveryType::Url(endpoint.to_string())).set_genesis_verification_key(
229            GenesisVerificationKey::JsonHex(genesis_verification_key.to_string()),
230        )
231    }
232
233    /// Constructs a new `ClientBuilder` that automatically discovers the aggregator for the given
234    /// Mithril network and with the given genesis verification key.
235    #[cfg(not(target_family = "wasm"))]
236    pub fn automatic(network: &str, genesis_verification_key: &str) -> ClientBuilder {
237        Self::new(AggregatorDiscoveryType::Automatic(MithrilNetwork::new(
238            network.to_string(),
239        )))
240        .set_genesis_verification_key(GenesisVerificationKey::JsonHex(
241            genesis_verification_key.to_string(),
242        ))
243    }
244
245    /// Constructs a new `ClientBuilder` without any dependency set.
246    pub fn new(aggregator_discovery: AggregatorDiscoveryType) -> ClientBuilder {
247        Self {
248            aggregator_discovery,
249            #[cfg(not(target_family = "wasm"))]
250            aggregator_capabilities: None,
251            #[cfg(not(target_family = "wasm"))]
252            aggregator_discoverer: None,
253            genesis_verification_key: None,
254            origin_tag: None,
255            client_type: None,
256            #[cfg(feature = "fs")]
257            ancillary_verification_key: None,
258            certificate_verifier: None,
259            #[cfg(feature = "fs")]
260            http_file_downloader: None,
261            #[cfg(feature = "unstable")]
262            certificate_verifier_cache: None,
263            era_fetcher: None,
264            logger: None,
265            feedback_receivers: vec![],
266            options: ClientOptions::default(),
267        }
268    }
269
270    /// Sets the genesis verification key to use when verifying certificates.
271    pub fn set_genesis_verification_key(
272        mut self,
273        genesis_verification_key: GenesisVerificationKey,
274    ) -> ClientBuilder {
275        self.genesis_verification_key = Some(genesis_verification_key);
276
277        self
278    }
279
280    /// Sets the aggregator capabilities expected to be matched by the aggregator with which the client will interact.
281    #[cfg(not(target_family = "wasm"))]
282    pub fn with_capabilities(
283        mut self,
284        capabilities: RequiredAggregatorCapabilities,
285    ) -> ClientBuilder {
286        self.aggregator_capabilities = Some(capabilities);
287
288        self
289    }
290
291    /// Sets the aggregator discoverer to use to find the aggregator endpoint when in automatic discovery.
292    #[cfg(not(target_family = "wasm"))]
293    pub fn with_aggregator_discoverer(
294        mut self,
295        discoverer: Arc<dyn AggregatorDiscoverer<AggregatorEndpoint>>,
296    ) -> ClientBuilder {
297        self.aggregator_discoverer = Some(discoverer);
298
299        self
300    }
301
302    /// Returns a `Client` that uses the dependencies provided to this `ClientBuilder`.
303    ///
304    /// The builder will try to create the missing dependencies using default implementations
305    /// if possible.
306    pub fn build(self) -> MithrilResult<Client> {
307        let logger = self
308            .logger
309            .clone()
310            .unwrap_or_else(|| Logger::root(slog::Discard, o!()));
311
312        let genesis_verification_key = match self.genesis_verification_key {
313            Some(GenesisVerificationKey::JsonHex(ref key)) => key,
314            None => {
315                return Err(anyhow!(
316                    "The genesis verification key must be provided to build the client with the 'set_genesis_verification_key' function"
317                ));
318            }
319        };
320
321        let feedback_sender = FeedbackSender::new(&self.feedback_receivers);
322
323        let aggregator_client = Arc::new(self.build_aggregator_client(logger.clone())?);
324
325        let mithril_era_client = match self.era_fetcher {
326            None => Arc::new(MithrilEraClient::new(aggregator_client.clone())),
327            Some(era_fetcher) => Arc::new(MithrilEraClient::new(era_fetcher)),
328        };
329
330        let certificate_verifier = match self.certificate_verifier {
331            None => Arc::new(
332                MithrilCertificateVerifier::new(
333                    aggregator_client.clone(),
334                    genesis_verification_key,
335                    feedback_sender.clone(),
336                    #[cfg(feature = "unstable")]
337                    self.certificate_verifier_cache,
338                    logger.clone(),
339                )
340                .with_context(|| "Building certificate verifier failed")?,
341            ),
342            Some(verifier) => verifier,
343        };
344        let certificate_client = Arc::new(CertificateClient::new(
345            aggregator_client.clone(),
346            certificate_verifier,
347            logger.clone(),
348        ));
349
350        let mithril_stake_distribution_client = Arc::new(MithrilStakeDistributionClient::new(
351            aggregator_client.clone(),
352        ));
353
354        #[cfg(feature = "fs")]
355        let http_file_downloader = match self.http_file_downloader {
356            None => Arc::new(RetryDownloader::new(
357                Arc::new(
358                    HttpFileDownloader::new(feedback_sender.clone(), logger.clone())
359                        .with_context(|| "Building http file downloader failed")?,
360                ),
361                FileDownloadRetryPolicy::default(),
362            )),
363            Some(http_file_downloader) => http_file_downloader,
364        };
365
366        #[cfg(feature = "fs")]
367        let ancillary_verifier = match self.ancillary_verification_key {
368            None => None,
369            Some(verification_key) => Some(Arc::new(AncillaryVerifier::new(
370                verification_key
371                    .try_into()
372                    .with_context(|| "Building ancillary verifier failed")?,
373            ))),
374        };
375
376        let snapshot_client = Arc::new(SnapshotClient::new(
377            aggregator_client.clone(),
378            #[cfg(feature = "fs")]
379            http_file_downloader.clone(),
380            #[cfg(feature = "fs")]
381            ancillary_verifier.clone(),
382            #[cfg(feature = "fs")]
383            feedback_sender.clone(),
384            #[cfg(feature = "fs")]
385            logger.clone(),
386        ));
387
388        let cardano_database_client = Arc::new(CardanoDatabaseClient::new(
389            aggregator_client.clone(),
390            #[cfg(feature = "fs")]
391            http_file_downloader,
392            #[cfg(feature = "fs")]
393            ancillary_verifier,
394            #[cfg(feature = "fs")]
395            feedback_sender,
396            #[cfg(feature = "fs")]
397            Arc::new(TimestampTempDirectoryProvider::new(&format!(
398                "{}",
399                Utc::now().timestamp_micros()
400            ))),
401            #[cfg(feature = "fs")]
402            logger,
403        ));
404
405        let cardano_transaction_client =
406            Arc::new(CardanoTransactionClient::new(aggregator_client.clone()));
407
408        let cardano_stake_distribution_client =
409            Arc::new(CardanoStakeDistributionClient::new(aggregator_client));
410
411        Ok(Client {
412            certificate_client,
413            mithril_stake_distribution_client,
414            snapshot_client,
415            cardano_database_client,
416            cardano_transaction_client,
417            cardano_stake_distribution_client,
418            mithril_era_client,
419        })
420    }
421
422    /// Discover available aggregator endpoints for the given Mithril network and required capabilities.
423    #[cfg(not(target_family = "wasm"))]
424    pub fn discover_aggregator(
425        &self,
426        network: &MithrilNetwork,
427    ) -> MithrilResult<impl Iterator<Item = AggregatorEndpointWithCapabilities>> {
428        let discoverer = self
429            .aggregator_discoverer
430            .clone()
431            .unwrap_or_else(|| Self::default_aggregator_discoverer());
432        let discoverer = if let Some(capabilities) = &self.aggregator_capabilities {
433            Arc::new(CapableAggregatorDiscoverer::new(
434                capabilities.to_owned(),
435                discoverer.clone(),
436            )) as Arc<dyn AggregatorDiscoverer<AggregatorEndpointWithCapabilities>>
437        } else {
438            Arc::new(CapableAggregatorDiscoverer::new(
439                RequiredAggregatorCapabilities::All,
440                discoverer.clone(),
441            )) as Arc<dyn AggregatorDiscoverer<AggregatorEndpointWithCapabilities>>
442        };
443
444        tokio::task::block_in_place(move || {
445            tokio::runtime::Handle::current().block_on(async move {
446                discoverer
447                    .get_available_aggregators(network.to_owned())
448                    .await
449                    .with_context(|| "Discovering aggregator endpoint failed")
450            })
451        })
452    }
453
454    /// Default aggregator discoverer to use to find the aggregator endpoint when in automatic discovery.
455    #[cfg(not(target_family = "wasm"))]
456    fn default_aggregator_discoverer() -> Arc<dyn AggregatorDiscoverer<AggregatorEndpoint>> {
457        Arc::new(ShuffleAggregatorDiscoverer::new(
458            Arc::new(HttpConfigAggregatorDiscoverer::default()),
459            {
460                let mut seed = [0u8; 32];
461                let timestamp = Utc::now().timestamp_nanos_opt().unwrap_or(0);
462                seed[..8].copy_from_slice(&timestamp.to_le_bytes());
463
464                StdRng::from_seed(seed)
465            },
466        ))
467    }
468
469    fn build_aggregator_client(&self, logger: Logger) -> MithrilResult<AggregatorHttpClient> {
470        let aggregator_endpoint = match self.aggregator_discovery {
471            AggregatorDiscoveryType::Url(ref url) => url.clone(),
472            #[cfg(not(target_family = "wasm"))]
473            AggregatorDiscoveryType::Automatic(ref network) => self
474                .discover_aggregator(network)?
475                .next()
476                .with_context(|| "No aggregator was available through discovery")?
477                .into(),
478        };
479        let headers = self.compute_http_headers();
480
481        AggregatorHttpClient::builder(aggregator_endpoint)
482            .with_logger(logger)
483            .with_headers(headers)
484            .build()
485    }
486
487    fn compute_http_headers(&self) -> HashMap<String, String> {
488        let mut headers = self.options.http_headers.clone().unwrap_or_default();
489        if let Some(origin_tag) = self.origin_tag.clone() {
490            headers.insert(MITHRIL_ORIGIN_TAG_HEADER.to_string(), origin_tag);
491        }
492        if let Some(client_type) = self.client_type.clone() {
493            headers.insert(MITHRIL_CLIENT_TYPE_HEADER.to_string(), client_type);
494        } else if !headers.contains_key(MITHRIL_CLIENT_TYPE_HEADER) {
495            headers.insert(
496                MITHRIL_CLIENT_TYPE_HEADER.to_string(),
497                DEFAULT_CLIENT_TYPE.to_string(),
498            );
499        }
500
501        headers
502    }
503
504    /// Sets the [EraFetcher] that will be used by the client to retrieve the current Mithril era.
505    pub fn with_era_fetcher(mut self, era_fetcher: Arc<dyn EraFetcher>) -> ClientBuilder {
506        self.era_fetcher = Some(era_fetcher);
507        self
508    }
509
510    /// Set the [CertificateVerifier] that will be used to validate certificates.
511    pub fn with_certificate_verifier(
512        mut self,
513        certificate_verifier: Arc<dyn CertificateVerifier>,
514    ) -> ClientBuilder {
515        self.certificate_verifier = Some(certificate_verifier);
516        self
517    }
518
519    cfg_unstable! {
520        /// Set the [CertificateVerifierCache] that will be used to cache certificate validation results.
521        ///
522        /// Passing a `None` value will disable the cache if any was previously set.
523        pub fn with_certificate_verifier_cache(
524            mut self,
525            certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
526        ) -> ClientBuilder {
527            self.certificate_verifier_cache = certificate_verifier_cache;
528            self
529        }
530    }
531
532    cfg_fs! {
533        /// Set the [FileDownloader] that will be used to download artifacts with HTTP.
534        pub fn with_http_file_downloader(
535            mut self,
536            http_file_downloader: Arc<dyn FileDownloader>,
537        ) -> ClientBuilder {
538            self.http_file_downloader = Some(http_file_downloader);
539            self
540        }
541
542        /// Set the ancillary verification key to use when verifying the downloaded ancillary files.
543        pub fn set_ancillary_verification_key<T: Into<Option<String>>>(
544            mut self,
545            ancillary_verification_key: T,
546        ) -> ClientBuilder {
547            self.ancillary_verification_key = ancillary_verification_key.into();
548            self
549        }
550    }
551
552    /// Set the [Logger] to use.
553    pub fn with_logger(mut self, logger: Logger) -> Self {
554        self.logger = Some(logger);
555        self
556    }
557
558    /// Set the origin tag.
559    pub fn with_origin_tag(mut self, origin_tag: Option<String>) -> Self {
560        self.origin_tag = origin_tag;
561        self
562    }
563
564    /// Set the client type.
565    pub fn with_client_type(mut self, client_type: Option<String>) -> Self {
566        self.client_type = client_type;
567        self
568    }
569
570    /// Sets the options to be used by the client.
571    pub fn with_options(mut self, options: ClientOptions) -> Self {
572        self.options = options;
573        self
574    }
575
576    /// Add a [feedback receiver][FeedbackReceiver] to receive [events][crate::feedback::MithrilEvent]
577    /// for tasks that can have a long duration (ie: snapshot download or a long certificate chain
578    /// validation).
579    pub fn add_feedback_receiver(mut self, receiver: Arc<dyn FeedbackReceiver>) -> Self {
580        self.feedback_receivers.push(receiver);
581        self
582    }
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588
589    fn default_headers() -> HashMap<String, String> {
590        HashMap::from([(
591            MITHRIL_CLIENT_TYPE_HEADER.to_string(),
592            DEFAULT_CLIENT_TYPE.to_string(),
593        )])
594    }
595
596    #[tokio::test]
597    async fn compute_http_headers_returns_options_http_headers() {
598        let http_headers = default_headers();
599        let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
600            .with_options(ClientOptions {
601                http_headers: Some(http_headers.clone()),
602            });
603
604        let computed_headers = client_builder.compute_http_headers();
605
606        assert_eq!(computed_headers, http_headers);
607    }
608
609    #[tokio::test]
610    async fn compute_http_headers_with_origin_tag_returns_options_http_headers_with_origin_tag() {
611        let http_headers = default_headers();
612        let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
613            .with_options(ClientOptions {
614                http_headers: Some(http_headers.clone()),
615            })
616            .with_origin_tag(Some("CLIENT_TAG".to_string()));
617        let mut expected_headers = http_headers.clone();
618        expected_headers.insert(
619            MITHRIL_ORIGIN_TAG_HEADER.to_string(),
620            "CLIENT_TAG".to_string(),
621        );
622
623        let computed_headers = client_builder.compute_http_headers();
624        assert_eq!(computed_headers, expected_headers);
625    }
626
627    #[tokio::test]
628    async fn test_with_origin_tag_not_overwrite_other_client_options_attributes() {
629        let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
630            .with_options(ClientOptions { http_headers: None })
631            .with_origin_tag(Some("TEST".to_string()));
632        assert_eq!(None, builder.options.http_headers);
633        assert_eq!(Some("TEST".to_string()), builder.origin_tag);
634
635        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
636        let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
637            .with_options(ClientOptions {
638                http_headers: Some(http_headers.clone()),
639            })
640            .with_origin_tag(Some("TEST".to_string()));
641        assert_eq!(Some(http_headers), builder.options.http_headers);
642        assert_eq!(Some("TEST".to_string()), builder.origin_tag);
643    }
644
645    #[tokio::test]
646    async fn test_with_origin_tag_can_be_unset() {
647        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
648        let client_options = ClientOptions {
649            http_headers: Some(http_headers.clone()),
650        };
651        let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
652            .with_options(client_options)
653            .with_origin_tag(None);
654
655        assert_eq!(Some(http_headers), builder.options.http_headers);
656        assert_eq!(None, builder.origin_tag);
657    }
658
659    #[tokio::test]
660    async fn compute_http_headers_with_client_type_returns_options_http_headers_with_client_type() {
661        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
662        let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
663            .with_options(ClientOptions {
664                http_headers: Some(http_headers.clone()),
665            })
666            .with_client_type(Some("CLIENT_TYPE".to_string()));
667
668        let computed_headers = client_builder.compute_http_headers();
669
670        assert_eq!(
671            computed_headers,
672            HashMap::from([
673                ("Key".to_string(), "Value".to_string()),
674                (
675                    MITHRIL_CLIENT_TYPE_HEADER.to_string(),
676                    "CLIENT_TYPE".to_string()
677                )
678            ])
679        );
680    }
681
682    #[tokio::test]
683    async fn compute_http_headers_with_options_containing_client_type_returns_client_type() {
684        let http_headers = HashMap::from([(
685            MITHRIL_CLIENT_TYPE_HEADER.to_string(),
686            "client type from options".to_string(),
687        )]);
688        let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
689            .with_options(ClientOptions {
690                http_headers: Some(http_headers.clone()),
691            });
692
693        let computed_headers = client_builder.compute_http_headers();
694
695        assert_eq!(computed_headers, http_headers);
696    }
697
698    #[tokio::test]
699    async fn test_with_client_type_not_overwrite_other_client_options_attributes() {
700        let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
701            .with_options(ClientOptions { http_headers: None })
702            .with_client_type(Some("TEST".to_string()));
703        assert_eq!(None, builder.options.http_headers);
704        assert_eq!(Some("TEST".to_string()), builder.client_type);
705
706        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
707        let builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
708            .with_options(ClientOptions {
709                http_headers: Some(http_headers.clone()),
710            })
711            .with_client_type(Some("TEST".to_string()));
712        assert_eq!(Some(http_headers), builder.options.http_headers);
713        assert_eq!(Some("TEST".to_string()), builder.client_type);
714    }
715
716    #[tokio::test]
717    async fn test_given_a_none_client_type_compute_http_headers_will_set_client_type_to_default_value()
718     {
719        let builder_without_client_type =
720            ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()));
721        let computed_headers = builder_without_client_type.compute_http_headers();
722
723        assert_eq!(
724            computed_headers,
725            HashMap::from([(
726                MITHRIL_CLIENT_TYPE_HEADER.to_string(),
727                DEFAULT_CLIENT_TYPE.to_string()
728            )])
729        );
730
731        let builder_with_none_client_type =
732            ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string())).with_client_type(None);
733        let computed_headers = builder_with_none_client_type.compute_http_headers();
734
735        assert_eq!(
736            computed_headers,
737            HashMap::from([(
738                MITHRIL_CLIENT_TYPE_HEADER.to_string(),
739                DEFAULT_CLIENT_TYPE.to_string()
740            )])
741        );
742    }
743
744    #[tokio::test]
745    async fn test_compute_http_headers_will_compute_client_type_header_from_struct_attribute_over_options()
746     {
747        let http_headers = HashMap::from([(
748            MITHRIL_CLIENT_TYPE_HEADER.to_string(),
749            "client type from options".to_string(),
750        )]);
751        let client_builder = ClientBuilder::new(AggregatorDiscoveryType::Url("".to_string()))
752            .with_options(ClientOptions {
753                http_headers: Some(http_headers.clone()),
754            })
755            .with_client_type(Some("client type".to_string()));
756
757        let computed_headers = client_builder.compute_http_headers();
758
759        assert_eq!(
760            computed_headers,
761            HashMap::from([(
762                MITHRIL_CLIENT_TYPE_HEADER.to_string(),
763                "client type".to_string()
764            )])
765        );
766    }
767}