diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index edfbe3e66..3f863ab5b 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -6413,6 +6413,25 @@ void dc_event_unref(dc_event_t* event); #define DC_EVENT_CHATLIST_ITEM_CHANGED 2301 +/** + * 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. + */ + +#define DC_EVENT_ACCOUNTS_CHANGED 2302 + +/** + * Inform that an account property that might be shown in the account list changed, namely: + * - is_configured (see dc_is_configured()) + * - displayname + * - selfavatar + * - private_tag + * + * This event is emitted from the account whose property changed. + */ + +#define DC_EVENT_ACCOUNTS_ITEM_CHANGED 2303 /** * Inform that some events have been skipped due to event channel overflow. diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index bc9354559..090ffe359 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -569,6 +569,8 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int EventType::AccountsBackgroundFetchDone => 2200, EventType::ChatlistChanged => 2300, EventType::ChatlistItemChanged { .. } => 2301, + EventType::AccountsChanged => 2302, + EventType::AccountsItemChanged => 2303, EventType::EventChannelOverflow { .. } => 2400, #[allow(unreachable_patterns)] #[cfg(test)] @@ -601,8 +603,10 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc: | EventType::ConfigSynced { .. } | EventType::IncomingMsgBunch { .. } | EventType::ErrorSelfNotInGroup(_) - | EventType::AccountsBackgroundFetchDone => 0, - EventType::ChatlistChanged => 0, + | EventType::AccountsBackgroundFetchDone + | EventType::ChatlistChanged + | EventType::AccountsChanged + | EventType::AccountsItemChanged => 0, EventType::IncomingReaction { contact_id, .. } | EventType::IncomingWebxdcNotify { contact_id, .. } => contact_id.to_u32() as libc::c_int, EventType::MsgsChanged { chat_id, .. } @@ -676,6 +680,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc: | EventType::AccountsBackgroundFetchDone | EventType::ChatlistChanged | EventType::ChatlistItemChanged { .. } + | EventType::AccountsChanged + | EventType::AccountsItemChanged | EventType::ConfigSynced { .. } | EventType::ChatModified(_) | EventType::WebxdcRealtimeAdvertisementReceived { .. } @@ -751,6 +757,8 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut | EventType::IncomingMsgBunch { .. } | EventType::ChatlistItemChanged { .. } | EventType::ChatlistChanged + | EventType::AccountsChanged + | EventType::AccountsItemChanged | EventType::WebxdcRealtimeAdvertisementReceived { .. } | EventType::EventChannelOverflow { .. } => ptr::null_mut(), EventType::ConfigureProgress { comment, .. } => { diff --git a/deltachat-jsonrpc/src/api/types/events.rs b/deltachat-jsonrpc/src/api/types/events.rs index f7abb7a7e..254463d4d 100644 --- a/deltachat-jsonrpc/src/api/types/events.rs +++ b/deltachat-jsonrpc/src/api/types/events.rs @@ -285,6 +285,20 @@ pub enum EventType { #[serde(rename_all = "camelCase")] ChatlistItemChanged { chat_id: Option }, + /// 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 is_configured()) + /// - displayname + /// - selfavatar + /// - private_tag + /// + /// This event is emitted from the account whose property changed. + AccountsItemChanged, + /// Inform than some events have been skipped due to event channel overflow. EventChannelOverflow { n: u64 }, } @@ -426,6 +440,8 @@ impl From for EventType { }, CoreEventType::ChatlistChanged => ChatlistChanged, CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n }, + CoreEventType::AccountsChanged => AccountsChanged, + CoreEventType::AccountsItemChanged => AccountsItemChanged, #[allow(unreachable_patterns)] #[cfg(test)] _ => unreachable!("This is just to silence a rust_analyzer false-positive"), diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/const.py b/deltachat-rpc-client/src/deltachat_rpc_client/const.py index 55988d79c..f2542f0ee 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/const.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/const.py @@ -61,6 +61,8 @@ class EventType(str, Enum): WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted" CHATLIST_CHANGED = "ChatlistChanged" CHATLIST_ITEM_CHANGED = "ChatlistItemChanged" + ACCOUNTS_CHANGED = "AccountsChanged" + ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged" CONFIG_SYNCED = "ConfigSynced" WEBXDC_REALTIME_DATA = "WebxdcRealtimeData" WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived" diff --git a/deltachat-rpc-client/tests/test_account_events.py b/deltachat-rpc-client/tests/test_account_events.py new file mode 100644 index 000000000..4c4d5c447 --- /dev/null +++ b/deltachat-rpc-client/tests/test_account_events.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from deltachat_rpc_client import EventType + +if TYPE_CHECKING: + from deltachat_rpc_client.pytestplugin import ACFactory + + +def test_event_on_configuration(acfactory: ACFactory) -> None: + """ + Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure + """ + + account = acfactory.new_preconfigured_account() + account.clear_all_events() + assert not account.is_configured() + future = account.configure.future() + while True: + event = account.wait_for_event() + if event.kind == EventType.ACCOUNTS_ITEM_CHANGED: + break + assert account.is_configured() + + future() + + +# other tests are written in rust: src/tests/account_events.rs diff --git a/node/constants.js b/node/constants.js index e277e957d..c96684fa9 100644 --- a/node/constants.js +++ b/node/constants.js @@ -31,6 +31,8 @@ module.exports = { DC_DOWNLOAD_IN_PROGRESS: 1000, DC_DOWNLOAD_UNDECIPHERABLE: 30, DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200, + DC_EVENT_ACCOUNTS_CHANGED: 2302, + DC_EVENT_ACCOUNTS_ITEM_CHANGED: 2303, DC_EVENT_CHANNEL_OVERFLOW: 2400, DC_EVENT_CHATLIST_CHANGED: 2300, DC_EVENT_CHATLIST_ITEM_CHANGED: 2301, diff --git a/node/events.js b/node/events.js index b75bcfd95..a28c165d6 100644 --- a/node/events.js +++ b/node/events.js @@ -44,5 +44,7 @@ module.exports = { 2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE', 2300: 'DC_EVENT_CHATLIST_CHANGED', 2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED', + 2302: 'DC_EVENT_ACCOUNTS_CHANGED', + 2303: 'DC_EVENT_ACCOUNTS_ITEM_CHANGED', 2400: 'DC_EVENT_CHANNEL_OVERFLOW' } diff --git a/node/lib/constants.ts b/node/lib/constants.ts index 3971bdfaf..6c5801616 100644 --- a/node/lib/constants.ts +++ b/node/lib/constants.ts @@ -31,6 +31,8 @@ export enum C { DC_DOWNLOAD_IN_PROGRESS = 1000, DC_DOWNLOAD_UNDECIPHERABLE = 30, DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200, + DC_EVENT_ACCOUNTS_CHANGED = 2302, + DC_EVENT_ACCOUNTS_ITEM_CHANGED = 2303, DC_EVENT_CHANNEL_OVERFLOW = 2400, DC_EVENT_CHATLIST_CHANGED = 2300, DC_EVENT_CHATLIST_ITEM_CHANGED = 2301, @@ -353,5 +355,7 @@ export const EventId2EventName: { [key: number]: string } = { 2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE', 2300: 'DC_EVENT_CHATLIST_CHANGED', 2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED', + 2302: 'DC_EVENT_ACCOUNTS_CHANGED', + 2303: 'DC_EVENT_ACCOUNTS_ITEM_CHANGED', 2400: 'DC_EVENT_CHANNEL_OVERFLOW', } diff --git a/src/accounts.rs b/src/accounts.rs index 425139d71..4d18316b4 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -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(()) } diff --git a/src/config.rs b/src/config.rs index 46eae017a..65b1ba671 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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 }); } diff --git a/src/configure.rs b/src/configure.rs index c317f6819..0bbe71e8a 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -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, }, + /// 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, diff --git a/src/imex.rs b/src/imex.rs index 07b55df5b..a7c639fb7 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -426,6 +426,7 @@ async fn import_backup_stream_inner( 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) diff --git a/src/test_utils.rs b/src/test_utils.rs index 85d232ded..382d2e430 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -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 diff --git a/src/tests.rs b/src/tests.rs index 3b417c0d4..6e642dce7 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,2 +1,3 @@ +mod account_events; mod aeap; mod verified_chats; diff --git a/src/tests/account_events.rs b/src/tests/account_events.rs new file mode 100644 index 000000000..d9148f0b7 --- /dev/null +++ b/src/tests/account_events.rs @@ -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(()) +}