1use std::collections::{BTreeMap, BTreeSet};
4use std::future::Future;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use anyhow::{Context as _, Result, bail, ensure};
9use async_channel::{self, Receiver, Sender};
10use futures::FutureExt as _;
11use futures_lite::FutureExt as _;
12use serde::{Deserialize, Serialize};
13use tokio::fs;
14use tokio::io::AsyncWriteExt;
15use tokio::task::{JoinHandle, JoinSet};
16use uuid::Uuid;
17
18#[cfg(not(target_os = "ios"))]
19use tokio::sync::oneshot;
20#[cfg(not(target_os = "ios"))]
21use tokio::time::{Duration, sleep};
22
23use crate::context::{Context, ContextBuilder};
24use crate::events::{Event, EventEmitter, EventType, Events};
25use crate::log::warn;
26use crate::push::PushSubscriber;
27use crate::stock_str::StockStrings;
28
29#[derive(Debug)]
31pub struct Accounts {
32 dir: PathBuf,
33 config: Config,
34 accounts: BTreeMap<u32, Context>,
36
37 events: Events,
39
40 pub(crate) stockstrings: StockStrings,
45
46 push_subscriber: PushSubscriber,
48
49 background_fetch_interrupt_sender: Arc<parking_lot::Mutex<Option<Sender<()>>>>,
55}
56
57impl Accounts {
58 pub async fn new(dir: PathBuf, writable: bool) -> Result<Self> {
60 if writable && !dir.exists() {
61 Accounts::create(&dir).await?;
62 }
63
64 Accounts::open(dir, writable).await
65 }
66
67 fn get_id(&self) -> u32 {
72 0
73 }
74
75 async fn create(dir: &Path) -> Result<()> {
77 fs::create_dir_all(dir)
78 .await
79 .context("failed to create folder")?;
80
81 Config::new(dir).await?;
82
83 Ok(())
84 }
85
86 async fn open(dir: PathBuf, writable: bool) -> Result<Self> {
89 ensure!(dir.exists(), "directory does not exist");
90
91 let config_file = dir.join(CONFIG_NAME);
92 ensure!(config_file.exists(), "{config_file:?} does not exist");
93
94 let config = Config::from_file(config_file, writable).await?;
95 let events = Events::new();
96 let stockstrings = StockStrings::new();
97 let push_subscriber = PushSubscriber::new();
98 let accounts = config
99 .load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
100 .await
101 .context("failed to load accounts")?;
102
103 Ok(Self {
104 dir,
105 config,
106 accounts,
107 events,
108 stockstrings,
109 push_subscriber,
110 background_fetch_interrupt_sender: Default::default(),
111 })
112 }
113
114 pub fn get_account(&self, id: u32) -> Option<Context> {
116 self.accounts.get(&id).cloned()
117 }
118
119 pub fn get_selected_account(&self) -> Option<Context> {
121 let id = self.config.get_selected_account();
122 self.accounts.get(&id).cloned()
123 }
124
125 pub fn get_selected_account_id(&self) -> Option<u32> {
127 match self.config.get_selected_account() {
128 0 => None,
129 id => Some(id),
130 }
131 }
132
133 pub async fn select_account(&mut self, id: u32) -> Result<()> {
135 self.config.select_account(id).await?;
136
137 Ok(())
138 }
139
140 pub async fn add_account(&mut self) -> Result<u32> {
144 let account_config = self.config.new_account().await?;
145 let dbfile = account_config.dbfile(&self.dir);
146
147 let ctx = ContextBuilder::new(dbfile)
148 .with_id(account_config.id)
149 .with_events(self.events.clone())
150 .with_stock_strings(self.stockstrings.clone())
151 .with_push_subscriber(self.push_subscriber.clone())
152 .build()
153 .await?;
154 ctx.open("".to_string()).await?;
157
158 self.accounts.insert(account_config.id, ctx);
159 self.emit_event(EventType::AccountsChanged);
160
161 Ok(account_config.id)
162 }
163
164 pub async fn add_closed_account(&mut self) -> Result<u32> {
166 let account_config = self.config.new_account().await?;
167 let dbfile = account_config.dbfile(&self.dir);
168
169 let ctx = ContextBuilder::new(dbfile)
170 .with_id(account_config.id)
171 .with_events(self.events.clone())
172 .with_stock_strings(self.stockstrings.clone())
173 .with_push_subscriber(self.push_subscriber.clone())
174 .build()
175 .await?;
176 self.accounts.insert(account_config.id, ctx);
177 self.emit_event(EventType::AccountsChanged);
178
179 Ok(account_config.id)
180 }
181
182 pub async fn remove_account(&mut self, id: u32) -> Result<()> {
184 let ctx = self
185 .accounts
186 .remove(&id)
187 .with_context(|| format!("no account with id {id}"))?;
188 ctx.stop_io().await;
189
190 ctx.sql.close().await;
202 drop(ctx);
203
204 if let Some(cfg) = self.config.get_account(id) {
205 let account_path = self.dir.join(cfg.dir);
206
207 try_many_times(|| fs::remove_dir_all(&account_path))
208 .await
209 .context("failed to remove account data")?;
210 }
211 self.config.remove_account(id).await?;
212 self.emit_event(EventType::AccountsChanged);
213
214 Ok(())
215 }
216
217 pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
221 let blobdir = Context::derive_blobdir(&dbfile);
222 let walfile = Context::derive_walfile(&dbfile);
223
224 ensure!(dbfile.exists(), "no database found: {}", dbfile.display());
225 ensure!(blobdir.exists(), "no blobdir found: {}", blobdir.display());
226
227 let old_id = self.config.get_selected_account();
228
229 let account_config = self
231 .config
232 .new_account()
233 .await
234 .context("failed to create new account")?;
235
236 let new_dbfile = account_config.dbfile(&self.dir);
237 let new_blobdir = Context::derive_blobdir(&new_dbfile);
238 let new_walfile = Context::derive_walfile(&new_dbfile);
239
240 let res = {
241 fs::create_dir_all(self.dir.join(&account_config.dir))
242 .await
243 .context("failed to create dir")?;
244 try_many_times(|| fs::rename(&dbfile, &new_dbfile))
245 .await
246 .context("failed to rename dbfile")?;
247 try_many_times(|| fs::rename(&blobdir, &new_blobdir))
248 .await
249 .context("failed to rename blobdir")?;
250 if walfile.exists() {
251 fs::rename(&walfile, &new_walfile)
252 .await
253 .context("failed to rename walfile")?;
254 }
255 Ok(())
256 };
257
258 match res {
259 Ok(_) => {
260 let ctx = Context::new(
261 &new_dbfile,
262 account_config.id,
263 self.events.clone(),
264 self.stockstrings.clone(),
265 )
266 .await?;
267 self.accounts.insert(account_config.id, ctx);
268 Ok(account_config.id)
269 }
270 Err(err) => {
271 let account_path = std::path::PathBuf::from(&account_config.dir);
272 try_many_times(|| fs::remove_dir_all(&account_path))
273 .await
274 .context("failed to remove account data")?;
275 self.config.remove_account(account_config.id).await?;
276
277 self.select_account(old_id).await?;
279
280 Err(err)
281 }
282 }
283 }
284
285 pub fn get_all(&self) -> Vec<u32> {
287 let mut ordered_ids = Vec::new();
288 let mut all_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
289
290 for &id in &self.config.inner.accounts_order {
292 if all_ids.remove(&id) {
293 ordered_ids.push(id);
294 }
295 }
296
297 for id in all_ids {
299 ordered_ids.push(id);
300 }
301
302 ordered_ids
303 }
304
305 pub async fn set_accounts_order(&mut self, order: Vec<u32>) -> Result<()> {
311 let existing_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
312
313 let mut filtered_order: Vec<u32> = order
315 .into_iter()
316 .filter(|id| existing_ids.contains(id))
317 .collect();
318
319 for &id in &existing_ids {
321 if !filtered_order.contains(&id) {
322 filtered_order.push(id);
323 }
324 }
325
326 self.config.inner.accounts_order = filtered_order;
327 self.config.sync().await?;
328 self.emit_event(EventType::AccountsChanged);
329 Ok(())
330 }
331
332 pub async fn start_io(&mut self) {
334 for account in self.accounts.values_mut() {
335 account.start_io().await;
336 }
337 }
338
339 pub async fn stop_io(&self) {
341 info!(self, "Stopping IO for all accounts.");
344 for account in self.accounts.values() {
345 account.stop_io().await;
346 }
347 }
348
349 pub async fn maybe_network(&self) {
351 for account in self.accounts.values() {
352 account.scheduler.maybe_network().await;
353 }
354 }
355
356 pub async fn maybe_network_lost(&self) {
358 for account in self.accounts.values() {
359 account.scheduler.maybe_network_lost(account).await;
360 }
361 }
362
363 async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
373 let n_accounts = accounts.len();
374 events.emit(Event {
375 id: 0,
376 typ: EventType::Info(format!(
377 "Starting background fetch for {n_accounts} accounts."
378 )),
379 });
380 let mut set = JoinSet::new();
381 for account in accounts {
382 set.spawn(async move {
383 if let Err(error) = account.background_fetch().await {
384 warn!(account, "{error:#}");
385 }
386 });
387 }
388 set.join_all().await;
389 events.emit(Event {
390 id: 0,
391 typ: EventType::Info(format!(
392 "Finished background fetch for {n_accounts} accounts."
393 )),
394 });
395 }
396
397 async fn background_fetch_with_timeout(
411 accounts: Vec<Context>,
412 events: Events,
413 timeout: std::time::Duration,
414 interrupt_sender: Arc<parking_lot::Mutex<Option<Sender<()>>>>,
415 interrupt_receiver: Option<Receiver<()>>,
416 ) {
417 let Some(interrupt_receiver) = interrupt_receiver else {
418 return;
420 };
421 if let Err(_err) = tokio::time::timeout(
422 timeout,
423 Self::background_fetch_no_timeout(accounts, events.clone())
424 .race(interrupt_receiver.recv().map(|_| ())),
425 )
426 .await
427 {
428 events.emit(Event {
429 id: 0,
430 typ: EventType::Warning("Background fetch timed out.".to_string()),
431 });
432 }
433 events.emit(Event {
434 id: 0,
435 typ: EventType::AccountsBackgroundFetchDone,
436 });
437 (*interrupt_sender.lock()) = None;
438 }
439
440 pub fn background_fetch(
454 &self,
455 timeout: std::time::Duration,
456 ) -> impl Future<Output = ()> + use<> {
457 let accounts: Vec<Context> = self.accounts.values().cloned().collect();
458 let events = self.events.clone();
459 let (sender, receiver) = async_channel::bounded(1);
460 let receiver = {
461 let mut lock = self.background_fetch_interrupt_sender.lock();
462 if (*lock).is_some() {
463 None
466 } else {
467 *lock = Some(sender);
468 Some(receiver)
469 }
470 };
471 Self::background_fetch_with_timeout(
472 accounts,
473 events,
474 timeout,
475 self.background_fetch_interrupt_sender.clone(),
476 receiver,
477 )
478 }
479
480 pub fn stop_background_fetch(&self) {
488 let mut lock = self.background_fetch_interrupt_sender.lock();
489 if let Some(sender) = lock.take() {
490 sender.try_send(()).ok();
491 }
492 }
493
494 pub fn emit_event(&self, event: EventType) {
496 self.events.emit(Event { id: 0, typ: event })
497 }
498
499 pub fn get_event_emitter(&self) -> EventEmitter {
501 self.events.get_emitter()
502 }
503
504 pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
506 self.push_subscriber.set_device_token(token).await;
507 Ok(())
508 }
509}
510
511const CONFIG_NAME: &str = "accounts.toml";
513
514#[cfg(not(target_os = "ios"))]
516const LOCKFILE_NAME: &str = "accounts.lock";
517
518const DB_NAME: &str = "dc.db";
520
521#[derive(Debug)]
523struct Config {
524 file: PathBuf,
525 inner: InnerConfig,
526 lock_task: Option<JoinHandle<anyhow::Result<()>>>,
529}
530
531#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
535struct InnerConfig {
536 pub selected_account: u32,
538 pub next_id: u32,
539 pub accounts: Vec<AccountConfig>,
540 #[serde(default)]
543 pub accounts_order: Vec<u32>,
544}
545
546impl Drop for Config {
547 fn drop(&mut self) {
548 if let Some(lock_task) = self.lock_task.take() {
549 lock_task.abort();
550 }
551 }
552}
553
554impl Config {
555 #[cfg(target_os = "ios")]
556 async fn create_lock_task(_dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
557 Ok(None)
561 }
562
563 #[cfg(not(target_os = "ios"))]
564 async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
565 let lockfile = dir.join(LOCKFILE_NAME);
566 let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
567 let (locked_tx, locked_rx) = oneshot::channel();
568 let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
569 let mut timeout = Duration::from_millis(100);
570 let _guard = loop {
571 match lock.try_write() {
572 Ok(guard) => break Ok(guard),
573 Err(err) => {
574 if timeout.as_millis() > 1600 {
575 break Err(err);
576 }
577 sleep(timeout).await;
581 if err.kind() == std::io::ErrorKind::WouldBlock {
582 timeout *= 2;
583 }
584 }
585 }
586 }?;
587 locked_tx
588 .send(())
589 .ok()
590 .context("Cannot notify about lockfile locking")?;
591 let (_tx, rx) = oneshot::channel();
592 rx.await?;
593 Ok(())
594 });
595 if locked_rx.await.is_err() {
596 bail!(
597 "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)"
598 );
599 };
600 Ok(Some(lock_task))
601 }
602
603 async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
605 let dir = file.parent().context("Cannot get config file directory")?;
606 let inner = InnerConfig {
607 accounts: Vec::new(),
608 selected_account: 0,
609 next_id: 1,
610 accounts_order: Vec::new(),
611 };
612 if !lock {
613 let cfg = Self {
614 file,
615 inner,
616 lock_task: None,
617 };
618 return Ok(cfg);
619 }
620 let lock_task = Self::create_lock_task(dir.to_path_buf()).await?;
621 let cfg = Self {
622 file,
623 inner,
624 lock_task,
625 };
626 Ok(cfg)
627 }
628
629 pub async fn new(dir: &Path) -> Result<Self> {
631 let lock = true;
632 let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
633 cfg.sync().await?;
634
635 Ok(cfg)
636 }
637
638 async fn sync(&mut self) -> Result<()> {
642 #[cfg(not(target_os = "ios"))]
643 ensure!(
644 !self
645 .lock_task
646 .as_ref()
647 .context("Config is read-only")?
648 .is_finished()
649 );
650
651 let tmp_path = self.file.with_extension("toml.tmp");
652 let mut file = fs::File::create(&tmp_path)
653 .await
654 .context("failed to create a tmp config")?;
655 file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
656 .await
657 .context("failed to write a tmp config")?;
658 file.sync_data()
659 .await
660 .context("failed to sync a tmp config")?;
661 drop(file);
662 fs::rename(&tmp_path, &self.file)
663 .await
664 .context("failed to rename config")?;
665 Ok(())
666 }
667
668 pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
670 let mut config = Self::new_nosync(file, writable).await?;
671 let bytes = fs::read(&config.file)
672 .await
673 .context("Failed to read file")?;
674 let s = std::str::from_utf8(&bytes)?;
675 config.inner = toml::from_str(s).context("Failed to parse config")?;
676
677 let mut modified = false;
680 for account in &mut config.inner.accounts {
681 if account.dir.is_absolute()
682 && let Some(old_path_parent) = account.dir.parent()
683 && let Ok(new_path) = account.dir.strip_prefix(old_path_parent)
684 {
685 account.dir = new_path.to_path_buf();
686 modified = true;
687 }
688 }
689 if modified && writable {
690 config.sync().await?;
691 }
692
693 Ok(config)
694 }
695
696 pub async fn load_accounts(
701 &self,
702 events: &Events,
703 stockstrings: &StockStrings,
704 push_subscriber: PushSubscriber,
705 dir: &Path,
706 ) -> Result<BTreeMap<u32, Context>> {
707 let mut accounts = BTreeMap::new();
708
709 for account_config in &self.inner.accounts {
710 let dbfile = account_config.dbfile(dir);
711 let ctx = ContextBuilder::new(dbfile.clone())
712 .with_id(account_config.id)
713 .with_events(events.clone())
714 .with_stock_strings(stockstrings.clone())
715 .with_push_subscriber(push_subscriber.clone())
716 .build()
717 .await
718 .with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
719 ctx.open("".to_string()).await?;
722
723 accounts.insert(account_config.id, ctx);
724 }
725
726 Ok(accounts)
727 }
728
729 async fn new_account(&mut self) -> Result<AccountConfig> {
731 let id = {
732 let id = self.inner.next_id;
733 let uuid = Uuid::new_v4();
734 let target_dir = PathBuf::from(uuid.to_string());
735
736 self.inner.accounts.push(AccountConfig {
737 id,
738 dir: target_dir,
739 uuid,
740 });
741 self.inner.next_id += 1;
742
743 self.inner.accounts_order.push(id);
745
746 id
747 };
748
749 self.sync().await?;
750
751 self.select_account(id)
752 .await
753 .context("failed to select just added account")?;
754 let cfg = self
755 .get_account(id)
756 .context("failed to get just added account")?;
757 Ok(cfg)
758 }
759
760 pub async fn remove_account(&mut self, id: u32) -> Result<()> {
762 {
763 if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
764 self.inner.accounts.remove(idx);
766 }
767
768 self.inner.accounts_order.retain(|&x| x != id);
770
771 if self.inner.selected_account == id {
772 self.inner.selected_account = self
774 .inner
775 .accounts
776 .first()
777 .map(|e| e.id)
778 .unwrap_or_default();
779 }
780 }
781
782 self.sync().await
783 }
784
785 fn get_account(&self, id: u32) -> Option<AccountConfig> {
787 self.inner.accounts.iter().find(|e| e.id == id).cloned()
788 }
789
790 pub fn get_selected_account(&self) -> u32 {
792 self.inner.selected_account
793 }
794
795 pub async fn select_account(&mut self, id: u32) -> Result<()> {
797 {
798 ensure!(
799 self.inner.accounts.iter().any(|e| e.id == id),
800 "invalid account id: {id}"
801 );
802
803 self.inner.selected_account = id;
804 }
805
806 self.sync().await?;
807 Ok(())
808 }
809}
810
811async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
820where
821 F: Fn() -> Fut,
822 Fut: Future<Output = std::result::Result<(), T>>,
823{
824 let mut counter = 0;
825 loop {
826 counter += 1;
827
828 if let Err(err) = f().await {
829 if counter > 60 {
830 return Err(err);
831 }
832
833 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
835 } else {
836 break;
837 }
838 }
839 Ok(())
840}
841
842#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
844struct AccountConfig {
845 pub id: u32,
847
848 pub dir: std::path::PathBuf,
852
853 pub uuid: Uuid,
855}
856
857impl AccountConfig {
858 pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf {
860 accounts_dir.join(&self.dir).join(DB_NAME)
861 }
862}
863
864#[cfg(test)]
865mod tests {
866 use super::*;
867 use crate::stock_str::{self, StockMessage};
868
869 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
870 async fn test_account_new_open() {
871 let dir = tempfile::tempdir().unwrap();
872 let p: PathBuf = dir.path().join("accounts1");
873
874 {
875 let writable = true;
876 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
877 accounts.add_account().await.unwrap();
878
879 assert_eq!(accounts.accounts.len(), 1);
880 assert_eq!(accounts.config.get_selected_account(), 1);
881 }
882 for writable in [true, false] {
883 let accounts = Accounts::new(p.clone(), writable).await.unwrap();
884
885 assert_eq!(accounts.accounts.len(), 1);
886 assert_eq!(accounts.config.get_selected_account(), 1);
887 }
888 }
889
890 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
891 async fn test_account_new_open_conflict() {
892 let dir = tempfile::tempdir().unwrap();
893 let p: PathBuf = dir.path().join("accounts");
894 let writable = true;
895 let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
896
897 let writable = true;
898 assert!(Accounts::new(p.clone(), writable).await.is_err());
899
900 let writable = false;
901 let accounts = Accounts::new(p, writable).await.unwrap();
902 assert_eq!(accounts.accounts.len(), 0);
903 assert_eq!(accounts.config.get_selected_account(), 0);
904 }
905
906 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
907 async fn test_account_new_add_remove() {
908 let dir = tempfile::tempdir().unwrap();
909 let p: PathBuf = dir.path().join("accounts");
910
911 let writable = true;
912 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
913 assert_eq!(accounts.accounts.len(), 0);
914 assert_eq!(accounts.config.get_selected_account(), 0);
915
916 let id = accounts.add_account().await.unwrap();
917 assert_eq!(id, 1);
918 assert_eq!(accounts.accounts.len(), 1);
919 assert_eq!(accounts.config.get_selected_account(), 1);
920
921 let id = accounts.add_account().await.unwrap();
922 assert_eq!(id, 2);
923 assert_eq!(accounts.config.get_selected_account(), id);
924 assert_eq!(accounts.accounts.len(), 2);
925
926 accounts.select_account(1).await.unwrap();
927 assert_eq!(accounts.config.get_selected_account(), 1);
928
929 accounts.remove_account(1).await.unwrap();
930 assert_eq!(accounts.config.get_selected_account(), 2);
931 assert_eq!(accounts.accounts.len(), 1);
932 }
933
934 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
935 async fn test_accounts_remove_last() -> Result<()> {
936 let dir = tempfile::tempdir()?;
937 let p: PathBuf = dir.path().join("accounts");
938
939 let writable = true;
940 let mut accounts = Accounts::new(p.clone(), writable).await?;
941 assert!(accounts.get_selected_account().is_none());
942 assert_eq!(accounts.config.get_selected_account(), 0);
943
944 let id = accounts.add_account().await?;
945 assert!(accounts.get_selected_account().is_some());
946 assert_eq!(id, 1);
947 assert_eq!(accounts.accounts.len(), 1);
948 assert_eq!(accounts.config.get_selected_account(), id);
949
950 accounts.remove_account(id).await?;
951 assert!(accounts.get_selected_account().is_none());
952
953 Ok(())
954 }
955
956 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
957 async fn test_migrate_account() {
958 let dir = tempfile::tempdir().unwrap();
959 let p: PathBuf = dir.path().join("accounts");
960
961 let writable = true;
962 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
963 assert_eq!(accounts.accounts.len(), 0);
964 assert_eq!(accounts.config.get_selected_account(), 0);
965
966 let extern_dbfile: PathBuf = dir.path().join("other");
967 let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
968 .await
969 .unwrap();
970 ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
971 .await
972 .unwrap();
973
974 drop(ctx);
975
976 accounts
977 .migrate_account(extern_dbfile.clone())
978 .await
979 .unwrap();
980 assert_eq!(accounts.accounts.len(), 1);
981 assert_eq!(accounts.config.get_selected_account(), 1);
982
983 let ctx = accounts.get_selected_account().unwrap();
984 assert_eq!(
985 "me@mail.com",
986 ctx.get_config(crate::config::Config::Addr)
987 .await
988 .unwrap()
989 .unwrap()
990 );
991 }
992
993 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
995 async fn test_accounts_sorted() {
996 let dir = tempfile::tempdir().unwrap();
997 let p: PathBuf = dir.path().join("accounts");
998
999 let writable = true;
1000 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
1001
1002 for expected_id in 1..10 {
1003 let id = accounts.add_account().await.unwrap();
1004 assert_eq!(id, expected_id);
1005 }
1006
1007 let ids = accounts.get_all();
1008 for (i, expected_id) in (1..10).enumerate() {
1009 assert_eq!(ids.get(i), Some(&expected_id));
1010 }
1011 }
1012
1013 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1014 async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
1015 let dir = tempfile::tempdir()?;
1016 let p: PathBuf = dir.path().join("accounts");
1017 let dummy_accounts = 10;
1018
1019 let (id0, id1, id2) = {
1020 let writable = true;
1021 let mut accounts = Accounts::new(p.clone(), writable).await?;
1022 accounts.add_account().await?;
1023 let ids = accounts.get_all();
1024 assert_eq!(ids.len(), 1);
1025
1026 let id0 = *ids.first().unwrap();
1027 let ctx = accounts.get_account(id0).unwrap();
1028 ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
1029 .await?;
1030
1031 let id1 = accounts.add_account().await?;
1032 let ctx = accounts.get_account(id1).unwrap();
1033 ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
1034 .await?;
1035
1036 for _ in 0..dummy_accounts {
1038 let to_delete = accounts.add_account().await?;
1039 accounts.remove_account(to_delete).await?;
1040 }
1041
1042 let id2 = accounts.add_account().await?;
1043 let ctx = accounts.get_account(id2).unwrap();
1044 ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
1045 .await?;
1046
1047 accounts.select_account(id1).await?;
1048
1049 (id0, id1, id2)
1050 };
1051 assert!(id0 > 0);
1052 assert!(id1 > id0);
1053 assert!(id2 > id1 + dummy_accounts);
1054
1055 let (id0_reopened, id1_reopened, id2_reopened) = {
1056 let writable = false;
1057 let accounts = Accounts::new(p.clone(), writable).await?;
1058 let ctx = accounts.get_selected_account().unwrap();
1059 assert_eq!(
1060 ctx.get_config(crate::config::Config::Addr).await?,
1061 Some("two@example.org".to_string())
1062 );
1063
1064 let ids = accounts.get_all();
1065 assert_eq!(ids.len(), 3);
1066
1067 let id0 = *ids.first().unwrap();
1068 let ctx = accounts.get_account(id0).unwrap();
1069 assert_eq!(
1070 ctx.get_config(crate::config::Config::Addr).await?,
1071 Some("one@example.org".to_string())
1072 );
1073
1074 let id1 = *ids.get(1).unwrap();
1075 let t = accounts.get_account(id1).unwrap();
1076 assert_eq!(
1077 t.get_config(crate::config::Config::Addr).await?,
1078 Some("two@example.org".to_string())
1079 );
1080
1081 let id2 = *ids.get(2).unwrap();
1082 let ctx = accounts.get_account(id2).unwrap();
1083 assert_eq!(
1084 ctx.get_config(crate::config::Config::Addr).await?,
1085 Some("three@example.org".to_string())
1086 );
1087
1088 (id0, id1, id2)
1089 };
1090 assert_eq!(id0, id0_reopened);
1091 assert_eq!(id1, id1_reopened);
1092 assert_eq!(id2, id2_reopened);
1093
1094 Ok(())
1095 }
1096
1097 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1098 async fn test_no_accounts_event_emitter() -> Result<()> {
1099 let dir = tempfile::tempdir().unwrap();
1100 let p: PathBuf = dir.path().join("accounts");
1101
1102 let writable = true;
1103 let accounts = Accounts::new(p.clone(), writable).await?;
1104
1105 assert_eq!(accounts.accounts.len(), 0);
1107
1108 let event_emitter = accounts.get_event_emitter();
1110
1111 let duration = std::time::Duration::from_millis(1);
1113 assert!(
1114 tokio::time::timeout(duration, event_emitter.recv())
1115 .await
1116 .is_err()
1117 );
1118
1119 drop(accounts);
1121 assert_eq!(event_emitter.recv().await, None);
1122
1123 Ok(())
1124 }
1125
1126 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1127 async fn test_encrypted_account() -> Result<()> {
1128 let dir = tempfile::tempdir().context("failed to create tempdir")?;
1129 let p: PathBuf = dir.path().join("accounts");
1130
1131 let writable = true;
1132 let mut accounts = Accounts::new(p.clone(), writable)
1133 .await
1134 .context("failed to create accounts manager")?;
1135
1136 assert_eq!(accounts.accounts.len(), 0);
1137 let account_id = accounts
1138 .add_closed_account()
1139 .await
1140 .context("failed to add closed account")?;
1141 let account = accounts
1142 .get_selected_account()
1143 .context("failed to get account")?;
1144 assert_eq!(account.id, account_id);
1145 let passphrase_set_success = account
1146 .open("foobar".to_string())
1147 .await
1148 .context("failed to set passphrase")?;
1149 assert!(passphrase_set_success);
1150 drop(accounts);
1151
1152 let writable = false;
1153 let accounts = Accounts::new(p.clone(), writable)
1154 .await
1155 .context("failed to create second accounts manager")?;
1156 let account = accounts
1157 .get_selected_account()
1158 .context("failed to get account")?;
1159 assert_eq!(account.is_open().await, false);
1160
1161 assert_eq!(account.open("barfoo".to_string()).await?, false);
1163 assert_eq!(account.open("".to_string()).await?, false);
1164
1165 assert_eq!(account.open("foobar".to_string()).await?, true);
1166 assert_eq!(account.is_open().await, true);
1167
1168 Ok(())
1169 }
1170
1171 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1173 async fn test_accounts_share_translations() -> Result<()> {
1174 let dir = tempfile::tempdir().unwrap();
1175 let p: PathBuf = dir.path().join("accounts");
1176
1177 let writable = true;
1178 let mut accounts = Accounts::new(p.clone(), writable).await?;
1179 accounts.add_account().await?;
1180 accounts.add_account().await?;
1181
1182 let account1 = accounts.get_account(1).context("failed to get account 1")?;
1183 let account2 = accounts.get_account(2).context("failed to get account 2")?;
1184
1185 assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
1186 assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
1187 account1
1188 .set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
1189 .await?;
1190 assert_eq!(stock_str::no_messages(&account1).await, "foobar");
1191 assert_eq!(stock_str::no_messages(&account2).await, "foobar");
1192
1193 Ok(())
1194 }
1195}