upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/config.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/config.rs')
-rw-r--r--src/config.rs261
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}