1use std::collections::BTreeMap;
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 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(
356 &self,
357 timeout: std::time::Duration,
358 ) -> impl Future<Output = ()> + use<> {
359 let accounts: Vec<Context> = self.accounts.values().cloned().collect();
360 let events = self.events.clone();
361 Self::background_fetch_with_timeout(accounts, events, timeout)
362 }
363
364 pub fn emit_event(&self, event: EventType) {
366 self.events.emit(Event { id: 0, typ: event })
367 }
368
369 pub fn get_event_emitter(&self) -> EventEmitter {
371 self.events.get_emitter()
372 }
373
374 pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
376 self.push_subscriber.set_device_token(token).await;
377 Ok(())
378 }
379}
380
381const CONFIG_NAME: &str = "accounts.toml";
383
384#[cfg(not(target_os = "ios"))]
386const LOCKFILE_NAME: &str = "accounts.lock";
387
388const DB_NAME: &str = "dc.db";
390
391#[derive(Debug)]
393struct Config {
394 file: PathBuf,
395 inner: InnerConfig,
396 lock_task: Option<JoinHandle<anyhow::Result<()>>>,
399}
400
401#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
405struct InnerConfig {
406 pub selected_account: u32,
408 pub next_id: u32,
409 pub accounts: Vec<AccountConfig>,
410}
411
412impl Drop for Config {
413 fn drop(&mut self) {
414 if let Some(lock_task) = self.lock_task.take() {
415 lock_task.abort();
416 }
417 }
418}
419
420impl Config {
421 #[cfg(target_os = "ios")]
422 async fn create_lock_task(_dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
423 Ok(None)
427 }
428
429 #[cfg(not(target_os = "ios"))]
430 async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
431 let lockfile = dir.join(LOCKFILE_NAME);
432 let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
433 let (locked_tx, locked_rx) = oneshot::channel();
434 let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
435 let mut timeout = Duration::from_millis(100);
436 let _guard = loop {
437 match lock.try_write() {
438 Ok(guard) => break Ok(guard),
439 Err(err) => {
440 if timeout.as_millis() > 1600 {
441 break Err(err);
442 }
443 sleep(timeout).await;
447 if err.kind() == std::io::ErrorKind::WouldBlock {
448 timeout *= 2;
449 }
450 }
451 }
452 }?;
453 locked_tx
454 .send(())
455 .ok()
456 .context("Cannot notify about lockfile locking")?;
457 let (_tx, rx) = oneshot::channel();
458 rx.await?;
459 Ok(())
460 });
461 if locked_rx.await.is_err() {
462 bail!(
463 "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)"
464 );
465 };
466 Ok(Some(lock_task))
467 }
468
469 async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
471 let dir = file.parent().context("Cannot get config file directory")?;
472 let inner = InnerConfig {
473 accounts: Vec::new(),
474 selected_account: 0,
475 next_id: 1,
476 };
477 if !lock {
478 let cfg = Self {
479 file,
480 inner,
481 lock_task: None,
482 };
483 return Ok(cfg);
484 }
485 let lock_task = Self::create_lock_task(dir.to_path_buf()).await?;
486 let cfg = Self {
487 file,
488 inner,
489 lock_task,
490 };
491 Ok(cfg)
492 }
493
494 pub async fn new(dir: &Path) -> Result<Self> {
496 let lock = true;
497 let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
498 cfg.sync().await?;
499
500 Ok(cfg)
501 }
502
503 async fn sync(&mut self) -> Result<()> {
507 #[cfg(not(target_os = "ios"))]
508 ensure!(
509 !self
510 .lock_task
511 .as_ref()
512 .context("Config is read-only")?
513 .is_finished()
514 );
515
516 let tmp_path = self.file.with_extension("toml.tmp");
517 let mut file = fs::File::create(&tmp_path)
518 .await
519 .context("failed to create a tmp config")?;
520 file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
521 .await
522 .context("failed to write a tmp config")?;
523 file.sync_data()
524 .await
525 .context("failed to sync a tmp config")?;
526 drop(file);
527 fs::rename(&tmp_path, &self.file)
528 .await
529 .context("failed to rename config")?;
530 Ok(())
531 }
532
533 pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
535 let mut config = Self::new_nosync(file, writable).await?;
536 let bytes = fs::read(&config.file)
537 .await
538 .context("Failed to read file")?;
539 let s = std::str::from_utf8(&bytes)?;
540 config.inner = toml::from_str(s).context("Failed to parse config")?;
541
542 let mut modified = false;
545 for account in &mut config.inner.accounts {
546 if account.dir.is_absolute() {
547 if let Some(old_path_parent) = account.dir.parent() {
548 if let Ok(new_path) = account.dir.strip_prefix(old_path_parent) {
549 account.dir = new_path.to_path_buf();
550 modified = true;
551 }
552 }
553 }
554 }
555 if modified && writable {
556 config.sync().await?;
557 }
558
559 Ok(config)
560 }
561
562 pub async fn load_accounts(
567 &self,
568 events: &Events,
569 stockstrings: &StockStrings,
570 push_subscriber: PushSubscriber,
571 dir: &Path,
572 ) -> Result<BTreeMap<u32, Context>> {
573 let mut accounts = BTreeMap::new();
574
575 for account_config in &self.inner.accounts {
576 let dbfile = account_config.dbfile(dir);
577 let ctx = ContextBuilder::new(dbfile.clone())
578 .with_id(account_config.id)
579 .with_events(events.clone())
580 .with_stock_strings(stockstrings.clone())
581 .with_push_subscriber(push_subscriber.clone())
582 .build()
583 .await
584 .with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
585 ctx.open("".to_string()).await?;
588
589 accounts.insert(account_config.id, ctx);
590 }
591
592 Ok(accounts)
593 }
594
595 async fn new_account(&mut self) -> Result<AccountConfig> {
597 let id = {
598 let id = self.inner.next_id;
599 let uuid = Uuid::new_v4();
600 let target_dir = PathBuf::from(uuid.to_string());
601
602 self.inner.accounts.push(AccountConfig {
603 id,
604 dir: target_dir,
605 uuid,
606 });
607 self.inner.next_id += 1;
608 id
609 };
610
611 self.sync().await?;
612
613 self.select_account(id)
614 .await
615 .context("failed to select just added account")?;
616 let cfg = self
617 .get_account(id)
618 .context("failed to get just added account")?;
619 Ok(cfg)
620 }
621
622 pub async fn remove_account(&mut self, id: u32) -> Result<()> {
624 {
625 if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
626 self.inner.accounts.remove(idx);
628 }
629 if self.inner.selected_account == id {
630 self.inner.selected_account = self
632 .inner
633 .accounts
634 .first()
635 .map(|e| e.id)
636 .unwrap_or_default();
637 }
638 }
639
640 self.sync().await
641 }
642
643 fn get_account(&self, id: u32) -> Option<AccountConfig> {
645 self.inner.accounts.iter().find(|e| e.id == id).cloned()
646 }
647
648 pub fn get_selected_account(&self) -> u32 {
650 self.inner.selected_account
651 }
652
653 pub async fn select_account(&mut self, id: u32) -> Result<()> {
655 {
656 ensure!(
657 self.inner.accounts.iter().any(|e| e.id == id),
658 "invalid account id: {}",
659 id
660 );
661
662 self.inner.selected_account = id;
663 }
664
665 self.sync().await?;
666 Ok(())
667 }
668}
669
670async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
679where
680 F: Fn() -> Fut,
681 Fut: Future<Output = std::result::Result<(), T>>,
682{
683 let mut counter = 0;
684 loop {
685 counter += 1;
686
687 if let Err(err) = f().await {
688 if counter > 60 {
689 return Err(err);
690 }
691
692 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
694 } else {
695 break;
696 }
697 }
698 Ok(())
699}
700
701#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
703struct AccountConfig {
704 pub id: u32,
706
707 pub dir: std::path::PathBuf,
711
712 pub uuid: Uuid,
714}
715
716impl AccountConfig {
717 pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf {
719 accounts_dir.join(&self.dir).join(DB_NAME)
720 }
721}
722
723#[cfg(test)]
724mod tests {
725 use super::*;
726 use crate::stock_str::{self, StockMessage};
727
728 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
729 async fn test_account_new_open() {
730 let dir = tempfile::tempdir().unwrap();
731 let p: PathBuf = dir.path().join("accounts1");
732
733 {
734 let writable = true;
735 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
736 accounts.add_account().await.unwrap();
737
738 assert_eq!(accounts.accounts.len(), 1);
739 assert_eq!(accounts.config.get_selected_account(), 1);
740 }
741 for writable in [true, false] {
742 let accounts = Accounts::new(p.clone(), writable).await.unwrap();
743
744 assert_eq!(accounts.accounts.len(), 1);
745 assert_eq!(accounts.config.get_selected_account(), 1);
746 }
747 }
748
749 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
750 async fn test_account_new_open_conflict() {
751 let dir = tempfile::tempdir().unwrap();
752 let p: PathBuf = dir.path().join("accounts");
753 let writable = true;
754 let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
755
756 let writable = true;
757 assert!(Accounts::new(p.clone(), writable).await.is_err());
758
759 let writable = false;
760 let accounts = Accounts::new(p, writable).await.unwrap();
761 assert_eq!(accounts.accounts.len(), 0);
762 assert_eq!(accounts.config.get_selected_account(), 0);
763 }
764
765 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
766 async fn test_account_new_add_remove() {
767 let dir = tempfile::tempdir().unwrap();
768 let p: PathBuf = dir.path().join("accounts");
769
770 let writable = true;
771 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
772 assert_eq!(accounts.accounts.len(), 0);
773 assert_eq!(accounts.config.get_selected_account(), 0);
774
775 let id = accounts.add_account().await.unwrap();
776 assert_eq!(id, 1);
777 assert_eq!(accounts.accounts.len(), 1);
778 assert_eq!(accounts.config.get_selected_account(), 1);
779
780 let id = accounts.add_account().await.unwrap();
781 assert_eq!(id, 2);
782 assert_eq!(accounts.config.get_selected_account(), id);
783 assert_eq!(accounts.accounts.len(), 2);
784
785 accounts.select_account(1).await.unwrap();
786 assert_eq!(accounts.config.get_selected_account(), 1);
787
788 accounts.remove_account(1).await.unwrap();
789 assert_eq!(accounts.config.get_selected_account(), 2);
790 assert_eq!(accounts.accounts.len(), 1);
791 }
792
793 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
794 async fn test_accounts_remove_last() -> Result<()> {
795 let dir = tempfile::tempdir()?;
796 let p: PathBuf = dir.path().join("accounts");
797
798 let writable = true;
799 let mut accounts = Accounts::new(p.clone(), writable).await?;
800 assert!(accounts.get_selected_account().is_none());
801 assert_eq!(accounts.config.get_selected_account(), 0);
802
803 let id = accounts.add_account().await?;
804 assert!(accounts.get_selected_account().is_some());
805 assert_eq!(id, 1);
806 assert_eq!(accounts.accounts.len(), 1);
807 assert_eq!(accounts.config.get_selected_account(), id);
808
809 accounts.remove_account(id).await?;
810 assert!(accounts.get_selected_account().is_none());
811
812 Ok(())
813 }
814
815 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
816 async fn test_migrate_account() {
817 let dir = tempfile::tempdir().unwrap();
818 let p: PathBuf = dir.path().join("accounts");
819
820 let writable = true;
821 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
822 assert_eq!(accounts.accounts.len(), 0);
823 assert_eq!(accounts.config.get_selected_account(), 0);
824
825 let extern_dbfile: PathBuf = dir.path().join("other");
826 let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
827 .await
828 .unwrap();
829 ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
830 .await
831 .unwrap();
832
833 drop(ctx);
834
835 accounts
836 .migrate_account(extern_dbfile.clone())
837 .await
838 .unwrap();
839 assert_eq!(accounts.accounts.len(), 1);
840 assert_eq!(accounts.config.get_selected_account(), 1);
841
842 let ctx = accounts.get_selected_account().unwrap();
843 assert_eq!(
844 "me@mail.com",
845 ctx.get_config(crate::config::Config::Addr)
846 .await
847 .unwrap()
848 .unwrap()
849 );
850 }
851
852 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
854 async fn test_accounts_sorted() {
855 let dir = tempfile::tempdir().unwrap();
856 let p: PathBuf = dir.path().join("accounts");
857
858 let writable = true;
859 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
860
861 for expected_id in 1..10 {
862 let id = accounts.add_account().await.unwrap();
863 assert_eq!(id, expected_id);
864 }
865
866 let ids = accounts.get_all();
867 for (i, expected_id) in (1..10).enumerate() {
868 assert_eq!(ids.get(i), Some(&expected_id));
869 }
870 }
871
872 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
873 async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
874 let dir = tempfile::tempdir()?;
875 let p: PathBuf = dir.path().join("accounts");
876 let dummy_accounts = 10;
877
878 let (id0, id1, id2) = {
879 let writable = true;
880 let mut accounts = Accounts::new(p.clone(), writable).await?;
881 accounts.add_account().await?;
882 let ids = accounts.get_all();
883 assert_eq!(ids.len(), 1);
884
885 let id0 = *ids.first().unwrap();
886 let ctx = accounts.get_account(id0).unwrap();
887 ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
888 .await?;
889
890 let id1 = accounts.add_account().await?;
891 let ctx = accounts.get_account(id1).unwrap();
892 ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
893 .await?;
894
895 for _ in 0..dummy_accounts {
897 let to_delete = accounts.add_account().await?;
898 accounts.remove_account(to_delete).await?;
899 }
900
901 let id2 = accounts.add_account().await?;
902 let ctx = accounts.get_account(id2).unwrap();
903 ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
904 .await?;
905
906 accounts.select_account(id1).await?;
907
908 (id0, id1, id2)
909 };
910 assert!(id0 > 0);
911 assert!(id1 > id0);
912 assert!(id2 > id1 + dummy_accounts);
913
914 let (id0_reopened, id1_reopened, id2_reopened) = {
915 let writable = false;
916 let accounts = Accounts::new(p.clone(), writable).await?;
917 let ctx = accounts.get_selected_account().unwrap();
918 assert_eq!(
919 ctx.get_config(crate::config::Config::Addr).await?,
920 Some("two@example.org".to_string())
921 );
922
923 let ids = accounts.get_all();
924 assert_eq!(ids.len(), 3);
925
926 let id0 = *ids.first().unwrap();
927 let ctx = accounts.get_account(id0).unwrap();
928 assert_eq!(
929 ctx.get_config(crate::config::Config::Addr).await?,
930 Some("one@example.org".to_string())
931 );
932
933 let id1 = *ids.get(1).unwrap();
934 let t = accounts.get_account(id1).unwrap();
935 assert_eq!(
936 t.get_config(crate::config::Config::Addr).await?,
937 Some("two@example.org".to_string())
938 );
939
940 let id2 = *ids.get(2).unwrap();
941 let ctx = accounts.get_account(id2).unwrap();
942 assert_eq!(
943 ctx.get_config(crate::config::Config::Addr).await?,
944 Some("three@example.org".to_string())
945 );
946
947 (id0, id1, id2)
948 };
949 assert_eq!(id0, id0_reopened);
950 assert_eq!(id1, id1_reopened);
951 assert_eq!(id2, id2_reopened);
952
953 Ok(())
954 }
955
956 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
957 async fn test_no_accounts_event_emitter() -> Result<()> {
958 let dir = tempfile::tempdir().unwrap();
959 let p: PathBuf = dir.path().join("accounts");
960
961 let writable = true;
962 let accounts = Accounts::new(p.clone(), writable).await?;
963
964 assert_eq!(accounts.accounts.len(), 0);
966
967 let event_emitter = accounts.get_event_emitter();
969
970 let duration = std::time::Duration::from_millis(1);
972 assert!(
973 tokio::time::timeout(duration, event_emitter.recv())
974 .await
975 .is_err()
976 );
977
978 drop(accounts);
980 assert_eq!(event_emitter.recv().await, None);
981
982 Ok(())
983 }
984
985 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
986 async fn test_encrypted_account() -> Result<()> {
987 let dir = tempfile::tempdir().context("failed to create tempdir")?;
988 let p: PathBuf = dir.path().join("accounts");
989
990 let writable = true;
991 let mut accounts = Accounts::new(p.clone(), writable)
992 .await
993 .context("failed to create accounts manager")?;
994
995 assert_eq!(accounts.accounts.len(), 0);
996 let account_id = accounts
997 .add_closed_account()
998 .await
999 .context("failed to add closed account")?;
1000 let account = accounts
1001 .get_selected_account()
1002 .context("failed to get account")?;
1003 assert_eq!(account.id, account_id);
1004 let passphrase_set_success = account
1005 .open("foobar".to_string())
1006 .await
1007 .context("failed to set passphrase")?;
1008 assert!(passphrase_set_success);
1009 drop(accounts);
1010
1011 let writable = false;
1012 let accounts = Accounts::new(p.clone(), writable)
1013 .await
1014 .context("failed to create second accounts manager")?;
1015 let account = accounts
1016 .get_selected_account()
1017 .context("failed to get account")?;
1018 assert_eq!(account.is_open().await, false);
1019
1020 assert_eq!(account.open("barfoo".to_string()).await?, false);
1022 assert_eq!(account.open("".to_string()).await?, false);
1023
1024 assert_eq!(account.open("foobar".to_string()).await?, true);
1025 assert_eq!(account.is_open().await, true);
1026
1027 Ok(())
1028 }
1029
1030 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1032 async fn test_accounts_share_translations() -> Result<()> {
1033 let dir = tempfile::tempdir().unwrap();
1034 let p: PathBuf = dir.path().join("accounts");
1035
1036 let writable = true;
1037 let mut accounts = Accounts::new(p.clone(), writable).await?;
1038 accounts.add_account().await?;
1039 accounts.add_account().await?;
1040
1041 let account1 = accounts.get_account(1).context("failed to get account 1")?;
1042 let account2 = accounts.get_account(2).context("failed to get account 2")?;
1043
1044 assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
1045 assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
1046 account1
1047 .set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
1048 .await?;
1049 assert_eq!(stock_str::no_messages(&account1).await, "foobar");
1050 assert_eq!(stock_str::no_messages(&account2).await, "foobar");
1051
1052 Ok(())
1053 }
1054}