First API tests.
This commit is contained in:
353
tests/src/jmap/email_get.rs
Normal file
353
tests/src/jmap/email_get.rs
Normal file
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart JMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
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 mail_parser::{HeaderName, RfcHeader};
|
||||
|
||||
use crate::jmap::replace_blob_ids;
|
||||
|
||||
pub async fn test(server: Arc<JMAP>, client: &mut Client) {
|
||||
println!("Running Email Get tests...");
|
||||
|
||||
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
test_dir.push("resources");
|
||||
test_dir.push("jmap_mail_get");
|
||||
|
||||
let coco1 = "implement";
|
||||
let mailbox_id = "a".to_string();
|
||||
/*let mailbox_id = client
|
||||
.set_default_account_id(Id::new(1).to_string())
|
||||
.mailbox_create("JMAP Get", None::<String>, Role::None)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();*/
|
||||
|
||||
for file_name in fs::read_dir(&test_dir).unwrap() {
|
||||
let mut file_name = file_name.as_ref().unwrap().path();
|
||||
if file_name.extension().map_or(true, |e| e != "eml") {
|
||||
continue;
|
||||
}
|
||||
let is_headers_test = file_name.file_name().unwrap() == "headers.eml";
|
||||
|
||||
let blob = fs::read(&file_name).unwrap();
|
||||
let blob_len = blob.len();
|
||||
let email = client
|
||||
.email_import(
|
||||
blob,
|
||||
[mailbox_id.clone()],
|
||||
["tag".to_string()].into(),
|
||||
((blob_len * 1000000) as i64).into(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut request = client.build();
|
||||
request
|
||||
.get_email()
|
||||
.ids([email.id().unwrap()])
|
||||
.properties([
|
||||
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,
|
||||
])
|
||||
.arguments()
|
||||
.body_properties(if !is_headers_test {
|
||||
[
|
||||
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,
|
||||
]
|
||||
} else {
|
||||
[
|
||||
email::BodyProperty::PartId,
|
||||
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,
|
||||
}),
|
||||
]
|
||||
})
|
||||
.fetch_all_body_values(true)
|
||||
.max_body_value_bytes(100);
|
||||
|
||||
let mut result = request
|
||||
.send_get_email()
|
||||
.await
|
||||
.unwrap()
|
||||
.take_list()
|
||||
.pop()
|
||||
.unwrap()
|
||||
.into_test();
|
||||
|
||||
if is_headers_test {
|
||||
for property in all_headers() {
|
||||
let mut request = client.build();
|
||||
request
|
||||
.get_email()
|
||||
.ids([email.id().unwrap()])
|
||||
.properties([property]);
|
||||
result.headers.extend(
|
||||
request
|
||||
.send_get_email()
|
||||
.await
|
||||
.unwrap()
|
||||
.take_list()
|
||||
.pop()
|
||||
.unwrap()
|
||||
.into_test()
|
||||
.headers,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let result = replace_blob_ids(serde_json::to_string_pretty(&result).unwrap());
|
||||
|
||||
file_name.set_extension("json");
|
||||
|
||||
if fs::read(&file_name).unwrap() != result.as_bytes() {
|
||||
file_name.set_extension("failed");
|
||||
fs::write(&file_name, result.as_bytes()).unwrap();
|
||||
panic!("Test failed, output saved to {}", file_name.display());
|
||||
}
|
||||
}
|
||||
|
||||
let coco = "implement";
|
||||
//client.mailbox_destroy(&mailbox_id, true).await.unwrap();
|
||||
|
||||
//server.store.assert_is_empty();
|
||||
}
|
||||
|
||||
pub fn all_headers() -> Vec<email::Property> {
|
||||
let mut properties = Vec::new();
|
||||
|
||||
for header in [
|
||||
HeaderName::Rfc(RfcHeader::From),
|
||||
HeaderName::Rfc(RfcHeader::To),
|
||||
HeaderName::Rfc(RfcHeader::Cc),
|
||||
HeaderName::Rfc(RfcHeader::Bcc),
|
||||
HeaderName::Other("X-Address-Single".into()),
|
||||
HeaderName::Other("X-Address".into()),
|
||||
HeaderName::Other("X-AddressList-Single".into()),
|
||||
HeaderName::Other("X-AddressList".into()),
|
||||
HeaderName::Other("X-AddressesGroup-Single".into()),
|
||||
HeaderName::Other("X-AddressesGroup".into()),
|
||||
] {
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Raw,
|
||||
name: header.as_str().to_string(),
|
||||
all: true,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Raw,
|
||||
name: header.as_str().to_string(),
|
||||
all: false,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Addresses,
|
||||
name: header.as_str().to_string(),
|
||||
all: true,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Addresses,
|
||||
name: header.as_str().to_string(),
|
||||
all: false,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::GroupedAddresses,
|
||||
name: header.as_str().to_string(),
|
||||
all: true,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::GroupedAddresses,
|
||||
name: header.as_str().to_string(),
|
||||
all: false,
|
||||
}));
|
||||
}
|
||||
|
||||
for header in [
|
||||
HeaderName::Rfc(RfcHeader::ListPost),
|
||||
HeaderName::Rfc(RfcHeader::ListSubscribe),
|
||||
HeaderName::Rfc(RfcHeader::ListUnsubscribe),
|
||||
HeaderName::Rfc(RfcHeader::ListOwner),
|
||||
HeaderName::Other("X-List-Single".into()),
|
||||
HeaderName::Other("X-List".into()),
|
||||
] {
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Raw,
|
||||
name: header.as_str().to_string(),
|
||||
all: true,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Raw,
|
||||
name: header.as_str().to_string(),
|
||||
all: false,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::URLs,
|
||||
name: header.as_str().to_string(),
|
||||
all: true,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::URLs,
|
||||
name: header.as_str().to_string(),
|
||||
all: false,
|
||||
}));
|
||||
}
|
||||
|
||||
for header in [
|
||||
HeaderName::Rfc(RfcHeader::Date),
|
||||
HeaderName::Rfc(RfcHeader::ResentDate),
|
||||
HeaderName::Other("X-Date-Single".into()),
|
||||
HeaderName::Other("X-Date".into()),
|
||||
] {
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Raw,
|
||||
name: header.as_str().to_string(),
|
||||
all: true,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Raw,
|
||||
name: header.as_str().to_string(),
|
||||
all: false,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Date,
|
||||
name: header.as_str().to_string(),
|
||||
all: true,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Date,
|
||||
name: header.as_str().to_string(),
|
||||
all: false,
|
||||
}));
|
||||
}
|
||||
|
||||
for header in [
|
||||
HeaderName::Rfc(RfcHeader::MessageId),
|
||||
HeaderName::Rfc(RfcHeader::References),
|
||||
HeaderName::Other("X-Id-Single".into()),
|
||||
HeaderName::Other("X-Id".into()),
|
||||
] {
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Raw,
|
||||
name: header.as_str().to_string(),
|
||||
all: true,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Raw,
|
||||
name: header.as_str().to_string(),
|
||||
all: false,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::MessageIds,
|
||||
name: header.as_str().to_string(),
|
||||
all: true,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::MessageIds,
|
||||
name: header.as_str().to_string(),
|
||||
all: false,
|
||||
}));
|
||||
}
|
||||
|
||||
for header in [
|
||||
HeaderName::Rfc(RfcHeader::Subject),
|
||||
HeaderName::Rfc(RfcHeader::Keywords),
|
||||
HeaderName::Other("X-Text-Single".into()),
|
||||
HeaderName::Other("X-Text".into()),
|
||||
] {
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Raw,
|
||||
name: header.as_str().to_string(),
|
||||
all: true,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Raw,
|
||||
name: header.as_str().to_string(),
|
||||
all: false,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Text,
|
||||
name: header.as_str().to_string(),
|
||||
all: true,
|
||||
}));
|
||||
properties.push(email::Property::Header(Header {
|
||||
form: HeaderForm::Text,
|
||||
name: header.as_str().to_string(),
|
||||
all: false,
|
||||
}));
|
||||
}
|
||||
|
||||
properties
|
||||
}
|
||||
149
tests/src/jmap/mod.rs
Normal file
149
tests/src/jmap/mod.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use jmap::{api::SessionManager, JMAP};
|
||||
use jmap_client::client::{Client, Credentials};
|
||||
use jmap_proto::types::id::Id;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use crate::{add_test_certs, store::TempDir};
|
||||
|
||||
pub mod email_get;
|
||||
|
||||
const SERVER: &str = "
|
||||
[server]
|
||||
hostname = 'jmap.example.org'
|
||||
|
||||
[server.listener.jmap]
|
||||
bind = ['127.0.0.1:8899']
|
||||
url = 'https://127.0.0.1:8899'
|
||||
protocol = 'jmap'
|
||||
|
||||
[server.socket]
|
||||
reuse-addr = true
|
||||
|
||||
[server.tls]
|
||||
enable = true
|
||||
implicit = false
|
||||
certificate = 'default'
|
||||
|
||||
[store]
|
||||
db.path = '{TMP}/sqlite.db'
|
||||
blob.path = '{TMP}'
|
||||
|
||||
[certificate.default]
|
||||
cert = 'file://{CERT}'
|
||||
private-key = 'file://{PK}'
|
||||
";
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn jmap_tests() {
|
||||
tracing::subscriber::set_global_default(
|
||||
tracing_subscriber::FmtSubscriber::builder()
|
||||
.with_max_level(tracing::Level::WARN)
|
||||
.finish(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let delete = true;
|
||||
let mut params = init_jmap_tests(delete).await;
|
||||
email_get::test(params.server.clone(), &mut params.client).await;
|
||||
if delete {
|
||||
params.temp_dir.delete();
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct JMAPTest {
|
||||
server: Arc<JMAP>,
|
||||
client: Client,
|
||||
temp_dir: TempDir,
|
||||
shutdown_tx: watch::Sender<bool>,
|
||||
}
|
||||
|
||||
async fn init_jmap_tests(delete_if_exists: bool) -> JMAPTest {
|
||||
// Load and parse config
|
||||
let temp_dir = TempDir::new("jmap_tests", delete_if_exists);
|
||||
let settings = utils::config::Config::parse(
|
||||
&add_test_certs(SERVER).replace("{TMP}", &temp_dir.path.display().to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
let servers = settings.parse_servers().unwrap();
|
||||
|
||||
// Start JMAP server
|
||||
let manager = SessionManager::from(JMAP::new(&settings).await);
|
||||
let shutdown_tx = servers.spawn(&settings, |server, shutdown_rx| {
|
||||
server.spawn(manager.clone(), shutdown_rx);
|
||||
});
|
||||
|
||||
// Create client
|
||||
let mut client = Client::new()
|
||||
.credentials(Credentials::bearer("DO_NOT_ATTEMPT_THIS_AT_HOME"))
|
||||
.accept_invalid_certs(true)
|
||||
.connect("https://127.0.0.1:8899")
|
||||
.await
|
||||
.unwrap();
|
||||
client.set_default_account_id(Id::new(1));
|
||||
|
||||
JMAPTest {
|
||||
server: manager.inner,
|
||||
temp_dir,
|
||||
client,
|
||||
shutdown_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_values(string: &str, name: &str) -> Vec<String> {
|
||||
let mut last_pos = 0;
|
||||
let mut values = Vec::new();
|
||||
|
||||
while let Some(pos) = string[last_pos..].find(name) {
|
||||
let mut value = string[last_pos + pos + name.len()..]
|
||||
.split('"')
|
||||
.nth(1)
|
||||
.unwrap();
|
||||
if value.ends_with('\\') {
|
||||
value = &value[..value.len() - 1];
|
||||
}
|
||||
values.push(value.to_string());
|
||||
last_pos += pos + name.len();
|
||||
}
|
||||
|
||||
values
|
||||
}
|
||||
|
||||
pub fn replace_values(mut string: String, find: &[String], replace: &[String]) -> String {
|
||||
for (find, replace) in find.iter().zip(replace.iter()) {
|
||||
string = string.replace(find, replace);
|
||||
}
|
||||
string
|
||||
}
|
||||
|
||||
pub fn replace_boundaries(string: String) -> String {
|
||||
let values = find_values(&string, "boundary=");
|
||||
if !values.is_empty() {
|
||||
replace_values(
|
||||
string,
|
||||
&values,
|
||||
&(0..values.len())
|
||||
.map(|i| format!("boundary_{}", i))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
} else {
|
||||
string
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replace_blob_ids(string: String) -> String {
|
||||
let values = find_values(&string, "blobId\":");
|
||||
if !values.is_empty() {
|
||||
replace_values(
|
||||
string,
|
||||
&values,
|
||||
&(0..values.len())
|
||||
.map(|i| format!("blob_{}", i))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
} else {
|
||||
string
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,20 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod jmap;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod store;
|
||||
|
||||
pub fn add_test_certs(config: &str) -> String {
|
||||
let mut cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
cert_path.push("resources");
|
||||
let mut cert = cert_path.clone();
|
||||
cert.push("tls_cert.pem");
|
||||
let mut pk = cert_path.clone();
|
||||
pk.push("tls_privatekey.pem");
|
||||
|
||||
config
|
||||
.replace("{CERT}", cert.as_path().to_str().unwrap())
|
||||
.replace("{PK}", pk.as_path().to_str().unwrap())
|
||||
}
|
||||
|
||||
@@ -7,15 +7,20 @@ use std::{io::Read, sync::Arc};
|
||||
use ::store::Store;
|
||||
use utils::config::Config;
|
||||
|
||||
struct TempDir {
|
||||
path: std::path::PathBuf,
|
||||
pub struct TempDir {
|
||||
pub path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn store_test() {
|
||||
let temp_dir = TempDir::new("store_tests", true);
|
||||
pub async fn store_tests() {
|
||||
let insert = false;
|
||||
let temp_dir = TempDir::new("store_tests", insert);
|
||||
let config_file = format!(
|
||||
concat!("[blob.store]\n", "path = \"{}\"\n", "hash = 1\n"),
|
||||
concat!(
|
||||
"store.blob.path = \"{}\"\n",
|
||||
"store.db.path = \"{}/sqlite.db\"\n"
|
||||
),
|
||||
temp_dir.path.display(),
|
||||
temp_dir.path.display()
|
||||
);
|
||||
let db = Arc::new(
|
||||
@@ -23,20 +28,17 @@ pub async fn store_test() {
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
let insert = true;
|
||||
if insert {
|
||||
db.destroy().await;
|
||||
}
|
||||
//assign_id::test(db).await;
|
||||
blobs::test(db).await;
|
||||
//query::test(db, insert).await;
|
||||
//blobs::test(db).await;
|
||||
query::test(db, insert).await;
|
||||
temp_dir.delete();
|
||||
}
|
||||
|
||||
pub fn deflate_artwork_data() -> Vec<u8> {
|
||||
let mut csv_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
csv_path.push("src");
|
||||
csv_path.push("tests");
|
||||
csv_path.push("resources");
|
||||
csv_path.push("artwork_data.csv.gz");
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ use std::{
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use jmap_proto::types::keyword::Keyword;
|
||||
use store::{ahash::AHashMap, query::sort::Pagination};
|
||||
|
||||
use store::{
|
||||
@@ -154,7 +155,7 @@ pub async fn test(db: Arc<Store>, do_insert: bool) {
|
||||
if !field.is_empty() {
|
||||
builder.value(
|
||||
field_id,
|
||||
field.to_lowercase(),
|
||||
Keyword::Other(field.to_lowercase()),
|
||||
F_VALUE | F_INDEX | F_BITMAP,
|
||||
);
|
||||
}
|
||||
@@ -249,7 +250,7 @@ pub async fn test_filter(db: Arc<Store>) {
|
||||
(
|
||||
vec![
|
||||
Filter::has_text(fields["artist"], "mauro kunst", Language::None),
|
||||
Filter::has_keyword(fields["artistRole"], "artist"),
|
||||
Filter::is_in_bitmap(fields["artistRole"], Keyword::Other("artist".to_string())),
|
||||
Filter::Or,
|
||||
Filter::eq(fields["year"], 1969u32),
|
||||
Filter::eq(fields["year"], 1971u32),
|
||||
@@ -283,7 +284,7 @@ pub async fn test_filter(db: Arc<Store>) {
|
||||
(
|
||||
vec![
|
||||
Filter::And,
|
||||
Filter::has_keyword(fields["artist"], "warhol"),
|
||||
Filter::has_text(fields["artist"], "warhol", Language::None),
|
||||
Filter::Not,
|
||||
Filter::has_english_text(fields["title"], "'campbell'"),
|
||||
Filter::End,
|
||||
|
||||
Reference in New Issue
Block a user