mithril_client/cardano_database_client/
fetch.rs1use 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 common::Epoch,
10};
11
12pub struct InternalArtifactRetriever {
13 pub(super) aggregator_client: Arc<dyn AggregatorClient>,
14}
15
16impl InternalArtifactRetriever {
17 pub fn new(aggregator_client: Arc<dyn AggregatorClient>) -> Self {
19 Self { aggregator_client }
20 }
21
22 pub async fn list(&self) -> MithrilResult<Vec<CardanoDatabaseSnapshotListItem>> {
24 self.fetch_list_with_aggregator_request(AggregatorRequest::ListCardanoDatabaseSnapshots)
25 .await
26 }
27
28 pub async fn list_by_epoch(
30 &self,
31 epoch: Epoch,
32 ) -> MithrilResult<Vec<CardanoDatabaseSnapshotListItem>> {
33 self.fetch_list_with_aggregator_request(
34 AggregatorRequest::ListCardanoDatabaseSnapshotByEpoch { epoch },
35 )
36 .await
37 }
38
39 pub async fn list_for_latest_epoch(
41 &self,
42 ) -> MithrilResult<Vec<CardanoDatabaseSnapshotListItem>> {
43 self.fetch_list_with_aggregator_request(
44 AggregatorRequest::ListCardanoDatabaseSnapshotForLatestEpoch { offset: None },
45 )
46 .await
47 }
48
49 pub async fn list_for_latest_epoch_with_offset(
51 &self,
52 offset: u64,
53 ) -> MithrilResult<Vec<CardanoDatabaseSnapshotListItem>> {
54 self.fetch_list_with_aggregator_request(
55 AggregatorRequest::ListCardanoDatabaseSnapshotForLatestEpoch {
56 offset: Some(offset),
57 },
58 )
59 .await
60 }
61
62 pub async fn get(&self, hash: &str) -> MithrilResult<Option<CardanoDatabaseSnapshot>> {
64 self.fetch_with_aggregator_request(AggregatorRequest::GetCardanoDatabaseSnapshot {
65 hash: hash.to_string(),
66 })
67 .await
68 }
69
70 async fn fetch_with_aggregator_request<T: DeserializeOwned>(
73 &self,
74 request: AggregatorRequest,
75 ) -> MithrilResult<Option<T>> {
76 match self.aggregator_client.get_content(request).await {
77 Ok(content) => {
78 let result = serde_json::from_str(&content)
79 .with_context(|| "CardanoDatabase client can not deserialize artifact")?;
80
81 Ok(Some(result))
82 }
83 Err(AggregatorClientError::RemoteServerLogical(_)) => Ok(None),
84 Err(e) => Err(e.into()),
85 }
86 }
87
88 async fn fetch_list_with_aggregator_request(
89 &self,
90 request: AggregatorRequest,
91 ) -> MithrilResult<Vec<CardanoDatabaseSnapshotListItem>> {
92 let response = self
93 .aggregator_client
94 .get_content(request)
95 .await
96 .with_context(|| "CardanoDatabase client can not get the artifact list")?;
97
98 serde_json::from_str(&response)
99 .with_context(|| "CardanoDatabase client can not deserialize artifact list")
100 }
101}
102
103#[cfg(test)]
104mod tests {
105
106 use anyhow::anyhow;
107 use chrono::{DateTime, Utc};
108 use mockall::predicate::eq;
109
110 use mithril_common::entities::{CardanoDbBeacon, Epoch};
111 use mithril_common::test::double::Dummy;
112
113 use crate::aggregator_client;
114 use crate::cardano_database_client::CardanoDatabaseClientDependencyInjector;
115
116 use super::*;
117
118 fn config_aggregator_client_to_always_returns_invalid_json(
119 http_client: &mut aggregator_client::MockAggregatorClient,
120 ) {
121 http_client
122 .expect_get_content()
123 .returning(move |_| Ok("invalid json structure".to_string()));
124 }
125
126 fn fake_messages() -> Vec<CardanoDatabaseSnapshotListItem> {
127 vec![
128 CardanoDatabaseSnapshotListItem {
129 hash: "hash-123".to_string(),
130 merkle_root: "mkroot-123".to_string(),
131 beacon: CardanoDbBeacon {
132 epoch: Epoch(1),
133 immutable_file_number: 123,
134 },
135 certificate_hash: "cert-hash-123".to_string(),
136 total_db_size_uncompressed: 800796318,
137 created_at: DateTime::parse_from_rfc3339("2025-01-19T13:43:05.618857482Z")
138 .unwrap()
139 .with_timezone(&Utc),
140 cardano_node_version: "0.0.1".to_string(),
141 },
142 CardanoDatabaseSnapshotListItem {
143 hash: "hash-456".to_string(),
144 merkle_root: "mkroot-456".to_string(),
145 beacon: CardanoDbBeacon {
146 epoch: Epoch(2),
147 immutable_file_number: 456,
148 },
149 certificate_hash: "cert-hash-456".to_string(),
150 total_db_size_uncompressed: 2960713808,
151 created_at: DateTime::parse_from_rfc3339("2025-01-27T15:22:05.618857482Z")
152 .unwrap()
153 .with_timezone(&Utc),
154 cardano_node_version: "0.0.1".to_string(),
155 },
156 ]
157 }
158
159 mod list {
160
161 use super::*;
162
163 #[tokio::test]
164 async fn list_cardano_database_snapshots_returns_messages() {
165 let message = fake_messages();
166 let client = CardanoDatabaseClientDependencyInjector::new()
167 .with_aggregator_client_mock_config(|http_client| {
168 http_client
169 .expect_get_content()
170 .with(eq(AggregatorRequest::ListCardanoDatabaseSnapshots))
171 .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
172 })
173 .build_cardano_database_client();
174
175 let messages = client.list().await.unwrap();
176
177 assert_eq!(2, messages.len());
178 assert_eq!("hash-123".to_string(), messages[0].hash);
179 assert_eq!("hash-456".to_string(), messages[1].hash);
180 }
181
182 #[tokio::test]
183 async fn list_cardano_database_snapshots_returns_error_when_invalid_json_structure_in_response()
184 {
185 let client = CardanoDatabaseClientDependencyInjector::new()
186 .with_aggregator_client_mock_config(
187 config_aggregator_client_to_always_returns_invalid_json,
188 )
189 .build_cardano_database_client();
190
191 client
192 .list()
193 .await
194 .expect_err("List Cardano databases should return an error");
195 }
196
197 #[tokio::test]
198 async fn list_cardano_database_snapshots_by_epoch_returns_messages() {
199 let message = fake_messages();
200 let client = CardanoDatabaseClientDependencyInjector::new()
201 .with_aggregator_client_mock_config(|http_client| {
202 http_client
203 .expect_get_content()
204 .with(eq(AggregatorRequest::ListCardanoDatabaseSnapshotByEpoch {
205 epoch: Epoch(4),
206 }))
207 .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
208 })
209 .build_cardano_database_client();
210
211 let messages = client.list_by_epoch(Epoch(4)).await.unwrap();
212
213 assert_eq!(2, messages.len());
214 assert_eq!("hash-123".to_string(), messages[0].hash);
215 assert_eq!("hash-456".to_string(), messages[1].hash);
216 }
217
218 #[tokio::test]
219 async fn list_cardano_database_snapshots_returns_by_epoch_error_when_invalid_json_structure_in_response()
220 {
221 let client = CardanoDatabaseClientDependencyInjector::new()
222 .with_aggregator_client_mock_config(
223 config_aggregator_client_to_always_returns_invalid_json,
224 )
225 .build_cardano_database_client();
226
227 client
228 .list_by_epoch(Epoch(4))
229 .await
230 .expect_err("List Cardano databases should return an error");
231 }
232
233 #[tokio::test]
234 async fn list_cardano_database_snapshots_for_latest_epoch_returns_messages() {
235 let message = fake_messages();
236 let client = CardanoDatabaseClientDependencyInjector::new()
237 .with_aggregator_client_mock_config(|http_client| {
238 http_client
239 .expect_get_content()
240 .with(eq(
241 AggregatorRequest::ListCardanoDatabaseSnapshotForLatestEpoch {
242 offset: None,
243 },
244 ))
245 .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
246 })
247 .build_cardano_database_client();
248
249 let messages = client.list_for_latest_epoch().await.unwrap();
250
251 assert_eq!(2, messages.len());
252 assert_eq!("hash-123".to_string(), messages[0].hash);
253 assert_eq!("hash-456".to_string(), messages[1].hash);
254 }
255
256 #[tokio::test]
257 async fn list_cardano_database_snapshots_returns_for_latest_epoch_error_when_invalid_json_structure_in_response()
258 {
259 let client = CardanoDatabaseClientDependencyInjector::new()
260 .with_aggregator_client_mock_config(
261 config_aggregator_client_to_always_returns_invalid_json,
262 )
263 .build_cardano_database_client();
264
265 client
266 .list_for_latest_epoch()
267 .await
268 .expect_err("List Cardano databases should return an error");
269 }
270
271 #[tokio::test]
272 async fn list_cardano_database_snapshots_for_latest_epoch_with_offset_returns_messages() {
273 let message = fake_messages();
274 let client = CardanoDatabaseClientDependencyInjector::new()
275 .with_aggregator_client_mock_config(|http_client| {
276 http_client
277 .expect_get_content()
278 .with(eq(
279 AggregatorRequest::ListCardanoDatabaseSnapshotForLatestEpoch {
280 offset: Some(42),
281 },
282 ))
283 .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
284 })
285 .build_cardano_database_client();
286
287 let messages = client.list_for_latest_epoch_with_offset(42).await.unwrap();
288
289 assert_eq!(2, messages.len());
290 assert_eq!("hash-123".to_string(), messages[0].hash);
291 assert_eq!("hash-456".to_string(), messages[1].hash);
292 }
293
294 #[tokio::test]
295 async fn list_cardano_database_snapshots_returns_for_latest_epoch_with_offset_error_when_invalid_json_structure_in_response()
296 {
297 let client = CardanoDatabaseClientDependencyInjector::new()
298 .with_aggregator_client_mock_config(
299 config_aggregator_client_to_always_returns_invalid_json,
300 )
301 .build_cardano_database_client();
302
303 client
304 .list_for_latest_epoch_with_offset(42)
305 .await
306 .expect_err("List Cardano databases should return an error");
307 }
308 }
309
310 mod get {
311 use super::*;
312
313 #[tokio::test]
314 async fn get_cardano_database_snapshot_returns_message() {
315 let expected_cardano_database_snapshot = CardanoDatabaseSnapshot {
316 hash: "hash-123".to_string(),
317 ..CardanoDatabaseSnapshot::dummy()
318 };
319 let message = expected_cardano_database_snapshot.clone();
320 let client = CardanoDatabaseClientDependencyInjector::new()
321 .with_aggregator_client_mock_config(|http_client| {
322 http_client
323 .expect_get_content()
324 .with(eq(AggregatorRequest::GetCardanoDatabaseSnapshot {
325 hash: "hash-123".to_string(),
326 }))
327 .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
328 })
329 .build_cardano_database_client();
330
331 let cardano_database = client
332 .get("hash-123")
333 .await
334 .unwrap()
335 .expect("This test returns a Cardano database");
336
337 assert_eq!(expected_cardano_database_snapshot, cardano_database);
338 }
339
340 #[tokio::test]
341 async fn get_cardano_database_snapshot_returns_error_when_invalid_json_structure_in_response()
342 {
343 let client = CardanoDatabaseClientDependencyInjector::new()
344 .with_aggregator_client_mock_config(
345 config_aggregator_client_to_always_returns_invalid_json,
346 )
347 .build_cardano_database_client();
348
349 client
350 .get("hash-123")
351 .await
352 .expect_err("Get Cardano database should return an error");
353 }
354
355 #[tokio::test]
356 async fn get_cardano_database_snapshot_returns_none_when_not_found_or_remote_server_logical_error()
357 {
358 let client = CardanoDatabaseClientDependencyInjector::new()
359 .with_aggregator_client_mock_config(|http_client| {
360 http_client.expect_get_content().return_once(move |_| {
361 Err(AggregatorClientError::RemoteServerLogical(anyhow!(
362 "not found"
363 )))
364 });
365 })
366 .build_cardano_database_client();
367
368 let result = client.get("hash-123").await.unwrap();
369
370 assert!(result.is_none());
371 }
372
373 #[tokio::test]
374 async fn get_cardano_database_snapshot_returns_error() {
375 let client = CardanoDatabaseClientDependencyInjector::new()
376 .with_aggregator_client_mock_config(|http_client| {
377 http_client.expect_get_content().return_once(move |_| {
378 Err(AggregatorClientError::SubsystemError(anyhow!("error")))
379 });
380 })
381 .build_cardano_database_client();
382
383 client
384 .get("hash-123")
385 .await
386 .expect_err("Get Cardano database should return an error");
387 }
388 }
389}