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