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 {} not found",
933 expected_name
934 );
935 }
936
937 Ok(())
938 }
939
940 #[tokio::test]
941 async fn test_process_networks_duplicate_names() -> Result<()> {
942 use crate::config::network::test_utils::*;
943
944 let networks = vec![
945 create_evm_network_wrapped("mainnet"),
946 create_solana_network_wrapped("mainnet"),
947 ];
948
949 let config = Config {
950 signers: vec![],
951 relayers: vec![],
952 notifications: vec![],
953 networks: NetworksFileConfig::new(networks).unwrap(),
954 plugins: Some(vec![]),
955 };
956
957 let app_state = ThinData(create_test_app_state());
958
959 process_networks(&config, &app_state).await?;
960
961 let stored_networks = app_state.network_repository.list_all().await?;
962 assert_eq!(stored_networks.len(), 2);
963
964 let mainnet_networks: Vec<_> = stored_networks
965 .iter()
966 .filter(|n| n.name == "mainnet")
967 .collect();
968 assert_eq!(mainnet_networks.len(), 2);
969 assert!(mainnet_networks
970 .iter()
971 .any(|n| n.network_type == NetworkType::Evm));
972 assert!(mainnet_networks
973 .iter()
974 .any(|n| n.network_type == NetworkType::Solana));
975
976 Ok(())
977 }
978
979 #[tokio::test]
980 async fn test_process_networks() -> Result<()> {
981 use crate::config::network::test_utils::*;
982
983 let networks = vec![
984 create_evm_network_wrapped("mainnet"),
985 create_solana_network_wrapped("devnet"),
986 ];
987
988 let config = Config {
989 signers: vec![],
990 relayers: vec![],
991 notifications: vec![],
992 networks: NetworksFileConfig::new(networks).unwrap(),
993 plugins: Some(vec![]),
994 };
995
996 let app_state = ThinData(create_test_app_state());
997
998 process_networks(&config, &app_state).await?;
999
1000 let stored_networks = app_state.network_repository.list_all().await?;
1001 assert_eq!(stored_networks.len(), 2);
1002 assert!(stored_networks
1003 .iter()
1004 .any(|n| n.name == "mainnet" && n.network_type == NetworkType::Evm));
1005 assert!(stored_networks
1006 .iter()
1007 .any(|n| n.name == "devnet" && n.network_type == NetworkType::Solana));
1008
1009 Ok(())
1010 }
1011
1012 #[tokio::test]
1013 async fn test_process_relayers() -> Result<()> {
1014 let signers = vec![SignerFileConfig {
1016 id: "test-signer-1".to_string(),
1017 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
1018 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
1019 passphrase: PlainOrEnvValue::Plain {
1020 value: SecretString::new("test"),
1021 },
1022 }),
1023 }];
1024
1025 let relayers = vec![RelayerFileConfig {
1027 id: "test-relayer-1".to_string(),
1028 network_type: ConfigFileNetworkType::Evm,
1029 signer_id: "test-signer-1".to_string(),
1030 name: "test-relayer-1".to_string(),
1031 network: "test-network".to_string(),
1032 paused: false,
1033 policies: None,
1034 notification_id: None,
1035 custom_rpc_urls: None,
1036 }];
1037
1038 let config = Config {
1040 signers: signers.clone(),
1041 relayers,
1042 notifications: vec![],
1043 networks: NetworksFileConfig::new(vec![]).unwrap(),
1044 plugins: Some(vec![]),
1045 };
1046
1047 let app_state = ThinData(create_test_app_state());
1049
1050 process_signers(&config, &app_state).await?;
1052
1053 process_relayers(&config, &app_state).await?;
1055
1056 let stored_relayers = app_state.relayer_repository.list_all().await?;
1058 assert_eq!(stored_relayers.len(), 1);
1059 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1060 assert_eq!(stored_relayers[0].signer_id, "test-signer-1");
1061 assert!(!stored_relayers[0].address.is_empty()); Ok(())
1064 }
1065
1066 #[tokio::test]
1067 async fn test_process_plugins() -> Result<()> {
1068 let plugins = vec![
1070 PluginFileConfig {
1071 id: "test-plugin-1".to_string(),
1072 path: "/app/plugins/test.ts".to_string(),
1073 timeout: None,
1074 emit_logs: false,
1075 emit_traces: false,
1076 config: None,
1077 raw_response: false,
1078 allow_get_invocation: false,
1079 forward_logs: false,
1080 },
1081 PluginFileConfig {
1082 id: "test-plugin-2".to_string(),
1083 path: "/app/plugins/test2.ts".to_string(),
1084 timeout: Some(12),
1085 emit_logs: false,
1086 emit_traces: false,
1087 config: None,
1088 raw_response: false,
1089 allow_get_invocation: false,
1090 forward_logs: false,
1091 },
1092 ];
1093
1094 let config = Config {
1096 signers: vec![],
1097 relayers: vec![],
1098 notifications: vec![],
1099 networks: NetworksFileConfig::new(vec![]).unwrap(),
1100 plugins: Some(plugins),
1101 };
1102
1103 let app_state = ThinData(create_test_app_state());
1105
1106 process_plugins(&config, &app_state).await?;
1108
1109 let plugin_1 = app_state
1111 .plugin_repository
1112 .get_by_id("test-plugin-1")
1113 .await?;
1114 let plugin_2 = app_state
1115 .plugin_repository
1116 .get_by_id("test-plugin-2")
1117 .await?;
1118
1119 assert!(plugin_1.is_some());
1120 assert!(plugin_2.is_some());
1121
1122 let plugin_1 = plugin_1.unwrap();
1123 let plugin_2 = plugin_2.unwrap();
1124
1125 assert_eq!(plugin_1.path, "/app/plugins/test.ts");
1126 assert_eq!(plugin_2.path, "/app/plugins/test2.ts");
1127
1128 assert_eq!(
1130 plugin_1.timeout.as_secs(),
1131 Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS).as_secs()
1132 );
1133 assert_eq!(
1134 plugin_2.timeout.as_secs(),
1135 Duration::from_secs(12).as_secs()
1136 );
1137
1138 Ok(())
1139 }
1140
1141 #[tokio::test]
1142 async fn test_process_api_key() -> Result<()> {
1143 let server_config = Arc::new(crate::utils::mocks::mockutils::create_test_server_config(
1144 RepositoryStorageType::InMemory,
1145 ));
1146 let app_state = ThinData(create_test_app_state());
1147
1148 process_api_key(&server_config, &app_state).await?;
1149
1150 let pagination_query = PaginationQuery {
1151 page: 1,
1152 per_page: 10,
1153 };
1154
1155 let stored_api_keys = app_state
1156 .api_key_repository
1157 .list_paginated(pagination_query)
1158 .await?;
1159 assert_eq!(stored_api_keys.items.len(), 1);
1160 assert_eq!(stored_api_keys.items[0].name, "default");
1161
1162 Ok(())
1163 }
1164
1165 #[tokio::test]
1166 async fn test_process_config_file() -> Result<()> {
1167 let signers = vec![SignerFileConfig {
1169 id: "test-signer-1".to_string(),
1170 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
1171 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
1172 passphrase: PlainOrEnvValue::Plain {
1173 value: SecretString::new("test"),
1174 },
1175 }),
1176 }];
1177
1178 let relayers = vec![RelayerFileConfig {
1179 id: "test-relayer-1".to_string(),
1180 network_type: ConfigFileNetworkType::Evm,
1181 signer_id: "test-signer-1".to_string(),
1182 name: "test-relayer-1".to_string(),
1183 network: "test-network".to_string(),
1184 paused: false,
1185 policies: None,
1186 notification_id: None,
1187 custom_rpc_urls: None,
1188 }];
1189
1190 let notifications = vec![NotificationConfig {
1191 id: "test-notification-1".to_string(),
1192 r#type: NotificationType::Webhook,
1193 url: "https://hooks.slack.com/test1".to_string(),
1194 signing_key: None,
1195 }];
1196
1197 let plugins = vec![PluginFileConfig {
1198 id: "test-plugin-1".to_string(),
1199 path: "/app/plugins/test.ts".to_string(),
1200 timeout: None,
1201 emit_logs: false,
1202 emit_traces: false,
1203 allow_get_invocation: false,
1204 config: None,
1205 raw_response: false,
1206 forward_logs: false,
1207 }];
1208
1209 let config = Config {
1211 signers,
1212 relayers,
1213 notifications,
1214 networks: NetworksFileConfig::new(vec![]).unwrap(),
1215 plugins: Some(plugins),
1216 };
1217
1218 let signer_repo = Arc::new(InMemorySignerRepository::default());
1220 let relayer_repo = Arc::new(RelayerRepositoryStorage::new_in_memory());
1221 let notification_repo = Arc::new(InMemoryNotificationRepository::default());
1222 let network_repo = Arc::new(InMemoryNetworkRepository::default());
1223 let transaction_repo = Arc::new(TransactionRepositoryStorage::InMemory(
1224 InMemoryTransactionRepository::new(),
1225 ));
1226 let transaction_counter = Arc::new(InMemoryTransactionCounter::default());
1227 let plugin_repo = Arc::new(InMemoryPluginRepository::default());
1228 let api_key_repo = Arc::new(InMemoryApiKeyRepository::default());
1229
1230 let mut mock_job_producer = MockJobProducerTrait::new();
1232 mock_job_producer
1233 .expect_produce_transaction_request_job()
1234 .returning(|_, _| Box::pin(async { Ok(()) }));
1235 mock_job_producer
1236 .expect_produce_submit_transaction_job()
1237 .returning(|_, _| Box::pin(async { Ok(()) }));
1238 mock_job_producer
1239 .expect_produce_check_transaction_status_job()
1240 .returning(|_, _| Box::pin(async { Ok(()) }));
1241 mock_job_producer
1242 .expect_produce_send_notification_job()
1243 .returning(|_, _| Box::pin(async { Ok(()) }));
1244 let job_producer = Arc::new(mock_job_producer);
1245
1246 let app_state = ThinData(AppState {
1248 signer_repository: signer_repo.clone(),
1249 relayer_repository: relayer_repo.clone(),
1250 notification_repository: notification_repo.clone(),
1251 network_repository: network_repo.clone(),
1252 transaction_repository: transaction_repo.clone(),
1253 transaction_counter_store: transaction_counter.clone(),
1254 job_producer: job_producer.clone(),
1255 plugin_repository: plugin_repo.clone(),
1256 api_key_repository: api_key_repo.clone(),
1257 });
1258
1259 let server_config = Arc::new(crate::utils::mocks::mockutils::create_test_server_config(
1261 RepositoryStorageType::InMemory,
1262 ));
1263 process_config_file(config, server_config, &app_state).await?;
1264
1265 let stored_signers = signer_repo.list_all().await?;
1267 assert_eq!(stored_signers.len(), 1);
1268 assert_eq!(stored_signers[0].id, "test-signer-1");
1269
1270 let stored_relayers = relayer_repo.list_all().await?;
1271 assert_eq!(stored_relayers.len(), 1);
1272 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1273 assert_eq!(stored_relayers[0].signer_id, "test-signer-1");
1274
1275 let stored_notifications = notification_repo.list_all().await?;
1276 assert_eq!(stored_notifications.len(), 1);
1277 assert_eq!(stored_notifications[0].id, "test-notification-1");
1278
1279 let stored_plugin = plugin_repo.get_by_id("test-plugin-1").await?;
1280 assert_eq!(stored_plugin.unwrap().path, "/app/plugins/test.ts");
1281
1282 Ok(())
1283 }
1284
1285 #[tokio::test]
1286 async fn test_process_signer_google_cloud_kms() {
1287 use crate::models::SecretString;
1288
1289 let signer = SignerFileConfig {
1290 id: "gcp-kms-signer".to_string(),
1291 config: SignerFileConfigEnum::GoogleCloudKms(GoogleCloudKmsSignerFileConfig {
1292 service_account: GoogleCloudKmsServiceAccountFileConfig {
1293 private_key: PlainOrEnvValue::Plain {
1294 value: SecretString::new("-----BEGIN EXAMPLE PRIVATE KEY-----\nFAKEKEYDATA\n-----END EXAMPLE PRIVATE KEY-----\n"),
1295 },
1296 client_email: PlainOrEnvValue::Plain {
1297 value: SecretString::new("test-service-account@example.com"),
1298 },
1299 private_key_id: PlainOrEnvValue::Plain {
1300 value: SecretString::new("fake-private-key-id"),
1301 },
1302 client_id: "fake-client-id".to_string(),
1303 project_id: "fake-project-id".to_string(),
1304 auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),
1305 token_uri: "https://oauth2.googleapis.com/token".to_string(),
1306 client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test-service-account%40example.com".to_string(),
1307 auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs".to_string(),
1308 universe_domain: "googleapis.com".to_string(),
1309 },
1310 key: GoogleCloudKmsKeyFileConfig {
1311 location: "global".to_string(),
1312 key_id: "fake-key-id".to_string(),
1313 key_ring_id: "fake-key-ring-id".to_string(),
1314 key_version: 1,
1315 },
1316 }),
1317 };
1318
1319 let result = process_signer(&signer).await;
1320
1321 assert!(
1322 result.is_ok(),
1323 "Failed to process Google Cloud KMS signer: {:?}",
1324 result.err()
1325 );
1326 let model = result.unwrap();
1327
1328 assert_eq!(model.id, "gcp-kms-signer");
1329 }
1330
1331 #[tokio::test]
1332 async fn test_is_redis_populated_empty_repositories() -> Result<()> {
1333 let app_state = ThinData(create_test_app_state());
1335
1336 assert!(!app_state.relayer_repository.has_entries().await?);
1338 assert!(!app_state.transaction_repository.has_entries().await?);
1339 assert!(!app_state.signer_repository.has_entries().await?);
1340 assert!(!app_state.notification_repository.has_entries().await?);
1341 assert!(!app_state.network_repository.has_entries().await?);
1342
1343 let result = is_redis_populated(&app_state).await?;
1345 assert!(!result, "Expected false when all repositories are empty");
1346
1347 Ok(())
1348 }
1349
1350 #[tokio::test]
1351 async fn test_is_redis_populated_relayer_repository_has_entries() -> Result<()> {
1352 let app_state = ThinData(create_test_app_state());
1353
1354 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1356 app_state.relayer_repository.create(relayer).await?;
1357
1358 assert!(app_state.relayer_repository.has_entries().await?);
1360
1361 let result = is_redis_populated(&app_state).await?;
1363 assert!(result, "Expected true when relayer repository has entries");
1364
1365 Ok(())
1366 }
1367
1368 #[tokio::test]
1369 async fn test_is_redis_populated_transaction_repository_has_entries() -> Result<()> {
1370 let app_state = ThinData(create_test_app_state());
1371
1372 let transaction = TransactionRepoModel::default();
1374 app_state.transaction_repository.create(transaction).await?;
1375
1376 assert!(app_state.transaction_repository.has_entries().await?);
1378
1379 let result = is_redis_populated(&app_state).await?;
1381 assert!(
1382 result,
1383 "Expected true when transaction repository has entries"
1384 );
1385
1386 Ok(())
1387 }
1388
1389 #[tokio::test]
1390 async fn test_is_redis_populated_signer_repository_has_entries() -> Result<()> {
1391 let app_state = ThinData(create_test_app_state());
1392
1393 let signer = create_mock_signer();
1395 app_state.signer_repository.create(signer).await?;
1396
1397 assert!(app_state.signer_repository.has_entries().await?);
1399
1400 let result = is_redis_populated(&app_state).await?;
1402 assert!(result, "Expected true when signer repository has entries");
1403
1404 Ok(())
1405 }
1406
1407 #[tokio::test]
1408 async fn test_is_redis_populated_notification_repository_has_entries() -> Result<()> {
1409 let app_state = ThinData(create_test_app_state());
1410
1411 let notification = create_mock_notification("test-notification".to_string());
1413 app_state
1414 .notification_repository
1415 .create(notification)
1416 .await?;
1417
1418 assert!(app_state.notification_repository.has_entries().await?);
1420
1421 let result = is_redis_populated(&app_state).await?;
1423 assert!(
1424 result,
1425 "Expected true when notification repository has entries"
1426 );
1427
1428 Ok(())
1429 }
1430
1431 #[tokio::test]
1432 async fn test_is_redis_populated_network_repository_has_entries() -> Result<()> {
1433 let app_state = ThinData(create_test_app_state());
1434
1435 let network = create_mock_network();
1437 app_state.network_repository.create(network).await?;
1438
1439 assert!(app_state.network_repository.has_entries().await?);
1441
1442 let result = is_redis_populated(&app_state).await?;
1444 assert!(result, "Expected true when network repository has entries");
1445
1446 Ok(())
1447 }
1448
1449 #[tokio::test]
1450 async fn test_is_redis_populated_multiple_repositories_have_entries() -> Result<()> {
1451 let app_state = ThinData(create_test_app_state());
1452
1453 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1455 let signer = create_mock_signer();
1456 let notification = create_mock_notification("test-notification".to_string());
1457 let network = create_mock_network();
1458
1459 app_state.relayer_repository.create(relayer).await?;
1460 app_state.signer_repository.create(signer).await?;
1461 app_state
1462 .notification_repository
1463 .create(notification)
1464 .await?;
1465 app_state.network_repository.create(network).await?;
1466
1467 assert!(app_state.relayer_repository.has_entries().await?);
1469 assert!(app_state.signer_repository.has_entries().await?);
1470 assert!(app_state.notification_repository.has_entries().await?);
1471 assert!(app_state.network_repository.has_entries().await?);
1472
1473 let result = is_redis_populated(&app_state).await?;
1475 assert!(
1476 result,
1477 "Expected true when multiple repositories have entries"
1478 );
1479
1480 Ok(())
1481 }
1482
1483 #[tokio::test]
1484 async fn test_is_redis_populated_comprehensive_scenario() -> Result<()> {
1485 let app_state = ThinData(create_test_app_state());
1486
1487 let result = is_redis_populated(&app_state).await?;
1489 assert!(!result, "Expected false when all repositories are empty");
1490
1491 let relayer = create_mock_relayer("test-relayer".to_string(), false);
1493 app_state.relayer_repository.create(relayer).await?;
1494 let result = is_redis_populated(&app_state).await?;
1495 assert!(result, "Expected true after adding one entry");
1496
1497 app_state.relayer_repository.drop_all_entries().await?;
1499 let result = is_redis_populated(&app_state).await?;
1500 assert!(!result, "Expected false after clearing all repositories");
1501
1502 let signer = create_mock_signer();
1504 app_state.signer_repository.create(signer).await?;
1505 let result = is_redis_populated(&app_state).await?;
1506 assert!(result, "Expected true after adding signer");
1507
1508 let notification = create_mock_notification("test-notification".to_string());
1509 app_state
1510 .notification_repository
1511 .create(notification)
1512 .await?;
1513 let result = is_redis_populated(&app_state).await?;
1514 assert!(result, "Expected true after adding notification");
1515
1516 Ok(())
1517 }
1518
1519 fn create_test_server_config_with_settings(
1521 storage_type: RepositoryStorageType,
1522 reset_storage_on_start: bool,
1523 ) -> ServerConfig {
1524 ServerConfig {
1525 repository_storage_type: storage_type.clone(),
1526 reset_storage_on_start,
1527 ..create_test_server_config(storage_type)
1528 }
1529 }
1530
1531 fn create_minimal_test_config() -> Config {
1533 Config {
1534 signers: vec![SignerFileConfig {
1535 id: "test-signer-1".to_string(),
1536 config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
1537 path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
1538 passphrase: PlainOrEnvValue::Plain {
1539 value: SecretString::new("test"),
1540 },
1541 }),
1542 }],
1543 relayers: vec![RelayerFileConfig {
1544 id: "test-relayer-1".to_string(),
1545 network_type: ConfigFileNetworkType::Evm,
1546 signer_id: "test-signer-1".to_string(),
1547 name: "test-relayer-1".to_string(),
1548 network: "test-network".to_string(),
1549 paused: false,
1550 policies: None,
1551 notification_id: None,
1552 custom_rpc_urls: None,
1553 }],
1554 notifications: vec![NotificationConfig {
1555 id: "test-notification-1".to_string(),
1556 r#type: NotificationType::Webhook,
1557 url: "https://hooks.slack.com/test1".to_string(),
1558 signing_key: None,
1559 }],
1560 networks: NetworksFileConfig::new(vec![]).unwrap(),
1561 plugins: None,
1562 }
1563 }
1564
1565 #[tokio::test]
1566 async fn test_should_process_config_file_inmemory_storage() -> Result<()> {
1567 let config = create_minimal_test_config();
1568
1569 let server_config = Arc::new(create_test_server_config_with_settings(
1571 RepositoryStorageType::InMemory,
1572 false,
1573 ));
1574
1575 let app_state = ThinData(create_test_app_state());
1576 process_config_file(config.clone(), server_config.clone(), &app_state).await?;
1577
1578 let stored_relayers = app_state.relayer_repository.list_all().await?;
1579 assert_eq!(stored_relayers.len(), 1);
1580 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1581
1582 let server_config2 = Arc::new(create_test_server_config_with_settings(
1584 RepositoryStorageType::InMemory,
1585 true,
1586 ));
1587
1588 let app_state2 = ThinData(create_test_app_state());
1589 process_config_file(config.clone(), server_config2, &app_state2).await?;
1590
1591 let stored_relayers = app_state2.relayer_repository.list_all().await?;
1592 assert_eq!(stored_relayers.len(), 1);
1593 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1594
1595 Ok(())
1596 }
1597
1598 #[tokio::test]
1599 async fn test_should_process_config_file_redis_storage_empty_repositories() -> Result<()> {
1600 let config = create_minimal_test_config();
1601 let server_config = Arc::new(create_test_server_config_with_settings(
1602 RepositoryStorageType::Redis,
1603 false,
1604 ));
1605
1606 let app_state = ThinData(create_test_app_state());
1607 process_config_file(config, server_config, &app_state).await?;
1608
1609 let stored_relayers = app_state.relayer_repository.list_all().await?;
1610 assert_eq!(stored_relayers.len(), 1);
1611 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1612
1613 Ok(())
1614 }
1615
1616 #[tokio::test]
1617 async fn ai_process_config_file_skips_when_redis_populated() -> Result<()> {
1618 let config = create_minimal_test_config();
1619 let server_config = Arc::new(create_test_server_config_with_settings(
1620 RepositoryStorageType::Redis,
1621 false,
1622 ));
1623
1624 let app_state = ThinData(create_test_app_state());
1625
1626 app_state
1627 .relayer_repository
1628 .create(create_mock_relayer("existing-relayer".to_string(), false))
1629 .await?;
1630
1631 process_config_file(config, server_config, &app_state).await?;
1632
1633 let stored_relayers = app_state.relayer_repository.list_all().await?;
1634 assert_eq!(stored_relayers.len(), 1);
1635 assert_eq!(stored_relayers[0].id, "existing-relayer");
1636
1637 Ok(())
1638 }
1639
1640 #[tokio::test]
1641 async fn test_should_not_process_config_file_redis_storage_populated_repositories() -> Result<()>
1642 {
1643 let config = create_minimal_test_config();
1644 let server_config = Arc::new(create_test_server_config_with_settings(
1645 RepositoryStorageType::Redis,
1646 false,
1647 ));
1648
1649 let app_state1 = ThinData(create_test_app_state());
1651 let app_state2 = ThinData(create_test_app_state());
1652
1653 let existing_relayer1 = create_mock_relayer("existing-relayer".to_string(), false);
1655 let existing_relayer2 = create_mock_relayer("existing-relayer".to_string(), false);
1656 app_state1
1657 .relayer_repository
1658 .create(existing_relayer1)
1659 .await?;
1660 app_state2
1661 .relayer_repository
1662 .create(existing_relayer2)
1663 .await?;
1664
1665 assert!(app_state1.relayer_repository.has_entries().await?);
1667 assert!(!app_state1.signer_repository.has_entries().await?);
1668
1669 process_config_file(config, server_config, &app_state2).await?;
1671
1672 let relayer_from_config = app_state2
1673 .relayer_repository
1674 .get_by_id("test-relayer-1".to_string())
1675 .await;
1676 assert!(
1677 relayer_from_config.is_err(),
1678 "Relayer from config should not be found"
1679 );
1680
1681 let existing_relayer = app_state2
1682 .relayer_repository
1683 .get_by_id("existing-relayer".to_string())
1684 .await?;
1685 assert_eq!(existing_relayer.id, "existing-relayer");
1686
1687 Ok(())
1689 }
1690
1691 #[tokio::test]
1692 async fn test_should_process_config_file_redis_storage_with_reset_flag() -> Result<()> {
1693 let config = create_minimal_test_config();
1694 let server_config = Arc::new(create_test_server_config_with_settings(
1695 RepositoryStorageType::Redis,
1696 true, ));
1698
1699 let app_state = ThinData(create_test_app_state());
1700
1701 let existing_relayer = create_mock_relayer("existing-relayer".to_string(), false);
1703 let existing_signer = create_mock_signer();
1704 app_state
1705 .relayer_repository
1706 .create(existing_relayer)
1707 .await?;
1708 app_state.signer_repository.create(existing_signer).await?;
1709
1710 process_config_file(config, server_config, &app_state).await?;
1712
1713 let stored_relayer = app_state
1714 .relayer_repository
1715 .get_by_id("existing-relayer".to_string())
1716 .await;
1717 assert!(
1718 stored_relayer.is_err(),
1719 "Existing relayer should not be found"
1720 );
1721
1722 let stored_signer = app_state
1723 .signer_repository
1724 .get_by_id("existing-signer".to_string())
1725 .await;
1726 assert!(
1727 stored_signer.is_err(),
1728 "Existing signer should not be found"
1729 );
1730
1731 Ok(())
1732 }
1733}