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 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 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!("Updating indexes for network {}", network.id);
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!(
104 "Added chain ID index for network {} with chain_id {}",
105 network.id, chain_id
106 );
107 }
108
109 if let Some(old) = old_network {
111 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 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 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 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 let name_key = self.network_name_index_key(&network.network_type, &network.name);
161 pipe.del(&name_key);
162
163 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 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 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 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 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 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 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 let network = self.get_by_id(id.clone()).await?;
479
480 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 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 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 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 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 let networks_result = self.get_networks_by_ids(&network_ids).await?;
554 let networks = networks_result.results;
555
556 let mut pipe = redis::pipe();
558 pipe.atomic();
559
560 for network_id in &network_ids {
562 let network_key = self.network_key(network_id);
563 pipe.del(&network_key);
564 }
565
566 for network in &networks {
568 let name_key = self.network_name_index_key(&network.network_type, &network.name);
570 pipe.del(&name_key);
571
572 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 pipe.del(&network_list_key);
581
582 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 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 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 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 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 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 repo.create(network.clone()).await.unwrap();
823
824 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 repo.create(network.clone()).await.unwrap();
844
845 let result = repo.delete_by_id(network.id.clone()).await;
847 assert!(result.is_ok());
848
849 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 repo.create(network1.clone()).await.unwrap();
1116 repo.create(network2.clone()).await.unwrap();
1117
1118 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 let result = repo.drop_all_entries().await;
1129 assert!(result.is_ok());
1130
1131 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 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 if let crate::models::NetworkConfigData::Evm(ref mut evm_config) = network.config {
1165 evm_config.chain_id = Some(12345);
1166 }
1167
1168 repo.create(network.clone()).await.unwrap();
1170
1171 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 repo.drop_all_entries().await.unwrap();
1185
1186 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}