openzeppelin_relayer/config/config_file/
mod.rs

1//! This module provides functionality for loading and validating configuration files
2//! for a blockchain relayer application. It includes definitions for configuration
3//! structures, error handling, and validation logic to ensure that the configuration
4//! is correct and complete before use.
5//!
6//! The module supports configuration for different network types, including EVM, Solana,
7//! and Stellar, and ensures that test signers are only used with test networks.
8//!
9//! # Modules
10//! - `relayer`: Handles relayer-specific configuration.
11//! - `signer`: Manages signer-specific configuration.
12//! - `notification`: Deals with notification-specific configuration.
13//! - `network`: Handles network configuration, including network overrides and custom networks.
14//!
15//! # Errors
16//! The module defines a comprehensive set of errors to handle various issues that might
17//! arise during configuration loading and validation, such as missing fields, invalid
18//! formats, and invalid references.
19//!
20//! # Usage
21//! To use this module, load a configuration file using `load_config`, which will parse
22//! the file and validate its contents. If the configuration is valid, it can be used
23//! to initialize the application components.
24use crate::{
25    config::ConfigFileError,
26    models::{
27        relayer::{RelayerFileConfig, RelayersFileConfig},
28        signer::{SignerFileConfig, SignersFileConfig},
29        NotificationConfig, NotificationConfigs,
30    },
31};
32use serde::{Deserialize, Serialize};
33use std::{
34    collections::HashSet,
35    fs::{self},
36};
37
38mod plugin;
39pub use plugin::*;
40
41pub mod network;
42pub use network::{
43    EvmNetworkConfig, GasPriceCacheConfig, NetworkConfigCommon, NetworkFileConfig,
44    NetworksFileConfig, SolanaNetworkConfig, StellarNetworkConfig,
45};
46
47#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
48#[serde(rename_all = "lowercase")]
49pub enum ConfigFileNetworkType {
50    Evm,
51    Stellar,
52    Solana,
53}
54
55#[derive(Debug, Serialize, Deserialize, Clone)]
56pub struct Config {
57    pub relayers: Vec<RelayerFileConfig>,
58    pub signers: Vec<SignerFileConfig>,
59    pub notifications: Vec<NotificationConfig>,
60    pub networks: NetworksFileConfig,
61    pub plugins: Option<Vec<PluginFileConfig>>,
62}
63
64impl Config {
65    /// Validates the configuration by checking the validity of relayers, signers, and
66    /// notifications.
67    ///
68    /// This method ensures that all references between relayers, signers, and notifications are
69    /// valid. It also checks that test signers are only used with test networks.
70    ///
71    /// # Errors
72    /// Returns a `ConfigFileError` if any validation checks fail.
73    pub fn validate(&self) -> Result<(), ConfigFileError> {
74        self.validate_networks()?;
75        self.validate_relayers(&self.networks)?;
76        self.validate_signers()?;
77        self.validate_notifications()?;
78        self.validate_plugins()?;
79
80        self.validate_relayer_signer_refs()?;
81        self.validate_relayer_notification_refs()?;
82
83        Ok(())
84    }
85
86    /// Validates that all relayer references to signers are valid.
87    ///
88    /// This method checks that each relayer references an existing signer and that test signers
89    /// are only used with test networks.
90    ///
91    /// # Errors
92    /// Returns a `ConfigFileError::InvalidReference` if a relayer references a non-existent signer.
93    /// Returns a `ConfigFileError::TestSigner` if a test signer is used on a production network.
94    fn validate_relayer_signer_refs(&self) -> Result<(), ConfigFileError> {
95        let signer_ids: HashSet<_> = self.signers.iter().map(|s| &s.id).collect();
96
97        for relayer in &self.relayers {
98            if !signer_ids.contains(&relayer.signer_id) {
99                return Err(ConfigFileError::InvalidReference(format!(
100                    "Relayer '{}' references non-existent signer '{}'",
101                    relayer.id, relayer.signer_id
102                )));
103            }
104        }
105
106        Ok(())
107    }
108
109    /// Validates that all relayer references to notifications are valid.
110    ///
111    /// This method checks that each relayer references an existing notification, if specified.
112    ///
113    /// # Errors
114    /// Returns a `ConfigFileError::InvalidReference` if a relayer references a non-existent
115    /// notification.
116    fn validate_relayer_notification_refs(&self) -> Result<(), ConfigFileError> {
117        let notification_ids: HashSet<_> = self.notifications.iter().map(|s| &s.id).collect();
118
119        for relayer in &self.relayers {
120            if let Some(notification_id) = &relayer.notification_id {
121                if !notification_ids.contains(notification_id) {
122                    return Err(ConfigFileError::InvalidReference(format!(
123                        "Relayer '{}' references non-existent notification '{}'",
124                        relayer.id, notification_id
125                    )));
126                }
127            }
128        }
129
130        Ok(())
131    }
132
133    /// Validates that all relayers are valid and have unique IDs.
134    fn validate_relayers(&self, networks: &NetworksFileConfig) -> Result<(), ConfigFileError> {
135        RelayersFileConfig::new(self.relayers.clone()).validate(networks)
136    }
137
138    /// Validates that all signers are valid and have unique IDs.
139    fn validate_signers(&self) -> Result<(), ConfigFileError> {
140        SignersFileConfig::new(self.signers.clone()).validate()
141    }
142
143    /// Validates that all notifications are valid and have unique IDs.
144    fn validate_notifications(&self) -> Result<(), ConfigFileError> {
145        NotificationConfigs::new(self.notifications.clone()).validate()
146    }
147
148    /// Validates that all networks are valid and have unique IDs.
149    fn validate_networks(&self) -> Result<(), ConfigFileError> {
150        if self.networks.is_empty() {
151            return Ok(()); // No networks to validate
152        }
153
154        self.networks.validate()
155    }
156
157    /// Validates that all plugins are valid and have unique IDs.
158    fn validate_plugins(&self) -> Result<(), ConfigFileError> {
159        if let Some(plugins) = &self.plugins {
160            PluginsFileConfig::new(plugins.clone()).validate()
161        } else {
162            Ok(())
163        }
164    }
165}
166
167/// Loads and validates a configuration file from the specified path.
168///
169/// This function reads the configuration file, parses it as JSON, and validates its contents.
170/// If the configuration is valid, it returns a `Config` object.
171///
172/// # Arguments
173/// * `config_file_path` - A string slice that holds the path to the configuration file.
174///
175/// # Errors
176/// Returns a `ConfigFileError` if the file cannot be read, parsed, or if the configuration is
177/// invalid.
178pub fn load_config(config_file_path: &str) -> Result<Config, ConfigFileError> {
179    let config_str = fs::read_to_string(config_file_path)?;
180    let config: Config = serde_json::from_str(&config_str)?;
181    config.validate()?;
182    Ok(config)
183}
184
185#[cfg(test)]
186mod tests {
187    use crate::models::{
188        signer::{LocalSignerFileConfig, SignerFileConfig, SignerFileConfigEnum},
189        NotificationType, PlainOrEnvValue, SecretString,
190    };
191    use std::path::Path;
192
193    use super::*;
194
195    fn create_valid_config() -> Config {
196        Config {
197            relayers: vec![RelayerFileConfig {
198                id: "test-1".to_string(),
199                name: "Test Relayer".to_string(),
200                network: "test-network".to_string(),
201                paused: false,
202                network_type: ConfigFileNetworkType::Evm,
203                policies: None,
204                signer_id: "test-1".to_string(),
205                notification_id: Some("test-1".to_string()),
206                custom_rpc_urls: None,
207            }],
208            signers: vec![SignerFileConfig {
209                id: "test-1".to_string(),
210                config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
211                    path: "tests/utils/test_keys/unit-test-local-signer.json".to_string(),
212                    passphrase: PlainOrEnvValue::Plain {
213                        value: SecretString::new("test"),
214                    },
215                }),
216            }],
217            notifications: vec![NotificationConfig {
218                id: "test-1".to_string(),
219                r#type: NotificationType::Webhook,
220                url: "https://api.example.com/notifications".to_string(),
221                signing_key: None,
222            }],
223            networks: NetworksFileConfig::new(vec![NetworkFileConfig::Evm(EvmNetworkConfig {
224                common: NetworkConfigCommon {
225                    network: "test-network".to_string(),
226                    from: None,
227                    rpc_urls: Some(vec!["https://rpc.test.example.com".to_string()]),
228                    explorer_urls: Some(vec!["https://explorer.test.example.com".to_string()]),
229                    average_blocktime_ms: Some(12000),
230                    is_testnet: Some(true),
231                    tags: Some(vec!["test".to_string()]),
232                },
233                chain_id: Some(31337),
234                required_confirmations: Some(1),
235                features: None,
236                symbol: Some("ETH".to_string()),
237                gas_price_cache: None,
238            })])
239            .expect("Failed to create NetworksFileConfig for test"),
240            plugins: Some(vec![PluginFileConfig {
241                id: "test-1".to_string(),
242                path: "/app/plugins/test-plugin.ts".to_string(),
243                timeout: None,
244                emit_logs: false,
245                emit_traces: false,
246            }]),
247        }
248    }
249
250    #[test]
251    fn test_valid_config_validation() {
252        let config = create_valid_config();
253        assert!(config.validate().is_ok());
254    }
255
256    #[test]
257    fn test_empty_relayers() {
258        let config = Config {
259            relayers: Vec::new(),
260            signers: Vec::new(),
261            notifications: Vec::new(),
262            networks: NetworksFileConfig::new(vec![NetworkFileConfig::Evm(EvmNetworkConfig {
263                common: NetworkConfigCommon {
264                    network: "test-network".to_string(),
265                    from: None,
266                    rpc_urls: Some(vec!["https://rpc.test.example.com".to_string()]),
267                    explorer_urls: Some(vec!["https://explorer.test.example.com".to_string()]),
268                    average_blocktime_ms: Some(12000),
269                    is_testnet: Some(true),
270                    tags: Some(vec!["test".to_string()]),
271                },
272                chain_id: Some(31337),
273                required_confirmations: Some(1),
274                features: None,
275                symbol: Some("ETH".to_string()),
276                gas_price_cache: None,
277            })])
278            .unwrap(),
279            plugins: Some(vec![]),
280        };
281        assert!(config.validate().is_ok());
282    }
283
284    #[test]
285    fn test_empty_signers() {
286        let config = Config {
287            relayers: Vec::new(),
288            signers: Vec::new(),
289            notifications: Vec::new(),
290            networks: NetworksFileConfig::new(vec![NetworkFileConfig::Evm(EvmNetworkConfig {
291                common: NetworkConfigCommon {
292                    network: "test-network".to_string(),
293                    from: None,
294                    rpc_urls: Some(vec!["https://rpc.test.example.com".to_string()]),
295                    explorer_urls: Some(vec!["https://explorer.test.example.com".to_string()]),
296                    average_blocktime_ms: Some(12000),
297                    is_testnet: Some(true),
298                    tags: Some(vec!["test".to_string()]),
299                },
300                chain_id: Some(31337),
301                required_confirmations: Some(1),
302                features: None,
303                symbol: Some("ETH".to_string()),
304                gas_price_cache: None,
305            })])
306            .unwrap(),
307            plugins: Some(vec![]),
308        };
309        assert!(config.validate().is_ok());
310    }
311
312    #[test]
313    fn test_invalid_id_format() {
314        let mut config = create_valid_config();
315        config.relayers[0].id = "invalid@id".to_string();
316        assert!(matches!(
317            config.validate(),
318            Err(ConfigFileError::InvalidIdFormat(_))
319        ));
320    }
321
322    #[test]
323    fn test_id_too_long() {
324        let mut config = create_valid_config();
325        config.relayers[0].id = "a".repeat(37);
326        assert!(matches!(
327            config.validate(),
328            Err(ConfigFileError::InvalidIdLength(_))
329        ));
330    }
331
332    #[test]
333    fn test_relayers_duplicate_ids() {
334        let mut config = create_valid_config();
335        config.relayers.push(config.relayers[0].clone());
336        assert!(matches!(
337            config.validate(),
338            Err(ConfigFileError::DuplicateId(_))
339        ));
340    }
341
342    #[test]
343    fn test_signers_duplicate_ids() {
344        let mut config = create_valid_config();
345        config.signers.push(config.signers[0].clone());
346
347        assert!(matches!(
348            config.validate(),
349            Err(ConfigFileError::DuplicateId(_))
350        ));
351    }
352
353    #[test]
354    fn test_missing_name() {
355        let mut config = create_valid_config();
356        config.relayers[0].name = "".to_string();
357        assert!(matches!(
358            config.validate(),
359            Err(ConfigFileError::MissingField(_))
360        ));
361    }
362
363    #[test]
364    fn test_missing_network() {
365        let mut config = create_valid_config();
366        config.relayers[0].network = "".to_string();
367        assert!(matches!(
368            config.validate(),
369            Err(ConfigFileError::InvalidFormat(_))
370        ));
371    }
372
373    #[test]
374    fn test_invalid_signer_id_reference() {
375        let mut config = create_valid_config();
376        config.relayers[0].signer_id = "invalid@id".to_string();
377        assert!(matches!(
378            config.validate(),
379            Err(ConfigFileError::InvalidReference(_))
380        ));
381    }
382
383    #[test]
384    fn test_invalid_notification_id_reference() {
385        let mut config = create_valid_config();
386        config.relayers[0].notification_id = Some("invalid@id".to_string());
387        assert!(matches!(
388            config.validate(),
389            Err(ConfigFileError::InvalidReference(_))
390        ));
391    }
392
393    #[test]
394    fn test_config_with_networks() {
395        let mut config = create_valid_config();
396        config.relayers[0].network = "custom-evm".to_string();
397
398        let network_items = vec![serde_json::from_value(serde_json::json!({
399            "type": "evm",
400            "network": "custom-evm",
401            "required_confirmations": 1,
402            "chain_id": 1234,
403            "rpc_urls": ["https://rpc.example.com"],
404            "symbol": "ETH"
405        }))
406        .unwrap()];
407        config.networks = NetworksFileConfig::new(network_items).unwrap();
408
409        assert!(
410            config.validate().is_ok(),
411            "Error validating config: {:?}",
412            config.validate().err()
413        );
414    }
415
416    #[test]
417    fn test_config_with_invalid_networks() {
418        let mut config = create_valid_config();
419        let network_items = vec![serde_json::from_value(serde_json::json!({
420            "type": "evm",
421            "network": "invalid-network",
422            "rpc_urls": ["https://rpc.example.com"]
423        }))
424        .unwrap()];
425        config.networks = NetworksFileConfig::new(network_items.clone())
426            .expect("Should allow creation, validation happens later or should fail here");
427
428        let result = config.validate();
429        assert!(result.is_err());
430        assert!(matches!(
431            result,
432            Err(ConfigFileError::MissingField(_)) | Err(ConfigFileError::InvalidFormat(_))
433        ));
434    }
435
436    #[test]
437    fn test_config_with_duplicate_network_names() {
438        let mut config = create_valid_config();
439        let network_items = vec![
440            serde_json::from_value(serde_json::json!({
441                "type": "evm",
442                "network": "custom-evm",
443                "chain_id": 1234,
444                "rpc_urls": ["https://rpc1.example.com"]
445            }))
446            .unwrap(),
447            serde_json::from_value(serde_json::json!({
448                "type": "evm",
449                "network": "custom-evm",
450                "chain_id": 5678,
451                "rpc_urls": ["https://rpc2.example.com"]
452            }))
453            .unwrap(),
454        ];
455        let networks_config_result = NetworksFileConfig::new(network_items);
456        assert!(
457            networks_config_result.is_err(),
458            "NetworksFileConfig::new should detect duplicate IDs"
459        );
460
461        if let Ok(parsed_networks) = networks_config_result {
462            config.networks = parsed_networks;
463            let result = config.validate();
464            assert!(result.is_err());
465            assert!(matches!(result, Err(ConfigFileError::DuplicateId(_))));
466        } else if let Err(e) = networks_config_result {
467            assert!(matches!(e, ConfigFileError::DuplicateId(_)));
468        }
469    }
470
471    #[test]
472    fn test_config_with_invalid_network_inheritance() {
473        let mut config = create_valid_config();
474        let network_items = vec![serde_json::from_value(serde_json::json!({
475            "type": "evm",
476            "network": "custom-evm",
477            "from": "non-existent-network",
478            "rpc_urls": ["https://rpc.example.com"]
479        }))
480        .unwrap()];
481        let networks_config_result = NetworksFileConfig::new(network_items);
482
483        match networks_config_result {
484            Ok(parsed_networks) => {
485                config.networks = parsed_networks;
486                let validation_result = config.validate();
487                assert!(
488                    validation_result.is_err(),
489                    "Validation should fail due to invalid inheritance reference"
490                );
491                assert!(matches!(
492                    validation_result,
493                    Err(ConfigFileError::InvalidReference(_))
494                ));
495            }
496            Err(e) => {
497                assert!(
498                    matches!(e, ConfigFileError::InvalidReference(_)),
499                    "Expected InvalidReference from new or flatten"
500                );
501            }
502        }
503    }
504
505    #[test]
506    fn test_deserialize_config_with_evm_network() {
507        let config_str = r#"
508        {
509            "relayers": [],
510            "signers": [],
511            "notifications": [],
512            "plugins": [],
513            "networks": [
514                {
515                    "type": "evm",
516                    "network": "custom-evm",
517                    "chain_id": 1234,
518                    "required_confirmations": 1,
519                    "symbol": "ETH",
520                    "rpc_urls": ["https://rpc.example.com"]
521                }
522            ]
523        }
524        "#;
525        let result: Result<Config, _> = serde_json::from_str(config_str);
526        assert!(result.is_ok());
527        let config = result.unwrap();
528        assert_eq!(config.networks.len(), 1);
529
530        let network_config = config.networks.first().expect("Should have one network");
531        assert!(matches!(network_config, NetworkFileConfig::Evm(_)));
532        if let NetworkFileConfig::Evm(evm_config) = network_config {
533            assert_eq!(evm_config.common.network, "custom-evm");
534            assert_eq!(evm_config.chain_id, Some(1234));
535        }
536    }
537
538    #[test]
539    fn test_deserialize_config_with_solana_network() {
540        let config_str = r#"
541        {
542            "relayers": [],
543            "signers": [],
544            "notifications": [],
545            "plugins": [],
546            "networks": [
547                {
548                    "type": "solana",
549                    "network": "custom-solana",
550                    "rpc_urls": ["https://rpc.solana.example.com"]
551                }
552            ]
553        }
554        "#;
555        let result: Result<Config, _> = serde_json::from_str(config_str);
556        assert!(result.is_ok());
557        let config = result.unwrap();
558        assert_eq!(config.networks.len(), 1);
559
560        let network_config = config.networks.first().expect("Should have one network");
561        assert!(matches!(network_config, NetworkFileConfig::Solana(_)));
562        if let NetworkFileConfig::Solana(sol_config) = network_config {
563            assert_eq!(sol_config.common.network, "custom-solana");
564        }
565    }
566
567    #[test]
568    fn test_deserialize_config_with_stellar_network() {
569        let config_str = r#"
570        {
571            "relayers": [],
572            "signers": [],
573            "notifications": [],
574            "plugins": [],
575            "networks": [
576                {
577                    "type": "stellar",
578                    "network": "custom-stellar",
579                    "rpc_urls": ["https://rpc.stellar.example.com"]
580                }
581            ]
582        }
583        "#;
584        let result: Result<Config, _> = serde_json::from_str(config_str);
585        assert!(result.is_ok());
586        let config = result.unwrap();
587        assert_eq!(config.networks.len(), 1);
588
589        let network_config = config.networks.first().expect("Should have one network");
590        assert!(matches!(network_config, NetworkFileConfig::Stellar(_)));
591        if let NetworkFileConfig::Stellar(stl_config) = network_config {
592            assert_eq!(stl_config.common.network, "custom-stellar");
593        }
594    }
595
596    #[test]
597    fn test_deserialize_config_with_mixed_networks() {
598        let config_str = r#"
599        {
600            "relayers": [],
601            "signers": [],
602            "notifications": [],
603            "plugins": [],
604            "networks": [
605                {
606                    "type": "evm",
607                    "network": "custom-evm",
608                    "chain_id": 1234,
609                    "required_confirmations": 1,
610                    "symbol": "ETH",
611                    "rpc_urls": ["https://rpc.example.com"]
612                },
613                {
614                    "type": "solana",
615                    "network": "custom-solana",
616                    "rpc_urls": ["https://rpc.solana.example.com"]
617                }
618            ]
619        }
620        "#;
621        let result: Result<Config, _> = serde_json::from_str(config_str);
622        assert!(result.is_ok());
623        let config = result.unwrap();
624        assert_eq!(config.networks.len(), 2);
625    }
626
627    #[test]
628    #[should_panic(
629        expected = "NetworksFileConfig cannot be empty - networks must contain at least one network configuration"
630    )]
631    fn test_deserialize_config_with_empty_networks_array() {
632        let config_str = r#"
633        {
634            "relayers": [],
635            "signers": [],
636            "notifications": [],
637            "networks": []
638        }
639        "#;
640        let _result: Config = serde_json::from_str(config_str).unwrap();
641    }
642
643    #[test]
644    fn test_deserialize_config_without_networks_field() {
645        let config_str = r#"
646        {
647            "relayers": [],
648            "signers": [],
649            "notifications": []
650        }
651        "#;
652        let result: Result<Config, _> = serde_json::from_str(config_str);
653        assert!(result.is_ok());
654    }
655
656    use std::fs::File;
657    use std::io::Write;
658    use tempfile::tempdir;
659
660    fn setup_network_file(dir_path: &Path, file_name: &str, content: &str) {
661        let file_path = dir_path.join(file_name);
662        let mut file = File::create(&file_path).expect("Failed to create temp network file");
663        writeln!(file, "{}", content).expect("Failed to write to temp network file");
664    }
665
666    #[test]
667    fn test_deserialize_config_with_networks_from_directory() {
668        let dir = tempdir().expect("Failed to create temp dir");
669        let network_dir_path = dir.path();
670
671        setup_network_file(
672            network_dir_path,
673            "evm_net.json",
674            r#"{"networks": [{"type": "evm", "network": "custom-evm-file", "required_confirmations": 1, "symbol": "ETH", "chain_id": 5678, "rpc_urls": ["https://rpc.file-evm.com"]}]}"#,
675        );
676        setup_network_file(
677            network_dir_path,
678            "sol_net.json",
679            r#"{"networks": [{"type": "solana", "network": "custom-solana-file", "rpc_urls": ["https://rpc.file-solana.com"]}]}"#,
680        );
681
682        let config_json = serde_json::json!({
683            "relayers": [],
684            "signers": [],
685            "notifications": [],
686            "plugins": [],
687            "networks": network_dir_path.to_str().expect("Path should be valid UTF-8")
688        });
689        let config_str =
690            serde_json::to_string(&config_json).expect("Failed to serialize test config to string");
691
692        let result: Result<Config, _> = serde_json::from_str(&config_str);
693        assert!(result.is_ok(), "Deserialization failed: {:?}", result.err());
694
695        if let Ok(config) = result {
696            assert_eq!(
697                config.networks.len(),
698                2,
699                "Incorrect number of networks loaded"
700            );
701            let has_evm = config.networks.iter().any(|n| matches!(n, NetworkFileConfig::Evm(evm) if evm.common.network == "custom-evm-file"));
702            let has_solana = config.networks.iter().any(|n| matches!(n, NetworkFileConfig::Solana(sol) if sol.common.network == "custom-solana-file"));
703            assert!(has_evm, "EVM network from file not found or incorrect");
704            assert!(
705                has_solana,
706                "Solana network from file not found or incorrect"
707            );
708        }
709    }
710
711    #[test]
712    fn test_deserialize_config_with_empty_networks_directory() {
713        let dir = tempdir().expect("Failed to create temp dir");
714        let network_dir_path = dir.path();
715
716        let config_json = serde_json::json!({
717            "relayers": [],
718            "signers": [],
719            "notifications": [],
720            "networks": network_dir_path.to_str().expect("Path should be valid UTF-8")
721        });
722        let config_str =
723            serde_json::to_string(&config_json).expect("Failed to serialize test config to string");
724
725        let result: Result<Config, _> = serde_json::from_str(&config_str);
726        assert!(
727            result.is_err(),
728            "Deserialization should fail for empty directory"
729        );
730    }
731
732    #[test]
733    fn test_deserialize_config_with_non_existent_networks_directory() {
734        let dir = tempdir().expect("Failed to create temp dir");
735        let non_existent_path = dir.path().join("non_existent_sub_dir");
736
737        let config_json = serde_json::json!({
738            "relayers": [],
739            "signers": [],
740            "notifications": [],
741            "networks": non_existent_path.to_str().expect("Path should be valid UTF-8")
742        });
743        let config_str =
744            serde_json::to_string(&config_json).expect("Failed to serialize test config to string");
745
746        let result: Result<Config, _> = serde_json::from_str(&config_str);
747        assert!(
748            result.is_err(),
749            "Deserialization should fail for non-existent directory"
750        );
751    }
752
753    #[test]
754    fn test_deserialize_config_with_networks_path_as_file() {
755        let dir = tempdir().expect("Failed to create temp dir");
756        let network_file_path = dir.path().join("im_a_file.json");
757        File::create(&network_file_path).expect("Failed to create temp file");
758
759        let config_json = serde_json::json!({
760            "relayers": [],
761            "signers": [],
762            "notifications": [],
763            "networks": network_file_path.to_str().expect("Path should be valid UTF-8")
764        });
765        let config_str =
766            serde_json::to_string(&config_json).expect("Failed to serialize test config to string");
767
768        let result: Result<Config, _> = serde_json::from_str(&config_str);
769        assert!(
770            result.is_err(),
771            "Deserialization should fail if path is a file, not a directory"
772        );
773    }
774
775    #[test]
776    fn test_deserialize_config_network_dir_with_invalid_json_file() {
777        let dir = tempdir().expect("Failed to create temp dir");
778        let network_dir_path = dir.path();
779        setup_network_file(
780            network_dir_path,
781            "invalid.json",
782            r#"{"networks": [{"type": "evm", "network": "broken""#,
783        ); // Malformed JSON
784
785        let config_json = serde_json::json!({
786            "relayers": [], "signers": [], "notifications": [],
787            "networks": network_dir_path.to_str().expect("Path should be valid UTF-8")
788        });
789        let config_str = serde_json::to_string(&config_json).expect("Failed to serialize");
790
791        let result: Result<Config, _> = serde_json::from_str(&config_str);
792        assert!(
793            result.is_err(),
794            "Deserialization should fail with invalid JSON in network file"
795        );
796    }
797
798    #[test]
799    fn test_deserialize_config_network_dir_with_non_network_config_json_file() {
800        let dir = tempdir().expect("Failed to create temp dir");
801        let network_dir_path = dir.path();
802        setup_network_file(network_dir_path, "not_a_network.json", r#"{"foo": "bar"}"#); // Valid JSON, but not NetworkFileConfig
803
804        let config_json = serde_json::json!({
805            "relayers": [], "signers": [], "notifications": [],
806            "networks": network_dir_path.to_str().expect("Path should be valid UTF-8")
807        });
808        let config_str = serde_json::to_string(&config_json).expect("Failed to serialize");
809
810        let result: Result<Config, _> = serde_json::from_str(&config_str);
811        assert!(
812            result.is_err(),
813            "Deserialization should fail if file is not a valid NetworkFileConfig"
814        );
815    }
816
817    #[test]
818    fn test_deserialize_config_still_works_with_array_of_networks() {
819        let config_str = r#"
820        {
821            "relayers": [],
822            "signers": [],
823            "notifications": [],
824            "plugins": [],
825            "networks": [
826                {
827                    "type": "evm",
828                    "network": "custom-evm-array",
829                    "chain_id": 1234,
830                    "required_confirmations": 1,
831                    "symbol": "ETH",
832                    "rpc_urls": ["https://rpc.example.com"]
833                }
834            ]
835        }
836        "#;
837        let result: Result<Config, _> = serde_json::from_str(config_str);
838        assert!(
839            result.is_ok(),
840            "Deserialization with array failed: {:?}",
841            result.err()
842        );
843        if let Ok(config) = result {
844            assert_eq!(config.networks.len(), 1);
845
846            let network_config = config.networks.first().expect("Should have one network");
847            assert!(matches!(network_config, NetworkFileConfig::Evm(_)));
848            if let NetworkFileConfig::Evm(evm_config) = network_config {
849                assert_eq!(evm_config.common.network, "custom-evm-array");
850            }
851        }
852    }
853
854    #[test]
855    fn test_create_valid_networks_file_config_works() {
856        let networks = vec![NetworkFileConfig::Evm(EvmNetworkConfig {
857            common: NetworkConfigCommon {
858                network: "test-network".to_string(),
859                from: None,
860                rpc_urls: Some(vec!["https://rpc.test.example.com".to_string()]),
861                explorer_urls: Some(vec!["https://explorer.test.example.com".to_string()]),
862                average_blocktime_ms: Some(12000),
863                is_testnet: Some(true),
864                tags: Some(vec!["test".to_string()]),
865            },
866            chain_id: Some(31337),
867            required_confirmations: Some(1),
868            features: None,
869            symbol: Some("ETH".to_string()),
870            gas_price_cache: None,
871        })];
872
873        let config = NetworksFileConfig::new(networks).unwrap();
874        assert_eq!(config.len(), 1);
875        assert_eq!(config.first().unwrap().network_name(), "test-network");
876    }
877
878    fn setup_config_file(dir_path: &Path, file_name: &str, content: &str) {
879        let file_path = dir_path.join(file_name);
880        let mut file = File::create(&file_path).expect("Failed to create temp config file");
881        write!(file, "{}", content).expect("Failed to write to temp config file");
882    }
883
884    #[test]
885    fn test_load_config_success() {
886        let dir = tempdir().expect("Failed to create temp dir");
887        let config_path = dir.path().join("valid_config.json");
888
889        let config_content = serde_json::json!({
890            "relayers": [{
891                "id": "test-relayer",
892                "name": "Test Relayer",
893                "network": "test-network",
894                "paused": false,
895                "network_type": "evm",
896                "signer_id": "test-signer"
897            }],
898            "signers": [{
899                "id": "test-signer",
900                "type": "local",
901                "config": {
902                    "path": "tests/utils/test_keys/unit-test-local-signer.json",
903                    "passphrase": {
904                        "value": "test",
905                        "type": "plain"
906                    }
907                }
908            }],
909            "notifications": [{
910                "id": "test-notification",
911                "type": "webhook",
912                "url": "https://api.example.com/notifications"
913            }],
914            "networks": [{
915                "type": "evm",
916                "network": "test-network",
917                "chain_id": 31337,
918                "required_confirmations": 1,
919                "symbol": "ETH",
920                "rpc_urls": ["https://rpc.test.example.com"],
921                "is_testnet": true
922            }],
923            "plugins": [{
924                "id": "plugin-id",
925                "path": "/app/plugins/plugin.ts",
926                "timeout": 12
927            }],
928        });
929
930        setup_config_file(dir.path(), "valid_config.json", &config_content.to_string());
931
932        let result = load_config(config_path.to_str().unwrap());
933        assert!(result.is_ok());
934
935        let config = result.unwrap();
936        assert_eq!(config.relayers.len(), 1);
937        assert_eq!(config.signers.len(), 1);
938        assert_eq!(config.networks.len(), 1);
939        assert_eq!(config.plugins.unwrap().len(), 1);
940    }
941
942    #[test]
943    fn test_load_config_file_not_found() {
944        let result = load_config("non_existent_file.json");
945        assert!(result.is_err());
946        assert!(matches!(result.unwrap_err(), ConfigFileError::IoError(_)));
947    }
948
949    #[test]
950    fn test_load_config_invalid_json() {
951        let dir = tempdir().expect("Failed to create temp dir");
952        let config_path = dir.path().join("invalid.json");
953
954        setup_config_file(dir.path(), "invalid.json", "{ invalid json }");
955
956        let result = load_config(config_path.to_str().unwrap());
957        assert!(result.is_err());
958        assert!(matches!(result.unwrap_err(), ConfigFileError::JsonError(_)));
959    }
960
961    #[test]
962    fn test_load_config_invalid_config_structure() {
963        let dir = tempdir().expect("Failed to create temp dir");
964        let config_path = dir.path().join("invalid_structure.json");
965
966        let invalid_config = serde_json::json!({
967            "relayers": "not_an_array",
968            "signers": [],
969            "notifications": [],
970            "networks": [{
971                "type": "evm",
972                "network": "test-network",
973                "chain_id": 31337,
974                "required_confirmations": 1,
975                "symbol": "ETH",
976                "rpc_urls": ["https://rpc.test.example.com"]
977            }]
978        });
979
980        setup_config_file(
981            dir.path(),
982            "invalid_structure.json",
983            &invalid_config.to_string(),
984        );
985
986        let result = load_config(config_path.to_str().unwrap());
987        assert!(result.is_err());
988        assert!(matches!(result.unwrap_err(), ConfigFileError::JsonError(_)));
989    }
990
991    #[test]
992    fn test_load_config_with_unicode_content() {
993        let dir = tempdir().expect("Failed to create temp dir");
994        let config_path = dir.path().join("unicode_config.json");
995
996        // Use ASCII-compatible IDs since the validation might reject Unicode in IDs
997        let config_content = serde_json::json!({
998            "relayers": [{
999                "id": "test-relayer-unicode",
1000                "name": "Test Relayer 测试",
1001                "network": "test-network-unicode",
1002                "paused": false,
1003                "network_type": "evm",
1004                "signer_id": "test-signer-unicode"
1005            }],
1006            "signers": [{
1007                "id": "test-signer-unicode",
1008                "type": "local",
1009                "config": {
1010                    "path": "tests/utils/test_keys/unit-test-local-signer.json",
1011                    "passphrase": {
1012                        "value": "test",
1013                        "type": "plain"
1014                    }
1015                }
1016            }],
1017            "notifications": [{
1018                "id": "test-notification-unicode",
1019                "type": "webhook",
1020                "url": "https://api.example.com/notifications"
1021            }],
1022            "networks": [{
1023                "type": "evm",
1024                "network": "test-network-unicode",
1025                "chain_id": 31337,
1026                "required_confirmations": 1,
1027                "symbol": "ETH",
1028                "rpc_urls": ["https://rpc.test.example.com"],
1029                "is_testnet": true
1030            }],
1031            "plugins": []
1032        });
1033
1034        setup_config_file(
1035            dir.path(),
1036            "unicode_config.json",
1037            &config_content.to_string(),
1038        );
1039
1040        let result = load_config(config_path.to_str().unwrap());
1041        assert!(result.is_ok());
1042
1043        let config = result.unwrap();
1044        assert_eq!(config.relayers[0].id, "test-relayer-unicode");
1045        assert_eq!(config.signers[0].id, "test-signer-unicode");
1046    }
1047
1048    #[test]
1049    fn test_load_config_with_empty_file() {
1050        let dir = tempdir().expect("Failed to create temp dir");
1051        let config_path = dir.path().join("empty.json");
1052
1053        setup_config_file(dir.path(), "empty.json", "");
1054
1055        let result = load_config(config_path.to_str().unwrap());
1056        assert!(result.is_err());
1057        assert!(matches!(result.unwrap_err(), ConfigFileError::JsonError(_)));
1058    }
1059
1060    #[test]
1061    fn test_config_serialization_works() {
1062        let config = create_valid_config();
1063
1064        let serialized = serde_json::to_string(&config);
1065        assert!(serialized.is_ok());
1066
1067        // Just test that serialization works, not round-trip due to complex serde structure
1068        let serialized_str = serialized.unwrap();
1069        assert!(!serialized_str.is_empty());
1070        assert!(serialized_str.contains("relayers"));
1071        assert!(serialized_str.contains("signers"));
1072        assert!(serialized_str.contains("networks"));
1073    }
1074
1075    #[test]
1076    fn test_config_serialization_contains_expected_fields() {
1077        let config = create_valid_config();
1078
1079        let serialized = serde_json::to_string(&config);
1080        assert!(serialized.is_ok());
1081
1082        let serialized_str = serialized.unwrap();
1083
1084        // Check that important fields are present in serialized JSON
1085        assert!(serialized_str.contains("\"id\":\"test-1\""));
1086        assert!(serialized_str.contains("\"name\":\"Test Relayer\""));
1087        assert!(serialized_str.contains("\"network\":\"test-network\""));
1088        assert!(serialized_str.contains("\"type\":\"evm\""));
1089    }
1090
1091    #[test]
1092    fn test_validate_relayers_method() {
1093        let config = create_valid_config();
1094        let result = config.validate_relayers(&config.networks);
1095        assert!(result.is_ok());
1096    }
1097
1098    #[test]
1099    fn test_validate_signers_method() {
1100        let config = create_valid_config();
1101        let result = config.validate_signers();
1102        assert!(result.is_ok());
1103    }
1104
1105    #[test]
1106    fn test_validate_notifications_method() {
1107        let config = create_valid_config();
1108        let result = config.validate_notifications();
1109        assert!(result.is_ok());
1110    }
1111
1112    #[test]
1113    fn test_validate_networks_method() {
1114        let config = create_valid_config();
1115        let result = config.validate_networks();
1116        assert!(result.is_ok());
1117    }
1118
1119    #[test]
1120    fn test_validate_plugins_method() {
1121        let config = create_valid_config();
1122        let result = config.validate_plugins();
1123        assert!(result.is_ok());
1124    }
1125
1126    #[test]
1127    fn test_validate_plugins_method_with_empty_plugins() {
1128        let config = Config {
1129            relayers: vec![],
1130            signers: vec![],
1131            notifications: vec![],
1132            networks: NetworksFileConfig::new(vec![]).unwrap(),
1133            plugins: Some(vec![]),
1134        };
1135        let result = config.validate_plugins();
1136        assert!(result.is_ok());
1137    }
1138
1139    #[test]
1140    fn test_validate_plugins_method_with_invalid_plugin_extension() {
1141        let config = Config {
1142            relayers: vec![],
1143            signers: vec![],
1144            notifications: vec![],
1145            networks: NetworksFileConfig::new(vec![]).unwrap(),
1146            plugins: Some(vec![PluginFileConfig {
1147                id: "id".to_string(),
1148                path: "/app/plugins/test-plugin.js".to_string(),
1149                timeout: None,
1150                emit_logs: false,
1151                emit_traces: false,
1152            }]),
1153        };
1154        let result = config.validate_plugins();
1155        assert!(result.is_err());
1156    }
1157
1158    #[test]
1159    fn test_config_with_maximum_length_ids() {
1160        let mut config = create_valid_config();
1161        let max_length_id = "a".repeat(36); // Maximum allowed length
1162        config.relayers[0].id = max_length_id.clone();
1163        config.relayers[0].signer_id = config.signers[0].id.clone();
1164
1165        let result = config.validate();
1166        assert!(result.is_ok());
1167    }
1168
1169    #[test]
1170    fn test_config_with_special_characters_in_names() {
1171        let mut config = create_valid_config();
1172        config.relayers[0].name = "Test-Relayer_123!@#$%^&*()".to_string();
1173
1174        let result = config.validate();
1175        assert!(result.is_ok());
1176    }
1177
1178    #[test]
1179    fn test_config_with_very_long_urls() {
1180        let mut config = create_valid_config();
1181        let long_url = format!(
1182            "https://very-long-domain-name-{}.example.com/api/v1/endpoint",
1183            "x".repeat(100)
1184        );
1185        config.notifications[0].url = long_url;
1186
1187        let result = config.validate();
1188        assert!(result.is_ok());
1189    }
1190
1191    #[test]
1192    fn test_config_with_only_signers_validation() {
1193        let config = Config {
1194            relayers: vec![],
1195            signers: vec![SignerFileConfig {
1196                id: "test-signer".to_string(),
1197                config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
1198                    path: "test-path".to_string(),
1199                    passphrase: PlainOrEnvValue::Plain {
1200                        value: SecretString::new("test-passphrase"),
1201                    },
1202                }),
1203            }],
1204            notifications: vec![],
1205            networks: NetworksFileConfig::new(vec![NetworkFileConfig::Evm(EvmNetworkConfig {
1206                common: NetworkConfigCommon {
1207                    network: "test-network".to_string(),
1208                    from: None,
1209                    rpc_urls: Some(vec!["https://rpc.test.example.com".to_string()]),
1210                    explorer_urls: Some(vec!["https://explorer.test.example.com".to_string()]),
1211                    average_blocktime_ms: Some(12000),
1212                    is_testnet: Some(true),
1213                    tags: Some(vec!["test".to_string()]),
1214                },
1215                chain_id: Some(31337),
1216                required_confirmations: Some(1),
1217                features: None,
1218                symbol: Some("ETH".to_string()),
1219                gas_price_cache: None,
1220            })])
1221            .unwrap(),
1222            plugins: Some(vec![]),
1223        };
1224
1225        let result = config.validate();
1226        assert!(result.is_ok());
1227    }
1228
1229    #[test]
1230    fn test_config_with_only_notifications() {
1231        let config = Config {
1232            relayers: vec![],
1233            signers: vec![],
1234            notifications: vec![NotificationConfig {
1235                id: "test-notification".to_string(),
1236                r#type: NotificationType::Webhook,
1237                url: "https://api.example.com/notifications".to_string(),
1238                signing_key: None,
1239            }],
1240            networks: NetworksFileConfig::new(vec![NetworkFileConfig::Evm(EvmNetworkConfig {
1241                common: NetworkConfigCommon {
1242                    network: "test-network".to_string(),
1243                    from: None,
1244                    rpc_urls: Some(vec!["https://rpc.test.example.com".to_string()]),
1245                    explorer_urls: Some(vec!["https://explorer.test.example.com".to_string()]),
1246                    average_blocktime_ms: Some(12000),
1247                    is_testnet: Some(true),
1248                    tags: Some(vec!["test".to_string()]),
1249                },
1250                chain_id: Some(31337),
1251                required_confirmations: Some(1),
1252                features: None,
1253                symbol: Some("ETH".to_string()),
1254                gas_price_cache: None,
1255            })])
1256            .unwrap(),
1257            plugins: Some(vec![]),
1258        };
1259
1260        let result = config.validate();
1261        assert!(result.is_ok());
1262    }
1263
1264    #[test]
1265    fn test_config_with_mixed_network_types_in_relayers() {
1266        let mut config = create_valid_config();
1267
1268        // Add Solana relayer
1269        config.relayers.push(RelayerFileConfig {
1270            id: "solana-relayer".to_string(),
1271            name: "Solana Test Relayer".to_string(),
1272            network: "devnet".to_string(),
1273            paused: false,
1274            network_type: ConfigFileNetworkType::Solana,
1275            policies: None,
1276            signer_id: "test-1".to_string(),
1277            notification_id: None,
1278            custom_rpc_urls: None,
1279        });
1280
1281        // Add Stellar relayer
1282        config.relayers.push(RelayerFileConfig {
1283            id: "stellar-relayer".to_string(),
1284            name: "Stellar Test Relayer".to_string(),
1285            network: "testnet".to_string(),
1286            paused: true,
1287            network_type: ConfigFileNetworkType::Stellar,
1288            policies: None,
1289            signer_id: "test-1".to_string(),
1290            notification_id: Some("test-1".to_string()),
1291            custom_rpc_urls: None,
1292        });
1293
1294        let devnet_network = NetworkFileConfig::Solana(SolanaNetworkConfig {
1295            common: NetworkConfigCommon {
1296                network: "devnet".to_string(),
1297                from: None,
1298                rpc_urls: Some(vec!["https://api.devnet.solana.com".to_string()]),
1299                explorer_urls: Some(vec!["https://explorer.solana.com".to_string()]),
1300                average_blocktime_ms: Some(400),
1301                is_testnet: Some(true),
1302                tags: Some(vec!["test".to_string()]),
1303            },
1304        });
1305
1306        let testnet_network = NetworkFileConfig::Stellar(StellarNetworkConfig {
1307            common: NetworkConfigCommon {
1308                network: "testnet".to_string(),
1309                from: None,
1310                rpc_urls: Some(vec!["https://soroban-testnet.stellar.org".to_string()]),
1311                explorer_urls: Some(vec!["https://stellar.expert/explorer/testnet".to_string()]),
1312                average_blocktime_ms: Some(5000),
1313                is_testnet: Some(true),
1314                tags: Some(vec!["test".to_string()]),
1315            },
1316            passphrase: Some("Test SDF Network ; September 2015".to_string()),
1317        });
1318
1319        let mut networks = config.networks.networks;
1320        networks.push(devnet_network);
1321        networks.push(testnet_network);
1322        config.networks =
1323            NetworksFileConfig::new(networks).expect("Failed to create NetworksFileConfig");
1324
1325        let result = config.validate();
1326        assert!(result.is_ok());
1327    }
1328
1329    #[test]
1330    fn test_config_with_all_network_types() {
1331        let mut config = create_valid_config();
1332
1333        // Add Solana network
1334        let solana_network = NetworkFileConfig::Solana(SolanaNetworkConfig {
1335            common: NetworkConfigCommon {
1336                network: "solana-test".to_string(),
1337                from: None,
1338                rpc_urls: Some(vec!["https://api.devnet.solana.com".to_string()]),
1339                explorer_urls: Some(vec!["https://explorer.test.example.com".to_string()]),
1340                average_blocktime_ms: Some(400),
1341                is_testnet: Some(true),
1342                tags: Some(vec!["solana".to_string()]),
1343            },
1344        });
1345
1346        // Add Stellar network
1347        let stellar_network = NetworkFileConfig::Stellar(StellarNetworkConfig {
1348            common: NetworkConfigCommon {
1349                network: "stellar-test".to_string(),
1350                from: None,
1351                rpc_urls: Some(vec!["https://horizon-testnet.stellar.org".to_string()]),
1352                explorer_urls: Some(vec!["https://explorer.test.example.com".to_string()]),
1353                average_blocktime_ms: Some(5000),
1354                is_testnet: Some(true),
1355                tags: Some(vec!["stellar".to_string()]),
1356            },
1357            passphrase: Some("Test Network ; September 2015".to_string()),
1358        });
1359
1360        // Get the existing networks and add new ones
1361        let mut existing_networks = Vec::new();
1362        for network in config.networks.iter() {
1363            existing_networks.push(network.clone());
1364        }
1365        existing_networks.push(solana_network);
1366        existing_networks.push(stellar_network);
1367
1368        config.networks = NetworksFileConfig::new(existing_networks).unwrap();
1369
1370        let result = config.validate();
1371        assert!(result.is_ok());
1372    }
1373
1374    #[test]
1375    fn test_config_error_propagation_from_relayers() {
1376        let mut config = create_valid_config();
1377        config.relayers[0].id = "".to_string(); // Invalid empty ID
1378
1379        let result = config.validate();
1380        assert!(result.is_err());
1381        assert!(matches!(
1382            result.unwrap_err(),
1383            ConfigFileError::MissingField(_)
1384        ));
1385    }
1386
1387    #[test]
1388    fn test_config_error_propagation_from_signers() {
1389        let mut config = create_valid_config();
1390        config.signers[0].id = "".to_string(); // Invalid empty ID
1391
1392        let result = config.validate();
1393        assert!(result.is_err());
1394        // The error should be InvalidIdLength since empty ID is caught by signer validation
1395        assert!(matches!(
1396            result.unwrap_err(),
1397            ConfigFileError::InvalidIdLength(_)
1398        ));
1399    }
1400
1401    #[test]
1402    fn test_config_error_propagation_from_notifications() {
1403        let mut config = create_valid_config();
1404        config.notifications[0].id = "".to_string(); // Invalid empty ID
1405
1406        let result = config.validate();
1407        assert!(result.is_err());
1408
1409        let error = result.unwrap_err();
1410        assert!(matches!(error, ConfigFileError::InvalidFormat(_)));
1411    }
1412
1413    #[test]
1414    fn test_config_with_paused_relayers() {
1415        let mut config = create_valid_config();
1416        config.relayers[0].paused = true;
1417
1418        let result = config.validate();
1419        assert!(result.is_ok()); // Paused relayers should still be valid
1420    }
1421
1422    #[test]
1423    fn test_config_with_none_notification_id() {
1424        let mut config = create_valid_config();
1425        config.relayers[0].notification_id = None;
1426
1427        let result = config.validate();
1428        assert!(result.is_ok()); // None notification_id should be valid
1429    }
1430
1431    #[test]
1432    fn test_config_file_network_type_display() {
1433        let evm = ConfigFileNetworkType::Evm;
1434        let solana = ConfigFileNetworkType::Solana;
1435        let stellar = ConfigFileNetworkType::Stellar;
1436
1437        // Test that Debug formatting works (which is what we have)
1438        let evm_str = format!("{:?}", evm);
1439        let solana_str = format!("{:?}", solana);
1440        let stellar_str = format!("{:?}", stellar);
1441
1442        assert!(evm_str.contains("Evm"));
1443        assert!(solana_str.contains("Solana"));
1444        assert!(stellar_str.contains("Stellar"));
1445    }
1446
1447    #[test]
1448    fn test_config_file_plugins_validation_with_empty_plugins() {
1449        let config = Config {
1450            relayers: vec![],
1451            signers: vec![],
1452            notifications: vec![],
1453            networks: NetworksFileConfig::new(vec![]).unwrap(),
1454            plugins: None,
1455        };
1456        let result = config.validate_plugins();
1457        assert!(result.is_ok());
1458    }
1459
1460    #[test]
1461    fn test_config_file_without_plugins() {
1462        let dir = tempdir().expect("Failed to create temp dir");
1463        let config_path = dir.path().join("valid_config.json");
1464
1465        let config_content = serde_json::json!({
1466            "relayers": [{
1467                "id": "test-relayer",
1468                "name": "Test Relayer",
1469                "network": "test-network",
1470                "paused": false,
1471                "network_type": "evm",
1472                "signer_id": "test-signer"
1473            }],
1474            "signers": [{
1475                "id": "test-signer",
1476                "type": "local",
1477                "config": {
1478                    "path": "tests/utils/test_keys/unit-test-local-signer.json",
1479                    "passphrase": {
1480                        "value": "test",
1481                        "type": "plain"
1482                    }
1483                }
1484            }],
1485            "notifications": [{
1486                "id": "test-notification",
1487                "type": "webhook",
1488                "url": "https://api.example.com/notifications"
1489            }],
1490            "networks": [{
1491                "type": "evm",
1492                "network": "test-network",
1493                "chain_id": 31337,
1494                "required_confirmations": 1,
1495                "symbol": "ETH",
1496                "rpc_urls": ["https://rpc.test.example.com"],
1497                "is_testnet": true
1498            }]
1499        });
1500
1501        setup_config_file(dir.path(), "valid_config.json", &config_content.to_string());
1502
1503        let result = load_config(config_path.to_str().unwrap());
1504        assert!(result.is_ok());
1505
1506        let config = result.unwrap();
1507        assert_eq!(config.relayers.len(), 1);
1508        assert_eq!(config.signers.len(), 1);
1509        assert_eq!(config.networks.len(), 1);
1510        assert!(config.plugins.is_none());
1511    }
1512}