Add more detail to securejoin tests

This also checks that some of the correct user interactions happen,
checking we get a joiner event and the verified chat messages.

It also extends the test utils with the ability to distinguish the
different context logs by having them named.
This commit is contained in:
Floris Bruynooghe
2021-01-18 22:11:53 +01:00
parent 83dd1c6232
commit 687c92d738
4 changed files with 302 additions and 59 deletions

View File

@@ -64,7 +64,10 @@ pub struct InnerContext {
pub(crate) last_full_folder_scan: Mutex<Option<Instant>>,
/// 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,

View File

@@ -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")

View File

@@ -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,

View File

@@ -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<Box<dyn Future<Output = ()> + Send + 'static>> + Send + Sync + 'static;
/// Map of [`Context::id`] to names for [`TestContext`]s.
static CONTEXT_NAMES: Lazy<std::sync::RwLock<BTreeMap<u32, String>>> =
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<u32>,
/// Functions to call for events received.
event_sinks: Arc<RwLock<Vec<Box<EventSink>>>>,
}
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<String>) -> Self {
Self::new_named(Some(name.into())).await
}
async fn new_named(name: Option<String>) -> 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<RwLock<Vec<Box<EventSink>>>> = 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<String>) {
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<F, R>(&self, sink: F)
where
// Aka `F: EventSink` but type aliases are not allowed.
F: Fn(Event) -> R + Send + Sync + 'static,
R: Future<Output = ()> + 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<str>, msg: &Message) {
let contact = Contact::get_by_id(context, msg.get_from_id())
.await