store.rs (6854B)
1 use crate::error::RadrootsNostrAccountsError; 2 use crate::model::RadrootsNostrAccountStoreState; 3 use radroots_runtime::json::{JsonFile, JsonWriteOptions}; 4 use std::path::{Path, PathBuf}; 5 use std::sync::{Arc, RwLock}; 6 7 pub trait RadrootsNostrAccountStore: Send + Sync { 8 fn load(&self) -> Result<RadrootsNostrAccountStoreState, RadrootsNostrAccountsError>; 9 fn save( 10 &self, 11 state: &RadrootsNostrAccountStoreState, 12 ) -> Result<(), RadrootsNostrAccountsError>; 13 } 14 15 #[derive(Debug, Clone)] 16 pub struct RadrootsNostrFileAccountStore { 17 path: PathBuf, 18 } 19 20 impl RadrootsNostrFileAccountStore { 21 pub fn new(path: impl AsRef<Path>) -> Self { 22 Self { 23 path: path.as_ref().to_path_buf(), 24 } 25 } 26 27 pub fn path(&self) -> &Path { 28 self.path.as_path() 29 } 30 } 31 32 #[derive(Debug, Clone, Default)] 33 pub struct RadrootsNostrMemoryAccountStore { 34 state: Arc<RwLock<RadrootsNostrAccountStoreState>>, 35 } 36 37 impl RadrootsNostrMemoryAccountStore { 38 pub fn new() -> Self { 39 Self::default() 40 } 41 } 42 43 impl RadrootsNostrAccountStore for RadrootsNostrFileAccountStore { 44 fn load(&self) -> Result<RadrootsNostrAccountStoreState, RadrootsNostrAccountsError> { 45 if !self.path.exists() { 46 return Ok(RadrootsNostrAccountStoreState::default()); 47 } 48 let file = JsonFile::<RadrootsNostrAccountStoreState>::load(self.path.as_path())?; 49 Ok(file.value) 50 } 51 52 fn save( 53 &self, 54 state: &RadrootsNostrAccountStoreState, 55 ) -> Result<(), RadrootsNostrAccountsError> { 56 let mut file = JsonFile::load_or_create_with(self.path.as_path(), || state.clone())?; 57 file.set_options(JsonWriteOptions { 58 pretty: true, 59 mode_unix: Some(0o600), 60 }); 61 file.value = state.clone(); 62 if let Err(err) = file.save() { 63 return Err(err.into()); 64 } 65 Ok(()) 66 } 67 } 68 69 impl RadrootsNostrAccountStore for RadrootsNostrMemoryAccountStore { 70 fn load(&self) -> Result<RadrootsNostrAccountStoreState, RadrootsNostrAccountsError> { 71 let guard = self 72 .state 73 .read() 74 .map_err(|_| RadrootsNostrAccountsError::Store("memory store lock poisoned".into()))?; 75 Ok(guard.clone()) 76 } 77 78 fn save( 79 &self, 80 state: &RadrootsNostrAccountStoreState, 81 ) -> Result<(), RadrootsNostrAccountsError> { 82 let mut guard = self 83 .state 84 .write() 85 .map_err(|_| RadrootsNostrAccountsError::Store("memory store lock poisoned".into()))?; 86 *guard = state.clone(); 87 Ok(()) 88 } 89 } 90 91 #[cfg(test)] 92 mod tests { 93 use super::*; 94 use std::thread; 95 96 #[test] 97 fn file_store_round_trip() { 98 let temp = tempfile::tempdir().expect("tempdir"); 99 let path = temp.path().join("accounts.json"); 100 let store = RadrootsNostrFileAccountStore::new(path.as_path()); 101 102 let state = RadrootsNostrAccountStoreState::default(); 103 store.save(&state).expect("save"); 104 let loaded = store.load().expect("load"); 105 assert_eq!(loaded.version, state.version); 106 assert!(loaded.accounts.is_empty()); 107 } 108 109 #[test] 110 fn file_store_load_missing_and_path_accessor() { 111 let temp = tempfile::tempdir().expect("tempdir"); 112 let path = temp.path().join("missing.json"); 113 let store = RadrootsNostrFileAccountStore::new(path.as_path()); 114 115 assert_eq!(store.path(), path.as_path()); 116 let loaded = store.load().expect("load"); 117 assert_eq!( 118 loaded.version, 119 RadrootsNostrAccountStoreState::default().version 120 ); 121 assert!(loaded.accounts.is_empty()); 122 } 123 124 #[test] 125 fn file_store_load_reports_parse_error() { 126 let temp = tempfile::tempdir().expect("tempdir"); 127 let path = temp.path().join("invalid.json"); 128 std::fs::write(&path, "{").expect("write invalid json"); 129 let store = RadrootsNostrFileAccountStore::new(path.as_path()); 130 131 let err = store.load().expect_err("invalid json"); 132 assert!(err.to_string().starts_with("store error:")); 133 } 134 135 #[test] 136 fn file_store_save_reports_parse_error() { 137 let temp = tempfile::tempdir().expect("tempdir"); 138 let path = temp.path().join("invalid.json"); 139 std::fs::write(&path, "{").expect("write invalid json"); 140 let store = RadrootsNostrFileAccountStore::new(path.as_path()); 141 142 let err = store 143 .save(&RadrootsNostrAccountStoreState::default()) 144 .expect_err("invalid json save"); 145 assert!(err.to_string().starts_with("store error:")); 146 } 147 148 #[cfg(unix)] 149 #[test] 150 fn file_store_save_reports_write_error() { 151 use std::os::unix::fs::PermissionsExt; 152 153 let temp = tempfile::tempdir().expect("tempdir"); 154 let path = temp.path().join("accounts.json"); 155 let json = 156 serde_json::to_string(&RadrootsNostrAccountStoreState::default()).expect("serialize"); 157 std::fs::write(&path, json).expect("write json"); 158 let store = RadrootsNostrFileAccountStore::new(path.as_path()); 159 160 let mut perms = std::fs::metadata(temp.path()) 161 .expect("dir metadata") 162 .permissions(); 163 perms.set_mode(0o500); 164 std::fs::set_permissions(temp.path(), perms).expect("set perms"); 165 166 let err = store 167 .save(&RadrootsNostrAccountStoreState::default()) 168 .expect_err("read-only save"); 169 assert!(err.to_string().starts_with("store error:")); 170 171 let mut perms = std::fs::metadata(temp.path()) 172 .expect("dir metadata") 173 .permissions(); 174 perms.set_mode(0o700); 175 std::fs::set_permissions(temp.path(), perms).expect("restore perms"); 176 } 177 178 #[test] 179 fn memory_store_round_trip() { 180 let store = RadrootsNostrMemoryAccountStore::new(); 181 let state = RadrootsNostrAccountStoreState::default(); 182 store.save(&state).expect("save"); 183 184 let loaded = store.load().expect("load"); 185 assert_eq!(loaded.version, state.version); 186 assert_eq!(loaded.default_account_id, state.default_account_id); 187 } 188 189 #[test] 190 fn memory_store_reports_poisoned_lock() { 191 let store = RadrootsNostrMemoryAccountStore::new(); 192 let shared = store.state.clone(); 193 let _ = thread::spawn(move || { 194 let _guard = shared.write().expect("write"); 195 panic!("poison memory store"); 196 }) 197 .join(); 198 199 let load = store.load().expect_err("poisoned load"); 200 assert!(load.to_string().contains("memory store lock poisoned")); 201 202 let save = store 203 .save(&RadrootsNostrAccountStoreState::default()) 204 .expect_err("poisoned save"); 205 assert!(save.to_string().contains("memory store lock poisoned")); 206 } 207 }