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