manager.rs (75241B)
1 use crate::error::RadrootsNostrAccountsError; 2 use crate::model::{ 3 RadrootsNostrAccountRecord, RadrootsNostrAccountStatus, RadrootsNostrAccountStoreState, 4 }; 5 #[cfg(feature = "memory-vault")] 6 use crate::store::RadrootsNostrMemoryAccountStore; 7 use crate::store::{RadrootsNostrAccountStore, RadrootsNostrFileAccountStore}; 8 #[cfg(feature = "memory-vault")] 9 use crate::vault::RadrootsNostrSecretVaultMemory; 10 #[cfg(feature = "os-keyring")] 11 use crate::vault::RadrootsNostrSecretVaultOsKeyring; 12 use crate::vault::{RadrootsSecretVault, account_secret_slot}; 13 use radroots_identity::{RadrootsIdentity, RadrootsIdentityId, RadrootsIdentityPublic}; 14 use radroots_nostr_signer::prelude::{ 15 RadrootsNostrLocalSignerAvailability, RadrootsNostrLocalSignerCapability, 16 RadrootsNostrSignerCapability, 17 }; 18 use radroots_protected_store::RadrootsProtectedFileSecretVault; 19 use radroots_secret_vault::{ 20 RadrootsResolvedSecretBackend, RadrootsSecretBackend, RadrootsSecretBackendAvailability, 21 RadrootsSecretBackendSelection, RadrootsSecretVaultError, 22 }; 23 use std::path::Path; 24 use std::sync::{Arc, RwLock}; 25 use std::time::{SystemTime, UNIX_EPOCH}; 26 use zeroize::Zeroizing; 27 28 #[derive(Clone)] 29 pub struct RadrootsNostrAccountsManager { 30 store: Arc<dyn RadrootsNostrAccountStore>, 31 vault: Arc<dyn RadrootsSecretVault>, 32 state: Arc<RwLock<RadrootsNostrAccountStoreState>>, 33 } 34 35 impl RadrootsNostrAccountsManager { 36 #[cfg(feature = "memory-vault")] 37 pub fn new_in_memory() -> Self { 38 Self { 39 store: Arc::new(RadrootsNostrMemoryAccountStore::new()), 40 vault: Arc::new(RadrootsNostrSecretVaultMemory::new()), 41 state: Arc::new(RwLock::new(RadrootsNostrAccountStoreState::default())), 42 } 43 } 44 45 pub fn new( 46 store: Arc<dyn RadrootsNostrAccountStore>, 47 vault: Arc<dyn RadrootsSecretVault>, 48 ) -> Result<Self, RadrootsNostrAccountsError> { 49 let mut state = store.load()?; 50 let mut state_dirty = match state.version { 51 1 => { 52 state.version = crate::model::RADROOTS_NOSTR_ACCOUNTS_STORE_VERSION; 53 true 54 } 55 crate::model::RADROOTS_NOSTR_ACCOUNTS_STORE_VERSION => false, 56 _ => { 57 return Err(RadrootsNostrAccountsError::InvalidState(format!( 58 "unsupported accounts schema version {}", 59 state.version 60 ))); 61 } 62 }; 63 64 if let Some(default_account_id) = state.default_account_id.clone() { 65 let exists = state 66 .accounts 67 .iter() 68 .any(|record| record.account_id == default_account_id); 69 if !exists { 70 state.default_account_id = None; 71 state_dirty = true; 72 } 73 } 74 75 if state_dirty { 76 store.save(&state)?; 77 } 78 79 Ok(Self { 80 store, 81 vault, 82 state: Arc::new(RwLock::new(state)), 83 }) 84 } 85 86 pub fn new_file_backed( 87 path: impl AsRef<Path>, 88 vault: Arc<dyn RadrootsSecretVault>, 89 ) -> Result<Self, RadrootsNostrAccountsError> { 90 Self::new( 91 Arc::new(RadrootsNostrFileAccountStore::new(path.as_ref())), 92 vault, 93 ) 94 } 95 96 pub fn new_file_backed_with_vault<V>( 97 path: impl AsRef<Path>, 98 vault: V, 99 ) -> Result<Self, RadrootsNostrAccountsError> 100 where 101 V: RadrootsSecretVault + 'static, 102 { 103 Self::new_file_backed(path, Arc::new(vault)) 104 } 105 106 pub fn resolve_local_backend( 107 selection: RadrootsSecretBackendSelection, 108 availability: RadrootsSecretBackendAvailability, 109 ) -> Result<RadrootsResolvedSecretBackend, RadrootsSecretVaultError> { 110 selection.resolve(availability) 111 } 112 113 pub fn new_local_file_backed( 114 path: impl AsRef<Path>, 115 secrets_dir: impl AsRef<Path>, 116 selection: RadrootsSecretBackendSelection, 117 availability: RadrootsSecretBackendAvailability, 118 host_vault_service_name: impl Into<String>, 119 ) -> Result<(Self, RadrootsResolvedSecretBackend), RadrootsNostrAccountsError> { 120 let resolved = Self::resolve_local_backend(selection, availability) 121 .map_err(|error| RadrootsNostrAccountsError::Vault(error.to_string()))?; 122 let vault = local_file_backed_secret_vault( 123 resolved.backend, 124 secrets_dir.as_ref(), 125 host_vault_service_name.into(), 126 )?; 127 let manager = Self::new_file_backed(path, vault)?; 128 Ok((manager, resolved)) 129 } 130 131 pub fn list_accounts( 132 &self, 133 ) -> Result<Vec<RadrootsNostrAccountRecord>, RadrootsNostrAccountsError> { 134 let guard = self.state.read().map_err(|_| { 135 RadrootsNostrAccountsError::Store("accounts state lock poisoned".into()) 136 })?; 137 Ok(guard.accounts.clone()) 138 } 139 140 pub fn default_account_id( 141 &self, 142 ) -> Result<Option<RadrootsIdentityId>, RadrootsNostrAccountsError> { 143 let guard = self.state.read().map_err(|_| { 144 RadrootsNostrAccountsError::Store("accounts state lock poisoned".into()) 145 })?; 146 Ok(guard.default_account_id.clone()) 147 } 148 149 pub fn default_account( 150 &self, 151 ) -> Result<Option<RadrootsNostrAccountRecord>, RadrootsNostrAccountsError> { 152 let guard = self.state.read().map_err(|_| { 153 RadrootsNostrAccountsError::Store("accounts state lock poisoned".into()) 154 })?; 155 let Some(default_account_id) = guard.default_account_id.as_ref() else { 156 return Ok(None); 157 }; 158 Ok(guard 159 .accounts 160 .iter() 161 .find(|record| &record.account_id == default_account_id) 162 .cloned()) 163 } 164 165 pub fn default_public_identity( 166 &self, 167 ) -> Result<Option<RadrootsIdentityPublic>, RadrootsNostrAccountsError> { 168 Ok(self 169 .default_account()? 170 .map(|record| record.public_identity.clone())) 171 } 172 173 pub fn default_account_status( 174 &self, 175 ) -> Result<RadrootsNostrAccountStatus, RadrootsNostrAccountsError> { 176 let Some(record) = self.default_account()? else { 177 return Ok(RadrootsNostrAccountStatus::NotConfigured); 178 }; 179 180 Ok(match self.local_signer_availability(&record)? { 181 RadrootsNostrLocalSignerAvailability::PublicOnly => { 182 RadrootsNostrAccountStatus::PublicOnly { account: record } 183 } 184 RadrootsNostrLocalSignerAvailability::SecretBacked => { 185 RadrootsNostrAccountStatus::Ready { account: record } 186 } 187 }) 188 } 189 190 pub fn default_signing_identity( 191 &self, 192 ) -> Result<Option<RadrootsIdentity>, RadrootsNostrAccountsError> { 193 let Some(record) = self.default_account()? else { 194 return Ok(None); 195 }; 196 self.resolve_signing_identity(record) 197 } 198 199 pub fn get_signing_identity( 200 &self, 201 account_id: &RadrootsIdentityId, 202 ) -> Result<Option<RadrootsIdentity>, RadrootsNostrAccountsError> { 203 let guard = self.state.read().map_err(|_| { 204 RadrootsNostrAccountsError::Store("accounts state lock poisoned".into()) 205 })?; 206 let Some(record) = guard 207 .accounts 208 .iter() 209 .find(|record| &record.account_id == account_id) 210 .cloned() 211 else { 212 return Ok(None); 213 }; 214 drop(guard); 215 self.resolve_signing_identity(record) 216 } 217 218 pub fn default_signer_capability( 219 &self, 220 ) -> Result<Option<RadrootsNostrSignerCapability>, RadrootsNostrAccountsError> { 221 let Some(record) = self.default_account()? else { 222 return Ok(None); 223 }; 224 Ok(Some(self.local_signer_capability(record)?)) 225 } 226 227 pub fn get_signer_capability( 228 &self, 229 account_id: &RadrootsIdentityId, 230 ) -> Result<Option<RadrootsNostrSignerCapability>, RadrootsNostrAccountsError> { 231 let guard = self.state.read().map_err(|_| { 232 RadrootsNostrAccountsError::Store("accounts state lock poisoned".into()) 233 })?; 234 let Some(record) = guard 235 .accounts 236 .iter() 237 .find(|record| &record.account_id == account_id) 238 .cloned() 239 else { 240 return Ok(None); 241 }; 242 drop(guard); 243 Ok(Some(self.local_signer_capability(record)?)) 244 } 245 246 pub fn resolve_signing_identity_for_signer( 247 &self, 248 signer: &RadrootsNostrSignerCapability, 249 ) -> Result<Option<RadrootsIdentity>, RadrootsNostrAccountsError> { 250 match signer { 251 RadrootsNostrSignerCapability::LocalAccount(capability) => { 252 self.get_signing_identity(&capability.account_id) 253 } 254 RadrootsNostrSignerCapability::RemoteSession(_) => Ok(None), 255 } 256 } 257 258 pub fn upsert_identity( 259 &self, 260 identity: &RadrootsIdentity, 261 label: Option<String>, 262 make_default: bool, 263 ) -> Result<RadrootsIdentityId, RadrootsNostrAccountsError> { 264 let account_id = identity.id(); 265 let secret_key_hex = Zeroizing::new(identity.secret_key_hex()); 266 self.vault.store_secret( 267 account_secret_slot(&account_id).as_str(), 268 secret_key_hex.as_str(), 269 )?; 270 271 let public_identity = identity.to_public(); 272 self.upsert_public_identity(public_identity, label, make_default) 273 } 274 275 /// Attaches matching secret material to an existing account without import semantics. 276 pub fn attach_identity_secret( 277 &self, 278 account_id: &RadrootsIdentityId, 279 identity: &RadrootsIdentity, 280 make_default: bool, 281 ) -> Result<RadrootsNostrAccountRecord, RadrootsNostrAccountsError> { 282 let account_id = account_id.clone(); 283 let public_key_hex = identity.public_key_hex(); 284 let updated_at_unix = now_unix_secs(); 285 let mut guard = self.state.write().map_err(|_| { 286 RadrootsNostrAccountsError::Store("accounts state lock poisoned".into()) 287 })?; 288 let mut next = guard.clone(); 289 let Some(record) = next 290 .accounts 291 .iter_mut() 292 .find(|record| record.account_id == account_id) 293 else { 294 return Err(RadrootsNostrAccountsError::AccountNotFound( 295 account_id.to_string(), 296 )); 297 }; 298 if record.public_identity.public_key_hex.as_str() != public_key_hex.as_str() { 299 return Err(RadrootsNostrAccountsError::PublicKeyMismatch); 300 } 301 302 let secret_key_hex = Zeroizing::new(identity.secret_key_hex()); 303 self.vault.store_secret( 304 account_secret_slot(&account_id).as_str(), 305 secret_key_hex.as_str(), 306 )?; 307 308 record.touch_updated(updated_at_unix); 309 let updated_record = record.clone(); 310 if make_default { 311 next.default_account_id = Some(account_id); 312 } 313 self.store.save(&next)?; 314 *guard = next; 315 Ok(updated_record) 316 } 317 318 pub fn upsert_public_identity( 319 &self, 320 public_identity: RadrootsIdentityPublic, 321 label: Option<String>, 322 make_default: bool, 323 ) -> Result<RadrootsIdentityId, RadrootsNostrAccountsError> { 324 let updated_at_unix = now_unix_secs(); 325 let account_id = public_identity.id.clone(); 326 self.update_state(|state| { 327 if public_identity.id.as_str() != public_identity.public_key_hex { 328 return Err(RadrootsNostrAccountsError::InvalidState( 329 "public identity id does not match public key".into(), 330 )); 331 } 332 if let Some(existing) = state 333 .accounts 334 .iter_mut() 335 .find(|record| record.account_id == account_id) 336 { 337 existing.public_identity = public_identity.clone(); 338 if let Some(next_label) = label.clone() { 339 existing.label = Some(next_label); 340 } 341 existing.touch_updated(updated_at_unix); 342 } else { 343 state.accounts.push(RadrootsNostrAccountRecord::new( 344 public_identity.clone(), 345 label.clone(), 346 updated_at_unix, 347 )); 348 } 349 350 if state.default_account_id.is_none() || make_default { 351 state.default_account_id = Some(account_id.clone()); 352 } 353 Ok(()) 354 })?; 355 Ok(account_id) 356 } 357 358 pub fn generate_identity( 359 &self, 360 label: Option<String>, 361 make_default: bool, 362 ) -> Result<RadrootsIdentityId, RadrootsNostrAccountsError> { 363 let identity = RadrootsIdentity::generate(); 364 self.upsert_identity(&identity, label, make_default) 365 } 366 367 pub fn set_default_account( 368 &self, 369 account_id: &RadrootsIdentityId, 370 ) -> Result<(), RadrootsNostrAccountsError> { 371 let account_id = account_id.clone(); 372 self.update_state(|state| { 373 let exists = state 374 .accounts 375 .iter() 376 .any(|record| record.account_id == account_id); 377 if !exists { 378 return Err(RadrootsNostrAccountsError::AccountNotFound( 379 account_id.to_string(), 380 )); 381 } 382 state.default_account_id = Some(account_id); 383 Ok(()) 384 }) 385 } 386 387 pub fn clear_default_account(&self) -> Result<(), RadrootsNostrAccountsError> { 388 self.update_state(|state| { 389 state.default_account_id = None; 390 Ok(()) 391 }) 392 } 393 394 pub fn resolve_account_selector( 395 &self, 396 selector: &str, 397 ) -> Result<RadrootsNostrAccountRecord, RadrootsNostrAccountsError> { 398 let normalized = selector.trim(); 399 if normalized.is_empty() { 400 return Err(RadrootsNostrAccountsError::InvalidAccountSelector( 401 "account selector cannot be empty".to_owned(), 402 )); 403 } 404 405 let guard = self.state.read().map_err(|_| { 406 RadrootsNostrAccountsError::Store("accounts state lock poisoned".into()) 407 })?; 408 if let Some(record) = guard 409 .accounts 410 .iter() 411 .find(|record| { 412 record.account_id.as_str() == normalized 413 || record.public_identity.public_key_npub == normalized 414 }) 415 .cloned() 416 { 417 return Ok(record); 418 } 419 420 let mut label_matches = guard 421 .accounts 422 .iter() 423 .filter(|record| record.label.as_deref() == Some(normalized)) 424 .cloned(); 425 let Some(record) = label_matches.next() else { 426 return Err(RadrootsNostrAccountsError::AccountNotFound( 427 normalized.to_owned(), 428 )); 429 }; 430 if label_matches.next().is_some() { 431 return Err(RadrootsNostrAccountsError::AmbiguousAccountSelector( 432 normalized.to_owned(), 433 )); 434 } 435 Ok(record) 436 } 437 438 pub fn remove_account( 439 &self, 440 account_id: &RadrootsIdentityId, 441 ) -> Result<(), RadrootsNostrAccountsError> { 442 let account_id = account_id.clone(); 443 self.update_state(|state| { 444 let before = state.accounts.len(); 445 state 446 .accounts 447 .retain(|record| record.account_id != account_id); 448 if state.accounts.len() == before { 449 return Err(RadrootsNostrAccountsError::AccountNotFound( 450 account_id.to_string(), 451 )); 452 } 453 454 if state.default_account_id.as_ref() == Some(&account_id) { 455 state.default_account_id = None; 456 } 457 Ok(()) 458 })?; 459 self.vault 460 .remove_secret(account_secret_slot(&account_id).as_str())?; 461 Ok(()) 462 } 463 464 pub fn export_secret_hex( 465 &self, 466 account_id: &RadrootsIdentityId, 467 ) -> Result<Option<String>, RadrootsNostrAccountsError> { 468 self.vault 469 .load_secret(account_secret_slot(account_id).as_str()) 470 .map_err(Into::into) 471 } 472 473 pub fn migrate_legacy_identity_file( 474 &self, 475 path: impl AsRef<Path>, 476 label: Option<String>, 477 make_default: bool, 478 ) -> Result<RadrootsIdentityId, RadrootsNostrAccountsError> { 479 let identity = RadrootsIdentity::load_from_path_auto(path)?; 480 self.upsert_identity(&identity, label, make_default) 481 } 482 483 fn resolve_signing_identity( 484 &self, 485 record: RadrootsNostrAccountRecord, 486 ) -> Result<Option<RadrootsIdentity>, RadrootsNostrAccountsError> { 487 let Some(secret_key_hex) = self 488 .vault 489 .load_secret(account_secret_slot(&record.account_id).as_str())? 490 else { 491 return Ok(None); 492 }; 493 let secret_key_hex = Zeroizing::new(secret_key_hex); 494 let mut identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str())?; 495 if identity.public_key_hex() != record.public_identity.public_key_hex { 496 return Err(RadrootsNostrAccountsError::PublicKeyMismatch); 497 } 498 if let Some(profile) = record.public_identity.profile { 499 identity.set_profile(profile); 500 } 501 Ok(Some(identity)) 502 } 503 504 fn local_signer_capability( 505 &self, 506 record: RadrootsNostrAccountRecord, 507 ) -> Result<RadrootsNostrSignerCapability, RadrootsNostrAccountsError> { 508 let availability = self.local_signer_availability(&record)?; 509 Ok(RadrootsNostrSignerCapability::LocalAccount(Box::new( 510 RadrootsNostrLocalSignerCapability::new( 511 record.account_id, 512 record.public_identity, 513 availability, 514 ), 515 ))) 516 } 517 518 fn local_signer_availability( 519 &self, 520 record: &RadrootsNostrAccountRecord, 521 ) -> Result<RadrootsNostrLocalSignerAvailability, RadrootsNostrAccountsError> { 522 let Some(secret_key_hex) = self 523 .vault 524 .load_secret(account_secret_slot(&record.account_id).as_str())? 525 else { 526 return Ok(RadrootsNostrLocalSignerAvailability::PublicOnly); 527 }; 528 529 let secret_key_hex = Zeroizing::new(secret_key_hex); 530 let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str())?; 531 if identity.public_key_hex() != record.public_identity.public_key_hex { 532 return Err(RadrootsNostrAccountsError::PublicKeyMismatch); 533 } 534 Ok(RadrootsNostrLocalSignerAvailability::SecretBacked) 535 } 536 537 fn update_state( 538 &self, 539 update: impl FnOnce( 540 &mut RadrootsNostrAccountStoreState, 541 ) -> Result<(), RadrootsNostrAccountsError>, 542 ) -> Result<(), RadrootsNostrAccountsError> { 543 let mut guard = self.state.write().map_err(|_| { 544 RadrootsNostrAccountsError::Store("accounts state lock poisoned".into()) 545 })?; 546 let mut next = guard.clone(); 547 update(&mut next)?; 548 self.store.save(&next)?; 549 *guard = next; 550 Ok(()) 551 } 552 } 553 554 fn local_file_backed_secret_vault( 555 backend: RadrootsSecretBackend, 556 secrets_dir: &Path, 557 _host_vault_service_name: String, 558 ) -> Result<Arc<dyn RadrootsSecretVault>, RadrootsNostrAccountsError> { 559 match backend { 560 #[cfg(feature = "os-keyring")] 561 RadrootsSecretBackend::HostVault(_) => Ok(Arc::new( 562 RadrootsNostrSecretVaultOsKeyring::new(_host_vault_service_name), 563 )), 564 #[cfg(not(feature = "os-keyring"))] 565 RadrootsSecretBackend::HostVault(_) => Err(RadrootsNostrAccountsError::Vault( 566 "host_vault backend requires radroots_nostr_accounts os-keyring support".into(), 567 )), 568 RadrootsSecretBackend::EncryptedFile => { 569 Ok(Arc::new(RadrootsProtectedFileSecretVault::new(secrets_dir))) 570 } 571 #[cfg(feature = "memory-vault")] 572 RadrootsSecretBackend::Memory => Ok(Arc::new(RadrootsNostrSecretVaultMemory::new())), 573 #[cfg(not(feature = "memory-vault"))] 574 RadrootsSecretBackend::Memory => Err(RadrootsNostrAccountsError::Vault( 575 "memory backend requires radroots_nostr_accounts memory-vault support".into(), 576 )), 577 RadrootsSecretBackend::ExternalCommand => Err(RadrootsNostrAccountsError::Vault( 578 "external_command secret backend is not supported for local accounts".into(), 579 )), 580 } 581 } 582 583 fn now_unix_secs() -> u64 { 584 SystemTime::now() 585 .duration_since(UNIX_EPOCH) 586 .map(|duration| duration.as_secs()) 587 .unwrap_or(0) 588 } 589 590 #[cfg(test)] 591 mod tests { 592 use super::*; 593 use crate::store::{ 594 RadrootsNostrAccountStore, RadrootsNostrFileAccountStore, RadrootsNostrMemoryAccountStore, 595 }; 596 use crate::vault::RadrootsNostrSecretVaultMemory; 597 use crate::vault::RadrootsSecretVault; 598 use radroots_identity::RadrootsIdentityProfile; 599 use radroots_secret_vault::{ 600 RadrootsHostVaultCapabilities, RadrootsSecretBackend, RadrootsSecretBackendAvailability, 601 RadrootsSecretBackendSelection, 602 }; 603 use serde_json::json; 604 use std::fs; 605 use std::sync::Arc; 606 use std::sync::RwLock; 607 use std::thread; 608 609 struct LoadErrorStore; 610 611 impl RadrootsNostrAccountStore for LoadErrorStore { 612 fn load(&self) -> Result<RadrootsNostrAccountStoreState, RadrootsNostrAccountsError> { 613 Err(RadrootsNostrAccountsError::Store( 614 "store load failed".into(), 615 )) 616 } 617 618 fn save( 619 &self, 620 _state: &RadrootsNostrAccountStoreState, 621 ) -> Result<(), RadrootsNostrAccountsError> { 622 Ok(()) 623 } 624 } 625 626 struct SaveErrorStore { 627 state: RwLock<RadrootsNostrAccountStoreState>, 628 } 629 630 impl SaveErrorStore { 631 fn new(state: RadrootsNostrAccountStoreState) -> Self { 632 Self { 633 state: RwLock::new(state), 634 } 635 } 636 } 637 638 impl RadrootsNostrAccountStore for SaveErrorStore { 639 fn load(&self) -> Result<RadrootsNostrAccountStoreState, RadrootsNostrAccountsError> { 640 let guard = self.state.read().map_err(|_| { 641 RadrootsNostrAccountsError::Store("save error store poisoned".into()) 642 })?; 643 Ok(guard.clone()) 644 } 645 646 fn save( 647 &self, 648 _state: &RadrootsNostrAccountStoreState, 649 ) -> Result<(), RadrootsNostrAccountsError> { 650 Err(RadrootsNostrAccountsError::Store( 651 "store save failed".into(), 652 )) 653 } 654 } 655 656 struct VaultStoreError; 657 658 impl RadrootsSecretVault for VaultStoreError { 659 fn store_secret( 660 &self, 661 _slot: &str, 662 _secret: &str, 663 ) -> Result<(), radroots_secret_vault::RadrootsSecretVaultAccessError> { 664 Err( 665 radroots_secret_vault::RadrootsSecretVaultAccessError::Backend( 666 "vault store failed".into(), 667 ), 668 ) 669 } 670 671 fn load_secret( 672 &self, 673 _slot: &str, 674 ) -> Result<Option<String>, radroots_secret_vault::RadrootsSecretVaultAccessError> { 675 Ok(None) 676 } 677 678 fn remove_secret( 679 &self, 680 _slot: &str, 681 ) -> Result<(), radroots_secret_vault::RadrootsSecretVaultAccessError> { 682 Ok(()) 683 } 684 } 685 686 struct VaultLoadError; 687 688 impl RadrootsSecretVault for VaultLoadError { 689 fn store_secret( 690 &self, 691 _slot: &str, 692 _secret: &str, 693 ) -> Result<(), radroots_secret_vault::RadrootsSecretVaultAccessError> { 694 Ok(()) 695 } 696 697 fn load_secret( 698 &self, 699 _slot: &str, 700 ) -> Result<Option<String>, radroots_secret_vault::RadrootsSecretVaultAccessError> { 701 Err( 702 radroots_secret_vault::RadrootsSecretVaultAccessError::Backend( 703 "vault load failed".into(), 704 ), 705 ) 706 } 707 708 fn remove_secret( 709 &self, 710 _slot: &str, 711 ) -> Result<(), radroots_secret_vault::RadrootsSecretVaultAccessError> { 712 Ok(()) 713 } 714 } 715 716 struct VaultInvalidSecret; 717 718 impl RadrootsSecretVault for VaultInvalidSecret { 719 fn store_secret( 720 &self, 721 _slot: &str, 722 _secret: &str, 723 ) -> Result<(), radroots_secret_vault::RadrootsSecretVaultAccessError> { 724 Ok(()) 725 } 726 727 fn load_secret( 728 &self, 729 _slot: &str, 730 ) -> Result<Option<String>, radroots_secret_vault::RadrootsSecretVaultAccessError> { 731 Ok(Some("invalid-secret".to_string())) 732 } 733 734 fn remove_secret( 735 &self, 736 _slot: &str, 737 ) -> Result<(), radroots_secret_vault::RadrootsSecretVaultAccessError> { 738 Ok(()) 739 } 740 } 741 742 struct VaultRemoveError; 743 744 impl RadrootsSecretVault for VaultRemoveError { 745 fn store_secret( 746 &self, 747 _slot: &str, 748 _secret: &str, 749 ) -> Result<(), radroots_secret_vault::RadrootsSecretVaultAccessError> { 750 Ok(()) 751 } 752 753 fn load_secret( 754 &self, 755 _slot: &str, 756 ) -> Result<Option<String>, radroots_secret_vault::RadrootsSecretVaultAccessError> { 757 Ok(None) 758 } 759 760 fn remove_secret( 761 &self, 762 _slot: &str, 763 ) -> Result<(), radroots_secret_vault::RadrootsSecretVaultAccessError> { 764 Err( 765 radroots_secret_vault::RadrootsSecretVaultAccessError::Backend( 766 "vault remove failed".into(), 767 ), 768 ) 769 } 770 } 771 772 fn poison_manager_state(manager: &RadrootsNostrAccountsManager) { 773 let state = manager.state.clone(); 774 let _ = thread::spawn(move || { 775 let _guard = state.write().expect("write"); 776 panic!("poison manager state"); 777 }) 778 .join(); 779 } 780 781 fn status_kind(status: &RadrootsNostrAccountStatus) -> &'static str { 782 match status { 783 RadrootsNostrAccountStatus::NotConfigured => "not-configured", 784 RadrootsNostrAccountStatus::PublicOnly { .. } => "public-only", 785 RadrootsNostrAccountStatus::Ready { .. } => "ready", 786 } 787 } 788 789 fn status_account(status: &RadrootsNostrAccountStatus) -> Option<&RadrootsNostrAccountRecord> { 790 match status { 791 RadrootsNostrAccountStatus::NotConfigured => None, 792 RadrootsNostrAccountStatus::PublicOnly { account } 793 | RadrootsNostrAccountStatus::Ready { account } => Some(account), 794 } 795 } 796 797 #[test] 798 fn manager_persists_default_account_and_restores_signing_identity() { 799 let temp = tempfile::tempdir().expect("tempdir"); 800 let store = Arc::new(RadrootsNostrFileAccountStore::new( 801 temp.path().join("accounts.json"), 802 )); 803 let vault = Arc::new(RadrootsNostrSecretVaultMemory::new()); 804 let manager = 805 RadrootsNostrAccountsManager::new(store.clone(), vault.clone()).expect("manager"); 806 let created_id = manager 807 .generate_identity(Some("primary".into()), true) 808 .expect("create identity"); 809 810 let default_account_id = manager 811 .default_account_id() 812 .expect("default") 813 .expect("default id"); 814 assert_eq!(default_account_id, created_id); 815 816 let manager2 = RadrootsNostrAccountsManager::new(store, vault).expect("manager2"); 817 let default_account_id_2 = manager2 818 .default_account_id() 819 .expect("default2") 820 .expect("default2 id"); 821 assert_eq!(default_account_id_2, created_id); 822 assert!( 823 manager2 824 .default_signing_identity() 825 .expect("signing") 826 .is_some() 827 ); 828 } 829 830 #[test] 831 fn new_file_backed_with_vault_persists_default_account() { 832 let temp = tempfile::tempdir().expect("tempdir"); 833 let path = temp.path().join("accounts.json"); 834 let manager = RadrootsNostrAccountsManager::new_file_backed_with_vault( 835 &path, 836 RadrootsNostrSecretVaultMemory::new(), 837 ) 838 .expect("manager"); 839 let identity = RadrootsIdentity::generate(); 840 let account_id = manager 841 .upsert_identity(&identity, Some("primary".into()), true) 842 .expect("upsert"); 843 844 let reloaded = RadrootsNostrAccountsManager::new_file_backed_with_vault( 845 &path, 846 RadrootsNostrSecretVaultMemory::new(), 847 ) 848 .expect("reloaded"); 849 850 assert_eq!( 851 reloaded.default_account_id().expect("default"), 852 Some(account_id) 853 ); 854 assert_eq!(reloaded.list_accounts().expect("accounts").len(), 1); 855 } 856 857 #[test] 858 fn new_migrates_legacy_store_file_to_default_account_semantics() { 859 let temp = tempfile::tempdir().expect("tempdir"); 860 let path = temp.path().join("accounts.json"); 861 let identity = RadrootsIdentity::generate(); 862 let public_identity = identity.to_public(); 863 let account_id = public_identity.id.clone(); 864 let legacy_record = 865 RadrootsNostrAccountRecord::new(public_identity, Some("legacy".into()), 1); 866 fs::write( 867 &path, 868 serde_json::to_vec_pretty(&json!({ 869 "version": 1, 870 "selected_account_id": account_id, 871 "accounts": [legacy_record], 872 })) 873 .expect("serialize legacy store"), 874 ) 875 .expect("write legacy store"); 876 877 let vault = Arc::new(RadrootsNostrSecretVaultMemory::new()); 878 vault 879 .store_secret( 880 account_secret_slot(&account_id).as_str(), 881 identity.secret_key_hex().as_str(), 882 ) 883 .expect("store secret"); 884 885 let manager = RadrootsNostrAccountsManager::new( 886 Arc::new(RadrootsNostrFileAccountStore::new(&path)), 887 vault, 888 ) 889 .expect("manager"); 890 891 assert_eq!( 892 manager.default_account_id().expect("default"), 893 Some(account_id.clone()) 894 ); 895 896 let migrated_store: serde_json::Value = 897 serde_json::from_slice(&fs::read(&path).expect("read migrated store")) 898 .expect("parse migrated store"); 899 assert_eq!( 900 migrated_store["version"], 901 serde_json::Value::from(crate::model::RADROOTS_NOSTR_ACCOUNTS_STORE_VERSION), 902 ); 903 assert_eq!( 904 migrated_store["default_account_id"], 905 serde_json::Value::from(account_id.to_string()), 906 ); 907 assert!(migrated_store.get("selected_account_id").is_none()); 908 } 909 910 #[test] 911 fn new_reports_save_error_when_dirty_state_requires_rewrite() { 912 let mut state = RadrootsNostrAccountStoreState::default(); 913 state.version = 1; 914 let store = Arc::new(SaveErrorStore::new(state)); 915 let vault = Arc::new(RadrootsNostrSecretVaultMemory::new()); 916 917 let err = RadrootsNostrAccountsManager::new(store, vault) 918 .err() 919 .expect("dirty state save error"); 920 921 assert_eq!(err.to_string(), "store error: store save failed"); 922 } 923 924 #[test] 925 fn resolve_local_backend_applies_shared_fallback_policy() { 926 let resolved = RadrootsNostrAccountsManager::resolve_local_backend( 927 RadrootsSecretBackendSelection { 928 primary: RadrootsSecretBackend::HostVault( 929 radroots_secret_vault::RadrootsHostVaultPolicy::desktop(), 930 ), 931 fallback: Some(RadrootsSecretBackend::EncryptedFile), 932 }, 933 RadrootsSecretBackendAvailability { 934 host_vault: RadrootsHostVaultCapabilities::unavailable(), 935 encrypted_file: true, 936 external_command: false, 937 memory: false, 938 }, 939 ) 940 .expect("fallback resolves"); 941 942 assert_eq!(resolved.backend, RadrootsSecretBackend::EncryptedFile); 943 assert!(resolved.used_fallback); 944 } 945 946 #[test] 947 fn new_local_file_backed_rejects_external_command_backend() { 948 let temp = tempfile::tempdir().expect("tempdir"); 949 let err = RadrootsNostrAccountsManager::new_local_file_backed( 950 temp.path().join("accounts.json"), 951 temp.path().join("secrets"), 952 RadrootsSecretBackendSelection { 953 primary: RadrootsSecretBackend::ExternalCommand, 954 fallback: None, 955 }, 956 RadrootsSecretBackendAvailability { 957 host_vault: RadrootsHostVaultCapabilities::unavailable(), 958 encrypted_file: true, 959 external_command: true, 960 memory: false, 961 }, 962 "org.radroots.test.local-account", 963 ) 964 .err() 965 .expect("external command must be rejected"); 966 967 assert_eq!( 968 err.to_string(), 969 "vault error: external_command secret backend is not supported for local accounts" 970 ); 971 } 972 973 #[test] 974 fn new_local_file_backed_reports_backend_resolution_error() { 975 let temp = tempfile::tempdir().expect("tempdir"); 976 let err = RadrootsNostrAccountsManager::new_local_file_backed( 977 temp.path().join("accounts.json"), 978 temp.path().join("secrets"), 979 RadrootsSecretBackendSelection { 980 primary: RadrootsSecretBackend::HostVault( 981 radroots_secret_vault::RadrootsHostVaultPolicy::desktop(), 982 ), 983 fallback: None, 984 }, 985 RadrootsSecretBackendAvailability { 986 host_vault: RadrootsHostVaultCapabilities::unavailable(), 987 encrypted_file: false, 988 external_command: false, 989 memory: false, 990 }, 991 "org.radroots.test.local-account", 992 ) 993 .err() 994 .expect("backend resolution error"); 995 996 assert_eq!( 997 err.to_string(), 998 "vault error: secret backend host_vault is unavailable" 999 ); 1000 } 1001 1002 #[test] 1003 fn new_local_file_backed_reports_store_load_error() { 1004 let temp = tempfile::tempdir().expect("tempdir"); 1005 let err = RadrootsNostrAccountsManager::new_local_file_backed( 1006 temp.path(), 1007 temp.path().join("secrets"), 1008 RadrootsSecretBackendSelection { 1009 primary: RadrootsSecretBackend::EncryptedFile, 1010 fallback: None, 1011 }, 1012 RadrootsSecretBackendAvailability { 1013 host_vault: RadrootsHostVaultCapabilities::unavailable(), 1014 encrypted_file: true, 1015 external_command: false, 1016 memory: false, 1017 }, 1018 "org.radroots.test.local-account", 1019 ) 1020 .err() 1021 .expect("store load error"); 1022 1023 assert!(err.to_string().starts_with("store error:")); 1024 } 1025 1026 #[test] 1027 fn new_local_file_backed_resolves_encrypted_file_backend() { 1028 let temp = tempfile::tempdir().expect("tempdir"); 1029 let (manager, resolved) = RadrootsNostrAccountsManager::new_local_file_backed( 1030 temp.path().join("accounts.json"), 1031 temp.path().join("secrets"), 1032 RadrootsSecretBackendSelection { 1033 primary: RadrootsSecretBackend::EncryptedFile, 1034 fallback: None, 1035 }, 1036 RadrootsSecretBackendAvailability { 1037 host_vault: RadrootsHostVaultCapabilities::unavailable(), 1038 encrypted_file: true, 1039 external_command: false, 1040 memory: false, 1041 }, 1042 "org.radroots.test.local-account", 1043 ) 1044 .expect("encrypted file manager"); 1045 1046 assert_eq!(resolved.backend, RadrootsSecretBackend::EncryptedFile); 1047 assert!(!resolved.used_fallback); 1048 assert!(manager.list_accounts().expect("accounts").is_empty()); 1049 } 1050 1051 #[test] 1052 #[cfg(not(feature = "os-keyring"))] 1053 fn local_file_backed_secret_vault_rejects_host_vault_without_feature() { 1054 let temp = tempfile::tempdir().expect("tempdir"); 1055 let err = local_file_backed_secret_vault( 1056 RadrootsSecretBackend::HostVault( 1057 radroots_secret_vault::RadrootsHostVaultPolicy::desktop(), 1058 ), 1059 temp.path(), 1060 "org.radroots.test.local-account".into(), 1061 ) 1062 .err() 1063 .expect("host vault requires feature"); 1064 1065 assert_eq!( 1066 err.to_string(), 1067 "vault error: host_vault backend requires radroots_nostr_accounts os-keyring support" 1068 ); 1069 } 1070 1071 #[test] 1072 #[cfg(feature = "memory-vault")] 1073 fn local_file_backed_secret_vault_resolves_memory_backend() { 1074 let temp = tempfile::tempdir().expect("tempdir"); 1075 let vault = local_file_backed_secret_vault( 1076 RadrootsSecretBackend::Memory, 1077 temp.path(), 1078 "org.radroots.test.local-account".into(), 1079 ) 1080 .expect("memory vault"); 1081 1082 vault.store_secret("slot", "secret").expect("store"); 1083 assert_eq!( 1084 vault.load_secret("slot").expect("load").as_deref(), 1085 Some("secret") 1086 ); 1087 } 1088 1089 #[test] 1090 fn watch_only_account_has_no_signing_identity() { 1091 let temp = tempfile::tempdir().expect("tempdir"); 1092 let store = Arc::new(RadrootsNostrFileAccountStore::new( 1093 temp.path().join("accounts.json"), 1094 )); 1095 let vault = Arc::new(RadrootsNostrSecretVaultMemory::new()); 1096 let manager = RadrootsNostrAccountsManager::new(store, vault).expect("manager"); 1097 1098 let identity = RadrootsIdentity::generate(); 1099 let public = identity.to_public(); 1100 manager 1101 .upsert_public_identity(public, Some("watch".into()), true) 1102 .expect("watch"); 1103 1104 assert!( 1105 manager 1106 .default_signing_identity() 1107 .expect("signing") 1108 .is_none() 1109 ); 1110 let status = manager 1111 .default_account_status() 1112 .expect("default account status"); 1113 assert_eq!(status_kind(&status), "public-only"); 1114 let account = status_account(&status).expect("account"); 1115 assert_eq!(account.label.as_deref(), Some("watch")); 1116 } 1117 1118 #[test] 1119 fn attach_identity_secret_upgrades_existing_watch_only_account() { 1120 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1121 let identity = RadrootsIdentity::generate(); 1122 let account_id = manager 1123 .upsert_public_identity(identity.to_public(), Some("watch".into()), false) 1124 .expect("watch"); 1125 manager.clear_default_account().expect("clear default"); 1126 1127 let attached = manager 1128 .attach_identity_secret(&account_id, &identity, false) 1129 .expect("attach secret"); 1130 1131 assert_eq!(attached.account_id, account_id); 1132 assert_eq!(attached.label.as_deref(), Some("watch")); 1133 assert_eq!( 1134 attached.public_identity.public_key_hex, 1135 identity.public_key_hex() 1136 ); 1137 assert_eq!(manager.list_accounts().expect("list").len(), 1); 1138 let signing_identity = manager 1139 .get_signing_identity(&account_id) 1140 .expect("signing") 1141 .expect("secret backed"); 1142 assert_eq!(signing_identity.public_key_hex(), identity.public_key_hex()); 1143 assert_eq!(manager.default_account_id().expect("default"), None); 1144 } 1145 1146 #[test] 1147 fn attach_identity_secret_preserves_existing_default_when_not_requested() { 1148 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1149 let default_account_id = manager 1150 .generate_identity(Some("primary".into()), true) 1151 .expect("primary"); 1152 let identity = RadrootsIdentity::generate(); 1153 let account_id = manager 1154 .upsert_public_identity(identity.to_public(), Some("watch".into()), false) 1155 .expect("watch"); 1156 1157 manager 1158 .attach_identity_secret(&account_id, &identity, false) 1159 .expect("attach secret"); 1160 1161 assert_eq!( 1162 manager.default_account_id().expect("default"), 1163 Some(default_account_id) 1164 ); 1165 } 1166 1167 #[test] 1168 fn attach_identity_secret_can_explicitly_make_default() { 1169 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1170 manager 1171 .generate_identity(Some("primary".into()), true) 1172 .expect("primary"); 1173 let identity = RadrootsIdentity::generate(); 1174 let account_id = manager 1175 .upsert_public_identity(identity.to_public(), Some("watch".into()), false) 1176 .expect("watch"); 1177 1178 manager 1179 .attach_identity_secret(&account_id, &identity, true) 1180 .expect("attach secret"); 1181 1182 assert_eq!( 1183 manager.default_account_id().expect("default"), 1184 Some(account_id) 1185 ); 1186 } 1187 1188 #[test] 1189 fn attach_identity_secret_rejects_missing_account_without_storing_secret() { 1190 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1191 let identity = RadrootsIdentity::generate(); 1192 let missing_id = identity.id(); 1193 1194 let err = manager 1195 .attach_identity_secret(&missing_id, &identity, false) 1196 .expect_err("missing account"); 1197 1198 assert_eq!(err.to_string(), format!("account not found: {missing_id}")); 1199 assert!( 1200 manager 1201 .export_secret_hex(&missing_id) 1202 .expect("export") 1203 .is_none() 1204 ); 1205 assert!(manager.list_accounts().expect("list").is_empty()); 1206 } 1207 1208 #[test] 1209 fn attach_identity_secret_rejects_public_key_mismatch_without_storing_secret() { 1210 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1211 let public_identity = RadrootsIdentity::generate(); 1212 let account_id = manager 1213 .upsert_public_identity(public_identity.to_public(), Some("watch".into()), false) 1214 .expect("watch"); 1215 manager.clear_default_account().expect("clear default"); 1216 let mismatched_identity = RadrootsIdentity::generate(); 1217 1218 let err = manager 1219 .attach_identity_secret(&account_id, &mismatched_identity, false) 1220 .expect_err("public key mismatch"); 1221 1222 assert_eq!(err.to_string(), "public key does not match secret key"); 1223 assert!( 1224 manager 1225 .export_secret_hex(&account_id) 1226 .expect("export") 1227 .is_none() 1228 ); 1229 assert!( 1230 manager 1231 .get_signing_identity(&account_id) 1232 .expect("signing") 1233 .is_none() 1234 ); 1235 assert_eq!(manager.default_account_id().expect("default"), None); 1236 } 1237 1238 #[test] 1239 fn attach_identity_secret_reports_vault_store_error() { 1240 let manager = RadrootsNostrAccountsManager::new( 1241 Arc::new(RadrootsNostrMemoryAccountStore::new()), 1242 Arc::new(VaultStoreError), 1243 ) 1244 .expect("manager"); 1245 let identity = RadrootsIdentity::generate(); 1246 let account_id = manager 1247 .upsert_public_identity(identity.to_public(), Some("watch".into()), false) 1248 .expect("watch"); 1249 1250 let err = manager 1251 .attach_identity_secret(&account_id, &identity, false) 1252 .expect_err("vault store error"); 1253 1254 assert!(err.to_string().starts_with("vault error:")); 1255 } 1256 1257 #[test] 1258 fn attach_identity_secret_reports_store_save_error_after_secret_store() { 1259 let identity = RadrootsIdentity::generate(); 1260 let public_identity = identity.to_public(); 1261 let account_id = public_identity.id.clone(); 1262 let mut state = RadrootsNostrAccountStoreState::default(); 1263 state.accounts.push(RadrootsNostrAccountRecord::new( 1264 public_identity, 1265 Some("watch".into()), 1266 1, 1267 )); 1268 let manager = RadrootsNostrAccountsManager::new( 1269 Arc::new(SaveErrorStore::new(state)), 1270 Arc::new(RadrootsNostrSecretVaultMemory::new()), 1271 ) 1272 .expect("manager"); 1273 1274 let err = manager 1275 .attach_identity_secret(&account_id, &identity, false) 1276 .expect_err("store save error"); 1277 1278 assert_eq!(err.to_string(), "store error: store save failed"); 1279 } 1280 1281 #[test] 1282 fn default_account_status_reports_ready_for_signing_identity() { 1283 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1284 let default_account_id = manager 1285 .generate_identity(Some("primary".into()), true) 1286 .expect("generate"); 1287 1288 let status = manager 1289 .default_account_status() 1290 .expect("default account status"); 1291 assert_eq!(status_kind(&status), "ready"); 1292 let account = status_account(&status).expect("account"); 1293 assert_eq!(account.account_id, default_account_id); 1294 assert_eq!(account.label.as_deref(), Some("primary")); 1295 1296 let signer = manager 1297 .default_signer_capability() 1298 .expect("default signer capability") 1299 .expect("signer capability"); 1300 let local = signer.local_account().expect("local signer"); 1301 assert_eq!(local.account_id, default_account_id); 1302 assert!(local.is_secret_backed()); 1303 } 1304 1305 #[test] 1306 fn migrate_legacy_identity_file_imports_identity() { 1307 let temp = tempfile::tempdir().expect("tempdir"); 1308 let legacy_path = temp.path().join("legacy_identity.json"); 1309 let legacy_identity = RadrootsIdentity::generate(); 1310 legacy_identity 1311 .save_json(&legacy_path) 1312 .expect("legacy save"); 1313 1314 let store = Arc::new(RadrootsNostrFileAccountStore::new( 1315 temp.path().join("accounts.json"), 1316 )); 1317 let vault = Arc::new(RadrootsNostrSecretVaultMemory::new()); 1318 let manager = RadrootsNostrAccountsManager::new(store, vault).expect("manager"); 1319 let id = manager 1320 .migrate_legacy_identity_file(&legacy_path, Some("legacy".into()), true) 1321 .expect("migrate"); 1322 assert_eq!( 1323 manager 1324 .default_account_id() 1325 .expect("default") 1326 .expect("default id"), 1327 id 1328 ); 1329 } 1330 1331 #[test] 1332 fn upsert_public_identity_without_label_preserves_existing_label() { 1333 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1334 let account_id = manager 1335 .generate_identity(Some("primary".into()), true) 1336 .expect("generate"); 1337 1338 let existing = manager 1339 .default_public_identity() 1340 .expect("default public") 1341 .expect("public identity"); 1342 manager 1343 .upsert_public_identity(existing, None, false) 1344 .expect("upsert"); 1345 1346 let records = manager.list_accounts().expect("list"); 1347 let record = records 1348 .into_iter() 1349 .find(|record| record.account_id == account_id) 1350 .expect("account"); 1351 assert_eq!(record.label.as_deref(), Some("primary")); 1352 } 1353 1354 #[test] 1355 fn new_rejects_unsupported_schema_version() { 1356 let store = Arc::new(RadrootsNostrMemoryAccountStore::new()); 1357 let vault = Arc::new(RadrootsNostrSecretVaultMemory::new()); 1358 let mut state = RadrootsNostrAccountStoreState::default(); 1359 state.version = crate::model::RADROOTS_NOSTR_ACCOUNTS_STORE_VERSION + 1; 1360 store.save(&state).expect("save"); 1361 1362 let err = RadrootsNostrAccountsManager::new(store, vault) 1363 .err() 1364 .expect("unsupported schema version"); 1365 assert!(err.to_string().contains("invalid account state")); 1366 } 1367 1368 #[test] 1369 fn new_clears_orphaned_default_account() { 1370 let store = Arc::new(RadrootsNostrMemoryAccountStore::new()); 1371 let vault = Arc::new(RadrootsNostrSecretVaultMemory::new()); 1372 let mut state = RadrootsNostrAccountStoreState::default(); 1373 state.default_account_id = Some(RadrootsIdentity::generate().id()); 1374 store.save(&state).expect("save"); 1375 1376 let manager = RadrootsNostrAccountsManager::new(store, vault).expect("manager"); 1377 assert!(manager.default_account_id().expect("default").is_none()); 1378 } 1379 1380 #[test] 1381 fn default_methods_return_none_when_state_is_empty() { 1382 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1383 assert!( 1384 manager 1385 .default_account() 1386 .expect("default account") 1387 .is_none() 1388 ); 1389 assert!( 1390 manager 1391 .default_public_identity() 1392 .expect("default public") 1393 .is_none() 1394 ); 1395 assert!( 1396 manager 1397 .default_signing_identity() 1398 .expect("default signing") 1399 .is_none() 1400 ); 1401 assert!( 1402 manager 1403 .default_signer_capability() 1404 .expect("default signer capability") 1405 .is_none() 1406 ); 1407 let status = manager 1408 .default_account_status() 1409 .expect("default account status"); 1410 assert_eq!(status_kind(&status), "not-configured"); 1411 assert!(status_account(&status).is_none()); 1412 1413 let missing_id = RadrootsIdentity::generate().id(); 1414 assert!( 1415 manager 1416 .get_signing_identity(&missing_id) 1417 .expect("signing") 1418 .is_none() 1419 ); 1420 assert!( 1421 manager 1422 .get_signer_capability(&missing_id) 1423 .expect("signer capability") 1424 .is_none() 1425 ); 1426 } 1427 1428 #[test] 1429 fn default_account_status_propagates_secret_integrity_errors() { 1430 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1431 let account_id = manager 1432 .generate_identity(Some("primary".into()), true) 1433 .expect("generate"); 1434 manager 1435 .vault 1436 .remove_secret(account_secret_slot(&account_id).as_str()) 1437 .expect("remove secret"); 1438 1439 let status = manager 1440 .default_account_status() 1441 .expect("default account status"); 1442 assert_eq!(status_kind(&status), "public-only"); 1443 let account = status_account(&status).expect("account"); 1444 assert_eq!(account.account_id, account_id); 1445 1446 let wrong_identity = RadrootsIdentity::generate(); 1447 manager 1448 .vault 1449 .store_secret( 1450 account_secret_slot(&account_id).as_str(), 1451 wrong_identity.secret_key_hex().as_str(), 1452 ) 1453 .expect("store wrong secret"); 1454 1455 let err = manager 1456 .default_account_status() 1457 .expect_err("public key mismatch"); 1458 assert_eq!(err.to_string(), "public key does not match secret key"); 1459 } 1460 1461 #[test] 1462 fn default_account_status_propagates_store_vault_and_secret_parse_errors() { 1463 let poisoned_manager = RadrootsNostrAccountsManager::new_in_memory(); 1464 poison_manager_state(&poisoned_manager); 1465 let default_err = poisoned_manager 1466 .default_account_status() 1467 .expect_err("default status poisoned"); 1468 assert!(default_err.to_string().starts_with("store error:")); 1469 1470 let mut load_error_state = RadrootsNostrAccountStoreState::default(); 1471 let load_error_public = RadrootsIdentity::generate().to_public(); 1472 load_error_state 1473 .accounts 1474 .push(RadrootsNostrAccountRecord::new( 1475 load_error_public.clone(), 1476 Some("watch".into()), 1477 1, 1478 )); 1479 load_error_state.default_account_id = Some(load_error_public.id.clone()); 1480 let load_error_store = Arc::new(RadrootsNostrMemoryAccountStore::new()); 1481 load_error_store 1482 .save(&load_error_state) 1483 .expect("save state"); 1484 let vault_load_error_manager = 1485 RadrootsNostrAccountsManager::new(load_error_store, Arc::new(VaultLoadError)) 1486 .expect("manager"); 1487 let vault_load_error = vault_load_error_manager 1488 .default_account_status() 1489 .expect_err("vault load error"); 1490 assert!(vault_load_error.to_string().starts_with("vault error:")); 1491 1492 let mut invalid_secret_state = RadrootsNostrAccountStoreState::default(); 1493 let invalid_secret_public = RadrootsIdentity::generate().to_public(); 1494 invalid_secret_state 1495 .accounts 1496 .push(RadrootsNostrAccountRecord::new( 1497 invalid_secret_public.clone(), 1498 Some("invalid".into()), 1499 1, 1500 )); 1501 invalid_secret_state.default_account_id = Some(invalid_secret_public.id.clone()); 1502 let invalid_secret_store = Arc::new(RadrootsNostrMemoryAccountStore::new()); 1503 invalid_secret_store 1504 .save(&invalid_secret_state) 1505 .expect("save state"); 1506 let invalid_secret_manager = 1507 RadrootsNostrAccountsManager::new(invalid_secret_store, Arc::new(VaultInvalidSecret)) 1508 .expect("manager"); 1509 let invalid_secret = invalid_secret_manager 1510 .default_account_status() 1511 .expect_err("invalid secret"); 1512 assert!(invalid_secret.to_string().starts_with("identity error:")); 1513 } 1514 1515 #[test] 1516 fn signer_capability_paths_propagate_secret_parse_errors() { 1517 let mut invalid_secret_state = RadrootsNostrAccountStoreState::default(); 1518 let invalid_secret_public = RadrootsIdentity::generate().to_public(); 1519 invalid_secret_state 1520 .accounts 1521 .push(RadrootsNostrAccountRecord::new( 1522 invalid_secret_public.clone(), 1523 Some("invalid".into()), 1524 1, 1525 )); 1526 invalid_secret_state.default_account_id = Some(invalid_secret_public.id.clone()); 1527 let invalid_secret_store = Arc::new(RadrootsNostrMemoryAccountStore::new()); 1528 invalid_secret_store 1529 .save(&invalid_secret_state) 1530 .expect("save state"); 1531 let invalid_secret_manager = 1532 RadrootsNostrAccountsManager::new(invalid_secret_store, Arc::new(VaultInvalidSecret)) 1533 .expect("manager"); 1534 1535 let default_signer_error = invalid_secret_manager 1536 .default_signer_capability() 1537 .expect_err("default signer invalid secret"); 1538 assert!( 1539 default_signer_error 1540 .to_string() 1541 .starts_with("identity error:") 1542 ); 1543 1544 let signer_error = invalid_secret_manager 1545 .get_signer_capability(&invalid_secret_public.id) 1546 .expect_err("signer invalid secret"); 1547 assert!(signer_error.to_string().starts_with("identity error:")); 1548 } 1549 1550 #[test] 1551 fn select_remove_export_and_lookup_paths() { 1552 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1553 let first_id = manager 1554 .generate_identity(Some("first".into()), true) 1555 .expect("first"); 1556 let second_id = manager 1557 .generate_identity(Some("second".into()), false) 1558 .expect("second"); 1559 1560 manager 1561 .set_default_account(&second_id) 1562 .expect("set default second"); 1563 assert_eq!( 1564 manager.default_account_id().expect("default"), 1565 Some(second_id.clone()) 1566 ); 1567 assert!( 1568 manager 1569 .export_secret_hex(&second_id) 1570 .expect("export") 1571 .is_some() 1572 ); 1573 assert!( 1574 manager 1575 .get_signing_identity(&second_id) 1576 .expect("signing") 1577 .is_some() 1578 ); 1579 1580 manager.remove_account(&second_id).expect("remove second"); 1581 assert_eq!(manager.default_account_id().expect("default"), None); 1582 assert!( 1583 manager 1584 .export_secret_hex(&second_id) 1585 .expect("export after remove") 1586 .is_none() 1587 ); 1588 assert!( 1589 manager 1590 .get_signing_identity(&first_id) 1591 .expect("first signing") 1592 .is_some() 1593 ); 1594 1595 let set_default_missing = manager 1596 .set_default_account(&second_id) 1597 .expect_err("missing default"); 1598 assert!( 1599 set_default_missing 1600 .to_string() 1601 .contains("account not found") 1602 ); 1603 let remove_missing = manager 1604 .remove_account(&second_id) 1605 .expect_err("missing remove"); 1606 assert!(remove_missing.to_string().contains("account not found")); 1607 } 1608 1609 #[test] 1610 fn upsert_public_identity_updates_label_and_respects_default_flag() { 1611 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1612 let original_default = manager 1613 .generate_identity(Some("primary".into()), true) 1614 .expect("generate"); 1615 1616 let existing = manager 1617 .default_public_identity() 1618 .expect("default public") 1619 .expect("public"); 1620 manager 1621 .upsert_public_identity(existing.clone(), Some("renamed".into()), false) 1622 .expect("upsert existing"); 1623 1624 let renamed = manager 1625 .list_accounts() 1626 .expect("list") 1627 .into_iter() 1628 .find(|record| record.account_id == existing.id) 1629 .expect("record"); 1630 assert_eq!(renamed.label.as_deref(), Some("renamed")); 1631 1632 let watch_only = RadrootsIdentity::generate().to_public(); 1633 let watch_id = watch_only.id.clone(); 1634 manager 1635 .upsert_public_identity(watch_only.clone(), Some("watch".into()), false) 1636 .expect("upsert watch"); 1637 assert_eq!( 1638 manager.default_account_id().expect("default"), 1639 Some(original_default.clone()) 1640 ); 1641 1642 manager 1643 .upsert_public_identity(watch_only, Some("watch".into()), true) 1644 .expect("replace default"); 1645 assert_eq!( 1646 manager.default_account_id().expect("default"), 1647 Some(watch_id) 1648 ); 1649 } 1650 1651 #[test] 1652 fn upsert_public_identity_rejects_mismatched_id() { 1653 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1654 let mut public_identity = RadrootsIdentity::generate().to_public(); 1655 let other = RadrootsIdentity::generate().to_public(); 1656 public_identity.id = other.id.clone(); 1657 1658 let err = manager 1659 .upsert_public_identity(public_identity, None, true) 1660 .expect_err("id mismatch"); 1661 assert!(err.to_string().starts_with("invalid account state:")); 1662 } 1663 1664 #[test] 1665 fn remove_non_default_account_keeps_current_default() { 1666 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1667 let default_account_id = manager 1668 .generate_identity(Some("selected".into()), true) 1669 .expect("default"); 1670 let removable_id = manager 1671 .generate_identity(Some("removable".into()), false) 1672 .expect("removable"); 1673 1674 manager.remove_account(&removable_id).expect("remove"); 1675 assert_eq!( 1676 manager.default_account_id().expect("default"), 1677 Some(default_account_id) 1678 ); 1679 } 1680 1681 #[test] 1682 fn clear_default_account_clears_default_without_removing_accounts() { 1683 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1684 manager 1685 .generate_identity(Some("primary".into()), true) 1686 .expect("primary"); 1687 manager 1688 .generate_identity(Some("secondary".into()), false) 1689 .expect("secondary"); 1690 1691 manager.clear_default_account().expect("clear default"); 1692 1693 assert!(manager.default_account_id().expect("default").is_none()); 1694 assert_eq!(manager.list_accounts().expect("accounts").len(), 2); 1695 } 1696 1697 #[test] 1698 fn resolve_account_selector_matches_exact_id_npub_and_unique_label() { 1699 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1700 let account_id = manager 1701 .generate_identity(Some("primary".into()), true) 1702 .expect("primary"); 1703 let default_account = manager 1704 .default_account() 1705 .expect("default account") 1706 .expect("default record"); 1707 let npub = default_account.public_identity.public_key_npub.clone(); 1708 1709 let resolved_by_id = manager 1710 .resolve_account_selector(account_id.as_str()) 1711 .expect("resolve by id"); 1712 assert_eq!(resolved_by_id.account_id, account_id); 1713 1714 let resolved_by_npub = manager 1715 .resolve_account_selector(&npub) 1716 .expect("resolve by npub"); 1717 assert_eq!(resolved_by_npub.account_id, account_id); 1718 1719 let resolved_by_label = manager 1720 .resolve_account_selector("primary") 1721 .expect("resolve by label"); 1722 assert_eq!(resolved_by_label.account_id, account_id); 1723 } 1724 1725 #[test] 1726 fn resolve_account_selector_rejects_empty_and_ambiguous_labels() { 1727 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1728 manager 1729 .generate_identity(Some("shared".into()), true) 1730 .expect("first"); 1731 manager 1732 .generate_identity(Some("shared".into()), false) 1733 .expect("second"); 1734 1735 let empty = manager 1736 .resolve_account_selector(" ") 1737 .expect_err("empty selector"); 1738 assert!(empty.to_string().starts_with("invalid account selector:")); 1739 1740 let ambiguous = manager 1741 .resolve_account_selector("shared") 1742 .expect_err("ambiguous selector"); 1743 assert!( 1744 ambiguous 1745 .to_string() 1746 .starts_with("account selector is ambiguous:") 1747 ); 1748 1749 let missing = manager 1750 .resolve_account_selector("missing") 1751 .expect_err("missing selector"); 1752 assert_eq!(missing.to_string(), "account not found: missing"); 1753 } 1754 1755 #[test] 1756 fn remove_account_propagates_vault_remove_error() { 1757 let store = Arc::new(RadrootsNostrMemoryAccountStore::new()); 1758 let vault = Arc::new(VaultRemoveError); 1759 let manager = RadrootsNostrAccountsManager::new(store, vault.clone()).expect("manager"); 1760 let public = RadrootsIdentity::generate().to_public(); 1761 let account_id = public.id.clone(); 1762 vault 1763 .store_secret(account_secret_slot(&account_id).as_str(), "secret") 1764 .expect("vault store"); 1765 assert!( 1766 vault 1767 .load_secret(account_secret_slot(&account_id).as_str()) 1768 .expect("vault load") 1769 .is_none() 1770 ); 1771 manager 1772 .upsert_public_identity(public, Some("remove".into()), true) 1773 .expect("upsert"); 1774 1775 let err = manager 1776 .remove_account(&account_id) 1777 .expect_err("remove error"); 1778 assert!(err.to_string().starts_with("vault error:")); 1779 } 1780 1781 #[test] 1782 fn resolve_signing_identity_mismatch_and_profile_paths() { 1783 let store = Arc::new(RadrootsNostrMemoryAccountStore::new()); 1784 let vault = Arc::new(RadrootsNostrSecretVaultMemory::new()); 1785 let manager = RadrootsNostrAccountsManager::new(store, vault.clone()).expect("manager"); 1786 1787 let mismatch_public = RadrootsIdentity::generate().to_public(); 1788 let mismatch_id = mismatch_public.id.clone(); 1789 manager 1790 .upsert_public_identity(mismatch_public, Some("mismatch".into()), true) 1791 .expect("upsert mismatch"); 1792 1793 let wrong_identity = RadrootsIdentity::generate(); 1794 vault 1795 .store_secret( 1796 account_secret_slot(&mismatch_id).as_str(), 1797 wrong_identity.secret_key_hex().as_str(), 1798 ) 1799 .expect("vault store"); 1800 1801 let mismatch = manager 1802 .default_signing_identity() 1803 .expect_err("public key mismatch"); 1804 assert!( 1805 mismatch 1806 .to_string() 1807 .contains("public key does not match secret key") 1808 ); 1809 1810 let mut with_profile = RadrootsIdentity::generate(); 1811 let profile = RadrootsIdentityProfile { 1812 identifier: Some("profile-id".to_string()), 1813 ..RadrootsIdentityProfile::default() 1814 }; 1815 with_profile.set_profile(profile); 1816 let profile_id = manager 1817 .upsert_identity(&with_profile, Some("profile".into()), true) 1818 .expect("upsert profile"); 1819 let resolved = manager 1820 .get_signing_identity(&profile_id) 1821 .expect("resolve") 1822 .expect("identity"); 1823 assert_eq!( 1824 resolved 1825 .profile() 1826 .and_then(|value| value.identifier.clone()) 1827 .as_deref(), 1828 Some("profile-id") 1829 ); 1830 1831 let local_signer = manager 1832 .get_signer_capability(&profile_id) 1833 .expect("local signer capability") 1834 .expect("local signer"); 1835 assert!( 1836 manager 1837 .resolve_signing_identity_for_signer(&local_signer) 1838 .expect("resolve local signer") 1839 .is_some() 1840 ); 1841 1842 let remote_signer = RadrootsNostrSignerCapability::RemoteSession(Box::new( 1843 radroots_nostr_signer::prelude::RadrootsNostrRemoteSessionSignerCapability::new( 1844 radroots_nostr_signer::prelude::RadrootsNostrSignerConnectionId::new_v7(), 1845 RadrootsIdentity::generate().to_public(), 1846 RadrootsIdentity::generate().to_public(), 1847 ), 1848 )); 1849 assert!( 1850 manager 1851 .resolve_signing_identity_for_signer(&remote_signer) 1852 .expect("resolve remote signer") 1853 .is_none() 1854 ); 1855 } 1856 1857 #[test] 1858 fn manager_propagates_store_and_vault_errors() { 1859 let load_error = RadrootsNostrAccountsManager::new( 1860 Arc::new(LoadErrorStore), 1861 Arc::new(RadrootsNostrSecretVaultMemory::new()), 1862 ) 1863 .err() 1864 .expect("load error manager"); 1865 assert!(load_error.to_string().starts_with("store error:")); 1866 1867 let save_error_store = Arc::new(SaveErrorStore::new( 1868 RadrootsNostrAccountStoreState::default(), 1869 )); 1870 let save_error_manager = RadrootsNostrAccountsManager::new( 1871 save_error_store, 1872 Arc::new(RadrootsNostrSecretVaultMemory::new()), 1873 ) 1874 .expect("manager"); 1875 let save_error = save_error_manager 1876 .upsert_public_identity(RadrootsIdentity::generate().to_public(), None, true) 1877 .expect_err("save error"); 1878 assert!(save_error.to_string().starts_with("store error:")); 1879 1880 let vault_store_error_manager = RadrootsNostrAccountsManager::new( 1881 Arc::new(RadrootsNostrMemoryAccountStore::new()), 1882 Arc::new(VaultStoreError), 1883 ) 1884 .expect("manager"); 1885 let identity = RadrootsIdentity::generate(); 1886 let vault_store_error = vault_store_error_manager 1887 .upsert_identity(&identity, None, true) 1888 .expect_err("vault store error"); 1889 assert!(vault_store_error.to_string().starts_with("vault error:")); 1890 1891 let mut load_error_state = RadrootsNostrAccountStoreState::default(); 1892 let load_error_public = RadrootsIdentity::generate().to_public(); 1893 load_error_state 1894 .accounts 1895 .push(RadrootsNostrAccountRecord::new( 1896 load_error_public.clone(), 1897 Some("watch".into()), 1898 1, 1899 )); 1900 load_error_state.default_account_id = Some(load_error_public.id.clone()); 1901 let load_error_store = Arc::new(RadrootsNostrMemoryAccountStore::new()); 1902 load_error_store 1903 .save(&load_error_state) 1904 .expect("save state"); 1905 let vault_load_error_manager = 1906 RadrootsNostrAccountsManager::new(load_error_store, Arc::new(VaultLoadError)) 1907 .expect("manager"); 1908 let vault_load_error = vault_load_error_manager 1909 .default_signing_identity() 1910 .expect_err("vault load error"); 1911 assert!(vault_load_error.to_string().starts_with("vault error:")); 1912 1913 let mut invalid_secret_state = RadrootsNostrAccountStoreState::default(); 1914 let invalid_secret_public = RadrootsIdentity::generate().to_public(); 1915 invalid_secret_state 1916 .accounts 1917 .push(RadrootsNostrAccountRecord::new( 1918 invalid_secret_public.clone(), 1919 Some("invalid".into()), 1920 1, 1921 )); 1922 invalid_secret_state.default_account_id = Some(invalid_secret_public.id.clone()); 1923 let invalid_secret_store = Arc::new(RadrootsNostrMemoryAccountStore::new()); 1924 invalid_secret_store 1925 .save(&invalid_secret_state) 1926 .expect("save state"); 1927 let invalid_secret_manager = 1928 RadrootsNostrAccountsManager::new(invalid_secret_store, Arc::new(VaultInvalidSecret)) 1929 .expect("manager"); 1930 let invalid_secret = invalid_secret_manager 1931 .default_signing_identity() 1932 .expect_err("invalid secret"); 1933 assert!(invalid_secret.to_string().starts_with("identity error:")); 1934 } 1935 1936 #[test] 1937 fn migrate_legacy_identity_file_returns_error_for_missing_path() { 1938 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1939 let temp = tempfile::tempdir().expect("tempdir"); 1940 let missing = temp.path().join("missing_legacy.json"); 1941 let migrated = manager 1942 .migrate_legacy_identity_file(&missing, None, false) 1943 .expect_err("missing legacy"); 1944 assert!(migrated.to_string().starts_with("identity error:")); 1945 } 1946 1947 #[test] 1948 fn manager_reports_poisoned_state_locks() { 1949 let manager = RadrootsNostrAccountsManager::new_in_memory(); 1950 poison_manager_state(&manager); 1951 1952 let list_err = manager.list_accounts().expect_err("list poisoned"); 1953 assert!(list_err.to_string().starts_with("store error:")); 1954 let default_id_err = manager 1955 .default_account_id() 1956 .expect_err("default id poisoned"); 1957 assert!(default_id_err.to_string().starts_with("store error:")); 1958 let default_err = manager.default_account().expect_err("default poisoned"); 1959 assert!(default_err.to_string().starts_with("store error:")); 1960 let default_public_err = manager 1961 .default_public_identity() 1962 .expect_err("default public poisoned"); 1963 assert!(default_public_err.to_string().starts_with("store error:")); 1964 let default_signing_err = manager 1965 .default_signing_identity() 1966 .expect_err("default signing poisoned"); 1967 assert!(default_signing_err.to_string().starts_with("store error:")); 1968 let default_signer_err = manager 1969 .default_signer_capability() 1970 .expect_err("default signer poisoned"); 1971 assert!(default_signer_err.to_string().starts_with("store error:")); 1972 1973 let account_id = RadrootsIdentity::generate().id(); 1974 let signing_err = manager 1975 .get_signing_identity(&account_id) 1976 .expect_err("signing poisoned"); 1977 assert!(signing_err.to_string().starts_with("store error:")); 1978 let attach_identity = RadrootsIdentity::generate(); 1979 let attach_err = manager 1980 .attach_identity_secret(&account_id, &attach_identity, false) 1981 .expect_err("attach poisoned"); 1982 assert!(attach_err.to_string().starts_with("store error:")); 1983 let signer_err = manager 1984 .get_signer_capability(&account_id) 1985 .expect_err("signer poisoned"); 1986 assert!(signer_err.to_string().starts_with("store error:")); 1987 let selector_err = manager 1988 .resolve_account_selector("missing") 1989 .expect_err("selector poisoned"); 1990 assert!(selector_err.to_string().starts_with("store error:")); 1991 let clear_default_err = manager 1992 .clear_default_account() 1993 .expect_err("clear default poisoned"); 1994 assert!(clear_default_err.to_string().starts_with("store error:")); 1995 let set_default_err = manager 1996 .set_default_account(&account_id) 1997 .expect_err("default poisoned"); 1998 assert!(set_default_err.to_string().starts_with("store error:")); 1999 let remove_err = manager 2000 .remove_account(&account_id) 2001 .expect_err("remove poisoned"); 2002 assert!(remove_err.to_string().starts_with("store error:")); 2003 let upsert_err = manager 2004 .upsert_public_identity(RadrootsIdentity::generate().to_public(), None, false) 2005 .expect_err("upsert poisoned"); 2006 assert!(upsert_err.to_string().starts_with("store error:")); 2007 } 2008 2009 #[test] 2010 fn stub_store_and_vault_methods_are_exercised() { 2011 let load_error_store = LoadErrorStore; 2012 let load_error_store_result = 2013 load_error_store.save(&RadrootsNostrAccountStoreState::default()); 2014 assert!(load_error_store_result.is_ok()); 2015 2016 let save_error_store = SaveErrorStore::new(RadrootsNostrAccountStoreState::default()); 2017 let loaded = save_error_store.load().expect("load"); 2018 assert_eq!( 2019 loaded.version, 2020 RadrootsNostrAccountStoreState::default().version 2021 ); 2022 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 2023 let _guard = save_error_store.state.write().expect("write"); 2024 panic!("poison save error store"); 2025 })); 2026 let poisoned_load = save_error_store.load().expect_err("poisoned load"); 2027 assert!(poisoned_load.to_string().starts_with("store error:")); 2028 2029 let account_id = RadrootsIdentity::generate().id(); 2030 let vault_store_error = VaultStoreError; 2031 assert!( 2032 vault_store_error 2033 .load_secret(account_secret_slot(&account_id).as_str()) 2034 .expect("load") 2035 .is_none() 2036 ); 2037 vault_store_error 2038 .remove_secret(account_secret_slot(&account_id).as_str()) 2039 .expect("remove"); 2040 2041 let vault_load_error = VaultLoadError; 2042 vault_load_error 2043 .store_secret(account_secret_slot(&account_id).as_str(), "secret") 2044 .expect("store"); 2045 vault_load_error 2046 .remove_secret(account_secret_slot(&account_id).as_str()) 2047 .expect("remove"); 2048 2049 let vault_invalid_secret = VaultInvalidSecret; 2050 vault_invalid_secret 2051 .store_secret(account_secret_slot(&account_id).as_str(), "secret") 2052 .expect("store"); 2053 vault_invalid_secret 2054 .remove_secret(account_secret_slot(&account_id).as_str()) 2055 .expect("remove"); 2056 } 2057 }