1use std::collections::{BTreeMap, BTreeSet};
4use std::future::Future;
5use std::path::{Path, PathBuf};
6
7use anyhow::{Context as _, Result, bail, ensure};
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::{Duration, sleep};
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 fn get_id(&self) -> u32 {
61 0
62 }
63
64 async fn create(dir: &Path) -> Result<()> {
66 fs::create_dir_all(dir)
67 .await
68 .context("failed to create folder")?;
69
70 Config::new(dir).await?;
71
72 Ok(())
73 }
74
75 async fn open(dir: PathBuf, writable: bool) -> Result<Self> {
78 ensure!(dir.exists(), "directory does not exist");
79
80 let config_file = dir.join(CONFIG_NAME);
81 ensure!(config_file.exists(), "{:?} does not exist", config_file);
82
83 let config = Config::from_file(config_file, writable).await?;
84 let events = Events::new();
85 let stockstrings = StockStrings::new();
86 let push_subscriber = PushSubscriber::new();
87 let accounts = config
88 .load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
89 .await
90 .context("failed to load accounts")?;
91
92 Ok(Self {
93 dir,
94 config,
95 accounts,
96 events,
97 stockstrings,
98 push_subscriber,
99 })
100 }
101
102 pub fn get_account(&self, id: u32) -> Option<Context> {
104 self.accounts.get(&id).cloned()
105 }
106
107 pub fn get_selected_account(&self) -> Option<Context> {
109 let id = self.config.get_selected_account();
110 self.accounts.get(&id).cloned()
111 }
112
113 pub fn get_selected_account_id(&self) -> Option<u32> {
115 match self.config.get_selected_account() {
116 0 => None,
117 id => Some(id),
118 }
119 }
120
121 pub async fn select_account(&mut self, id: u32) -> Result<()> {
123 self.config.select_account(id).await?;
124
125 Ok(())
126 }
127
128 pub async fn add_account(&mut self) -> Result<u32> {
132 let account_config = self.config.new_account().await?;
133 let dbfile = account_config.dbfile(&self.dir);
134
135 let ctx = ContextBuilder::new(dbfile)
136 .with_id(account_config.id)
137 .with_events(self.events.clone())
138 .with_stock_strings(self.stockstrings.clone())
139 .with_push_subscriber(self.push_subscriber.clone())
140 .build()
141 .await?;
142 ctx.open("".to_string()).await?;
145
146 self.accounts.insert(account_config.id, ctx);
147 self.emit_event(EventType::AccountsChanged);
148
149 Ok(account_config.id)
150 }
151
152 pub async fn add_closed_account(&mut self) -> Result<u32> {
154 let account_config = self.config.new_account().await?;
155 let dbfile = account_config.dbfile(&self.dir);
156
157 let ctx = ContextBuilder::new(dbfile)
158 .with_id(account_config.id)
159 .with_events(self.events.clone())
160 .with_stock_strings(self.stockstrings.clone())
161 .with_push_subscriber(self.push_subscriber.clone())
162 .build()
163 .await?;
164 self.accounts.insert(account_config.id, ctx);
165 self.emit_event(EventType::AccountsChanged);
166
167 Ok(account_config.id)
168 }
169
170 pub async fn remove_account(&mut self, id: u32) -> Result<()> {
172 let ctx = self
173 .accounts
174 .remove(&id)
175 .with_context(|| format!("no account with id {id}"))?;
176 ctx.stop_io().await;
177
178 ctx.sql.close().await;
190 drop(ctx);
191
192 if let Some(cfg) = self.config.get_account(id) {
193 let account_path = self.dir.join(cfg.dir);
194
195 try_many_times(|| fs::remove_dir_all(&account_path))
196 .await
197 .context("failed to remove account data")?;
198 }
199 self.config.remove_account(id).await?;
200 self.emit_event(EventType::AccountsChanged);
201
202 Ok(())
203 }
204
205 pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
209 let blobdir = Context::derive_blobdir(&dbfile);
210 let walfile = Context::derive_walfile(&dbfile);
211
212 ensure!(dbfile.exists(), "no database found: {}", dbfile.display());
213 ensure!(blobdir.exists(), "no blobdir found: {}", blobdir.display());
214
215 let old_id = self.config.get_selected_account();
216
217 let account_config = self
219 .config
220 .new_account()
221 .await
222 .context("failed to create new account")?;
223
224 let new_dbfile = account_config.dbfile(&self.dir);
225 let new_blobdir = Context::derive_blobdir(&new_dbfile);
226 let new_walfile = Context::derive_walfile(&new_dbfile);
227
228 let res = {
229 fs::create_dir_all(self.dir.join(&account_config.dir))
230 .await
231 .context("failed to create dir")?;
232 try_many_times(|| fs::rename(&dbfile, &new_dbfile))
233 .await
234 .context("failed to rename dbfile")?;
235 try_many_times(|| fs::rename(&blobdir, &new_blobdir))
236 .await
237 .context("failed to rename blobdir")?;
238 if walfile.exists() {
239 fs::rename(&walfile, &new_walfile)
240 .await
241 .context("failed to rename walfile")?;
242 }
243 Ok(())
244 };
245
246 match res {
247 Ok(_) => {
248 let ctx = Context::new(
249 &new_dbfile,
250 account_config.id,
251 self.events.clone(),
252 self.stockstrings.clone(),
253 )
254 .await?;
255 self.accounts.insert(account_config.id, ctx);
256 Ok(account_config.id)
257 }
258 Err(err) => {
259 let account_path = std::path::PathBuf::from(&account_config.dir);
260 try_many_times(|| fs::remove_dir_all(&account_path))
261 .await
262 .context("failed to remove account data")?;
263 self.config.remove_account(account_config.id).await?;
264
265 self.select_account(old_id).await?;
267
268 Err(err)
269 }
270 }
271 }
272
273 pub fn get_all(&self) -> Vec<u32> {
275 let mut ordered_ids = Vec::new();
276 let mut all_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
277
278 for &id in &self.config.inner.accounts_order {
280 if all_ids.remove(&id) {
281 ordered_ids.push(id);
282 }
283 }
284
285 for id in all_ids {
287 ordered_ids.push(id);
288 }
289
290 ordered_ids
291 }
292
293 pub async fn set_accounts_order(&mut self, order: Vec<u32>) -> Result<()> {
299 let existing_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
300
301 let mut filtered_order: Vec<u32> = order
303 .into_iter()
304 .filter(|id| existing_ids.contains(id))
305 .collect();
306
307 for &id in &existing_ids {
309 if !filtered_order.contains(&id) {
310 filtered_order.push(id);
311 }
312 }
313
314 self.config.inner.accounts_order = filtered_order;
315 self.config.sync().await?;
316 self.emit_event(EventType::AccountsChanged);
317 Ok(())
318 }
319
320 pub async fn start_io(&mut self) {
322 for account in self.accounts.values_mut() {
323 account.start_io().await;
324 }
325 }
326
327 pub async fn stop_io(&self) {
329 info!(self, "Stopping IO for all accounts.");
332 for account in self.accounts.values() {
333 account.stop_io().await;
334 }
335 }
336
337 pub async fn maybe_network(&self) {
339 for account in self.accounts.values() {
340 account.scheduler.maybe_network().await;
341 }
342 }
343
344 pub async fn maybe_network_lost(&self) {
346 for account in self.accounts.values() {
347 account.scheduler.maybe_network_lost(account).await;
348 }
349 }
350
351 async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
356 let n_accounts = accounts.len();
357 events.emit(Event {
358 id: 0,
359 typ: EventType::Info(format!(
360 "Starting background fetch for {n_accounts} accounts."
361 )),
362 });
363 let mut set = JoinSet::new();
364 for account in accounts {
365 set.spawn(async move {
366 if let Err(error) = account.background_fetch().await {
367 warn!(account, "{error:#}");
368 }
369 });
370 }
371 set.join_all().await;
372 events.emit(Event {
373 id: 0,
374 typ: EventType::Info(format!(
375 "Finished background fetch for {n_accounts} accounts."
376 )),
377 });
378 }
379
380 async fn background_fetch_with_timeout(
382 accounts: Vec<Context>,
383 events: Events,
384 timeout: std::time::Duration,
385 ) {
386 if let Err(_err) = tokio::time::timeout(
387 timeout,
388 Self::background_fetch_no_timeout(accounts, events.clone()),
389 )
390 .await
391 {
392 events.emit(Event {
393 id: 0,
394 typ: EventType::Warning("Background fetch timed out.".to_string()),
395 });
396 }
397 events.emit(Event {
398 id: 0,
399 typ: EventType::AccountsBackgroundFetchDone,
400 });
401 }
402
403 pub fn background_fetch(
412 &self,
413 timeout: std::time::Duration,
414 ) -> impl Future<Output = ()> + use<> {
415 let accounts: Vec<Context> = self.accounts.values().cloned().collect();
416 let events = self.events.clone();
417 Self::background_fetch_with_timeout(accounts, events, timeout)
418 }
419
420 pub fn emit_event(&self, event: EventType) {
422 self.events.emit(Event { id: 0, typ: event })
423 }
424
425 pub fn get_event_emitter(&self) -> EventEmitter {
427 self.events.get_emitter()
428 }
429
430 pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
432 self.push_subscriber.set_device_token(token).await;
433 Ok(())
434 }
435}
436
437const CONFIG_NAME: &str = "accounts.toml";
439
440#[cfg(not(target_os = "ios"))]
442const LOCKFILE_NAME: &str = "accounts.lock";
443
444const DB_NAME: &str = "dc.db";
446
447#[derive(Debug)]
449struct Config {
450 file: PathBuf,
451 inner: InnerConfig,
452 lock_task: Option<JoinHandle<anyhow::Result<()>>>,
455}
456
457#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
461struct InnerConfig {
462 pub selected_account: u32,
464 pub next_id: u32,
465 pub accounts: Vec<AccountConfig>,
466 #[serde(default)]
469 pub accounts_order: Vec<u32>,
470}
471
472impl Drop for Config {
473 fn drop(&mut self) {
474 if let Some(lock_task) = self.lock_task.take() {
475 lock_task.abort();
476 }
477 }
478}
479
480impl Config {
481 #[cfg(target_os = "ios")]
482 async fn create_lock_task(_dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
483 Ok(None)
487 }
488
489 #[cfg(not(target_os = "ios"))]
490 async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
491 let lockfile = dir.join(LOCKFILE_NAME);
492 let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
493 let (locked_tx, locked_rx) = oneshot::channel();
494 let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
495 let mut timeout = Duration::from_millis(100);
496 let _guard = loop {
497 match lock.try_write() {
498 Ok(guard) => break Ok(guard),
499 Err(err) => {
500 if timeout.as_millis() > 1600 {
501 break Err(err);
502 }
503 sleep(timeout).await;
507 if err.kind() == std::io::ErrorKind::WouldBlock {
508 timeout *= 2;
509 }
510 }
511 }
512 }?;
513 locked_tx
514 .send(())
515 .ok()
516 .context("Cannot notify about lockfile locking")?;
517 let (_tx, rx) = oneshot::channel();
518 rx.await?;
519 Ok(())
520 });
521 if locked_rx.await.is_err() {
522 bail!(
523 "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)"
524 );
525 };
526 Ok(Some(lock_task))
527 }
528
529 async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
531 let dir = file.parent().context("Cannot get config file directory")?;
532 let inner = InnerConfig {
533 accounts: Vec::new(),
534 selected_account: 0,
535 next_id: 1,
536 accounts_order: Vec::new(),
537 };
538 if !lock {
539 let cfg = Self {
540 file,
541 inner,
542 lock_task: None,
543 };
544 return Ok(cfg);
545 }
546 let lock_task = Self::create_lock_task(dir.to_path_buf()).await?;
547 let cfg = Self {
548 file,
549 inner,
550 lock_task,
551 };
552 Ok(cfg)
553 }
554
555 pub async fn new(dir: &Path) -> Result<Self> {
557 let lock = true;
558 let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
559 cfg.sync().await?;
560
561 Ok(cfg)
562 }
563
564 async fn sync(&mut self) -> Result<()> {
568 #[cfg(not(target_os = "ios"))]
569 ensure!(
570 !self
571 .lock_task
572 .as_ref()
573 .context("Config is read-only")?
574 .is_finished()
575 );
576
577 let tmp_path = self.file.with_extension("toml.tmp");
578 let mut file = fs::File::create(&tmp_path)
579 .await
580 .context("failed to create a tmp config")?;
581 file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
582 .await
583 .context("failed to write a tmp config")?;
584 file.sync_data()
585 .await
586 .context("failed to sync a tmp config")?;
587 drop(file);
588 fs::rename(&tmp_path, &self.file)
589 .await
590 .context("failed to rename config")?;
591 Ok(())
592 }
593
594 pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
596 let mut config = Self::new_nosync(file, writable).await?;
597 let bytes = fs::read(&config.file)
598 .await
599 .context("Failed to read file")?;
600 let s = std::str::from_utf8(&bytes)?;
601 config.inner = toml::from_str(s).context("Failed to parse config")?;
602
603 let mut modified = false;
606 for account in &mut config.inner.accounts {
607 if account.dir.is_absolute() {
608 if let Some(old_path_parent) = account.dir.parent() {
609 if let Ok(new_path) = account.dir.strip_prefix(old_path_parent) {
610 account.dir = new_path.to_path_buf();
611 modified = true;
612 }
613 }
614 }
615 }
616 if modified && writable {
617 config.sync().await?;
618 }
619
620 Ok(config)
621 }
622
623 pub async fn load_accounts(
628 &self,
629 events: &Events,
630 stockstrings: &StockStrings,
631 push_subscriber: PushSubscriber,
632 dir: &Path,
633 ) -> Result<BTreeMap<u32, Context>> {
634 let mut accounts = BTreeMap::new();
635
636 for account_config in &self.inner.accounts {
637 let dbfile = account_config.dbfile(dir);
638 let ctx = ContextBuilder::new(dbfile.clone())
639 .with_id(account_config.id)
640 .with_events(events.clone())
641 .with_stock_strings(stockstrings.clone())
642 .with_push_subscriber(push_subscriber.clone())
643 .build()
644 .await
645 .with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
646 ctx.open("".to_string()).await?;
649
650 accounts.insert(account_config.id, ctx);
651 }
652
653 Ok(accounts)
654 }
655
656 async fn new_account(&mut self) -> Result<AccountConfig> {
658 let id = {
659 let id = self.inner.next_id;
660 let uuid = Uuid::new_v4();
661 let target_dir = PathBuf::from(uuid.to_string());
662
663 self.inner.accounts.push(AccountConfig {
664 id,
665 dir: target_dir,
666 uuid,
667 });
668 self.inner.next_id += 1;
669
670 self.inner.accounts_order.push(id);
672
673 id
674 };
675
676 self.sync().await?;
677
678 self.select_account(id)
679 .await
680 .context("failed to select just added account")?;
681 let cfg = self
682 .get_account(id)
683 .context("failed to get just added account")?;
684 Ok(cfg)
685 }
686
687 pub async fn remove_account(&mut self, id: u32) -> Result<()> {
689 {
690 if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
691 self.inner.accounts.remove(idx);
693 }
694
695 self.inner.accounts_order.retain(|&x| x != id);
697
698 if self.inner.selected_account == id {
699 self.inner.selected_account = self
701 .inner
702 .accounts
703 .first()
704 .map(|e| e.id)
705 .unwrap_or_default();
706 }
707 }
708
709 self.sync().await
710 }
711
712 fn get_account(&self, id: u32) -> Option<AccountConfig> {
714 self.inner.accounts.iter().find(|e| e.id == id).cloned()
715 }
716
717 pub fn get_selected_account(&self) -> u32 {
719 self.inner.selected_account
720 }
721
722 pub async fn select_account(&mut self, id: u32) -> Result<()> {
724 {
725 ensure!(
726 self.inner.accounts.iter().any(|e| e.id == id),
727 "invalid account id: {}",
728 id
729 );
730
731 self.inner.selected_account = id;
732 }
733
734 self.sync().await?;
735 Ok(())
736 }
737}
738
739async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
748where
749 F: Fn() -> Fut,
750 Fut: Future<Output = std::result::Result<(), T>>,
751{
752 let mut counter = 0;
753 loop {
754 counter += 1;
755
756 if let Err(err) = f().await {
757 if counter > 60 {
758 return Err(err);
759 }
760
761 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
763 } else {
764 break;
765 }
766 }
767 Ok(())
768}
769
770#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
772struct AccountConfig {
773 pub id: u32,
775
776 pub dir: std::path::PathBuf,
780
781 pub uuid: Uuid,
783}
784
785impl AccountConfig {
786 pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf {
788 accounts_dir.join(&self.dir).join(DB_NAME)
789 }
790}
791
792#[cfg(test)]
793mod tests {
794 use super::*;
795 use crate::stock_str::{self, StockMessage};
796
797 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
798 async fn test_account_new_open() {
799 let dir = tempfile::tempdir().unwrap();
800 let p: PathBuf = dir.path().join("accounts1");
801
802 {
803 let writable = true;
804 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
805 accounts.add_account().await.unwrap();
806
807 assert_eq!(accounts.accounts.len(), 1);
808 assert_eq!(accounts.config.get_selected_account(), 1);
809 }
810 for writable in [true, false] {
811 let accounts = Accounts::new(p.clone(), writable).await.unwrap();
812
813 assert_eq!(accounts.accounts.len(), 1);
814 assert_eq!(accounts.config.get_selected_account(), 1);
815 }
816 }
817
818 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
819 async fn test_account_new_open_conflict() {
820 let dir = tempfile::tempdir().unwrap();
821 let p: PathBuf = dir.path().join("accounts");
822 let writable = true;
823 let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
824
825 let writable = true;
826 assert!(Accounts::new(p.clone(), writable).await.is_err());
827
828 let writable = false;
829 let accounts = Accounts::new(p, writable).await.unwrap();
830 assert_eq!(accounts.accounts.len(), 0);
831 assert_eq!(accounts.config.get_selected_account(), 0);
832 }
833
834 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
835 async fn test_account_new_add_remove() {
836 let dir = tempfile::tempdir().unwrap();
837 let p: PathBuf = dir.path().join("accounts");
838
839 let writable = true;
840 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
841 assert_eq!(accounts.accounts.len(), 0);
842 assert_eq!(accounts.config.get_selected_account(), 0);
843
844 let id = accounts.add_account().await.unwrap();
845 assert_eq!(id, 1);
846 assert_eq!(accounts.accounts.len(), 1);
847 assert_eq!(accounts.config.get_selected_account(), 1);
848
849 let id = accounts.add_account().await.unwrap();
850 assert_eq!(id, 2);
851 assert_eq!(accounts.config.get_selected_account(), id);
852 assert_eq!(accounts.accounts.len(), 2);
853
854 accounts.select_account(1).await.unwrap();
855 assert_eq!(accounts.config.get_selected_account(), 1);
856
857 accounts.remove_account(1).await.unwrap();
858 assert_eq!(accounts.config.get_selected_account(), 2);
859 assert_eq!(accounts.accounts.len(), 1);
860 }
861
862 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
863 async fn test_accounts_remove_last() -> Result<()> {
864 let dir = tempfile::tempdir()?;
865 let p: PathBuf = dir.path().join("accounts");
866
867 let writable = true;
868 let mut accounts = Accounts::new(p.clone(), writable).await?;
869 assert!(accounts.get_selected_account().is_none());
870 assert_eq!(accounts.config.get_selected_account(), 0);
871
872 let id = accounts.add_account().await?;
873 assert!(accounts.get_selected_account().is_some());
874 assert_eq!(id, 1);
875 assert_eq!(accounts.accounts.len(), 1);
876 assert_eq!(accounts.config.get_selected_account(), id);
877
878 accounts.remove_account(id).await?;
879 assert!(accounts.get_selected_account().is_none());
880
881 Ok(())
882 }
883
884 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
885 async fn test_migrate_account() {
886 let dir = tempfile::tempdir().unwrap();
887 let p: PathBuf = dir.path().join("accounts");
888
889 let writable = true;
890 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
891 assert_eq!(accounts.accounts.len(), 0);
892 assert_eq!(accounts.config.get_selected_account(), 0);
893
894 let extern_dbfile: PathBuf = dir.path().join("other");
895 let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
896 .await
897 .unwrap();
898 ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
899 .await
900 .unwrap();
901
902 drop(ctx);
903
904 accounts
905 .migrate_account(extern_dbfile.clone())
906 .await
907 .unwrap();
908 assert_eq!(accounts.accounts.len(), 1);
909 assert_eq!(accounts.config.get_selected_account(), 1);
910
911 let ctx = accounts.get_selected_account().unwrap();
912 assert_eq!(
913 "me@mail.com",
914 ctx.get_config(crate::config::Config::Addr)
915 .await
916 .unwrap()
917 .unwrap()
918 );
919 }
920
921 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
923 async fn test_accounts_sorted() {
924 let dir = tempfile::tempdir().unwrap();
925 let p: PathBuf = dir.path().join("accounts");
926
927 let writable = true;
928 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
929
930 for expected_id in 1..10 {
931 let id = accounts.add_account().await.unwrap();
932 assert_eq!(id, expected_id);
933 }
934
935 let ids = accounts.get_all();
936 for (i, expected_id) in (1..10).enumerate() {
937 assert_eq!(ids.get(i), Some(&expected_id));
938 }
939 }
940
941 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
942 async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
943 let dir = tempfile::tempdir()?;
944 let p: PathBuf = dir.path().join("accounts");
945 let dummy_accounts = 10;
946
947 let (id0, id1, id2) = {
948 let writable = true;
949 let mut accounts = Accounts::new(p.clone(), writable).await?;
950 accounts.add_account().await?;
951 let ids = accounts.get_all();
952 assert_eq!(ids.len(), 1);
953
954 let id0 = *ids.first().unwrap();
955 let ctx = accounts.get_account(id0).unwrap();
956 ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
957 .await?;
958
959 let id1 = accounts.add_account().await?;
960 let ctx = accounts.get_account(id1).unwrap();
961 ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
962 .await?;
963
964 for _ in 0..dummy_accounts {
966 let to_delete = accounts.add_account().await?;
967 accounts.remove_account(to_delete).await?;
968 }
969
970 let id2 = accounts.add_account().await?;
971 let ctx = accounts.get_account(id2).unwrap();
972 ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
973 .await?;
974
975 accounts.select_account(id1).await?;
976
977 (id0, id1, id2)
978 };
979 assert!(id0 > 0);
980 assert!(id1 > id0);
981 assert!(id2 > id1 + dummy_accounts);
982
983 let (id0_reopened, id1_reopened, id2_reopened) = {
984 let writable = false;
985 let accounts = Accounts::new(p.clone(), writable).await?;
986 let ctx = accounts.get_selected_account().unwrap();
987 assert_eq!(
988 ctx.get_config(crate::config::Config::Addr).await?,
989 Some("two@example.org".to_string())
990 );
991
992 let ids = accounts.get_all();
993 assert_eq!(ids.len(), 3);
994
995 let id0 = *ids.first().unwrap();
996 let ctx = accounts.get_account(id0).unwrap();
997 assert_eq!(
998 ctx.get_config(crate::config::Config::Addr).await?,
999 Some("one@example.org".to_string())
1000 );
1001
1002 let id1 = *ids.get(1).unwrap();
1003 let t = accounts.get_account(id1).unwrap();
1004 assert_eq!(
1005 t.get_config(crate::config::Config::Addr).await?,
1006 Some("two@example.org".to_string())
1007 );
1008
1009 let id2 = *ids.get(2).unwrap();
1010 let ctx = accounts.get_account(id2).unwrap();
1011 assert_eq!(
1012 ctx.get_config(crate::config::Config::Addr).await?,
1013 Some("three@example.org".to_string())
1014 );
1015
1016 (id0, id1, id2)
1017 };
1018 assert_eq!(id0, id0_reopened);
1019 assert_eq!(id1, id1_reopened);
1020 assert_eq!(id2, id2_reopened);
1021
1022 Ok(())
1023 }
1024
1025 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1026 async fn test_no_accounts_event_emitter() -> Result<()> {
1027 let dir = tempfile::tempdir().unwrap();
1028 let p: PathBuf = dir.path().join("accounts");
1029
1030 let writable = true;
1031 let accounts = Accounts::new(p.clone(), writable).await?;
1032
1033 assert_eq!(accounts.accounts.len(), 0);
1035
1036 let event_emitter = accounts.get_event_emitter();
1038
1039 let duration = std::time::Duration::from_millis(1);
1041 assert!(
1042 tokio::time::timeout(duration, event_emitter.recv())
1043 .await
1044 .is_err()
1045 );
1046
1047 drop(accounts);
1049 assert_eq!(event_emitter.recv().await, None);
1050
1051 Ok(())
1052 }
1053
1054 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1055 async fn test_encrypted_account() -> Result<()> {
1056 let dir = tempfile::tempdir().context("failed to create tempdir")?;
1057 let p: PathBuf = dir.path().join("accounts");
1058
1059 let writable = true;
1060 let mut accounts = Accounts::new(p.clone(), writable)
1061 .await
1062 .context("failed to create accounts manager")?;
1063
1064 assert_eq!(accounts.accounts.len(), 0);
1065 let account_id = accounts
1066 .add_closed_account()
1067 .await
1068 .context("failed to add closed account")?;
1069 let account = accounts
1070 .get_selected_account()
1071 .context("failed to get account")?;
1072 assert_eq!(account.id, account_id);
1073 let passphrase_set_success = account
1074 .open("foobar".to_string())
1075 .await
1076 .context("failed to set passphrase")?;
1077 assert!(passphrase_set_success);
1078 drop(accounts);
1079
1080 let writable = false;
1081 let accounts = Accounts::new(p.clone(), writable)
1082 .await
1083 .context("failed to create second accounts manager")?;
1084 let account = accounts
1085 .get_selected_account()
1086 .context("failed to get account")?;
1087 assert_eq!(account.is_open().await, false);
1088
1089 assert_eq!(account.open("barfoo".to_string()).await?, false);
1091 assert_eq!(account.open("".to_string()).await?, false);
1092
1093 assert_eq!(account.open("foobar".to_string()).await?, true);
1094 assert_eq!(account.is_open().await, true);
1095
1096 Ok(())
1097 }
1098
1099 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1101 async fn test_accounts_share_translations() -> Result<()> {
1102 let dir = tempfile::tempdir().unwrap();
1103 let p: PathBuf = dir.path().join("accounts");
1104
1105 let writable = true;
1106 let mut accounts = Accounts::new(p.clone(), writable).await?;
1107 accounts.add_account().await?;
1108 accounts.add_account().await?;
1109
1110 let account1 = accounts.get_account(1).context("failed to get account 1")?;
1111 let account2 = accounts.get_account(2).context("failed to get account 2")?;
1112
1113 assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
1114 assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
1115 account1
1116 .set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
1117 .await?;
1118 assert_eq!(stock_str::no_messages(&account1).await, "foobar");
1119 assert_eq!(stock_str::no_messages(&account2).await, "foobar");
1120
1121 Ok(())
1122 }
1123}