accounts.rs (28189B)
1 use std::{ 2 fs, 3 path::{Path, PathBuf}, 4 }; 5 6 use radroots_app_core::AppSharedAccountsPaths; 7 use radroots_app_sqlite::{AppSqliteError, AppSqliteStore}; 8 use radroots_app_view::{ 9 AccountSummary, AccountSurfaceActivationProjection, ActiveSurface, AppIdentityProjection, 10 FarmId, FarmerActivationProjection, SelectedAccountProjection, SelectedSurfaceProjection, 11 }; 12 use radroots_identity::{IdentityError, RadrootsIdentity, RadrootsIdentityId}; 13 use radroots_nostr_accounts::prelude::{ 14 RadrootsNostrAccountRecord, RadrootsNostrAccountStatus, RadrootsNostrAccountsError, 15 RadrootsNostrAccountsManager, 16 }; 17 use radroots_secret_vault::{ 18 RadrootsHostVaultCapabilities, RadrootsSecretBackend, RadrootsSecretBackendAvailability, 19 RadrootsSecretBackendSelection, 20 }; 21 use thiserror::Error; 22 23 pub struct DesktopAccountsBootstrap { 24 pub accounts_manager: Option<RadrootsNostrAccountsManager>, 25 } 26 27 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 28 pub enum DesktopLocalIdentityImportMode { 29 RawSecretKey, 30 EncryptedSecretKey, 31 } 32 33 #[derive(Clone, Debug, Eq, PartialEq)] 34 pub struct DesktopLocalIdentityImportRequest { 35 pub mode: DesktopLocalIdentityImportMode, 36 pub secret_text: String, 37 pub password: Option<String>, 38 } 39 40 impl DesktopLocalIdentityImportRequest { 41 pub fn new( 42 mode: DesktopLocalIdentityImportMode, 43 secret_text: impl Into<String>, 44 password: Option<String>, 45 ) -> Self { 46 Self { 47 mode, 48 secret_text: secret_text.into(), 49 password, 50 } 51 } 52 53 pub fn raw_secret_key(secret_text: impl Into<String>) -> Self { 54 Self::new( 55 DesktopLocalIdentityImportMode::RawSecretKey, 56 secret_text, 57 None, 58 ) 59 } 60 61 pub fn encrypted_secret_key( 62 secret_text: impl Into<String>, 63 password: impl Into<String>, 64 ) -> Self { 65 Self::new( 66 DesktopLocalIdentityImportMode::EncryptedSecretKey, 67 secret_text, 68 Some(password.into()), 69 ) 70 } 71 } 72 73 pub fn bootstrap_desktop_accounts( 74 paths: &AppSharedAccountsPaths, 75 _sqlite_store: &AppSqliteStore, 76 ) -> Result<DesktopAccountsBootstrap, DesktopAccountsBootstrapError> { 77 bootstrap_desktop_accounts_with_availability(paths, secret_backend_availability()?) 78 } 79 80 pub fn generate_local_account( 81 manager: &RadrootsNostrAccountsManager, 82 sqlite_store: &AppSqliteStore, 83 label: Option<String>, 84 ) -> Result<AppIdentityProjection, DesktopAccountsCommandError> { 85 manager.generate_identity(label, true)?; 86 Ok(identity_projection_from_manager(manager, sqlite_store)?) 87 } 88 89 pub fn import_local_account( 90 manager: &RadrootsNostrAccountsManager, 91 sqlite_store: &AppSqliteStore, 92 request: &DesktopLocalIdentityImportRequest, 93 ) -> Result<AppIdentityProjection, DesktopAccountsCommandError> { 94 let identity = import_identity(request)?; 95 manager.upsert_identity(&identity, None, true)?; 96 Ok(identity_projection_from_manager(manager, sqlite_store)?) 97 } 98 99 pub fn select_local_account( 100 manager: &RadrootsNostrAccountsManager, 101 sqlite_store: &AppSqliteStore, 102 account_id: &str, 103 ) -> Result<AppIdentityProjection, DesktopAccountsCommandError> { 104 let account_id = RadrootsIdentityId::parse(account_id.trim())?; 105 manager.set_default_account(&account_id)?; 106 Ok(identity_projection_from_manager(manager, sqlite_store)?) 107 } 108 109 pub fn select_active_surface( 110 manager: &RadrootsNostrAccountsManager, 111 sqlite_store: &AppSqliteStore, 112 active_surface: ActiveSurface, 113 ) -> Result<AppIdentityProjection, DesktopAccountsCommandError> { 114 let Some(selected_account) = selected_account_record(manager)? else { 115 return Ok(identity_projection_from_manager(manager, sqlite_store)?); 116 }; 117 let selected_projection = 118 selected_account_projection_from_record(&selected_account, sqlite_store)?; 119 let activation = AccountSurfaceActivationProjection::new( 120 selected_projection.account.account_id.clone(), 121 SelectedSurfaceProjection::new(active_surface), 122 selected_projection.farmer_activation.clone(), 123 ); 124 125 sqlite_store.save_surface_activation(&activation)?; 126 Ok(identity_projection_from_manager(manager, sqlite_store)?) 127 } 128 129 pub fn remove_selected_local_key( 130 manager: &RadrootsNostrAccountsManager, 131 sqlite_store: &AppSqliteStore, 132 ) -> Result<AppIdentityProjection, DesktopAccountsCommandError> { 133 let Some(selected_account) = selected_account_record(manager)? else { 134 return Ok(identity_projection_from_manager(manager, sqlite_store)?); 135 }; 136 let account_id = selected_account.account_id.to_string(); 137 138 sqlite_store.clear_surface_activation(account_id.as_str())?; 139 manager.remove_account(&selected_account.account_id)?; 140 if let Some(next_account) = manager.list_accounts()?.into_iter().next() { 141 manager.set_default_account(&next_account.account_id)?; 142 } 143 144 Ok(identity_projection_from_manager(manager, sqlite_store)?) 145 } 146 147 pub fn reset_local_device_state( 148 manager: &RadrootsNostrAccountsManager, 149 sqlite_store: &AppSqliteStore, 150 accounts_paths: &AppSharedAccountsPaths, 151 ) -> Result<AppIdentityProjection, DesktopAccountsCommandError> { 152 let account_ids = manager 153 .list_accounts()? 154 .into_iter() 155 .map(|record| record.account_id) 156 .collect::<Vec<_>>(); 157 158 for account_id in &account_ids { 159 sqlite_store.clear_surface_activation(account_id.as_str())?; 160 } 161 for account_id in account_ids { 162 manager.remove_account(&account_id)?; 163 } 164 165 remove_accounts_file_if_present(accounts_paths.store_path.as_path())?; 166 Ok(identity_projection_from_manager(manager, sqlite_store)?) 167 } 168 169 fn bootstrap_desktop_accounts_with_availability( 170 paths: &AppSharedAccountsPaths, 171 availability: RadrootsSecretBackendAvailability, 172 ) -> Result<DesktopAccountsBootstrap, DesktopAccountsBootstrapError> { 173 ensure_directory(paths.data_root.as_path())?; 174 ensure_directory(paths.secrets_root.as_path())?; 175 176 let selection = local_account_secret_backend_selection(); 177 let (accounts_manager, _) = RadrootsNostrAccountsManager::new_local_file_backed( 178 paths.store_path.as_path(), 179 paths.secrets_root.as_path(), 180 selection, 181 availability, 182 "radroots_app_encrypted_file", 183 )?; 184 Ok(DesktopAccountsBootstrap { 185 accounts_manager: Some(accounts_manager), 186 }) 187 } 188 189 fn ensure_directory(path: &Path) -> Result<(), DesktopAccountsBootstrapError> { 190 fs::create_dir_all(path).map_err(|source| DesktopAccountsBootstrapError::CreateDirectory { 191 path: path.to_path_buf(), 192 source, 193 }) 194 } 195 196 fn local_account_secret_backend_selection() -> RadrootsSecretBackendSelection { 197 RadrootsSecretBackendSelection { 198 primary: RadrootsSecretBackend::EncryptedFile, 199 fallback: None, 200 } 201 } 202 203 fn secret_backend_availability() 204 -> Result<RadrootsSecretBackendAvailability, DesktopAccountsBootstrapError> { 205 Ok(RadrootsSecretBackendAvailability { 206 host_vault: RadrootsHostVaultCapabilities::unavailable(), 207 encrypted_file: true, 208 external_command: false, 209 memory: false, 210 }) 211 } 212 213 fn import_identity( 214 request: &DesktopLocalIdentityImportRequest, 215 ) -> Result<RadrootsIdentity, DesktopAccountsCommandError> { 216 match request.mode { 217 DesktopLocalIdentityImportMode::RawSecretKey => Ok(RadrootsIdentity::from_secret_key_str( 218 request.secret_text.trim(), 219 )?), 220 DesktopLocalIdentityImportMode::EncryptedSecretKey => { 221 let Some(password) = request.password.as_deref() else { 222 return Err(DesktopAccountsCommandError::EncryptedImportPasswordRequired); 223 }; 224 Ok(RadrootsIdentity::from_encrypted_secret_key_str( 225 request.secret_text.trim(), 226 password, 227 )?) 228 } 229 } 230 } 231 232 fn remove_accounts_file_if_present(path: &Path) -> Result<(), DesktopAccountsCommandError> { 233 match fs::remove_file(path) { 234 Ok(()) => Ok(()), 235 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()), 236 Err(source) => Err(DesktopAccountsCommandError::RemoveAccountStore { 237 path: path.to_path_buf(), 238 source, 239 }), 240 } 241 } 242 243 pub(crate) fn identity_projection_from_manager( 244 manager: &RadrootsNostrAccountsManager, 245 sqlite_store: &AppSqliteStore, 246 ) -> Result<AppIdentityProjection, DesktopAccountsProjectionError> { 247 let roster_records = manager.list_accounts()?; 248 let roster = account_roster_from_records(roster_records.as_slice()); 249 250 match manager.default_account_status()? { 251 RadrootsNostrAccountStatus::NotConfigured => { 252 Ok(AppIdentityProjection::missing_with_roster(roster)) 253 } 254 RadrootsNostrAccountStatus::PublicOnly { account } 255 | RadrootsNostrAccountStatus::Ready { account } => Ok(AppIdentityProjection::ready( 256 roster, 257 selected_account_projection_from_record(&account, sqlite_store)?, 258 )), 259 } 260 } 261 262 fn selected_account_projection_from_record( 263 record: &RadrootsNostrAccountRecord, 264 sqlite_store: &AppSqliteStore, 265 ) -> Result<SelectedAccountProjection, DesktopAccountsProjectionError> { 266 let account = account_summary_from_record(record); 267 268 Ok( 269 match sqlite_store.load_surface_activation(account.account_id.as_str())? { 270 Some(activation) => { 271 SelectedAccountProjection::from_surface_activation(account, activation) 272 } 273 None => { 274 let activation = default_farmer_surface_activation(account.account_id.as_str()); 275 sqlite_store.save_surface_activation(&activation)?; 276 SelectedAccountProjection::from_surface_activation(account, activation) 277 } 278 }, 279 ) 280 } 281 282 fn selected_account_record( 283 manager: &RadrootsNostrAccountsManager, 284 ) -> Result<Option<RadrootsNostrAccountRecord>, RadrootsNostrAccountsError> { 285 match manager.default_account_status()? { 286 RadrootsNostrAccountStatus::NotConfigured => Ok(None), 287 RadrootsNostrAccountStatus::PublicOnly { account } 288 | RadrootsNostrAccountStatus::Ready { account } => Ok(Some(account)), 289 } 290 } 291 292 fn default_farmer_surface_activation(account_id: &str) -> AccountSurfaceActivationProjection { 293 AccountSurfaceActivationProjection::new( 294 account_id, 295 SelectedSurfaceProjection::new(ActiveSurface::Farmer), 296 FarmerActivationProjection::active(FarmId::new()), 297 ) 298 } 299 300 fn account_roster_from_records(records: &[RadrootsNostrAccountRecord]) -> Vec<AccountSummary> { 301 records.iter().map(account_summary_from_record).collect() 302 } 303 304 fn account_summary_from_record(record: &RadrootsNostrAccountRecord) -> AccountSummary { 305 AccountSummary { 306 account_id: record.account_id.to_string(), 307 npub: record.public_identity.public_key_npub.clone(), 308 label: record.label.clone(), 309 custody: radroots_app_view::AccountCustody::LocalManaged, 310 } 311 } 312 313 #[derive(Debug, Error)] 314 pub enum DesktopAccountsProjectionError { 315 #[error(transparent)] 316 Accounts(#[from] RadrootsNostrAccountsError), 317 #[error(transparent)] 318 Sqlite(#[from] AppSqliteError), 319 } 320 321 #[derive(Debug, Error)] 322 pub enum DesktopAccountsCommandError { 323 #[error(transparent)] 324 Accounts(#[from] RadrootsNostrAccountsError), 325 #[error(transparent)] 326 Identity(#[from] IdentityError), 327 #[error(transparent)] 328 Sqlite(#[from] AppSqliteError), 329 #[error(transparent)] 330 Projection(#[from] DesktopAccountsProjectionError), 331 #[error("encrypted secret key import requires a password")] 332 EncryptedImportPasswordRequired, 333 #[error("failed to remove account store {path}: {source}")] 334 RemoveAccountStore { 335 path: PathBuf, 336 source: std::io::Error, 337 }, 338 } 339 340 #[derive(Debug, Error)] 341 pub enum DesktopAccountsBootstrapError { 342 #[error("failed to create runtime directory {path}: {source}")] 343 CreateDirectory { 344 path: PathBuf, 345 source: std::io::Error, 346 }, 347 #[error(transparent)] 348 Accounts(#[from] RadrootsNostrAccountsError), 349 #[error(transparent)] 350 Projection(#[from] DesktopAccountsProjectionError), 351 } 352 353 #[cfg(test)] 354 mod tests { 355 use std::{ 356 fs, 357 path::PathBuf, 358 sync::Arc, 359 time::{SystemTime, UNIX_EPOCH}, 360 }; 361 362 use radroots_app_core::AppSharedAccountsPaths; 363 use radroots_app_sqlite::{AppSqliteStore, DatabaseTarget}; 364 use radroots_app_view::{ 365 AccountSurfaceActivationProjection, ActiveSurface, AppStartupGate, IdentityReadiness, 366 SelectedSurfaceProjection, 367 }; 368 use radroots_identity::RadrootsIdentity; 369 use radroots_nostr_accounts::prelude::{ 370 RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore, 371 RadrootsNostrMemoryAccountStore, RadrootsNostrSecretVaultMemory, 372 }; 373 use radroots_secret_vault::RadrootsHostVaultCapabilities; 374 375 use super::{ 376 DesktopLocalIdentityImportRequest, account_summary_from_record, 377 bootstrap_desktop_accounts_with_availability, generate_local_account, 378 identity_projection_from_manager, import_local_account, remove_selected_local_key, 379 reset_local_device_state, select_local_account, selected_account_projection_from_record, 380 selected_account_record, 381 }; 382 383 fn temp_shared_accounts_paths(label: &str) -> AppSharedAccountsPaths { 384 let suffix = SystemTime::now() 385 .duration_since(UNIX_EPOCH) 386 .expect("clock") 387 .as_nanos(); 388 let base = std::env::temp_dir().join(format!("radroots_app_accounts_{label}_{suffix}")); 389 390 AppSharedAccountsPaths { 391 data_root: base.join("data/shared/accounts"), 392 secrets_root: base.join("secrets/shared/accounts"), 393 store_path: base.join("data/shared/accounts/store.json"), 394 } 395 } 396 397 fn unavailable_secret_backend_availability() 398 -> radroots_secret_vault::RadrootsSecretBackendAvailability { 399 radroots_secret_vault::RadrootsSecretBackendAvailability { 400 host_vault: RadrootsHostVaultCapabilities::unavailable(), 401 encrypted_file: false, 402 external_command: false, 403 memory: false, 404 } 405 } 406 407 #[test] 408 fn bootstrap_fails_when_encrypted_file_backend_is_unavailable() { 409 let paths = temp_shared_accounts_paths("blocked"); 410 fs::create_dir_all(paths.data_root.as_path()).expect("data root should create"); 411 fs::create_dir_all(paths.secrets_root.as_path()).expect("secrets root should create"); 412 match bootstrap_desktop_accounts_with_availability( 413 &paths, 414 unavailable_secret_backend_availability(), 415 ) { 416 Err(super::DesktopAccountsBootstrapError::Accounts(_)) => {} 417 Err(other) => panic!("unexpected bootstrap error: {other}"), 418 Ok(_) => panic!("bootstrap should fail when encrypted file backend is unavailable"), 419 } 420 421 cleanup_paths(&paths); 422 } 423 424 #[test] 425 fn manager_projection_uses_selected_account_and_activation_state() { 426 let sqlite_store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("sqlite store"); 427 let manager = RadrootsNostrAccountsManager::new( 428 Arc::new(RadrootsNostrMemoryAccountStore::new()), 429 Arc::new(RadrootsNostrSecretVaultMemory::new()), 430 ) 431 .expect("memory manager should build"); 432 let account_id = manager 433 .generate_identity(Some("North field".to_owned()), true) 434 .expect("account should generate"); 435 let selected_account = selected_account_record(&manager) 436 .expect("selected account should load") 437 .expect("selected account should exist"); 438 let selected_account_summary = account_summary_from_record(&selected_account); 439 let selected_account_projection = 440 selected_account_projection_from_record(&selected_account, &sqlite_store) 441 .expect("selected account projection"); 442 443 assert_eq!( 444 selected_account_projection.account, 445 selected_account_summary 446 ); 447 assert_eq!( 448 selected_account_projection.selected_surface, 449 SelectedSurfaceProjection::new(ActiveSurface::Farmer) 450 ); 451 assert!(selected_account_projection.farmer_activation.is_active()); 452 453 let activation = AccountSurfaceActivationProjection::new( 454 account_id.as_str(), 455 SelectedSurfaceProjection::new(ActiveSurface::Farmer), 456 radroots_app_view::FarmerActivationProjection::active(radroots_app_view::FarmId::new()), 457 ); 458 sqlite_store 459 .save_surface_activation(&activation) 460 .expect("surface activation should save"); 461 462 let projection = 463 identity_projection_from_manager(&manager, &sqlite_store).expect("projection"); 464 465 assert_eq!(projection.readiness, IdentityReadiness::Ready); 466 assert_eq!(projection.startup_gate(), AppStartupGate::Farmer); 467 assert_eq!(projection.roster.len(), 1); 468 assert_eq!( 469 projection 470 .selected_account 471 .as_ref() 472 .map(|account| account.account.account_id.as_str()), 473 Some(account_id.as_str()) 474 ); 475 assert_eq!( 476 projection 477 .selected_account 478 .as_ref() 479 .map(|account| account.active_surface()), 480 Some(ActiveSurface::Farmer) 481 ); 482 assert!( 483 projection 484 .selected_account 485 .as_ref() 486 .is_some_and(|account| account.farmer_activation.is_active()) 487 ); 488 } 489 490 #[test] 491 fn command_generate_and_select_support_multiple_local_accounts() { 492 let sqlite_store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("sqlite store"); 493 let manager = RadrootsNostrAccountsManager::new( 494 Arc::new(RadrootsNostrMemoryAccountStore::new()), 495 Arc::new(RadrootsNostrSecretVaultMemory::new()), 496 ) 497 .expect("memory manager should build"); 498 499 let first_projection = 500 generate_local_account(&manager, &sqlite_store, Some("First".to_owned())) 501 .expect("first account should generate"); 502 let first_account_id = first_projection 503 .selected_account 504 .as_ref() 505 .expect("first selected account") 506 .account 507 .account_id 508 .clone(); 509 510 let second_projection = 511 generate_local_account(&manager, &sqlite_store, Some("Second".to_owned())) 512 .expect("second account should generate"); 513 let second_account_id = second_projection 514 .selected_account 515 .as_ref() 516 .expect("second selected account") 517 .account 518 .account_id 519 .clone(); 520 521 assert_eq!(first_projection.roster.len(), 1); 522 assert_eq!(second_projection.roster.len(), 2); 523 assert_ne!(first_account_id, second_account_id); 524 assert_eq!( 525 second_projection 526 .selected_account 527 .as_ref() 528 .map(|account| account.account.label.as_deref()), 529 Some(Some("Second")) 530 ); 531 assert_eq!(second_projection.startup_gate(), AppStartupGate::Farmer); 532 } 533 534 #[test] 535 fn command_import_supports_raw_and_encrypted_secret_keys() { 536 let sqlite_store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("sqlite store"); 537 let manager = RadrootsNostrAccountsManager::new( 538 Arc::new(RadrootsNostrMemoryAccountStore::new()), 539 Arc::new(RadrootsNostrSecretVaultMemory::new()), 540 ) 541 .expect("memory manager should build"); 542 let raw_identity = RadrootsIdentity::generate(); 543 let encrypted_identity = RadrootsIdentity::generate(); 544 let encrypted_secret = encrypted_identity 545 .encrypt_secret_key_ncryptsec("radroots-password") 546 .expect("encrypted secret should export"); 547 548 let raw_projection = import_local_account( 549 &manager, 550 &sqlite_store, 551 &DesktopLocalIdentityImportRequest::raw_secret_key(raw_identity.nsec()), 552 ) 553 .expect("raw import should succeed"); 554 let encrypted_projection = import_local_account( 555 &manager, 556 &sqlite_store, 557 &DesktopLocalIdentityImportRequest::encrypted_secret_key( 558 encrypted_secret, 559 "radroots-password", 560 ), 561 ) 562 .expect("encrypted import should succeed"); 563 564 assert_eq!(raw_projection.roster.len(), 1); 565 assert_eq!(encrypted_projection.roster.len(), 2); 566 assert_eq!( 567 encrypted_projection 568 .selected_account 569 .as_ref() 570 .map(|account| account.account.account_id.as_str()), 571 Some(encrypted_identity.id().as_str()) 572 ); 573 } 574 575 #[test] 576 fn command_select_refreshes_selected_account_activation() { 577 let sqlite_store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("sqlite store"); 578 let manager = RadrootsNostrAccountsManager::new( 579 Arc::new(RadrootsNostrMemoryAccountStore::new()), 580 Arc::new(RadrootsNostrSecretVaultMemory::new()), 581 ) 582 .expect("memory manager should build"); 583 let first_account_id = manager 584 .generate_identity(Some("First".to_owned()), true) 585 .expect("first account should generate"); 586 let second_account_id = manager 587 .generate_identity(Some("Second".to_owned()), false) 588 .expect("second account should generate"); 589 let activation = AccountSurfaceActivationProjection::new( 590 second_account_id.as_str(), 591 SelectedSurfaceProjection::new(ActiveSurface::Farmer), 592 radroots_app_view::FarmerActivationProjection::active(radroots_app_view::FarmId::new()), 593 ); 594 sqlite_store 595 .save_surface_activation(&activation) 596 .expect("surface activation should save"); 597 598 let projection = select_local_account(&manager, &sqlite_store, second_account_id.as_str()) 599 .expect("selection should refresh"); 600 601 assert_eq!( 602 projection 603 .selected_account 604 .as_ref() 605 .map(|account| account.account.account_id.as_str()), 606 Some(second_account_id.as_str()) 607 ); 608 assert_eq!(projection.startup_gate(), AppStartupGate::Farmer); 609 assert_eq!( 610 projection 611 .selected_account 612 .as_ref() 613 .map(|account| account.active_surface()), 614 Some(ActiveSurface::Farmer) 615 ); 616 assert_eq!( 617 selected_account_record(&manager) 618 .expect("selected account") 619 .map(|account| account.account_id), 620 Some(second_account_id.clone()) 621 ); 622 assert_ne!( 623 first_account_id, 624 selected_account_record(&manager) 625 .expect("selected account") 626 .expect("selected") 627 .account_id 628 ); 629 } 630 631 #[test] 632 fn command_remove_selected_local_key_clears_activation_and_selects_next_account() { 633 let sqlite_store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("sqlite store"); 634 let manager = RadrootsNostrAccountsManager::new( 635 Arc::new(RadrootsNostrMemoryAccountStore::new()), 636 Arc::new(RadrootsNostrSecretVaultMemory::new()), 637 ) 638 .expect("memory manager should build"); 639 let first_account_id = manager 640 .generate_identity(Some("First".to_owned()), true) 641 .expect("first account should generate"); 642 let second_account_id = manager 643 .generate_identity(Some("Second".to_owned()), false) 644 .expect("second account should generate"); 645 manager 646 .set_default_account(&first_account_id) 647 .expect("first account should remain selected"); 648 let activation = AccountSurfaceActivationProjection::new( 649 first_account_id.as_str(), 650 SelectedSurfaceProjection::new(ActiveSurface::Farmer), 651 radroots_app_view::FarmerActivationProjection::active(radroots_app_view::FarmId::new()), 652 ); 653 sqlite_store 654 .save_surface_activation(&activation) 655 .expect("surface activation should save"); 656 657 let projection = remove_selected_local_key(&manager, &sqlite_store) 658 .expect("selected local key should remove"); 659 660 assert_eq!(projection.roster.len(), 1); 661 assert_eq!( 662 projection 663 .selected_account 664 .as_ref() 665 .map(|account| account.account.account_id.as_str()), 666 Some(second_account_id.as_str()) 667 ); 668 assert_eq!( 669 sqlite_store 670 .load_surface_activation(first_account_id.as_str()) 671 .expect("removed activation should load"), 672 None 673 ); 674 } 675 676 #[test] 677 fn command_reset_local_device_state_removes_store_file_and_all_activations() { 678 let paths = temp_shared_accounts_paths("reset"); 679 fs::create_dir_all(paths.data_root.as_path()).expect("data root should create"); 680 fs::create_dir_all(paths.secrets_root.as_path()).expect("secrets root should create"); 681 let manager = RadrootsNostrAccountsManager::new( 682 Arc::new(RadrootsNostrFileAccountStore::new( 683 paths.store_path.as_path(), 684 )), 685 Arc::new(RadrootsNostrSecretVaultMemory::new()), 686 ) 687 .expect("file-backed manager should build"); 688 let sqlite_store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("sqlite store"); 689 let first_account_id = manager 690 .generate_identity(Some("First".to_owned()), true) 691 .expect("first account should generate"); 692 let second_account_id = manager 693 .generate_identity(Some("Second".to_owned()), false) 694 .expect("second account should generate"); 695 sqlite_store 696 .save_surface_activation(&AccountSurfaceActivationProjection::new( 697 first_account_id.as_str(), 698 SelectedSurfaceProjection::new(ActiveSurface::Farmer), 699 radroots_app_view::FarmerActivationProjection::active( 700 radroots_app_view::FarmId::new(), 701 ), 702 )) 703 .expect("first activation should save"); 704 sqlite_store 705 .save_surface_activation(&AccountSurfaceActivationProjection::new( 706 second_account_id.as_str(), 707 SelectedSurfaceProjection::new(ActiveSurface::Farmer), 708 radroots_app_view::FarmerActivationProjection::active( 709 radroots_app_view::FarmId::new(), 710 ), 711 )) 712 .expect("second activation should save"); 713 assert!(paths.store_path.exists()); 714 715 let projection = reset_local_device_state(&manager, &sqlite_store, &paths) 716 .expect("device state should reset"); 717 718 assert_eq!(projection.readiness, IdentityReadiness::MissingAccount); 719 assert_eq!(projection.startup_gate(), AppStartupGate::SetupRequired); 720 assert!(projection.roster.is_empty()); 721 assert!(projection.selected_account.is_none()); 722 assert!(!paths.store_path.exists()); 723 assert_eq!( 724 sqlite_store 725 .load_surface_activation(first_account_id.as_str()) 726 .expect("first activation should load"), 727 None 728 ); 729 assert_eq!( 730 sqlite_store 731 .load_surface_activation(second_account_id.as_str()) 732 .expect("second activation should load"), 733 None 734 ); 735 736 cleanup_paths(&paths); 737 } 738 739 fn cleanup_paths(paths: &AppSharedAccountsPaths) { 740 let Some(base) = paths.data_root.ancestors().nth(3).map(PathBuf::from) else { 741 return; 742 }; 743 let _ = fs::remove_dir_all(base); 744 } 745 }