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                };
689                NetworkRepoModel::new_stellar(stellar_config)
690            }
691        }
692    }
693
694    async fn setup_test_repo() -> RedisNetworkRepository {
695        let redis_url = "redis://localhost:6379";
696        let random_id = Uuid::new_v4().to_string();
697        let key_prefix = format!("test_prefix_{}", random_id);
698
699        let client = redis::Client::open(redis_url).expect("Failed to create Redis client");
700        let connection_manager = ConnectionManager::new(client)
701            .await
702            .expect("Failed to create connection manager");
703
704        RedisNetworkRepository::new(Arc::new(connection_manager), key_prefix.to_string())
705            .expect("Failed to create repository")
706    }
707
708    #[tokio::test]
709    #[ignore = "Requires active Redis instance"]
710    async fn test_create_network() {
711        let repo = setup_test_repo().await;
712        let test_network_random = Uuid::new_v4().to_string();
713        let network = create_test_network(&test_network_random, NetworkType::Evm);
714
715        let result = repo.create(network.clone()).await;
716        assert!(result.is_ok());
717
718        let created = result.unwrap();
719        assert_eq!(created.id, network.id);
720        assert_eq!(created.name, network.name);
721        assert_eq!(created.network_type, network.network_type);
722    }
723
724    #[tokio::test]
725    #[ignore = "Requires active Redis instance"]
726    async fn test_get_network_by_id() {
727        let repo = setup_test_repo().await;
728        let test_network_random = Uuid::new_v4().to_string();
729        let network = create_test_network(&test_network_random, NetworkType::Evm);
730
731        repo.create(network.clone()).await.unwrap();
732
733        let retrieved = repo.get_by_id(network.id.clone()).await;
734        assert!(retrieved.is_ok());
735
736        let retrieved_network = retrieved.unwrap();
737        assert_eq!(retrieved_network.id, network.id);
738        assert_eq!(retrieved_network.name, network.name);
739        assert_eq!(retrieved_network.network_type, network.network_type);
740    }
741
742    #[tokio::test]
743    #[ignore = "Requires active Redis instance"]
744    async fn test_get_nonexistent_network() {
745        let repo = setup_test_repo().await;
746        let result = repo.get_by_id("nonexistent".to_string()).await;
747        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
748    }
749
750    #[tokio::test]
751    #[ignore = "Requires active Redis instance"]
752    async fn test_create_duplicate_network() {
753        let repo = setup_test_repo().await;
754        let test_network_random = Uuid::new_v4().to_string();
755        let network = create_test_network(&test_network_random, NetworkType::Evm);
756
757        repo.create(network.clone()).await.unwrap();
758        let result = repo.create(network).await;
759        assert!(matches!(
760            result,
761            Err(RepositoryError::ConstraintViolation(_))
762        ));
763    }
764
765    #[tokio::test]
766    #[ignore = "Requires active Redis instance"]
767    async fn test_update_network() {
768        let repo = setup_test_repo().await;
769        let random_id = Uuid::new_v4().to_string();
770        let random_name = Uuid::new_v4().to_string();
771        let mut network = create_test_network(&random_name, NetworkType::Evm);
772        network.id = format!("evm:{}", random_id);
773
774        // Create the network first
775        repo.create(network.clone()).await.unwrap();
776
777        // Update the network
778        let updated = repo.update(network.id.clone(), network.clone()).await;
779        assert!(updated.is_ok());
780
781        let updated_network = updated.unwrap();
782        assert_eq!(updated_network.id, network.id);
783        assert_eq!(updated_network.name, network.name);
784    }
785
786    #[tokio::test]
787    #[ignore = "Requires active Redis instance"]
788    async fn test_delete_network() {
789        let repo = setup_test_repo().await;
790        let random_id = Uuid::new_v4().to_string();
791        let random_name = Uuid::new_v4().to_string();
792        let mut network = create_test_network(&random_name, NetworkType::Evm);
793        network.id = format!("evm:{}", random_id);
794
795        // Create the network first
796        repo.create(network.clone()).await.unwrap();
797
798        // Delete the network
799        let result = repo.delete_by_id(network.id.clone()).await;
800        assert!(result.is_ok());
801
802        // Verify it's deleted
803        let get_result = repo.get_by_id(network.id).await;
804        assert!(matches!(get_result, Err(RepositoryError::NotFound(_))));
805    }
806
807    #[tokio::test]
808    #[ignore = "Requires active Redis instance"]
809    async fn test_list_all_networks() {
810        let repo = setup_test_repo().await;
811        let test_network_random = Uuid::new_v4().to_string();
812        let test_network_random2 = Uuid::new_v4().to_string();
813        let network1 = create_test_network(&test_network_random, NetworkType::Evm);
814        let network2 = create_test_network(&test_network_random2, NetworkType::Solana);
815
816        repo.create(network1.clone()).await.unwrap();
817        repo.create(network2.clone()).await.unwrap();
818
819        let networks = repo.list_all().await.unwrap();
820        assert_eq!(networks.len(), 2);
821
822        let ids: Vec<String> = networks.iter().map(|n| n.id.clone()).collect();
823        assert!(ids.contains(&network1.id));
824        assert!(ids.contains(&network2.id));
825    }
826
827    #[tokio::test]
828    #[ignore = "Requires active Redis instance"]
829    async fn test_count_networks() {
830        let repo = setup_test_repo().await;
831        let test_network_random = Uuid::new_v4().to_string();
832        let test_network_random2 = Uuid::new_v4().to_string();
833        let network1 = create_test_network(&test_network_random, NetworkType::Evm);
834        let network2 = create_test_network(&test_network_random2, NetworkType::Solana);
835
836        assert_eq!(repo.count().await.unwrap(), 0);
837
838        repo.create(network1).await.unwrap();
839        assert_eq!(repo.count().await.unwrap(), 1);
840
841        repo.create(network2).await.unwrap();
842        assert_eq!(repo.count().await.unwrap(), 2);
843    }
844
845    #[tokio::test]
846    #[ignore = "Requires active Redis instance"]
847    async fn test_list_paginated() {
848        let repo = setup_test_repo().await;
849        let test_network_random = Uuid::new_v4().to_string();
850        let test_network_random2 = Uuid::new_v4().to_string();
851        let test_network_random3 = Uuid::new_v4().to_string();
852        let network1 = create_test_network(&test_network_random, NetworkType::Evm);
853        let network2 = create_test_network(&test_network_random2, NetworkType::Solana);
854        let network3 = create_test_network(&test_network_random3, NetworkType::Stellar);
855
856        repo.create(network1).await.unwrap();
857        repo.create(network2).await.unwrap();
858        repo.create(network3).await.unwrap();
859
860        let query = PaginationQuery {
861            page: 1,
862            per_page: 2,
863        };
864
865        let result = repo.list_paginated(query).await.unwrap();
866        assert_eq!(result.items.len(), 2);
867        assert_eq!(result.total, 3);
868        assert_eq!(result.page, 1);
869        assert_eq!(result.per_page, 2);
870    }
871
872    #[tokio::test]
873    #[ignore = "Requires active Redis instance"]
874    async fn test_get_by_name() {
875        let repo = setup_test_repo().await;
876        let test_network_random = Uuid::new_v4().to_string();
877        let network = create_test_network(&test_network_random, NetworkType::Evm);
878
879        repo.create(network.clone()).await.unwrap();
880
881        let retrieved = repo
882            .get_by_name(NetworkType::Evm, &test_network_random)
883            .await
884            .unwrap();
885        assert!(retrieved.is_some());
886        assert_eq!(retrieved.unwrap().name, test_network_random);
887
888        let not_found = repo
889            .get_by_name(NetworkType::Solana, &test_network_random)
890            .await
891            .unwrap();
892        assert!(not_found.is_none());
893    }
894
895    #[tokio::test]
896    #[ignore = "Requires active Redis instance"]
897    async fn test_get_by_chain_id() {
898        let repo = setup_test_repo().await;
899        let test_network_random = Uuid::new_v4().to_string();
900        let network = create_test_network(&test_network_random, NetworkType::Evm);
901
902        repo.create(network.clone()).await.unwrap();
903
904        let retrieved = repo.get_by_chain_id(NetworkType::Evm, 1).await.unwrap();
905        assert!(retrieved.is_some());
906        assert_eq!(retrieved.unwrap().name, test_network_random);
907
908        let not_found = repo.get_by_chain_id(NetworkType::Evm, 999).await.unwrap();
909        assert!(not_found.is_none());
910
911        let solana_result = repo.get_by_chain_id(NetworkType::Solana, 1).await.unwrap();
912        assert!(solana_result.is_none());
913    }
914
915    #[tokio::test]
916    #[ignore = "Requires active Redis instance"]
917    async fn test_update_nonexistent_network() {
918        let repo = setup_test_repo().await;
919        let test_network_random = Uuid::new_v4().to_string();
920        let network = create_test_network(&test_network_random, NetworkType::Evm);
921
922        let result = repo.update(network.id.clone(), network).await;
923        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
924    }
925
926    #[tokio::test]
927    #[ignore = "Requires active Redis instance"]
928    async fn test_delete_nonexistent_network() {
929        let repo = setup_test_repo().await;
930
931        let result = repo.delete_by_id("nonexistent".to_string()).await;
932        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
933    }
934
935    #[tokio::test]
936    #[ignore = "Requires active Redis instance"]
937    async fn test_empty_id_validation() {
938        let repo = setup_test_repo().await;
939
940        let create_result = repo
941            .create(NetworkRepoModel {
942                id: "".to_string(),
943                name: "test".to_string(),
944                network_type: NetworkType::Evm,
945                config: NetworkConfigData::Evm(EvmNetworkConfig {
946                    common: NetworkConfigCommon {
947                        network: "test".to_string(),
948                        from: None,
949                        rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
950                        explorer_urls: None,
951                        average_blocktime_ms: Some(12000),
952                        is_testnet: Some(true),
953                        tags: None,
954                    },
955                    chain_id: Some(1),
956                    required_confirmations: Some(1),
957                    features: None,
958                    symbol: Some("ETH".to_string()),
959                    gas_price_cache: None,
960                }),
961            })
962            .await;
963
964        assert!(matches!(
965            create_result,
966            Err(RepositoryError::InvalidData(_))
967        ));
968
969        let get_result = repo.get_by_id("".to_string()).await;
970        assert!(matches!(get_result, Err(RepositoryError::InvalidData(_))));
971
972        let update_result = repo
973            .update(
974                "".to_string(),
975                create_test_network("test", NetworkType::Evm),
976            )
977            .await;
978        assert!(matches!(
979            update_result,
980            Err(RepositoryError::InvalidData(_))
981        ));
982
983        let delete_result = repo.delete_by_id("".to_string()).await;
984        assert!(matches!(
985            delete_result,
986            Err(RepositoryError::InvalidData(_))
987        ));
988    }
989
990    #[tokio::test]
991    #[ignore = "Requires active Redis instance"]
992    async fn test_pagination_validation() {
993        let repo = setup_test_repo().await;
994
995        let query = PaginationQuery {
996            page: 1,
997            per_page: 0,
998        };
999        let result = repo.list_paginated(query).await;
1000        assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
1001    }
1002
1003    #[tokio::test]
1004    #[ignore = "Requires active Redis instance"]
1005    async fn test_id_mismatch_validation() {
1006        let repo = setup_test_repo().await;
1007        let test_network_random = Uuid::new_v4().to_string();
1008        let network = create_test_network(&test_network_random, NetworkType::Evm);
1009
1010        repo.create(network.clone()).await.unwrap();
1011
1012        let result = repo.update("different-id".to_string(), network).await;
1013        assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
1014    }
1015
1016    #[tokio::test]
1017    #[ignore = "Requires active Redis instance"]
1018    async fn test_empty_name_validation() {
1019        let repo = setup_test_repo().await;
1020
1021        let result = repo.get_by_name(NetworkType::Evm, "").await;
1022        assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
1023    }
1024
1025    #[tokio::test]
1026    #[ignore = "Requires active Redis instance"]
1027    async fn test_has_entries_empty_storage() {
1028        let repo = setup_test_repo().await;
1029
1030        let result = repo.has_entries().await.unwrap();
1031        assert!(!result, "Empty storage should return false");
1032    }
1033
1034    #[tokio::test]
1035    #[ignore = "Requires active Redis instance"]
1036    async fn test_has_entries_with_data() {
1037        let repo = setup_test_repo().await;
1038        let test_network_random = Uuid::new_v4().to_string();
1039        let network = create_test_network(&test_network_random, NetworkType::Evm);
1040
1041        assert!(!repo.has_entries().await.unwrap());
1042
1043        repo.create(network).await.unwrap();
1044
1045        assert!(repo.has_entries().await.unwrap());
1046    }
1047
1048    #[tokio::test]
1049    #[ignore = "Requires active Redis instance"]
1050    async fn test_drop_all_entries_empty_storage() {
1051        let repo = setup_test_repo().await;
1052
1053        let result = repo.drop_all_entries().await;
1054        assert!(result.is_ok());
1055
1056        assert!(!repo.has_entries().await.unwrap());
1057    }
1058
1059    #[tokio::test]
1060    #[ignore = "Requires active Redis instance"]
1061    async fn test_drop_all_entries_with_data() {
1062        let repo = setup_test_repo().await;
1063        let test_network_random1 = Uuid::new_v4().to_string();
1064        let test_network_random2 = Uuid::new_v4().to_string();
1065        let network1 = create_test_network(&test_network_random1, NetworkType::Evm);
1066        let network2 = create_test_network(&test_network_random2, NetworkType::Solana);
1067
1068        // Add networks
1069        repo.create(network1.clone()).await.unwrap();
1070        repo.create(network2.clone()).await.unwrap();
1071
1072        // Verify they exist
1073        assert!(repo.has_entries().await.unwrap());
1074        assert_eq!(repo.count().await.unwrap(), 2);
1075        assert!(repo
1076            .get_by_name(NetworkType::Evm, &test_network_random1)
1077            .await
1078            .unwrap()
1079            .is_some());
1080
1081        // Drop all entries
1082        let result = repo.drop_all_entries().await;
1083        assert!(result.is_ok());
1084
1085        // Verify everything is cleaned up
1086        assert!(!repo.has_entries().await.unwrap());
1087        assert_eq!(repo.count().await.unwrap(), 0);
1088        assert!(repo
1089            .get_by_name(NetworkType::Evm, &test_network_random1)
1090            .await
1091            .unwrap()
1092            .is_none());
1093        assert!(repo
1094            .get_by_name(NetworkType::Solana, &test_network_random2)
1095            .await
1096            .unwrap()
1097            .is_none());
1098
1099        // Verify individual networks are gone
1100        assert!(matches!(
1101            repo.get_by_id(network1.id).await,
1102            Err(RepositoryError::NotFound(_))
1103        ));
1104        assert!(matches!(
1105            repo.get_by_id(network2.id).await,
1106            Err(RepositoryError::NotFound(_))
1107        ));
1108    }
1109
1110    #[tokio::test]
1111    #[ignore = "Requires active Redis instance"]
1112    async fn test_drop_all_entries_cleans_indexes() {
1113        let repo = setup_test_repo().await;
1114        let test_network_random = Uuid::new_v4().to_string();
1115        let mut network = create_test_network(&test_network_random, NetworkType::Evm);
1116
1117        // Ensure we have a specific chain ID for testing
1118        if let crate::models::NetworkConfigData::Evm(ref mut evm_config) = network.config {
1119            evm_config.chain_id = Some(12345);
1120        }
1121
1122        // Add network
1123        repo.create(network.clone()).await.unwrap();
1124
1125        // Verify indexes work
1126        assert!(repo
1127            .get_by_name(NetworkType::Evm, &test_network_random)
1128            .await
1129            .unwrap()
1130            .is_some());
1131        assert!(repo
1132            .get_by_chain_id(NetworkType::Evm, 12345)
1133            .await
1134            .unwrap()
1135            .is_some());
1136
1137        // Drop all entries
1138        repo.drop_all_entries().await.unwrap();
1139
1140        // Verify indexes are cleaned up
1141        assert!(repo
1142            .get_by_name(NetworkType::Evm, &test_network_random)
1143            .await
1144            .unwrap()
1145            .is_none());
1146        assert!(repo
1147            .get_by_chain_id(NetworkType::Evm, 12345)
1148            .await
1149            .unwrap()
1150            .is_none());
1151    }
1152}