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 plugins
332/// 2. Process signers
333/// 3. Process notifications
334/// 4. Process networks
335/// 5. Process relayers
336/// 6. Process API key
337pub 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        // Create a mock job producer
433        let mut mock_job_producer = MockJobProducerTrait::new();
434
435        // Set up expectations for the mock
436        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    // utility function to setup a mock AppRole login response
575    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        // Create test signers
697        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        // Create config
719        let config = Config {
720            signers,
721            relayers: vec![],
722            notifications: vec![],
723            networks: NetworksFileConfig::new(vec![]).unwrap(),
724            plugins: Some(vec![]),
725        };
726
727        // Create app state
728        let app_state = ThinData(create_test_app_state());
729
730        // Process signers
731        process_signers(&config, &app_state).await?;
732
733        // Verify signers were created
734        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        // Create test notifications
745        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        // Create config
761        let config = Config {
762            signers: vec![],
763            relayers: vec![],
764            notifications,
765            networks: NetworksFileConfig::new(vec![]).unwrap(),
766            plugins: Some(vec![]),
767        };
768
769        // Create app state
770        let app_state = ThinData(create_test_app_state());
771
772        // Process notifications
773        process_notifications(&config, &app_state).await?;
774
775        // Verify notifications were created
776        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        // Create test signers
1015        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        // Create test relayers
1026        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        // Create config
1039        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        // Create app state
1048        let app_state = ThinData(create_test_app_state());
1049
1050        // First process signers (required for relayers)
1051        process_signers(&config, &app_state).await?;
1052
1053        // Process relayers
1054        process_relayers(&config, &app_state).await?;
1055
1056        // Verify relayers were created
1057        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()); // Address should be populated
1062
1063        Ok(())
1064    }
1065
1066    #[tokio::test]
1067    async fn test_process_plugins() -> Result<()> {
1068        // Create test plugins
1069        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        // Create config
1095        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        // Create app state
1104        let app_state = ThinData(create_test_app_state());
1105
1106        // Process plugins
1107        process_plugins(&config, &app_state).await?;
1108
1109        // Verify plugins were created
1110        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        // check that the timeout is set to the default value when not provided.
1129        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        // Create test signers, relayers, and notifications
1168        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        // Create config
1210        let config = Config {
1211            signers,
1212            relayers,
1213            notifications,
1214            networks: NetworksFileConfig::new(vec![]).unwrap(),
1215            plugins: Some(plugins),
1216        };
1217
1218        // Create shared repositories
1219        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        // Create a mock job producer
1231        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        // Create app state
1247        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        // Process the entire config file
1260        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        // Verify all repositories were populated
1266        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        // Create fresh app state with all empty repositories
1334        let app_state = ThinData(create_test_app_state());
1335
1336        // All repositories should be empty
1337        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        // is_redis_populated should return false when all repositories are empty
1344        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        // Add a relayer to the repository
1355        let relayer = create_mock_relayer("test-relayer".to_string(), false);
1356        app_state.relayer_repository.create(relayer).await?;
1357
1358        // Verify relayer repository has entries
1359        assert!(app_state.relayer_repository.has_entries().await?);
1360
1361        // is_redis_populated should return true
1362        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        // Add a transaction to the repository
1373        let transaction = TransactionRepoModel::default();
1374        app_state.transaction_repository.create(transaction).await?;
1375
1376        // Verify transaction repository has entries
1377        assert!(app_state.transaction_repository.has_entries().await?);
1378
1379        // is_redis_populated should return true
1380        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        // Add a signer to the repository
1394        let signer = create_mock_signer();
1395        app_state.signer_repository.create(signer).await?;
1396
1397        // Verify signer repository has entries
1398        assert!(app_state.signer_repository.has_entries().await?);
1399
1400        // is_redis_populated should return true
1401        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        // Add a notification to the repository
1412        let notification = create_mock_notification("test-notification".to_string());
1413        app_state
1414            .notification_repository
1415            .create(notification)
1416            .await?;
1417
1418        // Verify notification repository has entries
1419        assert!(app_state.notification_repository.has_entries().await?);
1420
1421        // is_redis_populated should return true
1422        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        // Add a network to the repository
1436        let network = create_mock_network();
1437        app_state.network_repository.create(network).await?;
1438
1439        // Verify network repository has entries
1440        assert!(app_state.network_repository.has_entries().await?);
1441
1442        // is_redis_populated should return true
1443        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        // Add entries to multiple repositories
1454        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        // Verify multiple repositories have entries
1468        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        // is_redis_populated should return true
1474        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        // Test 1: Start with all empty repositories
1488        let result = is_redis_populated(&app_state).await?;
1489        assert!(!result, "Expected false when all repositories are empty");
1490
1491        // Test 2: Add entry to one repository
1492        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        // Test 3: Clear all repositories
1498        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        // Test 4: Add entries to different repositories and verify each time
1503        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    // Helper function to create test server config with specific settings
1520    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    // Helper function to create minimal test config
1532    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        // Test 1: InMemory storage with reset_storage_on_start = false
1570        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        // Test 2: InMemory storage with reset_storage_on_start = true
1583        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        // Create two identical app states to test the decision logic
1650        let app_state1 = ThinData(create_test_app_state());
1651        let app_state2 = ThinData(create_test_app_state());
1652
1653        // Pre-populate repositories to simulate Redis already having data
1654        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        // Check initial state
1666        assert!(app_state1.relayer_repository.has_entries().await?);
1667        assert!(!app_state1.signer_repository.has_entries().await?);
1668
1669        // Process config file - should NOT process because Redis is populated
1670        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        // The test passes if no errors occurred, which means the decision logic worked
1688        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, // reset_storage_on_start = true
1697        ));
1698
1699        let app_state = ThinData(create_test_app_state());
1700
1701        // Pre-populate repositories to simulate Redis already having data
1702        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        // Should process config file because reset_storage_on_start = true
1711        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}