mirror of
https://github.com/chatmail/core.git
synced 2026-04-18 05:56:31 +03:00
Not sure if that's the way to go because, you know, merge conflicts, but we could do this now.
312 lines
10 KiB
Rust
312 lines
10 KiB
Rust
//! Utilities to help writing tests.
|
|
//!
|
|
//! This module is only compiled for test runs.
|
|
|
|
use std::time::{Duration, Instant};
|
|
use std::{ops::Deref, str::FromStr};
|
|
|
|
use async_std::path::PathBuf;
|
|
use async_std::sync::RwLock;
|
|
use tempfile::{tempdir, TempDir};
|
|
|
|
use crate::context::Context;
|
|
use crate::dc_receive_imf::dc_receive_imf;
|
|
use crate::dc_tools::EmailAddress;
|
|
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, contact::Contact};
|
|
use crate::{
|
|
chat::{Chat, ChatId, ChatItem},
|
|
contact::Origin,
|
|
};
|
|
use crate::{config::Config, constants::DC_CONTACT_ID_SELF};
|
|
|
|
/// 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)]
|
|
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>,
|
|
}
|
|
|
|
impl TestContext {
|
|
/// Create 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
|
|
/// dropped.
|
|
///
|
|
/// [Context]: crate::context::Context
|
|
pub async fn new() -> Self {
|
|
use rand::Rng;
|
|
|
|
let dir = tempdir().unwrap();
|
|
let dbfile = dir.path().join("db.sqlite");
|
|
let id = rand::thread_rng().gen();
|
|
let ctx = Context::new("FakeOS".into(), dbfile.into(), id)
|
|
.await
|
|
.unwrap();
|
|
Self {
|
|
ctx,
|
|
dir,
|
|
recv_idx: RwLock::new(0),
|
|
}
|
|
}
|
|
|
|
/// Create a new configured [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;
|
|
t.configure_alice().await;
|
|
t
|
|
}
|
|
|
|
/// Create a new configured [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 keypair = bob_keypair();
|
|
t.configure_addr(&keypair.addr.to_string()).await;
|
|
key::store_self_keypair(&t, &keypair, key::KeyPairUse::Default)
|
|
.await
|
|
.expect("Failed to save Bob's key");
|
|
t
|
|
}
|
|
|
|
/// Configure with alice@example.com.
|
|
///
|
|
/// The context will be fake-configured as the alice user, with a pre-generated secret
|
|
/// key. The email address of the user is returned as a string.
|
|
pub async fn configure_alice(&self) -> String {
|
|
let keypair = alice_keypair();
|
|
self.configure_addr(&keypair.addr.to_string()).await;
|
|
key::store_self_keypair(&self.ctx, &keypair, key::KeyPairUse::Default)
|
|
.await
|
|
.expect("Failed to save Alice's key");
|
|
keypair.addr.to_string()
|
|
}
|
|
|
|
/// Configure as a given email address.
|
|
///
|
|
/// The context will be configured but the key will not be pre-generated so if a key is
|
|
/// used the fingerprint will be different every time.
|
|
pub async fn configure_addr(&self, addr: &str) {
|
|
self.ctx.set_config(Config::Addr, Some(addr)).await.unwrap();
|
|
self.ctx
|
|
.set_config(Config::ConfiguredAddr, Some(addr))
|
|
.await
|
|
.unwrap();
|
|
self.ctx
|
|
.set_config(Config::Configured, Some("1"))
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
/// Retrieve a sent message from the jobs table.
|
|
///
|
|
/// This retrieves and removes a message which has been scheduled to send from the jobs
|
|
/// table. Messages are returned in the order they have been sent.
|
|
///
|
|
/// Panics if there is no message or on any error.
|
|
pub async fn pop_sent_msg(&self) -> SentMessage {
|
|
let start = Instant::now();
|
|
let (rowid, foreign_id, raw_params) = loop {
|
|
let row = self
|
|
.ctx
|
|
.sql
|
|
.query_row(
|
|
r#"
|
|
SELECT id, foreign_id, param
|
|
FROM jobs
|
|
WHERE action=?
|
|
ORDER BY desired_timestamp;
|
|
"#,
|
|
paramsv![Action::SendMsgToSmtp],
|
|
|row| {
|
|
let id: i64 = row.get(0)?;
|
|
let foreign_id: i64 = row.get(1)?;
|
|
let param: String = row.get(2)?;
|
|
Ok((id, foreign_id, param))
|
|
},
|
|
)
|
|
.await;
|
|
if let Ok(row) = row {
|
|
break row;
|
|
}
|
|
if start.elapsed() < Duration::from_secs(3) {
|
|
async_std::task::sleep(Duration::from_millis(100)).await;
|
|
} else {
|
|
panic!("no sent message found in jobs table");
|
|
}
|
|
};
|
|
let id = MsgId::new(foreign_id as u32);
|
|
let params = Params::from_str(&raw_params).unwrap();
|
|
let blob_path = params
|
|
.get_blob(Param::File, &self.ctx, false)
|
|
.await
|
|
.expect("failed to parse blob from param")
|
|
.expect("no Param::File found in Params")
|
|
.to_abs_path();
|
|
self.ctx
|
|
.sql
|
|
.execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid])
|
|
.await
|
|
.expect("failed to remove job");
|
|
update_msg_state(&self.ctx, id, MessageState::OutDelivered).await;
|
|
SentMessage { params, blob_path }
|
|
}
|
|
|
|
/// Parse a message.
|
|
///
|
|
/// Parsing a message does not run the entire receive pipeline, but is not without
|
|
/// side-effects either. E.g. if the message includes autocrypt headers the relevant
|
|
/// peerstates will be updated. Later receiving the message using [recv_msg] is
|
|
/// unlikely to be affected as the peerstate would be processed again in exactly the
|
|
/// same way.
|
|
pub async fn parse_msg(&self, msg: &SentMessage) -> MimeMessage {
|
|
MimeMessage::from_bytes(&self.ctx, msg.payload().as_bytes())
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
/// Receive a message.
|
|
///
|
|
/// Receives a message using the `dc_receive_imf()` pipeline.
|
|
pub async fn recv_msg(&self, msg: &SentMessage) {
|
|
let mut idx = self.recv_idx.write().await;
|
|
*idx += 1;
|
|
dc_receive_imf(&self.ctx, msg.payload().as_bytes(), "INBOX", *idx, false)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
/// Get the most recent message of a chat.
|
|
///
|
|
/// Panics on errors or if the most recent message is a marker.
|
|
pub async fn get_last_msg(&self, chat_id: ChatId) -> Message {
|
|
let msgs = chat::get_chat_msgs(&self.ctx, chat_id, 0, None).await;
|
|
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
|
|
msg_id
|
|
} else {
|
|
panic!("Wrong item type");
|
|
};
|
|
Message::load_from_db(&self.ctx, *msg_id).await.unwrap()
|
|
}
|
|
|
|
pub async fn create_chat(&self, other: &TestContext) -> Chat {
|
|
let (contact_id, _modified) = Contact::add_or_lookup(
|
|
self,
|
|
other
|
|
.ctx
|
|
.get_config(Config::Displayname)
|
|
.await
|
|
.unwrap_or_default(),
|
|
other.ctx.get_config(Config::ConfiguredAddr).await.unwrap(),
|
|
Origin::ManuallyCreated,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let chat_id = chat::create_by_contact_id(self, contact_id).await.unwrap();
|
|
Chat::load_from_db(self, chat_id).await.unwrap()
|
|
}
|
|
|
|
pub async fn chat_with_contact(&self, name: &str, addr: &str) -> Chat {
|
|
let contact = Contact::create(self, name, addr)
|
|
.await
|
|
.expect("failed to create contact");
|
|
let chat_id = chat::create_by_contact_id(self, contact).await.unwrap();
|
|
Chat::load_from_db(self, chat_id).await.unwrap()
|
|
}
|
|
|
|
pub async fn get_self_chat(&self) -> Chat {
|
|
let chat_id = chat::create_by_contact_id(self, DC_CONTACT_ID_SELF)
|
|
.await
|
|
.unwrap();
|
|
Chat::load_from_db(self, chat_id).await.unwrap()
|
|
}
|
|
}
|
|
|
|
impl Deref for TestContext {
|
|
type Target = Context;
|
|
|
|
fn deref(&self) -> &Context {
|
|
&self.ctx
|
|
}
|
|
}
|
|
|
|
/// A raw message as it was scheduled to be sent.
|
|
///
|
|
/// This is a raw message, probably in the shape DC was planning to send it but not having
|
|
/// passed through a SMTP-IMAP pipeline.
|
|
#[derive(Debug, Clone)]
|
|
pub struct SentMessage {
|
|
params: Params,
|
|
blob_path: PathBuf,
|
|
}
|
|
|
|
impl SentMessage {
|
|
/// A recipient the message was destined for.
|
|
///
|
|
/// If there are multiple recipients this is just a random one, so is not very useful.
|
|
pub fn recipient(&self) -> EmailAddress {
|
|
let raw = self
|
|
.params
|
|
.get(Param::Recipients)
|
|
.expect("no recipients in params");
|
|
let rcpt = raw.split(' ').next().expect("no recipient found");
|
|
rcpt.parse().expect("failed to parse email address")
|
|
}
|
|
|
|
/// The raw message payload.
|
|
pub fn payload(&self) -> String {
|
|
std::fs::read_to_string(&self.blob_path).unwrap()
|
|
}
|
|
}
|
|
|
|
/// Load a pre-generated keypair for alice@example.com from disk.
|
|
///
|
|
/// This saves CPU cycles by avoiding having to generate a key.
|
|
///
|
|
/// The keypair was created using the crate::key::tests::gen_key test.
|
|
pub(crate) fn alice_keypair() -> key::KeyPair {
|
|
let addr = EmailAddress::new("alice@example.com").unwrap();
|
|
let public =
|
|
key::SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
|
|
.unwrap();
|
|
let secret =
|
|
key::SignedSecretKey::from_base64(include_str!("../test-data/key/alice-secret.asc"))
|
|
.unwrap();
|
|
key::KeyPair {
|
|
addr,
|
|
public,
|
|
secret,
|
|
}
|
|
}
|
|
|
|
/// Load a pre-generated keypair for bob@example.net from disk.
|
|
///
|
|
/// Like [alice_keypair] but a different key and identity.
|
|
pub(crate) fn bob_keypair() -> key::KeyPair {
|
|
let addr = EmailAddress::new("bob@example.net").unwrap();
|
|
let public =
|
|
key::SignedPublicKey::from_base64(include_str!("../test-data/key/bob-public.asc")).unwrap();
|
|
let secret =
|
|
key::SignedSecretKey::from_base64(include_str!("../test-data/key/bob-secret.asc")).unwrap();
|
|
key::KeyPair {
|
|
addr,
|
|
public,
|
|
secret,
|
|
}
|
|
}
|