1use 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 pub network: String,
21 pub from: Option<String>,
25 #[serde(deserialize_with = "deserialize_rpc_urls")]
28 pub rpc_urls: Option<Vec<RpcConfig>>,
29 pub explorer_urls: Option<Vec<String>>,
31 pub average_blocktime_ms: Option<u64>,
33 pub is_testnet: Option<bool>,
35 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 pub fn validate(&self) -> Result<(), ConfigFileError> {
76 if self.network.is_empty() {
78 return Err(ConfigFileError::MissingField("network name".into()));
79 }
80
81 if self.from.is_none() {
83 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 if let Some(configs) = &self.rpc_urls {
91 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_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 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
146pub 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()), (None, Some(parent)) => Some(parent.clone()), (None, None) => None,
166 }
167}
168
169pub 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
197fn 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 lazy_static! {
222 static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
223 }
224
225 fn setup_security_env() {
226 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 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 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 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, explorer_urls: None, average_blocktime_ms: None, is_testnet: None, tags: None, };
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 )]), explorer_urls: Some(vec!["https://child-explorer.example.com".to_string()]), average_blocktime_ms: None, is_testnet: Some(false), tags: Some(vec!["child-tag".to_string()]), };
488
489 let result = child.merge_with_parent(&parent);
490
491 assert_eq!(result.network, "child");
492 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)); assert_eq!(result.is_testnet, Some(false)); 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 let expected_tags = vec![
583 "production".to_string(),
584 "mainnet".to_string(),
585 "shared".to_string(), "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 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 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 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 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 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 #[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 #[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 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 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 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 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 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 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 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 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 env::set_var("RPC_BLOCK_PRIVATE_IPS", "false");
871
872 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 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 env::set_var("RPC_BLOCK_PRIVATE_IPS", "false");
892
893 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 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 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 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 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 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 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 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 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 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 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 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 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 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()), ]);
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 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 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 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 assert_eq!(result.from, Some("parent".to_string()));
1141 }
1142
1143 #[test]
1144 fn test_deserialize_simple_string_array_format() {
1145 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 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 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 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 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 assert!(serialized.contains("\"url\""));
1242 assert!(serialized.contains("\"weight\""));
1243
1244 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 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 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 assert!(result.rpc_urls.is_some());
1288 let rpc_configs = result.rpc_urls.unwrap();
1289 assert_eq!(rpc_configs.len(), 2);
1290
1291 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 assert_eq!(child_rpc1.weight, 50);
1303 assert_eq!(child_rpc2.weight, 75);
1304
1305 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}