1use super::NetworkRepository;
9use crate::models::{NetworkRepoModel, NetworkType, RepositoryError};
10use crate::repositories::redis_base::RedisRepository;
11use crate::repositories::{BatchRetrievalResult, PaginatedResult, PaginationQuery, Repository};
12use async_trait::async_trait;
13use redis::aio::ConnectionManager;
14use redis::AsyncCommands;
15use std::fmt;
16use std::sync::Arc;
17use tracing::{debug, error, warn};
18
19const NETWORK_PREFIX: &str = "network";
20const NETWORK_LIST_KEY: &str = "network_list";
21const NETWORK_NAME_INDEX_PREFIX: &str = "network_name";
22const NETWORK_CHAIN_ID_INDEX_PREFIX: &str = "network_chain_id";
23
24#[derive(Clone)]
25pub struct RedisNetworkRepository {
26 pub client: Arc<ConnectionManager>,
27 pub key_prefix: String,
28}
29
30impl RedisRepository for RedisNetworkRepository {}
31
32impl RedisNetworkRepository {
33 pub fn new(
34 connection_manager: Arc<ConnectionManager>,
35 key_prefix: String,
36 ) -> Result<Self, RepositoryError> {
37 if key_prefix.is_empty() {
38 return Err(RepositoryError::InvalidData(
39 "Redis key prefix cannot be empty".to_string(),
40 ));
41 }
42
43 Ok(Self {
44 client: connection_manager,
45 key_prefix,
46 })
47 }
48
49 fn network_key(&self, network_id: &str) -> String {
51 format!("{}:{}:{}", self.key_prefix, NETWORK_PREFIX, network_id)
52 }
53
54 fn network_list_key(&self) -> String {
56 format!("{}:{}", self.key_prefix, NETWORK_LIST_KEY)
57 }
58
59 fn network_name_index_key(&self, network_type: &NetworkType, name: &str) -> String {
61 format!(
62 "{}:{}:{}:{}",
63 self.key_prefix, NETWORK_NAME_INDEX_PREFIX, network_type, name
64 )
65 }
66
67 fn network_chain_id_index_key(&self, network_type: &NetworkType, chain_id: u64) -> String {
69 format!(
70 "{}:{}:{}:{}",
71 self.key_prefix, NETWORK_CHAIN_ID_INDEX_PREFIX, network_type, chain_id
72 )
73 }
74
75 fn extract_chain_id(&self, network: &NetworkRepoModel) -> Option<u64> {
77 match &network.config {
78 crate::models::NetworkConfigData::Evm(evm_config) => evm_config.chain_id,
79 _ => None,
80 }
81 }
82
83 async fn update_indexes(
85 &self,
86 network: &NetworkRepoModel,
87 old_network: Option<&NetworkRepoModel>,
88 ) -> Result<(), RepositoryError> {
89 let mut conn = self.client.as_ref().clone();
90 let mut pipe = redis::pipe();
91 pipe.atomic();
92
93 debug!(network_id = %network.id, "updating indexes for network");
94
95 let name_key = self.network_name_index_key(&network.network_type, &network.name);
97 pipe.set(&name_key, &network.id);
98
99 if let Some(chain_id) = self.extract_chain_id(network) {
101 let chain_id_key = self.network_chain_id_index_key(&network.network_type, chain_id);
102 pipe.set(&chain_id_key, &network.id);
103 debug!(network_id = %network.id, chain_id = %chain_id, "added chain ID index for network");
104 }
105
106 if let Some(old) = old_network {
108 if old.name != network.name || old.network_type != network.network_type {
110 let old_name_key = self.network_name_index_key(&old.network_type, &old.name);
111 pipe.del(&old_name_key);
112 debug!(network_id = %network.id, old_name = %old.name, new_name = %network.name, "removing old name index for network");
113 }
114
115 let old_chain_id = self.extract_chain_id(old);
117 let new_chain_id = self.extract_chain_id(network);
118
119 if old_chain_id != new_chain_id {
120 if let Some(old_chain_id) = old_chain_id {
121 let old_chain_id_key =
122 self.network_chain_id_index_key(&old.network_type, old_chain_id);
123 pipe.del(&old_chain_id_key);
124 debug!(network_id = %network.id, old_chain_id = %old_chain_id, new_chain_id = ?new_chain_id, "removing old chain ID index for network");
125 }
126 }
127 }
128
129 pipe.exec_async(&mut conn).await.map_err(|e| {
131 error!(network_id = %network.id, error = %e, "index update pipeline failed for network");
132 self.map_redis_error(e, &format!("update_indexes_for_network_{}", network.id))
133 })?;
134
135 debug!(network_id = %network.id, "successfully updated indexes for network");
136 Ok(())
137 }
138
139 async fn remove_all_indexes(&self, network: &NetworkRepoModel) -> Result<(), RepositoryError> {
141 let mut conn = self.client.as_ref().clone();
142 let mut pipe = redis::pipe();
143 pipe.atomic();
144
145 debug!(network_id = %network.id, "removing all indexes for network");
146
147 let name_key = self.network_name_index_key(&network.network_type, &network.name);
149 pipe.del(&name_key);
150
151 if let Some(chain_id) = self.extract_chain_id(network) {
153 let chain_id_key = self.network_chain_id_index_key(&network.network_type, chain_id);
154 pipe.del(&chain_id_key);
155 debug!(network_id = %network.id, chain_id = %chain_id, "removing chain ID index for network");
156 }
157
158 pipe.exec_async(&mut conn).await.map_err(|e| {
159 error!(network_id = %network.id, error = %e, "index removal failed for network");
160 self.map_redis_error(e, &format!("remove_indexes_for_network_{}", network.id))
161 })?;
162
163 debug!(network_id = %network.id, "successfully removed all indexes for network");
164 Ok(())
165 }
166
167 async fn get_networks_by_ids(
169 &self,
170 ids: &[String],
171 ) -> Result<BatchRetrievalResult<NetworkRepoModel>, RepositoryError> {
172 if ids.is_empty() {
173 debug!("no network IDs provided for batch fetch");
174 return Ok(BatchRetrievalResult {
175 results: vec![],
176 failed_ids: vec![],
177 });
178 }
179
180 let mut conn = self.client.as_ref().clone();
181 let keys: Vec<String> = ids.iter().map(|id| self.network_key(id)).collect();
182
183 debug!(count = %ids.len(), "batch fetching networks");
184
185 let values: Vec<Option<String>> = conn
186 .mget(&keys)
187 .await
188 .map_err(|e| self.map_redis_error(e, "batch_fetch_networks"))?;
189
190 let mut networks = Vec::new();
191 let mut failed_count = 0;
192 let mut failed_ids = Vec::new();
193
194 for (i, value) in values.into_iter().enumerate() {
195 match value {
196 Some(json) => {
197 match self.deserialize_entity::<NetworkRepoModel>(&json, &ids[i], "network") {
198 Ok(network) => networks.push(network),
199 Err(e) => {
200 failed_count += 1;
201 error!(network_id = %ids[i], error = %e, "failed to deserialize network");
202 failed_ids.push(ids[i].clone());
203 }
204 }
205 }
206 None => {
207 warn!(network_id = %ids[i], "network not found in batch fetch");
208 }
209 }
210 }
211
212 if failed_count > 0 {
213 warn!(failed_count = %failed_count, total_count = %ids.len(), "failed to deserialize networks in batch");
214 warn!(failed_ids = ?failed_ids, "failed to deserialize networks");
215 }
216
217 debug!(count = %networks.len(), "successfully fetched networks");
218 Ok(BatchRetrievalResult {
219 results: networks,
220 failed_ids,
221 })
222 }
223}
224
225impl fmt::Debug for RedisNetworkRepository {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 f.debug_struct("RedisNetworkRepository")
228 .field("client", &"<ConnectionManager>")
229 .field("key_prefix", &self.key_prefix)
230 .finish()
231 }
232}
233
234#[async_trait]
235impl Repository<NetworkRepoModel, String> for RedisNetworkRepository {
236 async fn create(&self, entity: NetworkRepoModel) -> Result<NetworkRepoModel, RepositoryError> {
237 if entity.id.is_empty() {
238 return Err(RepositoryError::InvalidData(
239 "Network ID cannot be empty".to_string(),
240 ));
241 }
242 if entity.name.is_empty() {
243 return Err(RepositoryError::InvalidData(
244 "Network name cannot be empty".to_string(),
245 ));
246 }
247 let key = self.network_key(&entity.id);
248 let network_list_key = self.network_list_key();
249 let mut conn = self.client.as_ref().clone();
250
251 debug!(network_id = %entity.id, "creating network");
252
253 let value = self.serialize_entity(&entity, |n| &n.id, "network")?;
254
255 let existing: Option<String> = conn
257 .get(&key)
258 .await
259 .map_err(|e| self.map_redis_error(e, "create_network_check_existing"))?;
260
261 if existing.is_some() {
262 warn!(network_id = %entity.id, "attempted to create network that already exists");
263 return Err(RepositoryError::ConstraintViolation(format!(
264 "Network with ID {} already exists",
265 entity.id
266 )));
267 }
268
269 let mut pipe = redis::pipe();
271 pipe.set(&key, &value);
272 pipe.sadd(&network_list_key, &entity.id);
273
274 pipe.exec_async(&mut conn)
275 .await
276 .map_err(|e| self.map_redis_error(e, "create_network_pipeline"))?;
277
278 self.update_indexes(&entity, None).await?;
280
281 debug!(network_id = %entity.id, "successfully created network");
282 Ok(entity)
283 }
284
285 async fn get_by_id(&self, id: String) -> Result<NetworkRepoModel, RepositoryError> {
286 if id.is_empty() {
287 return Err(RepositoryError::InvalidData(
288 "Network ID cannot be empty".to_string(),
289 ));
290 }
291
292 let key = self.network_key(&id);
293 let mut conn = self.client.as_ref().clone();
294
295 debug!(network_id = %id, "retrieving network");
296
297 let network_data: Option<String> = conn
298 .get(&key)
299 .await
300 .map_err(|e| self.map_redis_error(e, "get_network_by_id"))?;
301
302 match network_data {
303 Some(data) => {
304 let network = self.deserialize_entity::<NetworkRepoModel>(&data, &id, "network")?;
305 debug!(network_id = %id, "successfully retrieved network");
306 Ok(network)
307 }
308 None => {
309 debug!(network_id = %id, "network not found");
310 Err(RepositoryError::NotFound(format!(
311 "Network with ID {id} not found"
312 )))
313 }
314 }
315 }
316
317 async fn list_all(&self) -> Result<Vec<NetworkRepoModel>, RepositoryError> {
318 let network_list_key = self.network_list_key();
319 let mut conn = self.client.as_ref().clone();
320
321 debug!("listing all networks");
322
323 let ids: Vec<String> = conn
324 .smembers(&network_list_key)
325 .await
326 .map_err(|e| self.map_redis_error(e, "list_all_networks"))?;
327
328 if ids.is_empty() {
329 debug!("no networks found");
330 return Ok(Vec::new());
331 }
332
333 let networks = self.get_networks_by_ids(&ids).await?;
334 debug!(count = %networks.results.len(), "successfully retrieved networks");
335 Ok(networks.results)
336 }
337
338 async fn list_paginated(
339 &self,
340 query: PaginationQuery,
341 ) -> Result<PaginatedResult<NetworkRepoModel>, RepositoryError> {
342 if query.per_page == 0 {
343 return Err(RepositoryError::InvalidData(
344 "per_page must be greater than 0".to_string(),
345 ));
346 }
347
348 let network_list_key = self.network_list_key();
349 let mut conn = self.client.as_ref().clone();
350
351 debug!(page = %query.page, per_page = %query.per_page, "listing paginated networks");
352
353 let all_ids: Vec<String> = conn
354 .smembers(&network_list_key)
355 .await
356 .map_err(|e| self.map_redis_error(e, "list_paginated_networks"))?;
357
358 let total = all_ids.len() as u64;
359 let per_page = query.per_page as usize;
360 let page = query.page as usize;
361 let total_pages = all_ids.len().div_ceil(per_page);
362
363 if page > total_pages && !all_ids.is_empty() {
364 debug!(requested_page = %page, total_pages = %total_pages, "requested page exceeds total pages");
365 return Ok(PaginatedResult {
366 items: Vec::new(),
367 total,
368 page: query.page,
369 per_page: query.per_page,
370 });
371 }
372
373 let start_idx = (page - 1) * per_page;
374 let end_idx = std::cmp::min(start_idx + per_page, all_ids.len());
375
376 let page_ids = all_ids[start_idx..end_idx].to_vec();
377 let networks = self.get_networks_by_ids(&page_ids).await?;
378
379 debug!(count = %networks.results.len(), page = %query.page, "successfully retrieved networks for page");
380 Ok(PaginatedResult {
381 items: networks.results.clone(),
382 total,
383 page: query.page,
384 per_page: query.per_page,
385 })
386 }
387
388 async fn update(
389 &self,
390 id: String,
391 entity: NetworkRepoModel,
392 ) -> Result<NetworkRepoModel, RepositoryError> {
393 if id.is_empty() {
394 return Err(RepositoryError::InvalidData(
395 "Network ID cannot be empty".to_string(),
396 ));
397 }
398
399 if id != entity.id {
400 return Err(RepositoryError::InvalidData(format!(
401 "ID mismatch: provided ID '{}' doesn't match network ID '{}'",
402 id, entity.id
403 )));
404 }
405
406 let key = self.network_key(&id);
407 let mut conn = self.client.as_ref().clone();
408
409 debug!(network_id = %id, "updating network");
410
411 let old_network = self.get_by_id(id.clone()).await?;
413
414 let value = self.serialize_entity(&entity, |n| &n.id, "network")?;
415
416 let _: () = conn
417 .set(&key, &value)
418 .await
419 .map_err(|e| self.map_redis_error(e, "update_network"))?;
420
421 self.update_indexes(&entity, Some(&old_network)).await?;
423
424 debug!(network_id = %id, "successfully updated network");
425 Ok(entity)
426 }
427
428 async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
429 if id.is_empty() {
430 return Err(RepositoryError::InvalidData(
431 "Network ID cannot be empty".to_string(),
432 ));
433 }
434
435 let key = self.network_key(&id);
436 let network_list_key = self.network_list_key();
437 let mut conn = self.client.as_ref().clone();
438
439 debug!(network_id = %id, "deleting network");
440
441 let network = self.get_by_id(id.clone()).await?;
443
444 let mut pipe = redis::pipe();
446 pipe.del(&key);
447 pipe.srem(&network_list_key, &id);
448
449 pipe.exec_async(&mut conn)
450 .await
451 .map_err(|e| self.map_redis_error(e, "delete_network_pipeline"))?;
452
453 if let Err(e) = self.remove_all_indexes(&network).await {
455 error!(network_id = %id, error = %e, "failed to remove indexes for deleted network");
456 }
457
458 debug!(network_id = %id, "successfully deleted network");
459 Ok(())
460 }
461
462 async fn count(&self) -> Result<usize, RepositoryError> {
463 let network_list_key = self.network_list_key();
464 let mut conn = self.client.as_ref().clone();
465
466 debug!("counting networks");
467
468 let count: usize = conn
469 .scard(&network_list_key)
470 .await
471 .map_err(|e| self.map_redis_error(e, "count_networks"))?;
472
473 debug!(count = %count, "total networks count");
474 Ok(count)
475 }
476
477 async fn has_entries(&self) -> Result<bool, RepositoryError> {
480 let network_list_key = self.network_list_key();
481 let mut conn = self.client.as_ref().clone();
482
483 debug!("checking if network storage has entries");
484
485 let exists: bool = conn
486 .exists(&network_list_key)
487 .await
488 .map_err(|e| self.map_redis_error(e, "check_network_entries_exist"))?;
489
490 debug!(exists = %exists, "network storage has entries");
491 Ok(exists)
492 }
493
494 async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
498 let mut conn = self.client.as_ref().clone();
499
500 debug!("starting to drop all network entries from Redis storage");
501
502 let network_list_key = self.network_list_key();
504 let network_ids: Vec<String> = conn
505 .smembers(&network_list_key)
506 .await
507 .map_err(|e| self.map_redis_error(e, "get_network_ids_for_cleanup"))?;
508
509 if network_ids.is_empty() {
510 debug!("no network entries found to clean up");
511 return Ok(());
512 }
513
514 debug!(count = %network_ids.len(), "found networks to clean up");
515
516 let networks_result = self.get_networks_by_ids(&network_ids).await?;
518 let networks = networks_result.results;
519
520 let mut pipe = redis::pipe();
522 pipe.atomic();
523
524 for network_id in &network_ids {
526 let network_key = self.network_key(network_id);
527 pipe.del(&network_key);
528 }
529
530 for network in &networks {
532 let name_key = self.network_name_index_key(&network.network_type, &network.name);
534 pipe.del(&name_key);
535
536 if let Some(chain_id) = self.extract_chain_id(network) {
538 let chain_id_key = self.network_chain_id_index_key(&network.network_type, chain_id);
539 pipe.del(&chain_id_key);
540 }
541 }
542
543 pipe.del(&network_list_key);
545
546 pipe.exec_async(&mut conn).await.map_err(|e| {
548 error!(error = %e, "failed to execute cleanup pipeline");
549 self.map_redis_error(e, "drop_all_network_entries_pipeline")
550 })?;
551
552 debug!("successfully dropped all network entries from Redis storage");
553 Ok(())
554 }
555}
556
557#[async_trait]
558impl NetworkRepository for RedisNetworkRepository {
559 async fn get_by_name(
560 &self,
561 network_type: NetworkType,
562 name: &str,
563 ) -> Result<Option<NetworkRepoModel>, RepositoryError> {
564 if name.is_empty() {
565 return Err(RepositoryError::InvalidData(
566 "Network name cannot be empty".to_string(),
567 ));
568 }
569
570 let mut conn = self.client.as_ref().clone();
571
572 debug!(name = %name, network_type = ?network_type, "getting network by name");
573
574 let name_index_key = self.network_name_index_key(&network_type, name);
576 let network_id: Option<String> = conn
577 .get(&name_index_key)
578 .await
579 .map_err(|e| self.map_redis_error(e, "get_network_by_name_index"))?;
580
581 match network_id {
582 Some(id) => {
583 match self.get_by_id(id.clone()).await {
584 Ok(network) => {
585 debug!(name = %name, "found network by name");
586 Ok(Some(network))
587 }
588 Err(RepositoryError::NotFound(_)) => {
589 warn!(network_type = ?network_type, name = %name, "stale name index found for network");
591 Ok(None)
592 }
593 Err(e) => Err(e),
594 }
595 }
596 None => {
597 debug!(name = %name, "network not found by name");
598 Ok(None)
599 }
600 }
601 }
602
603 async fn get_by_chain_id(
604 &self,
605 network_type: NetworkType,
606 chain_id: u64,
607 ) -> Result<Option<NetworkRepoModel>, RepositoryError> {
608 if network_type != NetworkType::Evm {
610 return Ok(None);
611 }
612
613 let mut conn = self.client.as_ref().clone();
614
615 debug!(chain_id = %chain_id, network_type = ?network_type, "getting network by chain ID");
616
617 let chain_id_index_key = self.network_chain_id_index_key(&network_type, chain_id);
619 let network_id: Option<String> = conn
620 .get(&chain_id_index_key)
621 .await
622 .map_err(|e| self.map_redis_error(e, "get_network_by_chain_id_index"))?;
623
624 match network_id {
625 Some(id) => {
626 match self.get_by_id(id.clone()).await {
627 Ok(network) => {
628 debug!(chain_id = %chain_id, "found network by chain ID");
629 Ok(Some(network))
630 }
631 Err(RepositoryError::NotFound(_)) => {
632 warn!(network_type = ?network_type, chain_id = %chain_id, "stale chain ID index found for network");
634 Ok(None)
635 }
636 Err(e) => Err(e),
637 }
638 }
639 None => {
640 debug!(chain_id = %chain_id, "network not found by chain ID");
641 Ok(None)
642 }
643 }
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650 use crate::config::{
651 EvmNetworkConfig, NetworkConfigCommon, SolanaNetworkConfig, StellarNetworkConfig,
652 };
653 use crate::models::NetworkConfigData;
654 use redis::aio::ConnectionManager;
655 use uuid::Uuid;
656
657 fn create_test_network(name: &str, network_type: NetworkType) -> NetworkRepoModel {
658 let common = NetworkConfigCommon {
659 network: name.to_string(),
660 from: None,
661 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
662 explorer_urls: None,
663 average_blocktime_ms: Some(12000),
664 is_testnet: Some(true),
665 tags: None,
666 };
667
668 match network_type {
669 NetworkType::Evm => {
670 let evm_config = EvmNetworkConfig {
671 common,
672 chain_id: Some(1),
673 required_confirmations: Some(1),
674 features: None,
675 symbol: Some("ETH".to_string()),
676 gas_price_cache: None,
677 };
678 NetworkRepoModel::new_evm(evm_config)
679 }
680 NetworkType::Solana => {
681 let solana_config = SolanaNetworkConfig { common };
682 NetworkRepoModel::new_solana(solana_config)
683 }
684 NetworkType::Stellar => {
685 let stellar_config = StellarNetworkConfig {
686 common,
687 passphrase: None,
688 horizon_url: None,
689 };
690 NetworkRepoModel::new_stellar(stellar_config)
691 }
692 }
693 }
694
695 async fn setup_test_repo() -> RedisNetworkRepository {
696 let redis_url = "redis://localhost:6379";
697 let random_id = Uuid::new_v4().to_string();
698 let key_prefix = format!("test_prefix_{}", random_id);
699
700 let client = redis::Client::open(redis_url).expect("Failed to create Redis client");
701 let connection_manager = ConnectionManager::new(client)
702 .await
703 .expect("Failed to create connection manager");
704
705 RedisNetworkRepository::new(Arc::new(connection_manager), key_prefix.to_string())
706 .expect("Failed to create repository")
707 }
708
709 #[tokio::test]
710 #[ignore = "Requires active Redis instance"]
711 async fn test_create_network() {
712 let repo = setup_test_repo().await;
713 let test_network_random = Uuid::new_v4().to_string();
714 let network = create_test_network(&test_network_random, NetworkType::Evm);
715
716 let result = repo.create(network.clone()).await;
717 assert!(result.is_ok());
718
719 let created = result.unwrap();
720 assert_eq!(created.id, network.id);
721 assert_eq!(created.name, network.name);
722 assert_eq!(created.network_type, network.network_type);
723 }
724
725 #[tokio::test]
726 #[ignore = "Requires active Redis instance"]
727 async fn test_get_network_by_id() {
728 let repo = setup_test_repo().await;
729 let test_network_random = Uuid::new_v4().to_string();
730 let network = create_test_network(&test_network_random, NetworkType::Evm);
731
732 repo.create(network.clone()).await.unwrap();
733
734 let retrieved = repo.get_by_id(network.id.clone()).await;
735 assert!(retrieved.is_ok());
736
737 let retrieved_network = retrieved.unwrap();
738 assert_eq!(retrieved_network.id, network.id);
739 assert_eq!(retrieved_network.name, network.name);
740 assert_eq!(retrieved_network.network_type, network.network_type);
741 }
742
743 #[tokio::test]
744 #[ignore = "Requires active Redis instance"]
745 async fn test_get_nonexistent_network() {
746 let repo = setup_test_repo().await;
747 let result = repo.get_by_id("nonexistent".to_string()).await;
748 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
749 }
750
751 #[tokio::test]
752 #[ignore = "Requires active Redis instance"]
753 async fn test_create_duplicate_network() {
754 let repo = setup_test_repo().await;
755 let test_network_random = Uuid::new_v4().to_string();
756 let network = create_test_network(&test_network_random, NetworkType::Evm);
757
758 repo.create(network.clone()).await.unwrap();
759 let result = repo.create(network).await;
760 assert!(matches!(
761 result,
762 Err(RepositoryError::ConstraintViolation(_))
763 ));
764 }
765
766 #[tokio::test]
767 #[ignore = "Requires active Redis instance"]
768 async fn test_update_network() {
769 let repo = setup_test_repo().await;
770 let random_id = Uuid::new_v4().to_string();
771 let random_name = Uuid::new_v4().to_string();
772 let mut network = create_test_network(&random_name, NetworkType::Evm);
773 network.id = format!("evm:{}", random_id);
774
775 repo.create(network.clone()).await.unwrap();
777
778 let updated = repo.update(network.id.clone(), network.clone()).await;
780 assert!(updated.is_ok());
781
782 let updated_network = updated.unwrap();
783 assert_eq!(updated_network.id, network.id);
784 assert_eq!(updated_network.name, network.name);
785 }
786
787 #[tokio::test]
788 #[ignore = "Requires active Redis instance"]
789 async fn test_delete_network() {
790 let repo = setup_test_repo().await;
791 let random_id = Uuid::new_v4().to_string();
792 let random_name = Uuid::new_v4().to_string();
793 let mut network = create_test_network(&random_name, NetworkType::Evm);
794 network.id = format!("evm:{}", random_id);
795
796 repo.create(network.clone()).await.unwrap();
798
799 let result = repo.delete_by_id(network.id.clone()).await;
801 assert!(result.is_ok());
802
803 let get_result = repo.get_by_id(network.id).await;
805 assert!(matches!(get_result, Err(RepositoryError::NotFound(_))));
806 }
807
808 #[tokio::test]
809 #[ignore = "Requires active Redis instance"]
810 async fn test_list_all_networks() {
811 let repo = setup_test_repo().await;
812 let test_network_random = Uuid::new_v4().to_string();
813 let test_network_random2 = Uuid::new_v4().to_string();
814 let network1 = create_test_network(&test_network_random, NetworkType::Evm);
815 let network2 = create_test_network(&test_network_random2, NetworkType::Solana);
816
817 repo.create(network1.clone()).await.unwrap();
818 repo.create(network2.clone()).await.unwrap();
819
820 let networks = repo.list_all().await.unwrap();
821 assert_eq!(networks.len(), 2);
822
823 let ids: Vec<String> = networks.iter().map(|n| n.id.clone()).collect();
824 assert!(ids.contains(&network1.id));
825 assert!(ids.contains(&network2.id));
826 }
827
828 #[tokio::test]
829 #[ignore = "Requires active Redis instance"]
830 async fn test_count_networks() {
831 let repo = setup_test_repo().await;
832 let test_network_random = Uuid::new_v4().to_string();
833 let test_network_random2 = Uuid::new_v4().to_string();
834 let network1 = create_test_network(&test_network_random, NetworkType::Evm);
835 let network2 = create_test_network(&test_network_random2, NetworkType::Solana);
836
837 assert_eq!(repo.count().await.unwrap(), 0);
838
839 repo.create(network1).await.unwrap();
840 assert_eq!(repo.count().await.unwrap(), 1);
841
842 repo.create(network2).await.unwrap();
843 assert_eq!(repo.count().await.unwrap(), 2);
844 }
845
846 #[tokio::test]
847 #[ignore = "Requires active Redis instance"]
848 async fn test_list_paginated() {
849 let repo = setup_test_repo().await;
850 let test_network_random = Uuid::new_v4().to_string();
851 let test_network_random2 = Uuid::new_v4().to_string();
852 let test_network_random3 = Uuid::new_v4().to_string();
853 let network1 = create_test_network(&test_network_random, NetworkType::Evm);
854 let network2 = create_test_network(&test_network_random2, NetworkType::Solana);
855 let network3 = create_test_network(&test_network_random3, NetworkType::Stellar);
856
857 repo.create(network1).await.unwrap();
858 repo.create(network2).await.unwrap();
859 repo.create(network3).await.unwrap();
860
861 let query = PaginationQuery {
862 page: 1,
863 per_page: 2,
864 };
865
866 let result = repo.list_paginated(query).await.unwrap();
867 assert_eq!(result.items.len(), 2);
868 assert_eq!(result.total, 3);
869 assert_eq!(result.page, 1);
870 assert_eq!(result.per_page, 2);
871 }
872
873 #[tokio::test]
874 #[ignore = "Requires active Redis instance"]
875 async fn test_get_by_name() {
876 let repo = setup_test_repo().await;
877 let test_network_random = Uuid::new_v4().to_string();
878 let network = create_test_network(&test_network_random, NetworkType::Evm);
879
880 repo.create(network.clone()).await.unwrap();
881
882 let retrieved = repo
883 .get_by_name(NetworkType::Evm, &test_network_random)
884 .await
885 .unwrap();
886 assert!(retrieved.is_some());
887 assert_eq!(retrieved.unwrap().name, test_network_random);
888
889 let not_found = repo
890 .get_by_name(NetworkType::Solana, &test_network_random)
891 .await
892 .unwrap();
893 assert!(not_found.is_none());
894 }
895
896 #[tokio::test]
897 #[ignore = "Requires active Redis instance"]
898 async fn test_get_by_chain_id() {
899 let repo = setup_test_repo().await;
900 let test_network_random = Uuid::new_v4().to_string();
901 let network = create_test_network(&test_network_random, NetworkType::Evm);
902
903 repo.create(network.clone()).await.unwrap();
904
905 let retrieved = repo.get_by_chain_id(NetworkType::Evm, 1).await.unwrap();
906 assert!(retrieved.is_some());
907 assert_eq!(retrieved.unwrap().name, test_network_random);
908
909 let not_found = repo.get_by_chain_id(NetworkType::Evm, 999).await.unwrap();
910 assert!(not_found.is_none());
911
912 let solana_result = repo.get_by_chain_id(NetworkType::Solana, 1).await.unwrap();
913 assert!(solana_result.is_none());
914 }
915
916 #[tokio::test]
917 #[ignore = "Requires active Redis instance"]
918 async fn test_update_nonexistent_network() {
919 let repo = setup_test_repo().await;
920 let test_network_random = Uuid::new_v4().to_string();
921 let network = create_test_network(&test_network_random, NetworkType::Evm);
922
923 let result = repo.update(network.id.clone(), network).await;
924 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
925 }
926
927 #[tokio::test]
928 #[ignore = "Requires active Redis instance"]
929 async fn test_delete_nonexistent_network() {
930 let repo = setup_test_repo().await;
931
932 let result = repo.delete_by_id("nonexistent".to_string()).await;
933 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
934 }
935
936 #[tokio::test]
937 #[ignore = "Requires active Redis instance"]
938 async fn test_empty_id_validation() {
939 let repo = setup_test_repo().await;
940
941 let create_result = repo
942 .create(NetworkRepoModel {
943 id: "".to_string(),
944 name: "test".to_string(),
945 network_type: NetworkType::Evm,
946 config: NetworkConfigData::Evm(EvmNetworkConfig {
947 common: NetworkConfigCommon {
948 network: "test".to_string(),
949 from: None,
950 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
951 explorer_urls: None,
952 average_blocktime_ms: Some(12000),
953 is_testnet: Some(true),
954 tags: None,
955 },
956 chain_id: Some(1),
957 required_confirmations: Some(1),
958 features: None,
959 symbol: Some("ETH".to_string()),
960 gas_price_cache: None,
961 }),
962 })
963 .await;
964
965 assert!(matches!(
966 create_result,
967 Err(RepositoryError::InvalidData(_))
968 ));
969
970 let get_result = repo.get_by_id("".to_string()).await;
971 assert!(matches!(get_result, Err(RepositoryError::InvalidData(_))));
972
973 let update_result = repo
974 .update(
975 "".to_string(),
976 create_test_network("test", NetworkType::Evm),
977 )
978 .await;
979 assert!(matches!(
980 update_result,
981 Err(RepositoryError::InvalidData(_))
982 ));
983
984 let delete_result = repo.delete_by_id("".to_string()).await;
985 assert!(matches!(
986 delete_result,
987 Err(RepositoryError::InvalidData(_))
988 ));
989 }
990
991 #[tokio::test]
992 #[ignore = "Requires active Redis instance"]
993 async fn test_pagination_validation() {
994 let repo = setup_test_repo().await;
995
996 let query = PaginationQuery {
997 page: 1,
998 per_page: 0,
999 };
1000 let result = repo.list_paginated(query).await;
1001 assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
1002 }
1003
1004 #[tokio::test]
1005 #[ignore = "Requires active Redis instance"]
1006 async fn test_id_mismatch_validation() {
1007 let repo = setup_test_repo().await;
1008 let test_network_random = Uuid::new_v4().to_string();
1009 let network = create_test_network(&test_network_random, NetworkType::Evm);
1010
1011 repo.create(network.clone()).await.unwrap();
1012
1013 let result = repo.update("different-id".to_string(), network).await;
1014 assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
1015 }
1016
1017 #[tokio::test]
1018 #[ignore = "Requires active Redis instance"]
1019 async fn test_empty_name_validation() {
1020 let repo = setup_test_repo().await;
1021
1022 let result = repo.get_by_name(NetworkType::Evm, "").await;
1023 assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
1024 }
1025
1026 #[tokio::test]
1027 #[ignore = "Requires active Redis instance"]
1028 async fn test_has_entries_empty_storage() {
1029 let repo = setup_test_repo().await;
1030
1031 let result = repo.has_entries().await.unwrap();
1032 assert!(!result, "Empty storage should return false");
1033 }
1034
1035 #[tokio::test]
1036 #[ignore = "Requires active Redis instance"]
1037 async fn test_has_entries_with_data() {
1038 let repo = setup_test_repo().await;
1039 let test_network_random = Uuid::new_v4().to_string();
1040 let network = create_test_network(&test_network_random, NetworkType::Evm);
1041
1042 assert!(!repo.has_entries().await.unwrap());
1043
1044 repo.create(network).await.unwrap();
1045
1046 assert!(repo.has_entries().await.unwrap());
1047 }
1048
1049 #[tokio::test]
1050 #[ignore = "Requires active Redis instance"]
1051 async fn test_drop_all_entries_empty_storage() {
1052 let repo = setup_test_repo().await;
1053
1054 let result = repo.drop_all_entries().await;
1055 assert!(result.is_ok());
1056
1057 assert!(!repo.has_entries().await.unwrap());
1058 }
1059
1060 #[tokio::test]
1061 #[ignore = "Requires active Redis instance"]
1062 async fn test_drop_all_entries_with_data() {
1063 let repo = setup_test_repo().await;
1064 let test_network_random1 = Uuid::new_v4().to_string();
1065 let test_network_random2 = Uuid::new_v4().to_string();
1066 let network1 = create_test_network(&test_network_random1, NetworkType::Evm);
1067 let network2 = create_test_network(&test_network_random2, NetworkType::Solana);
1068
1069 repo.create(network1.clone()).await.unwrap();
1071 repo.create(network2.clone()).await.unwrap();
1072
1073 assert!(repo.has_entries().await.unwrap());
1075 assert_eq!(repo.count().await.unwrap(), 2);
1076 assert!(repo
1077 .get_by_name(NetworkType::Evm, &test_network_random1)
1078 .await
1079 .unwrap()
1080 .is_some());
1081
1082 let result = repo.drop_all_entries().await;
1084 assert!(result.is_ok());
1085
1086 assert!(!repo.has_entries().await.unwrap());
1088 assert_eq!(repo.count().await.unwrap(), 0);
1089 assert!(repo
1090 .get_by_name(NetworkType::Evm, &test_network_random1)
1091 .await
1092 .unwrap()
1093 .is_none());
1094 assert!(repo
1095 .get_by_name(NetworkType::Solana, &test_network_random2)
1096 .await
1097 .unwrap()
1098 .is_none());
1099
1100 assert!(matches!(
1102 repo.get_by_id(network1.id).await,
1103 Err(RepositoryError::NotFound(_))
1104 ));
1105 assert!(matches!(
1106 repo.get_by_id(network2.id).await,
1107 Err(RepositoryError::NotFound(_))
1108 ));
1109 }
1110
1111 #[tokio::test]
1112 #[ignore = "Requires active Redis instance"]
1113 async fn test_drop_all_entries_cleans_indexes() {
1114 let repo = setup_test_repo().await;
1115 let test_network_random = Uuid::new_v4().to_string();
1116 let mut network = create_test_network(&test_network_random, NetworkType::Evm);
1117
1118 if let crate::models::NetworkConfigData::Evm(ref mut evm_config) = network.config {
1120 evm_config.chain_id = Some(12345);
1121 }
1122
1123 repo.create(network.clone()).await.unwrap();
1125
1126 assert!(repo
1128 .get_by_name(NetworkType::Evm, &test_network_random)
1129 .await
1130 .unwrap()
1131 .is_some());
1132 assert!(repo
1133 .get_by_chain_id(NetworkType::Evm, 12345)
1134 .await
1135 .unwrap()
1136 .is_some());
1137
1138 repo.drop_all_entries().await.unwrap();
1140
1141 assert!(repo
1143 .get_by_name(NetworkType::Evm, &test_network_random)
1144 .await
1145 .unwrap()
1146 .is_none());
1147 assert!(repo
1148 .get_by_chain_id(NetworkType::Evm, 12345)
1149 .await
1150 .unwrap()
1151 .is_none());
1152 }
1153}