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