openzeppelin_relayer/config/config_file/network/
common.rs

1//! Common Network Configuration Components
2//!
3//! This module defines shared configuration structures and utilities common across
4//! all network types (EVM, Solana, Stellar) with inheritance and merging support.
5//!
6//! ## Key Features
7//!
8//! - **Inheritance support**: Child networks inherit from parents with override capability
9//! - **Smart merging**: Collections merge preserving unique items, primitives override
10//! - **Validation**: Required field checks and URL format validation
11
12use crate::config::{ConfigFileError, ServerConfig};
13use crate::models::{deserialize_rpc_urls, RpcConfig};
14use crate::utils::{sanitize_url_for_error, validate_safe_url};
15use serde::{Deserialize, Deserializer, Serialize};
16
17#[derive(Debug, Serialize, Clone)]
18pub struct NetworkConfigCommon {
19    /// Unique network identifier (e.g., "mainnet", "sepolia", "custom-devnet").
20    pub network: String,
21    /// Optional name of an existing network to inherit configuration from.
22    /// If set, this network will use the `from` network's settings as a base,
23    /// overriding specific fields as needed.
24    pub from: Option<String>,
25    /// List of RPC endpoint configurations for connecting to the network.
26    /// Supports both simple format (array of strings) and extended format (array of RpcConfig objects).
27    #[serde(deserialize_with = "deserialize_rpc_urls")]
28    pub rpc_urls: Option<Vec<RpcConfig>>,
29    /// List of Explorer endpoint URLs for connecting to the network.
30    pub explorer_urls: Option<Vec<String>>,
31    /// Estimated average time between blocks in milliseconds.
32    pub average_blocktime_ms: Option<u64>,
33    /// Flag indicating if the network is a testnet.
34    pub is_testnet: Option<bool>,
35    /// List of arbitrary tags for categorizing or filtering networks.
36    pub tags: Option<Vec<String>>,
37}
38
39impl<'de> Deserialize<'de> for NetworkConfigCommon {
40    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41    where
42        D: Deserializer<'de>,
43    {
44        #[derive(Deserialize)]
45        struct NetworkConfigCommonHelper {
46            network: String,
47            from: Option<String>,
48            #[serde(deserialize_with = "deserialize_rpc_urls")]
49            rpc_urls: Option<Vec<RpcConfig>>,
50            explorer_urls: Option<Vec<String>>,
51            average_blocktime_ms: Option<u64>,
52            is_testnet: Option<bool>,
53            tags: Option<Vec<String>>,
54        }
55
56        let helper = NetworkConfigCommonHelper::deserialize(deserializer)?;
57        Ok(NetworkConfigCommon {
58            network: helper.network,
59            from: helper.from,
60            rpc_urls: helper.rpc_urls,
61            explorer_urls: helper.explorer_urls,
62            average_blocktime_ms: helper.average_blocktime_ms,
63            is_testnet: helper.is_testnet,
64            tags: helper.tags,
65        })
66    }
67}
68
69impl NetworkConfigCommon {
70    /// Validates the common fields for a network configuration.
71    ///
72    /// # Returns
73    /// - `Ok(())` if common fields are valid.
74    /// - `Err(ConfigFileError)` if validation fails.
75    pub fn validate(&self) -> Result<(), ConfigFileError> {
76        // Validate network name
77        if self.network.is_empty() {
78            return Err(ConfigFileError::MissingField("network name".into()));
79        }
80
81        // If this is a base network (not inheriting), validate required fields
82        if self.from.is_none() {
83            // RPC URLs are required for base networks
84            if self.rpc_urls.is_none() || self.rpc_urls.as_ref().unwrap().is_empty() {
85                return Err(ConfigFileError::MissingField("rpc_urls".into()));
86            }
87        }
88
89        // Validate RPC URLs format and security if provided
90        if let Some(configs) = &self.rpc_urls {
91            // Get security configuration from environment
92            let allowed_hosts = ServerConfig::get_rpc_allowed_hosts();
93            let block_private_ips = ServerConfig::get_rpc_block_private_ips();
94
95            for config in configs {
96                // Validate URL format and security
97                validate_safe_url(&config.url, &allowed_hosts, block_private_ips).map_err(
98                    |err| {
99                        ConfigFileError::InvalidFormat(format!(
100                            "RPC URL validation failed for '{}': {err}",
101                            sanitize_url_for_error(&config.url)
102                        ))
103                    },
104                )?;
105            }
106        }
107
108        if let Some(urls) = &self.explorer_urls {
109            for url in urls {
110                reqwest::Url::parse(url).map_err(|_| {
111                    ConfigFileError::InvalidFormat(format!(
112                        "Invalid Explorer URL: {}",
113                        sanitize_url_for_error(url)
114                    ))
115                })?;
116            }
117        }
118
119        Ok(())
120    }
121
122    /// Creates a new configuration by merging this config with a parent, where child values override parent defaults.
123    ///
124    /// # Arguments
125    /// * `parent` - The parent configuration to merge with.
126    ///
127    /// # Returns
128    /// A new `NetworkConfigCommon` with merged values where child takes precedence over parent.
129    /// For RPC URLs: if child has RPC URLs, they completely override parent's. If child has no RPC URLs, parent's are inherited.
130    pub fn merge_with_parent(&self, parent: &Self) -> Self {
131        Self {
132            network: self.network.clone(),
133            from: self.from.clone(),
134            rpc_urls: merge_optional_rpc_config_vecs(&self.rpc_urls, &parent.rpc_urls),
135            explorer_urls: self
136                .explorer_urls
137                .clone()
138                .or_else(|| parent.explorer_urls.clone()),
139            average_blocktime_ms: self.average_blocktime_ms.or(parent.average_blocktime_ms),
140            is_testnet: self.is_testnet.or(parent.is_testnet),
141            tags: merge_tags(&self.tags, &parent.tags),
142        }
143    }
144}
145
146/// Combines child and parent RPC config vectors.
147///
148/// Behavior:
149/// - If child has RPC configs: Use child's configs (allows weight specification for child URLs).
150/// - If child has no RPC configs: Use parent's configs (inheritance).
151///
152/// # Arguments
153/// * `child` - Optional vector of child RPC configs.
154/// * `parent` - Optional vector of parent RPC configs.
155///
156/// # Returns
157/// An optional vector containing child's RPC configs, or parent's if child has none, or `None` if both inputs are `None`.
158pub fn merge_optional_rpc_config_vecs(
159    child: &Option<Vec<RpcConfig>>,
160    parent: &Option<Vec<RpcConfig>>,
161) -> Option<Vec<RpcConfig>> {
162    match (child, parent) {
163        (Some(child), _) => Some(child.clone()), // Child overrides parent
164        (None, Some(parent)) => Some(parent.clone()), // Inherit from parent
165        (None, None) => None,
166    }
167}
168
169/// Combines child and parent string vectors, preserving all unique items with child items taking precedence.
170///
171/// # Arguments
172/// * `child` - Optional vector of child items.
173/// * `parent` - Optional vector of parent items.
174///
175/// # Returns
176/// An optional vector containing all unique items from both sources, or `None` if both inputs are `None`.
177pub fn merge_optional_string_vecs(
178    child: &Option<Vec<String>>,
179    parent: &Option<Vec<String>>,
180) -> Option<Vec<String>> {
181    match (child, parent) {
182        (Some(child), Some(parent)) => {
183            let mut merged = parent.clone();
184            for item in child {
185                if !merged.contains(item) {
186                    merged.push(item.clone());
187                }
188            }
189            Some(merged)
190        }
191        (Some(items), None) => Some(items.clone()),
192        (None, Some(items)) => Some(items.clone()),
193        (None, None) => None,
194    }
195}
196
197/// Combines child and parent tag vectors, preserving all unique tags with child tags taking precedence.
198///
199/// # Arguments
200/// * `child_tags` - Optional vector of child tags.
201/// * `parent_tags` - Optional vector of parent tags.
202///
203/// # Returns
204/// An optional vector containing all unique tags from both sources, or `None` if both inputs are `None`.
205fn merge_tags(
206    child_tags: &Option<Vec<String>>,
207    parent_tags: &Option<Vec<String>>,
208) -> Option<Vec<String>> {
209    merge_optional_string_vecs(child_tags, parent_tags)
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use crate::config::config_file::network::test_utils::*;
216    use lazy_static::lazy_static;
217    use std::env;
218    use std::sync::Mutex;
219
220    // Use a mutex to ensure tests don't run in parallel when modifying env vars
221    lazy_static! {
222        static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
223    }
224
225    fn setup_security_env() {
226        // Clear security-related environment variables
227        env::remove_var("RPC_ALLOWED_HOSTS");
228        env::remove_var("RPC_RPC_BLOCK_PRIVATE_IPS");
229    }
230
231    #[test]
232    fn test_validate_success_base_network() {
233        let config = create_network_common("test-network");
234        let result = config.validate();
235        assert!(result.is_ok());
236    }
237
238    #[test]
239    fn test_validate_success_inheriting_network() {
240        let config = create_network_common_with_parent("child-network", "parent-network");
241        let result = config.validate();
242        assert!(result.is_ok());
243    }
244
245    #[test]
246    fn test_validate_empty_network_name() {
247        let mut config = create_network_common("test-network");
248        config.network = String::new();
249
250        let result = config.validate();
251        assert!(result.is_err());
252        assert!(matches!(
253            result.unwrap_err(),
254            ConfigFileError::MissingField(_)
255        ));
256    }
257
258    #[test]
259    fn test_validate_base_network_missing_rpc_urls() {
260        let mut config = create_network_common("test-network");
261        config.rpc_urls = None;
262
263        let result = config.validate();
264        assert!(result.is_err());
265        assert!(matches!(
266            result.unwrap_err(),
267            ConfigFileError::MissingField(_)
268        ));
269    }
270
271    #[test]
272    fn test_validate_base_network_empty_rpc_urls() {
273        let mut config = create_network_common("test-network");
274        config.rpc_urls = Some(vec![]);
275
276        let result = config.validate();
277        assert!(result.is_err());
278        assert!(matches!(
279            result.unwrap_err(),
280            ConfigFileError::MissingField(_)
281        ));
282    }
283
284    #[test]
285    fn test_validate_invalid_rpc_url_format() {
286        use crate::models::RpcConfig;
287        let mut config = create_network_common("test-network");
288        config.rpc_urls = Some(vec![RpcConfig::new("invalid-url".to_string())]);
289
290        let result = config.validate();
291        assert!(result.is_err());
292        assert!(matches!(
293            result.unwrap_err(),
294            ConfigFileError::InvalidFormat(_)
295        ));
296    }
297
298    #[test]
299    fn test_validate_multiple_invalid_rpc_urls() {
300        use crate::models::RpcConfig;
301        let mut config = create_network_common("test-network");
302        config.rpc_urls = Some(vec![
303            RpcConfig::new("https://valid.example.com".to_string()),
304            RpcConfig::new("invalid-url".to_string()),
305            RpcConfig::new("also-invalid".to_string()),
306        ]);
307
308        let result = config.validate();
309        assert!(result.is_err());
310        assert!(matches!(
311            result.unwrap_err(),
312            ConfigFileError::InvalidFormat(_)
313        ));
314    }
315
316    #[test]
317    fn test_validate_various_valid_rpc_url_formats() {
318        use crate::models::RpcConfig;
319        let mut config = create_network_common("test-network");
320        // Note: Only http and https schemes are allowed by SSRF validation
321        // localhost is allowed when RPC_BLOCK_PRIVATE_IPS is not set (default false)
322        config.rpc_urls = Some(vec![
323            RpcConfig::new("https://mainnet.infura.io/v3/key".to_string()),
324            RpcConfig::new("http://localhost:8545".to_string()),
325            RpcConfig::new("https://rpc.example.com:8080/path".to_string()),
326        ]);
327
328        let result = config.validate();
329        assert!(result.is_ok());
330    }
331
332    #[test]
333    fn test_validate_rejects_non_http_scheme() {
334        let mut config = create_network_common("test-network");
335        // wss:// is not allowed - only http and https
336        config.rpc_urls = Some(vec![RpcConfig::new("wss://ws.example.com".to_string())]);
337
338        let result = config.validate();
339        assert!(result.is_err());
340        assert!(matches!(
341            result.unwrap_err(),
342            ConfigFileError::InvalidFormat(_)
343        ));
344    }
345
346    #[test]
347    fn test_validate_inheriting_network_with_rpc_urls() {
348        use crate::models::RpcConfig;
349        let mut config = create_network_common_with_parent("child-network", "parent-network");
350        config.rpc_urls = Some(vec![RpcConfig::new(
351            "https://override.example.com".to_string(),
352        )]);
353
354        let result = config.validate();
355        assert!(result.is_ok());
356    }
357
358    #[test]
359    fn test_validate_inheriting_network_with_invalid_rpc_urls() {
360        use crate::models::RpcConfig;
361        let mut config = create_network_common_with_parent("child-network", "parent-network");
362        config.rpc_urls = Some(vec![RpcConfig::new("invalid-url".to_string())]);
363
364        let result = config.validate();
365        assert!(result.is_err());
366        assert!(matches!(
367            result.unwrap_err(),
368            ConfigFileError::InvalidFormat(_)
369        ));
370    }
371
372    #[test]
373    fn test_merge_with_parent_child_overrides() {
374        use crate::models::RpcConfig;
375        let parent = NetworkConfigCommon {
376            network: "parent".to_string(),
377            from: None,
378            rpc_urls: Some(vec![RpcConfig::new(
379                "https://parent-rpc.example.com".to_string(),
380            )]),
381            explorer_urls: Some(vec!["https://parent-explorer.example.com".to_string()]),
382            average_blocktime_ms: Some(10000),
383            is_testnet: Some(true),
384            tags: Some(vec!["parent-tag".to_string()]),
385        };
386
387        let child = NetworkConfigCommon {
388            network: "child".to_string(),
389            from: Some("parent".to_string()),
390            rpc_urls: Some(vec![RpcConfig::new(
391                "https://child-rpc.example.com".to_string(),
392            )]),
393            explorer_urls: Some(vec!["https://child-explorer.example.com".to_string()]),
394            average_blocktime_ms: Some(15000),
395            is_testnet: Some(false),
396            tags: Some(vec!["child-tag".to_string()]),
397        };
398
399        let result = child.merge_with_parent(&parent);
400
401        assert_eq!(result.network, "child");
402        assert_eq!(result.from, Some("parent".to_string()));
403        // Child's RPC URLs override parent's
404        assert_eq!(
405            result.rpc_urls,
406            Some(vec![RpcConfig::new(
407                "https://child-rpc.example.com".to_string()
408            )])
409        );
410        assert_eq!(result.average_blocktime_ms, Some(15000));
411        assert_eq!(result.is_testnet, Some(false));
412        assert_eq!(
413            result.tags,
414            Some(vec!["parent-tag".to_string(), "child-tag".to_string()])
415        );
416    }
417
418    #[test]
419    fn test_merge_with_parent_child_inherits() {
420        use crate::models::RpcConfig;
421        let parent = NetworkConfigCommon {
422            network: "parent".to_string(),
423            from: None,
424            rpc_urls: Some(vec![RpcConfig::new(
425                "https://parent-rpc.example.com".to_string(),
426            )]),
427            explorer_urls: Some(vec!["https://parent-explorer.example.com".to_string()]),
428            average_blocktime_ms: Some(10000),
429            is_testnet: Some(true),
430            tags: Some(vec!["parent-tag".to_string()]),
431        };
432
433        let child = NetworkConfigCommon {
434            network: "child".to_string(),
435            from: Some("parent".to_string()),
436            rpc_urls: None,             // Will inherit
437            explorer_urls: None,        // Will inherit
438            average_blocktime_ms: None, // Will inherit
439            is_testnet: None,           // Will inherit
440            tags: None,                 // Will inherit
441        };
442
443        let result = child.merge_with_parent(&parent);
444
445        assert_eq!(result.network, "child");
446        assert_eq!(result.from, Some("parent".to_string()));
447        assert_eq!(
448            result.rpc_urls,
449            Some(vec![RpcConfig::new(
450                "https://parent-rpc.example.com".to_string()
451            )])
452        );
453        assert_eq!(
454            result.explorer_urls,
455            Some(vec!["https://parent-explorer.example.com".to_string()])
456        );
457        assert_eq!(result.average_blocktime_ms, Some(10000));
458        assert_eq!(result.is_testnet, Some(true));
459        assert_eq!(result.tags, Some(vec!["parent-tag".to_string()]));
460    }
461
462    #[test]
463    fn test_merge_with_parent_mixed_inheritance() {
464        use crate::models::RpcConfig;
465        let parent = NetworkConfigCommon {
466            network: "parent".to_string(),
467            from: None,
468            rpc_urls: Some(vec![RpcConfig::new(
469                "https://parent-rpc.example.com".to_string(),
470            )]),
471            explorer_urls: Some(vec!["https://parent-explorer.example.com".to_string()]),
472            average_blocktime_ms: Some(10000),
473            is_testnet: Some(true),
474            tags: Some(vec!["parent-tag1".to_string(), "parent-tag2".to_string()]),
475        };
476
477        let child = NetworkConfigCommon {
478            network: "child".to_string(),
479            from: Some("parent".to_string()),
480            rpc_urls: Some(vec![RpcConfig::new(
481                "https://child-rpc.example.com".to_string(),
482            )]), // Override
483            explorer_urls: Some(vec!["https://child-explorer.example.com".to_string()]), // Override
484            average_blocktime_ms: None,                                                  // Inherit
485            is_testnet: Some(false),                                                     // Override
486            tags: Some(vec!["child-tag".to_string()]),                                   // Merge
487        };
488
489        let result = child.merge_with_parent(&parent);
490
491        assert_eq!(result.network, "child");
492        // Child's RPC URLs override parent's (complete override)
493        assert_eq!(
494            result.rpc_urls,
495            Some(vec![RpcConfig::new(
496                "https://child-rpc.example.com".to_string()
497            )])
498        );
499        assert_eq!(
500            result.explorer_urls,
501            Some(vec!["https://child-explorer.example.com".to_string()])
502        );
503        assert_eq!(result.average_blocktime_ms, Some(10000)); // Inherited
504        assert_eq!(result.is_testnet, Some(false)); // Overridden
505        assert_eq!(
506            result.tags,
507            Some(vec![
508                "parent-tag1".to_string(),
509                "parent-tag2".to_string(),
510                "child-tag".to_string()
511            ])
512        );
513    }
514
515    #[test]
516    fn test_merge_with_parent_both_empty() {
517        let parent = NetworkConfigCommon {
518            network: "parent".to_string(),
519            from: None,
520            rpc_urls: None,
521            explorer_urls: None,
522            average_blocktime_ms: None,
523            is_testnet: None,
524            tags: None,
525        };
526
527        let child = NetworkConfigCommon {
528            network: "child".to_string(),
529            from: Some("parent".to_string()),
530            rpc_urls: None,
531            explorer_urls: None,
532            average_blocktime_ms: None,
533            is_testnet: None,
534            tags: None,
535        };
536
537        let result = child.merge_with_parent(&parent);
538
539        assert_eq!(result.network, "child");
540        assert_eq!(result.from, Some("parent".to_string()));
541        assert_eq!(result.rpc_urls, None);
542        assert_eq!(result.explorer_urls, None);
543        assert_eq!(result.average_blocktime_ms, None);
544        assert_eq!(result.is_testnet, None);
545        assert_eq!(result.tags, None);
546    }
547
548    #[test]
549    fn test_merge_with_parent_complex_tag_merging() {
550        use crate::models::RpcConfig;
551        let parent = NetworkConfigCommon {
552            network: "parent".to_string(),
553            from: None,
554            rpc_urls: Some(vec![RpcConfig::new("https://rpc.example.com".to_string())]),
555            explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
556            average_blocktime_ms: Some(12000),
557            is_testnet: Some(true),
558            tags: Some(vec![
559                "production".to_string(),
560                "mainnet".to_string(),
561                "shared".to_string(),
562            ]),
563        };
564
565        let child = NetworkConfigCommon {
566            network: "child".to_string(),
567            from: Some("parent".to_string()),
568            rpc_urls: None,
569            explorer_urls: None,
570            average_blocktime_ms: None,
571            is_testnet: None,
572            tags: Some(vec![
573                "shared".to_string(),
574                "custom".to_string(),
575                "override".to_string(),
576            ]),
577        };
578
579        let result = child.merge_with_parent(&parent);
580
581        // Tags should be merged with parent first, then unique child tags added
582        let expected_tags = vec![
583            "production".to_string(),
584            "mainnet".to_string(),
585            "shared".to_string(), // Duplicate should not be added again
586            "custom".to_string(),
587            "override".to_string(),
588        ];
589        assert_eq!(result.tags, Some(expected_tags));
590    }
591
592    #[test]
593    fn test_merge_optional_string_vecs_both_some() {
594        let child = Some(vec!["child1".to_string(), "child2".to_string()]);
595        let parent = Some(vec!["parent1".to_string(), "parent2".to_string()]);
596        let result = merge_optional_string_vecs(&child, &parent);
597        assert_eq!(
598            result,
599            Some(vec![
600                "parent1".to_string(),
601                "parent2".to_string(),
602                "child1".to_string(),
603                "child2".to_string()
604            ])
605        );
606    }
607
608    #[test]
609    fn test_merge_optional_string_vecs_child_some_parent_none() {
610        let child = Some(vec!["child1".to_string()]);
611        let parent = None;
612        let result = merge_optional_string_vecs(&child, &parent);
613        assert_eq!(result, Some(vec!["child1".to_string()]));
614    }
615
616    #[test]
617    fn test_merge_optional_string_vecs_child_none_parent_some() {
618        let child = None;
619        let parent = Some(vec!["parent1".to_string()]);
620        let result = merge_optional_string_vecs(&child, &parent);
621        assert_eq!(result, Some(vec!["parent1".to_string()]));
622    }
623
624    #[test]
625    fn test_merge_optional_string_vecs_both_none() {
626        let child = None;
627        let parent = None;
628        let result = merge_optional_string_vecs(&child, &parent);
629        assert_eq!(result, None);
630    }
631
632    #[test]
633    fn test_merge_optional_string_vecs_duplicate_handling() {
634        // Test duplicate handling
635        let child = Some(vec!["duplicate".to_string(), "child1".to_string()]);
636        let parent = Some(vec!["duplicate".to_string(), "parent1".to_string()]);
637        let result = merge_optional_string_vecs(&child, &parent);
638        assert_eq!(
639            result,
640            Some(vec![
641                "duplicate".to_string(),
642                "parent1".to_string(),
643                "child1".to_string()
644            ])
645        );
646    }
647
648    #[test]
649    fn test_merge_optional_string_vecs_empty_vectors() {
650        // Test empty child vector
651        let child = Some(vec![]);
652        let parent = Some(vec!["parent1".to_string()]);
653        let result = merge_optional_string_vecs(&child, &parent);
654        assert_eq!(result, Some(vec!["parent1".to_string()]));
655
656        // Test empty parent vector
657        let child = Some(vec!["child1".to_string()]);
658        let parent = Some(vec![]);
659        let result = merge_optional_string_vecs(&child, &parent);
660        assert_eq!(result, Some(vec!["child1".to_string()]));
661
662        // Test both empty vectors
663        let child = Some(vec![]);
664        let parent = Some(vec![]);
665        let result = merge_optional_string_vecs(&child, &parent);
666        assert_eq!(result, Some(vec![]));
667    }
668
669    #[test]
670    fn test_merge_optional_string_vecs_multiple_duplicates() {
671        let child = Some(vec![
672            "a".to_string(),
673            "b".to_string(),
674            "c".to_string(),
675            "a".to_string(),
676        ]);
677        let parent = Some(vec!["b".to_string(), "d".to_string(), "a".to_string()]);
678        let result = merge_optional_string_vecs(&child, &parent);
679
680        // Should preserve parent order, then add unique child items
681        let expected = vec![
682            "b".to_string(),
683            "d".to_string(),
684            "a".to_string(),
685            "c".to_string(),
686        ];
687        assert_eq!(result, Some(expected));
688    }
689
690    #[test]
691    fn test_merge_optional_string_vecs_single_item_vectors() {
692        let child = Some(vec!["child".to_string()]);
693        let parent = Some(vec!["parent".to_string()]);
694        let result = merge_optional_string_vecs(&child, &parent);
695        assert_eq!(
696            result,
697            Some(vec!["parent".to_string(), "child".to_string()])
698        );
699    }
700
701    #[test]
702    fn test_merge_optional_string_vecs_identical_vectors() {
703        let child = Some(vec!["same1".to_string(), "same2".to_string()]);
704        let parent = Some(vec!["same1".to_string(), "same2".to_string()]);
705        let result = merge_optional_string_vecs(&child, &parent);
706        assert_eq!(result, Some(vec!["same1".to_string(), "same2".to_string()]));
707    }
708
709    // Edge Cases and Integration Tests
710    #[test]
711    fn test_network_config_common_clone() {
712        let config = create_network_common("test-network");
713        let cloned = config.clone();
714
715        assert_eq!(config.network, cloned.network);
716        assert_eq!(config.from, cloned.from);
717        assert_eq!(config.rpc_urls, cloned.rpc_urls);
718        assert_eq!(config.average_blocktime_ms, cloned.average_blocktime_ms);
719        assert_eq!(config.is_testnet, cloned.is_testnet);
720        assert_eq!(config.tags, cloned.tags);
721    }
722
723    #[test]
724    fn test_network_config_common_debug() {
725        let config = create_network_common("test-network");
726        let debug_str = format!("{config:?}");
727
728        assert!(debug_str.contains("NetworkConfigCommon"));
729        assert!(debug_str.contains("test-network"));
730    }
731
732    #[test]
733    fn test_validate_with_unicode_network_name() {
734        let mut config = create_network_common("test-network");
735        config.network = "测试网络".to_string();
736
737        let result = config.validate();
738        assert!(result.is_ok());
739    }
740
741    #[test]
742    fn test_validate_with_unicode_rpc_urls() {
743        use crate::models::RpcConfig;
744        let mut config = create_network_common("test-network");
745        config.rpc_urls = Some(vec![RpcConfig::new("https://测试.example.com".to_string())]);
746
747        let result = config.validate();
748        assert!(result.is_ok());
749    }
750
751    // ==========================================================================
752    // RPC URL Security Validation Tests
753    // These tests validate the SSRF protection for RPC URLs
754    // ==========================================================================
755
756    #[test]
757    fn test_validate_blocks_cloud_metadata_ip_always() {
758        let _lock = match ENV_MUTEX.lock() {
759            Ok(guard) => guard,
760            Err(poisoned) => poisoned.into_inner(),
761        };
762        setup_security_env();
763
764        // Cloud metadata endpoints should always be blocked regardless of RPC_BLOCK_PRIVATE_IPS
765        let mut config = create_network_common("test-network");
766        config.rpc_urls = Some(vec![RpcConfig::new(
767            "http://169.254.169.254/latest/meta-data".to_string(),
768        )]);
769
770        let result = config.validate();
771        assert!(result.is_err());
772        let err = result.unwrap_err();
773        assert!(matches!(err, ConfigFileError::InvalidFormat(_)));
774    }
775
776    #[test]
777    fn test_validate_blocks_cloud_metadata_hostname_always() {
778        let _lock = match ENV_MUTEX.lock() {
779            Ok(guard) => guard,
780            Err(poisoned) => poisoned.into_inner(),
781        };
782        setup_security_env();
783
784        // GCP metadata hostname should always be blocked
785        let mut config = create_network_common("test-network");
786        config.rpc_urls = Some(vec![RpcConfig::new(
787            "http://metadata.google.internal".to_string(),
788        )]);
789
790        let result = config.validate();
791        assert!(result.is_err());
792        let err = result.unwrap_err();
793        assert!(matches!(err, ConfigFileError::InvalidFormat(_)));
794    }
795
796    #[test]
797    fn test_validate_blocks_private_ip_when_block_private_ips_enabled() {
798        let _lock = match ENV_MUTEX.lock() {
799            Ok(guard) => guard,
800            Err(poisoned) => poisoned.into_inner(),
801        };
802        setup_security_env();
803        env::set_var("RPC_BLOCK_PRIVATE_IPS", "true");
804
805        // Private IPs should be blocked when RPC_BLOCK_PRIVATE_IPS is true
806        let mut config = create_network_common("test-network");
807        config.rpc_urls = Some(vec![RpcConfig::new("http://192.168.1.1:8545".to_string())]);
808
809        let result = config.validate();
810        assert!(result.is_err());
811        let err = result.unwrap_err();
812        assert!(matches!(err, ConfigFileError::InvalidFormat(_)));
813
814        // Clean up
815        env::remove_var("RPC_BLOCK_PRIVATE_IPS");
816    }
817
818    #[test]
819    fn test_validate_blocks_localhost_when_block_private_ips_enabled() {
820        let _lock = match ENV_MUTEX.lock() {
821            Ok(guard) => guard,
822            Err(poisoned) => poisoned.into_inner(),
823        };
824        setup_security_env();
825        env::set_var("RPC_BLOCK_PRIVATE_IPS", "true");
826
827        // Localhost should be blocked when RPC_BLOCK_PRIVATE_IPS is true
828        let mut config = create_network_common("test-network");
829        config.rpc_urls = Some(vec![RpcConfig::new("http://localhost:8545".to_string())]);
830
831        let result = config.validate();
832        assert!(result.is_err());
833        let err = result.unwrap_err();
834        assert!(matches!(err, ConfigFileError::InvalidFormat(_)));
835
836        // Clean up
837        env::remove_var("RPC_BLOCK_PRIVATE_IPS");
838    }
839
840    #[test]
841    fn test_validate_blocks_127_0_0_1_when_block_private_ips_enabled() {
842        let _lock = match ENV_MUTEX.lock() {
843            Ok(guard) => guard,
844            Err(poisoned) => poisoned.into_inner(),
845        };
846        setup_security_env();
847        env::set_var("RPC_BLOCK_PRIVATE_IPS", "true");
848
849        // 127.0.0.1 should be blocked when RPC_BLOCK_PRIVATE_IPS is true
850        let mut config = create_network_common("test-network");
851        config.rpc_urls = Some(vec![RpcConfig::new("http://127.0.0.1:8545".to_string())]);
852
853        let result = config.validate();
854        assert!(result.is_err());
855        let err = result.unwrap_err();
856        assert!(matches!(err, ConfigFileError::InvalidFormat(_)));
857
858        // Clean up
859        env::remove_var("RPC_BLOCK_PRIVATE_IPS");
860    }
861
862    #[test]
863    fn test_validate_allows_private_ip_when_block_private_ips_disabled() {
864        let _lock = match ENV_MUTEX.lock() {
865            Ok(guard) => guard,
866            Err(poisoned) => poisoned.into_inner(),
867        };
868        setup_security_env();
869        // Explicitly disable (default)
870        env::set_var("RPC_BLOCK_PRIVATE_IPS", "false");
871
872        // Private IPs should be allowed when RPC_BLOCK_PRIVATE_IPS is false
873        let mut config = create_network_common("test-network");
874        config.rpc_urls = Some(vec![RpcConfig::new("http://192.168.1.1:8545".to_string())]);
875
876        let result = config.validate();
877        assert!(result.is_ok());
878
879        // Clean up
880        env::remove_var("RPC_BLOCK_PRIVATE_IPS");
881    }
882
883    #[test]
884    fn test_validate_allows_localhost_when_block_private_ips_disabled() {
885        let _lock = match ENV_MUTEX.lock() {
886            Ok(guard) => guard,
887            Err(poisoned) => poisoned.into_inner(),
888        };
889        setup_security_env();
890        // Explicitly disable (default)
891        env::set_var("RPC_BLOCK_PRIVATE_IPS", "false");
892
893        // Localhost should be allowed when RPC_BLOCK_PRIVATE_IPS is false
894        let mut config = create_network_common("test-network");
895        config.rpc_urls = Some(vec![RpcConfig::new("http://localhost:8545".to_string())]);
896
897        let result = config.validate();
898        assert!(result.is_ok());
899
900        // Clean up
901        env::remove_var("RPC_BLOCK_PRIVATE_IPS");
902    }
903
904    #[test]
905    fn test_validate_blocks_non_allowed_host_when_allowlist_set() {
906        let _lock = match ENV_MUTEX.lock() {
907            Ok(guard) => guard,
908            Err(poisoned) => poisoned.into_inner(),
909        };
910        setup_security_env();
911        env::set_var("RPC_ALLOWED_HOSTS", "allowed.example.com,other.example.com");
912
913        // Non-allowed hosts should be blocked when allowlist is set
914        let mut config = create_network_common("test-network");
915        config.rpc_urls = Some(vec![RpcConfig::new(
916            "https://not-allowed.example.com".to_string(),
917        )]);
918
919        let result = config.validate();
920        assert!(result.is_err());
921        let err = result.unwrap_err();
922        assert!(matches!(err, ConfigFileError::InvalidFormat(_)));
923
924        // Clean up
925        env::remove_var("RPC_ALLOWED_HOSTS");
926    }
927
928    #[test]
929    fn test_validate_allows_host_in_allowlist() {
930        let _lock = match ENV_MUTEX.lock() {
931            Ok(guard) => guard,
932            Err(poisoned) => poisoned.into_inner(),
933        };
934        setup_security_env();
935        env::set_var("RPC_ALLOWED_HOSTS", "allowed.example.com,other.example.com");
936
937        // Hosts in the allowlist should be permitted
938        let mut config = create_network_common("test-network");
939        config.rpc_urls = Some(vec![RpcConfig::new(
940            "https://allowed.example.com:8545".to_string(),
941        )]);
942
943        let result = config.validate();
944        assert!(result.is_ok());
945
946        // Clean up
947        env::remove_var("RPC_ALLOWED_HOSTS");
948    }
949
950    #[test]
951    fn test_validate_allowlist_is_case_insensitive() {
952        let _lock = match ENV_MUTEX.lock() {
953            Ok(guard) => guard,
954            Err(poisoned) => poisoned.into_inner(),
955        };
956        setup_security_env();
957        env::set_var("RPC_ALLOWED_HOSTS", "Allowed.Example.COM");
958
959        // Allowlist matching should be case-insensitive (DNS is case-insensitive)
960        let mut config = create_network_common("test-network");
961        config.rpc_urls = Some(vec![RpcConfig::new(
962            "https://allowed.example.com:8545".to_string(),
963        )]);
964
965        let result = config.validate();
966        assert!(result.is_ok());
967
968        // Clean up
969        env::remove_var("RPC_ALLOWED_HOSTS");
970    }
971
972    #[test]
973    fn test_validate_blocks_10_x_private_range_when_enabled() {
974        let _lock = match ENV_MUTEX.lock() {
975            Ok(guard) => guard,
976            Err(poisoned) => poisoned.into_inner(),
977        };
978        setup_security_env();
979        env::set_var("RPC_BLOCK_PRIVATE_IPS", "true");
980
981        // 10.x.x.x private range should be blocked
982        let mut config = create_network_common("test-network");
983        config.rpc_urls = Some(vec![RpcConfig::new("http://10.0.0.1:8545".to_string())]);
984
985        let result = config.validate();
986        assert!(result.is_err());
987        let err = result.unwrap_err();
988        assert!(matches!(err, ConfigFileError::InvalidFormat(_)));
989
990        // Clean up
991        env::remove_var("RPC_BLOCK_PRIVATE_IPS");
992    }
993
994    #[test]
995    fn test_validate_blocks_172_16_private_range_when_enabled() {
996        let _lock = match ENV_MUTEX.lock() {
997            Ok(guard) => guard,
998            Err(poisoned) => poisoned.into_inner(),
999        };
1000        setup_security_env();
1001        env::set_var("RPC_BLOCK_PRIVATE_IPS", "true");
1002
1003        // 172.16.x.x - 172.31.x.x private range should be blocked
1004        let mut config = create_network_common("test-network");
1005        config.rpc_urls = Some(vec![RpcConfig::new("http://172.16.0.1:8545".to_string())]);
1006
1007        let result = config.validate();
1008        assert!(result.is_err());
1009        let err = result.unwrap_err();
1010        assert!(matches!(err, ConfigFileError::InvalidFormat(_)));
1011
1012        // Clean up
1013        env::remove_var("RPC_BLOCK_PRIVATE_IPS");
1014    }
1015
1016    #[test]
1017    fn test_validate_error_message_contains_sanitized_url() {
1018        let _lock = match ENV_MUTEX.lock() {
1019            Ok(guard) => guard,
1020            Err(poisoned) => poisoned.into_inner(),
1021        };
1022        setup_security_env();
1023
1024        // Test that error messages contain sanitized URLs (no credentials)
1025        let mut config = create_network_common("test-network");
1026        config.rpc_urls = Some(vec![RpcConfig::new("invalid-url".to_string())]);
1027
1028        let result = config.validate();
1029        assert!(result.is_err());
1030        if let ConfigFileError::InvalidFormat(msg) = result.unwrap_err() {
1031            assert!(msg.contains("RPC URL validation failed"));
1032        } else {
1033            panic!("Expected InvalidFormat error");
1034        }
1035    }
1036
1037    #[test]
1038    fn test_validate_multiple_urls_with_one_blocked() {
1039        let _lock = match ENV_MUTEX.lock() {
1040            Ok(guard) => guard,
1041            Err(poisoned) => poisoned.into_inner(),
1042        };
1043        setup_security_env();
1044        env::set_var("RPC_BLOCK_PRIVATE_IPS", "true");
1045
1046        // If any URL fails validation, the whole validation should fail
1047        let mut config = create_network_common("test-network");
1048        config.rpc_urls = Some(vec![
1049            RpcConfig::new("https://valid.example.com".to_string()),
1050            RpcConfig::new("http://localhost:8545".to_string()), // This should be blocked
1051        ]);
1052
1053        let result = config.validate();
1054        assert!(result.is_err());
1055        let err = result.unwrap_err();
1056        assert!(matches!(err, ConfigFileError::InvalidFormat(_)));
1057
1058        // Clean up
1059        env::remove_var("RPC_BLOCK_PRIVATE_IPS");
1060    }
1061
1062    #[test]
1063    fn test_validate_blocks_unspecified_ip_always() {
1064        let _lock = match ENV_MUTEX.lock() {
1065            Ok(guard) => guard,
1066            Err(poisoned) => poisoned.into_inner(),
1067        };
1068        setup_security_env();
1069
1070        // 0.0.0.0 should be blocked (unspecified address)
1071        let mut config = create_network_common("test-network");
1072        config.rpc_urls = Some(vec![RpcConfig::new("http://0.0.0.0:8545".to_string())]);
1073
1074        let result = config.validate();
1075        assert!(result.is_err());
1076        let err = result.unwrap_err();
1077        assert!(matches!(err, ConfigFileError::InvalidFormat(_)));
1078    }
1079
1080    #[test]
1081    fn test_merge_with_parent_preserves_child_network_name() {
1082        use crate::models::RpcConfig;
1083        let parent = NetworkConfigCommon {
1084            network: "parent-name".to_string(),
1085            from: None,
1086            rpc_urls: Some(vec![RpcConfig::new(
1087                "https://parent.example.com".to_string(),
1088            )]),
1089            explorer_urls: Some(vec!["https://parent.example.com".to_string()]),
1090            average_blocktime_ms: Some(10000),
1091            is_testnet: Some(true),
1092            tags: None,
1093        };
1094
1095        let child = NetworkConfigCommon {
1096            network: "child-name".to_string(),
1097            from: Some("parent-name".to_string()),
1098            rpc_urls: None,
1099            explorer_urls: None,
1100            average_blocktime_ms: None,
1101            is_testnet: None,
1102            tags: None,
1103        };
1104
1105        let result = child.merge_with_parent(&parent);
1106
1107        // Child network name should always be preserved
1108        assert_eq!(result.network, "child-name");
1109        assert_eq!(result.from, Some("parent-name".to_string()));
1110    }
1111
1112    #[test]
1113    fn test_merge_with_parent_preserves_child_from_field() {
1114        use crate::models::RpcConfig;
1115        let parent = NetworkConfigCommon {
1116            network: "parent".to_string(),
1117            from: Some("grandparent".to_string()),
1118            rpc_urls: Some(vec![RpcConfig::new(
1119                "https://parent.example.com".to_string(),
1120            )]),
1121            explorer_urls: Some(vec!["https://parent.example.com".to_string()]),
1122            average_blocktime_ms: Some(10000),
1123            is_testnet: Some(true),
1124            tags: None,
1125        };
1126
1127        let child = NetworkConfigCommon {
1128            network: "child".to_string(),
1129            from: Some("parent".to_string()),
1130            rpc_urls: None,
1131            explorer_urls: None,
1132            average_blocktime_ms: None,
1133            is_testnet: None,
1134            tags: None,
1135        };
1136
1137        let result = child.merge_with_parent(&parent);
1138
1139        // Child's 'from' field should be preserved, not inherited from parent
1140        assert_eq!(result.from, Some("parent".to_string()));
1141    }
1142
1143    #[test]
1144    fn test_deserialize_simple_string_array_format() {
1145        // Test that simple format (array of strings) is correctly converted to RpcConfig
1146        let json = r#"{
1147            "network": "test-network",
1148            "rpc_urls": ["https://rpc1.example.com", "https://rpc2.example.com"]
1149        }"#;
1150
1151        let config: NetworkConfigCommon = serde_json::from_str(json).unwrap();
1152        assert!(config.rpc_urls.is_some());
1153        let rpc_configs = config.rpc_urls.unwrap();
1154        assert_eq!(rpc_configs.len(), 2);
1155        assert_eq!(rpc_configs[0].url, "https://rpc1.example.com");
1156        assert_eq!(rpc_configs[0].weight, crate::constants::DEFAULT_RPC_WEIGHT);
1157        assert_eq!(rpc_configs[1].url, "https://rpc2.example.com");
1158        assert_eq!(rpc_configs[1].weight, crate::constants::DEFAULT_RPC_WEIGHT);
1159    }
1160
1161    #[test]
1162    fn test_deserialize_extended_object_array_format() {
1163        // Test that extended format (array of RpcConfig objects) works correctly
1164        let json = r#"{
1165            "network": "test-network",
1166            "rpc_urls": [
1167                {"url": "https://rpc1.example.com", "weight": 50},
1168                {"url": "https://rpc2.example.com", "weight": 100}
1169            ]
1170        }"#;
1171
1172        let config: NetworkConfigCommon = serde_json::from_str(json).unwrap();
1173        assert!(config.rpc_urls.is_some());
1174        let rpc_configs = config.rpc_urls.unwrap();
1175        assert_eq!(rpc_configs.len(), 2);
1176        assert_eq!(rpc_configs[0].url, "https://rpc1.example.com");
1177        assert_eq!(rpc_configs[0].weight, 50);
1178        assert_eq!(rpc_configs[1].url, "https://rpc2.example.com");
1179        assert_eq!(rpc_configs[1].weight, 100);
1180    }
1181
1182    #[test]
1183    fn test_deserialize_object_array_with_default_weight() {
1184        // Test that RpcConfig objects without weight get default weight
1185        let json = r#"{
1186            "network": "test-network",
1187            "rpc_urls": [
1188                {"url": "https://rpc1.example.com"}
1189            ]
1190        }"#;
1191
1192        let config: NetworkConfigCommon = serde_json::from_str(json).unwrap();
1193        assert!(config.rpc_urls.is_some());
1194        let rpc_configs = config.rpc_urls.unwrap();
1195        assert_eq!(rpc_configs.len(), 1);
1196        assert_eq!(rpc_configs[0].url, "https://rpc1.example.com");
1197        assert_eq!(rpc_configs[0].weight, crate::constants::DEFAULT_RPC_WEIGHT);
1198    }
1199
1200    #[test]
1201    fn test_serialize_preserves_weights() {
1202        // Test that serialization preserves weights
1203        use crate::models::RpcConfig;
1204        let config = NetworkConfigCommon {
1205            network: "test-network".to_string(),
1206            from: None,
1207            rpc_urls: Some(vec![
1208                RpcConfig::with_weight("https://rpc1.example.com".to_string(), 50).unwrap(),
1209                RpcConfig::new("https://rpc2.example.com".to_string()),
1210            ]),
1211            explorer_urls: None,
1212            average_blocktime_ms: None,
1213            is_testnet: None,
1214            tags: None,
1215        };
1216
1217        let serialized = serde_json::to_string(&config).unwrap();
1218        let deserialized: NetworkConfigCommon = serde_json::from_str(&serialized).unwrap();
1219
1220        assert!(deserialized.rpc_urls.is_some());
1221        let rpc_configs = deserialized.rpc_urls.unwrap();
1222        assert_eq!(rpc_configs.len(), 2);
1223        assert_eq!(rpc_configs[0].url, "https://rpc1.example.com");
1224        assert_eq!(rpc_configs[0].weight, 50);
1225        assert_eq!(rpc_configs[1].url, "https://rpc2.example.com");
1226        assert_eq!(rpc_configs[1].weight, crate::constants::DEFAULT_RPC_WEIGHT);
1227    }
1228
1229    #[test]
1230    fn test_roundtrip_simple_to_extended_format() {
1231        // Test that simple format can be read and then serialized in extended format
1232        let simple_json = r#"{
1233            "network": "test-network",
1234            "rpc_urls": ["https://rpc1.example.com", "https://rpc2.example.com"]
1235        }"#;
1236
1237        let config: NetworkConfigCommon = serde_json::from_str(simple_json).unwrap();
1238        let serialized = serde_json::to_string(&config).unwrap();
1239
1240        // The serialized version should be in extended format (with weights)
1241        assert!(serialized.contains("\"url\""));
1242        assert!(serialized.contains("\"weight\""));
1243
1244        // Deserialize again to verify it still works
1245        let deserialized: NetworkConfigCommon = serde_json::from_str(&serialized).unwrap();
1246        assert!(deserialized.rpc_urls.is_some());
1247        let rpc_configs = deserialized.rpc_urls.unwrap();
1248        assert_eq!(rpc_configs.len(), 2);
1249        assert_eq!(rpc_configs[0].url, "https://rpc1.example.com");
1250        assert_eq!(rpc_configs[1].url, "https://rpc2.example.com");
1251    }
1252
1253    #[test]
1254    fn test_merge_rpc_configs_override_behavior() {
1255        // Test that child RPC configs completely override parent configs
1256        use crate::models::RpcConfig;
1257        let parent = NetworkConfigCommon {
1258            network: "parent".to_string(),
1259            from: None,
1260            rpc_urls: Some(vec![
1261                RpcConfig::with_weight("https://rpc1.example.com".to_string(), 100).unwrap(),
1262                RpcConfig::with_weight("https://rpc2.example.com".to_string(), 100).unwrap(),
1263            ]),
1264            explorer_urls: None,
1265            average_blocktime_ms: None,
1266            is_testnet: None,
1267            tags: None,
1268        };
1269
1270        let child = NetworkConfigCommon {
1271            network: "child".to_string(),
1272            from: Some("parent".to_string()),
1273            // Child completely overrides parent's RPC URLs
1274            rpc_urls: Some(vec![
1275                RpcConfig::with_weight("https://child-rpc1.example.com".to_string(), 50).unwrap(),
1276                RpcConfig::with_weight("https://child-rpc2.example.com".to_string(), 75).unwrap(),
1277            ]),
1278            explorer_urls: None,
1279            average_blocktime_ms: None,
1280            is_testnet: None,
1281            tags: None,
1282        };
1283
1284        let result = child.merge_with_parent(&parent);
1285
1286        // Should have only child's RPC configs (complete override)
1287        assert!(result.rpc_urls.is_some());
1288        let rpc_configs = result.rpc_urls.unwrap();
1289        assert_eq!(rpc_configs.len(), 2);
1290
1291        // Find each config by URL
1292        let child_rpc1 = rpc_configs
1293            .iter()
1294            .find(|c| c.url == "https://child-rpc1.example.com")
1295            .unwrap();
1296        let child_rpc2 = rpc_configs
1297            .iter()
1298            .find(|c| c.url == "https://child-rpc2.example.com")
1299            .unwrap();
1300
1301        // Should have child's weights
1302        assert_eq!(child_rpc1.weight, 50);
1303        assert_eq!(child_rpc2.weight, 75);
1304
1305        // Should not have any parent URLs
1306        assert!(!rpc_configs
1307            .iter()
1308            .any(|c| c.url == "https://rpc1.example.com"));
1309        assert!(!rpc_configs
1310            .iter()
1311            .any(|c| c.url == "https://rpc2.example.com"));
1312    }
1313}