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 {} not found",
312 id
313 )))
314 }
315 }
316 }
317
318 async fn list_all(&self) -> Result<Vec<NetworkRepoModel>, RepositoryError> {
319 let network_list_key = self.network_list_key();
320 let mut conn = self.client.as_ref().clone();
321
322 debug!("listing all networks");
323
324 let ids: Vec<String> = conn
325 .smembers(&network_list_key)
326 .await
327 .map_err(|e| self.map_redis_error(e, "list_all_networks"))?;
328
329 if ids.is_empty() {
330 debug!("no networks found");
331 return Ok(Vec::new());
332 }
333
334 let networks = self.get_networks_by_ids(&ids).await?;
335 debug!(count = %networks.results.len(), "successfully retrieved networks");
336 Ok(networks.results)
337 }
338
339 async fn list_paginated(
340 &self,
341 query: PaginationQuery,
342 ) -> Result<PaginatedResult<NetworkRepoModel>, RepositoryError> {
343 if query.per_page == 0 {
344 return Err(RepositoryError::InvalidData(
345 "per_page must be greater than 0".to_string(),
346 ));
347 }
348
349 let network_list_key = self.network_list_key();
350 let mut conn = self.client.as_ref().clone();
351
352 debug!(page = %query.page, per_page = %query.per_page, "listing paginated networks");
353
354 let all_ids: Vec<String> = conn
355 .smembers(&network_list_key)
356 .await
357 .map_err(|e| self.map_redis_error(e, "list_paginated_networks"))?;
358
359 let total = all_ids.len() as u64;
360 let per_page = query.per_page as usize;
361 let page = query.page as usize;
362 let total_pages = all_ids.len().div_ceil(per_page);
363
364 if page > total_pages && !all_ids.is_empty() {
365 debug!(requested_page = %page, total_pages = %total_pages, "requested page exceeds total pages");
366 return Ok(PaginatedResult {
367 items: Vec::new(),
368 total,
369 page: query.page,
370 per_page: query.per_page,
371 });
372 }
373
374 let start_idx = (page - 1) * per_page;
375 let end_idx = std::cmp::min(start_idx + per_page, all_ids.len());
376
377 let page_ids = all_ids[start_idx..end_idx].to_vec();
378 let networks = self.get_networks_by_ids(&page_ids).await?;
379
380 debug!(count = %networks.results.len(), page = %query.page, "successfully retrieved networks for page");
381 Ok(PaginatedResult {
382 items: networks.results.clone(),
383 total,
384 page: query.page,
385 per_page: query.per_page,
386 })
387 }
388
389 async fn update(
390 &self,
391 id: String,
392 entity: NetworkRepoModel,
393 ) -> Result<NetworkRepoModel, RepositoryError> {
394 if id.is_empty() {
395 return Err(RepositoryError::InvalidData(
396 "Network ID cannot be empty".to_string(),
397 ));
398 }
399
400 if id != entity.id {
401 return Err(RepositoryError::InvalidData(format!(
402 "ID mismatch: provided ID '{}' doesn't match network ID '{}'",
403 id, entity.id
404 )));
405 }
406
407 let key = self.network_key(&id);
408 let mut conn = self.client.as_ref().clone();
409
410 debug!(network_id = %id, "updating network");
411
412 let old_network = self.get_by_id(id.clone()).await?;
414
415 let value = self.serialize_entity(&entity, |n| &n.id, "network")?;
416
417 let _: () = conn
418 .set(&key, &value)
419 .await
420 .map_err(|e| self.map_redis_error(e, "update_network"))?;
421
422 self.update_indexes(&entity, Some(&old_network)).await?;
424
425 debug!(network_id = %id, "successfully updated network");
426 Ok(entity)
427 }
428
429 async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
430 if id.is_empty() {
431 return Err(RepositoryError::InvalidData(
432 "Network ID cannot be empty".to_string(),
433 ));
434 }
435
436 let key = self.network_key(&id);
437 let network_list_key = self.network_list_key();
438 let mut conn = self.client.as_ref().clone();
439
440 debug!(network_id = %id, "deleting network");
441
442 let network = self.get_by_id(id.clone()).await?;
444
445 let mut pipe = redis::pipe();
447 pipe.del(&key);
448 pipe.srem(&network_list_key, &id);
449
450 pipe.exec_async(&mut conn)
451 .await
452 .map_err(|e| self.map_redis_error(e, "delete_network_pipeline"))?;
453
454 if let Err(e) = self.remove_all_indexes(&network).await {
456 error!(network_id = %id, error = %e, "failed to remove indexes for deleted network");
457 }
458
459 debug!(network_id = %id, "successfully deleted network");
460 Ok(())
461 }
462
463 async fn count(&self) -> Result<usize, RepositoryError> {
464 let network_list_key = self.network_list_key();
465 let mut conn = self.client.as_ref().clone();
466
467 debug!("counting networks");
468
469 let count: usize = conn
470 .scard(&network_list_key)
471 .await
472 .map_err(|e| self.map_redis_error(e, "count_networks"))?;
473
474 debug!(count = %count, "total networks count");
475 Ok(count)
476 }
477
478 async fn has_entries(&self) -> Result<bool, RepositoryError> {
481 let network_list_key = self.network_list_key();
482 let mut conn = self.client.as_ref().clone();
483
484 debug!("checking if network storage has entries");
485
486 let exists: bool = conn
487 .exists(&network_list_key)
488 .await
489 .map_err(|e| self.map_redis_error(e, "check_network_entries_exist"))?;
490
491 debug!(exists = %exists, "network storage has entries");
492 Ok(exists)
493 }
494
495 async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
499 let mut conn = self.client.as_ref().clone();
500
501 debug!("starting to drop all network entries from Redis storage");
502
503 let network_list_key = self.network_list_key();
505 let network_ids: Vec<String> = conn
506 .smembers(&network_list_key)
507 .await
508 .map_err(|e| self.map_redis_error(e, "get_network_ids_for_cleanup"))?;
509
510 if network_ids.is_empty() {
511 debug!("no network entries found to clean up");
512 return Ok(());
513 }
514
515 debug!(count = %network_ids.len(), "found networks to clean up");
516
517 let networks_result = self.get_networks_by_ids(&network_ids).await?;
519 let networks = networks_result.results;
520
521 let mut pipe = redis::pipe();
523 pipe.atomic();
524
525 for network_id in &network_ids {
527 let network_key = self.network_key(network_id);
528 pipe.del(&network_key);
529 }
530
531 for network in &networks {
533 let name_key = self.network_name_index_key(&network.network_type, &network.name);
535 pipe.del(&name_key);
536
537 if let Some(chain_id) = self.extract_chain_id(network) {
539 let chain_id_key = self.network_chain_id_index_key(&network.network_type, chain_id);
540 pipe.del(&chain_id_key);
541 }
542 }
543
544 pipe.del(&network_list_key);
546
547 pipe.exec_async(&mut conn).await.map_err(|e| {
549 error!(error = %e, "failed to execute cleanup pipeline");
550 self.map_redis_error(e, "drop_all_network_entries_pipeline")
551 })?;
552
553 debug!("successfully dropped all network entries from Redis storage");
554 Ok(())
555 }
556}
557
558#[async_trait]
559impl NetworkRepository for RedisNetworkRepository {
560 async fn get_by_name(
561 &self,
562 network_type: NetworkType,
563 name: &str,
564 ) -> Result<Option<NetworkRepoModel>, RepositoryError> {
565 if name.is_empty() {
566 return Err(RepositoryError::InvalidData(
567 "Network name cannot be empty".to_string(),
568 ));
569 }
570
571 let mut conn = self.client.as_ref().clone();
572
573 debug!(name = %name, network_type = ?network_type, "getting network by name");
574
575 let name_index_key = self.network_name_index_key(&network_type, name);
577 let network_id: Option<String> = conn
578 .get(&name_index_key)
579 .await
580 .map_err(|e| self.map_redis_error(e, "get_network_by_name_index"))?;
581
582 match network_id {
583 Some(id) => {
584 match self.get_by_id(id.clone()).await {
585 Ok(network) => {
586 debug!(name = %name, "found network by name");
587 Ok(Some(network))
588 }
589 Err(RepositoryError::NotFound(_)) => {
590 warn!(network_type = ?network_type, name = %name, "stale name index found for network");
592 Ok(None)
593 }
594 Err(e) => Err(e),
595 }
596 }
597 None => {
598 debug!(name = %name, "network not found by name");
599 Ok(None)
600 }
601 }
602 }
603
604 async fn get_by_chain_id(
605 &self,
606 network_type: NetworkType,
607 chain_id: u64,
608 ) -> Result<Option<NetworkRepoModel>, RepositoryError> {
609 if network_type != NetworkType::Evm {
611 return Ok(None);
612 }
613
614 let mut conn = self.client.as_ref().clone();
615
616 debug!(chain_id = %chain_id, network_type = ?network_type, "getting network by chain ID");
617
618 let chain_id_index_key = self.network_chain_id_index_key(&network_type, chain_id);
620 let network_id: Option<String> = conn
621 .get(&chain_id_index_key)
622 .await
623 .map_err(|e| self.map_redis_error(e, "get_network_by_chain_id_index"))?;
624
625 match network_id {
626 Some(id) => {
627 match self.get_by_id(id.clone()).await {
628 Ok(network) => {
629 debug!(chain_id = %chain_id, "found network by chain ID");
630 Ok(Some(network))
631 }
632 Err(RepositoryError::NotFound(_)) => {
633 warn!(network_type = ?network_type, chain_id = %chain_id, "stale chain ID index found for network");
635 Ok(None)
636 }
637 Err(e) => Err(e),
638 }
639 }
640 None => {
641 debug!(chain_id = %chain_id, "network not found by chain ID");
642 Ok(None)
643 }
644 }
645 }
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651 use crate::config::{
652 EvmNetworkConfig, NetworkConfigCommon, SolanaNetworkConfig, StellarNetworkConfig,
653 };
654 use crate::models::NetworkConfigData;
655 use redis::aio::ConnectionManager;
656 use uuid::Uuid;
657
658 fn create_test_network(name: &str, network_type: NetworkType) -> NetworkRepoModel {
659 let common = NetworkConfigCommon {
660 network: name.to_string(),
661 from: None,
662 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
663 explorer_urls: None,
664 average_blocktime_ms: Some(12000),
665 is_testnet: Some(true),
666 tags: None,
667 };
668
669 match network_type {
670 NetworkType::Evm => {
671 let evm_config = EvmNetworkConfig {
672 common,
673 chain_id: Some(1),
674 required_confirmations: Some(1),
675 features: None,
676 symbol: Some("ETH".to_string()),
677 gas_price_cache: None,
678 };
679 NetworkRepoModel::new_evm(evm_config)
680 }
681 NetworkType::Solana => {
682 let solana_config = SolanaNetworkConfig { common };
683 NetworkRepoModel::new_solana(solana_config)
684 }
685 NetworkType::Stellar => {
686 let stellar_config = StellarNetworkConfig {
687 common,
688 passphrase: None,
689 };
690 NetworkRepoModel::new_stellar(stellar_config)
691 }
692 }
693 }
694
695 async fn setup_test_repo() -> RedisNetworkRepository {
696 let redis_url = "redis://localhost:6379";
697 let random_id = Uuid::new_v4().to_string();
698 let key_prefix = format!("test_prefix_{}", random_id);
699
700 let client = redis::Client::open(redis_url).expect("Failed to create Redis client");
701 let connection_manager = ConnectionManager::new(client)
702 .await
703 .expect("Failed to create connection manager");
704
705 RedisNetworkRepository::new(Arc::new(connection_manager), key_prefix.to_string())
706 .expect("Failed to create repository")
707 }
708
709 #[tokio::test]
710 #[ignore = "Requires active Redis instance"]
711 async fn test_create_network() {
712 let repo = setup_test_repo().await;
713 let test_network_random = Uuid::new_v4().to_string();
714 let network = create_test_network(&test_network_random, NetworkType::Evm);
715
716 let result = repo.create(network.clone()).await;
717 assert!(result.is_ok());
718
719 let created = result.unwrap();
720 assert_eq!(created.id, network.id);
721 assert_eq!(created.name, network.name);
722 assert_eq!(created.network_type, network.network_type);
723 }
724
725 #[tokio::test]
726 #[ignore = "Requires active Redis instance"]
727 async fn test_get_network_by_id() {
728 let repo = setup_test_repo().await;
729 let test_network_random = Uuid::new_v4().to_string();
730 let network = create_test_network(&test_network_random, NetworkType::Evm);
731
732 repo.create(network.clone()).await.unwrap();
733
734 let retrieved = repo.get_by_id(network.id.clone()).await;
735 assert!(retrieved.is_ok());
736
737 let retrieved_network = retrieved.unwrap();
738 assert_eq!(retrieved_network.id, network.id);
739 assert_eq!(retrieved_network.name, network.name);
740 assert_eq!(retrieved_network.network_type, network.network_type);
741 }
742
743 #[tokio::test]
744 #[ignore = "Requires active Redis instance"]
745 async fn test_get_nonexistent_network() {
746 let repo = setup_test_repo().await;
747 let result = repo.get_by_id("nonexistent".to_string()).await;
748 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
749 }
750
751 #[tokio::test]
752 #[ignore = "Requires active Redis instance"]
753 async fn test_create_duplicate_network() {
754 let repo = setup_test_repo().await;
755 let test_network_random = Uuid::new_v4().to_string();
756 let network = create_test_network(&test_network_random, NetworkType::Evm);
757
758 repo.create(network.clone()).await.unwrap();
759 let result = repo.create(network).await;
760 assert!(matches!(
761 result,
762 Err(RepositoryError::ConstraintViolation(_))
763 ));
764 }
765
766 #[tokio::test]
767 #[ignore = "Requires active Redis instance"]
768 async fn test_update_network() {
769 let repo = setup_test_repo().await;
770 let random_id = Uuid::new_v4().to_string();
771 let random_name = Uuid::new_v4().to_string();
772 let mut network = create_test_network(&random_name, NetworkType::Evm);
773 network.id = format!("evm:{}", random_id);
774
775 repo.create(network.clone()).await.unwrap();
777
778 let updated = repo.update(network.id.clone(), network.clone()).await;
780 assert!(updated.is_ok());
781
782 let updated_network = updated.unwrap();
783 assert_eq!(updated_network.id, network.id);
784 assert_eq!(updated_network.name, network.name);
785 }
786
787 #[tokio::test]
788 #[ignore = "Requires active Redis instance"]
789 async fn test_delete_network() {
790 let repo = setup_test_repo().await;
791 let random_id = Uuid::new_v4().to_string();
792 let random_name = Uuid::new_v4().to_string();
793 let mut network = create_test_network(&random_name, NetworkType::Evm);
794 network.id = format!("evm:{}", random_id);
795
796 repo.create(network.clone()).await.unwrap();
798
799 let result = repo.delete_by_id(network.id.clone()).await;
801 assert!(result.is_ok());
802
803 let get_result = repo.get_by_id(network.id).await;
805 assert!(matches!(get_result, Err(RepositoryError::NotFound(_))));
806 }
807
808 #[tokio::test]
809 #[ignore = "Requires active Redis instance"]
810 async fn test_list_all_networks() {
811 let repo = setup_test_repo().await;
812 let test_network_random = Uuid::new_v4().to_string();
813 let test_network_random2 = Uuid::new_v4().to_string();
814 let network1 = create_test_network(&test_network_random, NetworkType::Evm);
815 let network2 = create_test_network(&test_network_random2, NetworkType::Solana);
816
817 repo.create(network1.clone()).await.unwrap();
818 repo.create(network2.clone()).await.unwrap();
819
820 let networks = repo.list_all().await.unwrap();
821 assert_eq!(networks.len(), 2);
822
823 let ids: Vec<String> = networks.iter().map(|n| n.id.clone()).collect();
824 assert!(ids.contains(&network1.id));
825 assert!(ids.contains(&network2.id));
826 }
827
828 #[tokio::test]
829 #[ignore = "Requires active Redis instance"]
830 async fn test_count_networks() {
831 let repo = setup_test_repo().await;
832 let test_network_random = Uuid::new_v4().to_string();
833 let test_network_random2 = Uuid::new_v4().to_string();
834 let network1 = create_test_network(&test_network_random, NetworkType::Evm);
835 let network2 = create_test_network(&test_network_random2, NetworkType::Solana);
836
837 assert_eq!(repo.count().await.unwrap(), 0);
838
839 repo.create(network1).await.unwrap();
840 assert_eq!(repo.count().await.unwrap(), 1);
841
842 repo.create(network2).await.unwrap();
843 assert_eq!(repo.count().await.unwrap(), 2);
844 }
845
846 #[tokio::test]
847 #[ignore = "Requires active Redis instance"]
848 async fn test_list_paginated() {
849 let repo = setup_test_repo().await;
850 let test_network_random = Uuid::new_v4().to_string();
851 let test_network_random2 = Uuid::new_v4().to_string();
852 let test_network_random3 = Uuid::new_v4().to_string();
853 let network1 = create_test_network(&test_network_random, NetworkType::Evm);
854 let network2 = create_test_network(&test_network_random2, NetworkType::Solana);
855 let network3 = create_test_network(&test_network_random3, NetworkType::Stellar);
856
857 repo.create(network1).await.unwrap();
858 repo.create(network2).await.unwrap();
859 repo.create(network3).await.unwrap();
860
861 let query = PaginationQuery {
862 page: 1,
863 per_page: 2,
864 };
865
866 let result = repo.list_paginated(query).await.unwrap();
867 assert_eq!(result.items.len(), 2);
868 assert_eq!(result.total, 3);
869 assert_eq!(result.page, 1);
870 assert_eq!(result.per_page, 2);
871 }
872
873 #[tokio::test]
874 #[ignore = "Requires active Redis instance"]
875 async fn test_get_by_name() {
876 let repo = setup_test_repo().await;
877 let test_network_random = Uuid::new_v4().to_string();
878 let network = create_test_network(&test_network_random, NetworkType::Evm);
879
880 repo.create(network.clone()).await.unwrap();
881
882 let retrieved = repo
883 .get_by_name(NetworkType::Evm, &test_network_random)
884 .await
885 .unwrap();
886 assert!(retrieved.is_some());
887 assert_eq!(retrieved.unwrap().name, test_network_random);
888
889 let not_found = repo
890 .get_by_name(NetworkType::Solana, &test_network_random)
891 .await
892 .unwrap();
893 assert!(not_found.is_none());
894 }
895
896 #[tokio::test]
897 #[ignore = "Requires active Redis instance"]
898 async fn test_get_by_chain_id() {
899 let repo = setup_test_repo().await;
900 let test_network_random = Uuid::new_v4().to_string();
901 let network = create_test_network(&test_network_random, NetworkType::Evm);
902
903 repo.create(network.clone()).await.unwrap();
904
905 let retrieved = repo.get_by_chain_id(NetworkType::Evm, 1).await.unwrap();
906 assert!(retrieved.is_some());
907 assert_eq!(retrieved.unwrap().name, test_network_random);
908
909 let not_found = repo.get_by_chain_id(NetworkType::Evm, 999).await.unwrap();
910 assert!(not_found.is_none());
911
912 let solana_result = repo.get_by_chain_id(NetworkType::Solana, 1).await.unwrap();
913 assert!(solana_result.is_none());
914 }
915
916 #[tokio::test]
917 #[ignore = "Requires active Redis instance"]
918 async fn test_update_nonexistent_network() {
919 let repo = setup_test_repo().await;
920 let test_network_random = Uuid::new_v4().to_string();
921 let network = create_test_network(&test_network_random, NetworkType::Evm);
922
923 let result = repo.update(network.id.clone(), network).await;
924 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
925 }
926
927 #[tokio::test]
928 #[ignore = "Requires active Redis instance"]
929 async fn test_delete_nonexistent_network() {
930 let repo = setup_test_repo().await;
931
932 let result = repo.delete_by_id("nonexistent".to_string()).await;
933 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
934 }
935
936 #[tokio::test]
937 #[ignore = "Requires active Redis instance"]
938 async fn test_empty_id_validation() {
939 let repo = setup_test_repo().await;
940
941 let create_result = repo
942 .create(NetworkRepoModel {
943 id: "".to_string(),
944 name: "test".to_string(),
945 network_type: NetworkType::Evm,
946 config: NetworkConfigData::Evm(EvmNetworkConfig {
947 common: NetworkConfigCommon {
948 network: "test".to_string(),
949 from: None,
950 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
951 explorer_urls: None,
952 average_blocktime_ms: Some(12000),
953 is_testnet: Some(true),
954 tags: None,
955 },
956 chain_id: Some(1),
957 required_confirmations: Some(1),
958 features: None,
959 symbol: Some("ETH".to_string()),
960 gas_price_cache: None,
961 }),
962 })
963 .await;
964
965 assert!(matches!(
966 create_result,
967 Err(RepositoryError::InvalidData(_))
968 ));
969
970 let get_result = repo.get_by_id("".to_string()).await;
971 assert!(matches!(get_result, Err(RepositoryError::InvalidData(_))));
972
973 let update_result = repo
974 .update(
975 "".to_string(),
976 create_test_network("test", NetworkType::Evm),
977 )
978 .await;
979 assert!(matches!(
980 update_result,
981 Err(RepositoryError::InvalidData(_))
982 ));
983
984 let delete_result = repo.delete_by_id("".to_string()).await;
985 assert!(matches!(
986 delete_result,
987 Err(RepositoryError::InvalidData(_))
988 ));
989 }
990
991 #[tokio::test]
992 #[ignore = "Requires active Redis instance"]
993 async fn test_pagination_validation() {
994 let repo = setup_test_repo().await;
995
996 let query = PaginationQuery {
997 page: 1,
998 per_page: 0,
999 };
1000 let result = repo.list_paginated(query).await;
1001 assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
1002 }
1003
1004 #[tokio::test]
1005 #[ignore = "Requires active Redis instance"]
1006 async fn test_id_mismatch_validation() {
1007 let repo = setup_test_repo().await;
1008 let test_network_random = Uuid::new_v4().to_string();
1009 let network = create_test_network(&test_network_random, NetworkType::Evm);
1010
1011 repo.create(network.clone()).await.unwrap();
1012
1013 let result = repo.update("different-id".to_string(), network).await;
1014 assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
1015 }
1016
1017 #[tokio::test]
1018 #[ignore = "Requires active Redis instance"]
1019 async fn test_empty_name_validation() {
1020 let repo = setup_test_repo().await;
1021
1022 let result = repo.get_by_name(NetworkType::Evm, "").await;
1023 assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
1024 }
1025
1026 #[tokio::test]
1027 #[ignore = "Requires active Redis instance"]
1028 async fn test_has_entries_empty_storage() {
1029 let repo = setup_test_repo().await;
1030
1031 let result = repo.has_entries().await.unwrap();
1032 assert!(!result, "Empty storage should return false");
1033 }
1034
1035 #[tokio::test]
1036 #[ignore = "Requires active Redis instance"]
1037 async fn test_has_entries_with_data() {
1038 let repo = setup_test_repo().await;
1039 let test_network_random = Uuid::new_v4().to_string();
1040 let network = create_test_network(&test_network_random, NetworkType::Evm);
1041
1042 assert!(!repo.has_entries().await.unwrap());
1043
1044 repo.create(network).await.unwrap();
1045
1046 assert!(repo.has_entries().await.unwrap());
1047 }
1048
1049 #[tokio::test]
1050 #[ignore = "Requires active Redis instance"]
1051 async fn test_drop_all_entries_empty_storage() {
1052 let repo = setup_test_repo().await;
1053
1054 let result = repo.drop_all_entries().await;
1055 assert!(result.is_ok());
1056
1057 assert!(!repo.has_entries().await.unwrap());
1058 }
1059
1060 #[tokio::test]
1061 #[ignore = "Requires active Redis instance"]
1062 async fn test_drop_all_entries_with_data() {
1063 let repo = setup_test_repo().await;
1064 let test_network_random1 = Uuid::new_v4().to_string();
1065 let test_network_random2 = Uuid::new_v4().to_string();
1066 let network1 = create_test_network(&test_network_random1, NetworkType::Evm);
1067 let network2 = create_test_network(&test_network_random2, NetworkType::Solana);
1068
1069 repo.create(network1.clone()).await.unwrap();
1071 repo.create(network2.clone()).await.unwrap();
1072
1073 assert!(repo.has_entries().await.unwrap());
1075 assert_eq!(repo.count().await.unwrap(), 2);
1076 assert!(repo
1077 .get_by_name(NetworkType::Evm, &test_network_random1)
1078 .await
1079 .unwrap()
1080 .is_some());
1081
1082 let result = repo.drop_all_entries().await;
1084 assert!(result.is_ok());
1085
1086 assert!(!repo.has_entries().await.unwrap());
1088 assert_eq!(repo.count().await.unwrap(), 0);
1089 assert!(repo
1090 .get_by_name(NetworkType::Evm, &test_network_random1)
1091 .await
1092 .unwrap()
1093 .is_none());
1094 assert!(repo
1095 .get_by_name(NetworkType::Solana, &test_network_random2)
1096 .await
1097 .unwrap()
1098 .is_none());
1099
1100 assert!(matches!(
1102 repo.get_by_id(network1.id).await,
1103 Err(RepositoryError::NotFound(_))
1104 ));
1105 assert!(matches!(
1106 repo.get_by_id(network2.id).await,
1107 Err(RepositoryError::NotFound(_))
1108 ));
1109 }
1110
1111 #[tokio::test]
1112 #[ignore = "Requires active Redis instance"]
1113 async fn test_drop_all_entries_cleans_indexes() {
1114 let repo = setup_test_repo().await;
1115 let test_network_random = Uuid::new_v4().to_string();
1116 let mut network = create_test_network(&test_network_random, NetworkType::Evm);
1117
1118 if let crate::models::NetworkConfigData::Evm(ref mut evm_config) = network.config {
1120 evm_config.chain_id = Some(12345);
1121 }
1122
1123 repo.create(network.clone()).await.unwrap();
1125
1126 assert!(repo
1128 .get_by_name(NetworkType::Evm, &test_network_random)
1129 .await
1130 .unwrap()
1131 .is_some());
1132 assert!(repo
1133 .get_by_chain_id(NetworkType::Evm, 12345)
1134 .await
1135 .unwrap()
1136 .is_some());
1137
1138 repo.drop_all_entries().await.unwrap();
1140
1141 assert!(repo
1143 .get_by_name(NetworkType::Evm, &test_network_random)
1144 .await
1145 .unwrap()
1146 .is_none());
1147 assert!(repo
1148 .get_by_chain_id(NetworkType::Evm, 12345)
1149 .await
1150 .unwrap()
1151 .is_none());
1152 }
1153}