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