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 {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        // Get the old network for index cleanup
412        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        // Update indexes
422        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        // Get network for index cleanup
442        let network = self.get_by_id(id.clone()).await?;
443
444        // Use Redis pipeline for atomic operations
445        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        // Remove indexes (log errors but don't fail the delete)
454        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    /// Check if Redis storage contains any network entries.
478    /// This is used to determine if Redis storage is being used for networks.
479    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    /// Drop all network-related entries from Redis storage.
495    /// This includes all network data, indexes, and the network list.
496    /// Use with caution as this will permanently delete all network data.
497    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        // First, get all network IDs to clean up their data and indexes
503        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        // Get all networks to clean up their indexes properly
517        let networks_result = self.get_networks_by_ids(&network_ids).await?;
518        let networks = networks_result.results;
519
520        // Use a pipeline for efficient batch operations
521        let mut pipe = redis::pipe();
522        pipe.atomic();
523
524        // Delete all network data entries
525        for network_id in &network_ids {
526            let network_key = self.network_key(network_id);
527            pipe.del(&network_key);
528        }
529
530        // Delete all index entries
531        for network in &networks {
532            // Delete name index
533            let name_key = self.network_name_index_key(&network.network_type, &network.name);
534            pipe.del(&name_key);
535
536            // Delete chain ID index if applicable
537            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        // Delete the network list
544        pipe.del(&network_list_key);
545
546        // Execute all deletions
547        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        // Use name index for O(1) lookup
575        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                        // Network was deleted but index wasn't cleaned up
590                        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        // Only EVM networks have chain_id
609        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        // Use chain ID index for O(1) lookup
618        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                        // Network was deleted but index wasn't cleaned up
633                        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        // 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}