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