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>(
338 config_file: Config,
339 server_config: Arc<ServerConfig>,
340 app_state: &ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
341) -> Result<()>
342where
343 J: JobProducerTrait + Send + Sync + 'static,
344 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
345 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
346 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
347 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
348 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
349 TCR: TransactionCounterTrait + Send + Sync + 'static,
350 PR: PluginRepositoryTrait + Send + Sync + 'static,
351 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
352{
353 let should_process_config_file = match server_config.repository_storage_type {
354 RepositoryStorageType::InMemory => true,
355 RepositoryStorageType::Redis => {
356 server_config.reset_storage_on_start || !is_redis_populated(app_state).await?
357 }
358 };
359
360 if !should_process_config_file {
361 info!("Skipping config file processing");
362 return Ok(());
363 }
364
365 if server_config.reset_storage_on_start {
366 info!("Resetting storage on start due to server config flag RESET_STORAGE_ON_START = true");
367 app_state.relayer_repository.drop_all_entries().await?;
368 app_state.transaction_repository.drop_all_entries().await?;
369 app_state.signer_repository.drop_all_entries().await?;
370 app_state.notification_repository.drop_all_entries().await?;
371 app_state.network_repository.drop_all_entries().await?;
372 app_state.plugin_repository.drop_all_entries().await?;
373 app_state.api_key_repository.drop_all_entries().await?;
374 }
375
376 if should_process_config_file {
377 info!("Processing config file");
378 process_plugins(&config_file, app_state).await?;
379 process_signers(&config_file, app_state).await?;
380 process_notifications(&config_file, app_state).await?;
381 process_networks(&config_file, app_state).await?;
382 process_relayers(&config_file, app_state).await?;
383 process_api_key(&server_config, app_state).await?;
384 }
385 Ok(())
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::{
392 config::{ConfigFileNetworkType, NetworksFileConfig, PluginFileConfig},
393 constants::DEFAULT_PLUGIN_TIMEOUT_SECONDS,
394 jobs::MockJobProducerTrait,
395 models::{
396 relayer::RelayerFileConfig, AppState, AwsKmsSignerFileConfig,
397 GoogleCloudKmsKeyFileConfig, GoogleCloudKmsServiceAccountFileConfig,
398 GoogleCloudKmsSignerFileConfig, LocalSignerFileConfig, NetworkType, NotificationConfig,
399 NotificationType, PaginationQuery, PlainOrEnvValue, SecretString, SignerConfigStorage,
400 SignerFileConfig, SignerFileConfigEnum, VaultSignerFileConfig,
401 VaultTransitSignerFileConfig,
402 },
403 repositories::{
404 ApiKeyRepositoryStorage, InMemoryApiKeyRepository, InMemoryNetworkRepository,
405 InMemoryNotificationRepository, InMemoryPluginRepository, InMemorySignerRepository,
406 InMemoryTransactionCounter, InMemoryTransactionRepository, NetworkRepositoryStorage,
407 NotificationRepositoryStorage, PluginRepositoryStorage, RelayerRepositoryStorage,
408 SignerRepositoryStorage, TransactionCounterRepositoryStorage,
409 TransactionRepositoryStorage,
410 },
411 utils::mocks::mockutils::{
412 create_mock_network, create_mock_notification, create_mock_relayer, create_mock_signer,
413 create_test_server_config,
414 },
415 };
416 use actix_web::web::ThinData;
417 use mockito;
418 use serde_json::json;
419 use std::{sync::Arc, time::Duration};
420
421 fn create_test_app_state() -> AppState<
422 MockJobProducerTrait,
423 RelayerRepositoryStorage,
424 TransactionRepositoryStorage,
425 NetworkRepositoryStorage,
426 NotificationRepositoryStorage,
427 SignerRepositoryStorage,
428 TransactionCounterRepositoryStorage,
429 PluginRepositoryStorage,
430 ApiKeyRepositoryStorage,
431 > {
432 let mut mock_job_producer = MockJobProducerTrait::new();
434
435 mock_job_producer
437 .expect_produce_transaction_request_job()
438 .returning(|_, _| Box::pin(async { Ok(()) }));
439
440 mock_job_producer
441 .expect_produce_submit_transaction_job()
442 .returning(|_, _| Box::pin(async { Ok(()) }));
443
444 mock_job_producer
445 .expect_produce_check_transaction_status_job()
446 .returning(|_, _| Box::pin(async { Ok(()) }));
447
448 mock_job_producer
449 .expect_produce_send_notification_job()
450 .returning(|_, _| Box::pin(async { Ok(()) }));
451
452 AppState {
453 relayer_repository: Arc::new(RelayerRepositoryStorage::new_in_memory()),
454 transaction_repository: Arc::new(TransactionRepositoryStorage::new_in_memory()),
455 signer_repository: Arc::new(SignerRepositoryStorage::new_in_memory()),
456 notification_repository: Arc::new(NotificationRepositoryStorage::new_in_memory()),
457 network_repository: Arc::new(NetworkRepositoryStorage::new_in_memory()),
458 transaction_counter_store: Arc::new(
459 TransactionCounterRepositoryStorage::new_in_memory(),
460 ),
461 job_producer: Arc::new(mock_job_producer),
462 plugin_repository: Arc::new(PluginRepositoryStorage::new_in_memory()),
463 api_key_repository: Arc::new(ApiKeyRepositoryStorage::new_in_memory()),
464 }
465 }
466
467 #[tokio::test]
468 async fn test_process_signer_test() {
469 let signer = SignerFileConfig {
470 id: "test-signer".to_string(),
471 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
472 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
473 passphrase: PlainOrEnvValue::Plain {
474 value: SecretString::new("test"),
475 },
476 }),
477 };
478
479 let result = process_signer(&signer).await;
480
481 assert!(
482 result.is_ok(),
483 "Failed to process test signer: {:?}",
484 result.err()
485 );
486 let model = result.unwrap();
487
488 assert_eq!(model.id, "test-signer");
489
490 match model.config {
491 SignerConfigStorage::Local(config) => {
492 assert!(!config.raw_key.is_empty());
493 assert_eq!(config.raw_key.len(), 32);
494 }
495 _ => panic!("Expected Local config"),
496 }
497 }
498
499 #[tokio::test]
500 async fn test_process_signer_vault_transit() -> Result<()> {
501 let signer = SignerFileConfig {
502 id: "vault-transit-signer".to_string(),
503 config: SignerFileConfigEnum::VaultTransit(VaultTransitSignerFileConfig {
504 key_name: "test-transit-key".to_string(),
505 address: "https://vault.example.com".to_string(),
506 namespace: Some("test-namespace".to_string()),
507 role_id: PlainOrEnvValue::Plain {
508 value: SecretString::new("test-role"),
509 },
510 secret_id: PlainOrEnvValue::Plain {
511 value: SecretString::new("test-secret"),
512 },
513 pubkey: "test-pubkey".to_string(),
514 mount_point: Some("transit".to_string()),
515 }),
516 };
517
518 let result = process_signer(&signer).await;
519
520 assert!(
521 result.is_ok(),
522 "Failed to process vault transit signer: {:?}",
523 result.err()
524 );
525 let model = result.unwrap();
526
527 assert_eq!(model.id, "vault-transit-signer");
528
529 match model.config {
530 SignerConfigStorage::VaultTransit(config) => {
531 assert_eq!(config.key_name, "test-transit-key");
532 assert_eq!(config.address, "https://vault.example.com");
533 assert_eq!(config.namespace, Some("test-namespace".to_string()));
534 assert_eq!(config.role_id.to_str().as_str(), "test-role");
535 assert_eq!(config.secret_id.to_str().as_str(), "test-secret");
536 assert_eq!(config.pubkey, "test-pubkey");
537 assert_eq!(config.mount_point, Some("transit".to_string()));
538 }
539 _ => panic!("Expected VaultTransit config"),
540 }
541
542 Ok(())
543 }
544
545 #[tokio::test]
546 async fn test_process_signer_aws_kms() -> Result<()> {
547 let signer = SignerFileConfig {
548 id: "aws-kms-signer".to_string(),
549 config: SignerFileConfigEnum::AwsKms(AwsKmsSignerFileConfig {
550 region: "us-east-1".to_string(),
551 key_id: "test-key-id".to_string(),
552 }),
553 };
554
555 let result = process_signer(&signer).await;
556
557 assert!(
558 result.is_ok(),
559 "Failed to process AWS KMS signer: {:?}",
560 result.err()
561 );
562 let model = result.unwrap();
563
564 assert_eq!(model.id, "aws-kms-signer");
565
566 match model.config {
567 SignerConfigStorage::AwsKms(_) => {}
568 _ => panic!("Expected AwsKms config"),
569 }
570
571 Ok(())
572 }
573
574 async fn setup_mock_approle_login(
576 mock_server: &mut mockito::ServerGuard,
577 role_id: &str,
578 secret_id: &str,
579 token: &str,
580 ) -> mockito::Mock {
581 mock_server
582 .mock("POST", "/v1/auth/approle/login")
583 .match_body(mockito::Matcher::Json(json!({
584 "role_id": role_id,
585 "secret_id": secret_id
586 })))
587 .with_status(200)
588 .with_header("content-type", "application/json")
589 .with_body(
590 serde_json::to_string(&json!({
591 "request_id": "test-request-id",
592 "lease_id": "",
593 "renewable": false,
594 "lease_duration": 0,
595 "data": null,
596 "wrap_info": null,
597 "warnings": null,
598 "auth": {
599 "client_token": token,
600 "accessor": "test-accessor",
601 "policies": ["default"],
602 "token_policies": ["default"],
603 "metadata": {
604 "role_name": "test-role"
605 },
606 "lease_duration": 3600,
607 "renewable": true,
608 "entity_id": "test-entity-id",
609 "token_type": "service",
610 "orphan": true
611 }
612 }))
613 .unwrap(),
614 )
615 .create_async()
616 .await
617 }
618
619 #[tokio::test]
620 async fn test_process_signer_vault() -> Result<()> {
621 let mut mock_server = mockito::Server::new_async().await;
622
623 let _login_mock = setup_mock_approle_login(
624 &mut mock_server,
625 "test-role-id",
626 "test-secret-id",
627 "test-token",
628 )
629 .await;
630
631 let _secret_mock = mock_server
632 .mock("GET", "/v1/secret/data/test-key")
633 .match_header("X-Vault-Token", "test-token")
634 .with_status(200)
635 .with_header("content-type", "application/json")
636 .with_body(serde_json::to_string(&json!({
637 "request_id": "test-request-id",
638 "lease_id": "",
639 "renewable": false,
640 "lease_duration": 0,
641 "data": {
642 "data": {
643 "value": "C5ACE14AB163556747F02C1110911537578FBE335FB74D18FBF82990AD70C3B9"
644 },
645 "metadata": {
646 "created_time": "2024-01-01T00:00:00Z",
647 "deletion_time": "",
648 "destroyed": false,
649 "version": 1
650 }
651 },
652 "wrap_info": null,
653 "warnings": null,
654 "auth": null
655 })).unwrap())
656 .create_async()
657 .await;
658
659 let signer = SignerFileConfig {
660 id: "vault-signer".to_string(),
661 config: SignerFileConfigEnum::Vault(VaultSignerFileConfig {
662 key_name: "test-key".to_string(),
663 address: mock_server.url(),
664 namespace: Some("test-namespace".to_string()),
665 role_id: PlainOrEnvValue::Plain {
666 value: SecretString::new("test-role-id"),
667 },
668 secret_id: PlainOrEnvValue::Plain {
669 value: SecretString::new("test-secret-id"),
670 },
671 mount_point: Some("secret".to_string()),
672 }),
673 };
674
675 let result = process_signer(&signer).await;
676
677 assert!(
678 result.is_ok(),
679 "Failed to process Vault signer: {:?}",
680 result.err()
681 );
682 let model = result.unwrap();
683
684 assert_eq!(model.id, "vault-signer");
685
686 match model.config {
687 SignerConfigStorage::Vault(_) => {}
688 _ => panic!("Expected Vault config"),
689 }
690
691 Ok(())
692 }
693
694 #[tokio::test]
695 async fn test_process_signers() -> Result<()> {
696 let signers = vec![
698 SignerFileConfig {
699 id: "test-signer-1".to_string(),
700 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
701 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
702 passphrase: PlainOrEnvValue::Plain {
703 value: SecretString::new("test"),
704 },
705 }),
706 },
707 SignerFileConfig {
708 id: "test-signer-2".to_string(),
709 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
710 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
711 passphrase: PlainOrEnvValue::Plain {
712 value: SecretString::new("test"),
713 },
714 }),
715 },
716 ];
717
718 let config = Config {
720 signers,
721 relayers: vec![],
722 notifications: vec![],
723 networks: NetworksFileConfig::new(vec![]).unwrap(),
724 plugins: Some(vec![]),
725 };
726
727 let app_state = ThinData(create_test_app_state());
729
730 process_signers(&config, &app_state).await?;
732
733 let stored_signers = app_state.signer_repository.list_all().await?;
735 assert_eq!(stored_signers.len(), 2);
736 assert!(stored_signers.iter().any(|s| s.id == "test-signer-1"));
737 assert!(stored_signers.iter().any(|s| s.id == "test-signer-2"));
738
739 Ok(())
740 }
741
742 #[tokio::test]
743 async fn test_process_notifications() -> Result<()> {
744 let notifications = vec![
746 NotificationConfig {
747 id: "test-notification-1".to_string(),
748 r#type: NotificationType::Webhook,
749 url: "https://hooks.slack.com/test1".to_string(),
750 signing_key: None,
751 },
752 NotificationConfig {
753 id: "test-notification-2".to_string(),
754 r#type: NotificationType::Webhook,
755 url: "https://hooks.slack.com/test2".to_string(),
756 signing_key: None,
757 },
758 ];
759
760 let config = Config {
762 signers: vec![],
763 relayers: vec![],
764 notifications,
765 networks: NetworksFileConfig::new(vec![]).unwrap(),
766 plugins: Some(vec![]),
767 };
768
769 let app_state = ThinData(create_test_app_state());
771
772 process_notifications(&config, &app_state).await?;
774
775 let stored_notifications = app_state.notification_repository.list_all().await?;
777 assert_eq!(stored_notifications.len(), 2);
778 assert!(stored_notifications
779 .iter()
780 .any(|n| n.id == "test-notification-1"));
781 assert!(stored_notifications
782 .iter()
783 .any(|n| n.id == "test-notification-2"));
784
785 Ok(())
786 }
787
788 #[tokio::test]
789 async fn test_process_networks_empty() -> Result<()> {
790 let config = Config {
791 signers: vec![],
792 relayers: vec![],
793 notifications: vec![],
794 networks: NetworksFileConfig::new(vec![]).unwrap(),
795 plugins: Some(vec![]),
796 };
797
798 let app_state = ThinData(create_test_app_state());
799
800 process_networks(&config, &app_state).await?;
801
802 let stored_networks = app_state.network_repository.list_all().await?;
803 assert_eq!(stored_networks.len(), 0);
804
805 Ok(())
806 }
807
808 #[tokio::test]
809 async fn test_process_networks_single_evm() -> Result<()> {
810 use crate::config::network::test_utils::*;
811
812 let networks = vec![create_evm_network_wrapped("mainnet")];
813
814 let config = Config {
815 signers: vec![],
816 relayers: vec![],
817 notifications: vec![],
818 networks: NetworksFileConfig::new(networks).unwrap(),
819 plugins: Some(vec![]),
820 };
821
822 let app_state = ThinData(create_test_app_state());
823
824 process_networks(&config, &app_state).await?;
825
826 let stored_networks = app_state.network_repository.list_all().await?;
827 assert_eq!(stored_networks.len(), 1);
828 assert_eq!(stored_networks[0].name, "mainnet");
829 assert_eq!(stored_networks[0].network_type, NetworkType::Evm);
830
831 Ok(())
832 }
833
834 #[tokio::test]
835 async fn test_process_networks_single_solana() -> Result<()> {
836 use crate::config::network::test_utils::*;
837
838 let networks = vec![create_solana_network_wrapped("devnet")];
839
840 let config = Config {
841 signers: vec![],
842 relayers: vec![],
843 notifications: vec![],
844 networks: NetworksFileConfig::new(networks).unwrap(),
845 plugins: Some(vec![]),
846 };
847
848 let app_state = ThinData(create_test_app_state());
849
850 process_networks(&config, &app_state).await?;
851
852 let stored_networks = app_state.network_repository.list_all().await?;
853 assert_eq!(stored_networks.len(), 1);
854 assert_eq!(stored_networks[0].name, "devnet");
855 assert_eq!(stored_networks[0].network_type, NetworkType::Solana);
856
857 Ok(())
858 }
859
860 #[tokio::test]
861 async fn test_process_networks_multiple_mixed() -> Result<()> {
862 use crate::config::network::test_utils::*;
863
864 let networks = vec![
865 create_evm_network_wrapped("mainnet"),
866 create_solana_network_wrapped("devnet"),
867 create_evm_network_wrapped("sepolia"),
868 create_solana_network_wrapped("testnet"),
869 ];
870
871 let config = Config {
872 signers: vec![],
873 relayers: vec![],
874 notifications: vec![],
875 networks: NetworksFileConfig::new(networks).unwrap(),
876 plugins: Some(vec![]),
877 };
878
879 let app_state = ThinData(create_test_app_state());
880
881 process_networks(&config, &app_state).await?;
882
883 let stored_networks = app_state.network_repository.list_all().await?;
884 assert_eq!(stored_networks.len(), 4);
885
886 let evm_networks: Vec<_> = stored_networks
887 .iter()
888 .filter(|n| n.network_type == NetworkType::Evm)
889 .collect();
890 assert_eq!(evm_networks.len(), 2);
891 assert!(evm_networks.iter().any(|n| n.name == "mainnet"));
892 assert!(evm_networks.iter().any(|n| n.name == "sepolia"));
893
894 let solana_networks: Vec<_> = stored_networks
895 .iter()
896 .filter(|n| n.network_type == NetworkType::Solana)
897 .collect();
898 assert_eq!(solana_networks.len(), 2);
899 assert!(solana_networks.iter().any(|n| n.name == "devnet"));
900 assert!(solana_networks.iter().any(|n| n.name == "testnet"));
901
902 Ok(())
903 }
904
905 #[tokio::test]
906 async fn test_process_networks_many_networks() -> Result<()> {
907 use crate::config::network::test_utils::*;
908
909 let networks = (0..10)
910 .map(|i| create_evm_network_wrapped(&format!("network-{i}")))
911 .collect();
912
913 let config = Config {
914 signers: vec![],
915 relayers: vec![],
916 notifications: vec![],
917 networks: NetworksFileConfig::new(networks).unwrap(),
918 plugins: Some(vec![]),
919 };
920
921 let app_state = ThinData(create_test_app_state());
922
923 process_networks(&config, &app_state).await?;
924
925 let stored_networks = app_state.network_repository.list_all().await?;
926 assert_eq!(stored_networks.len(), 10);
927
928 for i in 0..10 {
929 let expected_name = format!("network-{i}");
930 assert!(
931 stored_networks.iter().any(|n| n.name == expected_name),
932 "Network {expected_name} not found"
933 );
934 }
935
936 Ok(())
937 }
938
939 #[tokio::test]
940 async fn test_process_networks_duplicate_names() -> Result<()> {
941 use crate::config::network::test_utils::*;
942
943 let networks = vec![
944 create_evm_network_wrapped("mainnet"),
945 create_solana_network_wrapped("mainnet"),
946 ];
947
948 let config = Config {
949 signers: vec![],
950 relayers: vec![],
951 notifications: vec![],
952 networks: NetworksFileConfig::new(networks).unwrap(),
953 plugins: Some(vec![]),
954 };
955
956 let app_state = ThinData(create_test_app_state());
957
958 process_networks(&config, &app_state).await?;
959
960 let stored_networks = app_state.network_repository.list_all().await?;
961 assert_eq!(stored_networks.len(), 2);
962
963 let mainnet_networks: Vec<_> = stored_networks
964 .iter()
965 .filter(|n| n.name == "mainnet")
966 .collect();
967 assert_eq!(mainnet_networks.len(), 2);
968 assert!(mainnet_networks
969 .iter()
970 .any(|n| n.network_type == NetworkType::Evm));
971 assert!(mainnet_networks
972 .iter()
973 .any(|n| n.network_type == NetworkType::Solana));
974
975 Ok(())
976 }
977
978 #[tokio::test]
979 async fn test_process_networks() -> Result<()> {
980 use crate::config::network::test_utils::*;
981
982 let networks = vec![
983 create_evm_network_wrapped("mainnet"),
984 create_solana_network_wrapped("devnet"),
985 ];
986
987 let config = Config {
988 signers: vec![],
989 relayers: vec![],
990 notifications: vec![],
991 networks: NetworksFileConfig::new(networks).unwrap(),
992 plugins: Some(vec![]),
993 };
994
995 let app_state = ThinData(create_test_app_state());
996
997 process_networks(&config, &app_state).await?;
998
999 let stored_networks = app_state.network_repository.list_all().await?;
1000 assert_eq!(stored_networks.len(), 2);
1001 assert!(stored_networks
1002 .iter()
1003 .any(|n| n.name == "mainnet" && n.network_type == NetworkType::Evm));
1004 assert!(stored_networks
1005 .iter()
1006 .any(|n| n.name == "devnet" && n.network_type == NetworkType::Solana));
1007
1008 Ok(())
1009 }
1010
1011 #[tokio::test]
1012 async fn test_process_relayers() -> Result<()> {
1013 let signers = vec![SignerFileConfig {
1015 id: "test-signer-1".to_string(),
1016 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
1017 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
1018 passphrase: PlainOrEnvValue::Plain {
1019 value: SecretString::new("test"),
1020 },
1021 }),
1022 }];
1023
1024 let relayers = vec![RelayerFileConfig {
1026 id: "test-relayer-1".to_string(),
1027 network_type: ConfigFileNetworkType::Evm,
1028 signer_id: "test-signer-1".to_string(),
1029 name: "test-relayer-1".to_string(),
1030 network: "test-network".to_string(),
1031 paused: false,
1032 policies: None,
1033 notification_id: None,
1034 custom_rpc_urls: None,
1035 }];
1036
1037 let config = Config {
1039 signers: signers.clone(),
1040 relayers,
1041 notifications: vec![],
1042 networks: NetworksFileConfig::new(vec![]).unwrap(),
1043 plugins: Some(vec![]),
1044 };
1045
1046 let app_state = ThinData(create_test_app_state());
1048
1049 process_signers(&config, &app_state).await?;
1051
1052 process_relayers(&config, &app_state).await?;
1054
1055 let stored_relayers = app_state.relayer_repository.list_all().await?;
1057 assert_eq!(stored_relayers.len(), 1);
1058 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1059 assert_eq!(stored_relayers[0].signer_id, "test-signer-1");
1060 assert!(!stored_relayers[0].address.is_empty()); Ok(())
1063 }
1064
1065 #[tokio::test]
1066 async fn test_process_plugins() -> Result<()> {
1067 let plugins = vec![
1069 PluginFileConfig {
1070 id: "test-plugin-1".to_string(),
1071 path: "/app/plugins/test.ts".to_string(),
1072 timeout: None,
1073 emit_logs: false,
1074 emit_traces: false,
1075 config: None,
1076 raw_response: false,
1077 allow_get_invocation: false,
1078 forward_logs: false,
1079 },
1080 PluginFileConfig {
1081 id: "test-plugin-2".to_string(),
1082 path: "/app/plugins/test2.ts".to_string(),
1083 timeout: Some(12),
1084 emit_logs: false,
1085 emit_traces: false,
1086 config: None,
1087 raw_response: false,
1088 allow_get_invocation: false,
1089 forward_logs: false,
1090 },
1091 ];
1092
1093 let config = Config {
1095 signers: vec![],
1096 relayers: vec![],
1097 notifications: vec![],
1098 networks: NetworksFileConfig::new(vec![]).unwrap(),
1099 plugins: Some(plugins),
1100 };
1101
1102 let app_state = ThinData(create_test_app_state());
1104
1105 process_plugins(&config, &app_state).await?;
1107
1108 let plugin_1 = app_state
1110 .plugin_repository
1111 .get_by_id("test-plugin-1")
1112 .await?;
1113 let plugin_2 = app_state
1114 .plugin_repository
1115 .get_by_id("test-plugin-2")
1116 .await?;
1117
1118 assert!(plugin_1.is_some());
1119 assert!(plugin_2.is_some());
1120
1121 let plugin_1 = plugin_1.unwrap();
1122 let plugin_2 = plugin_2.unwrap();
1123
1124 assert_eq!(plugin_1.path, "/app/plugins/test.ts");
1125 assert_eq!(plugin_2.path, "/app/plugins/test2.ts");
1126
1127 assert_eq!(
1129 plugin_1.timeout.as_secs(),
1130 Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS).as_secs()
1131 );
1132 assert_eq!(
1133 plugin_2.timeout.as_secs(),
1134 Duration::from_secs(12).as_secs()
1135 );
1136
1137 Ok(())
1138 }
1139
1140 #[tokio::test]
1141 async fn test_process_api_key() -> Result<()> {
1142 let server_config = Arc::new(crate::utils::mocks::mockutils::create_test_server_config(
1143 RepositoryStorageType::InMemory,
1144 ));
1145 let app_state = ThinData(create_test_app_state());
1146
1147 process_api_key(&server_config, &app_state).await?;
1148
1149 let pagination_query = PaginationQuery {
1150 page: 1,
1151 per_page: 10,
1152 };
1153
1154 let stored_api_keys = app_state
1155 .api_key_repository
1156 .list_paginated(pagination_query)
1157 .await?;
1158 assert_eq!(stored_api_keys.items.len(), 1);
1159 assert_eq!(stored_api_keys.items[0].name, "default");
1160
1161 Ok(())
1162 }
1163
1164 #[tokio::test]
1165 async fn test_process_config_file() -> Result<()> {
1166 let signers = vec![SignerFileConfig {
1168 id: "test-signer-1".to_string(),
1169 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
1170 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
1171 passphrase: PlainOrEnvValue::Plain {
1172 value: SecretString::new("test"),
1173 },
1174 }),
1175 }];
1176
1177 let relayers = vec![RelayerFileConfig {
1178 id: "test-relayer-1".to_string(),
1179 network_type: ConfigFileNetworkType::Evm,
1180 signer_id: "test-signer-1".to_string(),
1181 name: "test-relayer-1".to_string(),
1182 network: "test-network".to_string(),
1183 paused: false,
1184 policies: None,
1185 notification_id: None,
1186 custom_rpc_urls: None,
1187 }];
1188
1189 let notifications = vec![NotificationConfig {
1190 id: "test-notification-1".to_string(),
1191 r#type: NotificationType::Webhook,
1192 url: "https://hooks.slack.com/test1".to_string(),
1193 signing_key: None,
1194 }];
1195
1196 let plugins = vec![PluginFileConfig {
1197 id: "test-plugin-1".to_string(),
1198 path: "/app/plugins/test.ts".to_string(),
1199 timeout: None,
1200 emit_logs: false,
1201 emit_traces: false,
1202 allow_get_invocation: false,
1203 config: None,
1204 raw_response: false,
1205 forward_logs: false,
1206 }];
1207
1208 let config = Config {
1210 signers,
1211 relayers,
1212 notifications,
1213 networks: NetworksFileConfig::new(vec![]).unwrap(),
1214 plugins: Some(plugins),
1215 };
1216
1217 let signer_repo = Arc::new(InMemorySignerRepository::default());
1219 let relayer_repo = Arc::new(RelayerRepositoryStorage::new_in_memory());
1220 let notification_repo = Arc::new(InMemoryNotificationRepository::default());
1221 let network_repo = Arc::new(InMemoryNetworkRepository::default());
1222 let transaction_repo = Arc::new(TransactionRepositoryStorage::InMemory(
1223 InMemoryTransactionRepository::new(),
1224 ));
1225 let transaction_counter = Arc::new(InMemoryTransactionCounter::default());
1226 let plugin_repo = Arc::new(InMemoryPluginRepository::default());
1227 let api_key_repo = Arc::new(InMemoryApiKeyRepository::default());
1228
1229 let mut mock_job_producer = MockJobProducerTrait::new();
1231 mock_job_producer
1232 .expect_produce_transaction_request_job()
1233 .returning(|_, _| Box::pin(async { Ok(()) }));
1234 mock_job_producer
1235 .expect_produce_submit_transaction_job()
1236 .returning(|_, _| Box::pin(async { Ok(()) }));
1237 mock_job_producer
1238 .expect_produce_check_transaction_status_job()
1239 .returning(|_, _| Box::pin(async { Ok(()) }));
1240 mock_job_producer
1241 .expect_produce_send_notification_job()
1242 .returning(|_, _| Box::pin(async { Ok(()) }));
1243 let job_producer = Arc::new(mock_job_producer);
1244
1245 let app_state = ThinData(AppState {
1247 signer_repository: signer_repo.clone(),
1248 relayer_repository: relayer_repo.clone(),
1249 notification_repository: notification_repo.clone(),
1250 network_repository: network_repo.clone(),
1251 transaction_repository: transaction_repo.clone(),
1252 transaction_counter_store: transaction_counter.clone(),
1253 job_producer: job_producer.clone(),
1254 plugin_repository: plugin_repo.clone(),
1255 api_key_repository: api_key_repo.clone(),
1256 });
1257
1258 let server_config = Arc::new(crate::utils::mocks::mockutils::create_test_server_config(
1260 RepositoryStorageType::InMemory,
1261 ));
1262 process_config_file(config, server_config, &app_state).await?;
1263
1264 let stored_signers = signer_repo.list_all().await?;
1266 assert_eq!(stored_signers.len(), 1);
1267 assert_eq!(stored_signers[0].id, "test-signer-1");
1268
1269 let stored_relayers = relayer_repo.list_all().await?;
1270 assert_eq!(stored_relayers.len(), 1);
1271 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1272 assert_eq!(stored_relayers[0].signer_id, "test-signer-1");
1273
1274 let stored_notifications = notification_repo.list_all().await?;
1275 assert_eq!(stored_notifications.len(), 1);
1276 assert_eq!(stored_notifications[0].id, "test-notification-1");
1277
1278 let stored_plugin = plugin_repo.get_by_id("test-plugin-1").await?;
1279 assert_eq!(stored_plugin.unwrap().path, "/app/plugins/test.ts");
1280
1281 Ok(())
1282 }
1283
1284 #[tokio::test]
1285 async fn test_process_signer_google_cloud_kms() {
1286 use crate::models::SecretString;
1287
1288 let signer = SignerFileConfig {
1289 id: "gcp-kms-signer".to_string(),
1290 config: SignerFileConfigEnum::GoogleCloudKms(GoogleCloudKmsSignerFileConfig {
1291 service_account: GoogleCloudKmsServiceAccountFileConfig {
1292 private_key: PlainOrEnvValue::Plain {
1293 value: SecretString::new("-----BEGIN EXAMPLE PRIVATE KEY-----\nFAKEKEYDATA\n-----END EXAMPLE PRIVATE KEY-----\n"),
1294 },
1295 client_email: PlainOrEnvValue::Plain {
1296 value: SecretString::new("test-service-account@example.com"),
1297 },
1298 private_key_id: PlainOrEnvValue::Plain {
1299 value: SecretString::new("fake-private-key-id"),
1300 },
1301 client_id: "fake-client-id".to_string(),
1302 project_id: "fake-project-id".to_string(),
1303 auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),
1304 token_uri: "https://oauth2.googleapis.com/token".to_string(),
1305 client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test-service-account%40example.com".to_string(),
1306 auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs".to_string(),
1307 universe_domain: "googleapis.com".to_string(),
1308 },
1309 key: GoogleCloudKmsKeyFileConfig {
1310 location: "global".to_string(),
1311 key_id: "fake-key-id".to_string(),
1312 key_ring_id: "fake-key-ring-id".to_string(),
1313 key_version: 1,
1314 },
1315 }),
1316 };
1317
1318 let result = process_signer(&signer).await;
1319
1320 assert!(
1321 result.is_ok(),
1322 "Failed to process Google Cloud KMS signer: {:?}",
1323 result.err()
1324 );
1325 let model = result.unwrap();
1326
1327 assert_eq!(model.id, "gcp-kms-signer");
1328 }
1329
1330 #[tokio::test]
1331 async fn test_is_redis_populated_empty_repositories() -> Result<()> {
1332 let app_state = ThinData(create_test_app_state());
1334
1335 assert!(!app_state.relayer_repository.has_entries().await?);
1337 assert!(!app_state.transaction_repository.has_entries().await?);
1338 assert!(!app_state.signer_repository.has_entries().await?);
1339 assert!(!app_state.notification_repository.has_entries().await?);
1340 assert!(!app_state.network_repository.has_entries().await?);
1341
1342 let result = is_redis_populated(&app_state).await?;
1344 assert!(!result, "Expected false when all repositories are empty");
1345
1346 Ok(())
1347 }
1348
1349 #[tokio::test]
1350 async fn test_is_redis_populated_relayer_repository_has_entries() -> Result<()> {
1351 let app_state = ThinData(create_test_app_state());
1352
1353 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1355 app_state.relayer_repository.create(relayer).await?;
1356
1357 assert!(app_state.relayer_repository.has_entries().await?);
1359
1360 let result = is_redis_populated(&app_state).await?;
1362 assert!(result, "Expected true when relayer repository has entries");
1363
1364 Ok(())
1365 }
1366
1367 #[tokio::test]
1368 async fn test_is_redis_populated_transaction_repository_has_entries() -> Result<()> {
1369 let app_state = ThinData(create_test_app_state());
1370
1371 let transaction = TransactionRepoModel::default();
1373 app_state.transaction_repository.create(transaction).await?;
1374
1375 assert!(app_state.transaction_repository.has_entries().await?);
1377
1378 let result = is_redis_populated(&app_state).await?;
1380 assert!(
1381 result,
1382 "Expected true when transaction repository has entries"
1383 );
1384
1385 Ok(())
1386 }
1387
1388 #[tokio::test]
1389 async fn test_is_redis_populated_signer_repository_has_entries() -> Result<()> {
1390 let app_state = ThinData(create_test_app_state());
1391
1392 let signer = create_mock_signer();
1394 app_state.signer_repository.create(signer).await?;
1395
1396 assert!(app_state.signer_repository.has_entries().await?);
1398
1399 let result = is_redis_populated(&app_state).await?;
1401 assert!(result, "Expected true when signer repository has entries");
1402
1403 Ok(())
1404 }
1405
1406 #[tokio::test]
1407 async fn test_is_redis_populated_notification_repository_has_entries() -> Result<()> {
1408 let app_state = ThinData(create_test_app_state());
1409
1410 let notification = create_mock_notification("test-notification".to_string());
1412 app_state
1413 .notification_repository
1414 .create(notification)
1415 .await?;
1416
1417 assert!(app_state.notification_repository.has_entries().await?);
1419
1420 let result = is_redis_populated(&app_state).await?;
1422 assert!(
1423 result,
1424 "Expected true when notification repository has entries"
1425 );
1426
1427 Ok(())
1428 }
1429
1430 #[tokio::test]
1431 async fn test_is_redis_populated_network_repository_has_entries() -> Result<()> {
1432 let app_state = ThinData(create_test_app_state());
1433
1434 let network = create_mock_network();
1436 app_state.network_repository.create(network).await?;
1437
1438 assert!(app_state.network_repository.has_entries().await?);
1440
1441 let result = is_redis_populated(&app_state).await?;
1443 assert!(result, "Expected true when network repository has entries");
1444
1445 Ok(())
1446 }
1447
1448 #[tokio::test]
1449 async fn test_is_redis_populated_multiple_repositories_have_entries() -> Result<()> {
1450 let app_state = ThinData(create_test_app_state());
1451
1452 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1454 let signer = create_mock_signer();
1455 let notification = create_mock_notification("test-notification".to_string());
1456 let network = create_mock_network();
1457
1458 app_state.relayer_repository.create(relayer).await?;
1459 app_state.signer_repository.create(signer).await?;
1460 app_state
1461 .notification_repository
1462 .create(notification)
1463 .await?;
1464 app_state.network_repository.create(network).await?;
1465
1466 assert!(app_state.relayer_repository.has_entries().await?);
1468 assert!(app_state.signer_repository.has_entries().await?);
1469 assert!(app_state.notification_repository.has_entries().await?);
1470 assert!(app_state.network_repository.has_entries().await?);
1471
1472 let result = is_redis_populated(&app_state).await?;
1474 assert!(
1475 result,
1476 "Expected true when multiple repositories have entries"
1477 );
1478
1479 Ok(())
1480 }
1481
1482 #[tokio::test]
1483 async fn test_is_redis_populated_comprehensive_scenario() -> Result<()> {
1484 let app_state = ThinData(create_test_app_state());
1485
1486 let result = is_redis_populated(&app_state).await?;
1488 assert!(!result, "Expected false when all repositories are empty");
1489
1490 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1492 app_state.relayer_repository.create(relayer).await?;
1493 let result = is_redis_populated(&app_state).await?;
1494 assert!(result, "Expected true after adding one entry");
1495
1496 app_state.relayer_repository.drop_all_entries().await?;
1498 let result = is_redis_populated(&app_state).await?;
1499 assert!(!result, "Expected false after clearing all repositories");
1500
1501 let signer = create_mock_signer();
1503 app_state.signer_repository.create(signer).await?;
1504 let result = is_redis_populated(&app_state).await?;
1505 assert!(result, "Expected true after adding signer");
1506
1507 let notification = create_mock_notification("test-notification".to_string());
1508 app_state
1509 .notification_repository
1510 .create(notification)
1511 .await?;
1512 let result = is_redis_populated(&app_state).await?;
1513 assert!(result, "Expected true after adding notification");
1514
1515 Ok(())
1516 }
1517
1518 fn create_test_server_config_with_settings(
1520 storage_type: RepositoryStorageType,
1521 reset_storage_on_start: bool,
1522 ) -> ServerConfig {
1523 ServerConfig {
1524 repository_storage_type: storage_type.clone(),
1525 reset_storage_on_start,
1526 ..create_test_server_config(storage_type)
1527 }
1528 }
1529
1530 fn create_minimal_test_config() -> Config {
1532 Config {
1533 signers: vec![SignerFileConfig {
1534 id: "test-signer-1".to_string(),
1535 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
1536 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
1537 passphrase: PlainOrEnvValue::Plain {
1538 value: SecretString::new("test"),
1539 },
1540 }),
1541 }],
1542 relayers: vec![RelayerFileConfig {
1543 id: "test-relayer-1".to_string(),
1544 network_type: ConfigFileNetworkType::Evm,
1545 signer_id: "test-signer-1".to_string(),
1546 name: "test-relayer-1".to_string(),
1547 network: "test-network".to_string(),
1548 paused: false,
1549 policies: None,
1550 notification_id: None,
1551 custom_rpc_urls: None,
1552 }],
1553 notifications: vec![NotificationConfig {
1554 id: "test-notification-1".to_string(),
1555 r#type: NotificationType::Webhook,
1556 url: "https://hooks.slack.com/test1".to_string(),
1557 signing_key: None,
1558 }],
1559 networks: NetworksFileConfig::new(vec![]).unwrap(),
1560 plugins: None,
1561 }
1562 }
1563
1564 #[tokio::test]
1565 async fn test_should_process_config_file_inmemory_storage() -> Result<()> {
1566 let config = create_minimal_test_config();
1567
1568 let server_config = Arc::new(create_test_server_config_with_settings(
1570 RepositoryStorageType::InMemory,
1571 false,
1572 ));
1573
1574 let app_state = ThinData(create_test_app_state());
1575 process_config_file(config.clone(), server_config.clone(), &app_state).await?;
1576
1577 let stored_relayers = app_state.relayer_repository.list_all().await?;
1578 assert_eq!(stored_relayers.len(), 1);
1579 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1580
1581 let server_config2 = Arc::new(create_test_server_config_with_settings(
1583 RepositoryStorageType::InMemory,
1584 true,
1585 ));
1586
1587 let app_state2 = ThinData(create_test_app_state());
1588 process_config_file(config.clone(), server_config2, &app_state2).await?;
1589
1590 let stored_relayers = app_state2.relayer_repository.list_all().await?;
1591 assert_eq!(stored_relayers.len(), 1);
1592 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1593
1594 Ok(())
1595 }
1596
1597 #[tokio::test]
1598 async fn test_should_process_config_file_redis_storage_empty_repositories() -> Result<()> {
1599 let config = create_minimal_test_config();
1600 let server_config = Arc::new(create_test_server_config_with_settings(
1601 RepositoryStorageType::Redis,
1602 false,
1603 ));
1604
1605 let app_state = ThinData(create_test_app_state());
1606 process_config_file(config, server_config, &app_state).await?;
1607
1608 let stored_relayers = app_state.relayer_repository.list_all().await?;
1609 assert_eq!(stored_relayers.len(), 1);
1610 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1611
1612 Ok(())
1613 }
1614
1615 #[tokio::test]
1616 async fn ai_process_config_file_skips_when_redis_populated() -> Result<()> {
1617 let config = create_minimal_test_config();
1618 let server_config = Arc::new(create_test_server_config_with_settings(
1619 RepositoryStorageType::Redis,
1620 false,
1621 ));
1622
1623 let app_state = ThinData(create_test_app_state());
1624
1625 app_state
1626 .relayer_repository
1627 .create(create_mock_relayer("existing-relayer".to_string(), false))
1628 .await?;
1629
1630 process_config_file(config, server_config, &app_state).await?;
1631
1632 let stored_relayers = app_state.relayer_repository.list_all().await?;
1633 assert_eq!(stored_relayers.len(), 1);
1634 assert_eq!(stored_relayers[0].id, "existing-relayer");
1635
1636 Ok(())
1637 }
1638
1639 #[tokio::test]
1640 async fn test_should_not_process_config_file_redis_storage_populated_repositories() -> Result<()>
1641 {
1642 let config = create_minimal_test_config();
1643 let server_config = Arc::new(create_test_server_config_with_settings(
1644 RepositoryStorageType::Redis,
1645 false,
1646 ));
1647
1648 let app_state1 = ThinData(create_test_app_state());
1650 let app_state2 = ThinData(create_test_app_state());
1651
1652 let existing_relayer1 = create_mock_relayer("existing-relayer".to_string(), false);
1654 let existing_relayer2 = create_mock_relayer("existing-relayer".to_string(), false);
1655 app_state1
1656 .relayer_repository
1657 .create(existing_relayer1)
1658 .await?;
1659 app_state2
1660 .relayer_repository
1661 .create(existing_relayer2)
1662 .await?;
1663
1664 assert!(app_state1.relayer_repository.has_entries().await?);
1666 assert!(!app_state1.signer_repository.has_entries().await?);
1667
1668 process_config_file(config, server_config, &app_state2).await?;
1670
1671 let relayer_from_config = app_state2
1672 .relayer_repository
1673 .get_by_id("test-relayer-1".to_string())
1674 .await;
1675 assert!(
1676 relayer_from_config.is_err(),
1677 "Relayer from config should not be found"
1678 );
1679
1680 let existing_relayer = app_state2
1681 .relayer_repository
1682 .get_by_id("existing-relayer".to_string())
1683 .await?;
1684 assert_eq!(existing_relayer.id, "existing-relayer");
1685
1686 Ok(())
1688 }
1689
1690 #[tokio::test]
1691 async fn test_should_process_config_file_redis_storage_with_reset_flag() -> Result<()> {
1692 let config = create_minimal_test_config();
1693 let server_config = Arc::new(create_test_server_config_with_settings(
1694 RepositoryStorageType::Redis,
1695 true, ));
1697
1698 let app_state = ThinData(create_test_app_state());
1699
1700 let existing_relayer = create_mock_relayer("existing-relayer".to_string(), false);
1702 let existing_signer = create_mock_signer();
1703 app_state
1704 .relayer_repository
1705 .create(existing_relayer)
1706 .await?;
1707 app_state.signer_repository.create(existing_signer).await?;
1708
1709 process_config_file(config, server_config, &app_state).await?;
1711
1712 let stored_relayer = app_state
1713 .relayer_repository
1714 .get_by_id("existing-relayer".to_string())
1715 .await;
1716 assert!(
1717 stored_relayer.is_err(),
1718 "Existing relayer should not be found"
1719 );
1720
1721 let stored_signer = app_state
1722 .signer_repository
1723 .get_by_id("existing-signer".to_string())
1724 .await;
1725 assert!(
1726 stored_signer.is_err(),
1727 "Existing signer should not be found"
1728 );
1729
1730 Ok(())
1731 }
1732}