diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-12 21:20:00 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-12 21:21:53 +0000 |
| commit | 1948312d40f34fca868d1ef6d6d94e165c09738c (patch) | |
| tree | f25f930785145023be6fe33e52904a5d8383a62d /src/config.rs | |
| parent | 82b56c37b26a2fac1a294873e539b19b9325dca6 (diff) | |
refactor(config): validate eagerly at startup and remove Result from runtime config methods
Refactors configuration validation to fail fast on fatal errors at startup
while gracefully handling recoverable issues (e.g., malformed whitelist entries).
Changes:
- Add Config::validate() for eager validation called immediately after load
- Remove Result<> from archive_config() and repository_config() methods
- WhitelistEntry::parse_whitelist() skips invalid entries with warnings
- Validate relay_owner_nsec format in Config::validate()
- Update all call sites to remove Result handling from config getters
Benefits:
- Fatal config errors (incompatible settings) fail at startup, not runtime
- Recoverable errors (bad whitelist entries) logged as warnings and skipped
- No Result handling scattered throughout runtime code after validation
- Config methods safe to call without error handling after validate()
Testing:
- Add 7 new tests for validation edge cases and error handling
- Total config tests: 40 (up from 33)
- All 320 library tests passing
Breaking change: Config users must call config.validate() after Config::load()
to ensure configuration is valid. This is enforced in main.rs.
Diffstat (limited to 'src/config.rs')
| -rw-r--r-- | src/config.rs | 261 |
1 files changed, 193 insertions, 68 deletions
diff --git a/src/config.rs b/src/config.rs index 8a9eb4d..37b1c1e 100644 --- a/src/config.rs +++ b/src/config.rs | |||
| @@ -85,16 +85,25 @@ impl WhitelistEntry { | |||
| 85 | } | 85 | } |
| 86 | 86 | ||
| 87 | /// Parse whitelist from comma-separated string | 87 | /// Parse whitelist from comma-separated string |
| 88 | pub fn parse_whitelist(input: &str) -> Result<Vec<Self>> { | 88 | /// |
| 89 | /// Skips invalid entries with warnings instead of failing. | ||
| 90 | /// This allows the config to load even if some whitelist entries are malformed. | ||
| 91 | pub fn parse_whitelist(input: &str) -> Vec<Self> { | ||
| 89 | if input.trim().is_empty() { | 92 | if input.trim().is_empty() { |
| 90 | return Ok(Vec::new()); | 93 | return Vec::new(); |
| 91 | } | 94 | } |
| 92 | 95 | ||
| 93 | input | 96 | input |
| 94 | .split(',') | 97 | .split(',') |
| 95 | .map(|s| s.trim()) | 98 | .map(|s| s.trim()) |
| 96 | .filter(|s| !s.is_empty()) | 99 | .filter(|s| !s.is_empty()) |
| 97 | .map(Self::parse) | 100 | .filter_map(|s| match Self::parse(s) { |
| 101 | Ok(entry) => Some(entry), | ||
| 102 | Err(e) => { | ||
| 103 | tracing::warn!("Skipping invalid whitelist entry '{}': {}", s, e); | ||
| 104 | None | ||
| 105 | } | ||
| 106 | }) | ||
| 98 | .collect() | 107 | .collect() |
| 99 | } | 108 | } |
| 100 | } | 109 | } |
| @@ -464,23 +473,60 @@ impl Config { | |||
| 464 | } | 473 | } |
| 465 | } | 474 | } |
| 466 | 475 | ||
| 476 | /// Validate configuration and return fatal errors | ||
| 477 | /// | ||
| 478 | /// This should be called immediately after Config::load() to fail fast on config errors. | ||
| 479 | /// Recoverable issues (e.g., malformed whitelist entries) are logged as warnings and skipped. | ||
| 480 | pub fn validate(&self) -> Result<()> { | ||
| 481 | // Validate relay owner nsec (should always be set by Config::load()) | ||
| 482 | let nsec = self | ||
| 483 | .relay_owner_nsec | ||
| 484 | .as_ref() | ||
| 485 | .context("relay_owner_nsec not set (should be set by Config::load())")?; | ||
| 486 | Keys::parse(nsec).context("Invalid relay_owner_nsec format")?; | ||
| 487 | |||
| 488 | // Validate archive configuration | ||
| 489 | let archive_whitelist = WhitelistEntry::parse_whitelist(&self.archive_whitelist); | ||
| 490 | let archive_enabled = self.archive_all || !archive_whitelist.is_empty(); | ||
| 491 | |||
| 492 | // Fatal error: archive_read_only=true without archive mode enabled | ||
| 493 | if let Some(true) = self.archive_read_only { | ||
| 494 | if !archive_enabled { | ||
| 495 | return Err(anyhow!( | ||
| 496 | "NGIT_ARCHIVE_READ_ONLY=true requires either NGIT_ARCHIVE_ALL=true or NGIT_ARCHIVE_WHITELIST to be set" | ||
| 497 | )); | ||
| 498 | } | ||
| 499 | } | ||
| 500 | |||
| 501 | // Validate repository whitelist configuration | ||
| 502 | let repository_whitelist = WhitelistEntry::parse_whitelist(&self.repository_whitelist); | ||
| 503 | |||
| 504 | // Fatal error: repository_whitelist with archive_read_only=true (incompatible) | ||
| 505 | if !repository_whitelist.is_empty() { | ||
| 506 | let read_only = self.archive_read_only.unwrap_or(archive_enabled); | ||
| 507 | if read_only { | ||
| 508 | return Err(anyhow!( | ||
| 509 | "NGIT_REPOSITORY_WHITELIST cannot be used with NGIT_ARCHIVE_READ_ONLY=true. \ | ||
| 510 | Archive read-only mode rejects announcements that don't match the archive whitelist, \ | ||
| 511 | regardless of service listing. Either set NGIT_ARCHIVE_READ_ONLY=false or use \ | ||
| 512 | NGIT_ARCHIVE_WHITELIST instead of NGIT_REPOSITORY_WHITELIST." | ||
| 513 | )); | ||
| 514 | } | ||
| 515 | } | ||
| 516 | |||
| 517 | Ok(()) | ||
| 518 | } | ||
| 519 | |||
| 467 | /// Get parsed archive configuration with computed read-only mode | 520 | /// Get parsed archive configuration with computed read-only mode |
| 468 | /// | 521 | /// |
| 469 | /// Read-only mode defaults to true if archive mode is enabled, false otherwise. | 522 | /// Read-only mode defaults to true if archive mode is enabled, false otherwise. |
| 470 | /// Throws error if explicitly set to true without archive mode enabled. | 523 | /// This method assumes config has been validated - call Config::validate() first! |
| 471 | pub fn archive_config(&self) -> Result<ArchiveConfig> { | 524 | pub fn archive_config(&self) -> ArchiveConfig { |
| 472 | let whitelist = WhitelistEntry::parse_whitelist(&self.archive_whitelist)?; | 525 | let whitelist = WhitelistEntry::parse_whitelist(&self.archive_whitelist); |
| 473 | let archive_enabled = self.archive_all || !whitelist.is_empty(); | 526 | let archive_enabled = self.archive_all || !whitelist.is_empty(); |
| 474 | 527 | ||
| 475 | let read_only = match self.archive_read_only { | 528 | let read_only = match self.archive_read_only { |
| 476 | Some(true) => { | 529 | Some(true) => true, // Already validated in validate() |
| 477 | if !archive_enabled { | ||
| 478 | return Err(anyhow!( | ||
| 479 | "NGIT_ARCHIVE_READ_ONLY=true requires either NGIT_ARCHIVE_ALL=true or NGIT_ARCHIVE_WHITELIST to be set" | ||
| 480 | )); | ||
| 481 | } | ||
| 482 | true | ||
| 483 | } | ||
| 484 | Some(false) => false, | 530 | Some(false) => false, |
| 485 | None => { | 531 | None => { |
| 486 | // Default: true if archive mode enabled, false otherwise | 532 | // Default: true if archive mode enabled, false otherwise |
| @@ -488,33 +534,19 @@ impl Config { | |||
| 488 | } | 534 | } |
| 489 | }; | 535 | }; |
| 490 | 536 | ||
| 491 | Ok(ArchiveConfig { | 537 | ArchiveConfig { |
| 492 | archive_all: self.archive_all, | 538 | archive_all: self.archive_all, |
| 493 | whitelist, | 539 | whitelist, |
| 494 | read_only, | 540 | read_only, |
| 495 | }) | 541 | } |
| 496 | } | 542 | } |
| 497 | 543 | ||
| 498 | /// Get parsed repository whitelist configuration | 544 | /// Get parsed repository whitelist configuration |
| 499 | /// | 545 | /// |
| 500 | /// Throws error if repository_whitelist is set together with archive_read_only=true | 546 | /// This method assumes config has been validated - call Config::validate() first! |
| 501 | pub fn repository_config(&self) -> Result<RepositoryConfig> { | 547 | pub fn repository_config(&self) -> RepositoryConfig { |
| 502 | let whitelist = WhitelistEntry::parse_whitelist(&self.repository_whitelist)?; | 548 | let whitelist = WhitelistEntry::parse_whitelist(&self.repository_whitelist); |
| 503 | 549 | RepositoryConfig { whitelist } | |
| 504 | // Validate incompatible configurations | ||
| 505 | if !whitelist.is_empty() { | ||
| 506 | let archive_config = self.archive_config()?; | ||
| 507 | if archive_config.read_only { | ||
| 508 | return Err(anyhow!( | ||
| 509 | "NGIT_REPOSITORY_WHITELIST cannot be used with NGIT_ARCHIVE_READ_ONLY=true. \ | ||
| 510 | Archive read-only mode rejects announcements that don't match the archive whitelist, \ | ||
| 511 | regardless of service listing. Either set NGIT_ARCHIVE_READ_ONLY=false or use \ | ||
| 512 | NGIT_ARCHIVE_WHITELIST instead of NGIT_REPOSITORY_WHITELIST." | ||
| 513 | )); | ||
| 514 | } | ||
| 515 | } | ||
| 516 | |||
| 517 | Ok(RepositoryConfig { whitelist }) | ||
| 518 | } | 550 | } |
| 519 | 551 | ||
| 520 | /// Create config for testing | 552 | /// Create config for testing |
| @@ -807,10 +839,10 @@ mod tests { | |||
| 807 | 839 | ||
| 808 | #[test] | 840 | #[test] |
| 809 | fn test_parse_whitelist_empty() { | 841 | fn test_parse_whitelist_empty() { |
| 810 | let whitelist = WhitelistEntry::parse_whitelist("").unwrap(); | 842 | let whitelist = WhitelistEntry::parse_whitelist(""); |
| 811 | assert!(whitelist.is_empty()); | 843 | assert!(whitelist.is_empty()); |
| 812 | 844 | ||
| 813 | let whitelist = WhitelistEntry::parse_whitelist(" ").unwrap(); | 845 | let whitelist = WhitelistEntry::parse_whitelist(" "); |
| 814 | assert!(whitelist.is_empty()); | 846 | assert!(whitelist.is_empty()); |
| 815 | } | 847 | } |
| 816 | 848 | ||
| @@ -823,12 +855,22 @@ mod tests { | |||
| 823 | let whitelist = WhitelistEntry::parse_whitelist(&format!( | 855 | let whitelist = WhitelistEntry::parse_whitelist(&format!( |
| 824 | "{},bitcoin-core,{}/linux", | 856 | "{},bitcoin-core,{}/linux", |
| 825 | test_npub1, test_npub2 | 857 | test_npub1, test_npub2 |
| 826 | )) | 858 | )); |
| 827 | .unwrap(); | ||
| 828 | assert_eq!(whitelist.len(), 3); | 859 | assert_eq!(whitelist.len(), 3); |
| 829 | } | 860 | } |
| 830 | 861 | ||
| 831 | #[test] | 862 | #[test] |
| 863 | fn test_parse_whitelist_invalid_npub_skipped() { | ||
| 864 | // Invalid entries should be skipped with warnings, not fail | ||
| 865 | let whitelist = WhitelistEntry::parse_whitelist("npub1invalid,bitcoin-core"); | ||
| 866 | assert_eq!(whitelist.len(), 1); // Only bitcoin-core should be parsed | ||
| 867 | assert!(matches!( | ||
| 868 | &whitelist[0], | ||
| 869 | WhitelistEntry::Identifier(id) if id == "bitcoin-core" | ||
| 870 | )); | ||
| 871 | } | ||
| 872 | |||
| 873 | #[test] | ||
| 832 | fn test_archive_config_parsing() { | 874 | fn test_archive_config_parsing() { |
| 833 | let keys = Keys::generate(); | 875 | let keys = Keys::generate(); |
| 834 | let test_npub = keys.public_key().to_bech32().unwrap(); | 876 | let test_npub = keys.public_key().to_bech32().unwrap(); |
| @@ -836,31 +878,22 @@ mod tests { | |||
| 836 | archive_whitelist: format!("{},bitcoin-core", test_npub), | 878 | archive_whitelist: format!("{},bitcoin-core", test_npub), |
| 837 | ..Config::for_testing() | 879 | ..Config::for_testing() |
| 838 | }; | 880 | }; |
| 839 | let archive_config = config.archive_config().unwrap(); | 881 | let archive_config = config.archive_config(); |
| 840 | assert_eq!(archive_config.whitelist.len(), 2); | 882 | assert_eq!(archive_config.whitelist.len(), 2); |
| 841 | } | 883 | } |
| 842 | 884 | ||
| 843 | #[test] | 885 | #[test] |
| 844 | fn test_archive_config_invalid_npub() { | ||
| 845 | let config = Config { | ||
| 846 | archive_whitelist: "npub1invalid".to_string(), | ||
| 847 | ..Config::for_testing() | ||
| 848 | }; | ||
| 849 | assert!(config.archive_config().is_err()); | ||
| 850 | } | ||
| 851 | |||
| 852 | #[test] | ||
| 853 | fn test_archive_read_only_defaults() { | 886 | fn test_archive_read_only_defaults() { |
| 854 | // Default: false when no archive mode | 887 | // Default: false when no archive mode |
| 855 | let config = Config::for_testing(); | 888 | let config = Config::for_testing(); |
| 856 | assert_eq!(config.archive_config().unwrap().read_only, false); | 889 | assert_eq!(config.archive_config().read_only, false); |
| 857 | 890 | ||
| 858 | // Default: true when archive_all is set | 891 | // Default: true when archive_all is set |
| 859 | let config = Config { | 892 | let config = Config { |
| 860 | archive_all: true, | 893 | archive_all: true, |
| 861 | ..Config::for_testing() | 894 | ..Config::for_testing() |
| 862 | }; | 895 | }; |
| 863 | assert_eq!(config.archive_config().unwrap().read_only, true); | 896 | assert_eq!(config.archive_config().read_only, true); |
| 864 | 897 | ||
| 865 | // Default: true when archive_whitelist is set | 898 | // Default: true when archive_whitelist is set |
| 866 | let keys = Keys::generate(); | 899 | let keys = Keys::generate(); |
| @@ -869,7 +902,7 @@ mod tests { | |||
| 869 | archive_whitelist: test_npub, | 902 | archive_whitelist: test_npub, |
| 870 | ..Config::for_testing() | 903 | ..Config::for_testing() |
| 871 | }; | 904 | }; |
| 872 | assert_eq!(config.archive_config().unwrap().read_only, true); | 905 | assert_eq!(config.archive_config().read_only, true); |
| 873 | } | 906 | } |
| 874 | 907 | ||
| 875 | #[test] | 908 | #[test] |
| @@ -880,7 +913,7 @@ mod tests { | |||
| 880 | archive_read_only: Some(true), | 913 | archive_read_only: Some(true), |
| 881 | ..Config::for_testing() | 914 | ..Config::for_testing() |
| 882 | }; | 915 | }; |
| 883 | assert_eq!(config.archive_config().unwrap().read_only, true); | 916 | assert_eq!(config.archive_config().read_only, true); |
| 884 | 917 | ||
| 885 | // Explicit false with archive_all (unusual but allowed) | 918 | // Explicit false with archive_all (unusual but allowed) |
| 886 | let config = Config { | 919 | let config = Config { |
| @@ -888,29 +921,26 @@ mod tests { | |||
| 888 | archive_read_only: Some(false), | 921 | archive_read_only: Some(false), |
| 889 | ..Config::for_testing() | 922 | ..Config::for_testing() |
| 890 | }; | 923 | }; |
| 891 | assert_eq!(config.archive_config().unwrap().read_only, false); | 924 | assert_eq!(config.archive_config().read_only, false); |
| 892 | 925 | ||
| 893 | // Explicit false without archive mode | 926 | // Explicit false without archive mode |
| 894 | let config = Config { | 927 | let config = Config { |
| 895 | archive_read_only: Some(false), | 928 | archive_read_only: Some(false), |
| 896 | ..Config::for_testing() | 929 | ..Config::for_testing() |
| 897 | }; | 930 | }; |
| 898 | assert_eq!(config.archive_config().unwrap().read_only, false); | 931 | assert_eq!(config.archive_config().read_only, false); |
| 899 | } | 932 | } |
| 900 | 933 | ||
| 901 | #[test] | 934 | #[test] |
| 902 | fn test_archive_read_only_error() { | 935 | fn test_archive_read_only_validation_error() { |
| 903 | // Error: true without archive mode | 936 | // Error: true without archive mode should fail validation |
| 904 | let config = Config { | 937 | let config = Config { |
| 905 | archive_read_only: Some(true), | 938 | archive_read_only: Some(true), |
| 906 | ..Config::for_testing() | 939 | ..Config::for_testing() |
| 907 | }; | 940 | }; |
| 908 | assert!(config.archive_config().is_err()); | 941 | let result = config.validate(); |
| 909 | assert!(config | 942 | assert!(result.is_err()); |
| 910 | .archive_config() | 943 | assert!(result.unwrap_err().to_string().contains("requires either")); |
| 911 | .unwrap_err() | ||
| 912 | .to_string() | ||
| 913 | .contains("requires either")); | ||
| 914 | } | 944 | } |
| 915 | 945 | ||
| 916 | #[test] | 946 | #[test] |
| @@ -921,7 +951,7 @@ mod tests { | |||
| 921 | repository_whitelist: format!("{},bitcoin-core", test_npub), | 951 | repository_whitelist: format!("{},bitcoin-core", test_npub), |
| 922 | ..Config::for_testing() | 952 | ..Config::for_testing() |
| 923 | }; | 953 | }; |
| 924 | let repo_config = config.repository_config().unwrap(); | 954 | let repo_config = config.repository_config(); |
| 925 | assert_eq!(repo_config.whitelist.len(), 2); | 955 | assert_eq!(repo_config.whitelist.len(), 2); |
| 926 | assert!(repo_config.enabled()); | 956 | assert!(repo_config.enabled()); |
| 927 | } | 957 | } |
| @@ -929,13 +959,13 @@ mod tests { | |||
| 929 | #[test] | 959 | #[test] |
| 930 | fn test_repository_whitelist_empty() { | 960 | fn test_repository_whitelist_empty() { |
| 931 | let config = Config::for_testing(); | 961 | let config = Config::for_testing(); |
| 932 | let repo_config = config.repository_config().unwrap(); | 962 | let repo_config = config.repository_config(); |
| 933 | assert!(repo_config.whitelist.is_empty()); | 963 | assert!(repo_config.whitelist.is_empty()); |
| 934 | assert!(!repo_config.enabled()); | 964 | assert!(!repo_config.enabled()); |
| 935 | } | 965 | } |
| 936 | 966 | ||
| 937 | #[test] | 967 | #[test] |
| 938 | fn test_repository_whitelist_incompatible_with_archive_read_only() { | 968 | fn test_repository_whitelist_validation_incompatible_with_archive_read_only() { |
| 939 | let keys = Keys::generate(); | 969 | let keys = Keys::generate(); |
| 940 | let test_npub = keys.public_key().to_bech32().unwrap(); | 970 | let test_npub = keys.public_key().to_bech32().unwrap(); |
| 941 | let config = Config { | 971 | let config = Config { |
| @@ -944,7 +974,7 @@ mod tests { | |||
| 944 | repository_whitelist: test_npub, | 974 | repository_whitelist: test_npub, |
| 945 | ..Config::for_testing() | 975 | ..Config::for_testing() |
| 946 | }; | 976 | }; |
| 947 | let result = config.repository_config(); | 977 | let result = config.validate(); |
| 948 | assert!(result.is_err()); | 978 | assert!(result.is_err()); |
| 949 | let err = result.unwrap_err().to_string(); | 979 | let err = result.unwrap_err().to_string(); |
| 950 | assert!(err.contains("cannot be used with")); | 980 | assert!(err.contains("cannot be used with")); |
| @@ -961,8 +991,8 @@ mod tests { | |||
| 961 | repository_whitelist: test_npub, | 991 | repository_whitelist: test_npub, |
| 962 | ..Config::for_testing() | 992 | ..Config::for_testing() |
| 963 | }; | 993 | }; |
| 964 | // Should not error | 994 | // Should not error on validation |
| 965 | assert!(config.repository_config().is_ok()); | 995 | assert!(config.validate().is_ok()); |
| 966 | } | 996 | } |
| 967 | 997 | ||
| 968 | #[test] | 998 | #[test] |
| @@ -980,4 +1010,99 @@ mod tests { | |||
| 980 | assert!(config.matches("npub1bob", "bitcoin-core")); | 1010 | assert!(config.matches("npub1bob", "bitcoin-core")); |
| 981 | assert!(!config.matches("npub1bob", "other-repo")); | 1011 | assert!(!config.matches("npub1bob", "other-repo")); |
| 982 | } | 1012 | } |
| 1013 | |||
| 1014 | #[test] | ||
| 1015 | fn test_validate_success_with_valid_config() { | ||
| 1016 | // Valid config should pass validation | ||
| 1017 | let keys = Keys::generate(); | ||
| 1018 | let test_npub = keys.public_key().to_bech32().unwrap(); | ||
| 1019 | let config = Config { | ||
| 1020 | archive_whitelist: format!("{},bitcoin-core", test_npub), | ||
| 1021 | archive_read_only: Some(false), | ||
| 1022 | repository_whitelist: "rust".to_string(), | ||
| 1023 | ..Config::for_testing() | ||
| 1024 | }; | ||
| 1025 | assert!(config.validate().is_ok()); | ||
| 1026 | } | ||
| 1027 | |||
| 1028 | #[test] | ||
| 1029 | fn test_validate_with_all_invalid_whitelist_entries() { | ||
| 1030 | // All invalid entries should be skipped with warnings, but validation should succeed | ||
| 1031 | let config = Config { | ||
| 1032 | archive_whitelist: "npub1invalid,npub1bad,npub1wrong".to_string(), | ||
| 1033 | ..Config::for_testing() | ||
| 1034 | }; | ||
| 1035 | assert!(config.validate().is_ok()); | ||
| 1036 | // All entries should be skipped | ||
| 1037 | let archive_config = config.archive_config(); | ||
| 1038 | assert_eq!(archive_config.whitelist.len(), 0); | ||
| 1039 | assert!(!archive_config.enabled()); | ||
| 1040 | } | ||
| 1041 | |||
| 1042 | #[test] | ||
| 1043 | fn test_validate_with_mixed_valid_invalid_entries() { | ||
| 1044 | let keys = Keys::generate(); | ||
| 1045 | let test_npub = keys.public_key().to_bech32().unwrap(); | ||
| 1046 | // Mixed valid and invalid entries - should keep valid ones | ||
| 1047 | let config = Config { | ||
| 1048 | repository_whitelist: format!("npub1invalid,{},bitcoin-core,npub1bad", test_npub), | ||
| 1049 | ..Config::for_testing() | ||
| 1050 | }; | ||
| 1051 | assert!(config.validate().is_ok()); | ||
| 1052 | let repo_config = config.repository_config(); | ||
| 1053 | // Should have 2 valid entries: the test_npub and bitcoin-core | ||
| 1054 | assert_eq!(repo_config.whitelist.len(), 2); | ||
| 1055 | } | ||
| 1056 | |||
| 1057 | #[test] | ||
| 1058 | fn test_whitelist_entry_with_extra_whitespace() { | ||
| 1059 | let keys = Keys::generate(); | ||
| 1060 | let test_npub = keys.public_key().to_bech32().unwrap(); | ||
| 1061 | // Whitespace should be trimmed | ||
| 1062 | let whitelist = | ||
| 1063 | WhitelistEntry::parse_whitelist(&format!(" {} , bitcoin-core , rust ", test_npub)); | ||
| 1064 | assert_eq!(whitelist.len(), 3); | ||
| 1065 | } | ||
| 1066 | |||
| 1067 | #[test] | ||
| 1068 | fn test_archive_config_with_all_invalid_entries_not_enabled() { | ||
| 1069 | // If all whitelist entries are invalid, archive mode should not be enabled | ||
| 1070 | let config = Config { | ||
| 1071 | archive_whitelist: "npub1invalid,npub1bad".to_string(), | ||
| 1072 | ..Config::for_testing() | ||
| 1073 | }; | ||
| 1074 | let archive_config = config.archive_config(); | ||
| 1075 | assert!(!archive_config.enabled()); | ||
| 1076 | assert_eq!(archive_config.whitelist.len(), 0); | ||
| 1077 | } | ||
| 1078 | |||
| 1079 | #[test] | ||
| 1080 | fn test_validate_detects_invalid_relay_owner_nsec() { | ||
| 1081 | // Invalid nsec should fail validation | ||
| 1082 | let config = Config { | ||
| 1083 | relay_owner_nsec: Some("nsec1invalid".to_string()), | ||
| 1084 | ..Config::for_testing() | ||
| 1085 | }; | ||
| 1086 | let result = config.validate(); | ||
| 1087 | assert!(result.is_err()); | ||
| 1088 | assert!(result | ||
| 1089 | .unwrap_err() | ||
| 1090 | .to_string() | ||
| 1091 | .contains("Invalid relay_owner_nsec")); | ||
| 1092 | } | ||
| 1093 | |||
| 1094 | #[test] | ||
| 1095 | fn test_validate_requires_relay_owner_nsec() { | ||
| 1096 | // Missing nsec should fail validation | ||
| 1097 | let config = Config { | ||
| 1098 | relay_owner_nsec: None, | ||
| 1099 | ..Config::for_testing() | ||
| 1100 | }; | ||
| 1101 | let result = config.validate(); | ||
| 1102 | assert!(result.is_err()); | ||
| 1103 | assert!(result | ||
| 1104 | .unwrap_err() | ||
| 1105 | .to_string() | ||
| 1106 | .contains("relay_owner_nsec not set")); | ||
| 1107 | } | ||
| 983 | } | 1108 | } |