openzeppelin_relayer/bootstrap/
config_processor.rs

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