openzeppelin_relayer/repositories/network/
network_redis.rs

1//! Redis implementation of the network repository.
2//!
3//! This module provides a Redis-based implementation of the `NetworkRepository` trait,
4//! allowing network configurations to be stored and retrieved from a Redis database.
5//! The implementation includes comprehensive error handling, logging, validation, and
6//! efficient indexing for fast lookups by name and chain ID.
7
8use 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    /// Generate key for network data: network:{network_id}
50    fn network_key(&self, network_id: &str) -> String {
51        format!("{}:{}:{}", self.key_prefix, NETWORK_PREFIX, network_id)
52    }
53
54    /// Generate key for network list: network_list (set of all network IDs)
55    fn network_list_key(&self) -> String {
56        format!("{}:{}", self.key_prefix, NETWORK_LIST_KEY)
57    }
58
59    /// Generate key for network name index: network_name:{network_type}:{name}
60    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    /// Generate key for network chain ID index: network_chain_id:{network_type}:{chain_id}
68    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    /// Extract chain ID from network configuration
76    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    /// Update indexes for a network
84    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        // Add name index
96        let name_key = self.network_name_index_key(&network.network_type, &network.name);
97        pipe.set(&name_key, &network.id);
98
99        // Add chain ID index if applicable
100        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        // Remove old indexes if updating
107        if let Some(old) = old_network {
108            // Remove old name index if name or type changed
109            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            // Handle chain ID index cleanup
116            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        // Execute all operations in a single pipeline
130        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    /// Remove all indexes for a network
140    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        // Remove name index
148        let name_key = self.network_name_index_key(&network.network_type, &network.name);
149        pipe.del(&name_key);
150
151        // Remove chain ID index if applicable
152        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    /// Batch fetch networks by IDs
168    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        // Check if network already exists
256        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        // Use Redis pipeline for atomic operations
270        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        // Update indexes
279        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 {} not found",
312                    id
313                )))
314            }
315        }
316    }
317
318    async fn list_all(&self) -> Result<Vec<NetworkRepoModel>, RepositoryError> {
319        let network_list_key = self.network_list_key();
320        let mut conn = self.client.as_ref().clone();
321
322        debug!("listing all networks");
323
324        let ids: Vec<String> = conn
325            .smembers(&network_list_key)
326            .await
327            .map_err(|e| self.map_redis_error(e, "list_all_networks"))?;
328
329        if ids.is_empty() {
330            debug!("no networks found");
331            return Ok(Vec::new());
332        }
333
334        let networks = self.get_networks_by_ids(&ids).await?;
335        debug!(count = %networks.results.len(), "successfully retrieved networks");
336        Ok(networks.results)
337    }
338
339    async fn list_paginated(
340        &self,
341        query: PaginationQuery,
342    ) -> Result<PaginatedResult<NetworkRepoModel>, RepositoryError> {
343        if query.per_page == 0 {
344            return Err(RepositoryError::InvalidData(
345                "per_page must be greater than 0".to_string(),
346            ));
347        }
348
349        let network_list_key = self.network_list_key();
350        let mut conn = self.client.as_ref().clone();
351
352        debug!(page = %query.page, per_page = %query.per_page, "listing paginated networks");
353
354        let all_ids: Vec<String> = conn
355            .smembers(&network_list_key)
356            .await
357            .map_err(|e| self.map_redis_error(e, "list_paginated_networks"))?;
358
359        let total = all_ids.len() as u64;
360        let per_page = query.per_page as usize;
361        let page = query.page as usize;
362        let total_pages = all_ids.len().div_ceil(per_page);
363
364        if page > total_pages && !all_ids.is_empty() {
365            debug!(requested_page = %page, total_pages = %total_pages, "requested page exceeds total pages");
366            return Ok(PaginatedResult {
367                items: Vec::new(),
368                total,
369                page: query.page,
370                per_page: query.per_page,
371            });
372        }
373
374        let start_idx = (page - 1) * per_page;
375        let end_idx = std::cmp::min(start_idx + per_page, all_ids.len());
376
377        let page_ids = all_ids[start_idx..end_idx].to_vec();
378        let networks = self.get_networks_by_ids(&page_ids).await?;
379
380        debug!(count = %networks.results.len(), page = %query.page, "successfully retrieved networks for page");
381        Ok(PaginatedResult {
382            items: networks.results.clone(),
383            total,
384            page: query.page,
385            per_page: query.per_page,
386        })
387    }
388
389    async fn update(
390        &self,
391        id: String,
392        entity: NetworkRepoModel,
393    ) -> Result<NetworkRepoModel, RepositoryError> {
394        if id.is_empty() {
395            return Err(RepositoryError::InvalidData(
396                "Network ID cannot be empty".to_string(),
397            ));
398        }
399
400        if id != entity.id {
401            return Err(RepositoryError::InvalidData(format!(
402                "ID mismatch: provided ID '{}' doesn't match network ID '{}'",
403                id, entity.id
404            )));
405        }
406
407        let key = self.network_key(&id);
408        let mut conn = self.client.as_ref().clone();
409
410        debug!(network_id = %id, "updating network");
411
412        // Get the old network for index cleanup
413        let old_network = self.get_by_id(id.clone()).await?;
414
415        let value = self.serialize_entity(&entity, |n| &n.id, "network")?;
416
417        let _: () = conn
418            .set(&key, &value)
419            .await
420            .map_err(|e| self.map_redis_error(e, "update_network"))?;
421
422        // Update indexes
423        self.update_indexes(&entity, Some(&old_network)).await?;
424
425        debug!(network_id = %id, "successfully updated network");
426        Ok(entity)
427    }
428
429    async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
430        if id.is_empty() {
431            return Err(RepositoryError::InvalidData(
432                "Network ID cannot be empty".to_string(),
433            ));
434        }
435
436        let key = self.network_key(&id);
437        let network_list_key = self.network_list_key();
438        let mut conn = self.client.as_ref().clone();
439
440        debug!(network_id = %id, "deleting network");
441
442        // Get network for index cleanup
443        let network = self.get_by_id(id.clone()).await?;
444
445        // Use Redis pipeline for atomic operations
446        let mut pipe = redis::pipe();
447        pipe.del(&key);
448        pipe.srem(&network_list_key, &id);
449
450        pipe.exec_async(&mut conn)
451            .await
452            .map_err(|e| self.map_redis_error(e, "delete_network_pipeline"))?;
453
454        // Remove indexes (log errors but don't fail the delete)
455        if let Err(e) = self.remove_all_indexes(&network).await {
456            error!(network_id = %id, error = %e, "failed to remove indexes for deleted network");
457        }
458
459        debug!(network_id = %id, "successfully deleted network");
460        Ok(())
461    }
462
463    async fn count(&self) -> Result<usize, RepositoryError> {
464        let network_list_key = self.network_list_key();
465        let mut conn = self.client.as_ref().clone();
466
467        debug!("counting networks");
468
469        let count: usize = conn
470            .scard(&network_list_key)
471            .await
472            .map_err(|e| self.map_redis_error(e, "count_networks"))?;
473
474        debug!(count = %count, "total networks count");
475        Ok(count)
476    }
477
478    /// Check if Redis storage contains any network entries.
479    /// This is used to determine if Redis storage is being used for networks.
480    async fn has_entries(&self) -> Result<bool, RepositoryError> {
481        let network_list_key = self.network_list_key();
482        let mut conn = self.client.as_ref().clone();
483
484        debug!("checking if network storage has entries");
485
486        let exists: bool = conn
487            .exists(&network_list_key)
488            .await
489            .map_err(|e| self.map_redis_error(e, "check_network_entries_exist"))?;
490
491        debug!(exists = %exists, "network storage has entries");
492        Ok(exists)
493    }
494
495    /// Drop all network-related entries from Redis storage.
496    /// This includes all network data, indexes, and the network list.
497    /// Use with caution as this will permanently delete all network data.
498    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
499        let mut conn = self.client.as_ref().clone();
500
501        debug!("starting to drop all network entries from Redis storage");
502
503        // First, get all network IDs to clean up their data and indexes
504        let network_list_key = self.network_list_key();
505        let network_ids: Vec<String> = conn
506            .smembers(&network_list_key)
507            .await
508            .map_err(|e| self.map_redis_error(e, "get_network_ids_for_cleanup"))?;
509
510        if network_ids.is_empty() {
511            debug!("no network entries found to clean up");
512            return Ok(());
513        }
514
515        debug!(count = %network_ids.len(), "found networks to clean up");
516
517        // Get all networks to clean up their indexes properly
518        let networks_result = self.get_networks_by_ids(&network_ids).await?;
519        let networks = networks_result.results;
520
521        // Use a pipeline for efficient batch operations
522        let mut pipe = redis::pipe();
523        pipe.atomic();
524
525        // Delete all network data entries
526        for network_id in &network_ids {
527            let network_key = self.network_key(network_id);
528            pipe.del(&network_key);
529        }
530
531        // Delete all index entries
532        for network in &networks {
533            // Delete name index
534            let name_key = self.network_name_index_key(&network.network_type, &network.name);
535            pipe.del(&name_key);
536
537            // Delete chain ID index if applicable
538            if let Some(chain_id) = self.extract_chain_id(network) {
539                let chain_id_key = self.network_chain_id_index_key(&network.network_type, chain_id);
540                pipe.del(&chain_id_key);
541            }
542        }
543
544        // Delete the network list
545        pipe.del(&network_list_key);
546
547        // Execute all deletions
548        pipe.exec_async(&mut conn).await.map_err(|e| {
549            error!(error = %e, "failed to execute cleanup pipeline");
550            self.map_redis_error(e, "drop_all_network_entries_pipeline")
551        })?;
552
553        debug!("successfully dropped all network entries from Redis storage");
554        Ok(())
555    }
556}
557
558#[async_trait]
559impl NetworkRepository for RedisNetworkRepository {
560    async fn get_by_name(
561        &self,
562        network_type: NetworkType,
563        name: &str,
564    ) -> Result<Option<NetworkRepoModel>, RepositoryError> {
565        if name.is_empty() {
566            return Err(RepositoryError::InvalidData(
567                "Network name cannot be empty".to_string(),
568            ));
569        }
570
571        let mut conn = self.client.as_ref().clone();
572
573        debug!(name = %name, network_type = ?network_type, "getting network by name");
574
575        // Use name index for O(1) lookup
576        let name_index_key = self.network_name_index_key(&network_type, name);
577        let network_id: Option<String> = conn
578            .get(&name_index_key)
579            .await
580            .map_err(|e| self.map_redis_error(e, "get_network_by_name_index"))?;
581
582        match network_id {
583            Some(id) => {
584                match self.get_by_id(id.clone()).await {
585                    Ok(network) => {
586                        debug!(name = %name, "found network by name");
587                        Ok(Some(network))
588                    }
589                    Err(RepositoryError::NotFound(_)) => {
590                        // Network was deleted but index wasn't cleaned up
591                        warn!(network_type = ?network_type, name = %name, "stale name index found for network");
592                        Ok(None)
593                    }
594                    Err(e) => Err(e),
595                }
596            }
597            None => {
598                debug!(name = %name, "network not found by name");
599                Ok(None)
600            }
601        }
602    }
603
604    async fn get_by_chain_id(
605        &self,
606        network_type: NetworkType,
607        chain_id: u64,
608    ) -> Result<Option<NetworkRepoModel>, RepositoryError> {
609        // Only EVM networks have chain_id
610        if network_type != NetworkType::Evm {
611            return Ok(None);
612        }
613
614        let mut conn = self.client.as_ref().clone();
615
616        debug!(chain_id = %chain_id, network_type = ?network_type, "getting network by chain ID");
617
618        // Use chain ID index for O(1) lookup
619        let chain_id_index_key = self.network_chain_id_index_key(&network_type, chain_id);
620        let network_id: Option<String> = conn
621            .get(&chain_id_index_key)
622            .await
623            .map_err(|e| self.map_redis_error(e, "get_network_by_chain_id_index"))?;
624
625        match network_id {
626            Some(id) => {
627                match self.get_by_id(id.clone()).await {
628                    Ok(network) => {
629                        debug!(chain_id = %chain_id, "found network by chain ID");
630                        Ok(Some(network))
631                    }
632                    Err(RepositoryError::NotFound(_)) => {
633                        // Network was deleted but index wasn't cleaned up
634                        warn!(network_type = ?network_type, chain_id = %chain_id, "stale chain ID index found for network");
635                        Ok(None)
636                    }
637                    Err(e) => Err(e),
638                }
639            }
640            None => {
641                debug!(chain_id = %chain_id, "network not found by chain ID");
642                Ok(None)
643            }
644        }
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651    use crate::config::{
652        EvmNetworkConfig, NetworkConfigCommon, SolanaNetworkConfig, StellarNetworkConfig,
653    };
654    use crate::models::NetworkConfigData;
655    use redis::aio::ConnectionManager;
656    use uuid::Uuid;
657
658    fn create_test_network(name: &str, network_type: NetworkType) -> NetworkRepoModel {
659        let common = NetworkConfigCommon {
660            network: name.to_string(),
661            from: None,
662            rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
663            explorer_urls: None,
664            average_blocktime_ms: Some(12000),
665            is_testnet: Some(true),
666            tags: None,
667        };
668
669        match network_type {
670            NetworkType::Evm => {
671                let evm_config = EvmNetworkConfig {
672                    common,
673                    chain_id: Some(1),
674                    required_confirmations: Some(1),
675                    features: None,
676                    symbol: Some("ETH".to_string()),
677                    gas_price_cache: None,
678                };
679                NetworkRepoModel::new_evm(evm_config)
680            }
681            NetworkType::Solana => {
682                let solana_config = SolanaNetworkConfig { common };
683                NetworkRepoModel::new_solana(solana_config)
684            }
685            NetworkType::Stellar => {
686                let stellar_config = StellarNetworkConfig {
687                    common,
688                    passphrase: 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        // Create the network first
776        repo.create(network.clone()).await.unwrap();
777
778        // Update the network
779        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        // Create the network first
797        repo.create(network.clone()).await.unwrap();
798
799        // Delete the network
800        let result = repo.delete_by_id(network.id.clone()).await;
801        assert!(result.is_ok());
802
803        // Verify it's deleted
804        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        // Add networks
1070        repo.create(network1.clone()).await.unwrap();
1071        repo.create(network2.clone()).await.unwrap();
1072
1073        // Verify they exist
1074        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        // Drop all entries
1083        let result = repo.drop_all_entries().await;
1084        assert!(result.is_ok());
1085
1086        // Verify everything is cleaned up
1087        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        // Verify individual networks are gone
1101        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        // Ensure we have a specific chain ID for testing
1119        if let crate::models::NetworkConfigData::Evm(ref mut evm_config) = network.config {
1120            evm_config.chain_id = Some(12345);
1121        }
1122
1123        // Add network
1124        repo.create(network.clone()).await.unwrap();
1125
1126        // Verify indexes work
1127        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        // Drop all entries
1139        repo.drop_all_entries().await.unwrap();
1140
1141        // Verify indexes are cleaned up
1142        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}