diff --git a/src/context.rs b/src/context.rs index b0d6f3f6d..2dc0ed3bd 100644 --- a/src/context.rs +++ b/src/context.rs @@ -64,7 +64,10 @@ pub struct InnerContext { pub(crate) last_full_folder_scan: Mutex>, - /// Id for this context on the current device. + /// ID for this `Context` in the current process. + /// + /// This allows for multiple `Context`s open in a single process where each context can + /// be identified by this ID. pub(crate) id: u32, creation_time: SystemTime, diff --git a/src/events.rs b/src/events.rs index 643fab3a2..03ec0298b 100644 --- a/src/events.rs +++ b/src/events.rs @@ -73,9 +73,25 @@ impl async_std::stream::Stream for EventEmitter { } } +/// The event emitted by a [`Context`] from an [`EventEmitter`]. +/// +/// Events are documented on the C/FFI API in `deltachat.h` as `DC_EVENT_*` contants. The +/// context emits them in relation to various operations happening, a lot of these are again +/// documented in `deltachat.h`. +/// +/// This struct [`Deref`]s to the [`EventType`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Event { + /// The ID of the [`Context`] which emitted this event. + /// + /// This allows using multiple [`Context`]s in a single process as they are identified + /// by this ID. + /// + /// [`Context`]: crate::context::Context pub id: u32, + /// The event payload. + /// + /// These are documented in `deltachat.h` as the `DC_EVENT_*` constants. pub typ: EventType, } @@ -88,7 +104,9 @@ impl Deref for Event { } impl EventType { - /// Returns the corresponding Event id. + /// Returns the corresponding Event ID. + /// + /// These are the IDs used in the `DC_EVENT_*` constants in `deltachat.h`. pub fn as_id(&self) -> i32 { self.get_str("id") .expect("missing id") diff --git a/src/securejoin.rs b/src/securejoin.rs index e52749244..4a1b952f8 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -1103,8 +1103,11 @@ fn encrypted_and_signed( mod tests { use super::*; + use async_std::prelude::*; + use crate::chat; use crate::chat::ProtectionStatus; + use crate::events::Event; use crate::peerstate::Peerstate; use crate::test_utils::TestContext; @@ -1113,12 +1116,24 @@ mod tests { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; - // Generate QR-code, ChatId(0) indicates setup-contact + // Setup JoinerProgress sinks. + let (joiner_progress_tx, joiner_progress_rx) = async_std::sync::channel(100); + bob.add_event_sink(move |event: Event| { + let joiner_progress_tx = joiner_progress_tx.clone(); + async move { + if let EventType::SecurejoinJoinerProgress { .. } = event.typ { + joiner_progress_tx.try_send(event).unwrap(); + } + } + }) + .await; + + // Step 1: Generate QR-code, ChatId(0) indicates setup-contact let qr = dc_get_securejoin_qr(&alice.ctx, ChatId::new(0)) .await .unwrap(); - // Bob scans QR-code, sends vc-request + // Step 2: Bob scans QR-code, sends vc-request dc_join_securejoin(&bob.ctx, &qr).await.unwrap(); let sent = bob.pop_sent_msg().await; @@ -1128,7 +1143,7 @@ mod tests { assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vc-request"); assert!(msg.get(HeaderDef::SecureJoinInvitenumber).is_some()); - // Alice receives vc-request, sends vc-auth-required + // Step 3: Alice receives vc-request, sends vc-auth-required alice.recv_msg(&sent).await; let sent = alice.pop_sent_msg().await; @@ -1136,9 +1151,33 @@ mod tests { assert!(msg.was_encrypted()); assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vc-auth-required"); - // Bob receives vc-auth-required, sends vc-request-with-auth + // Step 4: Bob receives vc-auth-required, sends vc-request-with-auth bob.recv_msg(&sent).await; + // Check Bob emitted the JoinerProgress event. + { + let evt = joiner_progress_rx + .recv() + .timeout(Duration::from_secs(10)) + .await + .expect("timeout waiting for JoinerProgress event") + .expect("missing JoinerProgress event"); + match evt.typ { + EventType::SecurejoinJoinerProgress { + contact_id, + progress, + } => { + let alice_contact_id = + Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown) + .await; + assert_eq!(contact_id, alice_contact_id); + assert_eq!(progress, 400); + } + _ => panic!("Wrong event type"), + } + } + + // Check Bob sent the right message. let sent = bob.pop_sent_msg().await; let msg = alice.parse_msg(&sent).await; assert!(msg.was_encrypted()); @@ -1167,13 +1206,32 @@ mod tests { VerifiedStatus::Unverified ); - // Alice receives vc-request-with-auth, sends vc-contact-confirm + // Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm alice.recv_msg(&sent).await; assert_eq!( contact_bob.is_verified(&alice.ctx).await, VerifiedStatus::BidirectVerified ); + // Check Alice got the verified message in her 1:1 chat. + { + let chat = alice.create_chat(&bob).await; + let msg_id = chat::get_chat_msgs(&alice.ctx, chat.get_id(), 0x1, None) + .await + .into_iter() + .filter_map(|item| match item { + chat::ChatItem::Message { msg_id } => Some(msg_id), + _ => None, + }) + .max() + .expect("No messages in Alice's 1:1 chat"); + let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap(); + assert!(msg.is_info()); + let text = msg.get_text().unwrap(); + assert!(text.contains("bob@example.net verified")); + } + + // Check Alice sent the right message to Bob. let sent = alice.pop_sent_msg().await; let msg = bob.parse_msg(&sent).await; assert!(msg.was_encrypted()); @@ -1193,13 +1251,32 @@ mod tests { VerifiedStatus::Unverified ); - // Bob receives vc-contact-confirm, sends vc-contact-confirm-received + // Step 7: Bob receives vc-contact-confirm, sends vc-contact-confirm-received bob.recv_msg(&sent).await; assert_eq!( contact_alice.is_verified(&bob.ctx).await, VerifiedStatus::BidirectVerified ); + // Check Bob got the verified message in his 1:1 chat. + { + let chat = bob.create_chat(&alice).await; + let msg_id = chat::get_chat_msgs(&bob.ctx, chat.get_id(), 0x1, None) + .await + .into_iter() + .filter_map(|item| match item { + chat::ChatItem::Message { msg_id } => Some(msg_id), + _ => None, + }) + .max() + .expect("No messages in Bob's 1:1 chat"); + let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap(); + assert!(msg.is_info()); + let text = msg.get_text().unwrap(); + assert!(text.contains("alice@example.com verified")); + } + + // Check Bob sent the final message let sent = bob.pop_sent_msg().await; let msg = alice.parse_msg(&sent).await; assert!(msg.was_encrypted()); @@ -1221,6 +1298,18 @@ mod tests { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; + // Setup JoinerProgress sinks. + let (joiner_progress_tx, joiner_progress_rx) = async_std::sync::channel(100); + bob.add_event_sink(move |event: Event| { + let joiner_progress_tx = joiner_progress_tx.clone(); + async move { + if let EventType::SecurejoinJoinerProgress { .. } = event.typ { + joiner_progress_tx.try_send(event).unwrap(); + } + } + }) + .await; + // Ensure Bob knows Alice_FP let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await.unwrap(); let peerstate = Peerstate { @@ -1241,14 +1330,38 @@ mod tests { }; peerstate.save_to_db(&bob.ctx.sql, true).await.unwrap(); - // Generate QR-code, ChatId(0) indicates setup-contact + // Step 1: Generate QR-code, ChatId(0) indicates setup-contact let qr = dc_get_securejoin_qr(&alice.ctx, ChatId::new(0)) .await .unwrap(); - // Bob scans QR-code, sends vc-request-with-auth, skipping vc-request + // Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request dc_join_securejoin(&bob.ctx, &qr).await.unwrap(); + // Check Bob emitted the JoinerProgress event. + { + let evt = joiner_progress_rx + .recv() + .timeout(Duration::from_secs(10)) + .await + .expect("timeout waiting for JoinerProgress event") + .expect("missing JoinerProgress event"); + match evt.typ { + EventType::SecurejoinJoinerProgress { + contact_id, + progress, + } => { + let alice_contact_id = + Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown) + .await; + assert_eq!(contact_id, alice_contact_id); + assert_eq!(progress, 400); + } + _ => panic!("Wrong event type"), + } + } + + // Check Bob sent the right handshake message. let sent = bob.pop_sent_msg().await; let msg = alice.parse_msg(&sent).await; assert!(msg.was_encrypted()); @@ -1283,7 +1396,7 @@ mod tests { VerifiedStatus::Unverified ); - // Alice receives vc-request-with-auth, sends vc-contact-confirm + // Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm alice.recv_msg(&sent).await; assert_eq!( contact_bob.is_verified(&alice.ctx).await, @@ -1309,7 +1422,7 @@ mod tests { VerifiedStatus::Unverified ); - // Bob receives vc-contact-confirm, sends vc-contact-confirm-received + // Step 7: Bob receives vc-contact-confirm, sends vc-contact-confirm-received bob.recv_msg(&sent).await; assert_eq!( contact_alice.is_verified(&bob.ctx).await, @@ -1330,14 +1443,26 @@ mod tests { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; + // Setup JoinerProgress sinks. + let (joiner_progress_tx, joiner_progress_rx) = async_std::sync::channel(100); + bob.add_event_sink(move |event: Event| { + let joiner_progress_tx = joiner_progress_tx.clone(); + async move { + if let EventType::SecurejoinJoinerProgress { .. } = event.typ { + joiner_progress_tx.try_send(event).unwrap(); + } + } + }) + .await; + let chatid = chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat") .await .unwrap(); - // Generate QR-code, secure-join implied by chatid + // Step 1: Generate QR-code, secure-join implied by chatid let qr = dc_get_securejoin_qr(&alice.ctx, chatid).await.unwrap(); - // Bob scans QR-code, sends vg-request; blocks on ongoing process + // Step 2: Bob scans QR-code, sends vg-request; blocks on ongoing process let joiner = { let qr = qr.clone(); let ctx = bob.ctx.clone(); @@ -1351,7 +1476,7 @@ mod tests { assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vg-request"); assert!(msg.get(HeaderDef::SecureJoinInvitenumber).is_some()); - // Alice receives vg-request, sends vg-auth-required + // Step 3: Alice receives vg-request, sends vg-auth-required alice.recv_msg(&sent).await; let sent = alice.pop_sent_msg().await; @@ -1359,9 +1484,34 @@ mod tests { assert!(msg.was_encrypted()); assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vg-auth-required"); - // Bob receives vg-auth-required, sends vg-request-with-auth + // Step 4: Bob receives vg-auth-required, sends vg-request-with-auth bob.recv_msg(&sent).await; let sent = bob.pop_sent_msg().await; + + // Check Bob emitted the JoinerProgress event. + { + let evt = joiner_progress_rx + .recv() + .timeout(Duration::from_secs(10)) + .await + .expect("timeout waiting for JoinerProgress event") + .expect("missing JoinerProgress event"); + match evt.typ { + EventType::SecurejoinJoinerProgress { + contact_id, + progress, + } => { + let alice_contact_id = + Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown) + .await; + assert_eq!(contact_id, alice_contact_id); + assert_eq!(progress, 400); + } + _ => panic!("Wrong event type"), + } + } + + // Check Bob sent the right handshake message. let msg = alice.parse_msg(&sent).await; assert!(msg.was_encrypted()); assert_eq!( @@ -1389,7 +1539,7 @@ mod tests { VerifiedStatus::Unverified ); - // Alice receives vg-request-with-auth, sends vg-member-added + // Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added alice.recv_msg(&sent).await; assert_eq!( contact_bob.is_verified(&alice.ctx).await, @@ -1412,7 +1562,7 @@ mod tests { VerifiedStatus::Unverified ); - // Bob receives vg-member-added, sends vg-member-added-received + // Step 7: Bob receives vg-member-added, sends vg-member-added-received bob.recv_msg(&sent).await; assert_eq!( contact_alice.is_verified(&bob.ctx).await, diff --git a/src/test_utils.rs b/src/test_utils.rs index fc1e813e5..dd7013c0c 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -2,50 +2,61 @@ //! //! This module is only compiled for test runs. +use std::collections::BTreeMap; +use std::ops::Deref; +use std::str::FromStr; use std::time::{Duration, Instant}; -use std::{ops::Deref, str::FromStr}; use ansi_term::Color; +use async_std::future::Future; use async_std::path::PathBuf; -use async_std::sync::RwLock; +use async_std::pin::Pin; +use async_std::sync::{Arc, RwLock}; +use once_cell::sync::Lazy; use tempfile::{tempdir, TempDir}; +use crate::chat::{self, Chat, ChatId, ChatItem}; +use crate::chatlist::Chatlist; use crate::config::Config; use crate::constants::DC_CONTACT_ID_SELF; -use crate::contact::Contact; +use crate::contact::{Contact, Origin}; use crate::context::Context; use crate::dc_receive_imf::dc_receive_imf; use crate::dc_tools::EmailAddress; +use crate::events::{Event, EventType}; use crate::job::Action; use crate::key::{self, DcKey}; use crate::message::{update_msg_state, Message, MessageState, MsgId}; use crate::mimeparser::MimeMessage; use crate::param::{Param, Params}; -use crate::{chat, chatlist::Chatlist}; -use crate::{chat::ChatItem, EventType}; -use crate::{ - chat::{Chat, ChatId}, - contact::Origin, -}; use crate::constants::Viewtype; use crate::constants::DC_MSG_ID_DAYMARKER; use crate::constants::DC_MSG_ID_MARKER1; +type EventSink = + dyn Fn(Event) -> Pin + Send + 'static>> + Send + Sync + 'static; + +/// Map of [`Context::id`] to names for [`TestContext`]s. +static CONTEXT_NAMES: Lazy>> = + Lazy::new(|| std::sync::RwLock::new(BTreeMap::new())); + /// A Context and temporary directory. /// /// The temporary directory can be used to store the SQLite database, /// see e.g. [test_context] which does this. -#[derive(Debug)] +// #[derive(Debug)] pub(crate) struct TestContext { pub ctx: Context, pub dir: TempDir, /// Counter for fake IMAP UIDs in [recv_msg], for private use in that function only. recv_idx: RwLock, + /// Functions to call for events received. + event_sinks: Arc>>>, } impl TestContext { - /// Create a new [TestContext]. + /// Creates a new [TestContext]. /// /// The [Context] will be created and have an SQLite database named "db.sqlite" in the /// [TestContext.dir] directory. This directory is cleaned up when the [TestContext] is @@ -53,25 +64,48 @@ impl TestContext { /// /// [Context]: crate::context::Context pub async fn new() -> Self { + Self::new_named(None).await + } + + /// Creates a new [`TestContext`] with a set name used in event logging. + pub async fn with_name(name: impl Into) -> Self { + Self::new_named(Some(name.into())).await + } + + async fn new_named(name: Option) -> Self { use rand::Rng; let dir = tempdir().unwrap(); let dbfile = dir.path().join("db.sqlite"); let id = rand::thread_rng().gen(); + if let Some(name) = name { + let mut context_names = CONTEXT_NAMES.write().unwrap(); + context_names.insert(id, name); + } let ctx = Context::new("FakeOS".into(), dbfile.into(), id) .await .unwrap(); let events = ctx.get_event_emitter(); + let event_sinks: Arc>>> = Arc::new(RwLock::new(Vec::new())); + let sinks = Arc::clone(&event_sinks); async_std::task::spawn(async move { while let Some(event) = events.recv().await { - receive_event(event.typ); + { + let sinks = sinks.read().await; + for sink in sinks.iter() { + sink(event.clone()).await; + } + } + receive_event(event); } }); + Self { ctx, dir, recv_idx: RwLock::new(0), + event_sinks, } } @@ -80,7 +114,7 @@ impl TestContext { /// This is a shortcut which automatically calls [TestContext::configure_alice] after /// creating the context. pub async fn new_alice() -> Self { - let t = Self::new().await; + let t = Self::with_name("alice").await; t.configure_alice().await; t } @@ -89,7 +123,7 @@ impl TestContext { /// /// This is a shortcut which configures bob@example.net with a fixed key. pub async fn new_bob() -> Self { - let t = Self::new().await; + let t = Self::with_name("bob").await; let keypair = bob_keypair(); t.configure_addr(&keypair.addr.to_string()).await; key::store_self_keypair(&t, &keypair, key::KeyPairUse::Default) @@ -98,6 +132,31 @@ impl TestContext { t } + /// Sets a name for this [`TestContext`] if one isn't yet set. + /// + /// This will show up in events logged in the test output. + pub fn set_name(&self, name: impl Into) { + let mut context_names = CONTEXT_NAMES.write().unwrap(); + context_names + .entry(self.ctx.get_id()) + .or_insert_with(|| name.into()); + } + + /// Add a new callback which will receive events. + /// + /// The test context runs an async task receiving all events from the [`Context`], which + /// are logged to stdout. This allows you to register additional callbacks which will + /// receive all events in case your tests need to watch for a specific event. + pub async fn add_event_sink(&self, sink: F) + where + // Aka `F: EventSink` but type aliases are not allowed. + F: Fn(Event) -> R + Send + Sync + 'static, + R: Future + Send + 'static, + { + let mut sinks = self.event_sinks.write().await; + sinks.push(Box::new(move |evt| Box::pin(sink(evt)))); + } + /// Configure with alice@example.com. /// /// The context will be fake-configured as the alice user, with a pre-generated secret @@ -125,6 +184,9 @@ impl TestContext { .set_config(Config::Configured, Some("1")) .await .unwrap(); + if let Some(name) = addr.split('@').next() { + self.set_name(name); + } } /// Retrieve a sent message from the jobs table. @@ -390,95 +452,105 @@ pub(crate) fn bob_keypair() -> key::KeyPair { } } -fn receive_event(event: EventType) { +/// Pretty-print an event to stdout +/// +/// Done during tests this is captured by `cargo test` and associated with the test itself. +fn receive_event(event: Event) { let green = Color::Green.normal(); let yellow = Color::Yellow.normal(); let red = Color::Red.normal(); - match event { + let msg = match event.typ { EventType::Info(msg) => { - /* do not show the event as this would fill the screen */ - println!("{}", msg); + format!("INFO: {}", msg) } EventType::SmtpConnected(msg) => { - println!("[SMTP_CONNECTED] {}", msg); + format!("[SMTP_CONNECTED] {}", msg) } EventType::ImapConnected(msg) => { - println!("[IMAP_CONNECTED] {}", msg); + format!("[IMAP_CONNECTED] {}", msg) } EventType::SmtpMessageSent(msg) => { - println!("[SMTP_MESSAGE_SENT] {}", msg); + format!("[SMTP_MESSAGE_SENT] {}", msg) } EventType::Warning(msg) => { - println!("{}", yellow.paint(msg)); + format!("WARN: {}", yellow.paint(msg)) } EventType::Error(msg) => { - println!("{}", red.paint(msg)); + format!("ERROR: {}", red.paint(msg)) } EventType::ErrorNetwork(msg) => { - println!("{}", red.paint(format!("[NETWORK] msg={}", msg))); + format!("{}", red.paint(format!("[NETWORK] msg={}", msg))) } EventType::ErrorSelfNotInGroup(msg) => { - println!("{}", red.paint(format!("[SELF_NOT_IN_GROUP] {}", msg))); + format!("{}", red.paint(format!("[SELF_NOT_IN_GROUP] {}", msg))) } EventType::MsgsChanged { chat_id, msg_id } => { - println!( + format!( "{}", green.paint(format!( "Received MSGS_CHANGED(chat_id={}, msg_id={})", chat_id, msg_id, )) - ); + ) } EventType::ContactsChanged(_) => { - println!("{}", green.paint("Received CONTACTS_CHANGED()")); + format!("{}", green.paint("Received CONTACTS_CHANGED()")) } EventType::LocationChanged(contact) => { - println!( + format!( "{}", green.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact)) - ); + ) } EventType::ConfigureProgress { progress, comment } => { if let Some(comment) = comment { - println!( + format!( "{}", green.paint(format!( "Received CONFIGURE_PROGRESS({} ‰, {})", progress, comment )) - ); + ) } else { - println!( + format!( "{}", green.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress)) - ); + ) } } EventType::ImexProgress(progress) => { - println!( + format!( "{}", green.paint(format!("Received IMEX_PROGRESS({} ‰)", progress)) - ); + ) } EventType::ImexFileWritten(file) => { - println!( + format!( "{}", green.paint(format!("Received IMEX_FILE_WRITTEN({})", file.display())) - ); + ) } EventType::ChatModified(chat) => { - println!( + format!( "{}", green.paint(format!("Received CHAT_MODIFIED({})", chat)) - ); + ) } _ => { - println!("Received {:?}", event); + format!("Received {:?}", event) } + }; + let context_names = CONTEXT_NAMES.read().unwrap(); + match context_names.get(&event.id) { + Some(ref name) => println!("{} {}", name, msg), + None => println!("{} {}", event.id, msg), } } +/// Logs and individual message to stdout. +/// +/// This includes a bunch of the message meta-data as well. async fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) { let contact = Contact::get_by_id(context, msg.get_from_id()) .await