feat: add AccountsChanged and AccountsItemChanged events (#6118)

- **feat: add `AccountsChanged` and `AccountsItemChanged` events**
- **emit event and add tests**

closes #6106

TODO:
- [x] test receiving synced config from second device
- [x] bug: investigate how to delay the configuration event until it is
actually configured - because desktop gets the event but still shows
account as if it was unconfigured, maybe event is emitted before the
value is written to the database?
- [x] update node bindings constants
This commit is contained in:
Simon Laux
2024-11-25 14:34:33 +01:00
committed by GitHub
parent 5db574b44f
commit a319c1ea27
16 changed files with 286 additions and 4 deletions

View File

@@ -139,6 +139,7 @@ impl Accounts {
ctx.open("".to_string()).await?;
self.accounts.insert(account_config.id, ctx);
self.emit_event(EventType::AccountsChanged);
Ok(account_config.id)
}
@@ -156,6 +157,7 @@ impl Accounts {
.build()
.await?;
self.accounts.insert(account_config.id, ctx);
self.emit_event(EventType::AccountsChanged);
Ok(account_config.id)
}
@@ -190,6 +192,7 @@ impl Accounts {
.context("failed to remove account data")?;
}
self.config.remove_account(id).await?;
self.emit_event(EventType::AccountsChanged);
Ok(())
}

View File

@@ -791,6 +791,12 @@ impl Context {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
}
if matches!(
key,
Config::Displayname | Config::Selfavatar | Config::PrivateTag
) {
self.emit_event(EventType::AccountsItemChanged);
}
if key.is_synced() {
self.emit_event(EventType::ConfigSynced { key });
}

View File

@@ -36,10 +36,10 @@ use crate::message::Message;
use crate::oauth2::get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::time;
use crate::{chat, e2ee, provider};
use crate::{stock_str, EventType};
use deltachat_contact_tools::addr_cmp;
macro_rules! progress {
@@ -486,6 +486,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
update_device_chats_handle.await??;
ctx.sql.set_raw_config_bool("configured", true).await?;
ctx.emit_event(EventType::AccountsItemChanged);
Ok(configured_param)
}

View File

@@ -344,6 +344,20 @@ pub enum EventType {
chat_id: Option<ChatId>,
},
/// Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
///
/// This event is only emitted by the account manager
AccountsChanged,
/// Inform that an account property that might be shown in the account list changed, namely:
/// - is_configured (see [crate::context::Context::is_configured])
/// - displayname
/// - selfavatar
/// - private_tag
///
/// This event is emitted from the account whose property changed.
AccountsItemChanged,
/// Event for using in tests, e.g. as a fence between normally generated events.
#[cfg(test)]
Test,

View File

@@ -426,6 +426,7 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
if res.is_ok() {
context.emit_event(EventType::ImexProgress(999));
res = context.sql.run_migrations(context).await;
context.emit_event(EventType::AccountsItemChanged);
}
if res.is_ok() {
delete_and_reset_all_device_msgs(context)

View File

@@ -391,7 +391,7 @@ impl TestContext {
Self {
ctx,
dir,
evtracker: EventTracker(evtracker_receiver),
evtracker: EventTracker::new(evtracker_receiver),
_log_sink,
}
}
@@ -1087,6 +1087,10 @@ impl DerefMut for EventTracker {
}
impl EventTracker {
pub fn new(emitter: EventEmitter) -> Self {
Self(emitter)
}
/// Consumes emitted events returning the first matching one.
///
/// If no matching events are ready this will wait for new events to arrive and time out

View File

@@ -1,2 +1,3 @@
mod account_events;
mod aeap;
mod verified_chats;

170
src/tests/account_events.rs Normal file
View File

@@ -0,0 +1,170 @@
//! contains tests for account (list) events
use std::time::Duration;
use anyhow::Result;
use tempfile::tempdir;
use crate::accounts::Accounts;
use crate::config::Config;
use crate::imex::{get_backup, has_backup, imex, BackupProvider, ImexMode};
use crate::test_utils::{sync, EventTracker, TestContext, TestContextManager};
use crate::EventType;
async fn wait_for_item_changed(context: &TestContext) {
context
.evtracker
.get_matching(|evt| matches!(evt, EventType::AccountsItemChanged))
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_account_event() -> Result<()> {
let dir = tempdir().unwrap();
let mut manager = Accounts::new(dir.path().join("accounts"), true).await?;
let tracker = EventTracker::new(manager.get_event_emitter());
// create account
tracker.clear_events();
let account_id = manager.add_account().await?;
tracker
.get_matching(|evt| matches!(evt, EventType::AccountsChanged))
.await;
// remove account
tracker.clear_events();
manager.remove_account(account_id).await?;
tracker
.get_matching(|evt| matches!(evt, EventType::AccountsChanged))
.await;
// create closed account
tracker.clear_events();
manager.add_closed_account().await?;
tracker
.get_matching(|evt| matches!(evt, EventType::AccountsChanged))
.await;
Ok(())
}
// configuration is tested by python tests in deltachat-rpc-client/tests/test_account_events.py
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_displayname() -> Result<()> {
let mut tcm = TestContextManager::new();
let context = tcm.alice().await;
context.evtracker.clear_events();
context
.set_config(crate::config::Config::Displayname, Some("🐰 Alice"))
.await?;
wait_for_item_changed(&context).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_selfavatar() -> Result<()> {
let mut tcm = TestContextManager::new();
let context = tcm.alice().await;
let file = context.dir.path().join("avatar.jpg");
let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
tokio::fs::write(&file, bytes).await?;
context.evtracker.clear_events();
context
.set_config(
crate::config::Config::Selfavatar,
Some(file.to_str().unwrap()),
)
.await?;
wait_for_item_changed(&context).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_private_tag() -> Result<()> {
let mut tcm = TestContextManager::new();
let context = tcm.alice().await;
context.evtracker.clear_events();
context
.set_config(crate::config::Config::PrivateTag, Some("Wonderland"))
.await?;
wait_for_item_changed(&context).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_import_backup() -> Result<()> {
let mut tcm = TestContextManager::new();
let context1 = tcm.alice().await;
let backup_dir = tempfile::tempdir().unwrap();
assert!(
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
.await
.is_ok()
);
let context2 = TestContext::new().await;
assert!(!context2.is_configured().await?);
context2.evtracker.clear_events();
let backup = has_backup(&context2, backup_dir.path()).await?;
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None).await?;
assert!(context2.is_configured().await?);
wait_for_item_changed(&context2).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_receive_backup() {
let mut tcm = TestContextManager::new();
// Create first device.
let ctx0 = tcm.alice().await;
// Prepare to transfer backup.
let provider = BackupProvider::prepare(&ctx0).await.unwrap();
// Set up second device.
let ctx1 = tcm.unconfigured().await;
ctx1.evtracker.clear_events();
get_backup(&ctx1, provider.qr()).await.unwrap();
// Make sure the provider finishes without an error.
tokio::time::timeout(Duration::from_secs(30), provider)
.await
.expect("timed out")
.expect("error in provider");
wait_for_item_changed(&ctx1).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync() -> Result<()> {
let alice0 = TestContext::new_alice().await;
let alice1 = TestContext::new_alice().await;
for a in [&alice0, &alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let new_name = "new name";
alice0
.set_config(Config::Displayname, Some(new_name))
.await?;
alice1.evtracker.clear_events();
sync(&alice0, &alice1).await;
wait_for_item_changed(&alice1).await;
assert_eq!(
alice1.get_config(Config::Displayname).await?,
Some(new_name.to_owned())
);
assert!(alice0.get_config(Config::Selfavatar).await?.is_none());
let file = alice0.dir.path().join("avatar.png");
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
alice0
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
alice1.evtracker.clear_events();
sync(&alice0, &alice1).await;
wait_for_item_changed(&alice1).await;
Ok(())
}