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