1use std::sync::Arc;
4
5use crate::{
6 config::{Config, RepositoryStorageType, ServerConfig},
7 jobs::JobProducerTrait,
8 models::{
9 ApiKeyRepoModel, NetworkRepoModel, NotificationRepoModel, PluginModel, Relayer,
10 RelayerRepoModel, Signer as SignerDomainModel, SignerFileConfig, SignerRepoModel,
11 ThinDataAppState, TransactionRepoModel,
12 },
13 repositories::{
14 ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
15 Repository, TransactionCounterTrait, TransactionRepository,
16 },
17 services::signer::{Signer as SignerService, SignerFactory},
18};
19use color_eyre::{eyre::WrapErr, Report, Result};
20use futures::future::try_join_all;
21use tracing::info;
22
23async fn process_api_key<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
24 server_config: &ServerConfig,
25 app_state: &ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
26) -> Result<()>
27where
28 J: JobProducerTrait + Send + Sync + 'static,
29 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
30 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
31 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
32 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
33 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
34 TCR: TransactionCounterTrait + Send + Sync + 'static,
35 PR: PluginRepositoryTrait + Send + Sync + 'static,
36 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
37{
38 let api_key_model = ApiKeyRepoModel::new(
39 "default".to_string(),
40 server_config.api_key.clone(),
41 vec!["*".to_string()],
42 vec!["*".to_string()],
43 );
44
45 app_state
46 .api_key_repository
47 .create(api_key_model)
48 .await
49 .wrap_err("Failed to create api key repository entry")?;
50
51 Ok(())
52}
53
54async fn process_plugins<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
56 config_file: &Config,
57 app_state: &ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
58) -> Result<()>
59where
60 J: JobProducerTrait + Send + Sync + 'static,
61 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
62 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
63 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
64 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
65 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
66 TCR: TransactionCounterTrait + Send + Sync + 'static,
67 PR: PluginRepositoryTrait + Send + Sync + 'static,
68 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
69{
70 if let Some(plugins) = &config_file.plugins {
71 let plugin_futures = plugins.iter().map(|plugin| async {
72 let plugin_model = PluginModel::try_from(plugin.clone())
73 .wrap_err("Failed to convert plugin config")?;
74 app_state
75 .plugin_repository
76 .add(plugin_model)
77 .await
78 .wrap_err("Failed to create plugin repository entry")?;
79 Ok::<(), Report>(())
80 });
81
82 try_join_all(plugin_futures)
83 .await
84 .wrap_err("Failed to initialize plugin repository")?;
85 Ok(())
86 } else {
87 Ok(())
88 }
89}
90
91async fn process_signer(signer: &SignerFileConfig) -> Result<SignerRepoModel> {
93 let domain_signer = SignerDomainModel::try_from(signer.clone())
95 .wrap_err("Failed to convert signer config to domain model")?;
96
97 let signer_repo_model = SignerRepoModel::from(domain_signer);
99
100 Ok(signer_repo_model)
101}
102
103async fn process_signers<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
111 config_file: &Config,
112 app_state: &ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
113) -> Result<()>
114where
115 J: JobProducerTrait + Send + Sync + 'static,
116 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
117 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
118 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
119 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
120 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
121 TCR: TransactionCounterTrait + Send + Sync + 'static,
122 PR: PluginRepositoryTrait + Send + Sync + 'static,
123 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
124{
125 let signer_futures = config_file.signers.iter().map(|signer| async {
126 let signer_repo_model = process_signer(signer).await?;
127
128 app_state
129 .signer_repository
130 .create(signer_repo_model)
131 .await
132 .wrap_err("Failed to create signer repository entry")?;
133 Ok::<(), Report>(())
134 });
135
136 try_join_all(signer_futures)
137 .await
138 .wrap_err("Failed to initialize signer repository")?;
139 Ok(())
140}
141
142async fn process_notifications<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
150 config_file: &Config,
151 app_state: &ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
152) -> Result<()>
153where
154 J: JobProducerTrait + Send + Sync + 'static,
155 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
156 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
157 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
158 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
159 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
160 TCR: TransactionCounterTrait + Send + Sync + 'static,
161 PR: PluginRepositoryTrait + Send + Sync + 'static,
162 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
163{
164 let notification_futures = config_file.notifications.iter().map(|notification| async {
165 let notification_repo_model = NotificationRepoModel::try_from(notification.clone())
166 .wrap_err("Failed to convert notification config")?;
167
168 app_state
169 .notification_repository
170 .create(notification_repo_model)
171 .await
172 .wrap_err("Failed to create notification repository entry")?;
173 Ok::<(), Report>(())
174 });
175
176 try_join_all(notification_futures)
177 .await
178 .wrap_err("Failed to initialize notification repository")?;
179 Ok(())
180}
181
182async fn process_networks<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
190 config_file: &Config,
191 app_state: &ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
192) -> Result<()>
193where
194 J: JobProducerTrait + Send + Sync + 'static,
195 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
196 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
197 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
198 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
199 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
200 TCR: TransactionCounterTrait + Send + Sync + 'static,
201 PR: PluginRepositoryTrait + Send + Sync + 'static,
202 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
203{
204 let network_futures = config_file.networks.iter().map(|network| async move {
205 let network_repo_model = NetworkRepoModel::try_from(network.clone())?;
206
207 app_state
208 .network_repository
209 .create(network_repo_model)
210 .await
211 .wrap_err("Failed to create network repository entry")?;
212 Ok::<(), Report>(())
213 });
214
215 try_join_all(network_futures)
216 .await
217 .wrap_err("Failed to initialize network repository")?;
218 Ok(())
219}
220
221async fn process_relayers<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
232 config_file: &Config,
233 app_state: &ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
234) -> Result<()>
235where
236 J: JobProducerTrait + Send + Sync + 'static,
237 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
238 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
239 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
240 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
241 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
242 TCR: TransactionCounterTrait + Send + Sync + 'static,
243 PR: PluginRepositoryTrait + Send + Sync + 'static,
244 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
245{
246 let signers = app_state.signer_repository.list_all().await?;
247
248 let relayer_futures = config_file.relayers.iter().map(|relayer| async {
249 let domain_relayer = Relayer::try_from(relayer.clone())
251 .wrap_err("Failed to convert relayer config to domain model")?;
252 let mut repo_model = RelayerRepoModel::from(domain_relayer);
253 let signer_model = signers
254 .iter()
255 .find(|s| s.id == repo_model.signer_id)
256 .ok_or_else(|| eyre::eyre!("Signer not found"))?;
257
258 let network_type = repo_model.network_type;
259 let signer_service = SignerFactory::create_signer(
260 &network_type,
261 &SignerDomainModel::from(signer_model.clone()),
262 )
263 .await
264 .wrap_err("Failed to create signer service")?;
265
266 let address = signer_service.address().await?;
267 repo_model.address = address.to_string();
268
269 app_state
270 .relayer_repository
271 .create(repo_model)
272 .await
273 .wrap_err("Failed to create relayer repository entry")?;
274 Ok::<(), Report>(())
275 });
276
277 try_join_all(relayer_futures)
278 .await
279 .wrap_err("Failed to initialize relayer repository")?;
280 Ok(())
281}
282
283async fn is_redis_populated<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
288 app_state: &ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
289) -> Result<bool>
290where
291 J: JobProducerTrait + Send + Sync + 'static,
292 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
293 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
294 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
295 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
296 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
297 TCR: TransactionCounterTrait + Send + Sync + 'static,
298 PR: PluginRepositoryTrait + Send + Sync + 'static,
299 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
300{
301 if app_state.relayer_repository.has_entries().await? {
302 return Ok(true);
303 }
304
305 if app_state.transaction_repository.has_entries().await? {
306 return Ok(true);
307 }
308
309 if app_state.signer_repository.has_entries().await? {
310 return Ok(true);
311 }
312
313 if app_state.notification_repository.has_entries().await? {
314 return Ok(true);
315 }
316
317 if app_state.network_repository.has_entries().await? {
318 return Ok(true);
319 }
320
321 if app_state.plugin_repository.has_entries().await? {
322 return Ok(true);
323 }
324
325 Ok(false)
326}
327
328pub async fn process_config_file<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
336 config_file: Config,
337 server_config: Arc<ServerConfig>,
338 app_state: &ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
339) -> Result<()>
340where
341 J: JobProducerTrait + Send + Sync + 'static,
342 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
343 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
344 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
345 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
346 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
347 TCR: TransactionCounterTrait + Send + Sync + 'static,
348 PR: PluginRepositoryTrait + Send + Sync + 'static,
349 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
350{
351 let should_process_config_file = match server_config.repository_storage_type {
352 RepositoryStorageType::InMemory => true,
353 RepositoryStorageType::Redis => {
354 server_config.reset_storage_on_start || !is_redis_populated(app_state).await?
355 }
356 };
357
358 if !should_process_config_file {
359 info!("Skipping config file processing");
360 return Ok(());
361 }
362
363 if server_config.reset_storage_on_start {
364 info!("Resetting storage on start due to server config flag RESET_STORAGE_ON_START = true");
365 app_state.relayer_repository.drop_all_entries().await?;
366 app_state.transaction_repository.drop_all_entries().await?;
367 app_state.signer_repository.drop_all_entries().await?;
368 app_state.notification_repository.drop_all_entries().await?;
369 app_state.network_repository.drop_all_entries().await?;
370 app_state.plugin_repository.drop_all_entries().await?;
371 app_state.api_key_repository.drop_all_entries().await?;
372 }
373
374 if should_process_config_file {
375 info!("Processing config file");
376 process_plugins(&config_file, app_state).await?;
377 process_signers(&config_file, app_state).await?;
378 process_notifications(&config_file, app_state).await?;
379 process_networks(&config_file, app_state).await?;
380 process_relayers(&config_file, app_state).await?;
381 process_api_key(&server_config, app_state).await?;
382 }
383 Ok(())
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use crate::{
390 config::{ConfigFileNetworkType, NetworksFileConfig, PluginFileConfig},
391 constants::DEFAULT_PLUGIN_TIMEOUT_SECONDS,
392 jobs::MockJobProducerTrait,
393 models::{
394 relayer::RelayerFileConfig, AppState, AwsKmsSignerFileConfig,
395 GoogleCloudKmsKeyFileConfig, GoogleCloudKmsServiceAccountFileConfig,
396 GoogleCloudKmsSignerFileConfig, LocalSignerFileConfig, NetworkType, NotificationConfig,
397 NotificationType, PaginationQuery, PlainOrEnvValue, SecretString, SignerConfigStorage,
398 SignerFileConfig, SignerFileConfigEnum, VaultSignerFileConfig,
399 VaultTransitSignerFileConfig,
400 },
401 repositories::{
402 ApiKeyRepositoryStorage, InMemoryApiKeyRepository, InMemoryNetworkRepository,
403 InMemoryNotificationRepository, InMemoryPluginRepository, InMemorySignerRepository,
404 InMemoryTransactionCounter, InMemoryTransactionRepository, NetworkRepositoryStorage,
405 NotificationRepositoryStorage, PluginRepositoryStorage, RelayerRepositoryStorage,
406 SignerRepositoryStorage, TransactionCounterRepositoryStorage,
407 TransactionRepositoryStorage,
408 },
409 utils::mocks::mockutils::{
410 create_mock_network, create_mock_notification, create_mock_relayer, create_mock_signer,
411 create_test_server_config,
412 },
413 };
414 use actix_web::web::ThinData;
415 use mockito;
416 use serde_json::json;
417 use std::{sync::Arc, time::Duration};
418
419 fn create_test_app_state() -> AppState<
420 MockJobProducerTrait,
421 RelayerRepositoryStorage,
422 TransactionRepositoryStorage,
423 NetworkRepositoryStorage,
424 NotificationRepositoryStorage,
425 SignerRepositoryStorage,
426 TransactionCounterRepositoryStorage,
427 PluginRepositoryStorage,
428 ApiKeyRepositoryStorage,
429 > {
430 let mut mock_job_producer = MockJobProducerTrait::new();
432
433 mock_job_producer
435 .expect_produce_transaction_request_job()
436 .returning(|_, _| Box::pin(async { Ok(()) }));
437
438 mock_job_producer
439 .expect_produce_submit_transaction_job()
440 .returning(|_, _| Box::pin(async { Ok(()) }));
441
442 mock_job_producer
443 .expect_produce_check_transaction_status_job()
444 .returning(|_, _| Box::pin(async { Ok(()) }));
445
446 mock_job_producer
447 .expect_produce_send_notification_job()
448 .returning(|_, _| Box::pin(async { Ok(()) }));
449
450 AppState {
451 relayer_repository: Arc::new(RelayerRepositoryStorage::new_in_memory()),
452 transaction_repository: Arc::new(TransactionRepositoryStorage::new_in_memory()),
453 signer_repository: Arc::new(SignerRepositoryStorage::new_in_memory()),
454 notification_repository: Arc::new(NotificationRepositoryStorage::new_in_memory()),
455 network_repository: Arc::new(NetworkRepositoryStorage::new_in_memory()),
456 transaction_counter_store: Arc::new(
457 TransactionCounterRepositoryStorage::new_in_memory(),
458 ),
459 job_producer: Arc::new(mock_job_producer),
460 plugin_repository: Arc::new(PluginRepositoryStorage::new_in_memory()),
461 api_key_repository: Arc::new(ApiKeyRepositoryStorage::new_in_memory()),
462 }
463 }
464
465 #[tokio::test]
466 async fn test_process_signer_test() {
467 let signer = SignerFileConfig {
468 id: "test-signer".to_string(),
469 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
470 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
471 passphrase: PlainOrEnvValue::Plain {
472 value: SecretString::new("test"),
473 },
474 }),
475 };
476
477 let result = process_signer(&signer).await;
478
479 assert!(
480 result.is_ok(),
481 "Failed to process test signer: {:?}",
482 result.err()
483 );
484 let model = result.unwrap();
485
486 assert_eq!(model.id, "test-signer");
487
488 match model.config {
489 SignerConfigStorage::Local(config) => {
490 assert!(!config.raw_key.is_empty());
491 assert_eq!(config.raw_key.len(), 32);
492 }
493 _ => panic!("Expected Local config"),
494 }
495 }
496
497 #[tokio::test]
498 async fn test_process_signer_vault_transit() -> Result<()> {
499 let signer = SignerFileConfig {
500 id: "vault-transit-signer".to_string(),
501 config: SignerFileConfigEnum::VaultTransit(VaultTransitSignerFileConfig {
502 key_name: "test-transit-key".to_string(),
503 address: "https://vault.example.com".to_string(),
504 namespace: Some("test-namespace".to_string()),
505 role_id: PlainOrEnvValue::Plain {
506 value: SecretString::new("test-role"),
507 },
508 secret_id: PlainOrEnvValue::Plain {
509 value: SecretString::new("test-secret"),
510 },
511 pubkey: "test-pubkey".to_string(),
512 mount_point: Some("transit".to_string()),
513 }),
514 };
515
516 let result = process_signer(&signer).await;
517
518 assert!(
519 result.is_ok(),
520 "Failed to process vault transit signer: {:?}",
521 result.err()
522 );
523 let model = result.unwrap();
524
525 assert_eq!(model.id, "vault-transit-signer");
526
527 match model.config {
528 SignerConfigStorage::VaultTransit(config) => {
529 assert_eq!(config.key_name, "test-transit-key");
530 assert_eq!(config.address, "https://vault.example.com");
531 assert_eq!(config.namespace, Some("test-namespace".to_string()));
532 assert_eq!(config.role_id.to_str().as_str(), "test-role");
533 assert_eq!(config.secret_id.to_str().as_str(), "test-secret");
534 assert_eq!(config.pubkey, "test-pubkey");
535 assert_eq!(config.mount_point, Some("transit".to_string()));
536 }
537 _ => panic!("Expected VaultTransit config"),
538 }
539
540 Ok(())
541 }
542
543 #[tokio::test]
544 async fn test_process_signer_aws_kms() -> Result<()> {
545 let signer = SignerFileConfig {
546 id: "aws-kms-signer".to_string(),
547 config: SignerFileConfigEnum::AwsKms(AwsKmsSignerFileConfig {
548 region: "us-east-1".to_string(),
549 key_id: "test-key-id".to_string(),
550 }),
551 };
552
553 let result = process_signer(&signer).await;
554
555 assert!(
556 result.is_ok(),
557 "Failed to process AWS KMS signer: {:?}",
558 result.err()
559 );
560 let model = result.unwrap();
561
562 assert_eq!(model.id, "aws-kms-signer");
563
564 match model.config {
565 SignerConfigStorage::AwsKms(_) => {}
566 _ => panic!("Expected AwsKms config"),
567 }
568
569 Ok(())
570 }
571
572 async fn setup_mock_approle_login(
574 mock_server: &mut mockito::ServerGuard,
575 role_id: &str,
576 secret_id: &str,
577 token: &str,
578 ) -> mockito::Mock {
579 mock_server
580 .mock("POST", "/v1/auth/approle/login")
581 .match_body(mockito::Matcher::Json(json!({
582 "role_id": role_id,
583 "secret_id": secret_id
584 })))
585 .with_status(200)
586 .with_header("content-type", "application/json")
587 .with_body(
588 serde_json::to_string(&json!({
589 "request_id": "test-request-id",
590 "lease_id": "",
591 "renewable": false,
592 "lease_duration": 0,
593 "data": null,
594 "wrap_info": null,
595 "warnings": null,
596 "auth": {
597 "client_token": token,
598 "accessor": "test-accessor",
599 "policies": ["default"],
600 "token_policies": ["default"],
601 "metadata": {
602 "role_name": "test-role"
603 },
604 "lease_duration": 3600,
605 "renewable": true,
606 "entity_id": "test-entity-id",
607 "token_type": "service",
608 "orphan": true
609 }
610 }))
611 .unwrap(),
612 )
613 .create_async()
614 .await
615 }
616
617 #[tokio::test]
618 async fn test_process_signer_vault() -> Result<()> {
619 let mut mock_server = mockito::Server::new_async().await;
620
621 let _login_mock = setup_mock_approle_login(
622 &mut mock_server,
623 "test-role-id",
624 "test-secret-id",
625 "test-token",
626 )
627 .await;
628
629 let _secret_mock = mock_server
630 .mock("GET", "/v1/secret/data/test-key")
631 .match_header("X-Vault-Token", "test-token")
632 .with_status(200)
633 .with_header("content-type", "application/json")
634 .with_body(serde_json::to_string(&json!({
635 "request_id": "test-request-id",
636 "lease_id": "",
637 "renewable": false,
638 "lease_duration": 0,
639 "data": {
640 "data": {
641 "value": "C5ACE14AB163556747F02C1110911537578FBE335FB74D18FBF82990AD70C3B9"
642 },
643 "metadata": {
644 "created_time": "2024-01-01T00:00:00Z",
645 "deletion_time": "",
646 "destroyed": false,
647 "version": 1
648 }
649 },
650 "wrap_info": null,
651 "warnings": null,
652 "auth": null
653 })).unwrap())
654 .create_async()
655 .await;
656
657 let signer = SignerFileConfig {
658 id: "vault-signer".to_string(),
659 config: SignerFileConfigEnum::Vault(VaultSignerFileConfig {
660 key_name: "test-key".to_string(),
661 address: mock_server.url(),
662 namespace: Some("test-namespace".to_string()),
663 role_id: PlainOrEnvValue::Plain {
664 value: SecretString::new("test-role-id"),
665 },
666 secret_id: PlainOrEnvValue::Plain {
667 value: SecretString::new("test-secret-id"),
668 },
669 mount_point: Some("secret".to_string()),
670 }),
671 };
672
673 let result = process_signer(&signer).await;
674
675 assert!(
676 result.is_ok(),
677 "Failed to process Vault signer: {:?}",
678 result.err()
679 );
680 let model = result.unwrap();
681
682 assert_eq!(model.id, "vault-signer");
683
684 match model.config {
685 SignerConfigStorage::Vault(_) => {}
686 _ => panic!("Expected Vault config"),
687 }
688
689 Ok(())
690 }
691
692 #[tokio::test]
693 async fn test_process_signers() -> Result<()> {
694 let signers = vec![
696 SignerFileConfig {
697 id: "test-signer-1".to_string(),
698 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
699 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
700 passphrase: PlainOrEnvValue::Plain {
701 value: SecretString::new("test"),
702 },
703 }),
704 },
705 SignerFileConfig {
706 id: "test-signer-2".to_string(),
707 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
708 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
709 passphrase: PlainOrEnvValue::Plain {
710 value: SecretString::new("test"),
711 },
712 }),
713 },
714 ];
715
716 let config = Config {
718 signers,
719 relayers: vec![],
720 notifications: vec![],
721 networks: NetworksFileConfig::new(vec![]).unwrap(),
722 plugins: Some(vec![]),
723 };
724
725 let app_state = ThinData(create_test_app_state());
727
728 process_signers(&config, &app_state).await?;
730
731 let stored_signers = app_state.signer_repository.list_all().await?;
733 assert_eq!(stored_signers.len(), 2);
734 assert!(stored_signers.iter().any(|s| s.id == "test-signer-1"));
735 assert!(stored_signers.iter().any(|s| s.id == "test-signer-2"));
736
737 Ok(())
738 }
739
740 #[tokio::test]
741 async fn test_process_notifications() -> Result<()> {
742 let notifications = vec![
744 NotificationConfig {
745 id: "test-notification-1".to_string(),
746 r#type: NotificationType::Webhook,
747 url: "https://hooks.slack.com/test1".to_string(),
748 signing_key: None,
749 },
750 NotificationConfig {
751 id: "test-notification-2".to_string(),
752 r#type: NotificationType::Webhook,
753 url: "https://hooks.slack.com/test2".to_string(),
754 signing_key: None,
755 },
756 ];
757
758 let config = Config {
760 signers: vec![],
761 relayers: vec![],
762 notifications,
763 networks: NetworksFileConfig::new(vec![]).unwrap(),
764 plugins: Some(vec![]),
765 };
766
767 let app_state = ThinData(create_test_app_state());
769
770 process_notifications(&config, &app_state).await?;
772
773 let stored_notifications = app_state.notification_repository.list_all().await?;
775 assert_eq!(stored_notifications.len(), 2);
776 assert!(stored_notifications
777 .iter()
778 .any(|n| n.id == "test-notification-1"));
779 assert!(stored_notifications
780 .iter()
781 .any(|n| n.id == "test-notification-2"));
782
783 Ok(())
784 }
785
786 #[tokio::test]
787 async fn test_process_networks_empty() -> Result<()> {
788 let config = Config {
789 signers: vec![],
790 relayers: vec![],
791 notifications: vec![],
792 networks: NetworksFileConfig::new(vec![]).unwrap(),
793 plugins: Some(vec![]),
794 };
795
796 let app_state = ThinData(create_test_app_state());
797
798 process_networks(&config, &app_state).await?;
799
800 let stored_networks = app_state.network_repository.list_all().await?;
801 assert_eq!(stored_networks.len(), 0);
802
803 Ok(())
804 }
805
806 #[tokio::test]
807 async fn test_process_networks_single_evm() -> Result<()> {
808 use crate::config::network::test_utils::*;
809
810 let networks = vec![create_evm_network_wrapped("mainnet")];
811
812 let config = Config {
813 signers: vec![],
814 relayers: vec![],
815 notifications: vec![],
816 networks: NetworksFileConfig::new(networks).unwrap(),
817 plugins: Some(vec![]),
818 };
819
820 let app_state = ThinData(create_test_app_state());
821
822 process_networks(&config, &app_state).await?;
823
824 let stored_networks = app_state.network_repository.list_all().await?;
825 assert_eq!(stored_networks.len(), 1);
826 assert_eq!(stored_networks[0].name, "mainnet");
827 assert_eq!(stored_networks[0].network_type, NetworkType::Evm);
828
829 Ok(())
830 }
831
832 #[tokio::test]
833 async fn test_process_networks_single_solana() -> Result<()> {
834 use crate::config::network::test_utils::*;
835
836 let networks = vec![create_solana_network_wrapped("devnet")];
837
838 let config = Config {
839 signers: vec![],
840 relayers: vec![],
841 notifications: vec![],
842 networks: NetworksFileConfig::new(networks).unwrap(),
843 plugins: Some(vec![]),
844 };
845
846 let app_state = ThinData(create_test_app_state());
847
848 process_networks(&config, &app_state).await?;
849
850 let stored_networks = app_state.network_repository.list_all().await?;
851 assert_eq!(stored_networks.len(), 1);
852 assert_eq!(stored_networks[0].name, "devnet");
853 assert_eq!(stored_networks[0].network_type, NetworkType::Solana);
854
855 Ok(())
856 }
857
858 #[tokio::test]
859 async fn test_process_networks_multiple_mixed() -> Result<()> {
860 use crate::config::network::test_utils::*;
861
862 let networks = vec![
863 create_evm_network_wrapped("mainnet"),
864 create_solana_network_wrapped("devnet"),
865 create_evm_network_wrapped("sepolia"),
866 create_solana_network_wrapped("testnet"),
867 ];
868
869 let config = Config {
870 signers: vec![],
871 relayers: vec![],
872 notifications: vec![],
873 networks: NetworksFileConfig::new(networks).unwrap(),
874 plugins: Some(vec![]),
875 };
876
877 let app_state = ThinData(create_test_app_state());
878
879 process_networks(&config, &app_state).await?;
880
881 let stored_networks = app_state.network_repository.list_all().await?;
882 assert_eq!(stored_networks.len(), 4);
883
884 let evm_networks: Vec<_> = stored_networks
885 .iter()
886 .filter(|n| n.network_type == NetworkType::Evm)
887 .collect();
888 assert_eq!(evm_networks.len(), 2);
889 assert!(evm_networks.iter().any(|n| n.name == "mainnet"));
890 assert!(evm_networks.iter().any(|n| n.name == "sepolia"));
891
892 let solana_networks: Vec<_> = stored_networks
893 .iter()
894 .filter(|n| n.network_type == NetworkType::Solana)
895 .collect();
896 assert_eq!(solana_networks.len(), 2);
897 assert!(solana_networks.iter().any(|n| n.name == "devnet"));
898 assert!(solana_networks.iter().any(|n| n.name == "testnet"));
899
900 Ok(())
901 }
902
903 #[tokio::test]
904 async fn test_process_networks_many_networks() -> Result<()> {
905 use crate::config::network::test_utils::*;
906
907 let networks = (0..10)
908 .map(|i| create_evm_network_wrapped(&format!("network-{}", i)))
909 .collect();
910
911 let config = Config {
912 signers: vec![],
913 relayers: vec![],
914 notifications: vec![],
915 networks: NetworksFileConfig::new(networks).unwrap(),
916 plugins: Some(vec![]),
917 };
918
919 let app_state = ThinData(create_test_app_state());
920
921 process_networks(&config, &app_state).await?;
922
923 let stored_networks = app_state.network_repository.list_all().await?;
924 assert_eq!(stored_networks.len(), 10);
925
926 for i in 0..10 {
927 let expected_name = format!("network-{}", i);
928 assert!(
929 stored_networks.iter().any(|n| n.name == expected_name),
930 "Network {} not found",
931 expected_name
932 );
933 }
934
935 Ok(())
936 }
937
938 #[tokio::test]
939 async fn test_process_networks_duplicate_names() -> Result<()> {
940 use crate::config::network::test_utils::*;
941
942 let networks = vec![
943 create_evm_network_wrapped("mainnet"),
944 create_solana_network_wrapped("mainnet"),
945 ];
946
947 let config = Config {
948 signers: vec![],
949 relayers: vec![],
950 notifications: vec![],
951 networks: NetworksFileConfig::new(networks).unwrap(),
952 plugins: Some(vec![]),
953 };
954
955 let app_state = ThinData(create_test_app_state());
956
957 process_networks(&config, &app_state).await?;
958
959 let stored_networks = app_state.network_repository.list_all().await?;
960 assert_eq!(stored_networks.len(), 2);
961
962 let mainnet_networks: Vec<_> = stored_networks
963 .iter()
964 .filter(|n| n.name == "mainnet")
965 .collect();
966 assert_eq!(mainnet_networks.len(), 2);
967 assert!(mainnet_networks
968 .iter()
969 .any(|n| n.network_type == NetworkType::Evm));
970 assert!(mainnet_networks
971 .iter()
972 .any(|n| n.network_type == NetworkType::Solana));
973
974 Ok(())
975 }
976
977 #[tokio::test]
978 async fn test_process_networks() -> Result<()> {
979 use crate::config::network::test_utils::*;
980
981 let networks = vec![
982 create_evm_network_wrapped("mainnet"),
983 create_solana_network_wrapped("devnet"),
984 ];
985
986 let config = Config {
987 signers: vec![],
988 relayers: vec![],
989 notifications: vec![],
990 networks: NetworksFileConfig::new(networks).unwrap(),
991 plugins: Some(vec![]),
992 };
993
994 let app_state = ThinData(create_test_app_state());
995
996 process_networks(&config, &app_state).await?;
997
998 let stored_networks = app_state.network_repository.list_all().await?;
999 assert_eq!(stored_networks.len(), 2);
1000 assert!(stored_networks
1001 .iter()
1002 .any(|n| n.name == "mainnet" && n.network_type == NetworkType::Evm));
1003 assert!(stored_networks
1004 .iter()
1005 .any(|n| n.name == "devnet" && n.network_type == NetworkType::Solana));
1006
1007 Ok(())
1008 }
1009
1010 #[tokio::test]
1011 async fn test_process_relayers() -> Result<()> {
1012 let signers = vec![SignerFileConfig {
1014 id: "test-signer-1".to_string(),
1015 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
1016 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
1017 passphrase: PlainOrEnvValue::Plain {
1018 value: SecretString::new("test"),
1019 },
1020 }),
1021 }];
1022
1023 let relayers = vec![RelayerFileConfig {
1025 id: "test-relayer-1".to_string(),
1026 network_type: ConfigFileNetworkType::Evm,
1027 signer_id: "test-signer-1".to_string(),
1028 name: "test-relayer-1".to_string(),
1029 network: "test-network".to_string(),
1030 paused: false,
1031 policies: None,
1032 notification_id: None,
1033 custom_rpc_urls: None,
1034 }];
1035
1036 let config = Config {
1038 signers: signers.clone(),
1039 relayers,
1040 notifications: vec![],
1041 networks: NetworksFileConfig::new(vec![]).unwrap(),
1042 plugins: Some(vec![]),
1043 };
1044
1045 let app_state = ThinData(create_test_app_state());
1047
1048 process_signers(&config, &app_state).await?;
1050
1051 process_relayers(&config, &app_state).await?;
1053
1054 let stored_relayers = app_state.relayer_repository.list_all().await?;
1056 assert_eq!(stored_relayers.len(), 1);
1057 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1058 assert_eq!(stored_relayers[0].signer_id, "test-signer-1");
1059 assert!(!stored_relayers[0].address.is_empty()); Ok(())
1062 }
1063
1064 #[tokio::test]
1065 async fn test_process_plugins() -> Result<()> {
1066 let plugins = vec![
1068 PluginFileConfig {
1069 id: "test-plugin-1".to_string(),
1070 path: "/app/plugins/test.ts".to_string(),
1071 timeout: None,
1072 emit_logs: false,
1073 emit_traces: false,
1074 },
1075 PluginFileConfig {
1076 id: "test-plugin-2".to_string(),
1077 path: "/app/plugins/test2.ts".to_string(),
1078 timeout: Some(12),
1079 emit_logs: false,
1080 emit_traces: false,
1081 },
1082 ];
1083
1084 let config = Config {
1086 signers: vec![],
1087 relayers: vec![],
1088 notifications: vec![],
1089 networks: NetworksFileConfig::new(vec![]).unwrap(),
1090 plugins: Some(plugins),
1091 };
1092
1093 let app_state = ThinData(create_test_app_state());
1095
1096 process_plugins(&config, &app_state).await?;
1098
1099 let plugin_1 = app_state
1101 .plugin_repository
1102 .get_by_id("test-plugin-1")
1103 .await?;
1104 let plugin_2 = app_state
1105 .plugin_repository
1106 .get_by_id("test-plugin-2")
1107 .await?;
1108
1109 assert!(plugin_1.is_some());
1110 assert!(plugin_2.is_some());
1111
1112 let plugin_1 = plugin_1.unwrap();
1113 let plugin_2 = plugin_2.unwrap();
1114
1115 assert_eq!(plugin_1.path, "/app/plugins/test.ts");
1116 assert_eq!(plugin_2.path, "/app/plugins/test2.ts");
1117
1118 assert_eq!(
1120 plugin_1.timeout.as_secs(),
1121 Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS).as_secs()
1122 );
1123 assert_eq!(
1124 plugin_2.timeout.as_secs(),
1125 Duration::from_secs(12).as_secs()
1126 );
1127
1128 Ok(())
1129 }
1130
1131 #[tokio::test]
1132 async fn test_process_api_key() -> Result<()> {
1133 let server_config = Arc::new(crate::utils::mocks::mockutils::create_test_server_config(
1134 RepositoryStorageType::InMemory,
1135 ));
1136 let app_state = ThinData(create_test_app_state());
1137
1138 process_api_key(&server_config, &app_state).await?;
1139
1140 let pagination_query = PaginationQuery {
1141 page: 1,
1142 per_page: 10,
1143 };
1144
1145 let stored_api_keys = app_state
1146 .api_key_repository
1147 .list_paginated(pagination_query)
1148 .await?;
1149 assert_eq!(stored_api_keys.items.len(), 1);
1150 assert_eq!(stored_api_keys.items[0].name, "default");
1151
1152 Ok(())
1153 }
1154
1155 #[tokio::test]
1156 async fn test_process_config_file() -> Result<()> {
1157 let signers = vec![SignerFileConfig {
1159 id: "test-signer-1".to_string(),
1160 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
1161 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
1162 passphrase: PlainOrEnvValue::Plain {
1163 value: SecretString::new("test"),
1164 },
1165 }),
1166 }];
1167
1168 let relayers = vec![RelayerFileConfig {
1169 id: "test-relayer-1".to_string(),
1170 network_type: ConfigFileNetworkType::Evm,
1171 signer_id: "test-signer-1".to_string(),
1172 name: "test-relayer-1".to_string(),
1173 network: "test-network".to_string(),
1174 paused: false,
1175 policies: None,
1176 notification_id: None,
1177 custom_rpc_urls: None,
1178 }];
1179
1180 let notifications = vec![NotificationConfig {
1181 id: "test-notification-1".to_string(),
1182 r#type: NotificationType::Webhook,
1183 url: "https://hooks.slack.com/test1".to_string(),
1184 signing_key: None,
1185 }];
1186
1187 let plugins = vec![PluginFileConfig {
1188 id: "test-plugin-1".to_string(),
1189 path: "/app/plugins/test.ts".to_string(),
1190 timeout: None,
1191 emit_logs: false,
1192 emit_traces: false,
1193 }];
1194
1195 let config = Config {
1197 signers,
1198 relayers,
1199 notifications,
1200 networks: NetworksFileConfig::new(vec![]).unwrap(),
1201 plugins: Some(plugins),
1202 };
1203
1204 let signer_repo = Arc::new(InMemorySignerRepository::default());
1206 let relayer_repo = Arc::new(RelayerRepositoryStorage::new_in_memory());
1207 let notification_repo = Arc::new(InMemoryNotificationRepository::default());
1208 let network_repo = Arc::new(InMemoryNetworkRepository::default());
1209 let transaction_repo = Arc::new(TransactionRepositoryStorage::InMemory(
1210 InMemoryTransactionRepository::new(),
1211 ));
1212 let transaction_counter = Arc::new(InMemoryTransactionCounter::default());
1213 let plugin_repo = Arc::new(InMemoryPluginRepository::default());
1214 let api_key_repo = Arc::new(InMemoryApiKeyRepository::default());
1215
1216 let mut mock_job_producer = MockJobProducerTrait::new();
1218 mock_job_producer
1219 .expect_produce_transaction_request_job()
1220 .returning(|_, _| Box::pin(async { Ok(()) }));
1221 mock_job_producer
1222 .expect_produce_submit_transaction_job()
1223 .returning(|_, _| Box::pin(async { Ok(()) }));
1224 mock_job_producer
1225 .expect_produce_check_transaction_status_job()
1226 .returning(|_, _| Box::pin(async { Ok(()) }));
1227 mock_job_producer
1228 .expect_produce_send_notification_job()
1229 .returning(|_, _| Box::pin(async { Ok(()) }));
1230 let job_producer = Arc::new(mock_job_producer);
1231
1232 let app_state = ThinData(AppState {
1234 signer_repository: signer_repo.clone(),
1235 relayer_repository: relayer_repo.clone(),
1236 notification_repository: notification_repo.clone(),
1237 network_repository: network_repo.clone(),
1238 transaction_repository: transaction_repo.clone(),
1239 transaction_counter_store: transaction_counter.clone(),
1240 job_producer: job_producer.clone(),
1241 plugin_repository: plugin_repo.clone(),
1242 api_key_repository: api_key_repo.clone(),
1243 });
1244
1245 let server_config = Arc::new(crate::utils::mocks::mockutils::create_test_server_config(
1247 RepositoryStorageType::InMemory,
1248 ));
1249 process_config_file(config, server_config, &app_state).await?;
1250
1251 let stored_signers = signer_repo.list_all().await?;
1253 assert_eq!(stored_signers.len(), 1);
1254 assert_eq!(stored_signers[0].id, "test-signer-1");
1255
1256 let stored_relayers = relayer_repo.list_all().await?;
1257 assert_eq!(stored_relayers.len(), 1);
1258 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1259 assert_eq!(stored_relayers[0].signer_id, "test-signer-1");
1260
1261 let stored_notifications = notification_repo.list_all().await?;
1262 assert_eq!(stored_notifications.len(), 1);
1263 assert_eq!(stored_notifications[0].id, "test-notification-1");
1264
1265 let stored_plugin = plugin_repo.get_by_id("test-plugin-1").await?;
1266 assert_eq!(stored_plugin.unwrap().path, "/app/plugins/test.ts");
1267
1268 Ok(())
1269 }
1270
1271 #[tokio::test]
1272 async fn test_process_signer_google_cloud_kms() {
1273 use crate::models::SecretString;
1274
1275 let signer = SignerFileConfig {
1276 id: "gcp-kms-signer".to_string(),
1277 config: SignerFileConfigEnum::GoogleCloudKms(GoogleCloudKmsSignerFileConfig {
1278 service_account: GoogleCloudKmsServiceAccountFileConfig {
1279 private_key: PlainOrEnvValue::Plain {
1280 value: SecretString::new("-----BEGIN EXAMPLE PRIVATE KEY-----\nFAKEKEYDATA\n-----END EXAMPLE PRIVATE KEY-----\n"),
1281 },
1282 client_email: PlainOrEnvValue::Plain {
1283 value: SecretString::new("test-service-account@example.com"),
1284 },
1285 private_key_id: PlainOrEnvValue::Plain {
1286 value: SecretString::new("fake-private-key-id"),
1287 },
1288 client_id: "fake-client-id".to_string(),
1289 project_id: "fake-project-id".to_string(),
1290 auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),
1291 token_uri: "https://oauth2.googleapis.com/token".to_string(),
1292 client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test-service-account%40example.com".to_string(),
1293 auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs".to_string(),
1294 universe_domain: "googleapis.com".to_string(),
1295 },
1296 key: GoogleCloudKmsKeyFileConfig {
1297 location: "global".to_string(),
1298 key_id: "fake-key-id".to_string(),
1299 key_ring_id: "fake-key-ring-id".to_string(),
1300 key_version: 1,
1301 },
1302 }),
1303 };
1304
1305 let result = process_signer(&signer).await;
1306
1307 assert!(
1308 result.is_ok(),
1309 "Failed to process Google Cloud KMS signer: {:?}",
1310 result.err()
1311 );
1312 let model = result.unwrap();
1313
1314 assert_eq!(model.id, "gcp-kms-signer");
1315 }
1316
1317 #[tokio::test]
1318 async fn test_is_redis_populated_empty_repositories() -> Result<()> {
1319 let app_state = ThinData(create_test_app_state());
1321
1322 assert!(!app_state.relayer_repository.has_entries().await?);
1324 assert!(!app_state.transaction_repository.has_entries().await?);
1325 assert!(!app_state.signer_repository.has_entries().await?);
1326 assert!(!app_state.notification_repository.has_entries().await?);
1327 assert!(!app_state.network_repository.has_entries().await?);
1328
1329 let result = is_redis_populated(&app_state).await?;
1331 assert!(!result, "Expected false when all repositories are empty");
1332
1333 Ok(())
1334 }
1335
1336 #[tokio::test]
1337 async fn test_is_redis_populated_relayer_repository_has_entries() -> Result<()> {
1338 let app_state = ThinData(create_test_app_state());
1339
1340 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1342 app_state.relayer_repository.create(relayer).await?;
1343
1344 assert!(app_state.relayer_repository.has_entries().await?);
1346
1347 let result = is_redis_populated(&app_state).await?;
1349 assert!(result, "Expected true when relayer repository has entries");
1350
1351 Ok(())
1352 }
1353
1354 #[tokio::test]
1355 async fn test_is_redis_populated_transaction_repository_has_entries() -> Result<()> {
1356 let app_state = ThinData(create_test_app_state());
1357
1358 let transaction = TransactionRepoModel::default();
1360 app_state.transaction_repository.create(transaction).await?;
1361
1362 assert!(app_state.transaction_repository.has_entries().await?);
1364
1365 let result = is_redis_populated(&app_state).await?;
1367 assert!(
1368 result,
1369 "Expected true when transaction repository has entries"
1370 );
1371
1372 Ok(())
1373 }
1374
1375 #[tokio::test]
1376 async fn test_is_redis_populated_signer_repository_has_entries() -> Result<()> {
1377 let app_state = ThinData(create_test_app_state());
1378
1379 let signer = create_mock_signer();
1381 app_state.signer_repository.create(signer).await?;
1382
1383 assert!(app_state.signer_repository.has_entries().await?);
1385
1386 let result = is_redis_populated(&app_state).await?;
1388 assert!(result, "Expected true when signer repository has entries");
1389
1390 Ok(())
1391 }
1392
1393 #[tokio::test]
1394 async fn test_is_redis_populated_notification_repository_has_entries() -> Result<()> {
1395 let app_state = ThinData(create_test_app_state());
1396
1397 let notification = create_mock_notification("test-notification".to_string());
1399 app_state
1400 .notification_repository
1401 .create(notification)
1402 .await?;
1403
1404 assert!(app_state.notification_repository.has_entries().await?);
1406
1407 let result = is_redis_populated(&app_state).await?;
1409 assert!(
1410 result,
1411 "Expected true when notification repository has entries"
1412 );
1413
1414 Ok(())
1415 }
1416
1417 #[tokio::test]
1418 async fn test_is_redis_populated_network_repository_has_entries() -> Result<()> {
1419 let app_state = ThinData(create_test_app_state());
1420
1421 let network = create_mock_network();
1423 app_state.network_repository.create(network).await?;
1424
1425 assert!(app_state.network_repository.has_entries().await?);
1427
1428 let result = is_redis_populated(&app_state).await?;
1430 assert!(result, "Expected true when network repository has entries");
1431
1432 Ok(())
1433 }
1434
1435 #[tokio::test]
1436 async fn test_is_redis_populated_multiple_repositories_have_entries() -> Result<()> {
1437 let app_state = ThinData(create_test_app_state());
1438
1439 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1441 let signer = create_mock_signer();
1442 let notification = create_mock_notification("test-notification".to_string());
1443 let network = create_mock_network();
1444
1445 app_state.relayer_repository.create(relayer).await?;
1446 app_state.signer_repository.create(signer).await?;
1447 app_state
1448 .notification_repository
1449 .create(notification)
1450 .await?;
1451 app_state.network_repository.create(network).await?;
1452
1453 assert!(app_state.relayer_repository.has_entries().await?);
1455 assert!(app_state.signer_repository.has_entries().await?);
1456 assert!(app_state.notification_repository.has_entries().await?);
1457 assert!(app_state.network_repository.has_entries().await?);
1458
1459 let result = is_redis_populated(&app_state).await?;
1461 assert!(
1462 result,
1463 "Expected true when multiple repositories have entries"
1464 );
1465
1466 Ok(())
1467 }
1468
1469 #[tokio::test]
1470 async fn test_is_redis_populated_comprehensive_scenario() -> Result<()> {
1471 let app_state = ThinData(create_test_app_state());
1472
1473 let result = is_redis_populated(&app_state).await?;
1475 assert!(!result, "Expected false when all repositories are empty");
1476
1477 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1479 app_state.relayer_repository.create(relayer).await?;
1480 let result = is_redis_populated(&app_state).await?;
1481 assert!(result, "Expected true after adding one entry");
1482
1483 app_state.relayer_repository.drop_all_entries().await?;
1485 let result = is_redis_populated(&app_state).await?;
1486 assert!(!result, "Expected false after clearing all repositories");
1487
1488 let signer = create_mock_signer();
1490 app_state.signer_repository.create(signer).await?;
1491 let result = is_redis_populated(&app_state).await?;
1492 assert!(result, "Expected true after adding signer");
1493
1494 let notification = create_mock_notification("test-notification".to_string());
1495 app_state
1496 .notification_repository
1497 .create(notification)
1498 .await?;
1499 let result = is_redis_populated(&app_state).await?;
1500 assert!(result, "Expected true after adding notification");
1501
1502 Ok(())
1503 }
1504
1505 fn create_test_server_config_with_settings(
1507 storage_type: RepositoryStorageType,
1508 reset_storage_on_start: bool,
1509 ) -> ServerConfig {
1510 ServerConfig {
1511 repository_storage_type: storage_type.clone(),
1512 reset_storage_on_start,
1513 ..create_test_server_config(storage_type)
1514 }
1515 }
1516
1517 fn create_minimal_test_config() -> Config {
1519 Config {
1520 signers: vec![SignerFileConfig {
1521 id: "test-signer-1".to_string(),
1522 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
1523 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
1524 passphrase: PlainOrEnvValue::Plain {
1525 value: SecretString::new("test"),
1526 },
1527 }),
1528 }],
1529 relayers: vec![RelayerFileConfig {
1530 id: "test-relayer-1".to_string(),
1531 network_type: ConfigFileNetworkType::Evm,
1532 signer_id: "test-signer-1".to_string(),
1533 name: "test-relayer-1".to_string(),
1534 network: "test-network".to_string(),
1535 paused: false,
1536 policies: None,
1537 notification_id: None,
1538 custom_rpc_urls: None,
1539 }],
1540 notifications: vec![NotificationConfig {
1541 id: "test-notification-1".to_string(),
1542 r#type: NotificationType::Webhook,
1543 url: "https://hooks.slack.com/test1".to_string(),
1544 signing_key: None,
1545 }],
1546 networks: NetworksFileConfig::new(vec![]).unwrap(),
1547 plugins: None,
1548 }
1549 }
1550
1551 #[tokio::test]
1552 async fn test_should_process_config_file_inmemory_storage() -> Result<()> {
1553 let config = create_minimal_test_config();
1554
1555 let server_config = Arc::new(create_test_server_config_with_settings(
1557 RepositoryStorageType::InMemory,
1558 false,
1559 ));
1560
1561 let app_state = ThinData(create_test_app_state());
1562 process_config_file(config.clone(), server_config.clone(), &app_state).await?;
1563
1564 let stored_relayers = app_state.relayer_repository.list_all().await?;
1565 assert_eq!(stored_relayers.len(), 1);
1566 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1567
1568 let server_config2 = Arc::new(create_test_server_config_with_settings(
1570 RepositoryStorageType::InMemory,
1571 true,
1572 ));
1573
1574 let app_state2 = ThinData(create_test_app_state());
1575 process_config_file(config.clone(), server_config2, &app_state2).await?;
1576
1577 let stored_relayers = app_state2.relayer_repository.list_all().await?;
1578 assert_eq!(stored_relayers.len(), 1);
1579 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1580
1581 Ok(())
1582 }
1583
1584 #[tokio::test]
1585 async fn test_should_process_config_file_redis_storage_empty_repositories() -> Result<()> {
1586 let config = create_minimal_test_config();
1587 let server_config = Arc::new(create_test_server_config_with_settings(
1588 RepositoryStorageType::Redis,
1589 false,
1590 ));
1591
1592 let app_state = ThinData(create_test_app_state());
1593 process_config_file(config, server_config, &app_state).await?;
1594
1595 let stored_relayers = app_state.relayer_repository.list_all().await?;
1596 assert_eq!(stored_relayers.len(), 1);
1597 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1598
1599 Ok(())
1600 }
1601
1602 #[tokio::test]
1603 async fn test_should_not_process_config_file_redis_storage_populated_repositories() -> Result<()>
1604 {
1605 let config = create_minimal_test_config();
1606 let server_config = Arc::new(create_test_server_config_with_settings(
1607 RepositoryStorageType::Redis,
1608 false,
1609 ));
1610
1611 let app_state1 = ThinData(create_test_app_state());
1613 let app_state2 = ThinData(create_test_app_state());
1614
1615 let existing_relayer1 = create_mock_relayer("existing-relayer".to_string(), false);
1617 let existing_relayer2 = create_mock_relayer("existing-relayer".to_string(), false);
1618 app_state1
1619 .relayer_repository
1620 .create(existing_relayer1)
1621 .await?;
1622 app_state2
1623 .relayer_repository
1624 .create(existing_relayer2)
1625 .await?;
1626
1627 assert!(app_state1.relayer_repository.has_entries().await?);
1629 assert!(!app_state1.signer_repository.has_entries().await?);
1630
1631 process_config_file(config, server_config, &app_state2).await?;
1633
1634 let relayer_from_config = app_state2
1635 .relayer_repository
1636 .get_by_id("test-relayer-1".to_string())
1637 .await;
1638 assert!(
1639 relayer_from_config.is_err(),
1640 "Relayer from config should not be found"
1641 );
1642
1643 let existing_relayer = app_state2
1644 .relayer_repository
1645 .get_by_id("existing-relayer".to_string())
1646 .await?;
1647 assert_eq!(existing_relayer.id, "existing-relayer");
1648
1649 Ok(())
1651 }
1652
1653 #[tokio::test]
1654 async fn test_should_process_config_file_redis_storage_with_reset_flag() -> Result<()> {
1655 let config = create_minimal_test_config();
1656 let server_config = Arc::new(create_test_server_config_with_settings(
1657 RepositoryStorageType::Redis,
1658 true, ));
1660
1661 let app_state = ThinData(create_test_app_state());
1662
1663 let existing_relayer = create_mock_relayer("existing-relayer".to_string(), false);
1665 let existing_signer = create_mock_signer();
1666 app_state
1667 .relayer_repository
1668 .create(existing_relayer)
1669 .await?;
1670 app_state.signer_repository.create(existing_signer).await?;
1671
1672 process_config_file(config, server_config, &app_state).await?;
1674
1675 let stored_relayer = app_state
1676 .relayer_repository
1677 .get_by_id("existing-relayer".to_string())
1678 .await;
1679 assert!(
1680 stored_relayer.is_err(),
1681 "Existing relayer should not be found"
1682 );
1683
1684 let stored_signer = app_state
1685 .signer_repository
1686 .get_by_id("existing-signer".to_string())
1687 .await;
1688 assert!(
1689 stored_signer.is_err(),
1690 "Existing signer should not be found"
1691 );
1692
1693 Ok(())
1694 }
1695}