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::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(), "{config_file:?} does not exist");
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: {id}"
728 );
729
730 self.inner.selected_account = id;
731 }
732
733 self.sync().await?;
734 Ok(())
735 }
736}
737
738async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
747where
748 F: Fn() -> Fut,
749 Fut: Future<Output = std::result::Result<(), T>>,
750{
751 let mut counter = 0;
752 loop {
753 counter += 1;
754
755 if let Err(err) = f().await {
756 if counter > 60 {
757 return Err(err);
758 }
759
760 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
762 } else {
763 break;
764 }
765 }
766 Ok(())
767}
768
769#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
771struct AccountConfig {
772 pub id: u32,
774
775 pub dir: std::path::PathBuf,
779
780 pub uuid: Uuid,
782}
783
784impl AccountConfig {
785 pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf {
787 accounts_dir.join(&self.dir).join(DB_NAME)
788 }
789}
790
791#[cfg(test)]
792mod tests {
793 use super::*;
794 use crate::stock_str::{self, StockMessage};
795
796 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
797 async fn test_account_new_open() {
798 let dir = tempfile::tempdir().unwrap();
799 let p: PathBuf = dir.path().join("accounts1");
800
801 {
802 let writable = true;
803 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
804 accounts.add_account().await.unwrap();
805
806 assert_eq!(accounts.accounts.len(), 1);
807 assert_eq!(accounts.config.get_selected_account(), 1);
808 }
809 for writable in [true, false] {
810 let accounts = Accounts::new(p.clone(), writable).await.unwrap();
811
812 assert_eq!(accounts.accounts.len(), 1);
813 assert_eq!(accounts.config.get_selected_account(), 1);
814 }
815 }
816
817 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
818 async fn test_account_new_open_conflict() {
819 let dir = tempfile::tempdir().unwrap();
820 let p: PathBuf = dir.path().join("accounts");
821 let writable = true;
822 let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
823
824 let writable = true;
825 assert!(Accounts::new(p.clone(), writable).await.is_err());
826
827 let writable = false;
828 let accounts = Accounts::new(p, writable).await.unwrap();
829 assert_eq!(accounts.accounts.len(), 0);
830 assert_eq!(accounts.config.get_selected_account(), 0);
831 }
832
833 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
834 async fn test_account_new_add_remove() {
835 let dir = tempfile::tempdir().unwrap();
836 let p: PathBuf = dir.path().join("accounts");
837
838 let writable = true;
839 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
840 assert_eq!(accounts.accounts.len(), 0);
841 assert_eq!(accounts.config.get_selected_account(), 0);
842
843 let id = accounts.add_account().await.unwrap();
844 assert_eq!(id, 1);
845 assert_eq!(accounts.accounts.len(), 1);
846 assert_eq!(accounts.config.get_selected_account(), 1);
847
848 let id = accounts.add_account().await.unwrap();
849 assert_eq!(id, 2);
850 assert_eq!(accounts.config.get_selected_account(), id);
851 assert_eq!(accounts.accounts.len(), 2);
852
853 accounts.select_account(1).await.unwrap();
854 assert_eq!(accounts.config.get_selected_account(), 1);
855
856 accounts.remove_account(1).await.unwrap();
857 assert_eq!(accounts.config.get_selected_account(), 2);
858 assert_eq!(accounts.accounts.len(), 1);
859 }
860
861 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
862 async fn test_accounts_remove_last() -> Result<()> {
863 let dir = tempfile::tempdir()?;
864 let p: PathBuf = dir.path().join("accounts");
865
866 let writable = true;
867 let mut accounts = Accounts::new(p.clone(), writable).await?;
868 assert!(accounts.get_selected_account().is_none());
869 assert_eq!(accounts.config.get_selected_account(), 0);
870
871 let id = accounts.add_account().await?;
872 assert!(accounts.get_selected_account().is_some());
873 assert_eq!(id, 1);
874 assert_eq!(accounts.accounts.len(), 1);
875 assert_eq!(accounts.config.get_selected_account(), id);
876
877 accounts.remove_account(id).await?;
878 assert!(accounts.get_selected_account().is_none());
879
880 Ok(())
881 }
882
883 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
884 async fn test_migrate_account() {
885 let dir = tempfile::tempdir().unwrap();
886 let p: PathBuf = dir.path().join("accounts");
887
888 let writable = true;
889 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
890 assert_eq!(accounts.accounts.len(), 0);
891 assert_eq!(accounts.config.get_selected_account(), 0);
892
893 let extern_dbfile: PathBuf = dir.path().join("other");
894 let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
895 .await
896 .unwrap();
897 ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
898 .await
899 .unwrap();
900
901 drop(ctx);
902
903 accounts
904 .migrate_account(extern_dbfile.clone())
905 .await
906 .unwrap();
907 assert_eq!(accounts.accounts.len(), 1);
908 assert_eq!(accounts.config.get_selected_account(), 1);
909
910 let ctx = accounts.get_selected_account().unwrap();
911 assert_eq!(
912 "me@mail.com",
913 ctx.get_config(crate::config::Config::Addr)
914 .await
915 .unwrap()
916 .unwrap()
917 );
918 }
919
920 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
922 async fn test_accounts_sorted() {
923 let dir = tempfile::tempdir().unwrap();
924 let p: PathBuf = dir.path().join("accounts");
925
926 let writable = true;
927 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
928
929 for expected_id in 1..10 {
930 let id = accounts.add_account().await.unwrap();
931 assert_eq!(id, expected_id);
932 }
933
934 let ids = accounts.get_all();
935 for (i, expected_id) in (1..10).enumerate() {
936 assert_eq!(ids.get(i), Some(&expected_id));
937 }
938 }
939
940 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
941 async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
942 let dir = tempfile::tempdir()?;
943 let p: PathBuf = dir.path().join("accounts");
944 let dummy_accounts = 10;
945
946 let (id0, id1, id2) = {
947 let writable = true;
948 let mut accounts = Accounts::new(p.clone(), writable).await?;
949 accounts.add_account().await?;
950 let ids = accounts.get_all();
951 assert_eq!(ids.len(), 1);
952
953 let id0 = *ids.first().unwrap();
954 let ctx = accounts.get_account(id0).unwrap();
955 ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
956 .await?;
957
958 let id1 = accounts.add_account().await?;
959 let ctx = accounts.get_account(id1).unwrap();
960 ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
961 .await?;
962
963 for _ in 0..dummy_accounts {
965 let to_delete = accounts.add_account().await?;
966 accounts.remove_account(to_delete).await?;
967 }
968
969 let id2 = accounts.add_account().await?;
970 let ctx = accounts.get_account(id2).unwrap();
971 ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
972 .await?;
973
974 accounts.select_account(id1).await?;
975
976 (id0, id1, id2)
977 };
978 assert!(id0 > 0);
979 assert!(id1 > id0);
980 assert!(id2 > id1 + dummy_accounts);
981
982 let (id0_reopened, id1_reopened, id2_reopened) = {
983 let writable = false;
984 let accounts = Accounts::new(p.clone(), writable).await?;
985 let ctx = accounts.get_selected_account().unwrap();
986 assert_eq!(
987 ctx.get_config(crate::config::Config::Addr).await?,
988 Some("two@example.org".to_string())
989 );
990
991 let ids = accounts.get_all();
992 assert_eq!(ids.len(), 3);
993
994 let id0 = *ids.first().unwrap();
995 let ctx = accounts.get_account(id0).unwrap();
996 assert_eq!(
997 ctx.get_config(crate::config::Config::Addr).await?,
998 Some("one@example.org".to_string())
999 );
1000
1001 let id1 = *ids.get(1).unwrap();
1002 let t = accounts.get_account(id1).unwrap();
1003 assert_eq!(
1004 t.get_config(crate::config::Config::Addr).await?,
1005 Some("two@example.org".to_string())
1006 );
1007
1008 let id2 = *ids.get(2).unwrap();
1009 let ctx = accounts.get_account(id2).unwrap();
1010 assert_eq!(
1011 ctx.get_config(crate::config::Config::Addr).await?,
1012 Some("three@example.org".to_string())
1013 );
1014
1015 (id0, id1, id2)
1016 };
1017 assert_eq!(id0, id0_reopened);
1018 assert_eq!(id1, id1_reopened);
1019 assert_eq!(id2, id2_reopened);
1020
1021 Ok(())
1022 }
1023
1024 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1025 async fn test_no_accounts_event_emitter() -> Result<()> {
1026 let dir = tempfile::tempdir().unwrap();
1027 let p: PathBuf = dir.path().join("accounts");
1028
1029 let writable = true;
1030 let accounts = Accounts::new(p.clone(), writable).await?;
1031
1032 assert_eq!(accounts.accounts.len(), 0);
1034
1035 let event_emitter = accounts.get_event_emitter();
1037
1038 let duration = std::time::Duration::from_millis(1);
1040 assert!(
1041 tokio::time::timeout(duration, event_emitter.recv())
1042 .await
1043 .is_err()
1044 );
1045
1046 drop(accounts);
1048 assert_eq!(event_emitter.recv().await, None);
1049
1050 Ok(())
1051 }
1052
1053 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1054 async fn test_encrypted_account() -> Result<()> {
1055 let dir = tempfile::tempdir().context("failed to create tempdir")?;
1056 let p: PathBuf = dir.path().join("accounts");
1057
1058 let writable = true;
1059 let mut accounts = Accounts::new(p.clone(), writable)
1060 .await
1061 .context("failed to create accounts manager")?;
1062
1063 assert_eq!(accounts.accounts.len(), 0);
1064 let account_id = accounts
1065 .add_closed_account()
1066 .await
1067 .context("failed to add closed account")?;
1068 let account = accounts
1069 .get_selected_account()
1070 .context("failed to get account")?;
1071 assert_eq!(account.id, account_id);
1072 let passphrase_set_success = account
1073 .open("foobar".to_string())
1074 .await
1075 .context("failed to set passphrase")?;
1076 assert!(passphrase_set_success);
1077 drop(accounts);
1078
1079 let writable = false;
1080 let accounts = Accounts::new(p.clone(), writable)
1081 .await
1082 .context("failed to create second accounts manager")?;
1083 let account = accounts
1084 .get_selected_account()
1085 .context("failed to get account")?;
1086 assert_eq!(account.is_open().await, false);
1087
1088 assert_eq!(account.open("barfoo".to_string()).await?, false);
1090 assert_eq!(account.open("".to_string()).await?, false);
1091
1092 assert_eq!(account.open("foobar".to_string()).await?, true);
1093 assert_eq!(account.is_open().await, true);
1094
1095 Ok(())
1096 }
1097
1098 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1100 async fn test_accounts_share_translations() -> Result<()> {
1101 let dir = tempfile::tempdir().unwrap();
1102 let p: PathBuf = dir.path().join("accounts");
1103
1104 let writable = true;
1105 let mut accounts = Accounts::new(p.clone(), writable).await?;
1106 accounts.add_account().await?;
1107 accounts.add_account().await?;
1108
1109 let account1 = accounts.get_account(1).context("failed to get account 1")?;
1110 let account2 = accounts.get_account(2).context("failed to get account 2")?;
1111
1112 assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
1113 assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
1114 account1
1115 .set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
1116 .await?;
1117 assert_eq!(stock_str::no_messages(&account1).await, "foobar");
1118 assert_eq!(stock_str::no_messages(&account2).await, "foobar");
1119
1120 Ok(())
1121 }
1122}