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