mithril_client/cardano_database_client/
fetch.rs

1use std::sync::Arc;
2
3use anyhow::Context;
4use serde::de::DeserializeOwned;
5
6use crate::{
7    aggregator_client::{AggregatorClient, AggregatorClientError, AggregatorRequest},
8    CardanoDatabaseSnapshot, CardanoDatabaseSnapshotListItem, MithrilResult,
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
70    use crate::cardano_database_client::CardanoDatabaseClientDependencyInjector;
71
72    use super::*;
73
74    fn fake_messages() -> Vec<CardanoDatabaseSnapshotListItem> {
75        vec![
76            CardanoDatabaseSnapshotListItem {
77                hash: "hash-123".to_string(),
78                merkle_root: "mkroot-123".to_string(),
79                beacon: CardanoDbBeacon {
80                    epoch: Epoch(1),
81                    immutable_file_number: 123,
82                },
83                certificate_hash: "cert-hash-123".to_string(),
84                total_db_size_uncompressed: 800796318,
85                created_at: DateTime::parse_from_rfc3339("2025-01-19T13:43:05.618857482Z")
86                    .unwrap()
87                    .with_timezone(&Utc),
88                cardano_node_version: "0.0.1".to_string(),
89            },
90            CardanoDatabaseSnapshotListItem {
91                hash: "hash-456".to_string(),
92                merkle_root: "mkroot-456".to_string(),
93                beacon: CardanoDbBeacon {
94                    epoch: Epoch(2),
95                    immutable_file_number: 456,
96                },
97                certificate_hash: "cert-hash-456".to_string(),
98                total_db_size_uncompressed: 2960713808,
99                created_at: DateTime::parse_from_rfc3339("2025-01-27T15:22:05.618857482Z")
100                    .unwrap()
101                    .with_timezone(&Utc),
102                cardano_node_version: "0.0.1".to_string(),
103            },
104        ]
105    }
106
107    mod list {
108
109        use super::*;
110
111        #[tokio::test]
112        async fn list_cardano_database_snapshots_returns_messages() {
113            let message = fake_messages();
114            let client = CardanoDatabaseClientDependencyInjector::new()
115                .with_aggregator_client_mock_config(|http_client| {
116                    http_client
117                        .expect_get_content()
118                        .with(eq(AggregatorRequest::ListCardanoDatabaseSnapshots))
119                        .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
120                })
121                .build_cardano_database_client();
122
123            let messages = client.list().await.unwrap();
124
125            assert_eq!(2, messages.len());
126            assert_eq!("hash-123".to_string(), messages[0].hash);
127            assert_eq!("hash-456".to_string(), messages[1].hash);
128        }
129
130        #[tokio::test]
131        async fn list_cardano_database_snapshots_returns_error_when_invalid_json_structure_in_response(
132        ) {
133            let client = CardanoDatabaseClientDependencyInjector::new()
134                .with_aggregator_client_mock_config(|http_client| {
135                    http_client
136                        .expect_get_content()
137                        .return_once(move |_| Ok("invalid json structure".to_string()));
138                })
139                .build_cardano_database_client();
140
141            client
142                .list()
143                .await
144                .expect_err("List Cardano databases should return an error");
145        }
146    }
147
148    mod get {
149        use super::*;
150
151        #[tokio::test]
152        async fn get_cardano_database_snapshot_returns_message() {
153            let expected_cardano_database_snapshot = CardanoDatabaseSnapshot {
154                hash: "hash-123".to_string(),
155                ..CardanoDatabaseSnapshot::dummy()
156            };
157            let message = expected_cardano_database_snapshot.clone();
158            let client = CardanoDatabaseClientDependencyInjector::new()
159                .with_aggregator_client_mock_config(|http_client| {
160                    http_client
161                        .expect_get_content()
162                        .with(eq(AggregatorRequest::GetCardanoDatabaseSnapshot {
163                            hash: "hash-123".to_string(),
164                        }))
165                        .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
166                })
167                .build_cardano_database_client();
168
169            let cardano_database = client
170                .get("hash-123")
171                .await
172                .unwrap()
173                .expect("This test returns a Cardano database");
174
175            assert_eq!(expected_cardano_database_snapshot, cardano_database);
176        }
177
178        #[tokio::test]
179        async fn get_cardano_database_snapshot_returns_error_when_invalid_json_structure_in_response(
180        ) {
181            let client = CardanoDatabaseClientDependencyInjector::new()
182                .with_aggregator_client_mock_config(|http_client| {
183                    http_client
184                        .expect_get_content()
185                        .return_once(move |_| Ok("invalid json structure".to_string()));
186                })
187                .build_cardano_database_client();
188
189            client
190                .get("hash-123")
191                .await
192                .expect_err("Get Cardano database should return an error");
193        }
194
195        #[tokio::test]
196        async fn get_cardano_database_snapshot_returns_none_when_not_found_or_remote_server_logical_error(
197        ) {
198            let client = CardanoDatabaseClientDependencyInjector::new()
199                .with_aggregator_client_mock_config(|http_client| {
200                    http_client.expect_get_content().return_once(move |_| {
201                        Err(AggregatorClientError::RemoteServerLogical(anyhow!(
202                            "not found"
203                        )))
204                    });
205                })
206                .build_cardano_database_client();
207
208            let result = client.get("hash-123").await.unwrap();
209
210            assert!(result.is_none());
211        }
212
213        #[tokio::test]
214        async fn get_cardano_database_snapshot_returns_error() {
215            let client = CardanoDatabaseClientDependencyInjector::new()
216                .with_aggregator_client_mock_config(|http_client| {
217                    http_client.expect_get_content().return_once(move |_| {
218                        Err(AggregatorClientError::SubsystemError(anyhow!("error")))
219                    });
220                })
221                .build_cardano_database_client();
222
223            client
224                .get("hash-123")
225                .await
226                .expect_err("Get Cardano database should return an error");
227        }
228    }
229}