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