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