openzeppelin_relayer/config/config_file/network/
file_loading.rs

1//! Network Configuration File Loading
2//!
3//! This module provides utilities for loading network configurations from JSON files
4//! and directories, supporting both single-file and directory-based configuration layouts.
5//!
6//! ## Key Features
7//!
8//! - **Flexible loading**: Single files or entire directories of JSON configuration files
9//! - **Automatic discovery**: Scans directories for `.json` files recursively
10//! - **Validation**: Pre-loading validation to ensure directory contains valid configurations
11//!
12//! ## Supported File Structure
13//!
14//! ```text
15//! networks/
16//! ├── evm.json          # {"networks": [...]}
17//! ├── solana.json       # {"networks": [...]}
18//! └── stellar.json      # {"networks": [...]}
19//! ```
20//!
21//! ## Loading Process
22//!
23//! ### Directory Loading
24//! 1. **Discovery**: Scans directory for `.json` files (non-recursive)
25//! 2. **Validation**: Checks each file for proper JSON structure
26//! 3. **Parsing**: Deserializes each file into network configurations
27//! 4. **Aggregation**: Combines all configurations into a single collection
28//! 5. **Error handling**: Provides detailed context for any failures
29//!
30//! ### File Format Requirements
31//! Each JSON file must contain a top-level `networks` array:
32//! ```json
33//! {
34//!   "networks": [
35//!     {
36//!       "type": "evm",
37//!       "network": "ethereum-mainnet",
38//!       "chain_id": 1,
39//!       "required_confirmations": 12,
40//!       "symbol": "ETH",
41//!       "rpc_urls": ["https://eth.llamarpc.com"]
42//!     }
43//!   ]
44//! }
45//! ```
46//!
47//! ### Error Handling
48//! - **File not found**: Directory or individual files don't exist
49//! - **Permission errors**: Insufficient permissions to read files
50//! - **JSON parse errors**: Malformed JSON with line/column information
51//! - **Structure validation**: Missing required fields or wrong data types
52//! - **Empty collections**: Directories with no valid configuration files
53
54use super::NetworkFileConfig;
55use crate::config::ConfigFileError;
56use serde::Deserialize;
57use std::fs;
58use std::path::Path;
59
60// Helper struct for JSON files in the directory
61#[derive(Deserialize, Debug, Clone)]
62struct DirectoryNetworkList {
63    networks: Vec<NetworkFileConfig>,
64}
65
66pub struct NetworkFileLoader;
67
68impl NetworkFileLoader {
69    /// Reads and aggregates network configurations from all JSON files in the specified directory.
70    ///
71    /// # Arguments
72    /// * `path` - A path reference to the directory containing network configuration files.
73    ///
74    /// # Returns
75    /// - `Ok(Vec<NetworkFileConfig>)` containing all network configurations loaded from the directory.
76    /// - `Err(ConfigFileError)` with detailed context about what went wrong.
77    pub fn load_networks_from_directory(
78        path: impl AsRef<Path>,
79    ) -> Result<Vec<NetworkFileConfig>, ConfigFileError> {
80        let path = path.as_ref();
81
82        if !path.exists() {
83            return Err(ConfigFileError::InvalidFormat(format!(
84                "Path '{}' does not exist",
85                path.display()
86            )));
87        }
88
89        if !path.is_dir() {
90            return Err(ConfigFileError::InvalidFormat(format!(
91                "Path '{}' is not a directory",
92                path.display()
93            )));
94        }
95
96        // Validate that the directory contains at least one JSON configuration file
97        Self::validate_directory_has_configs(path)?;
98
99        let mut aggregated_networks = Vec::new();
100
101        // Read directory entries with better error handling
102        let entries = fs::read_dir(path).map_err(|e| {
103            ConfigFileError::InvalidFormat(format!(
104                "Failed to read directory '{}': {}",
105                path.display(),
106                e
107            ))
108        })?;
109
110        for entry_result in entries {
111            let entry = entry_result.map_err(|e| {
112                ConfigFileError::InvalidFormat(format!(
113                    "Failed to read directory entry in '{}': {}",
114                    path.display(),
115                    e
116                ))
117            })?;
118
119            let file_path = entry.path();
120
121            // Only process JSON files, skip directories and other file types
122            if Self::is_json_file(&file_path) {
123                match Self::load_network_file(&file_path) {
124                    Ok(mut networks) => {
125                        aggregated_networks.append(&mut networks);
126                    }
127                    Err(e) => {
128                        // Provide context about which file failed
129                        return Err(ConfigFileError::InvalidFormat(format!(
130                            "Failed to load network configuration from file '{}': {}",
131                            file_path.display(),
132                            e
133                        )));
134                    }
135                }
136            }
137        }
138
139        Ok(aggregated_networks)
140    }
141
142    /// Loads a single network configuration file.
143    ///
144    /// # Arguments
145    /// * `file_path` - Path to the JSON file containing network configurations.
146    ///
147    /// # Returns
148    /// - `Ok(Vec<NetworkFileConfig>)` containing the networks from the file.
149    /// - `Err(ConfigFileError)` if the file cannot be read or parsed.
150    fn load_network_file(file_path: &Path) -> Result<Vec<NetworkFileConfig>, ConfigFileError> {
151        let file_content = fs::read_to_string(file_path)
152            .map_err(|e| ConfigFileError::InvalidFormat(format!("Failed to read file: {}", e)))?;
153
154        let dir_network_list: DirectoryNetworkList = serde_json::from_str(&file_content)
155            .map_err(|e| ConfigFileError::InvalidFormat(format!("Failed to parse JSON: {}", e)))?;
156
157        Ok(dir_network_list.networks)
158    }
159
160    /// Checks if a path represents a JSON file.
161    ///
162    /// # Arguments
163    /// * `path` - The path to check.
164    ///
165    /// # Returns
166    /// - `true` if the path is a file with a `.json` extension.
167    /// - `false` otherwise.
168    fn is_json_file(path: &Path) -> bool {
169        path.is_file()
170            && path
171                .extension()
172                .and_then(|ext| ext.to_str())
173                .map(|ext| ext.eq_ignore_ascii_case("json"))
174                .unwrap_or(false)
175    }
176
177    /// Validates that a directory contains at least one JSON file.
178    ///
179    /// # Arguments
180    /// * `path` - Path to the directory to validate.
181    ///
182    /// # Returns
183    /// - `Ok(())` if the directory contains at least one JSON file.
184    /// - `Err(ConfigFileError)` if no JSON files are found.
185    pub fn validate_directory_has_configs(path: impl AsRef<Path>) -> Result<(), ConfigFileError> {
186        let path = path.as_ref();
187
188        if !path.is_dir() {
189            return Err(ConfigFileError::InvalidFormat(format!(
190                "Path '{}' is not a directory",
191                path.display()
192            )));
193        }
194
195        let entries = fs::read_dir(path).map_err(|e| {
196            ConfigFileError::InvalidFormat(format!(
197                "Failed to read directory '{}': {}",
198                path.display(),
199                e
200            ))
201        })?;
202
203        let has_json_files = entries
204            .filter_map(|entry| entry.ok())
205            .any(|entry| Self::is_json_file(&entry.path()));
206
207        if !has_json_files {
208            return Err(ConfigFileError::InvalidFormat(format!(
209                "Directory '{}' contains no JSON configuration files",
210                path.display()
211            )));
212        }
213
214        Ok(())
215    }
216
217    /// Loads networks from either a list or a directory path.
218    ///
219    /// This method handles the polymorphic loading behavior where the source
220    /// can be either a direct list of networks or a path to a directory.
221    ///
222    /// # Arguments
223    /// * `source` - Either a vector of networks or a path string.
224    ///
225    /// # Returns
226    /// - `Ok(Vec<NetworkFileConfig>)` containing the loaded networks.
227    /// - `Err(ConfigFileError)` if loading fails.
228    pub fn load_from_source(
229        source: NetworksSource,
230    ) -> Result<Vec<NetworkFileConfig>, ConfigFileError> {
231        match source {
232            NetworksSource::List(networks) => Ok(networks),
233            NetworksSource::Path(path_str) => Self::load_networks_from_directory(&path_str),
234        }
235    }
236}
237
238/// Represents the source of network configurations for deserialization.
239#[derive(Debug, Clone)]
240pub enum NetworksSource {
241    List(Vec<NetworkFileConfig>),
242    Path(String),
243}
244
245impl Default for NetworksSource {
246    fn default() -> Self {
247        NetworksSource::Path("./config/networks".to_string())
248    }
249}
250
251impl<'de> serde::Deserialize<'de> for NetworksSource {
252    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
253    where
254        D: serde::Deserializer<'de>,
255    {
256        use serde::de;
257        use serde_json::Value;
258
259        // First try to deserialize as a generic Value to determine the type
260        let value = Value::deserialize(deserializer)?;
261
262        match value {
263            Value::Null => Ok(NetworksSource::default()),
264            Value::String(s) => {
265                if s.is_empty() {
266                    Ok(NetworksSource::default())
267                } else {
268                    Ok(NetworksSource::Path(s))
269                }
270            }
271            Value::Array(arr) => {
272                let networks: Vec<NetworkFileConfig> = serde_json::from_value(Value::Array(arr))
273                    .map_err(|e| {
274                        de::Error::custom(format!("Failed to deserialize network array: {}", e))
275                    })?;
276                Ok(NetworksSource::List(networks))
277            }
278            _ => Err(de::Error::custom("Expected an array, string, or null")),
279        }
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use crate::config::config_file::network::test_utils::*;
287    use serde_json::json;
288    use std::fs::{create_dir, File};
289    use std::os::unix::fs::PermissionsExt;
290    use tempfile::tempdir;
291
292    #[test]
293    fn test_load_from_single_file() {
294        let dir = tempdir().expect("Failed to create temp dir");
295        let network_data = create_valid_evm_network_json();
296        create_temp_file(&dir, "config1.json", &network_data.to_string());
297
298        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
299        assert!(result.is_ok());
300        let networks = result.unwrap();
301        assert_eq!(networks.len(), 1);
302        assert_eq!(networks[0].network_name(), "test-evm");
303    }
304
305    #[test]
306    fn test_load_from_multiple_files() {
307        let dir = tempdir().expect("Failed to create temp dir");
308        let evm_data = create_valid_evm_network_json();
309        let solana_data = create_valid_solana_network_json();
310
311        create_temp_file(&dir, "evm.json", &evm_data.to_string());
312        create_temp_file(&dir, "solana.json", &solana_data.to_string());
313
314        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
315
316        assert!(result.is_ok());
317        let networks = result.unwrap();
318        assert_eq!(networks.len(), 2);
319
320        let network_names: Vec<&str> = networks.iter().map(|n| n.network_name()).collect();
321        assert!(network_names.contains(&"test-evm"));
322        assert!(network_names.contains(&"test-solana"));
323    }
324
325    #[test]
326    fn test_load_from_directory_multiple_networks_per_file() {
327        let dir = tempdir().expect("Failed to create temp dir");
328
329        let multi_network_data = json!({
330            "networks": [
331                {
332                    "type": "evm",
333                    "network": "evm-1",
334                    "chain_id": 1,
335                    "rpc_urls": ["http://localhost:8545"],
336                    "symbol": "ETH"
337                },
338                {
339                    "type": "evm",
340                    "network": "evm-2",
341                    "chain_id": 2,
342                    "rpc_urls": ["http://localhost:8546"],
343                    "symbol": "ETH2"
344                }
345            ]
346        });
347
348        create_temp_file(&dir, "multi.json", &multi_network_data.to_string());
349
350        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
351
352        assert!(result.is_ok());
353        let networks = result.unwrap();
354        assert_eq!(networks.len(), 2);
355        assert_eq!(networks[0].network_name(), "evm-1");
356        assert_eq!(networks[1].network_name(), "evm-2");
357    }
358
359    #[test]
360    fn test_load_from_directory_with_mixed_file_types() {
361        let dir = tempdir().expect("Failed to create temp dir");
362
363        let network_data = create_valid_evm_network_json();
364        create_temp_file(&dir, "config.json", &network_data.to_string());
365        create_temp_file(&dir, "readme.txt", "This is not a JSON file");
366        create_temp_file(&dir, "config.yaml", "networks: []");
367
368        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
369
370        assert!(result.is_ok());
371        let networks = result.unwrap();
372        assert_eq!(networks.len(), 1);
373        assert_eq!(networks[0].network_name(), "test-evm");
374    }
375
376    #[test]
377    fn test_load_from_directory_with_subdirectories() {
378        let dir = tempdir().expect("Failed to create temp dir");
379
380        let network_data = create_valid_evm_network_json();
381        create_temp_file(&dir, "config.json", &network_data.to_string());
382
383        // Create a subdirectory - should be ignored
384        let subdir_path = dir.path().join("subdir");
385        create_dir(&subdir_path).expect("Failed to create subdirectory");
386
387        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
388
389        assert!(result.is_ok());
390        let networks = result.unwrap();
391        assert_eq!(networks.len(), 1);
392    }
393
394    #[test]
395    fn test_load_from_nonexistent_directory() {
396        let dir = tempdir().expect("Failed to create temp dir");
397        let non_existent_path = dir.path().join("non_existent");
398
399        let result = NetworkFileLoader::load_networks_from_directory(&non_existent_path);
400
401        assert!(result.is_err());
402        assert!(matches!(
403            result.unwrap_err(),
404            ConfigFileError::InvalidFormat(_)
405        ));
406    }
407
408    #[test]
409    fn test_load_from_file_instead_of_directory() {
410        let dir = tempdir().expect("Failed to create temp dir");
411        let file_path = dir.path().join("not_a_dir.json");
412        File::create(&file_path).expect("Failed to create file");
413
414        let result = NetworkFileLoader::load_networks_from_directory(&file_path);
415
416        assert!(result.is_err());
417        assert!(matches!(
418            result.unwrap_err(),
419            ConfigFileError::InvalidFormat(_)
420        ));
421    }
422
423    #[test]
424    fn test_load_from_directory_with_no_json_files() {
425        let dir = tempdir().expect("Failed to create temp dir");
426
427        create_temp_file(&dir, "readme.txt", "This is not a JSON file");
428        create_temp_file(&dir, "config.yaml", "networks: []");
429
430        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
431
432        assert!(result.is_err());
433        assert!(matches!(
434            result.unwrap_err(),
435            ConfigFileError::InvalidFormat(_)
436        ));
437    }
438
439    #[test]
440    fn test_load_from_directory_with_invalid_json() {
441        let dir = tempdir().expect("Failed to create temp dir");
442
443        create_temp_file(
444            &dir,
445            "invalid.json",
446            r#"{"networks": [{"type": "evm", "network": "broken""#,
447        );
448
449        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
450
451        assert!(result.is_err());
452        assert!(matches!(
453            result.unwrap_err(),
454            ConfigFileError::InvalidFormat(_)
455        ));
456    }
457
458    #[test]
459    fn test_load_from_directory_with_wrong_json_structure() {
460        let dir = tempdir().expect("Failed to create temp dir");
461
462        create_temp_file(&dir, "wrong.json", r#"{"foo": "bar"}"#);
463
464        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
465
466        assert!(result.is_err());
467        assert!(matches!(
468            result.unwrap_err(),
469            ConfigFileError::InvalidFormat(_)
470        ));
471    }
472
473    #[test]
474    fn test_load_from_directory_with_empty_networks_array() {
475        let dir = tempdir().expect("Failed to create temp dir");
476
477        create_temp_file(&dir, "empty.json", r#"{"networks": []}"#);
478
479        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
480
481        assert!(result.is_ok());
482        let networks = result.unwrap();
483        assert_eq!(networks.len(), 0);
484    }
485
486    #[test]
487    fn test_load_from_directory_with_invalid_network_structure() {
488        let dir = tempdir().expect("Failed to create temp dir");
489
490        let invalid_network = create_invalid_network_json();
491
492        create_temp_file(&dir, "invalid_network.json", &invalid_network.to_string());
493
494        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
495
496        assert!(result.is_err());
497        assert!(matches!(
498            result.unwrap_err(),
499            ConfigFileError::InvalidFormat(_)
500        ));
501    }
502
503    #[test]
504    fn test_load_from_directory_partial_failure() {
505        let dir = tempdir().expect("Failed to create temp dir");
506
507        let valid_data = create_valid_evm_network_json();
508        create_temp_file(&dir, "valid.json", &valid_data.to_string());
509        create_temp_file(&dir, "invalid.json", r#"{"networks": [malformed"#);
510
511        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
512
513        // Should fail completely if any file fails
514        assert!(result.is_err());
515        assert!(matches!(
516            result.unwrap_err(),
517            ConfigFileError::InvalidFormat(_)
518        ));
519    }
520
521    #[test]
522    fn test_is_json_file() {
523        let dir = tempdir().expect("Failed to create temp dir");
524
525        let json_file = dir.path().join("config.json");
526        File::create(&json_file).expect("Failed to create JSON file");
527        assert!(NetworkFileLoader::is_json_file(&json_file));
528
529        let txt_file = dir.path().join("config.txt");
530        File::create(&txt_file).expect("Failed to create TXT file");
531        assert!(!NetworkFileLoader::is_json_file(&txt_file));
532
533        let json_upper_file = dir.path().join("config.JSON");
534        File::create(&json_upper_file).expect("Failed to create JSON file");
535        assert!(NetworkFileLoader::is_json_file(&json_upper_file));
536
537        let no_extension_file = dir.path().join("config");
538        File::create(&no_extension_file).expect("Failed to create file without extension");
539        assert!(!NetworkFileLoader::is_json_file(&no_extension_file));
540
541        // Test with directory
542        let subdir = dir.path().join("subdir");
543        create_dir(&subdir).expect("Failed to create subdirectory");
544        assert!(!NetworkFileLoader::is_json_file(&subdir));
545    }
546
547    #[test]
548    fn test_validate_directory_has_configs() {
549        let dir = tempdir().expect("Failed to create temp dir");
550
551        // Empty directory should fail validation
552        let result = NetworkFileLoader::validate_directory_has_configs(dir.path());
553        assert!(result.is_err());
554        assert!(matches!(
555            result.unwrap_err(),
556            ConfigFileError::InvalidFormat(_)
557        ));
558
559        // Directory with non-JSON files should fail
560        create_temp_file(&dir, "readme.txt", "Not JSON");
561        let result = NetworkFileLoader::validate_directory_has_configs(dir.path());
562        assert!(result.is_err());
563
564        // Directory with JSON file should pass validation
565        create_temp_file(&dir, "config.json", r#"{"networks": []}"#);
566        let result = NetworkFileLoader::validate_directory_has_configs(dir.path());
567        assert!(result.is_ok());
568    }
569
570    #[test]
571    fn test_validate_directory_has_configs_with_file_path() {
572        let dir = tempdir().expect("Failed to create temp dir");
573        let file_path = dir.path().join("not_a_dir.json");
574        File::create(&file_path).expect("Failed to create file");
575
576        let result = NetworkFileLoader::validate_directory_has_configs(&file_path);
577
578        assert!(result.is_err());
579        assert!(matches!(
580            result.unwrap_err(),
581            ConfigFileError::InvalidFormat(_)
582        ));
583    }
584
585    #[test]
586    fn test_load_from_source_with_list() {
587        let networks = vec![]; // Empty list for simplicity
588        let source = NetworksSource::List(networks.clone());
589
590        let result = NetworkFileLoader::load_from_source(source);
591
592        assert!(result.is_ok());
593        assert_eq!(result.unwrap().len(), 0);
594    }
595
596    #[test]
597    fn test_load_from_source_with_path() {
598        let dir = tempdir().expect("Failed to create temp dir");
599        let network_data = create_valid_evm_network_json();
600        create_temp_file(&dir, "config.json", &network_data.to_string());
601
602        let path_str = dir
603            .path()
604            .to_str()
605            .expect("Path should be valid UTF-8")
606            .to_string();
607        let source = NetworksSource::Path(path_str);
608
609        let result = NetworkFileLoader::load_from_source(source);
610
611        assert!(result.is_ok());
612        let networks = result.unwrap();
613        assert_eq!(networks.len(), 1);
614        assert_eq!(networks[0].network_name(), "test-evm");
615    }
616
617    #[test]
618    fn test_load_from_source_with_invalid_path() {
619        let source = NetworksSource::Path("/non/existent/path".to_string());
620
621        let result = NetworkFileLoader::load_from_source(source);
622
623        assert!(result.is_err());
624        assert!(matches!(
625            result.unwrap_err(),
626            ConfigFileError::InvalidFormat(_)
627        ));
628    }
629
630    #[test]
631    fn test_load_from_directory_with_unicode_filenames() {
632        let dir = tempdir().expect("Failed to create temp dir");
633
634        let network_data = create_valid_evm_network_json();
635        create_temp_file(&dir, "配置.json", &network_data.to_string());
636        create_temp_file(&dir, "конфиг.json", &network_data.to_string());
637
638        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
639
640        assert!(result.is_ok());
641        let networks = result.unwrap();
642        assert_eq!(networks.len(), 2);
643    }
644
645    #[test]
646    fn test_load_from_directory_with_unicode_content() {
647        let dir = tempdir().expect("Failed to create temp dir");
648
649        let unicode_network = json!({
650            "networks": [
651                {
652                    "type": "evm",
653                    "network": "测试网络",
654                    "chain_id": 1,
655                    "rpc_urls": ["http://localhost:8545"],
656                    "symbol": "ETH"
657                }
658            ]
659        });
660
661        create_temp_file(&dir, "unicode.json", &unicode_network.to_string());
662
663        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
664
665        assert!(result.is_ok());
666        let networks = result.unwrap();
667        assert_eq!(networks.len(), 1);
668        assert_eq!(networks[0].network_name(), "测试网络");
669    }
670
671    #[test]
672    fn test_load_from_directory_with_json_extension_but_invalid_content() {
673        let dir = tempdir().expect("Failed to create temp dir");
674
675        create_temp_file(&dir, "fake.json", "This is not JSON content at all!");
676
677        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
678
679        assert!(result.is_err());
680        assert!(matches!(
681            result.unwrap_err(),
682            ConfigFileError::InvalidFormat(_)
683        ));
684    }
685
686    #[test]
687    fn test_load_from_directory_with_large_number_of_files() {
688        let dir = tempdir().expect("Failed to create temp dir");
689
690        // Create 100 JSON files
691        for i in 0..100 {
692            let network_data = json!({
693                "networks": [
694                    {
695                        "type": "evm",
696                        "network": format!("test-network-{}", i),
697                        "chain_id": i + 1,
698                        "rpc_urls": [format!("http://localhost:{}", 8545 + i)],
699                        "symbol": "ETH"
700                    }
701                ]
702            });
703            create_temp_file(
704                &dir,
705                &format!("config_{}.json", i),
706                &network_data.to_string(),
707            );
708        }
709
710        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
711
712        assert!(result.is_ok());
713        let networks = result.unwrap();
714        assert_eq!(networks.len(), 100);
715    }
716
717    #[test]
718    fn test_networks_source_deserialization() {
719        // Test deserializing as list
720        let list_json = r#"[{"type": "evm", "network": "test", "chain_id": 1, "rpc_urls": ["http://localhost:8545"], "symbol": "ETH", "required_confirmations": 1}]"#;
721        let source: NetworksSource =
722            serde_json::from_str(list_json).expect("Failed to deserialize list");
723
724        match source {
725            NetworksSource::List(networks) => {
726                assert_eq!(networks.len(), 1);
727                assert_eq!(networks[0].network_name(), "test");
728            }
729            NetworksSource::Path(_) => panic!("Expected List variant"),
730        }
731
732        // Test deserializing as path
733        let path_json = r#""/path/to/configs""#;
734        let source: NetworksSource =
735            serde_json::from_str(path_json).expect("Failed to deserialize path");
736
737        match source {
738            NetworksSource::Path(path) => {
739                assert_eq!(path, "/path/to/configs");
740            }
741            NetworksSource::List(_) => panic!("Expected Path variant"),
742        }
743    }
744
745    #[cfg(unix)]
746    #[test]
747    fn test_load_from_directory_with_permission_issues() {
748        let dir = tempdir().expect("Failed to create temp dir");
749        let network_data = create_valid_evm_network_json();
750        create_temp_file(&dir, "config.json", &network_data.to_string());
751
752        // Remove read permissions from the directory
753        let mut perms = std::fs::metadata(dir.path())
754            .expect("Failed to get metadata")
755            .permissions();
756        perms.set_mode(0o000);
757        std::fs::set_permissions(dir.path(), perms).expect("Failed to set permissions");
758
759        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
760
761        // Restore permissions for cleanup
762        let mut perms = std::fs::metadata(dir.path())
763            .expect("Failed to get metadata")
764            .permissions();
765        perms.set_mode(0o755);
766        std::fs::set_permissions(dir.path(), perms).expect("Failed to restore permissions");
767
768        assert!(result.is_err());
769        assert!(matches!(
770            result.unwrap_err(),
771            ConfigFileError::InvalidFormat(_)
772        ));
773    }
774
775    #[test]
776    fn test_validate_directory_has_configs_with_nonexistent_directory() {
777        let dir = tempdir().expect("Failed to create temp dir");
778        let non_existent_path = dir.path().join("non_existent");
779
780        let result = NetworkFileLoader::validate_directory_has_configs(&non_existent_path);
781
782        assert!(result.is_err());
783        assert!(matches!(
784            result.unwrap_err(),
785            ConfigFileError::InvalidFormat(_)
786        ));
787    }
788
789    #[test]
790    fn test_is_json_file_with_nonexistent_file() {
791        let dir = tempdir().expect("Failed to create temp dir");
792        let non_existent_file = dir.path().join("nonexistent.json");
793
794        // Should return false for nonexistent files since is_file() returns false
795        assert!(!NetworkFileLoader::is_json_file(&non_existent_file));
796    }
797
798    #[cfg(unix)]
799    #[test]
800    fn test_load_from_directory_with_file_permission_issues() {
801        let dir = tempdir().expect("Failed to create temp dir");
802        let network_data = create_valid_evm_network_json();
803        create_temp_file(&dir, "config.json", &network_data.to_string());
804
805        // Remove read permissions from the file (not the directory)
806        let file_path = dir.path().join("config.json");
807        let mut perms = std::fs::metadata(&file_path)
808            .expect("Failed to get file metadata")
809            .permissions();
810        perms.set_mode(0o000);
811        std::fs::set_permissions(&file_path, perms).expect("Failed to set file permissions");
812
813        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
814
815        assert!(result.is_err());
816        assert!(matches!(
817            result.unwrap_err(),
818            ConfigFileError::InvalidFormat(_)
819        ));
820    }
821
822    #[test]
823    fn test_load_from_directory_empty_directory() {
824        let dir = tempdir().expect("Failed to create temp dir");
825
826        // Empty directory (no files at all) should fail validation
827        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
828
829        assert!(result.is_err());
830        assert!(matches!(
831            result.unwrap_err(),
832            ConfigFileError::InvalidFormat(_)
833        ));
834    }
835
836    #[test]
837    fn test_load_from_directory_with_json_containing_extra_fields() {
838        let dir = tempdir().expect("Failed to create temp dir");
839
840        // JSON with extra fields in the network config should fail due to deny_unknown_fields
841        let network_with_extra_fields = json!({
842            "networks": [
843                {
844                    "type": "evm",
845                    "network": "test-with-extra",
846                    "chain_id": 1,
847                    "rpc_urls": ["http://localhost:8545"],
848                    "symbol": "ETH",
849                    "extra_field": "should_cause_error"
850                }
851            ]
852        });
853
854        create_temp_file(
855            &dir,
856            "extra_fields.json",
857            &network_with_extra_fields.to_string(),
858        );
859
860        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
861
862        // Should fail because EVM networks have deny_unknown_fields
863        assert!(result.is_err());
864        assert!(matches!(
865            result.unwrap_err(),
866            ConfigFileError::InvalidFormat(_)
867        ));
868    }
869
870    #[test]
871    fn test_load_from_directory_with_json_containing_extra_top_level_fields() {
872        let dir = tempdir().expect("Failed to create temp dir");
873
874        // JSON with extra fields at the top level should be ignored
875        let network_with_extra_top_level = json!({
876            "networks": [
877                {
878                    "type": "evm",
879                    "network": "test-with-extra-top",
880                    "chain_id": 1,
881                    "rpc_urls": ["http://localhost:8545"],
882                    "symbol": "ETH",
883                    "required_confirmations": 1
884                }
885            ],
886            "extra_top_level": "ignored",
887            "another_extra": 42
888        });
889
890        create_temp_file(
891            &dir,
892            "extra_top_level.json",
893            &network_with_extra_top_level.to_string(),
894        );
895
896        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
897
898        // Should succeed because extra top-level fields are ignored by DirectoryNetworkList
899        assert!(result.is_ok());
900        let networks = result.unwrap();
901        assert_eq!(networks.len(), 1);
902        assert_eq!(networks[0].network_name(), "test-with-extra-top");
903    }
904
905    #[test]
906    fn test_load_from_directory_with_very_large_json() {
907        let dir = tempdir().expect("Failed to create temp dir");
908
909        let mut networks_array = Vec::new();
910        for i in 0..1000 {
911            networks_array.push(json!({
912                "type": "evm",
913                "network": format!("large-test-{}", i),
914                "chain_id": i + 1,
915                "rpc_urls": [format!("http://localhost:{}", 8545 + i)],
916                "symbol": "ETH"
917            }));
918        }
919
920        let large_json = json!({
921            "networks": networks_array
922        });
923
924        create_temp_file(&dir, "large.json", &large_json.to_string());
925
926        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
927
928        assert!(result.is_ok());
929        let networks = result.unwrap();
930        assert_eq!(networks.len(), 1000);
931    }
932
933    #[test]
934    fn test_load_from_directory_with_deeply_nested_json() {
935        let dir = tempdir().expect("Failed to create temp dir");
936
937        let complex_network = json!({
938            "networks": [
939                {
940                    "type": "evm",
941                    "network": "complex-nested",
942                    "chain_id": 1,
943                    "rpc_urls": ["http://localhost:8545"],
944                    "symbol": "ETH",
945                    "tags": ["mainnet", "production", "high-security"]
946                }
947            ]
948        });
949
950        create_temp_file(&dir, "complex.json", &complex_network.to_string());
951
952        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
953
954        assert!(result.is_ok());
955        let networks = result.unwrap();
956        assert_eq!(networks.len(), 1);
957        assert_eq!(networks[0].network_name(), "complex-nested");
958    }
959
960    #[test]
961    fn test_load_from_directory_with_null_values() {
962        let dir = tempdir().expect("Failed to create temp dir");
963
964        // Test JSON with null values in optional fields
965        let network_with_nulls = json!({
966            "networks": [
967                {
968                    "type": "evm",
969                    "network": "test-nulls",
970                    "chain_id": 1,
971                    "rpc_urls": ["http://localhost:8545"],
972                    "symbol": "ETH",
973                    "tags": null,
974                    "features": null
975                }
976            ]
977        });
978
979        create_temp_file(&dir, "nulls.json", &network_with_nulls.to_string());
980
981        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
982
983        assert!(result.is_ok());
984        let networks = result.unwrap();
985        assert_eq!(networks.len(), 1);
986        assert_eq!(networks[0].network_name(), "test-nulls");
987    }
988
989    #[test]
990    fn test_load_from_directory_with_special_characters_in_content() {
991        let dir = tempdir().expect("Failed to create temp dir");
992
993        let special_chars_network = json!({
994            "networks": [
995                {
996                    "type": "evm",
997                    "network": "test-special-chars-\n\t\r\"\\",
998                    "chain_id": 1,
999                    "rpc_urls": ["http://localhost:8545"],
1000                    "symbol": "ETH"
1001                }
1002            ]
1003        });
1004
1005        create_temp_file(&dir, "special.json", &special_chars_network.to_string());
1006
1007        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
1008
1009        assert!(result.is_ok());
1010        let networks = result.unwrap();
1011        assert_eq!(networks.len(), 1);
1012        assert_eq!(networks[0].network_name(), "test-special-chars-\n\t\r\"\\");
1013    }
1014
1015    #[cfg(unix)]
1016    #[test]
1017    fn test_load_from_directory_with_symbolic_links() {
1018        let dir = tempdir().expect("Failed to create temp dir");
1019        let network_data = create_valid_evm_network_json();
1020
1021        create_temp_file(&dir, "regular.json", &network_data.to_string());
1022
1023        // Create a symbolic link to the JSON file
1024        let regular_path = dir.path().join("regular.json");
1025        let symlink_path = dir.path().join("symlink.json");
1026
1027        if std::os::unix::fs::symlink(&regular_path, &symlink_path).is_ok() {
1028            let result = NetworkFileLoader::load_networks_from_directory(dir.path());
1029
1030            assert!(result.is_ok());
1031            let networks = result.unwrap();
1032            // Should load both the regular file and the symlink (2 networks total)
1033            assert_eq!(networks.len(), 2);
1034        }
1035    }
1036
1037    #[test]
1038    fn test_load_from_source_with_list_containing_networks() {
1039        // Test load_from_source with actual network data in the list
1040        let evm_network_json = create_valid_evm_network_json();
1041        let networks: Vec<NetworkFileConfig> =
1042            serde_json::from_value(evm_network_json["networks"].clone())
1043                .expect("Failed to deserialize networks");
1044
1045        let source = NetworksSource::List(networks.clone());
1046        let result = NetworkFileLoader::load_from_source(source);
1047
1048        assert!(result.is_ok());
1049        let loaded_networks = result.unwrap();
1050        assert_eq!(loaded_networks.len(), 1);
1051        assert_eq!(loaded_networks[0].network_name(), "test-evm");
1052    }
1053
1054    #[test]
1055    fn test_directory_network_list_deserialization() {
1056        // Test DirectoryNetworkList deserialization directly
1057        let json_str = r#"{"networks": []}"#;
1058        let result: Result<DirectoryNetworkList, _> = serde_json::from_str(json_str);
1059        assert!(result.is_ok());
1060        assert_eq!(result.unwrap().networks.len(), 0);
1061
1062        // Test with invalid structure
1063        let invalid_json = r#"{"not_networks": []}"#;
1064        let result: Result<DirectoryNetworkList, _> = serde_json::from_str(invalid_json);
1065        assert!(result.is_err());
1066    }
1067
1068    #[test]
1069    fn test_networks_source_clone_and_debug() {
1070        // Test that NetworksSource implements Clone and Debug properly
1071        let source = NetworksSource::Path("/test/path".to_string());
1072        let cloned = source.clone();
1073
1074        match (source, cloned) {
1075            (NetworksSource::Path(path1), NetworksSource::Path(path2)) => {
1076                assert_eq!(path1, path2);
1077            }
1078            _ => panic!("Clone didn't preserve variant"),
1079        }
1080
1081        // Test Debug formatting
1082        let source = NetworksSource::List(vec![]);
1083        let debug_str = format!("{:?}", source);
1084        assert!(debug_str.contains("List"));
1085    }
1086
1087    #[test]
1088    fn test_is_json_file_edge_cases() {
1089        let dir = tempdir().expect("Failed to create temp dir");
1090
1091        // Test file with .json in the middle of the name but different extension
1092        let misleading_file = dir.path().join("config.json.backup");
1093        File::create(&misleading_file).expect("Failed to create misleading file");
1094        assert!(!NetworkFileLoader::is_json_file(&misleading_file));
1095
1096        // Test file with multiple dots
1097        let multi_dot_file = dir.path().join("config.test.json");
1098        File::create(&multi_dot_file).expect("Failed to create multi-dot file");
1099        assert!(NetworkFileLoader::is_json_file(&multi_dot_file));
1100
1101        // Test file with mixed case in middle
1102        let mixed_case_file = dir.path().join("config.Json");
1103        File::create(&mixed_case_file).expect("Failed to create mixed case file");
1104        assert!(NetworkFileLoader::is_json_file(&mixed_case_file));
1105    }
1106
1107    #[cfg(unix)]
1108    #[test]
1109    fn test_validate_directory_has_configs_with_permission_issues() {
1110        let dir = tempdir().expect("Failed to create temp dir");
1111        create_temp_file(&dir, "config.json", r#"{"networks": []}"#);
1112
1113        // Remove read permissions from the directory
1114        let mut perms = std::fs::metadata(dir.path())
1115            .expect("Failed to get metadata")
1116            .permissions();
1117        perms.set_mode(0o000);
1118        std::fs::set_permissions(dir.path(), perms).expect("Failed to set permissions");
1119
1120        let result = NetworkFileLoader::validate_directory_has_configs(dir.path());
1121
1122        assert!(result.is_err());
1123        assert!(matches!(
1124            result.unwrap_err(),
1125            ConfigFileError::InvalidFormat(_)
1126        ));
1127    }
1128
1129    #[test]
1130    fn test_networks_source_default() {
1131        let default_source = NetworksSource::default();
1132        match default_source {
1133            NetworksSource::Path(path) => {
1134                assert_eq!(path, "./config/networks");
1135            }
1136            _ => panic!("Default should be a Path variant"),
1137        }
1138    }
1139
1140    #[test]
1141    fn test_networks_source_deserialize_null() {
1142        let json = r#"null"#;
1143        let result: Result<NetworksSource, _> = serde_json::from_str(json);
1144        assert!(result.is_ok());
1145
1146        match result.unwrap() {
1147            NetworksSource::Path(path) => {
1148                assert_eq!(path, "./config/networks");
1149            }
1150            _ => panic!("Expected default Path variant"),
1151        }
1152    }
1153
1154    #[test]
1155    fn test_networks_source_deserialize_empty_string() {
1156        let json = r#""""#;
1157        let result: Result<NetworksSource, _> = serde_json::from_str(json);
1158        assert!(result.is_ok());
1159
1160        match result.unwrap() {
1161            NetworksSource::Path(path) => {
1162                assert_eq!(path, "./config/networks");
1163            }
1164            _ => panic!("Expected default Path variant"),
1165        }
1166    }
1167
1168    #[test]
1169    fn test_networks_source_deserialize_valid_path() {
1170        let json = r#""/custom/path""#;
1171        let result: Result<NetworksSource, _> = serde_json::from_str(json);
1172        assert!(result.is_ok());
1173
1174        match result.unwrap() {
1175            NetworksSource::Path(path) => {
1176                assert_eq!(path, "/custom/path");
1177            }
1178            _ => panic!("Expected Path variant"),
1179        }
1180    }
1181
1182    #[test]
1183    fn test_networks_source_deserialize_array() {
1184        let json = r#"[{"type": "evm", "network": "test", "chain_id": 1, "rpc_urls": ["http://localhost:8545"], "symbol": "ETH", "required_confirmations": 1}]"#;
1185        let result: Result<NetworksSource, _> = serde_json::from_str(json);
1186        assert!(result.is_ok());
1187
1188        match result.unwrap() {
1189            NetworksSource::List(networks) => {
1190                assert_eq!(networks.len(), 1);
1191                assert_eq!(networks[0].network_name(), "test");
1192            }
1193            _ => panic!("Expected List variant"),
1194        }
1195    }
1196
1197    #[test]
1198    fn test_networks_source_deserialize_invalid_type() {
1199        let json = r#"42"#;
1200        let result: Result<NetworksSource, _> = serde_json::from_str(json);
1201        assert!(result.is_err());
1202    }
1203}