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 let events = Events::new();
64 Accounts::open(events, dir, writable).await
65 }
66
67 pub async fn new_with_events(dir: PathBuf, writable: bool, events: Events) -> Result<Self> {
70 if writable && !dir.exists() {
71 Accounts::create(&dir).await?;
72 }
73
74 Accounts::open(events, dir, writable).await
75 }
76
77 fn get_id(&self) -> u32 {
82 0
83 }
84
85 async fn create(dir: &Path) -> Result<()> {
87 fs::create_dir_all(dir)
88 .await
89 .context("failed to create folder")?;
90
91 Config::new(dir).await?;
92
93 Ok(())
94 }
95
96 async fn open(events: Events, dir: PathBuf, writable: bool) -> Result<Self> {
99 ensure!(dir.exists(), "directory does not exist");
100
101 let config_file = dir.join(CONFIG_NAME);
102 ensure!(config_file.exists(), "{config_file:?} does not exist");
103
104 let config = Config::from_file(config_file, writable).await?;
105
106 let stockstrings = StockStrings::new();
107 let push_subscriber = PushSubscriber::new();
108 let accounts = config
109 .load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
110 .await
111 .context("failed to load accounts")?;
112
113 Ok(Self {
114 dir,
115 config,
116 accounts,
117 events,
118 stockstrings,
119 push_subscriber,
120 background_fetch_interrupt_sender: Default::default(),
121 })
122 }
123
124 pub fn get_account(&self, id: u32) -> Option<Context> {
126 self.accounts.get(&id).cloned()
127 }
128
129 pub fn get_selected_account(&self) -> Option<Context> {
131 let id = self.config.get_selected_account();
132 self.accounts.get(&id).cloned()
133 }
134
135 pub fn get_selected_account_id(&self) -> Option<u32> {
137 match self.config.get_selected_account() {
138 0 => None,
139 id => Some(id),
140 }
141 }
142
143 pub async fn select_account(&mut self, id: u32) -> Result<()> {
145 self.config.select_account(id).await?;
146
147 Ok(())
148 }
149
150 pub async fn add_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 ctx.open("".to_string()).await?;
167
168 self.accounts.insert(account_config.id, ctx);
169 self.emit_event(EventType::AccountsChanged);
170
171 Ok(account_config.id)
172 }
173
174 pub async fn add_closed_account(&mut self) -> Result<u32> {
176 let account_config = self.config.new_account().await?;
177 let dbfile = account_config.dbfile(&self.dir);
178
179 let ctx = ContextBuilder::new(dbfile)
180 .with_id(account_config.id)
181 .with_events(self.events.clone())
182 .with_stock_strings(self.stockstrings.clone())
183 .with_push_subscriber(self.push_subscriber.clone())
184 .build()
185 .await?;
186 self.accounts.insert(account_config.id, ctx);
187 self.emit_event(EventType::AccountsChanged);
188
189 Ok(account_config.id)
190 }
191
192 pub async fn remove_account(&mut self, id: u32) -> Result<()> {
194 let ctx = self
195 .accounts
196 .remove(&id)
197 .with_context(|| format!("no account with id {id}"))?;
198 ctx.stop_io().await;
199
200 ctx.sql.close().await;
212 drop(ctx);
213
214 if let Some(cfg) = self.config.get_account(id) {
215 let account_path = self.dir.join(cfg.dir);
216
217 try_many_times(|| fs::remove_dir_all(&account_path))
218 .await
219 .context("failed to remove account data")?;
220 }
221 self.config.remove_account(id).await?;
222 self.emit_event(EventType::AccountsChanged);
223
224 Ok(())
225 }
226
227 pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
231 let blobdir = Context::derive_blobdir(&dbfile);
232 let walfile = Context::derive_walfile(&dbfile);
233
234 ensure!(dbfile.exists(), "no database found: {}", dbfile.display());
235 ensure!(blobdir.exists(), "no blobdir found: {}", blobdir.display());
236
237 let old_id = self.config.get_selected_account();
238
239 let account_config = self
241 .config
242 .new_account()
243 .await
244 .context("failed to create new account")?;
245
246 let new_dbfile = account_config.dbfile(&self.dir);
247 let new_blobdir = Context::derive_blobdir(&new_dbfile);
248 let new_walfile = Context::derive_walfile(&new_dbfile);
249
250 let res = {
251 fs::create_dir_all(self.dir.join(&account_config.dir))
252 .await
253 .context("failed to create dir")?;
254 try_many_times(|| fs::rename(&dbfile, &new_dbfile))
255 .await
256 .context("failed to rename dbfile")?;
257 try_many_times(|| fs::rename(&blobdir, &new_blobdir))
258 .await
259 .context("failed to rename blobdir")?;
260 if walfile.exists() {
261 fs::rename(&walfile, &new_walfile)
262 .await
263 .context("failed to rename walfile")?;
264 }
265 Ok(())
266 };
267
268 match res {
269 Ok(_) => {
270 let ctx = Context::new(
271 &new_dbfile,
272 account_config.id,
273 self.events.clone(),
274 self.stockstrings.clone(),
275 )
276 .await?;
277 self.accounts.insert(account_config.id, ctx);
278 Ok(account_config.id)
279 }
280 Err(err) => {
281 let account_path = std::path::PathBuf::from(&account_config.dir);
282 try_many_times(|| fs::remove_dir_all(&account_path))
283 .await
284 .context("failed to remove account data")?;
285 self.config.remove_account(account_config.id).await?;
286
287 self.select_account(old_id).await?;
289
290 Err(err)
291 }
292 }
293 }
294
295 pub fn get_all(&self) -> Vec<u32> {
297 let mut ordered_ids = Vec::new();
298 let mut all_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
299
300 for &id in &self.config.inner.accounts_order {
302 if all_ids.remove(&id) {
303 ordered_ids.push(id);
304 }
305 }
306
307 for id in all_ids {
309 ordered_ids.push(id);
310 }
311
312 ordered_ids
313 }
314
315 pub async fn set_accounts_order(&mut self, order: Vec<u32>) -> Result<()> {
321 let existing_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
322
323 let mut filtered_order: Vec<u32> = order
325 .into_iter()
326 .filter(|id| existing_ids.contains(id))
327 .collect();
328
329 for &id in &existing_ids {
331 if !filtered_order.contains(&id) {
332 filtered_order.push(id);
333 }
334 }
335
336 self.config.inner.accounts_order = filtered_order;
337 self.config.sync().await?;
338 self.emit_event(EventType::AccountsChanged);
339 Ok(())
340 }
341
342 pub async fn start_io(&mut self) {
344 for account in self.accounts.values_mut() {
345 account.start_io().await;
346 }
347 }
348
349 pub async fn stop_io(&self) {
351 info!(self, "Stopping IO for all accounts.");
354 for account in self.accounts.values() {
355 account.stop_io().await;
356 }
357 }
358
359 pub async fn maybe_network(&self) {
361 for account in self.accounts.values() {
362 account.scheduler.maybe_network().await;
363 }
364 }
365
366 pub async fn maybe_network_lost(&self) {
368 for account in self.accounts.values() {
369 account.scheduler.maybe_network_lost(account).await;
370 }
371 }
372
373 async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
383 let n_accounts = accounts.len();
384 events.emit(Event {
385 id: 0,
386 typ: EventType::Info(format!(
387 "Starting background fetch for {n_accounts} accounts."
388 )),
389 });
390 ::tracing::event!(
391 ::tracing::Level::INFO,
392 account_id = 0,
393 "Starting background fetch for {n_accounts} accounts."
394 );
395 let mut set = JoinSet::new();
396 for account in accounts {
397 set.spawn(async move {
398 if let Err(error) = account.background_fetch().await {
399 warn!(account, "{error:#}");
400 }
401 });
402 }
403 set.join_all().await;
404 events.emit(Event {
405 id: 0,
406 typ: EventType::Info(format!(
407 "Finished background fetch for {n_accounts} accounts."
408 )),
409 });
410 ::tracing::event!(
411 ::tracing::Level::INFO,
412 account_id = 0,
413 "Finished background fetch for {n_accounts} accounts."
414 );
415 }
416
417 async fn background_fetch_with_timeout(
431 accounts: Vec<Context>,
432 events: Events,
433 timeout: std::time::Duration,
434 interrupt_sender: Arc<parking_lot::Mutex<Option<Sender<()>>>>,
435 interrupt_receiver: Option<Receiver<()>>,
436 ) {
437 let Some(interrupt_receiver) = interrupt_receiver else {
438 return;
440 };
441 if let Err(_err) = tokio::time::timeout(
442 timeout,
443 Self::background_fetch_no_timeout(accounts, events.clone())
444 .race(interrupt_receiver.recv().map(|_| ())),
445 )
446 .await
447 {
448 events.emit(Event {
449 id: 0,
450 typ: EventType::Warning("Background fetch timed out.".to_string()),
451 });
452 ::tracing::event!(
453 ::tracing::Level::WARN,
454 account_id = 0,
455 "Background fetch timed out."
456 );
457 }
458 events.emit(Event {
459 id: 0,
460 typ: EventType::AccountsBackgroundFetchDone,
461 });
462 (*interrupt_sender.lock()) = None;
463 }
464
465 pub fn background_fetch(
479 &self,
480 timeout: std::time::Duration,
481 ) -> impl Future<Output = ()> + use<> {
482 let accounts: Vec<Context> = self.accounts.values().cloned().collect();
483 let events = self.events.clone();
484 let (sender, receiver) = async_channel::bounded(1);
485 let receiver = {
486 let mut lock = self.background_fetch_interrupt_sender.lock();
487 if (*lock).is_some() {
488 None
491 } else {
492 *lock = Some(sender);
493 Some(receiver)
494 }
495 };
496 Self::background_fetch_with_timeout(
497 accounts,
498 events,
499 timeout,
500 self.background_fetch_interrupt_sender.clone(),
501 receiver,
502 )
503 }
504
505 pub fn stop_background_fetch(&self) {
513 let mut lock = self.background_fetch_interrupt_sender.lock();
514 if let Some(sender) = lock.take() {
515 sender.try_send(()).ok();
516 }
517 }
518
519 pub fn emit_event(&self, event: EventType) {
521 self.events.emit(Event { id: 0, typ: event })
522 }
523
524 pub fn get_event_emitter(&self) -> EventEmitter {
526 self.events.get_emitter()
527 }
528
529 pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
531 self.push_subscriber.set_device_token(token).await;
532 Ok(())
533 }
534}
535
536const CONFIG_NAME: &str = "accounts.toml";
538
539#[cfg(not(target_os = "ios"))]
541const LOCKFILE_NAME: &str = "accounts.lock";
542
543const DB_NAME: &str = "dc.db";
545
546#[derive(Debug)]
548struct Config {
549 file: PathBuf,
550 inner: InnerConfig,
551 lock_task: Option<JoinHandle<anyhow::Result<()>>>,
554}
555
556#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
560struct InnerConfig {
561 pub selected_account: u32,
563 pub next_id: u32,
564 pub accounts: Vec<AccountConfig>,
565 #[serde(default)]
568 pub accounts_order: Vec<u32>,
569}
570
571impl Drop for Config {
572 fn drop(&mut self) {
573 if let Some(lock_task) = self.lock_task.take() {
574 lock_task.abort();
575 }
576 }
577}
578
579impl Config {
580 #[cfg(target_os = "ios")]
581 async fn create_lock_task(_dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
582 Ok(None)
586 }
587
588 #[cfg(not(target_os = "ios"))]
589 async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
590 let lockfile = dir.join(LOCKFILE_NAME);
591 let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
592 let (locked_tx, locked_rx) = oneshot::channel();
593 let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
594 let mut timeout = Duration::from_millis(100);
595 let _guard = loop {
596 match lock.try_write() {
597 Ok(guard) => break Ok(guard),
598 Err(err) => {
599 if timeout.as_millis() > 1600 {
600 break Err(err);
601 }
602 sleep(timeout).await;
606 if err.kind() == std::io::ErrorKind::WouldBlock {
607 timeout *= 2;
608 }
609 }
610 }
611 }?;
612 locked_tx
613 .send(())
614 .ok()
615 .context("Cannot notify about lockfile locking")?;
616 let (_tx, rx) = oneshot::channel();
617 rx.await?;
618 Ok(())
619 });
620 if locked_rx.await.is_err() {
621 bail!(
622 "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)"
623 );
624 };
625 Ok(Some(lock_task))
626 }
627
628 async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
630 let dir = file.parent().context("Cannot get config file directory")?;
631 let inner = InnerConfig {
632 accounts: Vec::new(),
633 selected_account: 0,
634 next_id: 1,
635 accounts_order: Vec::new(),
636 };
637 if !lock {
638 let cfg = Self {
639 file,
640 inner,
641 lock_task: None,
642 };
643 return Ok(cfg);
644 }
645 let lock_task = Self::create_lock_task(dir.to_path_buf()).await?;
646 let cfg = Self {
647 file,
648 inner,
649 lock_task,
650 };
651 Ok(cfg)
652 }
653
654 pub async fn new(dir: &Path) -> Result<Self> {
656 let lock = true;
657 let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
658 cfg.sync().await?;
659
660 Ok(cfg)
661 }
662
663 async fn sync(&mut self) -> Result<()> {
667 #[cfg(not(target_os = "ios"))]
668 ensure!(
669 !self
670 .lock_task
671 .as_ref()
672 .context("Config is read-only")?
673 .is_finished()
674 );
675
676 let tmp_path = self.file.with_extension("toml.tmp");
677 let mut file = fs::File::create(&tmp_path)
678 .await
679 .context("failed to create a tmp config")?;
680 file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
681 .await
682 .context("failed to write a tmp config")?;
683 file.sync_data()
684 .await
685 .context("failed to sync a tmp config")?;
686 drop(file);
687 fs::rename(&tmp_path, &self.file)
688 .await
689 .context("failed to rename config")?;
690 Ok(())
691 }
692
693 pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
695 let mut config = Self::new_nosync(file, writable).await?;
696 let bytes = fs::read(&config.file)
697 .await
698 .context("Failed to read file")?;
699 let s = std::str::from_utf8(&bytes)?;
700 config.inner = toml::from_str(s).context("Failed to parse config")?;
701
702 let mut modified = false;
705 for account in &mut config.inner.accounts {
706 if account.dir.is_absolute()
707 && let Some(old_path_parent) = account.dir.parent()
708 && let Ok(new_path) = account.dir.strip_prefix(old_path_parent)
709 {
710 account.dir = new_path.to_path_buf();
711 modified = true;
712 }
713 }
714 if modified && writable {
715 config.sync().await?;
716 }
717
718 Ok(config)
719 }
720
721 pub async fn load_accounts(
726 &self,
727 events: &Events,
728 stockstrings: &StockStrings,
729 push_subscriber: PushSubscriber,
730 dir: &Path,
731 ) -> Result<BTreeMap<u32, Context>> {
732 let mut accounts = BTreeMap::new();
733
734 for account_config in &self.inner.accounts {
735 let dbfile = account_config.dbfile(dir);
736 let ctx = ContextBuilder::new(dbfile.clone())
737 .with_id(account_config.id)
738 .with_events(events.clone())
739 .with_stock_strings(stockstrings.clone())
740 .with_push_subscriber(push_subscriber.clone())
741 .build()
742 .await
743 .with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
744 ctx.open("".to_string()).await?;
747
748 accounts.insert(account_config.id, ctx);
749 }
750
751 Ok(accounts)
752 }
753
754 async fn new_account(&mut self) -> Result<AccountConfig> {
756 let id = {
757 let id = self.inner.next_id;
758 let uuid = Uuid::new_v4();
759 let target_dir = PathBuf::from(uuid.to_string());
760
761 self.inner.accounts.push(AccountConfig {
762 id,
763 dir: target_dir,
764 uuid,
765 });
766 self.inner.next_id += 1;
767
768 self.inner.accounts_order.push(id);
770
771 id
772 };
773
774 self.sync().await?;
775
776 self.select_account(id)
777 .await
778 .context("failed to select just added account")?;
779 let cfg = self
780 .get_account(id)
781 .context("failed to get just added account")?;
782 Ok(cfg)
783 }
784
785 pub async fn remove_account(&mut self, id: u32) -> Result<()> {
787 {
788 if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
789 self.inner.accounts.remove(idx);
791 }
792
793 self.inner.accounts_order.retain(|&x| x != id);
795
796 if self.inner.selected_account == id {
797 self.inner.selected_account = self
799 .inner
800 .accounts
801 .first()
802 .map(|e| e.id)
803 .unwrap_or_default();
804 }
805 }
806
807 self.sync().await
808 }
809
810 fn get_account(&self, id: u32) -> Option<AccountConfig> {
812 self.inner.accounts.iter().find(|e| e.id == id).cloned()
813 }
814
815 pub fn get_selected_account(&self) -> u32 {
817 self.inner.selected_account
818 }
819
820 pub async fn select_account(&mut self, id: u32) -> Result<()> {
822 {
823 ensure!(
824 self.inner.accounts.iter().any(|e| e.id == id),
825 "invalid account id: {id}"
826 );
827
828 self.inner.selected_account = id;
829 }
830
831 self.sync().await?;
832 Ok(())
833 }
834}
835
836async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
845where
846 F: Fn() -> Fut,
847 Fut: Future<Output = std::result::Result<(), T>>,
848{
849 let mut counter = 0;
850 loop {
851 counter += 1;
852
853 if let Err(err) = f().await {
854 if counter > 60 {
855 return Err(err);
856 }
857
858 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
860 } else {
861 break;
862 }
863 }
864 Ok(())
865}
866
867#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
869struct AccountConfig {
870 pub id: u32,
872
873 pub dir: std::path::PathBuf,
877
878 pub uuid: Uuid,
880}
881
882impl AccountConfig {
883 pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf {
885 accounts_dir.join(&self.dir).join(DB_NAME)
886 }
887}
888
889#[cfg(test)]
890mod tests {
891 use super::*;
892 use crate::stock_str::{self, StockMessage};
893
894 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
895 async fn test_account_new_open() {
896 let dir = tempfile::tempdir().unwrap();
897 let p: PathBuf = dir.path().join("accounts1");
898
899 {
900 let writable = true;
901 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
902 accounts.add_account().await.unwrap();
903
904 assert_eq!(accounts.accounts.len(), 1);
905 assert_eq!(accounts.config.get_selected_account(), 1);
906 }
907 for writable in [true, false] {
908 let accounts = Accounts::new(p.clone(), writable).await.unwrap();
909
910 assert_eq!(accounts.accounts.len(), 1);
911 assert_eq!(accounts.config.get_selected_account(), 1);
912 }
913 }
914
915 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
916 async fn test_account_new_open_conflict() {
917 let dir = tempfile::tempdir().unwrap();
918 let p: PathBuf = dir.path().join("accounts");
919 let writable = true;
920 let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
921
922 let writable = true;
923 assert!(Accounts::new(p.clone(), writable).await.is_err());
924
925 let writable = false;
926 let accounts = Accounts::new(p, writable).await.unwrap();
927 assert_eq!(accounts.accounts.len(), 0);
928 assert_eq!(accounts.config.get_selected_account(), 0);
929 }
930
931 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
932 async fn test_account_new_add_remove() {
933 let dir = tempfile::tempdir().unwrap();
934 let p: PathBuf = dir.path().join("accounts");
935
936 let writable = true;
937 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
938 assert_eq!(accounts.accounts.len(), 0);
939 assert_eq!(accounts.config.get_selected_account(), 0);
940
941 let id = accounts.add_account().await.unwrap();
942 assert_eq!(id, 1);
943 assert_eq!(accounts.accounts.len(), 1);
944 assert_eq!(accounts.config.get_selected_account(), 1);
945
946 let id = accounts.add_account().await.unwrap();
947 assert_eq!(id, 2);
948 assert_eq!(accounts.config.get_selected_account(), id);
949 assert_eq!(accounts.accounts.len(), 2);
950
951 accounts.select_account(1).await.unwrap();
952 assert_eq!(accounts.config.get_selected_account(), 1);
953
954 accounts.remove_account(1).await.unwrap();
955 assert_eq!(accounts.config.get_selected_account(), 2);
956 assert_eq!(accounts.accounts.len(), 1);
957 }
958
959 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
960 async fn test_accounts_remove_last() -> Result<()> {
961 let dir = tempfile::tempdir()?;
962 let p: PathBuf = dir.path().join("accounts");
963
964 let writable = true;
965 let mut accounts = Accounts::new(p.clone(), writable).await?;
966 assert!(accounts.get_selected_account().is_none());
967 assert_eq!(accounts.config.get_selected_account(), 0);
968
969 let id = accounts.add_account().await?;
970 assert!(accounts.get_selected_account().is_some());
971 assert_eq!(id, 1);
972 assert_eq!(accounts.accounts.len(), 1);
973 assert_eq!(accounts.config.get_selected_account(), id);
974
975 accounts.remove_account(id).await?;
976 assert!(accounts.get_selected_account().is_none());
977
978 Ok(())
979 }
980
981 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
982 async fn test_migrate_account() {
983 let dir = tempfile::tempdir().unwrap();
984 let p: PathBuf = dir.path().join("accounts");
985
986 let writable = true;
987 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
988 assert_eq!(accounts.accounts.len(), 0);
989 assert_eq!(accounts.config.get_selected_account(), 0);
990
991 let extern_dbfile: PathBuf = dir.path().join("other");
992 let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
993 .await
994 .unwrap();
995 ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
996 .await
997 .unwrap();
998
999 drop(ctx);
1000
1001 accounts
1002 .migrate_account(extern_dbfile.clone())
1003 .await
1004 .unwrap();
1005 assert_eq!(accounts.accounts.len(), 1);
1006 assert_eq!(accounts.config.get_selected_account(), 1);
1007
1008 let ctx = accounts.get_selected_account().unwrap();
1009 assert_eq!(
1010 "me@mail.com",
1011 ctx.get_config(crate::config::Config::Addr)
1012 .await
1013 .unwrap()
1014 .unwrap()
1015 );
1016 }
1017
1018 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1020 async fn test_accounts_sorted() {
1021 let dir = tempfile::tempdir().unwrap();
1022 let p: PathBuf = dir.path().join("accounts");
1023
1024 let writable = true;
1025 let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
1026
1027 for expected_id in 1..10 {
1028 let id = accounts.add_account().await.unwrap();
1029 assert_eq!(id, expected_id);
1030 }
1031
1032 let ids = accounts.get_all();
1033 for (i, expected_id) in (1..10).enumerate() {
1034 assert_eq!(ids.get(i), Some(&expected_id));
1035 }
1036 }
1037
1038 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1039 async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
1040 let dir = tempfile::tempdir()?;
1041 let p: PathBuf = dir.path().join("accounts");
1042 let dummy_accounts = 10;
1043
1044 let (id0, id1, id2) = {
1045 let writable = true;
1046 let mut accounts = Accounts::new(p.clone(), writable).await?;
1047 accounts.add_account().await?;
1048 let ids = accounts.get_all();
1049 assert_eq!(ids.len(), 1);
1050
1051 let id0 = *ids.first().unwrap();
1052 let ctx = accounts.get_account(id0).unwrap();
1053 ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
1054 .await?;
1055
1056 let id1 = accounts.add_account().await?;
1057 let ctx = accounts.get_account(id1).unwrap();
1058 ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
1059 .await?;
1060
1061 for _ in 0..dummy_accounts {
1063 let to_delete = accounts.add_account().await?;
1064 accounts.remove_account(to_delete).await?;
1065 }
1066
1067 let id2 = accounts.add_account().await?;
1068 let ctx = accounts.get_account(id2).unwrap();
1069 ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
1070 .await?;
1071
1072 accounts.select_account(id1).await?;
1073
1074 (id0, id1, id2)
1075 };
1076 assert!(id0 > 0);
1077 assert!(id1 > id0);
1078 assert!(id2 > id1 + dummy_accounts);
1079
1080 let (id0_reopened, id1_reopened, id2_reopened) = {
1081 let writable = false;
1082 let accounts = Accounts::new(p.clone(), writable).await?;
1083 let ctx = accounts.get_selected_account().unwrap();
1084 assert_eq!(
1085 ctx.get_config(crate::config::Config::Addr).await?,
1086 Some("two@example.org".to_string())
1087 );
1088
1089 let ids = accounts.get_all();
1090 assert_eq!(ids.len(), 3);
1091
1092 let id0 = *ids.first().unwrap();
1093 let ctx = accounts.get_account(id0).unwrap();
1094 assert_eq!(
1095 ctx.get_config(crate::config::Config::Addr).await?,
1096 Some("one@example.org".to_string())
1097 );
1098
1099 let id1 = *ids.get(1).unwrap();
1100 let t = accounts.get_account(id1).unwrap();
1101 assert_eq!(
1102 t.get_config(crate::config::Config::Addr).await?,
1103 Some("two@example.org".to_string())
1104 );
1105
1106 let id2 = *ids.get(2).unwrap();
1107 let ctx = accounts.get_account(id2).unwrap();
1108 assert_eq!(
1109 ctx.get_config(crate::config::Config::Addr).await?,
1110 Some("three@example.org".to_string())
1111 );
1112
1113 (id0, id1, id2)
1114 };
1115 assert_eq!(id0, id0_reopened);
1116 assert_eq!(id1, id1_reopened);
1117 assert_eq!(id2, id2_reopened);
1118
1119 Ok(())
1120 }
1121
1122 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1123 async fn test_no_accounts_event_emitter() -> Result<()> {
1124 let dir = tempfile::tempdir().unwrap();
1125 let p: PathBuf = dir.path().join("accounts");
1126
1127 let writable = true;
1128 let accounts = Accounts::new(p.clone(), writable).await?;
1129
1130 assert_eq!(accounts.accounts.len(), 0);
1132
1133 let event_emitter = accounts.get_event_emitter();
1135
1136 let duration = std::time::Duration::from_millis(1);
1138 assert!(
1139 tokio::time::timeout(duration, event_emitter.recv())
1140 .await
1141 .is_err()
1142 );
1143
1144 drop(accounts);
1146 assert_eq!(event_emitter.recv().await, None);
1147
1148 Ok(())
1149 }
1150
1151 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1152 async fn test_encrypted_account() -> Result<()> {
1153 let dir = tempfile::tempdir().context("failed to create tempdir")?;
1154 let p: PathBuf = dir.path().join("accounts");
1155
1156 let writable = true;
1157 let mut accounts = Accounts::new(p.clone(), writable)
1158 .await
1159 .context("failed to create accounts manager")?;
1160
1161 assert_eq!(accounts.accounts.len(), 0);
1162 let account_id = accounts
1163 .add_closed_account()
1164 .await
1165 .context("failed to add closed account")?;
1166 let account = accounts
1167 .get_selected_account()
1168 .context("failed to get account")?;
1169 assert_eq!(account.id, account_id);
1170 let passphrase_set_success = account
1171 .open("foobar".to_string())
1172 .await
1173 .context("failed to set passphrase")?;
1174 assert!(passphrase_set_success);
1175 drop(accounts);
1176
1177 let writable = false;
1178 let accounts = Accounts::new(p.clone(), writable)
1179 .await
1180 .context("failed to create second accounts manager")?;
1181 let account = accounts
1182 .get_selected_account()
1183 .context("failed to get account")?;
1184 assert_eq!(account.is_open().await, false);
1185
1186 assert_eq!(account.open("barfoo".to_string()).await?, false);
1188 assert_eq!(account.open("".to_string()).await?, false);
1189
1190 assert_eq!(account.open("foobar".to_string()).await?, true);
1191 assert_eq!(account.is_open().await, true);
1192
1193 Ok(())
1194 }
1195
1196 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1198 async fn test_accounts_share_translations() -> Result<()> {
1199 let dir = tempfile::tempdir().unwrap();
1200 let p: PathBuf = dir.path().join("accounts");
1201
1202 let writable = true;
1203 let mut accounts = Accounts::new(p.clone(), writable).await?;
1204 accounts.add_account().await?;
1205 accounts.add_account().await?;
1206
1207 let account1 = accounts.get_account(1).context("failed to get account 1")?;
1208 let account2 = accounts.get_account(2).context("failed to get account 2")?;
1209
1210 assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
1211 assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
1212 account1
1213 .set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
1214 .await?;
1215 assert_eq!(stock_str::no_messages(&account1).await, "foobar");
1216 assert_eq!(stock_str::no_messages(&account2).await, "foobar");
1217
1218 Ok(())
1219 }
1220}