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