lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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 }