mithril_client/cardano_database_client/
fetch.rs

1use std::sync::Arc;
2
3use anyhow::Context;
4use serde::de::DeserializeOwned;
5
6use crate::{
7    CardanoDatabaseSnapshot, CardanoDatabaseSnapshotListItem, MithrilResult,
8    aggregator_client::{AggregatorClient, AggregatorClientError, AggregatorRequest},
9};
10
11pub struct InternalArtifactRetriever {
12    pub(super) aggregator_client: Arc<dyn AggregatorClient>,
13}
14
15impl InternalArtifactRetriever {
16    /// Constructs a new `InternalArtifactRetriever`
17    pub fn new(aggregator_client: Arc<dyn AggregatorClient>) -> Self {
18        Self { aggregator_client }
19    }
20
21    /// Fetch a list of signed CardanoDatabase
22    pub async fn list(&self) -> MithrilResult<Vec<CardanoDatabaseSnapshotListItem>> {
23        let response = self
24            .aggregator_client
25            .get_content(AggregatorRequest::ListCardanoDatabaseSnapshots)
26            .await
27            .with_context(|| "CardanoDatabase client can not get the artifact list")?;
28        let items = serde_json::from_str::<Vec<CardanoDatabaseSnapshotListItem>>(&response)
29            .with_context(|| "CardanoDatabase client can not deserialize artifact list")?;
30
31        Ok(items)
32    }
33
34    /// Get the given Cardano database data by hash.
35    pub async fn get(&self, hash: &str) -> MithrilResult<Option<CardanoDatabaseSnapshot>> {
36        self.fetch_with_aggregator_request(AggregatorRequest::GetCardanoDatabaseSnapshot {
37            hash: hash.to_string(),
38        })
39        .await
40    }
41
42    /// Fetch the given Cardano database data with an aggregator request.
43    /// If it cannot be found, a None is returned.
44    async fn fetch_with_aggregator_request<T: DeserializeOwned>(
45        &self,
46        request: AggregatorRequest,
47    ) -> MithrilResult<Option<T>> {
48        match self.aggregator_client.get_content(request).await {
49            Ok(content) => {
50                let result = serde_json::from_str(&content)
51                    .with_context(|| "CardanoDatabase client can not deserialize artifact")?;
52
53                Ok(Some(result))
54            }
55            Err(AggregatorClientError::RemoteServerLogical(_)) => Ok(None),
56            Err(e) => Err(e.into()),
57        }
58    }
59}
60
61#[cfg(test)]
62mod tests {
63
64    use anyhow::anyhow;
65    use chrono::{DateTime, Utc};
66    use mockall::predicate::eq;
67
68    use mithril_common::entities::{CardanoDbBeacon, Epoch};
69    use mithril_common::test::double::Dummy;
70
71    use crate::cardano_database_client::CardanoDatabaseClientDependencyInjector;
72
73    use super::*;
74
75    fn fake_messages() -> Vec<CardanoDatabaseSnapshotListItem> {
76        vec![
77            CardanoDatabaseSnapshotListItem {
78                hash: "hash-123".to_string(),
79                merkle_root: "mkroot-123".to_string(),
80                beacon: CardanoDbBeacon {
81                    epoch: Epoch(1),
82                    immutable_file_number: 123,
83                },
84                certificate_hash: "cert-hash-123".to_string(),
85                total_db_size_uncompressed: 800796318,
86                created_at: DateTime::parse_from_rfc3339("2025-01-19T13:43:05.618857482Z")
87                    .unwrap()
88                    .with_timezone(&Utc),
89                cardano_node_version: "0.0.1".to_string(),
90            },
91            CardanoDatabaseSnapshotListItem {
92                hash: "hash-456".to_string(),
93                merkle_root: "mkroot-456".to_string(),
94                beacon: CardanoDbBeacon {
95                    epoch: Epoch(2),
96                    immutable_file_number: 456,
97                },
98                certificate_hash: "cert-hash-456".to_string(),
99                total_db_size_uncompressed: 2960713808,
100                created_at: DateTime::parse_from_rfc3339("2025-01-27T15:22:05.618857482Z")
101                    .unwrap()
102                    .with_timezone(&Utc),
103                cardano_node_version: "0.0.1".to_string(),
104            },
105        ]
106    }
107
108    mod list {
109
110        use super::*;
111
112        #[tokio::test]
113        async fn list_cardano_database_snapshots_returns_messages() {
114            let message = fake_messages();
115            let client = CardanoDatabaseClientDependencyInjector::new()
116                .with_aggregator_client_mock_config(|http_client| {
117                    http_client
118                        .expect_get_content()
119                        .with(eq(AggregatorRequest::ListCardanoDatabaseSnapshots))
120                        .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
121                })
122                .build_cardano_database_client();
123
124            let messages = client.list().await.unwrap();
125
126            assert_eq!(2, messages.len());
127            assert_eq!("hash-123".to_string(), messages[0].hash);
128            assert_eq!("hash-456".to_string(), messages[1].hash);
129        }
130
131        #[tokio::test]
132        async fn list_cardano_database_snapshots_returns_error_when_invalid_json_structure_in_response()
133         {
134            let client = CardanoDatabaseClientDependencyInjector::new()
135                .with_aggregator_client_mock_config(|http_client| {
136                    http_client
137                        .expect_get_content()
138                        .return_once(move |_| Ok("invalid json structure".to_string()));
139                })
140                .build_cardano_database_client();
141
142            client
143                .list()
144                .await
145                .expect_err("List Cardano databases should return an error");
146        }
147    }
148
149    mod get {
150        use super::*;
151
152        #[tokio::test]
153        async fn get_cardano_database_snapshot_returns_message() {
154            let expected_cardano_database_snapshot = CardanoDatabaseSnapshot {
155                hash: "hash-123".to_string(),
156                ..CardanoDatabaseSnapshot::dummy()
157            };
158            let message = expected_cardano_database_snapshot.clone();
159            let client = CardanoDatabaseClientDependencyInjector::new()
160                .with_aggregator_client_mock_config(|http_client| {
161                    http_client
162                        .expect_get_content()
163                        .with(eq(AggregatorRequest::GetCardanoDatabaseSnapshot {
164                            hash: "hash-123".to_string(),
165                        }))
166                        .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
167                })
168                .build_cardano_database_client();
169
170            let cardano_database = client
171                .get("hash-123")
172                .await
173                .unwrap()
174                .expect("This test returns a Cardano database");
175
176            assert_eq!(expected_cardano_database_snapshot, cardano_database);
177        }
178
179        #[tokio::test]
180        async fn get_cardano_database_snapshot_returns_error_when_invalid_json_structure_in_response()
181         {
182            let client = CardanoDatabaseClientDependencyInjector::new()
183                .with_aggregator_client_mock_config(|http_client| {
184                    http_client
185                        .expect_get_content()
186                        .return_once(move |_| Ok("invalid json structure".to_string()));
187                })
188                .build_cardano_database_client();
189
190            client
191                .get("hash-123")
192                .await
193                .expect_err("Get Cardano database should return an error");
194        }
195
196        #[tokio::test]
197        async fn get_cardano_database_snapshot_returns_none_when_not_found_or_remote_server_logical_error()
198         {
199            let client = CardanoDatabaseClientDependencyInjector::new()
200                .with_aggregator_client_mock_config(|http_client| {
201                    http_client.expect_get_content().return_once(move |_| {
202                        Err(AggregatorClientError::RemoteServerLogical(anyhow!(
203                            "not found"
204                        )))
205                    });
206                })
207                .build_cardano_database_client();
208
209            let result = client.get("hash-123").await.unwrap();
210
211            assert!(result.is_none());
212        }
213
214        #[tokio::test]
215        async fn get_cardano_database_snapshot_returns_error() {
216            let client = CardanoDatabaseClientDependencyInjector::new()
217                .with_aggregator_client_mock_config(|http_client| {
218                    http_client.expect_get_content().return_once(move |_| {
219                        Err(AggregatorClientError::SubsystemError(anyhow!("error")))
220                    });
221                })
222                .build_cardano_database_client();
223
224            client
225                .get("hash-123")
226                .await
227                .expect_err("Get Cardano database should return an error");
228        }
229    }
230}