Email/changes tests passing
This commit is contained in:
@@ -20,6 +20,42 @@
|
||||
"sentAt": "1998-08-13T07:42:41Z",
|
||||
"bodyStructure": {
|
||||
"headers": [
|
||||
{
|
||||
"name": "Return-Path",
|
||||
"value": " <president@whitehouse.gov>"
|
||||
},
|
||||
{
|
||||
"name": "Received",
|
||||
"value": " from mailhost.whitehouse.gov ([192.168.51.200])\n by heartbeat.whitehouse.gov (8.8.8/8.8.8) with ESMTP id SAA22453\n for <vice-president@heartbeat.whitehouse.gov>;\n Mon, 13 Aug 1998 l8:14:23 +1000"
|
||||
},
|
||||
{
|
||||
"name": "Received",
|
||||
"value": " from the_big_box.whitehouse.gov ([192.168.51.50])\n by mailhost.whitehouse.gov (8.8.8/8.8.7) with ESMTP id RAA20366\n for vice-president@whitehouse.gov; Mon, 13 Aug 1998 17:42:41 +1000"
|
||||
},
|
||||
{
|
||||
"name": "Date",
|
||||
"value": " Mon, 13 Aug 1998 17:42:41 +1000"
|
||||
},
|
||||
{
|
||||
"name": "Message-ID",
|
||||
"value": " <199804130742.RAA20366@mai1host.whitehouse.gov>"
|
||||
},
|
||||
{
|
||||
"name": "From",
|
||||
"value": " Bill Clinton <president@whitehouse.gov>"
|
||||
},
|
||||
{
|
||||
"name": "To",
|
||||
"value": " A1 (The Enforcer) Gore <vice-president@whitehouse.gov>"
|
||||
},
|
||||
{
|
||||
"name": "Subject",
|
||||
"value": " Map of Argentina with Description"
|
||||
},
|
||||
{
|
||||
"name": "MIME-Version",
|
||||
"value": " 1.0"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": " multipart/mixed;\n boundary=\"DC8------------DC8638F443D87A7F0726DEF7\""
|
||||
|
||||
@@ -20,6 +20,30 @@
|
||||
"sentAt": "2021-12-14T10:48:25Z",
|
||||
"bodyStructure": {
|
||||
"headers": [
|
||||
{
|
||||
"name": "To",
|
||||
"value": " \"email@example.com\" <email@example.com>"
|
||||
},
|
||||
{
|
||||
"name": "From",
|
||||
"value": " Name <email@example.com>"
|
||||
},
|
||||
{
|
||||
"name": "Subject",
|
||||
"value": " HTML test"
|
||||
},
|
||||
{
|
||||
"name": "Message-ID",
|
||||
"value": " <random-message-id@example.com>"
|
||||
},
|
||||
{
|
||||
"name": "Date",
|
||||
"value": " Tue, 14 Dec 2021 11:48:25 +0100"
|
||||
},
|
||||
{
|
||||
"name": "MIME-Version",
|
||||
"value": " 1.0"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": " multipart/alternative;\r\n boundary=\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\""
|
||||
|
||||
294
tests/src/jmap/email_changes.rs
Normal file
294
tests/src/jmap/email_changes.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use jmap::JMAP;
|
||||
use jmap_client::client::Client;
|
||||
use jmap_proto::{
|
||||
parser::{json::Parser, JsonObjectParser},
|
||||
types::{collection::Collection, id::Id, state::State},
|
||||
};
|
||||
use store::{
|
||||
ahash::AHashSet,
|
||||
write::{log::ChangeLogBuilder, BatchBuilder},
|
||||
};
|
||||
|
||||
pub async fn test(server: Arc<JMAP>, client: &mut Client) {
|
||||
println!("Running Email Changes tests...");
|
||||
|
||||
let mut states = vec![State::Initial];
|
||||
|
||||
for (change_id, (changes, expected_changelog)) in [
|
||||
(
|
||||
vec![
|
||||
LogAction::Insert(0),
|
||||
LogAction::Insert(1),
|
||||
LogAction::Insert(2),
|
||||
],
|
||||
vec![vec![vec![0, 1, 2], vec![], vec![]]],
|
||||
),
|
||||
(
|
||||
vec![
|
||||
LogAction::Move(0, 3),
|
||||
LogAction::Insert(4),
|
||||
LogAction::Insert(5),
|
||||
LogAction::Update(1),
|
||||
LogAction::Update(2),
|
||||
],
|
||||
vec![
|
||||
vec![vec![1, 2, 3, 4, 5], vec![], vec![]],
|
||||
vec![vec![3, 4, 5], vec![1, 2], vec![0]],
|
||||
],
|
||||
),
|
||||
(
|
||||
vec![
|
||||
LogAction::Delete(1),
|
||||
LogAction::Insert(6),
|
||||
LogAction::Insert(7),
|
||||
LogAction::Update(2),
|
||||
],
|
||||
vec![
|
||||
vec![vec![2, 3, 4, 5, 6, 7], vec![], vec![]],
|
||||
vec![vec![3, 4, 5, 6, 7], vec![2], vec![0, 1]],
|
||||
vec![vec![6, 7], vec![2], vec![1]],
|
||||
],
|
||||
),
|
||||
(
|
||||
vec![
|
||||
LogAction::Update(4),
|
||||
LogAction::Update(5),
|
||||
LogAction::Update(6),
|
||||
LogAction::Update(7),
|
||||
],
|
||||
vec![
|
||||
vec![vec![2, 3, 4, 5, 6, 7], vec![], vec![]],
|
||||
vec![vec![3, 4, 5, 6, 7], vec![2], vec![0, 1]],
|
||||
vec![vec![6, 7], vec![2, 4, 5], vec![1]],
|
||||
vec![vec![], vec![4, 5, 6, 7], vec![]],
|
||||
],
|
||||
),
|
||||
(
|
||||
vec![
|
||||
LogAction::Delete(4),
|
||||
LogAction::Delete(5),
|
||||
LogAction::Delete(6),
|
||||
LogAction::Delete(7),
|
||||
],
|
||||
vec![
|
||||
vec![vec![2, 3], vec![], vec![]],
|
||||
vec![vec![3], vec![2], vec![0, 1]],
|
||||
vec![vec![], vec![2], vec![1, 4, 5]],
|
||||
vec![vec![], vec![], vec![4, 5, 6, 7]],
|
||||
vec![vec![], vec![], vec![4, 5, 6, 7]],
|
||||
],
|
||||
),
|
||||
(
|
||||
vec![
|
||||
LogAction::Insert(8),
|
||||
LogAction::Insert(9),
|
||||
LogAction::Insert(10),
|
||||
LogAction::Update(3),
|
||||
],
|
||||
vec![
|
||||
vec![vec![2, 3, 8, 9, 10], vec![], vec![]],
|
||||
vec![vec![3, 8, 9, 10], vec![2], vec![0, 1]],
|
||||
vec![vec![8, 9, 10], vec![2, 3], vec![1, 4, 5]],
|
||||
vec![vec![8, 9, 10], vec![3], vec![4, 5, 6, 7]],
|
||||
vec![vec![8, 9, 10], vec![3], vec![4, 5, 6, 7]],
|
||||
vec![vec![8, 9, 10], vec![3], vec![]],
|
||||
],
|
||||
),
|
||||
(
|
||||
vec![LogAction::Update(2), LogAction::Update(8)],
|
||||
vec![
|
||||
vec![vec![2, 3, 8, 9, 10], vec![], vec![]],
|
||||
vec![vec![3, 8, 9, 10], vec![2], vec![0, 1]],
|
||||
vec![vec![8, 9, 10], vec![2, 3], vec![1, 4, 5]],
|
||||
vec![vec![8, 9, 10], vec![2, 3], vec![4, 5, 6, 7]],
|
||||
vec![vec![8, 9, 10], vec![2, 3], vec![4, 5, 6, 7]],
|
||||
vec![vec![8, 9, 10], vec![2, 3], vec![]],
|
||||
vec![vec![], vec![2, 8], vec![]],
|
||||
],
|
||||
),
|
||||
(
|
||||
vec![
|
||||
LogAction::Move(9, 11),
|
||||
LogAction::Move(10, 12),
|
||||
LogAction::Delete(8),
|
||||
],
|
||||
vec![
|
||||
vec![vec![2, 3, 11, 12], vec![], vec![]],
|
||||
vec![vec![3, 11, 12], vec![2], vec![0, 1]],
|
||||
vec![vec![11, 12], vec![2, 3], vec![1, 4, 5]],
|
||||
vec![vec![11, 12], vec![2, 3], vec![4, 5, 6, 7]],
|
||||
vec![vec![11, 12], vec![2, 3], vec![4, 5, 6, 7]],
|
||||
vec![vec![11, 12], vec![2, 3], vec![]],
|
||||
vec![vec![11, 12], vec![2], vec![8, 9, 10]],
|
||||
vec![vec![11, 12], vec![], vec![8, 9, 10]],
|
||||
],
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let mut changelog = ChangeLogBuilder::with_change_id(change_id as u64);
|
||||
|
||||
for change in changes {
|
||||
match change {
|
||||
LogAction::Insert(id) => changelog.log_insert(Collection::Email, id),
|
||||
LogAction::Update(id) => changelog.log_update(Collection::Email, id),
|
||||
LogAction::Delete(id) => changelog.log_delete(Collection::Email, id),
|
||||
LogAction::UpdateChild(id) => changelog.log_child_update(Collection::Email, id),
|
||||
LogAction::Move(old_id, new_id) => {
|
||||
changelog.log_move(Collection::Email, old_id, new_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server
|
||||
.store
|
||||
.write(
|
||||
BatchBuilder::new()
|
||||
.with_account_id(1)
|
||||
.with_collection(Collection::Email)
|
||||
.custom(changelog)
|
||||
.build_batch(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut new_state = State::Initial;
|
||||
for (test_num, state) in (states).iter().enumerate() {
|
||||
let changes = client.email_changes(state.to_string(), None).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
expected_changelog[test_num],
|
||||
[changes.created(), changes.updated(), changes.destroyed()]
|
||||
.into_iter()
|
||||
.map(|list| {
|
||||
let mut list = list
|
||||
.iter()
|
||||
.map(|i| Id::from_bytes(i.as_bytes()).unwrap().into())
|
||||
.collect::<Vec<u64>>();
|
||||
list.sort_unstable();
|
||||
list
|
||||
})
|
||||
.collect::<Vec<Vec<_>>>(),
|
||||
"test_num: {}, state: {:?}",
|
||||
test_num,
|
||||
state
|
||||
);
|
||||
|
||||
if let State::Initial = state {
|
||||
new_state = State::parse_str(changes.new_state()).unwrap();
|
||||
}
|
||||
|
||||
for max_changes in 1..=8 {
|
||||
let mut insertions = expected_changelog[test_num][0]
|
||||
.iter()
|
||||
.copied()
|
||||
.collect::<AHashSet<_>>();
|
||||
let mut updates = expected_changelog[test_num][1]
|
||||
.iter()
|
||||
.copied()
|
||||
.collect::<AHashSet<_>>();
|
||||
let mut deletions = expected_changelog[test_num][2]
|
||||
.iter()
|
||||
.copied()
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
let mut int_state = state.clone();
|
||||
|
||||
for _ in 0..100 {
|
||||
let changes = client
|
||||
.email_changes(int_state.to_string(), max_changes.into())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
changes.created().len()
|
||||
+ changes.updated().len()
|
||||
+ changes.destroyed().len()
|
||||
<= max_changes,
|
||||
"{} > {}",
|
||||
changes.created().len()
|
||||
+ changes.updated().len()
|
||||
+ changes.destroyed().len(),
|
||||
max_changes
|
||||
);
|
||||
|
||||
changes.created().iter().for_each(|id| {
|
||||
assert!(
|
||||
insertions.remove(&Id::from_bytes(id.as_bytes()).unwrap()),
|
||||
"{:?} != {}",
|
||||
insertions,
|
||||
Id::from_bytes(id.as_bytes()).unwrap()
|
||||
);
|
||||
});
|
||||
changes.updated().iter().for_each(|id| {
|
||||
assert!(
|
||||
updates.remove(&Id::from_bytes(id.as_bytes()).unwrap()),
|
||||
"{:?} != {}",
|
||||
updates,
|
||||
Id::from_bytes(id.as_bytes()).unwrap()
|
||||
);
|
||||
});
|
||||
changes.destroyed().iter().for_each(|id| {
|
||||
assert!(
|
||||
deletions.remove(&Id::from_bytes(id.as_bytes()).unwrap()),
|
||||
"{:?} != {}",
|
||||
deletions,
|
||||
Id::from_bytes(id.as_bytes()).unwrap()
|
||||
);
|
||||
});
|
||||
|
||||
int_state = State::parse_str(changes.new_state()).unwrap();
|
||||
|
||||
if !changes.has_more_changes() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(insertions.len(), 0);
|
||||
assert_eq!(updates.len(), 0);
|
||||
assert_eq!(deletions.len(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
states.push(new_state);
|
||||
}
|
||||
|
||||
let changes = client
|
||||
.email_changes(State::Initial.to_string(), 0.into())
|
||||
.await
|
||||
.unwrap();
|
||||
let mut created = changes
|
||||
.created()
|
||||
.iter()
|
||||
.map(|i| Id::from_bytes(i.as_bytes()).unwrap().into())
|
||||
.collect::<Vec<u64>>();
|
||||
created.sort_unstable();
|
||||
|
||||
assert_eq!(created, vec![2, 3, 11, 12]);
|
||||
assert_eq!(changes.updated(), Vec::<String>::new());
|
||||
assert_eq!(changes.destroyed(), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum LogAction {
|
||||
Insert(u64),
|
||||
Update(u64),
|
||||
Delete(u64),
|
||||
UpdateChild(u64),
|
||||
Move(u64, u64),
|
||||
}
|
||||
|
||||
pub trait ParseState: Sized {
|
||||
fn parse_str(state: &str) -> Option<Self>;
|
||||
}
|
||||
|
||||
impl ParseState for State {
|
||||
fn parse_str(state: &str) -> Option<Self> {
|
||||
let state = format!("{state}\"");
|
||||
let mut parser = Parser::new(state.as_bytes());
|
||||
State::parse(&mut parser).ok()
|
||||
}
|
||||
}
|
||||
224
tests/src/jmap/email_parse.rs
Normal file
224
tests/src/jmap/email_parse.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
|
||||
use jmap::JMAP;
|
||||
use jmap_client::{
|
||||
client::Client,
|
||||
email::{self, Header, HeaderForm},
|
||||
mailbox::Role,
|
||||
};
|
||||
use jmap_proto::types::id::Id;
|
||||
|
||||
use crate::jmap::{email_get::all_headers, replace_blob_ids};
|
||||
|
||||
pub async fn test(server: Arc<JMAP>, client: &mut Client) {
|
||||
println!("Running Email Parse tests...");
|
||||
|
||||
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
test_dir.push("resources");
|
||||
test_dir.push("jmap_mail_parse");
|
||||
|
||||
let mailbox_id = client
|
||||
.set_default_account_id(Id::new(1).to_string())
|
||||
.mailbox_create("JMAP Parse", None::<String>, Role::None)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
|
||||
// Test parsing an email attachment
|
||||
for test_name in ["attachment.eml", "attachment_b64.eml"] {
|
||||
let mut test_file = test_dir.clone();
|
||||
test_file.push(test_name);
|
||||
|
||||
let email = client
|
||||
.email_import(
|
||||
fs::read(&test_file).unwrap(),
|
||||
[mailbox_id.clone()],
|
||||
None::<Vec<String>>,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let blob_id = client
|
||||
.email_get(email.id().unwrap(), Some([email::Property::Attachments]))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.attachments()
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap()
|
||||
.blob_id()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let email = client
|
||||
.email_parse(
|
||||
&blob_id,
|
||||
[
|
||||
email::Property::Id,
|
||||
email::Property::BlobId,
|
||||
email::Property::ThreadId,
|
||||
email::Property::MailboxIds,
|
||||
email::Property::Keywords,
|
||||
email::Property::Size,
|
||||
email::Property::ReceivedAt,
|
||||
email::Property::MessageId,
|
||||
email::Property::InReplyTo,
|
||||
email::Property::References,
|
||||
email::Property::Sender,
|
||||
email::Property::From,
|
||||
email::Property::To,
|
||||
email::Property::Cc,
|
||||
email::Property::Bcc,
|
||||
email::Property::ReplyTo,
|
||||
email::Property::Subject,
|
||||
email::Property::SentAt,
|
||||
email::Property::HasAttachment,
|
||||
email::Property::Preview,
|
||||
email::Property::BodyValues,
|
||||
email::Property::TextBody,
|
||||
email::Property::HtmlBody,
|
||||
email::Property::Attachments,
|
||||
email::Property::BodyStructure,
|
||||
]
|
||||
.into(),
|
||||
[
|
||||
email::BodyProperty::PartId,
|
||||
email::BodyProperty::BlobId,
|
||||
email::BodyProperty::Size,
|
||||
email::BodyProperty::Name,
|
||||
email::BodyProperty::Type,
|
||||
email::BodyProperty::Charset,
|
||||
email::BodyProperty::Headers,
|
||||
email::BodyProperty::Disposition,
|
||||
email::BodyProperty::Cid,
|
||||
email::BodyProperty::Language,
|
||||
email::BodyProperty::Location,
|
||||
]
|
||||
.into(),
|
||||
100.into(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if !test_name.contains("_b64") {
|
||||
for parts in [
|
||||
email.text_body().unwrap(),
|
||||
email.html_body().unwrap(),
|
||||
email.attachments().unwrap(),
|
||||
] {
|
||||
for part in parts {
|
||||
let blob_id = part.blob_id().unwrap();
|
||||
|
||||
let inner_blob = client.download(blob_id).await.unwrap();
|
||||
|
||||
test_file.set_extension(format!("part{}", part.part_id().unwrap()));
|
||||
|
||||
//fs::write(&test_file, inner_blob).unwrap();
|
||||
let expected_inner_blob = fs::read(&test_file).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
inner_blob,
|
||||
expected_inner_blob,
|
||||
"file: {}",
|
||||
test_file.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test_file.set_extension("json");
|
||||
|
||||
let result = replace_blob_ids(serde_json::to_string_pretty(&email.into_test()).unwrap());
|
||||
|
||||
if fs::read(&test_file).unwrap() != result.as_bytes() {
|
||||
test_file.set_extension("failed");
|
||||
fs::write(&test_file, result.as_bytes()).unwrap();
|
||||
panic!("Test failed, output saved to {}", test_file.display());
|
||||
}
|
||||
}
|
||||
|
||||
// Test header parsing on a temporary blob
|
||||
let mut test_file = test_dir;
|
||||
test_file.push("headers.eml");
|
||||
let blob_id = client
|
||||
.upload(None, fs::read(&test_file).unwrap(), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_blob_id();
|
||||
|
||||
let mut email = client
|
||||
.email_parse(
|
||||
&blob_id,
|
||||
[
|
||||
email::Property::Id,
|
||||
email::Property::MessageId,
|
||||
email::Property::InReplyTo,
|
||||
email::Property::References,
|
||||
email::Property::Sender,
|
||||
email::Property::From,
|
||||
email::Property::To,
|
||||
email::Property::Cc,
|
||||
email::Property::Bcc,
|
||||
email::Property::ReplyTo,
|
||||
email::Property::Subject,
|
||||
email::Property::SentAt,
|
||||
email::Property::Preview,
|
||||
email::Property::TextBody,
|
||||
email::Property::HtmlBody,
|
||||
email::Property::Attachments,
|
||||
]
|
||||
.into(),
|
||||
[
|
||||
email::BodyProperty::Size,
|
||||
email::BodyProperty::Name,
|
||||
email::BodyProperty::Type,
|
||||
email::BodyProperty::Charset,
|
||||
email::BodyProperty::Disposition,
|
||||
email::BodyProperty::Cid,
|
||||
email::BodyProperty::Language,
|
||||
email::BodyProperty::Location,
|
||||
email::BodyProperty::Header(Header {
|
||||
name: "X-Custom-Header".into(),
|
||||
form: HeaderForm::Raw,
|
||||
all: false,
|
||||
}),
|
||||
email::BodyProperty::Header(Header {
|
||||
name: "X-Custom-Header-2".into(),
|
||||
form: HeaderForm::Raw,
|
||||
all: false,
|
||||
}),
|
||||
]
|
||||
.into(),
|
||||
100.into(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_test();
|
||||
|
||||
for property in all_headers() {
|
||||
email.headers.extend(
|
||||
client
|
||||
.email_parse(&blob_id, [property].into(), [].into(), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_test()
|
||||
.headers,
|
||||
);
|
||||
}
|
||||
|
||||
test_file.set_extension("json");
|
||||
|
||||
let result = replace_blob_ids(serde_json::to_string_pretty(&email).unwrap());
|
||||
|
||||
if fs::read(&test_file).unwrap() != result.as_bytes() {
|
||||
test_file.set_extension("failed");
|
||||
fs::write(&test_file, result.as_bytes()).unwrap();
|
||||
panic!("Test failed, output saved to {}", test_file.display());
|
||||
}
|
||||
|
||||
client.mailbox_destroy(&mailbox_id, true).await.unwrap();
|
||||
|
||||
server.store.assert_is_empty().await;
|
||||
}
|
||||
276
tests/src/jmap/email_query_changes.rs
Normal file
276
tests/src/jmap/email_query_changes.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use jmap::JMAP;
|
||||
use jmap_client::{
|
||||
client::Client,
|
||||
core::query::{Comparator, Filter},
|
||||
email,
|
||||
mailbox::Role,
|
||||
};
|
||||
use jmap_proto::types::{collection::Collection, id::Id, property::Property, state::State};
|
||||
use store::{
|
||||
ahash::{AHashMap, AHashSet},
|
||||
write::{log::ChangeLogBuilder, BatchBuilder, F_BITMAP, F_CLEAR, F_VALUE},
|
||||
};
|
||||
|
||||
use crate::jmap::email_changes::{LogAction, ParseState};
|
||||
|
||||
pub async fn test(server: Arc<JMAP>, client: &mut Client) {
|
||||
println!("Running Email QueryChanges tests...");
|
||||
|
||||
let mailbox1_id = client
|
||||
.set_default_account_id(Id::new(1).to_string())
|
||||
.mailbox_create("JMAP Changes 1", None::<String>, Role::None)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
let mailbox2_id = client
|
||||
.mailbox_create("JMAP Changes 2", None::<String>, Role::None)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
|
||||
let mut states = vec![State::Initial];
|
||||
let mut id_map = AHashMap::default();
|
||||
|
||||
let mut updated_ids = AHashSet::default();
|
||||
let mut removed_ids = AHashSet::default();
|
||||
let mut type1_ids = AHashSet::default();
|
||||
|
||||
let mut thread_id = 100;
|
||||
|
||||
for (change_num, change) in [
|
||||
LogAction::Insert(0),
|
||||
LogAction::Insert(1),
|
||||
LogAction::Insert(2),
|
||||
LogAction::Move(0, 3),
|
||||
LogAction::Insert(4),
|
||||
LogAction::Insert(5),
|
||||
LogAction::Update(1),
|
||||
LogAction::Update(2),
|
||||
LogAction::Delete(1),
|
||||
LogAction::Insert(6),
|
||||
LogAction::Insert(7),
|
||||
LogAction::Update(2),
|
||||
LogAction::Update(4),
|
||||
LogAction::Update(5),
|
||||
LogAction::Update(6),
|
||||
LogAction::Update(7),
|
||||
LogAction::Delete(4),
|
||||
LogAction::Delete(5),
|
||||
LogAction::Delete(6),
|
||||
LogAction::Insert(8),
|
||||
LogAction::Insert(9),
|
||||
LogAction::Insert(10),
|
||||
LogAction::Update(3),
|
||||
LogAction::Update(2),
|
||||
LogAction::Update(8),
|
||||
LogAction::Move(9, 11),
|
||||
LogAction::Move(10, 12),
|
||||
LogAction::Delete(8),
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
match &change {
|
||||
LogAction::Insert(id) => {
|
||||
let jmap_id = Id::from_bytes(
|
||||
client
|
||||
.email_import(
|
||||
format!(
|
||||
"From: test_{}\nSubject: test_{}\n\ntest",
|
||||
if change_num % 2 == 0 { 1 } else { 2 },
|
||||
*id
|
||||
)
|
||||
.into_bytes(),
|
||||
[if change_num % 2 == 0 {
|
||||
&mailbox1_id
|
||||
} else {
|
||||
&mailbox2_id
|
||||
}],
|
||||
[if change_num % 2 == 0 { "1" } else { "2" }].into(),
|
||||
Some(*id as i64),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.id()
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
id_map.insert(*id, jmap_id);
|
||||
if change_num % 2 == 0 {
|
||||
type1_ids.insert(jmap_id);
|
||||
}
|
||||
}
|
||||
LogAction::Update(id) => {
|
||||
let id = *id_map.get(id).unwrap();
|
||||
let mut changelog = ChangeLogBuilder::new();
|
||||
changelog.log_update(Collection::Email, id);
|
||||
server.commit_changes(1, changelog).await.unwrap();
|
||||
updated_ids.insert(id);
|
||||
}
|
||||
LogAction::Delete(id) => {
|
||||
let id = *id_map.get(id).unwrap();
|
||||
client.email_destroy(&id.to_string()).await.unwrap();
|
||||
|
||||
// Delete virtual threadId created during tests (so assert_empty_store succeeds)
|
||||
server
|
||||
.store
|
||||
.write(
|
||||
BatchBuilder::new()
|
||||
.with_account_id(1)
|
||||
.with_collection(Collection::Email)
|
||||
.update_document(id.document_id())
|
||||
.bitmap(Property::ThreadId, id.prefix_id(), F_CLEAR)
|
||||
.build_batch(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
removed_ids.insert(id);
|
||||
}
|
||||
LogAction::Move(from, to) => {
|
||||
let id = *id_map.get(from).unwrap();
|
||||
let new_id = Id::from_parts(thread_id, id.document_id());
|
||||
|
||||
server
|
||||
.store
|
||||
.write(
|
||||
BatchBuilder::new()
|
||||
.with_account_id(1)
|
||||
.with_collection(Collection::Email)
|
||||
.update_document(id.document_id())
|
||||
.value(Property::ThreadId, id.prefix_id(), F_BITMAP | F_CLEAR)
|
||||
.value(Property::ThreadId, thread_id, F_VALUE | F_BITMAP)
|
||||
.custom(server.begin_changes(1).await.unwrap().with_log_move(
|
||||
Collection::Email,
|
||||
id,
|
||||
new_id,
|
||||
))
|
||||
.build_batch(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
id_map.insert(*to, new_id);
|
||||
if type1_ids.contains(&id) {
|
||||
type1_ids.insert(new_id);
|
||||
}
|
||||
removed_ids.insert(id);
|
||||
thread_id += 1;
|
||||
}
|
||||
LogAction::UpdateChild(_) => unreachable!(),
|
||||
}
|
||||
|
||||
let mut new_state = State::Initial;
|
||||
for state in &states {
|
||||
for (test_num, query) in vec![
|
||||
QueryChanges {
|
||||
filter: None,
|
||||
sort: vec![email::query::Comparator::received_at()],
|
||||
since_query_state: state.clone(),
|
||||
max_changes: 0,
|
||||
up_to_id: None,
|
||||
},
|
||||
QueryChanges {
|
||||
filter: Some(email::query::Filter::from("test_1").into()),
|
||||
sort: vec![email::query::Comparator::received_at()],
|
||||
since_query_state: state.clone(),
|
||||
max_changes: 0,
|
||||
up_to_id: None,
|
||||
},
|
||||
QueryChanges {
|
||||
filter: Some(email::query::Filter::in_mailbox(&mailbox1_id).into()),
|
||||
sort: vec![email::query::Comparator::received_at()],
|
||||
since_query_state: state.clone(),
|
||||
max_changes: 0,
|
||||
up_to_id: None,
|
||||
},
|
||||
QueryChanges {
|
||||
filter: None,
|
||||
sort: vec![email::query::Comparator::received_at()],
|
||||
since_query_state: state.clone(),
|
||||
max_changes: 0,
|
||||
up_to_id: id_map
|
||||
.get(&7)
|
||||
.map(|id| id.to_string().into())
|
||||
.unwrap_or(None),
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if test_num == 3 && query.up_to_id.is_none() {
|
||||
continue;
|
||||
}
|
||||
let mut request = client.build();
|
||||
let query_request = request
|
||||
.query_email_changes(query.since_query_state.to_string())
|
||||
.sort(query.sort);
|
||||
|
||||
if let Some(filter) = query.filter {
|
||||
query_request.filter(filter);
|
||||
}
|
||||
|
||||
if let Some(up_to_id) = query.up_to_id {
|
||||
query_request.up_to_id(up_to_id);
|
||||
}
|
||||
|
||||
let changes = request.send_query_email_changes().await.unwrap();
|
||||
|
||||
if test_num == 0 || test_num == 1 {
|
||||
// Immutable filters should not return modified ids, only deletions.
|
||||
for id in changes.removed() {
|
||||
let id = Id::from_bytes(id.as_bytes()).unwrap();
|
||||
assert!(
|
||||
removed_ids.contains(&id),
|
||||
"{:?} (id: {})",
|
||||
changes,
|
||||
id_map.iter().find(|(_, v)| **v == id).unwrap().0
|
||||
);
|
||||
}
|
||||
}
|
||||
if test_num == 1 || test_num == 2 {
|
||||
// Only type 1 results should be added to the list.
|
||||
for item in changes.added() {
|
||||
let id = Id::from_bytes(item.id().as_bytes()).unwrap();
|
||||
assert!(
|
||||
type1_ids.contains(&id),
|
||||
"{:?} (id: {})",
|
||||
changes,
|
||||
id_map.iter().find(|(_, v)| **v == id).unwrap().0
|
||||
);
|
||||
}
|
||||
}
|
||||
if test_num == 3 {
|
||||
// Only ids up to 7 should be added to the list.
|
||||
for item in changes.added() {
|
||||
let item_id = Id::from_bytes(item.id().as_bytes()).unwrap();
|
||||
let id = id_map.iter().find(|(_, v)| **v == item_id).unwrap().0;
|
||||
assert!(id < &7, "{:?} (id: {})", changes, id);
|
||||
}
|
||||
}
|
||||
|
||||
if let State::Initial = state {
|
||||
new_state = State::parse_str(changes.new_query_state()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
states.push(new_state);
|
||||
}
|
||||
|
||||
client.mailbox_destroy(&mailbox1_id, true).await.unwrap();
|
||||
client.mailbox_destroy(&mailbox2_id, true).await.unwrap();
|
||||
|
||||
server.store.assert_is_empty().await;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct QueryChanges {
|
||||
pub filter: Option<Filter<email::query::Filter>>,
|
||||
pub sort: Vec<Comparator<email::query::Comparator>>,
|
||||
pub since_query_state: State,
|
||||
pub max_changes: usize,
|
||||
pub up_to_id: Option<String>,
|
||||
}
|
||||
163
tests/src/jmap/email_search_snippet.rs
Normal file
163
tests/src/jmap/email_search_snippet.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
|
||||
use jmap::JMAP;
|
||||
use jmap_client::{client::Client, core::query, email::query::Filter, mailbox::Role};
|
||||
use jmap_proto::types::id::Id;
|
||||
use store::ahash::AHashMap;
|
||||
|
||||
pub async fn test(server: Arc<JMAP>, client: &mut Client) {
|
||||
println!("Running SearchSnippet tests...");
|
||||
|
||||
let mailbox_id = client
|
||||
.set_default_account_id(Id::new(1).to_string())
|
||||
.mailbox_create("JMAP SearchSnippet", None::<String>, Role::None)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
|
||||
let mut email_ids = AHashMap::default();
|
||||
|
||||
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
test_dir.push("resources");
|
||||
test_dir.push("jmap_mail_snippet");
|
||||
|
||||
// Import test messages
|
||||
for email_name in [
|
||||
"html",
|
||||
"subpart",
|
||||
"mixed",
|
||||
"text_plain",
|
||||
"text_plain_chinese",
|
||||
] {
|
||||
let mut file_name = test_dir.clone();
|
||||
file_name.push(format!("{}.eml", email_name));
|
||||
let email_id = client
|
||||
.email_import(
|
||||
fs::read(&file_name).unwrap(),
|
||||
[&mailbox_id],
|
||||
None::<Vec<&str>>,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
email_ids.insert(email_name, email_id);
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for (filter, email_name, snippet_subject, snippet_preview) in [
|
||||
(
|
||||
query::Filter::or(vec![
|
||||
query::Filter::or(vec![Filter::subject("friend"), Filter::subject("help")]),
|
||||
query::Filter::or(vec![Filter::body("secret"), Filter::body("call")]),
|
||||
]),
|
||||
"text_plain",
|
||||
Some("<mark>Help</mark> a <mark>friend</mark> from Abidjan Côte d'Ivoire"),
|
||||
Some(concat!(
|
||||
"d'Ivoire. He <mark>secretly</mark> <mark>called</mark> me on his bedside ",
|
||||
"and told me that he has a sum of $7.5M (Seven Million five Hundred Thousand",
|
||||
" Dollars) left in a suspense account in a local bank here in Abidjan Côte ",
|
||||
"d'Ivoire, that he used my name a"
|
||||
)),
|
||||
),
|
||||
(
|
||||
Filter::text("côte").into(),
|
||||
"text_plain",
|
||||
Some("Help a friend from Abidjan <mark>Côte</mark> d'Ivoire"),
|
||||
Some(concat!(
|
||||
"in Abidjan <mark>Côte</mark> d'Ivoire. He secretly called me on ",
|
||||
"his bedside and told me that he has a sum of $7.5M (Seven ",
|
||||
"Million five Hundred Thousand Dollars) left in a suspense ",
|
||||
"account in a local bank here in Abidjan <mark>Côte</mark> d'Ivoire, that "
|
||||
)),
|
||||
),
|
||||
(
|
||||
Filter::text("\"your country\"").into(),
|
||||
"text_plain",
|
||||
None,
|
||||
Some(concat!(
|
||||
"over to <mark>your</mark> <mark>country</mark> to further my education and ",
|
||||
"to secure a residential permit for me in <mark>your</mark> <mark>country",
|
||||
"</mark>. Moreover, I am willing to offer you 30 percent of the total sum as ",
|
||||
"compensation for your effort inp",
|
||||
)),
|
||||
),
|
||||
(
|
||||
Filter::text("overseas").into(),
|
||||
"text_plain",
|
||||
None,
|
||||
Some("nominated account <mark>overseas</mark>. "),
|
||||
),
|
||||
(
|
||||
Filter::text("孫子兵法").into(),
|
||||
"text_plain_chinese",
|
||||
Some("<mark>孫</mark><mark>子</mark><mark>兵法</mark>"),
|
||||
Some(concat!(
|
||||
"<"<mark>孫</mark><mark>子</mark><mark>兵法</mark>:"> ",
|
||||
"<mark>孫</mark><mark>子</mark>曰:兵者,國之大事,死生之地,存亡之道,",
|
||||
"不可不察也。 <mark>孫</mark><mark>子</mark>曰:凡用兵之法,馳車千駟"
|
||||
)),
|
||||
),
|
||||
(
|
||||
Filter::text("cia").into(),
|
||||
"subpart",
|
||||
None,
|
||||
Some("shouldn't the <mark>CIA</mark> have something like that? Bill"),
|
||||
),
|
||||
(
|
||||
Filter::text("frösche").into(),
|
||||
"html",
|
||||
Some("Die Hasen und die <mark>Frösche</mark>"),
|
||||
Some(concat!(
|
||||
"und die <mark>Frösche</mark> Die Hasen klagten einst über ihre mißliche Lage; ",
|
||||
""wir leben", sprach ein Redner, "in steter Furcht vor Menschen und ",
|
||||
"Tieren, eine Beute der Hunde, der Adler, ja fast aller Raubtiere! ",
|
||||
"Unsere stete Angst ist är")),
|
||||
),
|
||||
(
|
||||
Filter::text("es:galería vasto biblioteca").into(),
|
||||
"mixed",
|
||||
Some("<mark>Biblioteca</mark> de Babel"),
|
||||
Some(concat!(
|
||||
"llaman la *<mark>Biblioteca</mark>*) se compone de un número indefinido, y tal ",
|
||||
"vez infinito, de <mark>galerías</mark> hexagonales, con <mark>vastos</mark> ",
|
||||
"pozos de ventilación en el medio, cercados por barandas bajísimas. Desde ",
|
||||
"cualquier hexágono se "
|
||||
)),
|
||||
),
|
||||
] {
|
||||
let mut request = client.build();
|
||||
let result_ref = request
|
||||
.query_email()
|
||||
.filter(filter.clone())
|
||||
.result_reference();
|
||||
request
|
||||
.get_search_snippet()
|
||||
.filter(filter)
|
||||
.email_ids_ref(result_ref);
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_method_responses()
|
||||
.pop()
|
||||
.unwrap()
|
||||
.unwrap_get_search_snippet()
|
||||
.unwrap();
|
||||
let snippet = response
|
||||
.snippet(email_ids.get(email_name).unwrap())
|
||||
.unwrap_or_else(|| panic!("No snippet for {}", email_name));
|
||||
assert_eq!(snippet_subject, snippet.subject());
|
||||
assert_eq!(snippet_preview, snippet.preview());
|
||||
assert!(
|
||||
snippet.preview().map_or(0, |p| p.len()) <= 255,
|
||||
"len: {}",
|
||||
snippet.preview().map_or(0, |p| p.len())
|
||||
);
|
||||
}
|
||||
|
||||
// Destroy test data
|
||||
client.mailbox_destroy(&mailbox_id, true).await.unwrap();
|
||||
|
||||
server.store.assert_is_empty().await;
|
||||
}
|
||||
@@ -7,8 +7,12 @@ use tokio::sync::watch;
|
||||
|
||||
use crate::{add_test_certs, store::TempDir};
|
||||
|
||||
pub mod email_changes;
|
||||
pub mod email_get;
|
||||
pub mod email_parse;
|
||||
pub mod email_query;
|
||||
pub mod email_query_changes;
|
||||
pub mod email_search_snippet;
|
||||
pub mod email_set;
|
||||
pub mod mailbox;
|
||||
pub mod thread_get;
|
||||
@@ -38,6 +42,10 @@ blob.path = '{TMP}'
|
||||
[certificate.default]
|
||||
cert = 'file://{CERT}'
|
||||
private-key = 'file://{PK}'
|
||||
|
||||
[jmap.protocol]
|
||||
set.max-objects = 100000
|
||||
|
||||
";
|
||||
|
||||
#[tokio::test]
|
||||
@@ -51,12 +59,16 @@ pub async fn jmap_tests() {
|
||||
|
||||
let delete = true;
|
||||
let mut params = init_jmap_tests(delete).await;
|
||||
//email_query::test(params.server.clone(), &mut params.client, delete).await;
|
||||
//email_get::test(params.server.clone(), &mut params.client).await;
|
||||
//email_set::test(params.server.clone(), &mut params.client).await;
|
||||
//email_query::test(params.server.clone(), &mut params.client, delete).await;
|
||||
//email_parse::test(params.server.clone(), &mut params.client).await;
|
||||
//email_search_snippet::test(params.server.clone(), &mut params.client).await;
|
||||
//email_changes::test(params.server.clone(), &mut params.client).await;
|
||||
email_query_changes::test(params.server.clone(), &mut params.client).await;
|
||||
//thread_get::test(params.server.clone(), &mut params.client).await;
|
||||
//thread_merge::test(params.server.clone(), &mut params.client).await;
|
||||
mailbox::test(params.server.clone(), &mut params.client).await;
|
||||
//mailbox::test(params.server.clone(), &mut params.client).await;
|
||||
if delete {
|
||||
params.temp_dir.delete();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user