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(&dir, &format!("config_{i}.json"), &network_data.to_string());
704        }
705
706        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
707
708        assert!(result.is_ok());
709        let networks = result.unwrap();
710        assert_eq!(networks.len(), 100);
711    }
712
713    #[test]
714    fn test_networks_source_deserialization() {
715        // Test deserializing as list
716        let list_json = r#"[{"type": "evm", "network": "test", "chain_id": 1, "rpc_urls": ["http://localhost:8545"], "symbol": "ETH", "required_confirmations": 1}]"#;
717        let source: NetworksSource =
718            serde_json::from_str(list_json).expect("Failed to deserialize list");
719
720        match source {
721            NetworksSource::List(networks) => {
722                assert_eq!(networks.len(), 1);
723                assert_eq!(networks[0].network_name(), "test");
724            }
725            NetworksSource::Path(_) => panic!("Expected List variant"),
726        }
727
728        // Test deserializing as path
729        let path_json = r#""/path/to/configs""#;
730        let source: NetworksSource =
731            serde_json::from_str(path_json).expect("Failed to deserialize path");
732
733        match source {
734            NetworksSource::Path(path) => {
735                assert_eq!(path, "/path/to/configs");
736            }
737            NetworksSource::List(_) => panic!("Expected Path variant"),
738        }
739    }
740
741    #[cfg(unix)]
742    #[test]
743    fn test_load_from_directory_with_permission_issues() {
744        let dir = tempdir().expect("Failed to create temp dir");
745        let network_data = create_valid_evm_network_json();
746        create_temp_file(&dir, "config.json", &network_data.to_string());
747
748        // Remove read permissions from the directory
749        let mut perms = std::fs::metadata(dir.path())
750            .expect("Failed to get metadata")
751            .permissions();
752        perms.set_mode(0o000);
753        std::fs::set_permissions(dir.path(), perms).expect("Failed to set permissions");
754
755        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
756
757        // Restore permissions for cleanup
758        let mut perms = std::fs::metadata(dir.path())
759            .expect("Failed to get metadata")
760            .permissions();
761        perms.set_mode(0o755);
762        std::fs::set_permissions(dir.path(), perms).expect("Failed to restore permissions");
763
764        assert!(result.is_err());
765        assert!(matches!(
766            result.unwrap_err(),
767            ConfigFileError::InvalidFormat(_)
768        ));
769    }
770
771    #[test]
772    fn test_validate_directory_has_configs_with_nonexistent_directory() {
773        let dir = tempdir().expect("Failed to create temp dir");
774        let non_existent_path = dir.path().join("non_existent");
775
776        let result = NetworkFileLoader::validate_directory_has_configs(&non_existent_path);
777
778        assert!(result.is_err());
779        assert!(matches!(
780            result.unwrap_err(),
781            ConfigFileError::InvalidFormat(_)
782        ));
783    }
784
785    #[test]
786    fn test_is_json_file_with_nonexistent_file() {
787        let dir = tempdir().expect("Failed to create temp dir");
788        let non_existent_file = dir.path().join("nonexistent.json");
789
790        // Should return false for nonexistent files since is_file() returns false
791        assert!(!NetworkFileLoader::is_json_file(&non_existent_file));
792    }
793
794    #[cfg(unix)]
795    #[test]
796    fn test_load_from_directory_with_file_permission_issues() {
797        let dir = tempdir().expect("Failed to create temp dir");
798        let network_data = create_valid_evm_network_json();
799        create_temp_file(&dir, "config.json", &network_data.to_string());
800
801        // Remove read permissions from the file (not the directory)
802        let file_path = dir.path().join("config.json");
803        let mut perms = std::fs::metadata(&file_path)
804            .expect("Failed to get file metadata")
805            .permissions();
806        perms.set_mode(0o000);
807        std::fs::set_permissions(&file_path, perms).expect("Failed to set file permissions");
808
809        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
810
811        assert!(result.is_err());
812        assert!(matches!(
813            result.unwrap_err(),
814            ConfigFileError::InvalidFormat(_)
815        ));
816    }
817
818    #[test]
819    fn test_load_from_directory_empty_directory() {
820        let dir = tempdir().expect("Failed to create temp dir");
821
822        // Empty directory (no files at all) should fail validation
823        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
824
825        assert!(result.is_err());
826        assert!(matches!(
827            result.unwrap_err(),
828            ConfigFileError::InvalidFormat(_)
829        ));
830    }
831
832    #[test]
833    fn test_load_from_directory_with_json_containing_extra_fields() {
834        let dir = tempdir().expect("Failed to create temp dir");
835
836        // JSON with extra fields in the network config should fail due to deny_unknown_fields
837        let network_with_extra_fields = json!({
838            "networks": [
839                {
840                    "type": "evm",
841                    "network": "test-with-extra",
842                    "chain_id": 1,
843                    "rpc_urls": ["http://localhost:8545"],
844                    "symbol": "ETH",
845                    "extra_field": "should_cause_error"
846                }
847            ]
848        });
849
850        create_temp_file(
851            &dir,
852            "extra_fields.json",
853            &network_with_extra_fields.to_string(),
854        );
855
856        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
857
858        // Should fail because EVM networks have deny_unknown_fields
859        assert!(result.is_err());
860        assert!(matches!(
861            result.unwrap_err(),
862            ConfigFileError::InvalidFormat(_)
863        ));
864    }
865
866    #[test]
867    fn test_load_from_directory_with_json_containing_extra_top_level_fields() {
868        let dir = tempdir().expect("Failed to create temp dir");
869
870        // JSON with extra fields at the top level should be ignored
871        let network_with_extra_top_level = json!({
872            "networks": [
873                {
874                    "type": "evm",
875                    "network": "test-with-extra-top",
876                    "chain_id": 1,
877                    "rpc_urls": ["http://localhost:8545"],
878                    "symbol": "ETH",
879                    "required_confirmations": 1
880                }
881            ],
882            "extra_top_level": "ignored",
883            "another_extra": 42
884        });
885
886        create_temp_file(
887            &dir,
888            "extra_top_level.json",
889            &network_with_extra_top_level.to_string(),
890        );
891
892        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
893
894        // Should succeed because extra top-level fields are ignored by DirectoryNetworkList
895        assert!(result.is_ok());
896        let networks = result.unwrap();
897        assert_eq!(networks.len(), 1);
898        assert_eq!(networks[0].network_name(), "test-with-extra-top");
899    }
900
901    #[test]
902    fn test_load_from_directory_with_very_large_json() {
903        let dir = tempdir().expect("Failed to create temp dir");
904
905        let mut networks_array = Vec::new();
906        for i in 0..1000 {
907            networks_array.push(json!({
908                "type": "evm",
909                "network": format!("large-test-{}", i),
910                "chain_id": i + 1,
911                "rpc_urls": [format!("http://localhost:{}", 8545 + i)],
912                "symbol": "ETH"
913            }));
914        }
915
916        let large_json = json!({
917            "networks": networks_array
918        });
919
920        create_temp_file(&dir, "large.json", &large_json.to_string());
921
922        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
923
924        assert!(result.is_ok());
925        let networks = result.unwrap();
926        assert_eq!(networks.len(), 1000);
927    }
928
929    #[test]
930    fn test_load_from_directory_with_deeply_nested_json() {
931        let dir = tempdir().expect("Failed to create temp dir");
932
933        let complex_network = json!({
934            "networks": [
935                {
936                    "type": "evm",
937                    "network": "complex-nested",
938                    "chain_id": 1,
939                    "rpc_urls": ["http://localhost:8545"],
940                    "symbol": "ETH",
941                    "tags": ["mainnet", "production", "high-security"]
942                }
943            ]
944        });
945
946        create_temp_file(&dir, "complex.json", &complex_network.to_string());
947
948        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
949
950        assert!(result.is_ok());
951        let networks = result.unwrap();
952        assert_eq!(networks.len(), 1);
953        assert_eq!(networks[0].network_name(), "complex-nested");
954    }
955
956    #[test]
957    fn test_load_from_directory_with_null_values() {
958        let dir = tempdir().expect("Failed to create temp dir");
959
960        // Test JSON with null values in optional fields
961        let network_with_nulls = json!({
962            "networks": [
963                {
964                    "type": "evm",
965                    "network": "test-nulls",
966                    "chain_id": 1,
967                    "rpc_urls": ["http://localhost:8545"],
968                    "symbol": "ETH",
969                    "tags": null,
970                    "features": null
971                }
972            ]
973        });
974
975        create_temp_file(&dir, "nulls.json", &network_with_nulls.to_string());
976
977        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
978
979        assert!(result.is_ok());
980        let networks = result.unwrap();
981        assert_eq!(networks.len(), 1);
982        assert_eq!(networks[0].network_name(), "test-nulls");
983    }
984
985    #[test]
986    fn test_load_from_directory_with_special_characters_in_content() {
987        let dir = tempdir().expect("Failed to create temp dir");
988
989        let special_chars_network = json!({
990            "networks": [
991                {
992                    "type": "evm",
993                    "network": "test-special-chars-\n\t\r\"\\",
994                    "chain_id": 1,
995                    "rpc_urls": ["http://localhost:8545"],
996                    "symbol": "ETH"
997                }
998            ]
999        });
1000
1001        create_temp_file(&dir, "special.json", &special_chars_network.to_string());
1002
1003        let result = NetworkFileLoader::load_networks_from_directory(dir.path());
1004
1005        assert!(result.is_ok());
1006        let networks = result.unwrap();
1007        assert_eq!(networks.len(), 1);
1008        assert_eq!(networks[0].network_name(), "test-special-chars-\n\t\r\"\\");
1009    }
1010
1011    #[cfg(unix)]
1012    #[test]
1013    fn test_load_from_directory_with_symbolic_links() {
1014        let dir = tempdir().expect("Failed to create temp dir");
1015        let network_data = create_valid_evm_network_json();
1016
1017        create_temp_file(&dir, "regular.json", &network_data.to_string());
1018
1019        // Create a symbolic link to the JSON file
1020        let regular_path = dir.path().join("regular.json");
1021        let symlink_path = dir.path().join("symlink.json");
1022
1023        if std::os::unix::fs::symlink(&regular_path, &symlink_path).is_ok() {
1024            let result = NetworkFileLoader::load_networks_from_directory(dir.path());
1025
1026            assert!(result.is_ok());
1027            let networks = result.unwrap();
1028            // Should load both the regular file and the symlink (2 networks total)
1029            assert_eq!(networks.len(), 2);
1030        }
1031    }
1032
1033    #[test]
1034    fn test_load_from_source_with_list_containing_networks() {
1035        // Test load_from_source with actual network data in the list
1036        let evm_network_json = create_valid_evm_network_json();
1037        let networks: Vec<NetworkFileConfig> =
1038            serde_json::from_value(evm_network_json["networks"].clone())
1039                .expect("Failed to deserialize networks");
1040
1041        let source = NetworksSource::List(networks.clone());
1042        let result = NetworkFileLoader::load_from_source(source);
1043
1044        assert!(result.is_ok());
1045        let loaded_networks = result.unwrap();
1046        assert_eq!(loaded_networks.len(), 1);
1047        assert_eq!(loaded_networks[0].network_name(), "test-evm");
1048    }
1049
1050    #[test]
1051    fn test_directory_network_list_deserialization() {
1052        // Test DirectoryNetworkList deserialization directly
1053        let json_str = r#"{"networks": []}"#;
1054        let result: Result<DirectoryNetworkList, _> = serde_json::from_str(json_str);
1055        assert!(result.is_ok());
1056        assert_eq!(result.unwrap().networks.len(), 0);
1057
1058        // Test with invalid structure
1059        let invalid_json = r#"{"not_networks": []}"#;
1060        let result: Result<DirectoryNetworkList, _> = serde_json::from_str(invalid_json);
1061        assert!(result.is_err());
1062    }
1063
1064    #[test]
1065    fn test_networks_source_clone_and_debug() {
1066        // Test that NetworksSource implements Clone and Debug properly
1067        let source = NetworksSource::Path("/test/path".to_string());
1068        let cloned = source.clone();
1069
1070        match (source, cloned) {
1071            (NetworksSource::Path(path1), NetworksSource::Path(path2)) => {
1072                assert_eq!(path1, path2);
1073            }
1074            _ => panic!("Clone didn't preserve variant"),
1075        }
1076
1077        // Test Debug formatting
1078        let source = NetworksSource::List(vec![]);
1079        let debug_str = format!("{source:?}");
1080        assert!(debug_str.contains("List"));
1081    }
1082
1083    #[test]
1084    fn test_is_json_file_edge_cases() {
1085        let dir = tempdir().expect("Failed to create temp dir");
1086
1087        // Test file with .json in the middle of the name but different extension
1088        let misleading_file = dir.path().join("config.json.backup");
1089        File::create(&misleading_file).expect("Failed to create misleading file");
1090        assert!(!NetworkFileLoader::is_json_file(&misleading_file));
1091
1092        // Test file with multiple dots
1093        let multi_dot_file = dir.path().join("config.test.json");
1094        File::create(&multi_dot_file).expect("Failed to create multi-dot file");
1095        assert!(NetworkFileLoader::is_json_file(&multi_dot_file));
1096
1097        // Test file with mixed case in middle
1098        let mixed_case_file = dir.path().join("config.Json");
1099        File::create(&mixed_case_file).expect("Failed to create mixed case file");
1100        assert!(NetworkFileLoader::is_json_file(&mixed_case_file));
1101    }
1102
1103    #[cfg(unix)]
1104    #[test]
1105    fn test_validate_directory_has_configs_with_permission_issues() {
1106        let dir = tempdir().expect("Failed to create temp dir");
1107        create_temp_file(&dir, "config.json", r#"{"networks": []}"#);
1108
1109        // Remove read permissions from the directory
1110        let mut perms = std::fs::metadata(dir.path())
1111            .expect("Failed to get metadata")
1112            .permissions();
1113        perms.set_mode(0o000);
1114        std::fs::set_permissions(dir.path(), perms).expect("Failed to set permissions");
1115
1116        let result = NetworkFileLoader::validate_directory_has_configs(dir.path());
1117
1118        assert!(result.is_err());
1119        assert!(matches!(
1120            result.unwrap_err(),
1121            ConfigFileError::InvalidFormat(_)
1122        ));
1123    }
1124
1125    #[test]
1126    fn test_networks_source_default() {
1127        let default_source = NetworksSource::default();
1128        match default_source {
1129            NetworksSource::Path(path) => {
1130                assert_eq!(path, "./config/networks");
1131            }
1132            _ => panic!("Default should be a Path variant"),
1133        }
1134    }
1135
1136    #[test]
1137    fn test_networks_source_deserialize_null() {
1138        let json = r#"null"#;
1139        let result: Result<NetworksSource, _> = serde_json::from_str(json);
1140        assert!(result.is_ok());
1141
1142        match result.unwrap() {
1143            NetworksSource::Path(path) => {
1144                assert_eq!(path, "./config/networks");
1145            }
1146            _ => panic!("Expected default Path variant"),
1147        }
1148    }
1149
1150    #[test]
1151    fn test_networks_source_deserialize_empty_string() {
1152        let json = r#""""#;
1153        let result: Result<NetworksSource, _> = serde_json::from_str(json);
1154        assert!(result.is_ok());
1155
1156        match result.unwrap() {
1157            NetworksSource::Path(path) => {
1158                assert_eq!(path, "./config/networks");
1159            }
1160            _ => panic!("Expected default Path variant"),
1161        }
1162    }
1163
1164    #[test]
1165    fn test_networks_source_deserialize_valid_path() {
1166        let json = r#""/custom/path""#;
1167        let result: Result<NetworksSource, _> = serde_json::from_str(json);
1168        assert!(result.is_ok());
1169
1170        match result.unwrap() {
1171            NetworksSource::Path(path) => {
1172                assert_eq!(path, "/custom/path");
1173            }
1174            _ => panic!("Expected Path variant"),
1175        }
1176    }
1177
1178    #[test]
1179    fn test_networks_source_deserialize_array() {
1180        let json = r#"[{"type": "evm", "network": "test", "chain_id": 1, "rpc_urls": ["http://localhost:8545"], "symbol": "ETH", "required_confirmations": 1}]"#;
1181        let result: Result<NetworksSource, _> = serde_json::from_str(json);
1182        assert!(result.is_ok());
1183
1184        match result.unwrap() {
1185            NetworksSource::List(networks) => {
1186                assert_eq!(networks.len(), 1);
1187                assert_eq!(networks[0].network_name(), "test");
1188            }
1189            _ => panic!("Expected List variant"),
1190        }
1191    }
1192
1193    #[test]
1194    fn test_networks_source_deserialize_invalid_type() {
1195        let json = r#"42"#;
1196        let result: Result<NetworksSource, _> = serde_json::from_str(json);
1197        assert!(result.is_err());
1198    }
1199}