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