1use std::collections::BTreeMap;
4use std::future::Future;
5use std::path::{Path, PathBuf};
6
7use anyhow::{bail, ensure, Context as _, Result};
8use serde::{Deserialize, Serialize};
9use tokio::fs;
10use tokio::io::AsyncWriteExt;
11use tokio::task::{JoinHandle, JoinSet};
12use uuid::Uuid;
13
14#[cfg(not(target_os = "ios"))]
15use tokio::sync::oneshot;
16#[cfg(not(target_os = "ios"))]
17use tokio::time::{sleep, Duration};
18
19use crate::context::{Context, ContextBuilder};
20use crate::events::{Event, EventEmitter, EventType, Events};
21use crate::log::{info, warn};
22use crate::push::PushSubscriber;
23use crate::stock_str::StockStrings;
24
25#[derive(Debug)]
27pub struct Accounts {
28 dir: PathBuf,
29 config: Config,
30 accounts: BTreeMap<u32, Context>,
32
33 events: Events,
35
36 pub(crate) stockstrings: StockStrings,
41
42 push_subscriber: PushSubscriber,
44}
45
46impl Accounts {
47 pub async fn new(dir: PathBuf, writable: bool) -> Result<Self> {
49 if writable && !dir.exists() {
50 Accounts::create(&dir).await?;
51 }
52
53 Accounts::open(dir, writable).await
54 }
55
56 async fn create(dir: &Path) -> Result<()> {
58 fs::create_dir_all(dir)
59 .await
60 .context("failed to create folder")?;
61
62 Config::new(dir).await?;
63
64 Ok(())
65 }
66
67 async fn open(dir: PathBuf, writable: bool) -> Result<Self> {
70 ensure!(dir.exists(), "directory does not exist");
71
72 let config_file = dir.join(CONFIG_NAME);
73 ensure!(config_file.exists(), "{:?} does not exist", config_file);
74
75 let config = Config::from_file(config_file, writable).await?;
76 let events = Events::new();
77 let stockstrings = StockStrings::new();
78 let push_subscriber = PushSubscriber::new();
79 let accounts = config
80 .load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
81 .await
82 .context("failed to load accounts")?;
83
84 Ok(Self {
85 dir,
86 config,
87 accounts,
88 events,
89 stockstrings,
90 push_subscriber,
91 })
92 }
93
94 pub fn get_account(&self, id: u32) -> Option<Context> {
96 self.accounts.get(&id).cloned()
97 }
98
99 pub fn get_selected_account(&self) -> Option<Context> {
101 let id = self.config.get_selected_account();
102 self.accounts.get(&id).cloned()
103 }
104
105 pub fn get_selected_account_id(&self) -> Option<u32> {
107 match self.config.get_selected_account() {
108 0 => None,
109 id => Some(id),
110 }
111 }
112
113 pub async fn select_account(&mut self, id: u32) -> Result<()> {
115 self.config.select_account(id).await?;
116
117 Ok(())
118 }
119
120 pub async fn add_account(&mut self) -> Result<u32> {
124 let account_config = self.config.new_account().await?;
125 let dbfile = account_config.dbfile(&self.dir);
126
127 let ctx = ContextBuilder::new(dbfile)
128 .with_id(account_config.id)
129 .with_events(self.events.clone())
130 .with_stock_strings(self.stockstrings.clone())
131 .with_push_subscriber(self.push_subscriber.clone())
132 .build()
133 .await?;
134 ctx.open("".to_string()).await?;
137
138 self.accounts.insert(account_config.id, ctx);
139 self.emit_event(EventType::AccountsChanged);
140
141 Ok(account_config.id)
142 }
143
144 pub async fn add_closed_account(&mut self) -> Result<u32> {
146 let account_config = self.config.new_account().await?;
147 let dbfile = account_config.dbfile(&self.dir);
148
149 let ctx = ContextBuilder::new(dbfile)
150 .with_id(account_config.id)
151 .with_events(self.events.clone())
152 .with_stock_strings(self.stockstrings.clone())
153 .with_push_subscriber(self.push_subscriber.clone())
154 .build()
155 .await?;
156 self.accounts.insert(account_config.id, ctx);
157 self.emit_event(EventType::AccountsChanged);
158
159 Ok(account_config.id)
160 }
161
162 pub async fn remove_account(&mut self, id: u32) -> Result<()> {
164 let ctx = self
165 .accounts
166 .remove(&id)
167 .with_context(|| format!("no account with id {id}"))?;
168 ctx.stop_io().await;
169
170 ctx.sql.close().await;
182 drop(ctx);
183
184 if let Some(cfg) = self.config.get_account(id) {
185 let account_path = self.dir.join(cfg.dir);
186
187 try_many_times(|| fs::remove_dir_all(&account_path))
188 .await
189 .context("failed to remove account data")?;
190 }
191 self.config.remove_account(id).await?;
192 self.emit_event(EventType::AccountsChanged);
193
194 Ok(())
195 }
196
197 pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
201 let blobdir = Context::derive_blobdir(&dbfile);
202 let walfile = Context::derive_walfile(&dbfile);
203
204 ensure!(dbfile.exists(), "no database found: {}", dbfile.display());
205 ensure!(blobdir.exists(), "no blobdir found: {}", blobdir.display());
206
207 let old_id = self.config.get_selected_account();
208
209 let account_config = self
211 .config
212 .new_account()
213 .await
214 .context("failed to create new account")?;
215
216 let new_dbfile = account_config.dbfile(&self.dir);
217 let new_blobdir = Context::derive_blobdir(&new_dbfile);
218 let new_walfile = Context::derive_walfile(&new_dbfile);
219
220 let res = {
221 fs::create_dir_all(self.dir.join(&account_config.dir))
222 .await
223 .context("failed to create dir")?;
224 try_many_times(|| fs::rename(&dbfile, &new_dbfile))
225 .await
226 .context("failed to rename dbfile")?;
227 try_many_times(|| fs::rename(&blobdir, &new_blobdir))
228 .await
229 .context("failed to rename blobdir")?;
230 if walfile.exists() {
231 fs::rename(&walfile, &new_walfile)
232 .await
233 .context("failed to rename walfile")?;
234 }
235 Ok(())
236 };
237
238 match res {
239 Ok(_) => {
240 let ctx = Context::new(
241 &new_dbfile,
242 account_config.id,
243 self.events.clone(),
244 self.stockstrings.clone(),
245 )
246 .await?;
247 self.accounts.insert(account_config.id, ctx);
248 Ok(account_config.id)
249 }
250 Err(err) => {
251 let account_path = std::path::PathBuf::from(&account_config.dir);
252 try_many_times(|| fs::remove_dir_all(&account_path))
253 .await
254 .context("failed to remove account data")?;
255 self.config.remove_account(account_config.id).await?;
256
257 self.select_account(old_id).await?;
259
260 Err(err)
261 }
262 }
263 }
264
265 pub fn get_all(&self) -> Vec<u32> {
267 self.accounts.keys().copied().collect()
268 }
269
270 pub async fn start_io(&mut self) {
272 for account in self.accounts.values_mut() {
273 account.start_io().await;
274 }
275 }
276
277 pub async fn stop_io(&self) {
279 info!(self, "Stopping IO for all accounts.");
282 for account in self.accounts.values() {
283 account.stop_io().await;
284 }
285 }
286
287 pub async fn maybe_network(&self) {
289 for account in self.accounts.values() {
290 account.scheduler.maybe_network().await;
291 }
292 }
293
294 pub async fn maybe_network_lost(&self) {
296 for account in self.accounts.values() {
297 account.scheduler.maybe_network_lost(account).await;
298 }
299 }
300
301 async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
306 events.emit(Event {
307 id: 0,
308 typ: EventType::Info(format!(
309 "Starting background fetch for {} accounts.",
310 accounts.len()
311 )),
312 });
313 let mut set = JoinSet::new();
314 for account in accounts {
315 set.spawn(async move {
316 if let Err(error) = account.background_fetch().await {
317 warn!(account, "{error:#}");
318 }
319 });
320 }
321 set.join_all().await;
322 }
323
324 async fn background_fetch_with_timeout(
326 accounts: Vec<Context>,
327 events: Events,
328 timeout: std::time::Duration,
329 ) {
330 if let Err(_err) = tokio::time::timeout(
331 timeout,
332 Self::background_fetch_no_timeout(accounts, events.clone()),
333 )
334 .await
335 {
336 events.emit(Event {
337 id: 0,
338 typ: EventType::Warning("Background fetch timed out.".to_string()),
339 });
340 }
341 events.emit(Event {
342 id: 0,
343 typ: EventType::AccountsBackgroundFetchDone,
344 });
345 }
346
347 pub fn background_fetch(&self, timeout: std::time::Duration) -> impl Future<Output = ()> {
356 let accounts: Vec<Context> = self.accounts.values().cloned().collect();
357 let events = self.events.clone();
358 Self::background_fetch_with_timeout(accounts, events, timeout)
359 }
360
361 pub fn emit_event(&self, event: EventType) {
363 self.events.emit(Event { id: 0, typ: event })
364 }
365
366 pub fn get_event_emitter(&self) -> EventEmitter {
368 self.events.get_emitter()
369 }
370
371 pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
373 self.push_subscriber.set_device_token(token).await;
374 Ok(())
375 }
376}
377
378const CONFIG_NAME: &str = "accounts.toml";
380
381#[cfg(not(target_os = "ios"))]
383const LOCKFILE_NAME: &str = "accounts.lock";
384
385const DB_NAME: &str = "dc.db";
387
388#[derive(Debug)]
390struct Config {
391 file: PathBuf,
392 inner: InnerConfig,
393 lock_task: Option<JoinHandle<anyhow::Result<()>>>,
396}
397
398#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
402struct InnerConfig {
403 pub selected_account: u32,
405 pub next_id: u32,
406 pub accounts: Vec<AccountConfig>,
407}
408
409impl Drop for Config {
410 fn drop(&mut self) {
411 if let Some(lock_task) = self.lock_task.take() {
412 lock_task.abort();
413 }
414 }
415}
416
417impl Config {
418 #[cfg(target_os = "ios")]
419 async fn create_lock_task(_dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
420 Ok(None)
424 }
425
426 #[cfg(not(target_os = "ios"))]
427 async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
428 let lockfile = dir.join(LOCKFILE_NAME);
429 let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
430 let (locked_tx, locked_rx) = oneshot::channel();
431 let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
432 let mut timeout = Duration::from_millis(100);
433 let _guard = loop {
434 match lock.try_write() {
435 Ok(guard) => break Ok(guard),
436 Err(err) => {
437 if timeout.as_millis() > 1600 {
438 break Err(err);
439 }
440 sleep(timeout).await;
444 if err.kind() == std::io::ErrorKind::WouldBlock {
445 timeout *= 2;
446 }
447 }
448 }
449 }?;
450 locked_tx
451 .send(())
452 .ok()
453 .context("Cannot notify about lockfile locking")?;
454 let (_tx, rx) = oneshot::channel();
455 rx.await?;
456 Ok(())
457 });
458 if locked_rx.await.is_err() {
459 bail!("Delta Chat is already running. To use Delta Chat, you must first close the existing Delta Chat process, or restart your device. (accounts.lock file is already locked)");
460 };
461 Ok(Some(lock_task))
462 }
463
464 async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
466 let dir = file.parent().context("Cannot get config file directory")?;
467 let inner = InnerConfig {
468 accounts: Vec::new(),
469 selected_account: 0,
470 next_id: 1,
471 };
472 if !lock {
473 let cfg = Self {
474 file,
475 inner,
476 lock_task: None,
477 };
478 return Ok(cfg);
479 }
480 let lock_task = Self::create_lock_task(dir.to_path_buf()).await?;
481 let cfg = Self {
482 file,
483 inner,
484 lock_task,
485 };
486 Ok(cfg)
487 }
488
489 pub async fn new(dir: &Path) -> Result<Self> {
491 let lock = true;
492 let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
493 cfg.sync().await?;
494
495 Ok(cfg)
496 }
497
498 async fn sync(&mut self) -> Result<()> {
502 #[cfg(not(target_os = "ios"))]
503 ensure!(!self
504 .lock_task
505 .as_ref()
506 .context("Config is read-only")?
507 .is_finished());
508
509 let tmp_path = self.file.with_extension("toml.tmp");
510 let mut file = fs::File::create(&tmp_path)
511 .await
512 .context("failed to create a tmp config")?;
513 file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
514 .await
515 .context("failed to write a tmp config")?;
516 file.sync_data()
517 .await
518 .context("failed to sync a tmp config")?;
519 drop(file);
520 fs::rename(&tmp_path, &self.file)
521 .await
522 .context("failed to rename config")?;
523 Ok(())
524 }
525
526 pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
528 let mut config = Self::new_nosync(file, writable).await?;
529 let bytes = fs::read(&config.file)
530 .await
531 .context("Failed to read file")?;
532 let s = std::str::from_utf8(&bytes)?;
533 config.inner = toml::from_str(s).context("Failed to parse config")?;
534
535 let mut modified = false;
538 for account in &mut config.inner.accounts {
539 if account.dir.is_absolute() {
540 if let Some(old_path_parent) = account.dir.parent() {
541 if let Ok(new_path) = account.dir.strip_prefix(old_path_parent) {
542 account.dir = new_path.to_path_buf();
543 modified = true;
544 }
545 }
546 }
547 }
548 if modified && writable {
549 config.sync().await?;
550 }
551
552 Ok(config)
553 }
554
555 pub async fn load_accounts(
560 &self,
561 events: &Events,
562 stockstrings: &StockStrings,
563 push_subscriber: PushSubscriber,
564 dir: &Path,
565 ) -> Result<BTreeMap<u32, Context>> {
566 let mut accounts = BTreeMap::new();
567
568 for account_config in &self.inner.accounts {
569 let dbfile = account_config.dbfile(dir);
570 let ctx = ContextBuilder::new(dbfile.clone())
571 .with_id(account_config.id)
572 .with_events(events.clone())
573 .with_stock_strings(stockstrings.clone())
574 .with_push_subscriber(push_subscriber.clone())
575 .build()
576 .await
577 .with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
578 ctx.open("".to_string()).await?;
581
582 accounts.insert(account_config.id, ctx);
583 }
584
585 Ok(accounts)
586 }
587
588 async fn new_account(&mut self) -> Result<AccountConfig> {
590 let id = {
591 let id = self.inner.next_id;
592 let uuid = Uuid::new_v4();
593 let target_dir = PathBuf::from(uuid.to_string());
594
595 self.inner.accounts.push(AccountConfig {
596 id,
597 dir: target_dir,
598 uuid,
599 });
600 self.inner.next_id += 1;
601 id
602 };
603
604 self.sync().await?;
605
606 self.select_account(id)
607 .await
608 .context("failed to select just added account")?;
609 let cfg = self
610 .get_account(id)
611 .context("failed to get just added account")?;
612 Ok(cfg)
613 }
614
615 pub async fn remove_account(&mut self, id: u32) -> Result<()> {
617 {
618 if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
619 self.inner.accounts.remove(idx);
621 }
622 if self.inner.selected_account == id {
623 self.inner.selected_account = self
625 .inner
626 .accounts
627 .first()
628 .map(|e| e.id)
629 .unwrap_or_default();
630 }
631 }
632
633 self.sync().await
634 }
635
636 fn get_account(&self, id: u32) -> Option<AccountConfig> {
638 self.inner.accounts.iter().find(|e| e.id == id).cloned()
639 }
640
641 pub fn get_selected_account(&self) -> u32 {
643 self.inner.selected_account
644 }
645
646 pub async fn select_account(&mut self, id: u32) -> Result<()> {
648 {
649 ensure!(
650 self.inner.accounts.iter().any(|e| e.id == id),
651 "invalid account id: {}",
652 id
653 );
654
655 self.inner.selected_account = id;
656 }
657
658 self.sync().await?;
659 Ok(())
660 }
661}
662
663async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
672where
673 F: Fn() -> Fut,
674 Fut: Future<Output = std::result::Result<(), T>>,
675{
676 let mut counter = 0;
677 loop {
678 counter += 1;
679
680 if let Err(err) = f().await {
681 if counter > 60 {
682 return Err(err);
683 }
684
685 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
687 } else {
688 break;
689 }
690 }
691 Ok(())
692}
693
694#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
696struct AccountConfig {
697 pub id: u32,
699
700 pub dir: std::path::PathBuf,
704
705 pub uuid: Uuid,
707}
708
709impl AccountConfig {
710 pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf {
712 accounts_dir.join(&self.dir).join(DB_NAME)
713 }
714}
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719 use crate::stock_str::{self, StockMessage};
720
721 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
722 async fn test_account_new_open() {
723 let dir = tempfile::tempdir().unwrap();
724 let p: PathBuf = dir.path().join("accounts1");
725
726 {
727 let writable = true;
728 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
729 accounts.add_account().await.unwrap();
730
731 assert_eq!(accounts.accounts.len(), 1);
732 assert_eq!(accounts.config.get_selected_account(), 1);
733 }
734 for writable in [true, false] {
735 let accounts = Accounts::new(p.clone(), writable).await.unwrap();
736
737 assert_eq!(accounts.accounts.len(), 1);
738 assert_eq!(accounts.config.get_selected_account(), 1);
739 }
740 }
741
742 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
743 async fn test_account_new_open_conflict() {
744 let dir = tempfile::tempdir().unwrap();
745 let p: PathBuf = dir.path().join("accounts");
746 let writable = true;
747 let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
748
749 let writable = true;
750 assert!(Accounts::new(p.clone(), writable).await.is_err());
751
752 let writable = false;
753 let accounts = Accounts::new(p, writable).await.unwrap();
754 assert_eq!(accounts.accounts.len(), 0);
755 assert_eq!(accounts.config.get_selected_account(), 0);
756 }
757
758 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
759 async fn test_account_new_add_remove() {
760 let dir = tempfile::tempdir().unwrap();
761 let p: PathBuf = dir.path().join("accounts");
762
763 let writable = true;
764 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
765 assert_eq!(accounts.accounts.len(), 0);
766 assert_eq!(accounts.config.get_selected_account(), 0);
767
768 let id = accounts.add_account().await.unwrap();
769 assert_eq!(id, 1);
770 assert_eq!(accounts.accounts.len(), 1);
771 assert_eq!(accounts.config.get_selected_account(), 1);
772
773 let id = accounts.add_account().await.unwrap();
774 assert_eq!(id, 2);
775 assert_eq!(accounts.config.get_selected_account(), id);
776 assert_eq!(accounts.accounts.len(), 2);
777
778 accounts.select_account(1).await.unwrap();
779 assert_eq!(accounts.config.get_selected_account(), 1);
780
781 accounts.remove_account(1).await.unwrap();
782 assert_eq!(accounts.config.get_selected_account(), 2);
783 assert_eq!(accounts.accounts.len(), 1);
784 }
785
786 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
787 async fn test_accounts_remove_last() -> Result<()> {
788 let dir = tempfile::tempdir()?;
789 let p: PathBuf = dir.path().join("accounts");
790
791 let writable = true;
792 let mut accounts = Accounts::new(p.clone(), writable).await?;
793 assert!(accounts.get_selected_account().is_none());
794 assert_eq!(accounts.config.get_selected_account(), 0);
795
796 let id = accounts.add_account().await?;
797 assert!(accounts.get_selected_account().is_some());
798 assert_eq!(id, 1);
799 assert_eq!(accounts.accounts.len(), 1);
800 assert_eq!(accounts.config.get_selected_account(), id);
801
802 accounts.remove_account(id).await?;
803 assert!(accounts.get_selected_account().is_none());
804
805 Ok(())
806 }
807
808 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
809 async fn test_migrate_account() {
810 let dir = tempfile::tempdir().unwrap();
811 let p: PathBuf = dir.path().join("accounts");
812
813 let writable = true;
814 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
815 assert_eq!(accounts.accounts.len(), 0);
816 assert_eq!(accounts.config.get_selected_account(), 0);
817
818 let extern_dbfile: PathBuf = dir.path().join("other");
819 let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
820 .await
821 .unwrap();
822 ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
823 .await
824 .unwrap();
825
826 drop(ctx);
827
828 accounts
829 .migrate_account(extern_dbfile.clone())
830 .await
831 .unwrap();
832 assert_eq!(accounts.accounts.len(), 1);
833 assert_eq!(accounts.config.get_selected_account(), 1);
834
835 let ctx = accounts.get_selected_account().unwrap();
836 assert_eq!(
837 "me@mail.com",
838 ctx.get_config(crate::config::Config::Addr)
839 .await
840 .unwrap()
841 .unwrap()
842 );
843 }
844
845 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
847 async fn test_accounts_sorted() {
848 let dir = tempfile::tempdir().unwrap();
849 let p: PathBuf = dir.path().join("accounts");
850
851 let writable = true;
852 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
853
854 for expected_id in 1..10 {
855 let id = accounts.add_account().await.unwrap();
856 assert_eq!(id, expected_id);
857 }
858
859 let ids = accounts.get_all();
860 for (i, expected_id) in (1..10).enumerate() {
861 assert_eq!(ids.get(i), Some(&expected_id));
862 }
863 }
864
865 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
866 async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
867 let dir = tempfile::tempdir()?;
868 let p: PathBuf = dir.path().join("accounts");
869 let dummy_accounts = 10;
870
871 let (id0, id1, id2) = {
872 let writable = true;
873 let mut accounts = Accounts::new(p.clone(), writable).await?;
874 accounts.add_account().await?;
875 let ids = accounts.get_all();
876 assert_eq!(ids.len(), 1);
877
878 let id0 = *ids.first().unwrap();
879 let ctx = accounts.get_account(id0).unwrap();
880 ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
881 .await?;
882
883 let id1 = accounts.add_account().await?;
884 let ctx = accounts.get_account(id1).unwrap();
885 ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
886 .await?;
887
888 for _ in 0..dummy_accounts {
890 let to_delete = accounts.add_account().await?;
891 accounts.remove_account(to_delete).await?;
892 }
893
894 let id2 = accounts.add_account().await?;
895 let ctx = accounts.get_account(id2).unwrap();
896 ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
897 .await?;
898
899 accounts.select_account(id1).await?;
900
901 (id0, id1, id2)
902 };
903 assert!(id0 > 0);
904 assert!(id1 > id0);
905 assert!(id2 > id1 + dummy_accounts);
906
907 let (id0_reopened, id1_reopened, id2_reopened) = {
908 let writable = false;
909 let accounts = Accounts::new(p.clone(), writable).await?;
910 let ctx = accounts.get_selected_account().unwrap();
911 assert_eq!(
912 ctx.get_config(crate::config::Config::Addr).await?,
913 Some("two@example.org".to_string())
914 );
915
916 let ids = accounts.get_all();
917 assert_eq!(ids.len(), 3);
918
919 let id0 = *ids.first().unwrap();
920 let ctx = accounts.get_account(id0).unwrap();
921 assert_eq!(
922 ctx.get_config(crate::config::Config::Addr).await?,
923 Some("one@example.org".to_string())
924 );
925
926 let id1 = *ids.get(1).unwrap();
927 let t = accounts.get_account(id1).unwrap();
928 assert_eq!(
929 t.get_config(crate::config::Config::Addr).await?,
930 Some("two@example.org".to_string())
931 );
932
933 let id2 = *ids.get(2).unwrap();
934 let ctx = accounts.get_account(id2).unwrap();
935 assert_eq!(
936 ctx.get_config(crate::config::Config::Addr).await?,
937 Some("three@example.org".to_string())
938 );
939
940 (id0, id1, id2)
941 };
942 assert_eq!(id0, id0_reopened);
943 assert_eq!(id1, id1_reopened);
944 assert_eq!(id2, id2_reopened);
945
946 Ok(())
947 }
948
949 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
950 async fn test_no_accounts_event_emitter() -> Result<()> {
951 let dir = tempfile::tempdir().unwrap();
952 let p: PathBuf = dir.path().join("accounts");
953
954 let writable = true;
955 let accounts = Accounts::new(p.clone(), writable).await?;
956
957 assert_eq!(accounts.accounts.len(), 0);
959
960 let event_emitter = accounts.get_event_emitter();
962
963 let duration = std::time::Duration::from_millis(1);
965 assert!(tokio::time::timeout(duration, event_emitter.recv())
966 .await
967 .is_err());
968
969 drop(accounts);
971 assert_eq!(event_emitter.recv().await, None);
972
973 Ok(())
974 }
975
976 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
977 async fn test_encrypted_account() -> Result<()> {
978 let dir = tempfile::tempdir().context("failed to create tempdir")?;
979 let p: PathBuf = dir.path().join("accounts");
980
981 let writable = true;
982 let mut accounts = Accounts::new(p.clone(), writable)
983 .await
984 .context("failed to create accounts manager")?;
985
986 assert_eq!(accounts.accounts.len(), 0);
987 let account_id = accounts
988 .add_closed_account()
989 .await
990 .context("failed to add closed account")?;
991 let account = accounts
992 .get_selected_account()
993 .context("failed to get account")?;
994 assert_eq!(account.id, account_id);
995 let passphrase_set_success = account
996 .open("foobar".to_string())
997 .await
998 .context("failed to set passphrase")?;
999 assert!(passphrase_set_success);
1000 drop(accounts);
1001
1002 let writable = false;
1003 let accounts = Accounts::new(p.clone(), writable)
1004 .await
1005 .context("failed to create second accounts manager")?;
1006 let account = accounts
1007 .get_selected_account()
1008 .context("failed to get account")?;
1009 assert_eq!(account.is_open().await, false);
1010
1011 assert_eq!(account.open("barfoo".to_string()).await?, false);
1013 assert_eq!(account.open("".to_string()).await?, false);
1014
1015 assert_eq!(account.open("foobar".to_string()).await?, true);
1016 assert_eq!(account.is_open().await, true);
1017
1018 Ok(())
1019 }
1020
1021 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1023 async fn test_accounts_share_translations() -> Result<()> {
1024 let dir = tempfile::tempdir().unwrap();
1025 let p: PathBuf = dir.path().join("accounts");
1026
1027 let writable = true;
1028 let mut accounts = Accounts::new(p.clone(), writable).await?;
1029 accounts.add_account().await?;
1030 accounts.add_account().await?;
1031
1032 let account1 = accounts.get_account(1).context("failed to get account 1")?;
1033 let account2 = accounts.get_account(2).context("failed to get account 2")?;
1034
1035 assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
1036 assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
1037 account1
1038 .set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
1039 .await?;
1040 assert_eq!(stock_str::no_messages(&account1).await, "foobar");
1041 assert_eq!(stock_str::no_messages(&account2).await, "foobar");
1042
1043 Ok(())
1044 }
1045}