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