1use 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 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
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 let name_key = self.network_name_index_key(&network.network_type, &network.name);
99 pipe.set(&name_key, &network.id);
100
101 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 if let Some(old) = old_network {
110 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 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 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 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 let name_key = self.network_name_index_key(&network.network_type, &network.name);
153 pipe.del(&name_key);
154
155 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 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 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 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 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 };
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 };
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 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 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 let network = self.get_by_id(id.clone()).await?;
469
470 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 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 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 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 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 let networks_result = self.get_networks_by_ids(&network_ids).await?;
550 let networks = networks_result.results;
551
552 let mut pipe = redis::pipe();
554 pipe.atomic();
555
556 for network_id in &network_ids {
558 let network_key = self.network_key(network_id);
559 pipe.del(&network_key);
560 }
561
562 for network in &networks {
564 let name_key = self.network_name_index_key(&network.network_type, &network.name);
566 pipe.del(&name_key);
567
568 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 pipe.del(&network_list_key);
577
578 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 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 };
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 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 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 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 };
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 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 repo.create(network.clone()).await.unwrap();
828
829 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 repo.create(network.clone()).await.unwrap();
849
850 let result = repo.delete_by_id(network.id.clone()).await;
852 assert!(result.is_ok());
853
854 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 repo.create(network1.clone()).await.unwrap();
1124 repo.create(network2.clone()).await.unwrap();
1125
1126 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 let result = repo.drop_all_entries().await;
1137 assert!(result.is_ok());
1138
1139 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 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 if let crate::models::NetworkConfigData::Evm(ref mut evm_config) = network.config {
1173 evm_config.chain_id = Some(12345);
1174 }
1175
1176 repo.create(network.clone()).await.unwrap();
1178
1179 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 repo.drop_all_entries().await.unwrap();
1193
1194 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}