mirror of
https://github.com/chatmail/core.git
synced 2026-05-03 05:16:28 +03:00
refactor: Move even more tests into their own files (#6521)
As always, I moved the tests from the biggest files. I left out `mimefactory.rs` because @link2xt has an active PR modifying the tests.
This commit is contained in:
819
src/blob.rs
819
src/blob.rs
@@ -844,821 +844,4 @@ fn add_white_bg(img: &mut DynamicImage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod blob_tests;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::message::{Message, Viewtype};
|
|
||||||
use crate::sql;
|
|
||||||
use crate::test_utils::{self, TestContext};
|
|
||||||
use crate::tools::SystemTime;
|
|
||||||
|
|
||||||
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
|
|
||||||
tokio::task::block_in_place(move || {
|
|
||||||
let img = ImageReader::open(path)
|
|
||||||
.expect("failed to open image")
|
|
||||||
.with_guessed_format()
|
|
||||||
.expect("failed to guess format")
|
|
||||||
.decode()
|
|
||||||
.expect("failed to decode image");
|
|
||||||
assert_eq!(img.width(), width, "invalid width");
|
|
||||||
assert_eq!(img.height(), height, "invalid height");
|
|
||||||
img
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const FILE_BYTES: &[u8] = b"hello";
|
|
||||||
const FILE_DEDUPLICATED: &str = "ea8f163db38682925e4491c5e58d4bb.txt";
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_create() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let blob =
|
|
||||||
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
|
||||||
let fname = t.get_blobdir().join(FILE_DEDUPLICATED);
|
|
||||||
let data = fs::read(fname).await.unwrap();
|
|
||||||
assert_eq!(data, FILE_BYTES);
|
|
||||||
assert_eq!(blob.as_name(), format!("$BLOBDIR/{FILE_DEDUPLICATED}"));
|
|
||||||
assert_eq!(blob.to_abs_path(), t.get_blobdir().join(FILE_DEDUPLICATED));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_lowercase_ext() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let blob =
|
|
||||||
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.TXT").unwrap();
|
|
||||||
assert!(
|
|
||||||
blob.as_name().ends_with(".txt"),
|
|
||||||
"Blob {blob:?} should end with .txt"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_as_file_name() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let blob =
|
|
||||||
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
|
||||||
assert_eq!(blob.as_file_name(), FILE_DEDUPLICATED);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_as_rel_path() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let blob =
|
|
||||||
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
|
||||||
assert_eq!(blob.as_rel_path(), Path::new(FILE_DEDUPLICATED));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_suffix() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let blob =
|
|
||||||
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
|
||||||
assert_eq!(blob.suffix(), Some("txt"));
|
|
||||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "bar").unwrap();
|
|
||||||
assert_eq!(blob.suffix(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_create_dup() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
|
||||||
let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED);
|
|
||||||
assert!(foo_path.exists());
|
|
||||||
BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.txt").unwrap();
|
|
||||||
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
|
|
||||||
while let Ok(Some(dirent)) = dir.next_entry().await {
|
|
||||||
let fname = dirent.file_name();
|
|
||||||
if fname == foo_path.file_name().unwrap() {
|
|
||||||
assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES);
|
|
||||||
} else {
|
|
||||||
let name = fname.to_str().unwrap();
|
|
||||||
assert!(name.ends_with(".txt"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_double_ext() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.tar.gz").unwrap();
|
|
||||||
let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED).with_extension("gz");
|
|
||||||
assert!(foo_path.exists());
|
|
||||||
BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.tar.gz").unwrap();
|
|
||||||
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
|
|
||||||
while let Ok(Some(dirent)) = dir.next_entry().await {
|
|
||||||
let fname = dirent.file_name();
|
|
||||||
if fname == foo_path.file_name().unwrap() {
|
|
||||||
assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES);
|
|
||||||
} else {
|
|
||||||
let name = fname.to_str().unwrap();
|
|
||||||
println!("{name}");
|
|
||||||
assert_eq!(name.starts_with("foo"), false);
|
|
||||||
assert_eq!(name.ends_with(".tar.gz"), false);
|
|
||||||
assert!(name.ends_with(".gz"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_create_long_names() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let s = format!("file.{}", "a".repeat(100));
|
|
||||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"data", &s).unwrap();
|
|
||||||
let blobname = blob.as_name().split('/').last().unwrap();
|
|
||||||
assert!(blobname.len() < 70);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_create_and_copy() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let src = t.dir.path().join("src");
|
|
||||||
fs::write(&src, b"boo").await.unwrap();
|
|
||||||
let blob = BlobObject::create_and_copy(&t, src.as_ref()).await.unwrap();
|
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/src");
|
|
||||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
|
||||||
assert_eq!(data, b"boo");
|
|
||||||
|
|
||||||
let whoops = t.dir.path().join("whoops");
|
|
||||||
assert!(BlobObject::create_and_copy(&t, whoops.as_ref())
|
|
||||||
.await
|
|
||||||
.is_err());
|
|
||||||
let whoops = t.get_blobdir().join("whoops");
|
|
||||||
assert!(!whoops.exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_create_from_path() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
|
|
||||||
let src_ext = t.dir.path().join("external");
|
|
||||||
fs::write(&src_ext, b"boo").await.unwrap();
|
|
||||||
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/external");
|
|
||||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
|
||||||
assert_eq!(data, b"boo");
|
|
||||||
|
|
||||||
let src_int = t.get_blobdir().join("internal");
|
|
||||||
fs::write(&src_int, b"boo").await.unwrap();
|
|
||||||
let blob = BlobObject::new_from_path(&t, &src_int).await.unwrap();
|
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
|
|
||||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
|
||||||
assert_eq!(data, b"boo");
|
|
||||||
}
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_create_from_name_long() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
|
||||||
fs::write(&src_ext, b"boo").await.unwrap();
|
|
||||||
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
blob.as_name(),
|
|
||||||
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_is_blob_name() {
|
|
||||||
assert!(BlobObject::is_acceptible_blob_name("foo"));
|
|
||||||
assert!(BlobObject::is_acceptible_blob_name("foo.txt"));
|
|
||||||
assert!(BlobObject::is_acceptible_blob_name("f".repeat(128)));
|
|
||||||
assert!(!BlobObject::is_acceptible_blob_name("foo/bar"));
|
|
||||||
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
|
|
||||||
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sanitise_name() {
|
|
||||||
let (stem, ext) =
|
|
||||||
BlobObject::sanitise_name("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt");
|
|
||||||
assert_eq!(ext, ".txt");
|
|
||||||
assert!(!stem.is_empty());
|
|
||||||
|
|
||||||
// the extensions are kept together as between stem and extension a number may be added -
|
|
||||||
// and `foo.tar.gz` should become `foo-1234.tar.gz` and not `foo.tar-1234.gz`
|
|
||||||
let (stem, ext) = BlobObject::sanitise_name("wot.tar.gz");
|
|
||||||
assert_eq!(stem, "wot");
|
|
||||||
assert_eq!(ext, ".tar.gz");
|
|
||||||
|
|
||||||
let (stem, ext) = BlobObject::sanitise_name(".foo.bar");
|
|
||||||
assert_eq!(stem, "");
|
|
||||||
assert_eq!(ext, ".foo.bar");
|
|
||||||
|
|
||||||
let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
|
|
||||||
assert!(stem.contains("foo"));
|
|
||||||
assert!(!stem.contains('?'));
|
|
||||||
assert_eq!(ext, ".bar");
|
|
||||||
|
|
||||||
let (stem, ext) = BlobObject::sanitise_name("no-extension");
|
|
||||||
assert_eq!(stem, "no-extension");
|
|
||||||
assert_eq!(ext, "");
|
|
||||||
|
|
||||||
let (stem, ext) = BlobObject::sanitise_name("path/ignored\\this: is* forbidden?.c");
|
|
||||||
assert_eq!(ext, ".c");
|
|
||||||
assert!(!stem.contains("path"));
|
|
||||||
assert!(!stem.contains("ignored"));
|
|
||||||
assert!(stem.contains("this"));
|
|
||||||
assert!(stem.contains("forbidden"));
|
|
||||||
assert!(!stem.contains('/'));
|
|
||||||
assert!(!stem.contains('\\'));
|
|
||||||
assert!(!stem.contains(':'));
|
|
||||||
assert!(!stem.contains('*'));
|
|
||||||
assert!(!stem.contains('?'));
|
|
||||||
|
|
||||||
let (stem, ext) = BlobObject::sanitise_name(
|
|
||||||
"file.with_lots_of_characters_behind_point_and_double_ending.tar.gz",
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stem,
|
|
||||||
"file.with_lots_of_characters_behind_point_and_double_ending"
|
|
||||||
);
|
|
||||||
assert_eq!(ext, ".tar.gz");
|
|
||||||
|
|
||||||
let (stem, ext) = BlobObject::sanitise_name("a. tar.tar.gz");
|
|
||||||
assert_eq!(stem, "a. tar");
|
|
||||||
assert_eq!(ext, ".tar.gz");
|
|
||||||
|
|
||||||
let (stem, ext) = BlobObject::sanitise_name("Guia_uso_GNB (v0.8).pdf");
|
|
||||||
assert_eq!(stem, "Guia_uso_GNB (v0.8)");
|
|
||||||
assert_eq!(ext, ".pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_add_white_bg() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let bytes0 = include_bytes!("../test-data/image/logo.png").as_slice();
|
|
||||||
let bytes1 = include_bytes!("../test-data/image/avatar900x900.png").as_slice();
|
|
||||||
for (bytes, color) in [
|
|
||||||
(bytes0, [255u8, 255, 255, 255]),
|
|
||||||
(bytes1, [253u8, 198, 0, 255]),
|
|
||||||
] {
|
|
||||||
let avatar_src = t.dir.path().join("avatar.png");
|
|
||||||
fs::write(&avatar_src, bytes).await.unwrap();
|
|
||||||
|
|
||||||
let mut blob = BlobObject::new_from_path(&t, &avatar_src).await.unwrap();
|
|
||||||
let img_wh = 128;
|
|
||||||
let maybe_sticker = &mut false;
|
|
||||||
let strict_limits = true;
|
|
||||||
blob.recode_to_size(&t, None, maybe_sticker, img_wh, 20_000, strict_limits)
|
|
||||||
.unwrap();
|
|
||||||
tokio::task::block_in_place(move || {
|
|
||||||
let img = ImageReader::open(blob.to_abs_path())
|
|
||||||
.unwrap()
|
|
||||||
.with_guessed_format()
|
|
||||||
.unwrap()
|
|
||||||
.decode()
|
|
||||||
.unwrap();
|
|
||||||
assert!(img.width() == img_wh);
|
|
||||||
assert!(img.height() == img_wh);
|
|
||||||
assert_eq!(img.get_pixel(0, 0), Rgba(color));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_selfavatar_outside_blobdir() {
|
|
||||||
async fn file_size(path_buf: &Path) -> u64 {
|
|
||||||
fs::metadata(path_buf).await.unwrap().len()
|
|
||||||
}
|
|
||||||
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let avatar_src = t.dir.path().join("avatar.jpg");
|
|
||||||
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
|
||||||
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
|
||||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
|
||||||
let avatar_path = Path::new(&avatar_blob);
|
|
||||||
assert!(
|
|
||||||
avatar_blob.ends_with("d98cd30ed8f2129bf3968420208849d.jpg"),
|
|
||||||
"The avatar filename should be its hash, put instead it's {avatar_blob}"
|
|
||||||
);
|
|
||||||
let scaled_avatar_size = file_size(avatar_path).await;
|
|
||||||
assert!(scaled_avatar_size < avatar_bytes.len() as u64);
|
|
||||||
|
|
||||||
check_image_size(avatar_src, 1000, 1000);
|
|
||||||
check_image_size(
|
|
||||||
&avatar_blob,
|
|
||||||
constants::BALANCED_AVATAR_SIZE,
|
|
||||||
constants::BALANCED_AVATAR_SIZE,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut blob = BlobObject::new_from_path(&t, avatar_path).await.unwrap();
|
|
||||||
let maybe_sticker = &mut false;
|
|
||||||
let strict_limits = true;
|
|
||||||
blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits)
|
|
||||||
.unwrap();
|
|
||||||
let new_file_size = file_size(&blob.to_abs_path()).await;
|
|
||||||
assert!(new_file_size <= 3000);
|
|
||||||
assert!(new_file_size > 2000);
|
|
||||||
// The new file should be smaller:
|
|
||||||
assert!(new_file_size < scaled_avatar_size);
|
|
||||||
// And the original file should not be touched:
|
|
||||||
assert_eq!(file_size(avatar_path).await, scaled_avatar_size);
|
|
||||||
tokio::task::block_in_place(move || {
|
|
||||||
let img = ImageReader::open(blob.to_abs_path())
|
|
||||||
.unwrap()
|
|
||||||
.with_guessed_format()
|
|
||||||
.unwrap()
|
|
||||||
.decode()
|
|
||||||
.unwrap();
|
|
||||||
assert!(img.width() > 130);
|
|
||||||
assert_eq!(img.width(), img.height());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_selfavatar_in_blobdir() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let avatar_src = t.get_blobdir().join("avatar.png");
|
|
||||||
fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
check_image_size(&avatar_src, 900, 900);
|
|
||||||
|
|
||||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
|
||||||
assert!(
|
|
||||||
avatar_cfg.ends_with("9e7f409ac5c92b942cc4f31cee2770a.png"),
|
|
||||||
"Avatar file name {avatar_cfg} should end with its hash"
|
|
||||||
);
|
|
||||||
|
|
||||||
check_image_size(
|
|
||||||
avatar_cfg,
|
|
||||||
constants::BALANCED_AVATAR_SIZE,
|
|
||||||
constants::BALANCED_AVATAR_SIZE,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_selfavatar_copy_without_recode() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let avatar_src = t.dir.path().join("avatar.png");
|
|
||||||
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
|
||||||
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
|
||||||
let avatar_blob = t.get_blobdir().join("e9b6c7a78aa2e4f415644f55a553e73.png");
|
|
||||||
assert!(!avatar_blob.exists());
|
|
||||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(avatar_blob.exists());
|
|
||||||
assert_eq!(
|
|
||||||
fs::metadata(&avatar_blob).await.unwrap().len(),
|
|
||||||
avatar_bytes.len() as u64
|
|
||||||
);
|
|
||||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
|
||||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_recode_image_1() {
|
|
||||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
|
||||||
SendImageCheckMediaquality {
|
|
||||||
viewtype: Viewtype::Image,
|
|
||||||
media_quality_config: "0",
|
|
||||||
bytes,
|
|
||||||
extension: "jpg",
|
|
||||||
has_exif: true,
|
|
||||||
original_width: 1000,
|
|
||||||
original_height: 1000,
|
|
||||||
compressed_width: 1000,
|
|
||||||
compressed_height: 1000,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.test()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
SendImageCheckMediaquality {
|
|
||||||
viewtype: Viewtype::Image,
|
|
||||||
media_quality_config: "1",
|
|
||||||
bytes,
|
|
||||||
extension: "jpg",
|
|
||||||
has_exif: true,
|
|
||||||
original_width: 1000,
|
|
||||||
original_height: 1000,
|
|
||||||
compressed_width: 1000,
|
|
||||||
compressed_height: 1000,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.test()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_recode_image_2() {
|
|
||||||
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
|
|
||||||
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
|
|
||||||
let img_rotated = SendImageCheckMediaquality {
|
|
||||||
viewtype: Viewtype::Image,
|
|
||||||
media_quality_config: "0",
|
|
||||||
bytes,
|
|
||||||
extension: "jpg",
|
|
||||||
has_exif: true,
|
|
||||||
original_width: 2000,
|
|
||||||
original_height: 1800,
|
|
||||||
orientation: 270,
|
|
||||||
compressed_width: 1800,
|
|
||||||
compressed_height: 2000,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.test()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_correct_rotation(&img_rotated);
|
|
||||||
|
|
||||||
let mut buf = Cursor::new(vec![]);
|
|
||||||
img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
|
|
||||||
let bytes = buf.into_inner();
|
|
||||||
|
|
||||||
let img_rotated = SendImageCheckMediaquality {
|
|
||||||
viewtype: Viewtype::Image,
|
|
||||||
media_quality_config: "1",
|
|
||||||
bytes: &bytes,
|
|
||||||
extension: "jpg",
|
|
||||||
original_width: 1800,
|
|
||||||
original_height: 2000,
|
|
||||||
compressed_width: 1800,
|
|
||||||
compressed_height: 2000,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.test()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_correct_rotation(&img_rotated);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_recode_image_balanced_png() {
|
|
||||||
let bytes = include_bytes!("../test-data/image/screenshot.png");
|
|
||||||
|
|
||||||
SendImageCheckMediaquality {
|
|
||||||
viewtype: Viewtype::Image,
|
|
||||||
media_quality_config: "0",
|
|
||||||
bytes,
|
|
||||||
extension: "png",
|
|
||||||
original_width: 1920,
|
|
||||||
original_height: 1080,
|
|
||||||
compressed_width: 1920,
|
|
||||||
compressed_height: 1080,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.test()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
SendImageCheckMediaquality {
|
|
||||||
viewtype: Viewtype::Image,
|
|
||||||
media_quality_config: "1",
|
|
||||||
bytes,
|
|
||||||
extension: "png",
|
|
||||||
original_width: 1920,
|
|
||||||
original_height: 1080,
|
|
||||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
|
||||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.test()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
SendImageCheckMediaquality {
|
|
||||||
viewtype: Viewtype::File,
|
|
||||||
media_quality_config: "1",
|
|
||||||
bytes,
|
|
||||||
extension: "png",
|
|
||||||
original_width: 1920,
|
|
||||||
original_height: 1080,
|
|
||||||
compressed_width: 1920,
|
|
||||||
compressed_height: 1080,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.test()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
SendImageCheckMediaquality {
|
|
||||||
viewtype: Viewtype::File,
|
|
||||||
media_quality_config: "1",
|
|
||||||
bytes,
|
|
||||||
extension: "png",
|
|
||||||
original_width: 1920,
|
|
||||||
original_height: 1080,
|
|
||||||
compressed_width: 1920,
|
|
||||||
compressed_height: 1080,
|
|
||||||
set_draft: true,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.test()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
|
|
||||||
SendImageCheckMediaquality {
|
|
||||||
viewtype: Viewtype::Sticker,
|
|
||||||
media_quality_config: "0",
|
|
||||||
bytes,
|
|
||||||
extension: "png",
|
|
||||||
original_width: 1920,
|
|
||||||
original_height: 1080,
|
|
||||||
compressed_width: 1920,
|
|
||||||
compressed_height: 1080,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.test()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tests that RGBA PNG can be recoded into JPEG
|
|
||||||
/// by dropping alpha channel.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_recode_image_rgba_png_to_jpeg() {
|
|
||||||
let bytes = include_bytes!("../test-data/image/screenshot-rgba.png");
|
|
||||||
|
|
||||||
SendImageCheckMediaquality {
|
|
||||||
viewtype: Viewtype::Image,
|
|
||||||
media_quality_config: "1",
|
|
||||||
bytes,
|
|
||||||
extension: "png",
|
|
||||||
original_width: 1920,
|
|
||||||
original_height: 1080,
|
|
||||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
|
||||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.test()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_recode_image_huge_jpg() {
|
|
||||||
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
|
|
||||||
SendImageCheckMediaquality {
|
|
||||||
viewtype: Viewtype::Image,
|
|
||||||
media_quality_config: "0",
|
|
||||||
bytes,
|
|
||||||
extension: "jpg",
|
|
||||||
has_exif: true,
|
|
||||||
original_width: 1920,
|
|
||||||
original_height: 1080,
|
|
||||||
compressed_width: constants::BALANCED_IMAGE_SIZE,
|
|
||||||
compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.test()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_correct_rotation(img: &DynamicImage) {
|
|
||||||
// The test images are black in the bottom left corner after correctly applying
|
|
||||||
// the EXIF orientation
|
|
||||||
|
|
||||||
let [luma] = img.get_pixel(10, 10).to_luma().0;
|
|
||||||
assert_eq!(luma, 255);
|
|
||||||
let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0;
|
|
||||||
assert_eq!(luma, 255);
|
|
||||||
let [luma] = img
|
|
||||||
.get_pixel(img.width() - 10, img.height() - 10)
|
|
||||||
.to_luma()
|
|
||||||
.0;
|
|
||||||
assert_eq!(luma, 255);
|
|
||||||
let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0;
|
|
||||||
assert_eq!(luma, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct SendImageCheckMediaquality<'a> {
|
|
||||||
pub(crate) viewtype: Viewtype,
|
|
||||||
pub(crate) media_quality_config: &'a str,
|
|
||||||
pub(crate) bytes: &'a [u8],
|
|
||||||
pub(crate) extension: &'a str,
|
|
||||||
pub(crate) has_exif: bool,
|
|
||||||
pub(crate) original_width: u32,
|
|
||||||
pub(crate) original_height: u32,
|
|
||||||
pub(crate) orientation: i32,
|
|
||||||
pub(crate) compressed_width: u32,
|
|
||||||
pub(crate) compressed_height: u32,
|
|
||||||
pub(crate) set_draft: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SendImageCheckMediaquality<'_> {
|
|
||||||
pub(crate) async fn test(self) -> anyhow::Result<DynamicImage> {
|
|
||||||
let viewtype = self.viewtype;
|
|
||||||
let media_quality_config = self.media_quality_config;
|
|
||||||
let bytes = self.bytes;
|
|
||||||
let extension = self.extension;
|
|
||||||
let has_exif = self.has_exif;
|
|
||||||
let original_width = self.original_width;
|
|
||||||
let original_height = self.original_height;
|
|
||||||
let orientation = self.orientation;
|
|
||||||
let compressed_width = self.compressed_width;
|
|
||||||
let compressed_height = self.compressed_height;
|
|
||||||
let set_draft = self.set_draft;
|
|
||||||
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
let bob = TestContext::new_bob().await;
|
|
||||||
alice
|
|
||||||
.set_config(Config::MediaQuality, Some(media_quality_config))
|
|
||||||
.await?;
|
|
||||||
let file = alice.get_blobdir().join("file").with_extension(extension);
|
|
||||||
let file_name = format!("file.{extension}");
|
|
||||||
|
|
||||||
fs::write(&file, &bytes)
|
|
||||||
.await
|
|
||||||
.context("failed to write file")?;
|
|
||||||
check_image_size(&file, original_width, original_height);
|
|
||||||
|
|
||||||
let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
|
|
||||||
if has_exif {
|
|
||||||
let exif = exif.unwrap();
|
|
||||||
assert_eq!(exif_orientation(&exif, &alice), orientation);
|
|
||||||
} else {
|
|
||||||
assert!(exif.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut msg = Message::new(viewtype);
|
|
||||||
msg.set_file_and_deduplicate(&alice, &file, Some(&file_name), None)?;
|
|
||||||
let chat = alice.create_chat(&bob).await;
|
|
||||||
if set_draft {
|
|
||||||
chat.id.set_draft(&alice, Some(&mut msg)).await.unwrap();
|
|
||||||
msg = chat.id.get_draft(&alice).await.unwrap().unwrap();
|
|
||||||
assert_eq!(msg.get_viewtype(), Viewtype::File);
|
|
||||||
}
|
|
||||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
|
||||||
let alice_msg = alice.get_last_msg().await;
|
|
||||||
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
|
||||||
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
|
||||||
let file_saved = alice
|
|
||||||
.get_blobdir()
|
|
||||||
.join("saved-".to_string() + &alice_msg.get_filename().unwrap());
|
|
||||||
alice_msg.save_file(&alice, &file_saved).await?;
|
|
||||||
check_image_size(file_saved, compressed_width, compressed_height);
|
|
||||||
|
|
||||||
let bob_msg = bob.recv_msg(&sent).await;
|
|
||||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
|
|
||||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
|
||||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
|
||||||
let file_saved = bob
|
|
||||||
.get_blobdir()
|
|
||||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
|
||||||
bob_msg.save_file(&bob, &file_saved).await?;
|
|
||||||
if viewtype == Viewtype::File {
|
|
||||||
assert_eq!(file_saved.extension().unwrap(), extension);
|
|
||||||
let bytes1 = fs::read(&file_saved).await?;
|
|
||||||
assert_eq!(&bytes1, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
|
||||||
assert!(exif.is_none());
|
|
||||||
|
|
||||||
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
|
||||||
Ok(img)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_send_big_gif_as_image() -> Result<()> {
|
|
||||||
let bytes = include_bytes!("../test-data/image/screenshot.gif");
|
|
||||||
let (width, height) = (1920u32, 1080u32);
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
let bob = TestContext::new_bob().await;
|
|
||||||
alice
|
|
||||||
.set_config(
|
|
||||||
Config::MediaQuality,
|
|
||||||
Some(&(MediaQuality::Worse as i32).to_string()),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let file = alice.get_blobdir().join("file").with_extension("gif");
|
|
||||||
fs::write(&file, &bytes)
|
|
||||||
.await
|
|
||||||
.context("failed to write file")?;
|
|
||||||
let mut msg = Message::new(Viewtype::Image);
|
|
||||||
msg.set_file_and_deduplicate(&alice, &file, Some("file.gif"), None)?;
|
|
||||||
let chat = alice.create_chat(&bob).await;
|
|
||||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
|
||||||
let bob_msg = bob.recv_msg(&sent).await;
|
|
||||||
// DC must detect the image as GIF and send it w/o reencoding.
|
|
||||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Gif);
|
|
||||||
assert_eq!(bob_msg.get_width() as u32, width);
|
|
||||||
assert_eq!(bob_msg.get_height() as u32, height);
|
|
||||||
let file_saved = bob
|
|
||||||
.get_blobdir()
|
|
||||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
|
||||||
bob_msg.save_file(&bob, &file_saved).await?;
|
|
||||||
let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
|
||||||
assert_eq!(file_size, bytes.len() as u64);
|
|
||||||
check_image_size(file_saved, width, height);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_send_gif_as_sticker() -> Result<()> {
|
|
||||||
let bytes = include_bytes!("../test-data/image/image100x50.gif");
|
|
||||||
let alice = &TestContext::new_alice().await;
|
|
||||||
let file = alice.get_blobdir().join("file").with_extension("gif");
|
|
||||||
fs::write(&file, &bytes)
|
|
||||||
.await
|
|
||||||
.context("failed to write file")?;
|
|
||||||
let mut msg = Message::new(Viewtype::Sticker);
|
|
||||||
msg.set_file_and_deduplicate(alice, &file, None, None)?;
|
|
||||||
let chat = alice.get_self_chat().await;
|
|
||||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
|
||||||
let msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
|
|
||||||
// Message::force_sticker() wasn't used, still Viewtype::Sticker is preserved because of the
|
|
||||||
// extension.
|
|
||||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_create_and_deduplicate() -> Result<()> {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
|
|
||||||
let path = t.get_blobdir().join("anyfile.dat");
|
|
||||||
fs::write(&path, b"bla").await?;
|
|
||||||
let blob = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
|
||||||
assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f.dat");
|
|
||||||
assert_eq!(path.exists(), false);
|
|
||||||
|
|
||||||
assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
|
|
||||||
|
|
||||||
fs::write(&path, b"bla").await?;
|
|
||||||
let blob2 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
|
||||||
assert_eq!(blob2.name, blob.name);
|
|
||||||
|
|
||||||
let path_outside_blobdir = t.dir.path().join("anyfile.dat");
|
|
||||||
fs::write(&path_outside_blobdir, b"bla").await?;
|
|
||||||
let blob3 =
|
|
||||||
BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
|
|
||||||
assert!(path_outside_blobdir.exists());
|
|
||||||
assert_eq!(blob3.name, blob.name);
|
|
||||||
|
|
||||||
fs::write(&path, b"blabla").await?;
|
|
||||||
let blob4 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
|
||||||
assert_ne!(blob4.name, blob.name);
|
|
||||||
|
|
||||||
fs::remove_dir_all(t.get_blobdir()).await?;
|
|
||||||
let blob5 =
|
|
||||||
BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
|
|
||||||
assert_eq!(blob5.name, blob.name);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_create_and_deduplicate_from_bytes() -> Result<()> {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
|
|
||||||
fs::remove_dir(t.get_blobdir()).await?;
|
|
||||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
|
|
||||||
assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f");
|
|
||||||
|
|
||||||
assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
|
|
||||||
let modified1 = blob.to_abs_path().metadata()?.modified()?;
|
|
||||||
|
|
||||||
// Test that the modification time of the file is updated when a new file is created
|
|
||||||
// so that it's not deleted during housekeeping.
|
|
||||||
// We can't use SystemTime::shift() here because file creation uses the actual OS time,
|
|
||||||
// which we can't mock from our code.
|
|
||||||
tokio::time::sleep(Duration::from_millis(1100)).await;
|
|
||||||
|
|
||||||
let blob2 = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
|
|
||||||
assert_eq!(blob2.name, blob.name);
|
|
||||||
|
|
||||||
let modified2 = blob.to_abs_path().metadata()?.modified()?;
|
|
||||||
assert_ne!(modified1, modified2);
|
|
||||||
sql::housekeeping(&t).await?;
|
|
||||||
assert!(blob2.to_abs_path().exists());
|
|
||||||
|
|
||||||
// If we do shift the time by more than 1h, the blob file will be deleted during housekeeping:
|
|
||||||
SystemTime::shift(Duration::from_secs(65 * 60));
|
|
||||||
sql::housekeeping(&t).await?;
|
|
||||||
assert_eq!(blob2.to_abs_path().exists(), false);
|
|
||||||
|
|
||||||
let blob3 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
|
|
||||||
assert_ne!(blob3.name, blob.name);
|
|
||||||
|
|
||||||
{
|
|
||||||
// If something goes wrong and the blob file is overwritten,
|
|
||||||
// the correct content should be restored:
|
|
||||||
fs::write(blob3.to_abs_path(), b"bloblo").await?;
|
|
||||||
|
|
||||||
let blob4 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
|
|
||||||
let blob4_content = fs::read(blob4.to_abs_path()).await?;
|
|
||||||
assert_eq!(blob4_content, b"blabla");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
810
src/blob/blob_tests.rs
Normal file
810
src/blob/blob_tests.rs
Normal file
@@ -0,0 +1,810 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::message::{Message, Viewtype};
|
||||||
|
use crate::sql;
|
||||||
|
use crate::test_utils::{self, TestContext};
|
||||||
|
use crate::tools::SystemTime;
|
||||||
|
|
||||||
|
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
|
||||||
|
tokio::task::block_in_place(move || {
|
||||||
|
let img = ImageReader::open(path)
|
||||||
|
.expect("failed to open image")
|
||||||
|
.with_guessed_format()
|
||||||
|
.expect("failed to guess format")
|
||||||
|
.decode()
|
||||||
|
.expect("failed to decode image");
|
||||||
|
assert_eq!(img.width(), width, "invalid width");
|
||||||
|
assert_eq!(img.height(), height, "invalid height");
|
||||||
|
img
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILE_BYTES: &[u8] = b"hello";
|
||||||
|
const FILE_DEDUPLICATED: &str = "ea8f163db38682925e4491c5e58d4bb.txt";
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_create() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||||
|
let fname = t.get_blobdir().join(FILE_DEDUPLICATED);
|
||||||
|
let data = fs::read(fname).await.unwrap();
|
||||||
|
assert_eq!(data, FILE_BYTES);
|
||||||
|
assert_eq!(blob.as_name(), format!("$BLOBDIR/{FILE_DEDUPLICATED}"));
|
||||||
|
assert_eq!(blob.to_abs_path(), t.get_blobdir().join(FILE_DEDUPLICATED));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_lowercase_ext() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.TXT").unwrap();
|
||||||
|
assert!(
|
||||||
|
blob.as_name().ends_with(".txt"),
|
||||||
|
"Blob {blob:?} should end with .txt"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_as_file_name() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||||
|
assert_eq!(blob.as_file_name(), FILE_DEDUPLICATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_as_rel_path() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||||
|
assert_eq!(blob.as_rel_path(), Path::new(FILE_DEDUPLICATED));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_suffix() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||||
|
assert_eq!(blob.suffix(), Some("txt"));
|
||||||
|
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "bar").unwrap();
|
||||||
|
assert_eq!(blob.suffix(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_create_dup() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||||
|
let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED);
|
||||||
|
assert!(foo_path.exists());
|
||||||
|
BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.txt").unwrap();
|
||||||
|
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
|
||||||
|
while let Ok(Some(dirent)) = dir.next_entry().await {
|
||||||
|
let fname = dirent.file_name();
|
||||||
|
if fname == foo_path.file_name().unwrap() {
|
||||||
|
assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES);
|
||||||
|
} else {
|
||||||
|
let name = fname.to_str().unwrap();
|
||||||
|
assert!(name.ends_with(".txt"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_double_ext() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.tar.gz").unwrap();
|
||||||
|
let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED).with_extension("gz");
|
||||||
|
assert!(foo_path.exists());
|
||||||
|
BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.tar.gz").unwrap();
|
||||||
|
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
|
||||||
|
while let Ok(Some(dirent)) = dir.next_entry().await {
|
||||||
|
let fname = dirent.file_name();
|
||||||
|
if fname == foo_path.file_name().unwrap() {
|
||||||
|
assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES);
|
||||||
|
} else {
|
||||||
|
let name = fname.to_str().unwrap();
|
||||||
|
println!("{name}");
|
||||||
|
assert_eq!(name.starts_with("foo"), false);
|
||||||
|
assert_eq!(name.ends_with(".tar.gz"), false);
|
||||||
|
assert!(name.ends_with(".gz"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_create_long_names() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let s = format!("file.{}", "a".repeat(100));
|
||||||
|
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"data", &s).unwrap();
|
||||||
|
let blobname = blob.as_name().split('/').last().unwrap();
|
||||||
|
assert!(blobname.len() < 70);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_create_and_copy() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let src = t.dir.path().join("src");
|
||||||
|
fs::write(&src, b"boo").await.unwrap();
|
||||||
|
let blob = BlobObject::create_and_copy(&t, src.as_ref()).await.unwrap();
|
||||||
|
assert_eq!(blob.as_name(), "$BLOBDIR/src");
|
||||||
|
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||||
|
assert_eq!(data, b"boo");
|
||||||
|
|
||||||
|
let whoops = t.dir.path().join("whoops");
|
||||||
|
assert!(BlobObject::create_and_copy(&t, whoops.as_ref())
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
let whoops = t.get_blobdir().join("whoops");
|
||||||
|
assert!(!whoops.exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_create_from_path() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
|
let src_ext = t.dir.path().join("external");
|
||||||
|
fs::write(&src_ext, b"boo").await.unwrap();
|
||||||
|
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(blob.as_name(), "$BLOBDIR/external");
|
||||||
|
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||||
|
assert_eq!(data, b"boo");
|
||||||
|
|
||||||
|
let src_int = t.get_blobdir().join("internal");
|
||||||
|
fs::write(&src_int, b"boo").await.unwrap();
|
||||||
|
let blob = BlobObject::new_from_path(&t, &src_int).await.unwrap();
|
||||||
|
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
|
||||||
|
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||||
|
assert_eq!(data, b"boo");
|
||||||
|
}
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_create_from_name_long() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
||||||
|
fs::write(&src_ext, b"boo").await.unwrap();
|
||||||
|
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
blob.as_name(),
|
||||||
|
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_blob_name() {
|
||||||
|
assert!(BlobObject::is_acceptible_blob_name("foo"));
|
||||||
|
assert!(BlobObject::is_acceptible_blob_name("foo.txt"));
|
||||||
|
assert!(BlobObject::is_acceptible_blob_name("f".repeat(128)));
|
||||||
|
assert!(!BlobObject::is_acceptible_blob_name("foo/bar"));
|
||||||
|
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
|
||||||
|
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sanitise_name() {
|
||||||
|
let (stem, ext) = BlobObject::sanitise_name("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt");
|
||||||
|
assert_eq!(ext, ".txt");
|
||||||
|
assert!(!stem.is_empty());
|
||||||
|
|
||||||
|
// the extensions are kept together as between stem and extension a number may be added -
|
||||||
|
// and `foo.tar.gz` should become `foo-1234.tar.gz` and not `foo.tar-1234.gz`
|
||||||
|
let (stem, ext) = BlobObject::sanitise_name("wot.tar.gz");
|
||||||
|
assert_eq!(stem, "wot");
|
||||||
|
assert_eq!(ext, ".tar.gz");
|
||||||
|
|
||||||
|
let (stem, ext) = BlobObject::sanitise_name(".foo.bar");
|
||||||
|
assert_eq!(stem, "");
|
||||||
|
assert_eq!(ext, ".foo.bar");
|
||||||
|
|
||||||
|
let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
|
||||||
|
assert!(stem.contains("foo"));
|
||||||
|
assert!(!stem.contains('?'));
|
||||||
|
assert_eq!(ext, ".bar");
|
||||||
|
|
||||||
|
let (stem, ext) = BlobObject::sanitise_name("no-extension");
|
||||||
|
assert_eq!(stem, "no-extension");
|
||||||
|
assert_eq!(ext, "");
|
||||||
|
|
||||||
|
let (stem, ext) = BlobObject::sanitise_name("path/ignored\\this: is* forbidden?.c");
|
||||||
|
assert_eq!(ext, ".c");
|
||||||
|
assert!(!stem.contains("path"));
|
||||||
|
assert!(!stem.contains("ignored"));
|
||||||
|
assert!(stem.contains("this"));
|
||||||
|
assert!(stem.contains("forbidden"));
|
||||||
|
assert!(!stem.contains('/'));
|
||||||
|
assert!(!stem.contains('\\'));
|
||||||
|
assert!(!stem.contains(':'));
|
||||||
|
assert!(!stem.contains('*'));
|
||||||
|
assert!(!stem.contains('?'));
|
||||||
|
|
||||||
|
let (stem, ext) = BlobObject::sanitise_name(
|
||||||
|
"file.with_lots_of_characters_behind_point_and_double_ending.tar.gz",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stem,
|
||||||
|
"file.with_lots_of_characters_behind_point_and_double_ending"
|
||||||
|
);
|
||||||
|
assert_eq!(ext, ".tar.gz");
|
||||||
|
|
||||||
|
let (stem, ext) = BlobObject::sanitise_name("a. tar.tar.gz");
|
||||||
|
assert_eq!(stem, "a. tar");
|
||||||
|
assert_eq!(ext, ".tar.gz");
|
||||||
|
|
||||||
|
let (stem, ext) = BlobObject::sanitise_name("Guia_uso_GNB (v0.8).pdf");
|
||||||
|
assert_eq!(stem, "Guia_uso_GNB (v0.8)");
|
||||||
|
assert_eq!(ext, ".pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_add_white_bg() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let bytes0 = include_bytes!("../../test-data/image/logo.png").as_slice();
|
||||||
|
let bytes1 = include_bytes!("../../test-data/image/avatar900x900.png").as_slice();
|
||||||
|
for (bytes, color) in [
|
||||||
|
(bytes0, [255u8, 255, 255, 255]),
|
||||||
|
(bytes1, [253u8, 198, 0, 255]),
|
||||||
|
] {
|
||||||
|
let avatar_src = t.dir.path().join("avatar.png");
|
||||||
|
fs::write(&avatar_src, bytes).await.unwrap();
|
||||||
|
|
||||||
|
let mut blob = BlobObject::new_from_path(&t, &avatar_src).await.unwrap();
|
||||||
|
let img_wh = 128;
|
||||||
|
let maybe_sticker = &mut false;
|
||||||
|
let strict_limits = true;
|
||||||
|
blob.recode_to_size(&t, None, maybe_sticker, img_wh, 20_000, strict_limits)
|
||||||
|
.unwrap();
|
||||||
|
tokio::task::block_in_place(move || {
|
||||||
|
let img = ImageReader::open(blob.to_abs_path())
|
||||||
|
.unwrap()
|
||||||
|
.with_guessed_format()
|
||||||
|
.unwrap()
|
||||||
|
.decode()
|
||||||
|
.unwrap();
|
||||||
|
assert!(img.width() == img_wh);
|
||||||
|
assert!(img.height() == img_wh);
|
||||||
|
assert_eq!(img.get_pixel(0, 0), Rgba(color));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_selfavatar_outside_blobdir() {
|
||||||
|
async fn file_size(path_buf: &Path) -> u64 {
|
||||||
|
fs::metadata(path_buf).await.unwrap().len()
|
||||||
|
}
|
||||||
|
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let avatar_src = t.dir.path().join("avatar.jpg");
|
||||||
|
let avatar_bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
|
||||||
|
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||||
|
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||||
|
let avatar_path = Path::new(&avatar_blob);
|
||||||
|
assert!(
|
||||||
|
avatar_blob.ends_with("d98cd30ed8f2129bf3968420208849d.jpg"),
|
||||||
|
"The avatar filename should be its hash, put instead it's {avatar_blob}"
|
||||||
|
);
|
||||||
|
let scaled_avatar_size = file_size(avatar_path).await;
|
||||||
|
assert!(scaled_avatar_size < avatar_bytes.len() as u64);
|
||||||
|
|
||||||
|
check_image_size(avatar_src, 1000, 1000);
|
||||||
|
check_image_size(
|
||||||
|
&avatar_blob,
|
||||||
|
constants::BALANCED_AVATAR_SIZE,
|
||||||
|
constants::BALANCED_AVATAR_SIZE,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut blob = BlobObject::new_from_path(&t, avatar_path).await.unwrap();
|
||||||
|
let maybe_sticker = &mut false;
|
||||||
|
let strict_limits = true;
|
||||||
|
blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits)
|
||||||
|
.unwrap();
|
||||||
|
let new_file_size = file_size(&blob.to_abs_path()).await;
|
||||||
|
assert!(new_file_size <= 3000);
|
||||||
|
assert!(new_file_size > 2000);
|
||||||
|
// The new file should be smaller:
|
||||||
|
assert!(new_file_size < scaled_avatar_size);
|
||||||
|
// And the original file should not be touched:
|
||||||
|
assert_eq!(file_size(avatar_path).await, scaled_avatar_size);
|
||||||
|
tokio::task::block_in_place(move || {
|
||||||
|
let img = ImageReader::open(blob.to_abs_path())
|
||||||
|
.unwrap()
|
||||||
|
.with_guessed_format()
|
||||||
|
.unwrap()
|
||||||
|
.decode()
|
||||||
|
.unwrap();
|
||||||
|
assert!(img.width() > 130);
|
||||||
|
assert_eq!(img.width(), img.height());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_selfavatar_in_blobdir() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let avatar_src = t.get_blobdir().join("avatar.png");
|
||||||
|
fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
check_image_size(&avatar_src, 900, 900);
|
||||||
|
|
||||||
|
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||||
|
assert!(
|
||||||
|
avatar_cfg.ends_with("9e7f409ac5c92b942cc4f31cee2770a.png"),
|
||||||
|
"Avatar file name {avatar_cfg} should end with its hash"
|
||||||
|
);
|
||||||
|
|
||||||
|
check_image_size(
|
||||||
|
avatar_cfg,
|
||||||
|
constants::BALANCED_AVATAR_SIZE,
|
||||||
|
constants::BALANCED_AVATAR_SIZE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_selfavatar_copy_without_recode() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let avatar_src = t.dir.path().join("avatar.png");
|
||||||
|
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||||
|
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||||
|
let avatar_blob = t.get_blobdir().join("e9b6c7a78aa2e4f415644f55a553e73.png");
|
||||||
|
assert!(!avatar_blob.exists());
|
||||||
|
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(avatar_blob.exists());
|
||||||
|
assert_eq!(
|
||||||
|
fs::metadata(&avatar_blob).await.unwrap().len(),
|
||||||
|
avatar_bytes.len() as u64
|
||||||
|
);
|
||||||
|
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
||||||
|
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_recode_image_1() {
|
||||||
|
let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
|
||||||
|
SendImageCheckMediaquality {
|
||||||
|
viewtype: Viewtype::Image,
|
||||||
|
media_quality_config: "0",
|
||||||
|
bytes,
|
||||||
|
extension: "jpg",
|
||||||
|
has_exif: true,
|
||||||
|
original_width: 1000,
|
||||||
|
original_height: 1000,
|
||||||
|
compressed_width: 1000,
|
||||||
|
compressed_height: 1000,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.test()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
SendImageCheckMediaquality {
|
||||||
|
viewtype: Viewtype::Image,
|
||||||
|
media_quality_config: "1",
|
||||||
|
bytes,
|
||||||
|
extension: "jpg",
|
||||||
|
has_exif: true,
|
||||||
|
original_width: 1000,
|
||||||
|
original_height: 1000,
|
||||||
|
compressed_width: 1000,
|
||||||
|
compressed_height: 1000,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.test()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_recode_image_2() {
|
||||||
|
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
|
||||||
|
let bytes = include_bytes!("../../test-data/image/rectangle2000x1800-rotated.jpg");
|
||||||
|
let img_rotated = SendImageCheckMediaquality {
|
||||||
|
viewtype: Viewtype::Image,
|
||||||
|
media_quality_config: "0",
|
||||||
|
bytes,
|
||||||
|
extension: "jpg",
|
||||||
|
has_exif: true,
|
||||||
|
original_width: 2000,
|
||||||
|
original_height: 1800,
|
||||||
|
orientation: 270,
|
||||||
|
compressed_width: 1800,
|
||||||
|
compressed_height: 2000,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.test()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_correct_rotation(&img_rotated);
|
||||||
|
|
||||||
|
let mut buf = Cursor::new(vec![]);
|
||||||
|
img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
|
||||||
|
let bytes = buf.into_inner();
|
||||||
|
|
||||||
|
let img_rotated = SendImageCheckMediaquality {
|
||||||
|
viewtype: Viewtype::Image,
|
||||||
|
media_quality_config: "1",
|
||||||
|
bytes: &bytes,
|
||||||
|
extension: "jpg",
|
||||||
|
original_width: 1800,
|
||||||
|
original_height: 2000,
|
||||||
|
compressed_width: 1800,
|
||||||
|
compressed_height: 2000,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.test()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_correct_rotation(&img_rotated);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_recode_image_balanced_png() {
|
||||||
|
let bytes = include_bytes!("../../test-data/image/screenshot.png");
|
||||||
|
|
||||||
|
SendImageCheckMediaquality {
|
||||||
|
viewtype: Viewtype::Image,
|
||||||
|
media_quality_config: "0",
|
||||||
|
bytes,
|
||||||
|
extension: "png",
|
||||||
|
original_width: 1920,
|
||||||
|
original_height: 1080,
|
||||||
|
compressed_width: 1920,
|
||||||
|
compressed_height: 1080,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.test()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
SendImageCheckMediaquality {
|
||||||
|
viewtype: Viewtype::Image,
|
||||||
|
media_quality_config: "1",
|
||||||
|
bytes,
|
||||||
|
extension: "png",
|
||||||
|
original_width: 1920,
|
||||||
|
original_height: 1080,
|
||||||
|
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||||
|
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.test()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
SendImageCheckMediaquality {
|
||||||
|
viewtype: Viewtype::File,
|
||||||
|
media_quality_config: "1",
|
||||||
|
bytes,
|
||||||
|
extension: "png",
|
||||||
|
original_width: 1920,
|
||||||
|
original_height: 1080,
|
||||||
|
compressed_width: 1920,
|
||||||
|
compressed_height: 1080,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.test()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
SendImageCheckMediaquality {
|
||||||
|
viewtype: Viewtype::File,
|
||||||
|
media_quality_config: "1",
|
||||||
|
bytes,
|
||||||
|
extension: "png",
|
||||||
|
original_width: 1920,
|
||||||
|
original_height: 1080,
|
||||||
|
compressed_width: 1920,
|
||||||
|
compressed_height: 1080,
|
||||||
|
set_draft: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.test()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
|
||||||
|
SendImageCheckMediaquality {
|
||||||
|
viewtype: Viewtype::Sticker,
|
||||||
|
media_quality_config: "0",
|
||||||
|
bytes,
|
||||||
|
extension: "png",
|
||||||
|
original_width: 1920,
|
||||||
|
original_height: 1080,
|
||||||
|
compressed_width: 1920,
|
||||||
|
compressed_height: 1080,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.test()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that RGBA PNG can be recoded into JPEG
|
||||||
|
/// by dropping alpha channel.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_recode_image_rgba_png_to_jpeg() {
|
||||||
|
let bytes = include_bytes!("../../test-data/image/screenshot-rgba.png");
|
||||||
|
|
||||||
|
SendImageCheckMediaquality {
|
||||||
|
viewtype: Viewtype::Image,
|
||||||
|
media_quality_config: "1",
|
||||||
|
bytes,
|
||||||
|
extension: "png",
|
||||||
|
original_width: 1920,
|
||||||
|
original_height: 1080,
|
||||||
|
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||||
|
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.test()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_recode_image_huge_jpg() {
|
||||||
|
let bytes = include_bytes!("../../test-data/image/screenshot.jpg");
|
||||||
|
SendImageCheckMediaquality {
|
||||||
|
viewtype: Viewtype::Image,
|
||||||
|
media_quality_config: "0",
|
||||||
|
bytes,
|
||||||
|
extension: "jpg",
|
||||||
|
has_exif: true,
|
||||||
|
original_width: 1920,
|
||||||
|
original_height: 1080,
|
||||||
|
compressed_width: constants::BALANCED_IMAGE_SIZE,
|
||||||
|
compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.test()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_correct_rotation(img: &DynamicImage) {
|
||||||
|
// The test images are black in the bottom left corner after correctly applying
|
||||||
|
// the EXIF orientation
|
||||||
|
|
||||||
|
let [luma] = img.get_pixel(10, 10).to_luma().0;
|
||||||
|
assert_eq!(luma, 255);
|
||||||
|
let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0;
|
||||||
|
assert_eq!(luma, 255);
|
||||||
|
let [luma] = img
|
||||||
|
.get_pixel(img.width() - 10, img.height() - 10)
|
||||||
|
.to_luma()
|
||||||
|
.0;
|
||||||
|
assert_eq!(luma, 255);
|
||||||
|
let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0;
|
||||||
|
assert_eq!(luma, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct SendImageCheckMediaquality<'a> {
|
||||||
|
pub(crate) viewtype: Viewtype,
|
||||||
|
pub(crate) media_quality_config: &'a str,
|
||||||
|
pub(crate) bytes: &'a [u8],
|
||||||
|
pub(crate) extension: &'a str,
|
||||||
|
pub(crate) has_exif: bool,
|
||||||
|
pub(crate) original_width: u32,
|
||||||
|
pub(crate) original_height: u32,
|
||||||
|
pub(crate) orientation: i32,
|
||||||
|
pub(crate) compressed_width: u32,
|
||||||
|
pub(crate) compressed_height: u32,
|
||||||
|
pub(crate) set_draft: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendImageCheckMediaquality<'_> {
|
||||||
|
pub(crate) async fn test(self) -> anyhow::Result<DynamicImage> {
|
||||||
|
let viewtype = self.viewtype;
|
||||||
|
let media_quality_config = self.media_quality_config;
|
||||||
|
let bytes = self.bytes;
|
||||||
|
let extension = self.extension;
|
||||||
|
let has_exif = self.has_exif;
|
||||||
|
let original_width = self.original_width;
|
||||||
|
let original_height = self.original_height;
|
||||||
|
let orientation = self.orientation;
|
||||||
|
let compressed_width = self.compressed_width;
|
||||||
|
let compressed_height = self.compressed_height;
|
||||||
|
let set_draft = self.set_draft;
|
||||||
|
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
alice
|
||||||
|
.set_config(Config::MediaQuality, Some(media_quality_config))
|
||||||
|
.await?;
|
||||||
|
let file = alice.get_blobdir().join("file").with_extension(extension);
|
||||||
|
let file_name = format!("file.{extension}");
|
||||||
|
|
||||||
|
fs::write(&file, &bytes)
|
||||||
|
.await
|
||||||
|
.context("failed to write file")?;
|
||||||
|
check_image_size(&file, original_width, original_height);
|
||||||
|
|
||||||
|
let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
|
||||||
|
if has_exif {
|
||||||
|
let exif = exif.unwrap();
|
||||||
|
assert_eq!(exif_orientation(&exif, &alice), orientation);
|
||||||
|
} else {
|
||||||
|
assert!(exif.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut msg = Message::new(viewtype);
|
||||||
|
msg.set_file_and_deduplicate(&alice, &file, Some(&file_name), None)?;
|
||||||
|
let chat = alice.create_chat(&bob).await;
|
||||||
|
if set_draft {
|
||||||
|
chat.id.set_draft(&alice, Some(&mut msg)).await.unwrap();
|
||||||
|
msg = chat.id.get_draft(&alice).await.unwrap().unwrap();
|
||||||
|
assert_eq!(msg.get_viewtype(), Viewtype::File);
|
||||||
|
}
|
||||||
|
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||||
|
let alice_msg = alice.get_last_msg().await;
|
||||||
|
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
||||||
|
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
||||||
|
let file_saved = alice
|
||||||
|
.get_blobdir()
|
||||||
|
.join("saved-".to_string() + &alice_msg.get_filename().unwrap());
|
||||||
|
alice_msg.save_file(&alice, &file_saved).await?;
|
||||||
|
check_image_size(file_saved, compressed_width, compressed_height);
|
||||||
|
|
||||||
|
let bob_msg = bob.recv_msg(&sent).await;
|
||||||
|
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
|
||||||
|
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||||
|
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||||
|
let file_saved = bob
|
||||||
|
.get_blobdir()
|
||||||
|
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||||
|
bob_msg.save_file(&bob, &file_saved).await?;
|
||||||
|
if viewtype == Viewtype::File {
|
||||||
|
assert_eq!(file_saved.extension().unwrap(), extension);
|
||||||
|
let bytes1 = fs::read(&file_saved).await?;
|
||||||
|
assert_eq!(&bytes1, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||||
|
assert!(exif.is_none());
|
||||||
|
|
||||||
|
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
||||||
|
Ok(img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_send_big_gif_as_image() -> Result<()> {
|
||||||
|
let bytes = include_bytes!("../../test-data/image/screenshot.gif");
|
||||||
|
let (width, height) = (1920u32, 1080u32);
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
alice
|
||||||
|
.set_config(
|
||||||
|
Config::MediaQuality,
|
||||||
|
Some(&(MediaQuality::Worse as i32).to_string()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let file = alice.get_blobdir().join("file").with_extension("gif");
|
||||||
|
fs::write(&file, &bytes)
|
||||||
|
.await
|
||||||
|
.context("failed to write file")?;
|
||||||
|
let mut msg = Message::new(Viewtype::Image);
|
||||||
|
msg.set_file_and_deduplicate(&alice, &file, Some("file.gif"), None)?;
|
||||||
|
let chat = alice.create_chat(&bob).await;
|
||||||
|
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||||
|
let bob_msg = bob.recv_msg(&sent).await;
|
||||||
|
// DC must detect the image as GIF and send it w/o reencoding.
|
||||||
|
assert_eq!(bob_msg.get_viewtype(), Viewtype::Gif);
|
||||||
|
assert_eq!(bob_msg.get_width() as u32, width);
|
||||||
|
assert_eq!(bob_msg.get_height() as u32, height);
|
||||||
|
let file_saved = bob
|
||||||
|
.get_blobdir()
|
||||||
|
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||||
|
bob_msg.save_file(&bob, &file_saved).await?;
|
||||||
|
let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||||
|
assert_eq!(file_size, bytes.len() as u64);
|
||||||
|
check_image_size(file_saved, width, height);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_send_gif_as_sticker() -> Result<()> {
|
||||||
|
let bytes = include_bytes!("../../test-data/image/image100x50.gif");
|
||||||
|
let alice = &TestContext::new_alice().await;
|
||||||
|
let file = alice.get_blobdir().join("file").with_extension("gif");
|
||||||
|
fs::write(&file, &bytes)
|
||||||
|
.await
|
||||||
|
.context("failed to write file")?;
|
||||||
|
let mut msg = Message::new(Viewtype::Sticker);
|
||||||
|
msg.set_file_and_deduplicate(alice, &file, None, None)?;
|
||||||
|
let chat = alice.get_self_chat().await;
|
||||||
|
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||||
|
let msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
|
||||||
|
// Message::force_sticker() wasn't used, still Viewtype::Sticker is preserved because of the
|
||||||
|
// extension.
|
||||||
|
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_create_and_deduplicate() -> Result<()> {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
|
let path = t.get_blobdir().join("anyfile.dat");
|
||||||
|
fs::write(&path, b"bla").await?;
|
||||||
|
let blob = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
||||||
|
assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f.dat");
|
||||||
|
assert_eq!(path.exists(), false);
|
||||||
|
|
||||||
|
assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
|
||||||
|
|
||||||
|
fs::write(&path, b"bla").await?;
|
||||||
|
let blob2 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
||||||
|
assert_eq!(blob2.name, blob.name);
|
||||||
|
|
||||||
|
let path_outside_blobdir = t.dir.path().join("anyfile.dat");
|
||||||
|
fs::write(&path_outside_blobdir, b"bla").await?;
|
||||||
|
let blob3 =
|
||||||
|
BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
|
||||||
|
assert!(path_outside_blobdir.exists());
|
||||||
|
assert_eq!(blob3.name, blob.name);
|
||||||
|
|
||||||
|
fs::write(&path, b"blabla").await?;
|
||||||
|
let blob4 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
||||||
|
assert_ne!(blob4.name, blob.name);
|
||||||
|
|
||||||
|
fs::remove_dir_all(t.get_blobdir()).await?;
|
||||||
|
let blob5 =
|
||||||
|
BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
|
||||||
|
assert_eq!(blob5.name, blob.name);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_create_and_deduplicate_from_bytes() -> Result<()> {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
|
fs::remove_dir(t.get_blobdir()).await?;
|
||||||
|
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
|
||||||
|
assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f");
|
||||||
|
|
||||||
|
assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
|
||||||
|
let modified1 = blob.to_abs_path().metadata()?.modified()?;
|
||||||
|
|
||||||
|
// Test that the modification time of the file is updated when a new file is created
|
||||||
|
// so that it's not deleted during housekeeping.
|
||||||
|
// We can't use SystemTime::shift() here because file creation uses the actual OS time,
|
||||||
|
// which we can't mock from our code.
|
||||||
|
tokio::time::sleep(Duration::from_millis(1100)).await;
|
||||||
|
|
||||||
|
let blob2 = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
|
||||||
|
assert_eq!(blob2.name, blob.name);
|
||||||
|
|
||||||
|
let modified2 = blob.to_abs_path().metadata()?.modified()?;
|
||||||
|
assert_ne!(modified1, modified2);
|
||||||
|
sql::housekeeping(&t).await?;
|
||||||
|
assert!(blob2.to_abs_path().exists());
|
||||||
|
|
||||||
|
// If we do shift the time by more than 1h, the blob file will be deleted during housekeeping:
|
||||||
|
SystemTime::shift(Duration::from_secs(65 * 60));
|
||||||
|
sql::housekeeping(&t).await?;
|
||||||
|
assert_eq!(blob2.to_abs_path().exists(), false);
|
||||||
|
|
||||||
|
let blob3 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
|
||||||
|
assert_ne!(blob3.name, blob.name);
|
||||||
|
|
||||||
|
{
|
||||||
|
// If something goes wrong and the blob file is overwritten,
|
||||||
|
// the correct content should be restored:
|
||||||
|
fs::write(blob3.to_abs_path(), b"bloblo").await?;
|
||||||
|
|
||||||
|
let blob4 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
|
||||||
|
let blob4_content = fs::read(blob4.to_abs_path()).await?;
|
||||||
|
assert_eq!(blob4_content, b"blabla");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
652
src/context.rs
652
src/context.rs
@@ -1480,654 +1480,4 @@ pub fn get_version_str() -> &'static str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod context_tests;
|
||||||
use anyhow::Context as _;
|
|
||||||
use strum::IntoEnumIterator;
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::chat::{get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, MuteDuration};
|
|
||||||
use crate::chatlist::Chatlist;
|
|
||||||
use crate::constants::Chattype;
|
|
||||||
use crate::mimeparser::SystemMessage;
|
|
||||||
use crate::receive_imf::receive_imf;
|
|
||||||
use crate::test_utils::{get_chat_msg, TestContext};
|
|
||||||
use crate::tools::{create_outgoing_rfc724_mid, SystemTime};
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_wrong_db() -> Result<()> {
|
|
||||||
let tmp = tempfile::tempdir()?;
|
|
||||||
let dbfile = tmp.path().join("db.sqlite");
|
|
||||||
tokio::fs::write(&dbfile, b"123").await?;
|
|
||||||
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await?;
|
|
||||||
|
|
||||||
// Broken database is indistinguishable from encrypted one.
|
|
||||||
assert_eq!(res.is_open().await, false);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_get_fresh_msgs() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let fresh = t.get_fresh_msgs().await.unwrap();
|
|
||||||
assert!(fresh.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn receive_msg(t: &TestContext, chat: &Chat) {
|
|
||||||
let members = get_chat_contacts(t, chat.id).await.unwrap();
|
|
||||||
let contact = Contact::get_by_id(t, *members.first().unwrap())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let msg = format!(
|
|
||||||
"From: {}\n\
|
|
||||||
To: alice@example.org\n\
|
|
||||||
Message-ID: <{}>\n\
|
|
||||||
Chat-Version: 1.0\n\
|
|
||||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
|
||||||
\n\
|
|
||||||
hello\n",
|
|
||||||
contact.get_addr(),
|
|
||||||
create_outgoing_rfc724_mid()
|
|
||||||
);
|
|
||||||
println!("{msg}");
|
|
||||||
receive_imf(t, msg.as_bytes(), false).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_get_fresh_msgs_and_muted_chats() {
|
|
||||||
// receive various mails in 3 chats
|
|
||||||
let t = TestContext::new_alice().await;
|
|
||||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
|
||||||
let claire = t.create_chat_with_contact("", "claire@g.it").await;
|
|
||||||
let dave = t.create_chat_with_contact("", "dave@g.it").await;
|
|
||||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
|
||||||
|
|
||||||
receive_msg(&t, &bob).await;
|
|
||||||
assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
|
|
||||||
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
|
|
||||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
|
||||||
|
|
||||||
receive_msg(&t, &claire).await;
|
|
||||||
receive_msg(&t, &claire).await;
|
|
||||||
assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 2);
|
|
||||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
|
||||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
|
|
||||||
|
|
||||||
receive_msg(&t, &dave).await;
|
|
||||||
receive_msg(&t, &dave).await;
|
|
||||||
receive_msg(&t, &dave).await;
|
|
||||||
assert_eq!(get_chat_msgs(&t, dave.id).await.unwrap().len(), 3);
|
|
||||||
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
|
||||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
|
|
||||||
|
|
||||||
// mute one of the chats
|
|
||||||
set_muted(&t, claire.id, MuteDuration::Forever)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
|
||||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted
|
|
||||||
|
|
||||||
// receive more messages
|
|
||||||
receive_msg(&t, &bob).await;
|
|
||||||
receive_msg(&t, &claire).await;
|
|
||||||
receive_msg(&t, &dave).await;
|
|
||||||
assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 3);
|
|
||||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
|
||||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
|
|
||||||
|
|
||||||
// unmute claire again
|
|
||||||
set_muted(&t, claire.id, MuteDuration::NotMuted)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
|
||||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_get_fresh_msgs_and_muted_until() {
|
|
||||||
let t = TestContext::new_alice().await;
|
|
||||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
|
||||||
receive_msg(&t, &bob).await;
|
|
||||||
assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
|
|
||||||
|
|
||||||
// chat is unmuted by default, here and in the following assert(),
|
|
||||||
// we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
|
|
||||||
// have the same view to the database.
|
|
||||||
assert!(!bob.is_muted());
|
|
||||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
|
||||||
|
|
||||||
// test get_fresh_msgs() with mute_until in the future
|
|
||||||
set_muted(
|
|
||||||
&t,
|
|
||||||
bob.id,
|
|
||||||
MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
|
||||||
assert!(bob.is_muted());
|
|
||||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
|
||||||
|
|
||||||
// to test get_fresh_msgs() with mute_until in the past,
|
|
||||||
// we need to modify the database directly
|
|
||||||
t.sql
|
|
||||||
.execute(
|
|
||||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
|
||||||
(time() - 3600, bob.id),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
|
||||||
assert!(!bob.is_muted());
|
|
||||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
|
||||||
|
|
||||||
// test get_fresh_msgs() with "forever" mute_until
|
|
||||||
set_muted(&t, bob.id, MuteDuration::Forever).await.unwrap();
|
|
||||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
|
||||||
assert!(bob.is_muted());
|
|
||||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
|
||||||
|
|
||||||
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
|
|
||||||
// that results in "muted forever" by definition.
|
|
||||||
t.sql
|
|
||||||
.execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
|
||||||
assert!(!bob.is_muted());
|
|
||||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_muted_context() -> Result<()> {
|
|
||||||
let t = TestContext::new_alice().await;
|
|
||||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
|
||||||
t.set_config(Config::IsMuted, Some("1")).await?;
|
|
||||||
let chat = t.create_chat_with_contact("", "bob@g.it").await;
|
|
||||||
receive_msg(&t, &chat).await;
|
|
||||||
|
|
||||||
// muted contexts should still show dimmed badge counters eg. in the sidebars,
|
|
||||||
// (same as muted chats show dimmed badge counters in the chatlist)
|
|
||||||
// therefore the fresh messages count should not be affected.
|
|
||||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_blobdir_exists() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let dbfile = tmp.path().join("db.sqlite");
|
|
||||||
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
|
||||||
assert!(blobdir.is_dir());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_wrong_blogdir() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let dbfile = tmp.path().join("db.sqlite");
|
|
||||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
|
||||||
tokio::fs::write(&blobdir, b"123").await.unwrap();
|
|
||||||
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await;
|
|
||||||
assert!(res.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_sqlite_parent_not_exists() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let subdir = tmp.path().join("subdir");
|
|
||||||
let dbfile = subdir.join("db.sqlite");
|
|
||||||
let dbfile2 = dbfile.clone();
|
|
||||||
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(subdir.is_dir());
|
|
||||||
assert!(dbfile2.is_file());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_with_empty_blobdir() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let dbfile = tmp.path().join("db.sqlite");
|
|
||||||
let blobdir = PathBuf::new();
|
|
||||||
let res = Context::with_blobdir(
|
|
||||||
dbfile,
|
|
||||||
blobdir,
|
|
||||||
1,
|
|
||||||
Events::new(),
|
|
||||||
StockStrings::new(),
|
|
||||||
Default::default(),
|
|
||||||
);
|
|
||||||
assert!(res.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_with_blobdir_not_exists() {
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let dbfile = tmp.path().join("db.sqlite");
|
|
||||||
let blobdir = tmp.path().join("blobs");
|
|
||||||
let res = Context::with_blobdir(
|
|
||||||
dbfile,
|
|
||||||
blobdir,
|
|
||||||
1,
|
|
||||||
Events::new(),
|
|
||||||
StockStrings::new(),
|
|
||||||
Default::default(),
|
|
||||||
);
|
|
||||||
assert!(res.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn no_crashes_on_context_deref() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
std::mem::drop(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_get_info() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
|
|
||||||
let info = t.get_info().await.unwrap();
|
|
||||||
assert!(info.contains_key("database_dir"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_info_no_context() {
|
|
||||||
let info = get_info();
|
|
||||||
assert!(info.contains_key("deltachat_core_version"));
|
|
||||||
assert!(!info.contains_key("database_dir"));
|
|
||||||
assert_eq!(info.get("level").unwrap(), "awesome");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_get_info_completeness() {
|
|
||||||
// For easier debugging,
|
|
||||||
// get_info() shall return all important information configurable by the Config-values.
|
|
||||||
//
|
|
||||||
// There are exceptions for Config-values considered to be unimportant,
|
|
||||||
// too sensitive or summarized in another item.
|
|
||||||
let skip_from_get_info = vec![
|
|
||||||
"addr",
|
|
||||||
"displayname",
|
|
||||||
"imap_certificate_checks",
|
|
||||||
"mail_server",
|
|
||||||
"mail_user",
|
|
||||||
"mail_pw",
|
|
||||||
"mail_port",
|
|
||||||
"mail_security",
|
|
||||||
"notify_about_wrong_pw",
|
|
||||||
"self_reporting_id",
|
|
||||||
"selfstatus",
|
|
||||||
"send_server",
|
|
||||||
"send_user",
|
|
||||||
"send_pw",
|
|
||||||
"send_port",
|
|
||||||
"send_security",
|
|
||||||
"server_flags",
|
|
||||||
"skip_start_messages",
|
|
||||||
"smtp_certificate_checks",
|
|
||||||
"proxy_url", // May contain passwords, don't leak it to the logs.
|
|
||||||
"socks5_enabled", // SOCKS5 options are deprecated.
|
|
||||||
"socks5_host",
|
|
||||||
"socks5_port",
|
|
||||||
"socks5_user",
|
|
||||||
"socks5_password",
|
|
||||||
"key_id",
|
|
||||||
"webxdc_integration",
|
|
||||||
"device_token",
|
|
||||||
"encrypted_device_token",
|
|
||||||
];
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let info = t.get_info().await.unwrap();
|
|
||||||
for key in Config::iter() {
|
|
||||||
let key: String = key.to_string();
|
|
||||||
if !skip_from_get_info.contains(&&*key)
|
|
||||||
&& !key.starts_with("configured")
|
|
||||||
&& !key.starts_with("sys.")
|
|
||||||
{
|
|
||||||
assert!(
|
|
||||||
info.contains_key(&*key),
|
|
||||||
"'{key}' missing in get_info() output"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_search_msgs() -> Result<()> {
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
let self_talk = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
|
|
||||||
let chat = alice
|
|
||||||
.create_chat_with_contact("Bob", "bob@example.org")
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Global search finds nothing.
|
|
||||||
let res = alice.search_msgs(None, "foo").await?;
|
|
||||||
assert!(res.is_empty());
|
|
||||||
|
|
||||||
// Search in chat with Bob finds nothing.
|
|
||||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
|
||||||
assert!(res.is_empty());
|
|
||||||
|
|
||||||
// Add messages to chat with Bob.
|
|
||||||
let mut msg1 = Message::new_text("foobar".to_string());
|
|
||||||
send_msg(&alice, chat.id, &mut msg1).await?;
|
|
||||||
|
|
||||||
let mut msg2 = Message::new_text("barbaz".to_string());
|
|
||||||
send_msg(&alice, chat.id, &mut msg2).await?;
|
|
||||||
|
|
||||||
alice.send_text(chat.id, "Δ-Chat").await;
|
|
||||||
|
|
||||||
// Global search with a part of text finds the message.
|
|
||||||
let res = alice.search_msgs(None, "ob").await?;
|
|
||||||
assert_eq!(res.len(), 1);
|
|
||||||
|
|
||||||
// Global search for "bar" matches both "foobar" and "barbaz".
|
|
||||||
let res = alice.search_msgs(None, "bar").await?;
|
|
||||||
assert_eq!(res.len(), 2);
|
|
||||||
|
|
||||||
// Message added later is returned first.
|
|
||||||
assert_eq!(res.first(), Some(&msg2.id));
|
|
||||||
assert_eq!(res.get(1), Some(&msg1.id));
|
|
||||||
|
|
||||||
// Search is case-insensitive.
|
|
||||||
for chat_id in [None, Some(chat.id)] {
|
|
||||||
let res = alice.search_msgs(chat_id, "δ-chat").await?;
|
|
||||||
assert_eq!(res.len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global search with longer text does not find any message.
|
|
||||||
let res = alice.search_msgs(None, "foobarbaz").await?;
|
|
||||||
assert!(res.is_empty());
|
|
||||||
|
|
||||||
// Search for random string finds nothing.
|
|
||||||
let res = alice.search_msgs(None, "abc").await?;
|
|
||||||
assert!(res.is_empty());
|
|
||||||
|
|
||||||
// Search in chat with Bob finds the message.
|
|
||||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
|
||||||
assert_eq!(res.len(), 1);
|
|
||||||
|
|
||||||
// Search in Saved Messages does not find the message.
|
|
||||||
let res = alice.search_msgs(Some(self_talk), "foo").await?;
|
|
||||||
assert!(res.is_empty());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_search_unaccepted_requests() -> Result<()> {
|
|
||||||
let t = TestContext::new_alice().await;
|
|
||||||
receive_imf(
|
|
||||||
&t,
|
|
||||||
b"From: BobBar <bob@example.org>\n\
|
|
||||||
To: alice@example.org\n\
|
|
||||||
Subject: foo\n\
|
|
||||||
Message-ID: <msg1234@example.org>\n\
|
|
||||||
Chat-Version: 1.0\n\
|
|
||||||
Date: Tue, 25 Oct 2022 13:37:00 +0000\n\
|
|
||||||
\n\
|
|
||||||
hello bob, foobar test!\n",
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let chat_id = t.get_last_msg().await.get_chat_id();
|
|
||||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
|
||||||
assert_eq!(chat.get_type(), Chattype::Single);
|
|
||||||
assert!(chat.is_contact_request());
|
|
||||||
|
|
||||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
|
||||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
|
||||||
|
|
||||||
chat_id.block(&t).await?;
|
|
||||||
|
|
||||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
|
|
||||||
assert_eq!(
|
|
||||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0);
|
|
||||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0);
|
|
||||||
|
|
||||||
let contact_ids = get_chat_contacts(&t, chat_id).await?;
|
|
||||||
Contact::unblock(&t, *contact_ids.first().unwrap()).await?;
|
|
||||||
|
|
||||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
|
||||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_limit_search_msgs() -> Result<()> {
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
let chat = alice
|
|
||||||
.create_chat_with_contact("Bob", "bob@example.org")
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Add 999 messages
|
|
||||||
let mut msg = Message::new_text("foobar".to_string());
|
|
||||||
for _ in 0..999 {
|
|
||||||
send_msg(&alice, chat.id, &mut msg).await?;
|
|
||||||
}
|
|
||||||
let res = alice.search_msgs(None, "foo").await?;
|
|
||||||
assert_eq!(res.len(), 999);
|
|
||||||
|
|
||||||
// Add one more message, no limit yet
|
|
||||||
send_msg(&alice, chat.id, &mut msg).await?;
|
|
||||||
let res = alice.search_msgs(None, "foo").await?;
|
|
||||||
assert_eq!(res.len(), 1000);
|
|
||||||
|
|
||||||
// Add one more message, that one is truncated then
|
|
||||||
send_msg(&alice, chat.id, &mut msg).await?;
|
|
||||||
let res = alice.search_msgs(None, "foo").await?;
|
|
||||||
assert_eq!(res.len(), 1000);
|
|
||||||
|
|
||||||
// In-chat should not be not limited
|
|
||||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
|
||||||
assert_eq!(res.len(), 1001);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_check_passphrase() -> Result<()> {
|
|
||||||
let dir = tempdir()?;
|
|
||||||
let dbfile = dir.path().join("db.sqlite");
|
|
||||||
|
|
||||||
let context = ContextBuilder::new(dbfile.clone())
|
|
||||||
.with_id(1)
|
|
||||||
.build()
|
|
||||||
.await
|
|
||||||
.context("failed to create context")?;
|
|
||||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
|
||||||
assert_eq!(context.is_open().await, true);
|
|
||||||
drop(context);
|
|
||||||
|
|
||||||
let context = ContextBuilder::new(dbfile)
|
|
||||||
.with_id(2)
|
|
||||||
.build()
|
|
||||||
.await
|
|
||||||
.context("failed to create context")?;
|
|
||||||
assert_eq!(context.is_open().await, false);
|
|
||||||
assert_eq!(context.check_passphrase("bar".to_string()).await?, false);
|
|
||||||
assert_eq!(context.open("false".to_string()).await?, false);
|
|
||||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_context_change_passphrase() -> Result<()> {
|
|
||||||
let dir = tempdir()?;
|
|
||||||
let dbfile = dir.path().join("db.sqlite");
|
|
||||||
|
|
||||||
let context = ContextBuilder::new(dbfile)
|
|
||||||
.with_id(1)
|
|
||||||
.build()
|
|
||||||
.await
|
|
||||||
.context("failed to create context")?;
|
|
||||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
|
||||||
assert_eq!(context.is_open().await, true);
|
|
||||||
|
|
||||||
context
|
|
||||||
.set_config(Config::Addr, Some("alice@example.org"))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
context
|
|
||||||
.change_passphrase("bar".to_string())
|
|
||||||
.await
|
|
||||||
.context("Failed to change passphrase")?;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
context.get_config(Config::Addr).await?.unwrap(),
|
|
||||||
"alice@example.org"
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_ongoing() -> Result<()> {
|
|
||||||
let context = TestContext::new().await;
|
|
||||||
|
|
||||||
// No ongoing process allocated.
|
|
||||||
assert!(context.shall_stop_ongoing().await);
|
|
||||||
|
|
||||||
let receiver = context.alloc_ongoing().await?;
|
|
||||||
|
|
||||||
// Cannot allocate another ongoing process while the first one is running.
|
|
||||||
assert!(context.alloc_ongoing().await.is_err());
|
|
||||||
|
|
||||||
// Stop signal is not sent yet.
|
|
||||||
assert!(receiver.try_recv().is_err());
|
|
||||||
|
|
||||||
assert!(!context.shall_stop_ongoing().await);
|
|
||||||
|
|
||||||
// Send the stop signal.
|
|
||||||
context.stop_ongoing().await;
|
|
||||||
|
|
||||||
// Receive stop signal.
|
|
||||||
receiver.recv().await?;
|
|
||||||
|
|
||||||
assert!(context.shall_stop_ongoing().await);
|
|
||||||
|
|
||||||
// Ongoing process is still running even though stop signal was received,
|
|
||||||
// so another one cannot be allocated.
|
|
||||||
assert!(context.alloc_ongoing().await.is_err());
|
|
||||||
|
|
||||||
context.free_ongoing().await;
|
|
||||||
|
|
||||||
// No ongoing process allocated, should have been stopped already.
|
|
||||||
assert!(context.shall_stop_ongoing().await);
|
|
||||||
|
|
||||||
// Another ongoing process can be allocated now.
|
|
||||||
let _receiver = context.alloc_ongoing().await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_get_next_msgs() -> Result<()> {
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
let bob = TestContext::new_bob().await;
|
|
||||||
|
|
||||||
let alice_chat = alice.create_chat(&bob).await;
|
|
||||||
|
|
||||||
assert!(alice.get_next_msgs().await?.is_empty());
|
|
||||||
assert!(bob.get_next_msgs().await?.is_empty());
|
|
||||||
|
|
||||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
|
||||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
|
||||||
|
|
||||||
let bob_next_msg_ids = bob.get_next_msgs().await?;
|
|
||||||
assert_eq!(bob_next_msg_ids.len(), 1);
|
|
||||||
assert_eq!(bob_next_msg_ids.first(), Some(&received_msg.id));
|
|
||||||
|
|
||||||
bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32())
|
|
||||||
.await?;
|
|
||||||
assert!(bob.get_next_msgs().await?.is_empty());
|
|
||||||
|
|
||||||
// Next messages include self-sent messages.
|
|
||||||
let alice_next_msg_ids = alice.get_next_msgs().await?;
|
|
||||||
assert_eq!(alice_next_msg_ids.len(), 1);
|
|
||||||
assert_eq!(alice_next_msg_ids.first(), Some(&sent_msg.sender_msg_id));
|
|
||||||
|
|
||||||
alice
|
|
||||||
.set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32())
|
|
||||||
.await?;
|
|
||||||
assert!(alice.get_next_msgs().await?.is_empty());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_draft_self_report() -> Result<()> {
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
|
|
||||||
let chat_id = alice.draft_self_report().await?;
|
|
||||||
let msg = get_chat_msg(&alice, chat_id, 0, 1).await;
|
|
||||||
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
|
||||||
|
|
||||||
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
|
||||||
assert!(chat.is_protected());
|
|
||||||
|
|
||||||
let mut draft = chat_id.get_draft(&alice).await?.unwrap();
|
|
||||||
assert!(draft.text.starts_with("core_version"));
|
|
||||||
|
|
||||||
// Test that sending into the protected chat works:
|
|
||||||
let _sent = alice.send_msg(chat_id, &mut draft).await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
assert_eq!(
|
|
||||||
alice.get_config(Config::ShowEmails).await?,
|
|
||||||
Some("2".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
// Change the config circumventing the cache
|
|
||||||
// This simulates what the notification plugin on iOS might do
|
|
||||||
// because it runs in a different process
|
|
||||||
alice
|
|
||||||
.sql
|
|
||||||
.execute(
|
|
||||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
|
|
||||||
(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Alice's Delta Chat doesn't know about it yet:
|
|
||||||
assert_eq!(
|
|
||||||
alice.get_config(Config::ShowEmails).await?,
|
|
||||||
Some("2".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
// Starting IO will fail of course because no server settings are configured,
|
|
||||||
// but it should invalidate the caches:
|
|
||||||
alice.start_io().await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
alice.get_config(Config::ShowEmails).await?,
|
|
||||||
Some("0".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
649
src/context/context_tests.rs
Normal file
649
src/context/context_tests.rs
Normal file
@@ -0,0 +1,649 @@
|
|||||||
|
use anyhow::Context as _;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::chat::{get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, MuteDuration};
|
||||||
|
use crate::chatlist::Chatlist;
|
||||||
|
use crate::constants::Chattype;
|
||||||
|
use crate::mimeparser::SystemMessage;
|
||||||
|
use crate::receive_imf::receive_imf;
|
||||||
|
use crate::test_utils::{get_chat_msg, TestContext};
|
||||||
|
use crate::tools::{create_outgoing_rfc724_mid, SystemTime};
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_wrong_db() -> Result<()> {
|
||||||
|
let tmp = tempfile::tempdir()?;
|
||||||
|
let dbfile = tmp.path().join("db.sqlite");
|
||||||
|
tokio::fs::write(&dbfile, b"123").await?;
|
||||||
|
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await?;
|
||||||
|
|
||||||
|
// Broken database is indistinguishable from encrypted one.
|
||||||
|
assert_eq!(res.is_open().await, false);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_get_fresh_msgs() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let fresh = t.get_fresh_msgs().await.unwrap();
|
||||||
|
assert!(fresh.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_msg(t: &TestContext, chat: &Chat) {
|
||||||
|
let members = get_chat_contacts(t, chat.id).await.unwrap();
|
||||||
|
let contact = Contact::get_by_id(t, *members.first().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let msg = format!(
|
||||||
|
"From: {}\n\
|
||||||
|
To: alice@example.org\n\
|
||||||
|
Message-ID: <{}>\n\
|
||||||
|
Chat-Version: 1.0\n\
|
||||||
|
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||||
|
\n\
|
||||||
|
hello\n",
|
||||||
|
contact.get_addr(),
|
||||||
|
create_outgoing_rfc724_mid()
|
||||||
|
);
|
||||||
|
println!("{msg}");
|
||||||
|
receive_imf(t, msg.as_bytes(), false).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_get_fresh_msgs_and_muted_chats() {
|
||||||
|
// receive various mails in 3 chats
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||||
|
let claire = t.create_chat_with_contact("", "claire@g.it").await;
|
||||||
|
let dave = t.create_chat_with_contact("", "dave@g.it").await;
|
||||||
|
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||||
|
|
||||||
|
receive_msg(&t, &bob).await;
|
||||||
|
assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
|
||||||
|
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
|
||||||
|
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||||
|
|
||||||
|
receive_msg(&t, &claire).await;
|
||||||
|
receive_msg(&t, &claire).await;
|
||||||
|
assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 2);
|
||||||
|
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||||
|
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
|
||||||
|
|
||||||
|
receive_msg(&t, &dave).await;
|
||||||
|
receive_msg(&t, &dave).await;
|
||||||
|
receive_msg(&t, &dave).await;
|
||||||
|
assert_eq!(get_chat_msgs(&t, dave.id).await.unwrap().len(), 3);
|
||||||
|
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||||
|
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
|
||||||
|
|
||||||
|
// mute one of the chats
|
||||||
|
set_muted(&t, claire.id, MuteDuration::Forever)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||||
|
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted
|
||||||
|
|
||||||
|
// receive more messages
|
||||||
|
receive_msg(&t, &bob).await;
|
||||||
|
receive_msg(&t, &claire).await;
|
||||||
|
receive_msg(&t, &dave).await;
|
||||||
|
assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 3);
|
||||||
|
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||||
|
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
|
||||||
|
|
||||||
|
// unmute claire again
|
||||||
|
set_muted(&t, claire.id, MuteDuration::NotMuted)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||||
|
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_get_fresh_msgs_and_muted_until() {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||||
|
receive_msg(&t, &bob).await;
|
||||||
|
assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
|
||||||
|
|
||||||
|
// chat is unmuted by default, here and in the following assert(),
|
||||||
|
// we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
|
||||||
|
// have the same view to the database.
|
||||||
|
assert!(!bob.is_muted());
|
||||||
|
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||||
|
|
||||||
|
// test get_fresh_msgs() with mute_until in the future
|
||||||
|
set_muted(
|
||||||
|
&t,
|
||||||
|
bob.id,
|
||||||
|
MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||||
|
assert!(bob.is_muted());
|
||||||
|
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||||
|
|
||||||
|
// to test get_fresh_msgs() with mute_until in the past,
|
||||||
|
// we need to modify the database directly
|
||||||
|
t.sql
|
||||||
|
.execute(
|
||||||
|
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||||
|
(time() - 3600, bob.id),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||||
|
assert!(!bob.is_muted());
|
||||||
|
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||||
|
|
||||||
|
// test get_fresh_msgs() with "forever" mute_until
|
||||||
|
set_muted(&t, bob.id, MuteDuration::Forever).await.unwrap();
|
||||||
|
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||||
|
assert!(bob.is_muted());
|
||||||
|
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||||
|
|
||||||
|
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
|
||||||
|
// that results in "muted forever" by definition.
|
||||||
|
t.sql
|
||||||
|
.execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||||
|
assert!(!bob.is_muted());
|
||||||
|
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_muted_context() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||||
|
t.set_config(Config::IsMuted, Some("1")).await?;
|
||||||
|
let chat = t.create_chat_with_contact("", "bob@g.it").await;
|
||||||
|
receive_msg(&t, &chat).await;
|
||||||
|
|
||||||
|
// muted contexts should still show dimmed badge counters eg. in the sidebars,
|
||||||
|
// (same as muted chats show dimmed badge counters in the chatlist)
|
||||||
|
// therefore the fresh messages count should not be affected.
|
||||||
|
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_blobdir_exists() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let dbfile = tmp.path().join("db.sqlite");
|
||||||
|
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||||
|
assert!(blobdir.is_dir());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_wrong_blogdir() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let dbfile = tmp.path().join("db.sqlite");
|
||||||
|
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||||
|
tokio::fs::write(&blobdir, b"123").await.unwrap();
|
||||||
|
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_sqlite_parent_not_exists() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let subdir = tmp.path().join("subdir");
|
||||||
|
let dbfile = subdir.join("db.sqlite");
|
||||||
|
let dbfile2 = dbfile.clone();
|
||||||
|
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(subdir.is_dir());
|
||||||
|
assert!(dbfile2.is_file());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_with_empty_blobdir() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let dbfile = tmp.path().join("db.sqlite");
|
||||||
|
let blobdir = PathBuf::new();
|
||||||
|
let res = Context::with_blobdir(
|
||||||
|
dbfile,
|
||||||
|
blobdir,
|
||||||
|
1,
|
||||||
|
Events::new(),
|
||||||
|
StockStrings::new(),
|
||||||
|
Default::default(),
|
||||||
|
);
|
||||||
|
assert!(res.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_with_blobdir_not_exists() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let dbfile = tmp.path().join("db.sqlite");
|
||||||
|
let blobdir = tmp.path().join("blobs");
|
||||||
|
let res = Context::with_blobdir(
|
||||||
|
dbfile,
|
||||||
|
blobdir,
|
||||||
|
1,
|
||||||
|
Events::new(),
|
||||||
|
StockStrings::new(),
|
||||||
|
Default::default(),
|
||||||
|
);
|
||||||
|
assert!(res.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn no_crashes_on_context_deref() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
std::mem::drop(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_get_info() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
|
let info = t.get_info().await.unwrap();
|
||||||
|
assert!(info.contains_key("database_dir"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_info_no_context() {
|
||||||
|
let info = get_info();
|
||||||
|
assert!(info.contains_key("deltachat_core_version"));
|
||||||
|
assert!(!info.contains_key("database_dir"));
|
||||||
|
assert_eq!(info.get("level").unwrap(), "awesome");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_get_info_completeness() {
|
||||||
|
// For easier debugging,
|
||||||
|
// get_info() shall return all important information configurable by the Config-values.
|
||||||
|
//
|
||||||
|
// There are exceptions for Config-values considered to be unimportant,
|
||||||
|
// too sensitive or summarized in another item.
|
||||||
|
let skip_from_get_info = vec![
|
||||||
|
"addr",
|
||||||
|
"displayname",
|
||||||
|
"imap_certificate_checks",
|
||||||
|
"mail_server",
|
||||||
|
"mail_user",
|
||||||
|
"mail_pw",
|
||||||
|
"mail_port",
|
||||||
|
"mail_security",
|
||||||
|
"notify_about_wrong_pw",
|
||||||
|
"self_reporting_id",
|
||||||
|
"selfstatus",
|
||||||
|
"send_server",
|
||||||
|
"send_user",
|
||||||
|
"send_pw",
|
||||||
|
"send_port",
|
||||||
|
"send_security",
|
||||||
|
"server_flags",
|
||||||
|
"skip_start_messages",
|
||||||
|
"smtp_certificate_checks",
|
||||||
|
"proxy_url", // May contain passwords, don't leak it to the logs.
|
||||||
|
"socks5_enabled", // SOCKS5 options are deprecated.
|
||||||
|
"socks5_host",
|
||||||
|
"socks5_port",
|
||||||
|
"socks5_user",
|
||||||
|
"socks5_password",
|
||||||
|
"key_id",
|
||||||
|
"webxdc_integration",
|
||||||
|
"device_token",
|
||||||
|
"encrypted_device_token",
|
||||||
|
];
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let info = t.get_info().await.unwrap();
|
||||||
|
for key in Config::iter() {
|
||||||
|
let key: String = key.to_string();
|
||||||
|
if !skip_from_get_info.contains(&&*key)
|
||||||
|
&& !key.starts_with("configured")
|
||||||
|
&& !key.starts_with("sys.")
|
||||||
|
{
|
||||||
|
assert!(
|
||||||
|
info.contains_key(&*key),
|
||||||
|
"'{key}' missing in get_info() output"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_search_msgs() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let self_talk = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
|
||||||
|
let chat = alice
|
||||||
|
.create_chat_with_contact("Bob", "bob@example.org")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Global search finds nothing.
|
||||||
|
let res = alice.search_msgs(None, "foo").await?;
|
||||||
|
assert!(res.is_empty());
|
||||||
|
|
||||||
|
// Search in chat with Bob finds nothing.
|
||||||
|
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||||
|
assert!(res.is_empty());
|
||||||
|
|
||||||
|
// Add messages to chat with Bob.
|
||||||
|
let mut msg1 = Message::new_text("foobar".to_string());
|
||||||
|
send_msg(&alice, chat.id, &mut msg1).await?;
|
||||||
|
|
||||||
|
let mut msg2 = Message::new_text("barbaz".to_string());
|
||||||
|
send_msg(&alice, chat.id, &mut msg2).await?;
|
||||||
|
|
||||||
|
alice.send_text(chat.id, "Δ-Chat").await;
|
||||||
|
|
||||||
|
// Global search with a part of text finds the message.
|
||||||
|
let res = alice.search_msgs(None, "ob").await?;
|
||||||
|
assert_eq!(res.len(), 1);
|
||||||
|
|
||||||
|
// Global search for "bar" matches both "foobar" and "barbaz".
|
||||||
|
let res = alice.search_msgs(None, "bar").await?;
|
||||||
|
assert_eq!(res.len(), 2);
|
||||||
|
|
||||||
|
// Message added later is returned first.
|
||||||
|
assert_eq!(res.first(), Some(&msg2.id));
|
||||||
|
assert_eq!(res.get(1), Some(&msg1.id));
|
||||||
|
|
||||||
|
// Search is case-insensitive.
|
||||||
|
for chat_id in [None, Some(chat.id)] {
|
||||||
|
let res = alice.search_msgs(chat_id, "δ-chat").await?;
|
||||||
|
assert_eq!(res.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global search with longer text does not find any message.
|
||||||
|
let res = alice.search_msgs(None, "foobarbaz").await?;
|
||||||
|
assert!(res.is_empty());
|
||||||
|
|
||||||
|
// Search for random string finds nothing.
|
||||||
|
let res = alice.search_msgs(None, "abc").await?;
|
||||||
|
assert!(res.is_empty());
|
||||||
|
|
||||||
|
// Search in chat with Bob finds the message.
|
||||||
|
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||||
|
assert_eq!(res.len(), 1);
|
||||||
|
|
||||||
|
// Search in Saved Messages does not find the message.
|
||||||
|
let res = alice.search_msgs(Some(self_talk), "foo").await?;
|
||||||
|
assert!(res.is_empty());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_search_unaccepted_requests() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
receive_imf(
|
||||||
|
&t,
|
||||||
|
b"From: BobBar <bob@example.org>\n\
|
||||||
|
To: alice@example.org\n\
|
||||||
|
Subject: foo\n\
|
||||||
|
Message-ID: <msg1234@example.org>\n\
|
||||||
|
Chat-Version: 1.0\n\
|
||||||
|
Date: Tue, 25 Oct 2022 13:37:00 +0000\n\
|
||||||
|
\n\
|
||||||
|
hello bob, foobar test!\n",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let chat_id = t.get_last_msg().await.get_chat_id();
|
||||||
|
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||||
|
assert_eq!(chat.get_type(), Chattype::Single);
|
||||||
|
assert!(chat.is_contact_request());
|
||||||
|
|
||||||
|
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||||
|
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||||
|
|
||||||
|
chat_id.block(&t).await?;
|
||||||
|
|
||||||
|
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
|
||||||
|
assert_eq!(
|
||||||
|
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0);
|
||||||
|
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0);
|
||||||
|
|
||||||
|
let contact_ids = get_chat_contacts(&t, chat_id).await?;
|
||||||
|
Contact::unblock(&t, *contact_ids.first().unwrap()).await?;
|
||||||
|
|
||||||
|
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||||
|
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_limit_search_msgs() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let chat = alice
|
||||||
|
.create_chat_with_contact("Bob", "bob@example.org")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Add 999 messages
|
||||||
|
let mut msg = Message::new_text("foobar".to_string());
|
||||||
|
for _ in 0..999 {
|
||||||
|
send_msg(&alice, chat.id, &mut msg).await?;
|
||||||
|
}
|
||||||
|
let res = alice.search_msgs(None, "foo").await?;
|
||||||
|
assert_eq!(res.len(), 999);
|
||||||
|
|
||||||
|
// Add one more message, no limit yet
|
||||||
|
send_msg(&alice, chat.id, &mut msg).await?;
|
||||||
|
let res = alice.search_msgs(None, "foo").await?;
|
||||||
|
assert_eq!(res.len(), 1000);
|
||||||
|
|
||||||
|
// Add one more message, that one is truncated then
|
||||||
|
send_msg(&alice, chat.id, &mut msg).await?;
|
||||||
|
let res = alice.search_msgs(None, "foo").await?;
|
||||||
|
assert_eq!(res.len(), 1000);
|
||||||
|
|
||||||
|
// In-chat should not be not limited
|
||||||
|
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||||
|
assert_eq!(res.len(), 1001);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_check_passphrase() -> Result<()> {
|
||||||
|
let dir = tempdir()?;
|
||||||
|
let dbfile = dir.path().join("db.sqlite");
|
||||||
|
|
||||||
|
let context = ContextBuilder::new(dbfile.clone())
|
||||||
|
.with_id(1)
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.context("failed to create context")?;
|
||||||
|
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||||
|
assert_eq!(context.is_open().await, true);
|
||||||
|
drop(context);
|
||||||
|
|
||||||
|
let context = ContextBuilder::new(dbfile)
|
||||||
|
.with_id(2)
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.context("failed to create context")?;
|
||||||
|
assert_eq!(context.is_open().await, false);
|
||||||
|
assert_eq!(context.check_passphrase("bar".to_string()).await?, false);
|
||||||
|
assert_eq!(context.open("false".to_string()).await?, false);
|
||||||
|
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_context_change_passphrase() -> Result<()> {
|
||||||
|
let dir = tempdir()?;
|
||||||
|
let dbfile = dir.path().join("db.sqlite");
|
||||||
|
|
||||||
|
let context = ContextBuilder::new(dbfile)
|
||||||
|
.with_id(1)
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.context("failed to create context")?;
|
||||||
|
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||||
|
assert_eq!(context.is_open().await, true);
|
||||||
|
|
||||||
|
context
|
||||||
|
.set_config(Config::Addr, Some("alice@example.org"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
context
|
||||||
|
.change_passphrase("bar".to_string())
|
||||||
|
.await
|
||||||
|
.context("Failed to change passphrase")?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
context.get_config(Config::Addr).await?.unwrap(),
|
||||||
|
"alice@example.org"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_ongoing() -> Result<()> {
|
||||||
|
let context = TestContext::new().await;
|
||||||
|
|
||||||
|
// No ongoing process allocated.
|
||||||
|
assert!(context.shall_stop_ongoing().await);
|
||||||
|
|
||||||
|
let receiver = context.alloc_ongoing().await?;
|
||||||
|
|
||||||
|
// Cannot allocate another ongoing process while the first one is running.
|
||||||
|
assert!(context.alloc_ongoing().await.is_err());
|
||||||
|
|
||||||
|
// Stop signal is not sent yet.
|
||||||
|
assert!(receiver.try_recv().is_err());
|
||||||
|
|
||||||
|
assert!(!context.shall_stop_ongoing().await);
|
||||||
|
|
||||||
|
// Send the stop signal.
|
||||||
|
context.stop_ongoing().await;
|
||||||
|
|
||||||
|
// Receive stop signal.
|
||||||
|
receiver.recv().await?;
|
||||||
|
|
||||||
|
assert!(context.shall_stop_ongoing().await);
|
||||||
|
|
||||||
|
// Ongoing process is still running even though stop signal was received,
|
||||||
|
// so another one cannot be allocated.
|
||||||
|
assert!(context.alloc_ongoing().await.is_err());
|
||||||
|
|
||||||
|
context.free_ongoing().await;
|
||||||
|
|
||||||
|
// No ongoing process allocated, should have been stopped already.
|
||||||
|
assert!(context.shall_stop_ongoing().await);
|
||||||
|
|
||||||
|
// Another ongoing process can be allocated now.
|
||||||
|
let _receiver = context.alloc_ongoing().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_get_next_msgs() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
|
||||||
|
let alice_chat = alice.create_chat(&bob).await;
|
||||||
|
|
||||||
|
assert!(alice.get_next_msgs().await?.is_empty());
|
||||||
|
assert!(bob.get_next_msgs().await?.is_empty());
|
||||||
|
|
||||||
|
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||||
|
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||||
|
|
||||||
|
let bob_next_msg_ids = bob.get_next_msgs().await?;
|
||||||
|
assert_eq!(bob_next_msg_ids.len(), 1);
|
||||||
|
assert_eq!(bob_next_msg_ids.first(), Some(&received_msg.id));
|
||||||
|
|
||||||
|
bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32())
|
||||||
|
.await?;
|
||||||
|
assert!(bob.get_next_msgs().await?.is_empty());
|
||||||
|
|
||||||
|
// Next messages include self-sent messages.
|
||||||
|
let alice_next_msg_ids = alice.get_next_msgs().await?;
|
||||||
|
assert_eq!(alice_next_msg_ids.len(), 1);
|
||||||
|
assert_eq!(alice_next_msg_ids.first(), Some(&sent_msg.sender_msg_id));
|
||||||
|
|
||||||
|
alice
|
||||||
|
.set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32())
|
||||||
|
.await?;
|
||||||
|
assert!(alice.get_next_msgs().await?.is_empty());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_draft_self_report() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
|
||||||
|
let chat_id = alice.draft_self_report().await?;
|
||||||
|
let msg = get_chat_msg(&alice, chat_id, 0, 1).await;
|
||||||
|
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
||||||
|
|
||||||
|
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
||||||
|
assert!(chat.is_protected());
|
||||||
|
|
||||||
|
let mut draft = chat_id.get_draft(&alice).await?.unwrap();
|
||||||
|
assert!(draft.text.starts_with("core_version"));
|
||||||
|
|
||||||
|
// Test that sending into the protected chat works:
|
||||||
|
let _sent = alice.send_msg(chat_id, &mut draft).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
assert_eq!(
|
||||||
|
alice.get_config(Config::ShowEmails).await?,
|
||||||
|
Some("2".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Change the config circumventing the cache
|
||||||
|
// This simulates what the notification plugin on iOS might do
|
||||||
|
// because it runs in a different process
|
||||||
|
alice
|
||||||
|
.sql
|
||||||
|
.execute(
|
||||||
|
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Alice's Delta Chat doesn't know about it yet:
|
||||||
|
assert_eq!(
|
||||||
|
alice.get_config(Config::ShowEmails).await?,
|
||||||
|
Some("2".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Starting IO will fail of course because no server settings are configured,
|
||||||
|
// but it should invalidate the caches:
|
||||||
|
alice.start_io().await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
alice.get_config(Config::ShowEmails).await?,
|
||||||
|
Some("0".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
806
src/ephemeral.rs
806
src/ephemeral.rs
@@ -713,808 +713,4 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod ephemeral_tests;
|
||||||
use super::*;
|
|
||||||
use crate::chat::{marknoticed_chat, set_muted, ChatVisibility, MuteDuration};
|
|
||||||
use crate::config::Config;
|
|
||||||
use crate::constants::DC_CHAT_ID_ARCHIVED_LINK;
|
|
||||||
use crate::download::DownloadState;
|
|
||||||
use crate::location;
|
|
||||||
use crate::message::markseen_msgs;
|
|
||||||
use crate::receive_imf::receive_imf;
|
|
||||||
use crate::test_utils::{TestContext, TestContextManager};
|
|
||||||
use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE;
|
|
||||||
use crate::{
|
|
||||||
chat::{self, create_group_chat, send_text_msg, Chat, ChatItem, ProtectionStatus},
|
|
||||||
tools::IsNoneOrEmpty,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_stock_ephemeral_messages() {
|
|
||||||
let context = TestContext::new().await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
stock_ephemeral_timer_changed(&context, Timer::Disabled, ContactId::SELF).await,
|
|
||||||
"You disabled message deletion timer."
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
stock_ephemeral_timer_changed(
|
|
||||||
&context,
|
|
||||||
Timer::Enabled { duration: 1 },
|
|
||||||
ContactId::SELF
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
"You set message deletion timer to 1 s."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stock_ephemeral_timer_changed(
|
|
||||||
&context,
|
|
||||||
Timer::Enabled { duration: 30 },
|
|
||||||
ContactId::SELF
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
"You set message deletion timer to 30 s."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stock_ephemeral_timer_changed(
|
|
||||||
&context,
|
|
||||||
Timer::Enabled { duration: 60 },
|
|
||||||
ContactId::SELF
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
"You set message deletion timer to 1 minute."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stock_ephemeral_timer_changed(
|
|
||||||
&context,
|
|
||||||
Timer::Enabled { duration: 90 },
|
|
||||||
ContactId::SELF
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
"You set message deletion timer to 1.5 minutes."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stock_ephemeral_timer_changed(
|
|
||||||
&context,
|
|
||||||
Timer::Enabled { duration: 30 * 60 },
|
|
||||||
ContactId::SELF
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
"You set message deletion timer to 30 minutes."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stock_ephemeral_timer_changed(
|
|
||||||
&context,
|
|
||||||
Timer::Enabled { duration: 60 * 60 },
|
|
||||||
ContactId::SELF
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
"You set message deletion timer to 1 hour."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stock_ephemeral_timer_changed(
|
|
||||||
&context,
|
|
||||||
Timer::Enabled { duration: 5400 },
|
|
||||||
ContactId::SELF
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
"You set message deletion timer to 1.5 hours."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stock_ephemeral_timer_changed(
|
|
||||||
&context,
|
|
||||||
Timer::Enabled {
|
|
||||||
duration: 2 * 60 * 60
|
|
||||||
},
|
|
||||||
ContactId::SELF
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
"You set message deletion timer to 2 hours."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stock_ephemeral_timer_changed(
|
|
||||||
&context,
|
|
||||||
Timer::Enabled {
|
|
||||||
duration: 24 * 60 * 60
|
|
||||||
},
|
|
||||||
ContactId::SELF
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
"You set message deletion timer to 1 day."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stock_ephemeral_timer_changed(
|
|
||||||
&context,
|
|
||||||
Timer::Enabled {
|
|
||||||
duration: 2 * 24 * 60 * 60
|
|
||||||
},
|
|
||||||
ContactId::SELF
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
"You set message deletion timer to 2 days."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stock_ephemeral_timer_changed(
|
|
||||||
&context,
|
|
||||||
Timer::Enabled {
|
|
||||||
duration: 7 * 24 * 60 * 60
|
|
||||||
},
|
|
||||||
ContactId::SELF
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
"You set message deletion timer to 1 week."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stock_ephemeral_timer_changed(
|
|
||||||
&context,
|
|
||||||
Timer::Enabled {
|
|
||||||
duration: 4 * 7 * 24 * 60 * 60
|
|
||||||
},
|
|
||||||
ContactId::SELF
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
"You set message deletion timer to 4 weeks."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test enabling and disabling ephemeral timer remotely.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_ephemeral_enable_disable() -> Result<()> {
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
let bob = TestContext::new_bob().await;
|
|
||||||
|
|
||||||
let chat_alice = alice.create_chat(&bob).await.id;
|
|
||||||
let chat_bob = bob.create_chat(&alice).await.id;
|
|
||||||
|
|
||||||
chat_alice
|
|
||||||
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
|
||||||
.await?;
|
|
||||||
let sent = alice.pop_sent_msg().await;
|
|
||||||
bob.recv_msg(&sent).await;
|
|
||||||
assert_eq!(
|
|
||||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
|
||||||
Timer::Enabled { duration: 60 }
|
|
||||||
);
|
|
||||||
|
|
||||||
chat_alice
|
|
||||||
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
|
||||||
.await?;
|
|
||||||
let sent = alice.pop_sent_msg().await;
|
|
||||||
bob.recv_msg(&sent).await;
|
|
||||||
assert_eq!(
|
|
||||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
|
||||||
Timer::Disabled
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test that enabling ephemeral timer in unpromoted group does not send a message.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_ephemeral_unpromoted() -> Result<()> {
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
|
|
||||||
let chat_id =
|
|
||||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?;
|
|
||||||
|
|
||||||
// Group is unpromoted, the timer can be changed without sending a message.
|
|
||||||
assert!(chat_id.is_unpromoted(&alice).await?);
|
|
||||||
chat_id
|
|
||||||
.set_ephemeral_timer(&alice, Timer::Enabled { duration: 60 })
|
|
||||||
.await?;
|
|
||||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
|
||||||
assert!(sent.is_none());
|
|
||||||
assert_eq!(
|
|
||||||
chat_id.get_ephemeral_timer(&alice).await?,
|
|
||||||
Timer::Enabled { duration: 60 }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Promote the group.
|
|
||||||
send_text_msg(&alice, chat_id, "hi!".to_string()).await?;
|
|
||||||
assert!(chat_id.is_promoted(&alice).await?);
|
|
||||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
|
||||||
assert!(sent.is_some());
|
|
||||||
|
|
||||||
chat_id
|
|
||||||
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
|
||||||
.await?;
|
|
||||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
|
||||||
assert!(sent.is_some());
|
|
||||||
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test that timer is enabled even if the message explicitly enabling the timer is lost.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_ephemeral_enable_lost() -> Result<()> {
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
let bob = TestContext::new_bob().await;
|
|
||||||
|
|
||||||
let chat_alice = alice.create_chat(&bob).await.id;
|
|
||||||
let chat_bob = bob.create_chat(&alice).await.id;
|
|
||||||
|
|
||||||
// Alice enables the timer.
|
|
||||||
chat_alice
|
|
||||||
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
|
||||||
Timer::Enabled { duration: 60 }
|
|
||||||
);
|
|
||||||
// The message enabling the timer is lost.
|
|
||||||
let _sent = alice.pop_sent_msg().await;
|
|
||||||
assert_eq!(
|
|
||||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
|
||||||
Timer::Disabled,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Alice sends a text message.
|
|
||||||
let mut msg = Message::new(Viewtype::Text);
|
|
||||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
|
||||||
let sent = alice.pop_sent_msg().await;
|
|
||||||
|
|
||||||
// Bob receives text message and enables the timer, even though explicit timer update was
|
|
||||||
// lost previously.
|
|
||||||
bob.recv_msg(&sent).await;
|
|
||||||
assert_eq!(
|
|
||||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
|
||||||
Timer::Enabled { duration: 60 }
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test that Alice replying to the chat without a timer at the same time as Bob enables the
|
|
||||||
/// timer does not result in disabling the timer on the Bob's side.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_ephemeral_timer_rollback() -> Result<()> {
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
let bob = TestContext::new_bob().await;
|
|
||||||
|
|
||||||
let chat_alice = alice.create_chat(&bob).await.id;
|
|
||||||
let chat_bob = bob.create_chat(&alice).await.id;
|
|
||||||
|
|
||||||
// Alice sends message to Bob
|
|
||||||
let mut msg = Message::new(Viewtype::Text);
|
|
||||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
|
||||||
let sent = alice.pop_sent_msg().await;
|
|
||||||
bob.recv_msg(&sent).await;
|
|
||||||
|
|
||||||
// Alice sends second message to Bob, with no timer
|
|
||||||
let mut msg = Message::new(Viewtype::Text);
|
|
||||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
|
||||||
let sent = alice.pop_sent_msg().await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
|
||||||
Timer::Disabled
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bob sets ephemeral timer and sends a message about timer change
|
|
||||||
chat_bob
|
|
||||||
.set_ephemeral_timer(&bob.ctx, Timer::Enabled { duration: 60 })
|
|
||||||
.await?;
|
|
||||||
let sent_timer_change = bob.pop_sent_msg().await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
|
||||||
Timer::Enabled { duration: 60 }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bob receives message from Alice.
|
|
||||||
// Alice message has no timer. However, Bob should not disable timer,
|
|
||||||
// because Alice replies to old message.
|
|
||||||
bob.recv_msg(&sent).await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
|
||||||
Timer::Disabled
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
|
||||||
Timer::Enabled { duration: 60 }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Alice receives message from Bob
|
|
||||||
alice.recv_msg(&sent_timer_change).await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
|
||||||
Timer::Enabled { duration: 60 }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bob disables the chat timer.
|
|
||||||
// Note that the last message in the Bob's chat is from Alice and has no timer,
|
|
||||||
// but the chat timer is enabled.
|
|
||||||
chat_bob
|
|
||||||
.set_ephemeral_timer(&bob.ctx, Timer::Disabled)
|
|
||||||
.await?;
|
|
||||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
|
||||||
assert_eq!(
|
|
||||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
|
||||||
Timer::Disabled
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_ephemeral_delete_msgs() -> Result<()> {
|
|
||||||
let t = TestContext::new_alice().await;
|
|
||||||
let self_chat = t.get_self_chat().await;
|
|
||||||
|
|
||||||
assert_eq!(next_expiration_timestamp(&t).await, None);
|
|
||||||
|
|
||||||
t.send_text(self_chat.id, "Saved message, which we delete manually")
|
|
||||||
.await;
|
|
||||||
let msg = t.get_last_msg_in(self_chat.id).await;
|
|
||||||
msg.id.trash(&t, false).await?;
|
|
||||||
check_msg_is_deleted(&t, &self_chat, msg.id).await;
|
|
||||||
|
|
||||||
self_chat
|
|
||||||
.id
|
|
||||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 3600 })
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Send a saved message which will be deleted after 3600s
|
|
||||||
let now = time();
|
|
||||||
let msg = t.send_text(self_chat.id, "Message text").await;
|
|
||||||
|
|
||||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3599, time() + 3601)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Set DeleteDeviceAfter to 1800s. Then send a saved message which will
|
|
||||||
// still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages.
|
|
||||||
t.set_config(Config::DeleteDeviceAfter, Some("1800"))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let now = time();
|
|
||||||
let msg = t.send_text(self_chat.id, "Message text").await;
|
|
||||||
|
|
||||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3559, time() + 3601)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Send a message to Bob which will be deleted after 1800s because of DeleteDeviceAfter.
|
|
||||||
let bob_chat = t.create_chat_with_contact("", "bob@example.net").await;
|
|
||||||
let now = time();
|
|
||||||
let msg = t.send_text(bob_chat.id, "Message text").await;
|
|
||||||
|
|
||||||
check_msg_will_be_deleted(
|
|
||||||
&t,
|
|
||||||
msg.sender_msg_id,
|
|
||||||
&bob_chat,
|
|
||||||
now + 1799,
|
|
||||||
// The message may appear to be sent MAX_SECONDS_TO_LEND_FROM_FUTURE later and
|
|
||||||
// therefore be deleted MAX_SECONDS_TO_LEND_FROM_FUTURE later.
|
|
||||||
time() + 1801 + MAX_SECONDS_TO_LEND_FROM_FUTURE,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Enable ephemeral messages with Bob -> message will be deleted after 60s.
|
|
||||||
// This tests that the message is deleted at min(ephemeral deletion time, DeleteDeviceAfter deletion time).
|
|
||||||
bob_chat
|
|
||||||
.id
|
|
||||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let now = time();
|
|
||||||
let msg = t.send_text(bob_chat.id, "Message text").await;
|
|
||||||
|
|
||||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &bob_chat, now + 59, time() + 61)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_msg_will_be_deleted(
|
|
||||||
t: &TestContext,
|
|
||||||
msg_id: MsgId,
|
|
||||||
chat: &Chat,
|
|
||||||
not_deleted_at: i64,
|
|
||||||
deleted_at: i64,
|
|
||||||
) -> Result<()> {
|
|
||||||
let next_expiration = next_expiration_timestamp(t).await.unwrap();
|
|
||||||
|
|
||||||
assert!(next_expiration > not_deleted_at);
|
|
||||||
delete_expired_messages(t, not_deleted_at).await?;
|
|
||||||
|
|
||||||
let loaded = Message::load_from_db(t, msg_id).await?;
|
|
||||||
assert!(!loaded.text.is_empty());
|
|
||||||
assert_eq!(loaded.chat_id, chat.id);
|
|
||||||
|
|
||||||
assert!(next_expiration < deleted_at);
|
|
||||||
delete_expired_messages(t, deleted_at).await?;
|
|
||||||
t.evtracker
|
|
||||||
.get_matching(|evt| {
|
|
||||||
if let EventType::MsgDeleted {
|
|
||||||
msg_id: event_msg_id,
|
|
||||||
..
|
|
||||||
} = evt
|
|
||||||
{
|
|
||||||
*event_msg_id == msg_id
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let loaded = Message::load_from_db_optional(t, msg_id).await?;
|
|
||||||
assert!(loaded.is_none());
|
|
||||||
|
|
||||||
// Check that the msg was deleted locally.
|
|
||||||
check_msg_is_deleted(t, chat, msg_id).await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_msg_is_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
|
|
||||||
let chat_items = chat::get_chat_msgs(t, chat.id).await.unwrap();
|
|
||||||
// Check that the chat is empty except for possibly info messages:
|
|
||||||
for item in &chat_items {
|
|
||||||
if let ChatItem::Message { msg_id } = item {
|
|
||||||
let msg = Message::load_from_db(t, *msg_id).await.unwrap();
|
|
||||||
assert!(msg.is_info())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that if there is a message left, the text and metadata are gone
|
|
||||||
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
|
|
||||||
assert_eq!(msg.from_id, ContactId::UNDEFINED);
|
|
||||||
assert_eq!(msg.to_id, ContactId::UNDEFINED);
|
|
||||||
assert_eq!(msg.text, "");
|
|
||||||
let rawtxt: Option<String> = t
|
|
||||||
.sql
|
|
||||||
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(rawtxt.is_none_or_empty(), "{rawtxt:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_delete_expired_imap_messages() -> Result<()> {
|
|
||||||
let t = TestContext::new_alice().await;
|
|
||||||
const HOUR: i64 = 60 * 60;
|
|
||||||
let now = time();
|
|
||||||
for (id, timestamp, ephemeral_timestamp) in &[
|
|
||||||
(900, now - 2 * HOUR, 0),
|
|
||||||
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
|
|
||||||
(1010, now - 23 * HOUR, 0),
|
|
||||||
(1020, now - 21 * HOUR, 0),
|
|
||||||
(1030, now - 19 * HOUR, 0),
|
|
||||||
(2000, now - 18 * HOUR, now - HOUR),
|
|
||||||
(2020, now - 17 * HOUR, now + HOUR),
|
|
||||||
(3000, now + HOUR, 0),
|
|
||||||
] {
|
|
||||||
let message_id = id.to_string();
|
|
||||||
t.sql
|
|
||||||
.execute(
|
|
||||||
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
|
||||||
(id, &message_id, timestamp, ephemeral_timestamp),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
t.sql
|
|
||||||
.execute(
|
|
||||||
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
|
|
||||||
(&message_id, id),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
|
|
||||||
assert_eq!(
|
|
||||||
context
|
|
||||||
.sql
|
|
||||||
.count(
|
|
||||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
|
||||||
(id.to_string(),),
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
|
||||||
context
|
|
||||||
.sql
|
|
||||||
.execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should mark message 2000 for deletion.
|
|
||||||
delete_expired_imap_messages(&t).await?;
|
|
||||||
test_marked_for_deletion(&t, 2000).await?;
|
|
||||||
remove_uid(&t, 2000).await?;
|
|
||||||
// No other messages are marked for deletion.
|
|
||||||
assert_eq!(
|
|
||||||
t.sql
|
|
||||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
|
||||||
.await?,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
|
|
||||||
.await?;
|
|
||||||
delete_expired_imap_messages(&t).await?;
|
|
||||||
test_marked_for_deletion(&t, 1000).await?;
|
|
||||||
|
|
||||||
MsgId::new(1000)
|
|
||||||
.update_download_state(&t, DownloadState::Available)
|
|
||||||
.await?;
|
|
||||||
t.sql
|
|
||||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ())
|
|
||||||
.await?;
|
|
||||||
delete_expired_imap_messages(&t).await?;
|
|
||||||
test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
|
|
||||||
remove_uid(&t, 1000).await?;
|
|
||||||
|
|
||||||
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
|
|
||||||
.await?;
|
|
||||||
delete_expired_imap_messages(&t).await?;
|
|
||||||
test_marked_for_deletion(&t, 1010).await?;
|
|
||||||
t.sql
|
|
||||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
MsgId::new(1010)
|
|
||||||
.update_download_state(&t, DownloadState::Available)
|
|
||||||
.await?;
|
|
||||||
delete_expired_imap_messages(&t).await?;
|
|
||||||
// Keep downloadable for now.
|
|
||||||
assert_eq!(
|
|
||||||
t.sql
|
|
||||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
|
||||||
.await?,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
t.set_config(Config::DeleteServerAfter, Some("1")).await?;
|
|
||||||
delete_expired_imap_messages(&t).await?;
|
|
||||||
test_marked_for_deletion(&t, 3000).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regression test for a bug in the timer rollback protection.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_ephemeral_timer_references() -> Result<()> {
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
|
|
||||||
// Message with Message-ID <first@example.com> and no timer is received.
|
|
||||||
receive_imf(
|
|
||||||
&alice,
|
|
||||||
b"From: Bob <bob@example.com>\n\
|
|
||||||
To: Alice <alice@example.org>\n\
|
|
||||||
Chat-Version: 1.0\n\
|
|
||||||
Subject: Subject\n\
|
|
||||||
Message-ID: <first@example.com>\n\
|
|
||||||
Date: Sun, 22 Mar 2020 00:10:00 +0000\n\
|
|
||||||
\n\
|
|
||||||
hello\n",
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let msg = alice.get_last_msg().await;
|
|
||||||
let chat_id = msg.chat_id;
|
|
||||||
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
|
||||||
|
|
||||||
// Message with Message-ID <second@example.com> is received.
|
|
||||||
receive_imf(
|
|
||||||
&alice,
|
|
||||||
b"From: Bob <bob@example.com>\n\
|
|
||||||
To: Alice <alice@example.org>\n\
|
|
||||||
Chat-Version: 1.0\n\
|
|
||||||
Subject: Subject\n\
|
|
||||||
Message-ID: <second@example.com>\n\
|
|
||||||
Date: Sun, 22 Mar 2020 00:11:00 +0000\n\
|
|
||||||
Ephemeral-Timer: 60\n\
|
|
||||||
\n\
|
|
||||||
second message\n",
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
chat_id.get_ephemeral_timer(&alice).await?,
|
|
||||||
Timer::Enabled { duration: 60 }
|
|
||||||
);
|
|
||||||
let msg = alice.get_last_msg().await;
|
|
||||||
|
|
||||||
// Message is deleted when its timer expires.
|
|
||||||
msg.id.trash(&alice, false).await?;
|
|
||||||
|
|
||||||
// Message with Message-ID <third@example.com>, referencing <first@example.com> and
|
|
||||||
// <second@example.com>, is received. The message <second@example.come> is not in the
|
|
||||||
// database anymore, so the timer should be applied unconditionally without rollback
|
|
||||||
// protection.
|
|
||||||
//
|
|
||||||
// Previously Delta Chat fallen back to using <first@example.com> in this case and
|
|
||||||
// compared received timer value to the timer value of the <first@example.com>. Because
|
|
||||||
// their timer values are the same ("disabled"), Delta Chat assumed that the timer was not
|
|
||||||
// changed explicitly and the change should be ignored.
|
|
||||||
//
|
|
||||||
// The message also contains a quote of the first message to test that only References:
|
|
||||||
// header and not In-Reply-To: is consulted by the rollback protection.
|
|
||||||
receive_imf(
|
|
||||||
&alice,
|
|
||||||
b"From: Bob <bob@example.com>\n\
|
|
||||||
To: Alice <alice@example.org>\n\
|
|
||||||
Chat-Version: 1.0\n\
|
|
||||||
Subject: Subject\n\
|
|
||||||
Message-ID: <third@example.com>\n\
|
|
||||||
Date: Sun, 22 Mar 2020 00:12:00 +0000\n\
|
|
||||||
References: <first@example.com> <second@example.com>\n\
|
|
||||||
In-Reply-To: <first@example.com>\n\
|
|
||||||
\n\
|
|
||||||
> hello\n",
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let msg = alice.get_last_msg().await;
|
|
||||||
assert_eq!(
|
|
||||||
msg.chat_id.get_ephemeral_timer(&alice).await?,
|
|
||||||
Timer::Disabled
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that if we are offline for a time longer than the ephemeral timer duration, the message
|
|
||||||
// is deleted from the chat but is still in the "smtp" table, i.e. will be sent upon a
|
|
||||||
// successful reconnection.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_ephemeral_msg_offline() -> Result<()> {
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
let chat = alice
|
|
||||||
.create_chat_with_contact("Bob", "bob@example.org")
|
|
||||||
.await;
|
|
||||||
let duration = 60;
|
|
||||||
chat.id
|
|
||||||
.set_ephemeral_timer(&alice, Timer::Enabled { duration })
|
|
||||||
.await?;
|
|
||||||
let mut msg = Message::new_text("hi".to_string());
|
|
||||||
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
|
|
||||||
.await
|
|
||||||
.is_err());
|
|
||||||
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
|
|
||||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
|
||||||
let now = time();
|
|
||||||
check_msg_will_be_deleted(&alice, msg.id, &chat, now, now + i64::from(duration) + 1)
|
|
||||||
.await?;
|
|
||||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tests that POI location is deleted when ephemeral message expires.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_ephemeral_poi_location() -> Result<()> {
|
|
||||||
let mut tcm = TestContextManager::new();
|
|
||||||
let alice = &tcm.alice().await;
|
|
||||||
let bob = &tcm.bob().await;
|
|
||||||
|
|
||||||
let chat = alice.create_chat(bob).await;
|
|
||||||
|
|
||||||
let duration = 60;
|
|
||||||
chat.id
|
|
||||||
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
|
||||||
.await?;
|
|
||||||
let sent = alice.pop_sent_msg().await;
|
|
||||||
bob.recv_msg(&sent).await;
|
|
||||||
|
|
||||||
let mut poi_msg = Message::new_text("Here".to_string());
|
|
||||||
poi_msg.set_location(10.0, 20.0);
|
|
||||||
|
|
||||||
let alice_sent_message = alice.send_msg(chat.id, &mut poi_msg).await;
|
|
||||||
let bob_received_message = bob.recv_msg(&alice_sent_message).await;
|
|
||||||
markseen_msgs(bob, vec![bob_received_message.id]).await?;
|
|
||||||
|
|
||||||
for account in [alice, bob] {
|
|
||||||
let locations = location::get_range(account, None, None, 0, 0).await?;
|
|
||||||
assert_eq!(locations.len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemTime::shift(Duration::from_secs(100));
|
|
||||||
|
|
||||||
for account in [alice, bob] {
|
|
||||||
delete_expired_messages(account, time()).await?;
|
|
||||||
let locations = location::get_range(account, None, None, 0, 0).await?;
|
|
||||||
assert_eq!(locations.len(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tests that `.get_ephemeral_timer()` returns an error for invalid chat ID.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_get_ephemeral_timer_wrong_chat_id() -> Result<()> {
|
|
||||||
let context = TestContext::new().await;
|
|
||||||
let chat_id = ChatId::new(12345);
|
|
||||||
assert!(chat_id.get_ephemeral_timer(&context).await.is_err());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tests that ephemeral timer is started when the chat is noticed.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_noticed_ephemeral_timer() -> Result<()> {
|
|
||||||
let mut tcm = TestContextManager::new();
|
|
||||||
let alice = &tcm.alice().await;
|
|
||||||
let bob = &tcm.bob().await;
|
|
||||||
|
|
||||||
let chat = alice.create_chat(bob).await;
|
|
||||||
let duration = 60;
|
|
||||||
chat.id
|
|
||||||
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
|
||||||
.await?;
|
|
||||||
let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await;
|
|
||||||
|
|
||||||
marknoticed_chat(bob, bob_received_message.chat_id).await?;
|
|
||||||
SystemTime::shift(Duration::from_secs(100));
|
|
||||||
|
|
||||||
delete_expired_messages(bob, time()).await?;
|
|
||||||
|
|
||||||
assert!(Message::load_from_db_optional(bob, bob_received_message.id)
|
|
||||||
.await?
|
|
||||||
.is_none());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tests that archiving the chat starts ephemeral timer.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_archived_ephemeral_timer() -> Result<()> {
|
|
||||||
let mut tcm = TestContextManager::new();
|
|
||||||
let alice = &tcm.alice().await;
|
|
||||||
let bob = &tcm.bob().await;
|
|
||||||
|
|
||||||
let chat = alice.create_chat(bob).await;
|
|
||||||
let duration = 60;
|
|
||||||
chat.id
|
|
||||||
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
|
||||||
.await?;
|
|
||||||
let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await;
|
|
||||||
|
|
||||||
bob_received_message
|
|
||||||
.chat_id
|
|
||||||
.set_visibility(bob, ChatVisibility::Archived)
|
|
||||||
.await?;
|
|
||||||
SystemTime::shift(Duration::from_secs(100));
|
|
||||||
|
|
||||||
delete_expired_messages(bob, time()).await?;
|
|
||||||
|
|
||||||
assert!(Message::load_from_db_optional(bob, bob_received_message.id)
|
|
||||||
.await?
|
|
||||||
.is_none());
|
|
||||||
|
|
||||||
// Bob mutes the chat so it is not unarchived.
|
|
||||||
set_muted(bob, bob_received_message.chat_id, MuteDuration::Forever).await?;
|
|
||||||
|
|
||||||
// Now test that for already archived chat
|
|
||||||
// timer is started if all archived chats are marked as noticed.
|
|
||||||
let bob_received_message_2 = tcm.send_recv(alice, bob, "Hello again!").await;
|
|
||||||
assert_eq!(bob_received_message_2.state, MessageState::InFresh);
|
|
||||||
|
|
||||||
marknoticed_chat(bob, DC_CHAT_ID_ARCHIVED_LINK).await?;
|
|
||||||
SystemTime::shift(Duration::from_secs(100));
|
|
||||||
|
|
||||||
delete_expired_messages(bob, time()).await?;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
Message::load_from_db_optional(bob, bob_received_message_2.id)
|
|
||||||
.await?
|
|
||||||
.is_none()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
781
src/ephemeral/ephemeral_tests.rs
Normal file
781
src/ephemeral/ephemeral_tests.rs
Normal file
@@ -0,0 +1,781 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::chat::{marknoticed_chat, set_muted, ChatVisibility, MuteDuration};
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::constants::DC_CHAT_ID_ARCHIVED_LINK;
|
||||||
|
use crate::download::DownloadState;
|
||||||
|
use crate::location;
|
||||||
|
use crate::message::markseen_msgs;
|
||||||
|
use crate::receive_imf::receive_imf;
|
||||||
|
use crate::test_utils::{TestContext, TestContextManager};
|
||||||
|
use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE;
|
||||||
|
use crate::{
|
||||||
|
chat::{self, create_group_chat, send_text_msg, Chat, ChatItem, ProtectionStatus},
|
||||||
|
tools::IsNoneOrEmpty,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_stock_ephemeral_messages() {
|
||||||
|
let context = TestContext::new().await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
stock_ephemeral_timer_changed(&context, Timer::Disabled, ContactId::SELF).await,
|
||||||
|
"You disabled message deletion timer."
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 1 }, ContactId::SELF)
|
||||||
|
.await,
|
||||||
|
"You set message deletion timer to 1 s."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 30 }, ContactId::SELF)
|
||||||
|
.await,
|
||||||
|
"You set message deletion timer to 30 s."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 }, ContactId::SELF)
|
||||||
|
.await,
|
||||||
|
"You set message deletion timer to 1 minute."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 90 }, ContactId::SELF)
|
||||||
|
.await,
|
||||||
|
"You set message deletion timer to 1.5 minutes."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stock_ephemeral_timer_changed(
|
||||||
|
&context,
|
||||||
|
Timer::Enabled { duration: 30 * 60 },
|
||||||
|
ContactId::SELF
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
"You set message deletion timer to 30 minutes."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stock_ephemeral_timer_changed(
|
||||||
|
&context,
|
||||||
|
Timer::Enabled { duration: 60 * 60 },
|
||||||
|
ContactId::SELF
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
"You set message deletion timer to 1 hour."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 5400 }, ContactId::SELF)
|
||||||
|
.await,
|
||||||
|
"You set message deletion timer to 1.5 hours."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stock_ephemeral_timer_changed(
|
||||||
|
&context,
|
||||||
|
Timer::Enabled {
|
||||||
|
duration: 2 * 60 * 60
|
||||||
|
},
|
||||||
|
ContactId::SELF
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
"You set message deletion timer to 2 hours."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stock_ephemeral_timer_changed(
|
||||||
|
&context,
|
||||||
|
Timer::Enabled {
|
||||||
|
duration: 24 * 60 * 60
|
||||||
|
},
|
||||||
|
ContactId::SELF
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
"You set message deletion timer to 1 day."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stock_ephemeral_timer_changed(
|
||||||
|
&context,
|
||||||
|
Timer::Enabled {
|
||||||
|
duration: 2 * 24 * 60 * 60
|
||||||
|
},
|
||||||
|
ContactId::SELF
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
"You set message deletion timer to 2 days."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stock_ephemeral_timer_changed(
|
||||||
|
&context,
|
||||||
|
Timer::Enabled {
|
||||||
|
duration: 7 * 24 * 60 * 60
|
||||||
|
},
|
||||||
|
ContactId::SELF
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
"You set message deletion timer to 1 week."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stock_ephemeral_timer_changed(
|
||||||
|
&context,
|
||||||
|
Timer::Enabled {
|
||||||
|
duration: 4 * 7 * 24 * 60 * 60
|
||||||
|
},
|
||||||
|
ContactId::SELF
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
"You set message deletion timer to 4 weeks."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test enabling and disabling ephemeral timer remotely.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_ephemeral_enable_disable() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
|
||||||
|
let chat_alice = alice.create_chat(&bob).await.id;
|
||||||
|
let chat_bob = bob.create_chat(&alice).await.id;
|
||||||
|
|
||||||
|
chat_alice
|
||||||
|
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
||||||
|
.await?;
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
bob.recv_msg(&sent).await;
|
||||||
|
assert_eq!(
|
||||||
|
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||||
|
Timer::Enabled { duration: 60 }
|
||||||
|
);
|
||||||
|
|
||||||
|
chat_alice
|
||||||
|
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
||||||
|
.await?;
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
bob.recv_msg(&sent).await;
|
||||||
|
assert_eq!(
|
||||||
|
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||||
|
Timer::Disabled
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that enabling ephemeral timer in unpromoted group does not send a message.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_ephemeral_unpromoted() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
|
||||||
|
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?;
|
||||||
|
|
||||||
|
// Group is unpromoted, the timer can be changed without sending a message.
|
||||||
|
assert!(chat_id.is_unpromoted(&alice).await?);
|
||||||
|
chat_id
|
||||||
|
.set_ephemeral_timer(&alice, Timer::Enabled { duration: 60 })
|
||||||
|
.await?;
|
||||||
|
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||||
|
assert!(sent.is_none());
|
||||||
|
assert_eq!(
|
||||||
|
chat_id.get_ephemeral_timer(&alice).await?,
|
||||||
|
Timer::Enabled { duration: 60 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Promote the group.
|
||||||
|
send_text_msg(&alice, chat_id, "hi!".to_string()).await?;
|
||||||
|
assert!(chat_id.is_promoted(&alice).await?);
|
||||||
|
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||||
|
assert!(sent.is_some());
|
||||||
|
|
||||||
|
chat_id
|
||||||
|
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
||||||
|
.await?;
|
||||||
|
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||||
|
assert!(sent.is_some());
|
||||||
|
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that timer is enabled even if the message explicitly enabling the timer is lost.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_ephemeral_enable_lost() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
|
||||||
|
let chat_alice = alice.create_chat(&bob).await.id;
|
||||||
|
let chat_bob = bob.create_chat(&alice).await.id;
|
||||||
|
|
||||||
|
// Alice enables the timer.
|
||||||
|
chat_alice
|
||||||
|
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||||
|
Timer::Enabled { duration: 60 }
|
||||||
|
);
|
||||||
|
// The message enabling the timer is lost.
|
||||||
|
let _sent = alice.pop_sent_msg().await;
|
||||||
|
assert_eq!(
|
||||||
|
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||||
|
Timer::Disabled,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Alice sends a text message.
|
||||||
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
|
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
|
||||||
|
// Bob receives text message and enables the timer, even though explicit timer update was
|
||||||
|
// lost previously.
|
||||||
|
bob.recv_msg(&sent).await;
|
||||||
|
assert_eq!(
|
||||||
|
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||||
|
Timer::Enabled { duration: 60 }
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that Alice replying to the chat without a timer at the same time as Bob enables the
|
||||||
|
/// timer does not result in disabling the timer on the Bob's side.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_ephemeral_timer_rollback() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
|
||||||
|
let chat_alice = alice.create_chat(&bob).await.id;
|
||||||
|
let chat_bob = bob.create_chat(&alice).await.id;
|
||||||
|
|
||||||
|
// Alice sends message to Bob
|
||||||
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
|
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
bob.recv_msg(&sent).await;
|
||||||
|
|
||||||
|
// Alice sends second message to Bob, with no timer
|
||||||
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
|
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||||
|
Timer::Disabled
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bob sets ephemeral timer and sends a message about timer change
|
||||||
|
chat_bob
|
||||||
|
.set_ephemeral_timer(&bob.ctx, Timer::Enabled { duration: 60 })
|
||||||
|
.await?;
|
||||||
|
let sent_timer_change = bob.pop_sent_msg().await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||||
|
Timer::Enabled { duration: 60 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bob receives message from Alice.
|
||||||
|
// Alice message has no timer. However, Bob should not disable timer,
|
||||||
|
// because Alice replies to old message.
|
||||||
|
bob.recv_msg(&sent).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||||
|
Timer::Disabled
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||||
|
Timer::Enabled { duration: 60 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Alice receives message from Bob
|
||||||
|
alice.recv_msg(&sent_timer_change).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||||
|
Timer::Enabled { duration: 60 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bob disables the chat timer.
|
||||||
|
// Note that the last message in the Bob's chat is from Alice and has no timer,
|
||||||
|
// but the chat timer is enabled.
|
||||||
|
chat_bob
|
||||||
|
.set_ephemeral_timer(&bob.ctx, Timer::Disabled)
|
||||||
|
.await?;
|
||||||
|
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||||
|
assert_eq!(
|
||||||
|
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||||
|
Timer::Disabled
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_ephemeral_delete_msgs() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
let self_chat = t.get_self_chat().await;
|
||||||
|
|
||||||
|
assert_eq!(next_expiration_timestamp(&t).await, None);
|
||||||
|
|
||||||
|
t.send_text(self_chat.id, "Saved message, which we delete manually")
|
||||||
|
.await;
|
||||||
|
let msg = t.get_last_msg_in(self_chat.id).await;
|
||||||
|
msg.id.trash(&t, false).await?;
|
||||||
|
check_msg_is_deleted(&t, &self_chat, msg.id).await;
|
||||||
|
|
||||||
|
self_chat
|
||||||
|
.id
|
||||||
|
.set_ephemeral_timer(&t, Timer::Enabled { duration: 3600 })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Send a saved message which will be deleted after 3600s
|
||||||
|
let now = time();
|
||||||
|
let msg = t.send_text(self_chat.id, "Message text").await;
|
||||||
|
|
||||||
|
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3599, time() + 3601)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Set DeleteDeviceAfter to 1800s. Then send a saved message which will
|
||||||
|
// still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages.
|
||||||
|
t.set_config(Config::DeleteDeviceAfter, Some("1800"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let now = time();
|
||||||
|
let msg = t.send_text(self_chat.id, "Message text").await;
|
||||||
|
|
||||||
|
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3559, time() + 3601)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Send a message to Bob which will be deleted after 1800s because of DeleteDeviceAfter.
|
||||||
|
let bob_chat = t.create_chat_with_contact("", "bob@example.net").await;
|
||||||
|
let now = time();
|
||||||
|
let msg = t.send_text(bob_chat.id, "Message text").await;
|
||||||
|
|
||||||
|
check_msg_will_be_deleted(
|
||||||
|
&t,
|
||||||
|
msg.sender_msg_id,
|
||||||
|
&bob_chat,
|
||||||
|
now + 1799,
|
||||||
|
// The message may appear to be sent MAX_SECONDS_TO_LEND_FROM_FUTURE later and
|
||||||
|
// therefore be deleted MAX_SECONDS_TO_LEND_FROM_FUTURE later.
|
||||||
|
time() + 1801 + MAX_SECONDS_TO_LEND_FROM_FUTURE,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Enable ephemeral messages with Bob -> message will be deleted after 60s.
|
||||||
|
// This tests that the message is deleted at min(ephemeral deletion time, DeleteDeviceAfter deletion time).
|
||||||
|
bob_chat
|
||||||
|
.id
|
||||||
|
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let now = time();
|
||||||
|
let msg = t.send_text(bob_chat.id, "Message text").await;
|
||||||
|
|
||||||
|
check_msg_will_be_deleted(&t, msg.sender_msg_id, &bob_chat, now + 59, time() + 61)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_msg_will_be_deleted(
|
||||||
|
t: &TestContext,
|
||||||
|
msg_id: MsgId,
|
||||||
|
chat: &Chat,
|
||||||
|
not_deleted_at: i64,
|
||||||
|
deleted_at: i64,
|
||||||
|
) -> Result<()> {
|
||||||
|
let next_expiration = next_expiration_timestamp(t).await.unwrap();
|
||||||
|
|
||||||
|
assert!(next_expiration > not_deleted_at);
|
||||||
|
delete_expired_messages(t, not_deleted_at).await?;
|
||||||
|
|
||||||
|
let loaded = Message::load_from_db(t, msg_id).await?;
|
||||||
|
assert!(!loaded.text.is_empty());
|
||||||
|
assert_eq!(loaded.chat_id, chat.id);
|
||||||
|
|
||||||
|
assert!(next_expiration < deleted_at);
|
||||||
|
delete_expired_messages(t, deleted_at).await?;
|
||||||
|
t.evtracker
|
||||||
|
.get_matching(|evt| {
|
||||||
|
if let EventType::MsgDeleted {
|
||||||
|
msg_id: event_msg_id,
|
||||||
|
..
|
||||||
|
} = evt
|
||||||
|
{
|
||||||
|
*event_msg_id == msg_id
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let loaded = Message::load_from_db_optional(t, msg_id).await?;
|
||||||
|
assert!(loaded.is_none());
|
||||||
|
|
||||||
|
// Check that the msg was deleted locally.
|
||||||
|
check_msg_is_deleted(t, chat, msg_id).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_msg_is_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
|
||||||
|
let chat_items = chat::get_chat_msgs(t, chat.id).await.unwrap();
|
||||||
|
// Check that the chat is empty except for possibly info messages:
|
||||||
|
for item in &chat_items {
|
||||||
|
if let ChatItem::Message { msg_id } = item {
|
||||||
|
let msg = Message::load_from_db(t, *msg_id).await.unwrap();
|
||||||
|
assert!(msg.is_info())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that if there is a message left, the text and metadata are gone
|
||||||
|
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
|
||||||
|
assert_eq!(msg.from_id, ContactId::UNDEFINED);
|
||||||
|
assert_eq!(msg.to_id, ContactId::UNDEFINED);
|
||||||
|
assert_eq!(msg.text, "");
|
||||||
|
let rawtxt: Option<String> = t
|
||||||
|
.sql
|
||||||
|
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(rawtxt.is_none_or_empty(), "{rawtxt:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
const HOUR: i64 = 60 * 60;
|
||||||
|
let now = time();
|
||||||
|
for (id, timestamp, ephemeral_timestamp) in &[
|
||||||
|
(900, now - 2 * HOUR, 0),
|
||||||
|
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
|
||||||
|
(1010, now - 23 * HOUR, 0),
|
||||||
|
(1020, now - 21 * HOUR, 0),
|
||||||
|
(1030, now - 19 * HOUR, 0),
|
||||||
|
(2000, now - 18 * HOUR, now - HOUR),
|
||||||
|
(2020, now - 17 * HOUR, now + HOUR),
|
||||||
|
(3000, now + HOUR, 0),
|
||||||
|
] {
|
||||||
|
let message_id = id.to_string();
|
||||||
|
t.sql
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||||
|
(id, &message_id, timestamp, ephemeral_timestamp),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
t.sql
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
|
||||||
|
(&message_id, id),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
|
||||||
|
assert_eq!(
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.count(
|
||||||
|
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||||
|
(id.to_string(),),
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should mark message 2000 for deletion.
|
||||||
|
delete_expired_imap_messages(&t).await?;
|
||||||
|
test_marked_for_deletion(&t, 2000).await?;
|
||||||
|
remove_uid(&t, 2000).await?;
|
||||||
|
// No other messages are marked for deletion.
|
||||||
|
assert_eq!(
|
||||||
|
t.sql
|
||||||
|
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||||
|
.await?,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
|
||||||
|
.await?;
|
||||||
|
delete_expired_imap_messages(&t).await?;
|
||||||
|
test_marked_for_deletion(&t, 1000).await?;
|
||||||
|
|
||||||
|
MsgId::new(1000)
|
||||||
|
.update_download_state(&t, DownloadState::Available)
|
||||||
|
.await?;
|
||||||
|
t.sql
|
||||||
|
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ())
|
||||||
|
.await?;
|
||||||
|
delete_expired_imap_messages(&t).await?;
|
||||||
|
test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
|
||||||
|
remove_uid(&t, 1000).await?;
|
||||||
|
|
||||||
|
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
|
||||||
|
.await?;
|
||||||
|
delete_expired_imap_messages(&t).await?;
|
||||||
|
test_marked_for_deletion(&t, 1010).await?;
|
||||||
|
t.sql
|
||||||
|
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
MsgId::new(1010)
|
||||||
|
.update_download_state(&t, DownloadState::Available)
|
||||||
|
.await?;
|
||||||
|
delete_expired_imap_messages(&t).await?;
|
||||||
|
// Keep downloadable for now.
|
||||||
|
assert_eq!(
|
||||||
|
t.sql
|
||||||
|
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||||
|
.await?,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
t.set_config(Config::DeleteServerAfter, Some("1")).await?;
|
||||||
|
delete_expired_imap_messages(&t).await?;
|
||||||
|
test_marked_for_deletion(&t, 3000).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regression test for a bug in the timer rollback protection.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_ephemeral_timer_references() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
|
||||||
|
// Message with Message-ID <first@example.com> and no timer is received.
|
||||||
|
receive_imf(
|
||||||
|
&alice,
|
||||||
|
b"From: Bob <bob@example.com>\n\
|
||||||
|
To: Alice <alice@example.org>\n\
|
||||||
|
Chat-Version: 1.0\n\
|
||||||
|
Subject: Subject\n\
|
||||||
|
Message-ID: <first@example.com>\n\
|
||||||
|
Date: Sun, 22 Mar 2020 00:10:00 +0000\n\
|
||||||
|
\n\
|
||||||
|
hello\n",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let msg = alice.get_last_msg().await;
|
||||||
|
let chat_id = msg.chat_id;
|
||||||
|
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
||||||
|
|
||||||
|
// Message with Message-ID <second@example.com> is received.
|
||||||
|
receive_imf(
|
||||||
|
&alice,
|
||||||
|
b"From: Bob <bob@example.com>\n\
|
||||||
|
To: Alice <alice@example.org>\n\
|
||||||
|
Chat-Version: 1.0\n\
|
||||||
|
Subject: Subject\n\
|
||||||
|
Message-ID: <second@example.com>\n\
|
||||||
|
Date: Sun, 22 Mar 2020 00:11:00 +0000\n\
|
||||||
|
Ephemeral-Timer: 60\n\
|
||||||
|
\n\
|
||||||
|
second message\n",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
chat_id.get_ephemeral_timer(&alice).await?,
|
||||||
|
Timer::Enabled { duration: 60 }
|
||||||
|
);
|
||||||
|
let msg = alice.get_last_msg().await;
|
||||||
|
|
||||||
|
// Message is deleted when its timer expires.
|
||||||
|
msg.id.trash(&alice, false).await?;
|
||||||
|
|
||||||
|
// Message with Message-ID <third@example.com>, referencing <first@example.com> and
|
||||||
|
// <second@example.com>, is received. The message <second@example.come> is not in the
|
||||||
|
// database anymore, so the timer should be applied unconditionally without rollback
|
||||||
|
// protection.
|
||||||
|
//
|
||||||
|
// Previously Delta Chat fallen back to using <first@example.com> in this case and
|
||||||
|
// compared received timer value to the timer value of the <first@example.com>. Because
|
||||||
|
// their timer values are the same ("disabled"), Delta Chat assumed that the timer was not
|
||||||
|
// changed explicitly and the change should be ignored.
|
||||||
|
//
|
||||||
|
// The message also contains a quote of the first message to test that only References:
|
||||||
|
// header and not In-Reply-To: is consulted by the rollback protection.
|
||||||
|
receive_imf(
|
||||||
|
&alice,
|
||||||
|
b"From: Bob <bob@example.com>\n\
|
||||||
|
To: Alice <alice@example.org>\n\
|
||||||
|
Chat-Version: 1.0\n\
|
||||||
|
Subject: Subject\n\
|
||||||
|
Message-ID: <third@example.com>\n\
|
||||||
|
Date: Sun, 22 Mar 2020 00:12:00 +0000\n\
|
||||||
|
References: <first@example.com> <second@example.com>\n\
|
||||||
|
In-Reply-To: <first@example.com>\n\
|
||||||
|
\n\
|
||||||
|
> hello\n",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let msg = alice.get_last_msg().await;
|
||||||
|
assert_eq!(
|
||||||
|
msg.chat_id.get_ephemeral_timer(&alice).await?,
|
||||||
|
Timer::Disabled
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that if we are offline for a time longer than the ephemeral timer duration, the message
|
||||||
|
// is deleted from the chat but is still in the "smtp" table, i.e. will be sent upon a
|
||||||
|
// successful reconnection.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_ephemeral_msg_offline() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let chat = alice
|
||||||
|
.create_chat_with_contact("Bob", "bob@example.org")
|
||||||
|
.await;
|
||||||
|
let duration = 60;
|
||||||
|
chat.id
|
||||||
|
.set_ephemeral_timer(&alice, Timer::Enabled { duration })
|
||||||
|
.await?;
|
||||||
|
let mut msg = Message::new_text("hi".to_string());
|
||||||
|
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
|
||||||
|
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||||
|
let now = time();
|
||||||
|
check_msg_will_be_deleted(&alice, msg.id, &chat, now, now + i64::from(duration) + 1).await?;
|
||||||
|
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that POI location is deleted when ephemeral message expires.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_ephemeral_poi_location() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
let bob = &tcm.bob().await;
|
||||||
|
|
||||||
|
let chat = alice.create_chat(bob).await;
|
||||||
|
|
||||||
|
let duration = 60;
|
||||||
|
chat.id
|
||||||
|
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
||||||
|
.await?;
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
bob.recv_msg(&sent).await;
|
||||||
|
|
||||||
|
let mut poi_msg = Message::new_text("Here".to_string());
|
||||||
|
poi_msg.set_location(10.0, 20.0);
|
||||||
|
|
||||||
|
let alice_sent_message = alice.send_msg(chat.id, &mut poi_msg).await;
|
||||||
|
let bob_received_message = bob.recv_msg(&alice_sent_message).await;
|
||||||
|
markseen_msgs(bob, vec![bob_received_message.id]).await?;
|
||||||
|
|
||||||
|
for account in [alice, bob] {
|
||||||
|
let locations = location::get_range(account, None, None, 0, 0).await?;
|
||||||
|
assert_eq!(locations.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemTime::shift(Duration::from_secs(100));
|
||||||
|
|
||||||
|
for account in [alice, bob] {
|
||||||
|
delete_expired_messages(account, time()).await?;
|
||||||
|
let locations = location::get_range(account, None, None, 0, 0).await?;
|
||||||
|
assert_eq!(locations.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that `.get_ephemeral_timer()` returns an error for invalid chat ID.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_get_ephemeral_timer_wrong_chat_id() -> Result<()> {
|
||||||
|
let context = TestContext::new().await;
|
||||||
|
let chat_id = ChatId::new(12345);
|
||||||
|
assert!(chat_id.get_ephemeral_timer(&context).await.is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that ephemeral timer is started when the chat is noticed.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_noticed_ephemeral_timer() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
let bob = &tcm.bob().await;
|
||||||
|
|
||||||
|
let chat = alice.create_chat(bob).await;
|
||||||
|
let duration = 60;
|
||||||
|
chat.id
|
||||||
|
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
||||||
|
.await?;
|
||||||
|
let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await;
|
||||||
|
|
||||||
|
marknoticed_chat(bob, bob_received_message.chat_id).await?;
|
||||||
|
SystemTime::shift(Duration::from_secs(100));
|
||||||
|
|
||||||
|
delete_expired_messages(bob, time()).await?;
|
||||||
|
|
||||||
|
assert!(Message::load_from_db_optional(bob, bob_received_message.id)
|
||||||
|
.await?
|
||||||
|
.is_none());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that archiving the chat starts ephemeral timer.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_archived_ephemeral_timer() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
let bob = &tcm.bob().await;
|
||||||
|
|
||||||
|
let chat = alice.create_chat(bob).await;
|
||||||
|
let duration = 60;
|
||||||
|
chat.id
|
||||||
|
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
||||||
|
.await?;
|
||||||
|
let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await;
|
||||||
|
|
||||||
|
bob_received_message
|
||||||
|
.chat_id
|
||||||
|
.set_visibility(bob, ChatVisibility::Archived)
|
||||||
|
.await?;
|
||||||
|
SystemTime::shift(Duration::from_secs(100));
|
||||||
|
|
||||||
|
delete_expired_messages(bob, time()).await?;
|
||||||
|
|
||||||
|
assert!(Message::load_from_db_optional(bob, bob_received_message.id)
|
||||||
|
.await?
|
||||||
|
.is_none());
|
||||||
|
|
||||||
|
// Bob mutes the chat so it is not unarchived.
|
||||||
|
set_muted(bob, bob_received_message.chat_id, MuteDuration::Forever).await?;
|
||||||
|
|
||||||
|
// Now test that for already archived chat
|
||||||
|
// timer is started if all archived chats are marked as noticed.
|
||||||
|
let bob_received_message_2 = tcm.send_recv(alice, bob, "Hello again!").await;
|
||||||
|
assert_eq!(bob_received_message_2.state, MessageState::InFresh);
|
||||||
|
|
||||||
|
marknoticed_chat(bob, DC_CHAT_ID_ARCHIVED_LINK).await?;
|
||||||
|
SystemTime::shift(Duration::from_secs(100));
|
||||||
|
|
||||||
|
delete_expired_messages(bob, time()).await?;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
Message::load_from_db_optional(bob, bob_received_message_2.id)
|
||||||
|
.await?
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
340
src/imap.rs
340
src/imap.rs
@@ -2641,342 +2641,4 @@ async fn add_all_recipients_as_contacts(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod imap_tests;
|
||||||
use super::*;
|
|
||||||
use crate::test_utils::TestContext;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_folder_meaning_by_name() {
|
|
||||||
assert_eq!(get_folder_meaning_by_name("Gesendet"), FolderMeaning::Sent);
|
|
||||||
assert_eq!(get_folder_meaning_by_name("GESENDET"), FolderMeaning::Sent);
|
|
||||||
assert_eq!(get_folder_meaning_by_name("gesendet"), FolderMeaning::Sent);
|
|
||||||
assert_eq!(
|
|
||||||
get_folder_meaning_by_name("Messages envoyés"),
|
|
||||||
FolderMeaning::Sent
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
get_folder_meaning_by_name("mEsSaGes envoyÉs"),
|
|
||||||
FolderMeaning::Sent
|
|
||||||
);
|
|
||||||
assert_eq!(get_folder_meaning_by_name("xxx"), FolderMeaning::Unknown);
|
|
||||||
assert_eq!(get_folder_meaning_by_name("SPAM"), FolderMeaning::Spam);
|
|
||||||
assert_eq!(get_folder_meaning_by_name("Trash"), FolderMeaning::Trash);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_set_uid_next_validity() {
|
|
||||||
let t = TestContext::new_alice().await;
|
|
||||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
|
|
||||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 0);
|
|
||||||
|
|
||||||
set_uidvalidity(&t.ctx, "Inbox", 7).await.unwrap();
|
|
||||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 7);
|
|
||||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
|
|
||||||
|
|
||||||
set_uid_next(&t.ctx, "Inbox", 5).await.unwrap();
|
|
||||||
set_uidvalidity(&t.ctx, "Inbox", 6).await.unwrap();
|
|
||||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 5);
|
|
||||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_build_sequence_sets() {
|
|
||||||
assert_eq!(build_sequence_sets(&[]).unwrap(), vec![]);
|
|
||||||
|
|
||||||
let cases = vec![
|
|
||||||
(vec![1], "1"),
|
|
||||||
(vec![3291], "3291"),
|
|
||||||
(vec![1, 3, 5, 7, 9, 11], "1,3,5,7,9,11"),
|
|
||||||
(vec![1, 2, 3], "1:3"),
|
|
||||||
(vec![1, 4, 5, 6], "1,4:6"),
|
|
||||||
((1..=500).collect(), "1:500"),
|
|
||||||
(vec![3, 4, 8, 9, 10, 11, 39, 50, 2], "3:4,8:11,39,50,2"),
|
|
||||||
];
|
|
||||||
for (input, s) in cases {
|
|
||||||
assert_eq!(
|
|
||||||
build_sequence_sets(&input).unwrap(),
|
|
||||||
vec![(input, s.into())]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let has_number = |(uids, s): &(Vec<u32>, String), number| {
|
|
||||||
uids.iter().any(|&n| n == number)
|
|
||||||
&& s.split(',').any(|n| n.parse::<u32>().unwrap() == number)
|
|
||||||
};
|
|
||||||
|
|
||||||
let numbers: Vec<_> = (2..=500).step_by(2).collect();
|
|
||||||
let result = build_sequence_sets(&numbers).unwrap();
|
|
||||||
for (_, set) in &result {
|
|
||||||
assert!(set.len() < 1010);
|
|
||||||
assert!(!set.ends_with(','));
|
|
||||||
assert!(!set.starts_with(','));
|
|
||||||
}
|
|
||||||
assert!(result.len() == 1); // these UIDs fit in one set
|
|
||||||
for &number in &numbers {
|
|
||||||
assert!(result.iter().any(|r| has_number(r, number)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let numbers: Vec<_> = (1..=1000).step_by(3).collect();
|
|
||||||
let result = build_sequence_sets(&numbers).unwrap();
|
|
||||||
for (_, set) in &result {
|
|
||||||
assert!(set.len() < 1010);
|
|
||||||
assert!(!set.ends_with(','));
|
|
||||||
assert!(!set.starts_with(','));
|
|
||||||
}
|
|
||||||
let (last_uids, last_str) = result.last().unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
last_uids.get((last_uids.len() - 2)..).unwrap(),
|
|
||||||
&[997, 1000]
|
|
||||||
);
|
|
||||||
assert!(last_str.ends_with("997,1000"));
|
|
||||||
assert!(result.len() == 2); // This time we need 2 sets
|
|
||||||
for &number in &numbers {
|
|
||||||
assert!(result.iter().any(|r| has_number(r, number)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let numbers: Vec<_> = (30000000..=30002500).step_by(4).collect();
|
|
||||||
let result = build_sequence_sets(&numbers).unwrap();
|
|
||||||
for (_, set) in &result {
|
|
||||||
assert!(set.len() < 1010);
|
|
||||||
assert!(!set.ends_with(','));
|
|
||||||
assert!(!set.starts_with(','));
|
|
||||||
}
|
|
||||||
assert_eq!(result.len(), 6);
|
|
||||||
for &number in &numbers {
|
|
||||||
assert!(result.iter().any(|r| has_number(r, number)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_target_folder_combination(
|
|
||||||
folder: &str,
|
|
||||||
mvbox_move: bool,
|
|
||||||
chat_msg: bool,
|
|
||||||
expected_destination: &str,
|
|
||||||
accepted_chat: bool,
|
|
||||||
outgoing: bool,
|
|
||||||
setupmessage: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
println!("Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}");
|
|
||||||
|
|
||||||
let t = TestContext::new_alice().await;
|
|
||||||
t.ctx
|
|
||||||
.set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat"))
|
|
||||||
.await?;
|
|
||||||
t.ctx
|
|
||||||
.set_config(Config::ConfiguredSentboxFolder, Some("Sent"))
|
|
||||||
.await?;
|
|
||||||
t.ctx
|
|
||||||
.set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" }))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if accepted_chat {
|
|
||||||
let contact_id = Contact::create(&t.ctx, "", "bob@example.net").await?;
|
|
||||||
ChatId::create_for_contact(&t.ctx, contact_id).await?;
|
|
||||||
}
|
|
||||||
let temp;
|
|
||||||
|
|
||||||
let bytes = if setupmessage {
|
|
||||||
include_bytes!("../test-data/message/AutocryptSetupMessage.eml")
|
|
||||||
} else {
|
|
||||||
temp = format!(
|
|
||||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
|
||||||
{}\
|
|
||||||
Subject: foo\n\
|
|
||||||
Message-ID: <abc@example.com>\n\
|
|
||||||
{}\
|
|
||||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
|
||||||
\n\
|
|
||||||
hello\n",
|
|
||||||
if outgoing {
|
|
||||||
"From: alice@example.org\nTo: bob@example.net\n"
|
|
||||||
} else {
|
|
||||||
"From: bob@example.net\nTo: alice@example.org\n"
|
|
||||||
},
|
|
||||||
if chat_msg { "Chat-Version: 1.0\n" } else { "" },
|
|
||||||
);
|
|
||||||
temp.as_bytes()
|
|
||||||
};
|
|
||||||
|
|
||||||
let (headers, _) = mailparse::parse_headers(bytes)?;
|
|
||||||
let actual = if let Some(config) =
|
|
||||||
target_folder_cfg(&t, folder, get_folder_meaning_by_name(folder), &headers).await?
|
|
||||||
{
|
|
||||||
t.get_config(config).await?
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let expected = if expected_destination == folder {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(expected_destination)
|
|
||||||
};
|
|
||||||
assert_eq!(expected, actual.as_deref(), "For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// chat_msg means that the message was sent by Delta Chat
|
|
||||||
// The tuples are (folder, mvbox_move, chat_msg, expected_destination)
|
|
||||||
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
|
|
||||||
("INBOX", false, false, "INBOX"),
|
|
||||||
("INBOX", false, true, "INBOX"),
|
|
||||||
("INBOX", true, false, "INBOX"),
|
|
||||||
("INBOX", true, true, "DeltaChat"),
|
|
||||||
("Sent", false, false, "Sent"),
|
|
||||||
("Sent", false, true, "Sent"),
|
|
||||||
("Sent", true, false, "Sent"),
|
|
||||||
("Sent", true, true, "DeltaChat"),
|
|
||||||
("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
|
||||||
("Spam", false, true, "INBOX"),
|
|
||||||
("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
|
||||||
("Spam", true, true, "DeltaChat"),
|
|
||||||
];
|
|
||||||
|
|
||||||
// These are the same as above, but non-chat messages in Spam stay in Spam
|
|
||||||
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
|
|
||||||
("INBOX", false, false, "INBOX"),
|
|
||||||
("INBOX", false, true, "INBOX"),
|
|
||||||
("INBOX", true, false, "INBOX"),
|
|
||||||
("INBOX", true, true, "DeltaChat"),
|
|
||||||
("Sent", false, false, "Sent"),
|
|
||||||
("Sent", false, true, "Sent"),
|
|
||||||
("Sent", true, false, "Sent"),
|
|
||||||
("Sent", true, true, "DeltaChat"),
|
|
||||||
("Spam", false, false, "Spam"),
|
|
||||||
("Spam", false, true, "INBOX"),
|
|
||||||
("Spam", true, false, "Spam"),
|
|
||||||
("Spam", true, true, "DeltaChat"),
|
|
||||||
];
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_target_folder_incoming_accepted() -> Result<()> {
|
|
||||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
|
||||||
check_target_folder_combination(
|
|
||||||
folder,
|
|
||||||
*mvbox_move,
|
|
||||||
*chat_msg,
|
|
||||||
expected_destination,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_target_folder_incoming_request() -> Result<()> {
|
|
||||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
|
|
||||||
check_target_folder_combination(
|
|
||||||
folder,
|
|
||||||
*mvbox_move,
|
|
||||||
*chat_msg,
|
|
||||||
expected_destination,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_target_folder_outgoing() -> Result<()> {
|
|
||||||
// Test outgoing emails
|
|
||||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
|
||||||
check_target_folder_combination(
|
|
||||||
folder,
|
|
||||||
*mvbox_move,
|
|
||||||
*chat_msg,
|
|
||||||
expected_destination,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_target_folder_setupmsg() -> Result<()> {
|
|
||||||
// Test setupmessages
|
|
||||||
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
|
||||||
check_target_folder_combination(
|
|
||||||
folder,
|
|
||||||
*mvbox_move,
|
|
||||||
*chat_msg,
|
|
||||||
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_get_imap_search_command() -> Result<()> {
|
|
||||||
let t = TestContext::new_alice().await;
|
|
||||||
assert_eq!(
|
|
||||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
|
||||||
r#"FROM "alice@example.org""#
|
|
||||||
);
|
|
||||||
|
|
||||||
t.ctx.set_primary_self_addr("alice@another.com").await?;
|
|
||||||
assert_eq!(
|
|
||||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
|
||||||
r#"OR (FROM "alice@another.com") (FROM "alice@example.org")"#
|
|
||||||
);
|
|
||||||
|
|
||||||
t.ctx.set_primary_self_addr("alice@third.com").await?;
|
|
||||||
assert_eq!(
|
|
||||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
|
||||||
r#"OR (OR (FROM "alice@third.com") (FROM "alice@another.com")) (FROM "alice@example.org")"#
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_uid_grouper() {
|
|
||||||
// Input: sequence of (rowid: i64, uid: u32, target: String)
|
|
||||||
// Output: sequence of (target: String, rowid_set: Vec<i64>, uid_set: String)
|
|
||||||
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string())]);
|
|
||||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
|
||||||
assert_eq!(res, vec![("INBOX".to_string(), vec![1], "2".to_string())]);
|
|
||||||
|
|
||||||
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string()), (2, 3, "INBOX".to_string())]);
|
|
||||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
|
||||||
assert_eq!(
|
|
||||||
res,
|
|
||||||
vec![("INBOX".to_string(), vec![1, 2], "2:3".to_string())]
|
|
||||||
);
|
|
||||||
|
|
||||||
let grouper = UidGrouper::from([
|
|
||||||
(1, 2, "INBOX".to_string()),
|
|
||||||
(2, 2, "INBOX".to_string()),
|
|
||||||
(3, 3, "INBOX".to_string()),
|
|
||||||
]);
|
|
||||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
|
||||||
assert_eq!(
|
|
||||||
res,
|
|
||||||
vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_setmetadata_device_token() {
|
|
||||||
assert_eq!(
|
|
||||||
format_setmetadata("INBOX", "foobarbaz"),
|
|
||||||
"SETMETADATA \"INBOX\" (/private/devicetoken {9+}\r\nfoobarbaz)"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format_setmetadata("INBOX", "foo\r\nbar\r\nbaz\r\n"),
|
|
||||||
"SETMETADATA \"INBOX\" (/private/devicetoken {15+}\r\nfoo\r\nbar\r\nbaz\r\n)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
337
src/imap/imap_tests.rs
Normal file
337
src/imap/imap_tests.rs
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::test_utils::TestContext;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_folder_meaning_by_name() {
|
||||||
|
assert_eq!(get_folder_meaning_by_name("Gesendet"), FolderMeaning::Sent);
|
||||||
|
assert_eq!(get_folder_meaning_by_name("GESENDET"), FolderMeaning::Sent);
|
||||||
|
assert_eq!(get_folder_meaning_by_name("gesendet"), FolderMeaning::Sent);
|
||||||
|
assert_eq!(
|
||||||
|
get_folder_meaning_by_name("Messages envoyés"),
|
||||||
|
FolderMeaning::Sent
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_folder_meaning_by_name("mEsSaGes envoyÉs"),
|
||||||
|
FolderMeaning::Sent
|
||||||
|
);
|
||||||
|
assert_eq!(get_folder_meaning_by_name("xxx"), FolderMeaning::Unknown);
|
||||||
|
assert_eq!(get_folder_meaning_by_name("SPAM"), FolderMeaning::Spam);
|
||||||
|
assert_eq!(get_folder_meaning_by_name("Trash"), FolderMeaning::Trash);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_set_uid_next_validity() {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||||
|
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||||
|
|
||||||
|
set_uidvalidity(&t.ctx, "Inbox", 7).await.unwrap();
|
||||||
|
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 7);
|
||||||
|
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||||
|
|
||||||
|
set_uid_next(&t.ctx, "Inbox", 5).await.unwrap();
|
||||||
|
set_uidvalidity(&t.ctx, "Inbox", 6).await.unwrap();
|
||||||
|
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 5);
|
||||||
|
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_sequence_sets() {
|
||||||
|
assert_eq!(build_sequence_sets(&[]).unwrap(), vec![]);
|
||||||
|
|
||||||
|
let cases = vec![
|
||||||
|
(vec![1], "1"),
|
||||||
|
(vec![3291], "3291"),
|
||||||
|
(vec![1, 3, 5, 7, 9, 11], "1,3,5,7,9,11"),
|
||||||
|
(vec![1, 2, 3], "1:3"),
|
||||||
|
(vec![1, 4, 5, 6], "1,4:6"),
|
||||||
|
((1..=500).collect(), "1:500"),
|
||||||
|
(vec![3, 4, 8, 9, 10, 11, 39, 50, 2], "3:4,8:11,39,50,2"),
|
||||||
|
];
|
||||||
|
for (input, s) in cases {
|
||||||
|
assert_eq!(
|
||||||
|
build_sequence_sets(&input).unwrap(),
|
||||||
|
vec![(input, s.into())]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_number = |(uids, s): &(Vec<u32>, String), number| {
|
||||||
|
uids.iter().any(|&n| n == number)
|
||||||
|
&& s.split(',').any(|n| n.parse::<u32>().unwrap() == number)
|
||||||
|
};
|
||||||
|
|
||||||
|
let numbers: Vec<_> = (2..=500).step_by(2).collect();
|
||||||
|
let result = build_sequence_sets(&numbers).unwrap();
|
||||||
|
for (_, set) in &result {
|
||||||
|
assert!(set.len() < 1010);
|
||||||
|
assert!(!set.ends_with(','));
|
||||||
|
assert!(!set.starts_with(','));
|
||||||
|
}
|
||||||
|
assert!(result.len() == 1); // these UIDs fit in one set
|
||||||
|
for &number in &numbers {
|
||||||
|
assert!(result.iter().any(|r| has_number(r, number)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let numbers: Vec<_> = (1..=1000).step_by(3).collect();
|
||||||
|
let result = build_sequence_sets(&numbers).unwrap();
|
||||||
|
for (_, set) in &result {
|
||||||
|
assert!(set.len() < 1010);
|
||||||
|
assert!(!set.ends_with(','));
|
||||||
|
assert!(!set.starts_with(','));
|
||||||
|
}
|
||||||
|
let (last_uids, last_str) = result.last().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
last_uids.get((last_uids.len() - 2)..).unwrap(),
|
||||||
|
&[997, 1000]
|
||||||
|
);
|
||||||
|
assert!(last_str.ends_with("997,1000"));
|
||||||
|
assert!(result.len() == 2); // This time we need 2 sets
|
||||||
|
for &number in &numbers {
|
||||||
|
assert!(result.iter().any(|r| has_number(r, number)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let numbers: Vec<_> = (30000000..=30002500).step_by(4).collect();
|
||||||
|
let result = build_sequence_sets(&numbers).unwrap();
|
||||||
|
for (_, set) in &result {
|
||||||
|
assert!(set.len() < 1010);
|
||||||
|
assert!(!set.ends_with(','));
|
||||||
|
assert!(!set.starts_with(','));
|
||||||
|
}
|
||||||
|
assert_eq!(result.len(), 6);
|
||||||
|
for &number in &numbers {
|
||||||
|
assert!(result.iter().any(|r| has_number(r, number)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_target_folder_combination(
|
||||||
|
folder: &str,
|
||||||
|
mvbox_move: bool,
|
||||||
|
chat_msg: bool,
|
||||||
|
expected_destination: &str,
|
||||||
|
accepted_chat: bool,
|
||||||
|
outgoing: bool,
|
||||||
|
setupmessage: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
println!("Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}");
|
||||||
|
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
t.ctx
|
||||||
|
.set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat"))
|
||||||
|
.await?;
|
||||||
|
t.ctx
|
||||||
|
.set_config(Config::ConfiguredSentboxFolder, Some("Sent"))
|
||||||
|
.await?;
|
||||||
|
t.ctx
|
||||||
|
.set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" }))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if accepted_chat {
|
||||||
|
let contact_id = Contact::create(&t.ctx, "", "bob@example.net").await?;
|
||||||
|
ChatId::create_for_contact(&t.ctx, contact_id).await?;
|
||||||
|
}
|
||||||
|
let temp;
|
||||||
|
|
||||||
|
let bytes = if setupmessage {
|
||||||
|
include_bytes!("../../test-data/message/AutocryptSetupMessage.eml")
|
||||||
|
} else {
|
||||||
|
temp = format!(
|
||||||
|
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||||
|
{}\
|
||||||
|
Subject: foo\n\
|
||||||
|
Message-ID: <abc@example.com>\n\
|
||||||
|
{}\
|
||||||
|
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||||
|
\n\
|
||||||
|
hello\n",
|
||||||
|
if outgoing {
|
||||||
|
"From: alice@example.org\nTo: bob@example.net\n"
|
||||||
|
} else {
|
||||||
|
"From: bob@example.net\nTo: alice@example.org\n"
|
||||||
|
},
|
||||||
|
if chat_msg { "Chat-Version: 1.0\n" } else { "" },
|
||||||
|
);
|
||||||
|
temp.as_bytes()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (headers, _) = mailparse::parse_headers(bytes)?;
|
||||||
|
let actual = if let Some(config) =
|
||||||
|
target_folder_cfg(&t, folder, get_folder_meaning_by_name(folder), &headers).await?
|
||||||
|
{
|
||||||
|
t.get_config(config).await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected = if expected_destination == folder {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(expected_destination)
|
||||||
|
};
|
||||||
|
assert_eq!(expected, actual.as_deref(), "For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// chat_msg means that the message was sent by Delta Chat
|
||||||
|
// The tuples are (folder, mvbox_move, chat_msg, expected_destination)
|
||||||
|
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
|
||||||
|
("INBOX", false, false, "INBOX"),
|
||||||
|
("INBOX", false, true, "INBOX"),
|
||||||
|
("INBOX", true, false, "INBOX"),
|
||||||
|
("INBOX", true, true, "DeltaChat"),
|
||||||
|
("Sent", false, false, "Sent"),
|
||||||
|
("Sent", false, true, "Sent"),
|
||||||
|
("Sent", true, false, "Sent"),
|
||||||
|
("Sent", true, true, "DeltaChat"),
|
||||||
|
("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||||
|
("Spam", false, true, "INBOX"),
|
||||||
|
("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||||
|
("Spam", true, true, "DeltaChat"),
|
||||||
|
];
|
||||||
|
|
||||||
|
// These are the same as above, but non-chat messages in Spam stay in Spam
|
||||||
|
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
|
||||||
|
("INBOX", false, false, "INBOX"),
|
||||||
|
("INBOX", false, true, "INBOX"),
|
||||||
|
("INBOX", true, false, "INBOX"),
|
||||||
|
("INBOX", true, true, "DeltaChat"),
|
||||||
|
("Sent", false, false, "Sent"),
|
||||||
|
("Sent", false, true, "Sent"),
|
||||||
|
("Sent", true, false, "Sent"),
|
||||||
|
("Sent", true, true, "DeltaChat"),
|
||||||
|
("Spam", false, false, "Spam"),
|
||||||
|
("Spam", false, true, "INBOX"),
|
||||||
|
("Spam", true, false, "Spam"),
|
||||||
|
("Spam", true, true, "DeltaChat"),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_target_folder_incoming_accepted() -> Result<()> {
|
||||||
|
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||||
|
check_target_folder_combination(
|
||||||
|
folder,
|
||||||
|
*mvbox_move,
|
||||||
|
*chat_msg,
|
||||||
|
expected_destination,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_target_folder_incoming_request() -> Result<()> {
|
||||||
|
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
|
||||||
|
check_target_folder_combination(
|
||||||
|
folder,
|
||||||
|
*mvbox_move,
|
||||||
|
*chat_msg,
|
||||||
|
expected_destination,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_target_folder_outgoing() -> Result<()> {
|
||||||
|
// Test outgoing emails
|
||||||
|
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||||
|
check_target_folder_combination(
|
||||||
|
folder,
|
||||||
|
*mvbox_move,
|
||||||
|
*chat_msg,
|
||||||
|
expected_destination,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_target_folder_setupmsg() -> Result<()> {
|
||||||
|
// Test setupmessages
|
||||||
|
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||||
|
check_target_folder_combination(
|
||||||
|
folder,
|
||||||
|
*mvbox_move,
|
||||||
|
*chat_msg,
|
||||||
|
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_get_imap_search_command() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
assert_eq!(
|
||||||
|
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||||
|
r#"FROM "alice@example.org""#
|
||||||
|
);
|
||||||
|
|
||||||
|
t.ctx.set_primary_self_addr("alice@another.com").await?;
|
||||||
|
assert_eq!(
|
||||||
|
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||||
|
r#"OR (FROM "alice@another.com") (FROM "alice@example.org")"#
|
||||||
|
);
|
||||||
|
|
||||||
|
t.ctx.set_primary_self_addr("alice@third.com").await?;
|
||||||
|
assert_eq!(
|
||||||
|
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||||
|
r#"OR (OR (FROM "alice@third.com") (FROM "alice@another.com")) (FROM "alice@example.org")"#
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uid_grouper() {
|
||||||
|
// Input: sequence of (rowid: i64, uid: u32, target: String)
|
||||||
|
// Output: sequence of (target: String, rowid_set: Vec<i64>, uid_set: String)
|
||||||
|
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string())]);
|
||||||
|
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||||
|
assert_eq!(res, vec![("INBOX".to_string(), vec![1], "2".to_string())]);
|
||||||
|
|
||||||
|
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string()), (2, 3, "INBOX".to_string())]);
|
||||||
|
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
vec![("INBOX".to_string(), vec![1, 2], "2:3".to_string())]
|
||||||
|
);
|
||||||
|
|
||||||
|
let grouper = UidGrouper::from([
|
||||||
|
(1, 2, "INBOX".to_string()),
|
||||||
|
(2, 2, "INBOX".to_string()),
|
||||||
|
(3, 3, "INBOX".to_string()),
|
||||||
|
]);
|
||||||
|
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_setmetadata_device_token() {
|
||||||
|
assert_eq!(
|
||||||
|
format_setmetadata("INBOX", "foobarbaz"),
|
||||||
|
"SETMETADATA \"INBOX\" (/private/devicetoken {9+}\r\nfoobarbaz)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format_setmetadata("INBOX", "foo\r\nbar\r\nbaz\r\n"),
|
||||||
|
"SETMETADATA \"INBOX\" (/private/devicetoken {15+}\r\nfoo\r\nbar\r\nbaz\r\n)"
|
||||||
|
);
|
||||||
|
}
|
||||||
922
src/qr.rs
922
src/qr.rs
@@ -947,924 +947,4 @@ fn normalize_address(addr: &str) -> Result<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod qr_tests;
|
||||||
use super::*;
|
|
||||||
use crate::aheader::EncryptPreference;
|
|
||||||
use crate::chat::{create_group_chat, ProtectionStatus};
|
|
||||||
use crate::config::Config;
|
|
||||||
use crate::key::DcKey;
|
|
||||||
use crate::securejoin::get_securejoin_qr;
|
|
||||||
use crate::test_utils::{alice_keypair, TestContext};
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_http() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com:80").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Proxy {
|
|
||||||
url: "http://www.hello.com:80".to_string(),
|
|
||||||
host: "www.hello.com".to_string(),
|
|
||||||
port: 80
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// If it has no explicit port, then it is not a proxy.
|
|
||||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Url {
|
|
||||||
url: "http://www.hello.com".to_string(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// If it has a path, then it is not a proxy.
|
|
||||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com/").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Url {
|
|
||||||
url: "http://www.hello.com/".to_string(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com/hello").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Url {
|
|
||||||
url: "http://www.hello.com/hello".to_string(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test that QR code whitespace is stripped.
|
|
||||||
// Users can copy-paste QR code contents and "scan"
|
|
||||||
// from the clipboard.
|
|
||||||
let qr = check_qr(&ctx.ctx, " \thttp://www.hello.com/hello \n\t \r\n ").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Url {
|
|
||||||
url: "http://www.hello.com/hello".to_string(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_https() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com:443").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Proxy {
|
|
||||||
url: "https://www.hello.com:443".to_string(),
|
|
||||||
host: "www.hello.com".to_string(),
|
|
||||||
port: 443
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// If it has no explicit port, then it is not a proxy.
|
|
||||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Url {
|
|
||||||
url: "https://www.hello.com".to_string(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// If it has a path, then it is not a proxy.
|
|
||||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com/").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Url {
|
|
||||||
url: "https://www.hello.com/".to_string(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com/hello").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Url {
|
|
||||||
url: "https://www.hello.com/hello".to_string(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_text() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(&ctx.ctx, "I am so cool").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Text {
|
|
||||||
text: "I am so cool".to_string()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_vcard() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"BEGIN:VCARD\nVERSION:3.0\nN:Last;First\nEMAIL;TYPE=INTERNET:stress@test.local\nEND:VCARD"
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
if let Qr::Addr { contact_id, draft } = qr {
|
|
||||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
|
||||||
assert_eq!(contact.get_addr(), "stress@test.local");
|
|
||||||
assert_eq!(contact.get_name(), "First Last");
|
|
||||||
assert_eq!(contact.get_authname(), "");
|
|
||||||
assert_eq!(contact.get_display_name(), "First Last");
|
|
||||||
assert!(draft.is_none());
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR code type");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_matmsg() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"MATMSG:TO:\n\nstress@test.local ; \n\nSUB:\n\nSubject here\n\nBODY:\n\nhelloworld\n;;",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if let Qr::Addr { contact_id, draft } = qr {
|
|
||||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
|
||||||
assert_eq!(contact.get_addr(), "stress@test.local");
|
|
||||||
assert!(draft.is_none());
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR code type");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_mailto() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"mailto:stress@test.local?subject=hello&body=beautiful+world",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
if let Qr::Addr { contact_id, draft } = qr {
|
|
||||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
|
||||||
assert_eq!(contact.get_addr(), "stress@test.local");
|
|
||||||
assert_eq!(draft.unwrap(), "hello\nbeautiful world");
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR code type");
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = check_qr(&ctx.ctx, "mailto:no-questionmark@example.org").await?;
|
|
||||||
if let Qr::Addr { contact_id, draft } = res {
|
|
||||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
|
||||||
assert_eq!(contact.get_addr(), "no-questionmark@example.org");
|
|
||||||
assert!(draft.is_none());
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR code type");
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = check_qr(&ctx.ctx, "mailto:no-addr").await;
|
|
||||||
assert!(res.is_err());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_smtp() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
if let Qr::Addr { contact_id, draft } =
|
|
||||||
check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld").await?
|
|
||||||
{
|
|
||||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
|
||||||
assert_eq!(contact.get_addr(), "stress@test.local");
|
|
||||||
assert!(draft.is_none());
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR code type");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_ideltachat_link() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"https://i.delta.chat/#79252762C34C5096AF57958F4FC3D21A81B0F0A7&a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
|
||||||
).await?;
|
|
||||||
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
|
|
||||||
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"https://i.delta.chat#79252762C34C5096AF57958F4FC3D21A81B0F0A7&a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
|
||||||
).await?;
|
|
||||||
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too.
|
|
||||||
// see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_openpgp_tolerance_for_issue_1969() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7%23a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_openpgp_group() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
|
||||||
).await?;
|
|
||||||
if let Qr::AskVerifyGroup {
|
|
||||||
contact_id,
|
|
||||||
grpname,
|
|
||||||
..
|
|
||||||
} = qr
|
|
||||||
{
|
|
||||||
assert_ne!(contact_id, ContactId::UNDEFINED);
|
|
||||||
assert_eq!(grpname, "test ? test !");
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR code type");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
|
||||||
).await?;
|
|
||||||
if let Qr::AskVerifyGroup {
|
|
||||||
contact_id,
|
|
||||||
grpname,
|
|
||||||
..
|
|
||||||
} = qr
|
|
||||||
{
|
|
||||||
assert_ne!(contact_id, ContactId::UNDEFINED);
|
|
||||||
assert_eq!(grpname, "test ? test !");
|
|
||||||
|
|
||||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
|
||||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR code type");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_openpgp_invalid_token() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
// Token cannot contain "/"
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL/cxRL"
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
assert!(matches!(qr, Qr::FprMismatch { .. }));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_openpgp_secure_join() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
|
||||||
assert_ne!(contact_id, ContactId::UNDEFINED);
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR code type");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
|
||||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
|
||||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
|
||||||
assert_eq!(contact.get_authname(), "Jörn P. P.");
|
|
||||||
assert_eq!(contact.get_name(), "");
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR code type");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regression test
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
|
||||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
|
||||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
|
||||||
assert_eq!(contact.get_authname(), "");
|
|
||||||
assert_eq!(contact.get_name(), "");
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR code type");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_openpgp_fingerprint() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let alice_contact_id = Contact::create(&ctx, "Alice", "alice@example.org")
|
|
||||||
.await
|
|
||||||
.context("failed to create contact")?;
|
|
||||||
let pub_key = alice_keypair().public;
|
|
||||||
let peerstate = Peerstate {
|
|
||||||
addr: "alice@example.org".to_string(),
|
|
||||||
last_seen: 1,
|
|
||||||
last_seen_autocrypt: 1,
|
|
||||||
prefer_encrypt: EncryptPreference::Mutual,
|
|
||||||
public_key: Some(pub_key.clone()),
|
|
||||||
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
|
||||||
gossip_key: None,
|
|
||||||
gossip_timestamp: 0,
|
|
||||||
gossip_key_fingerprint: None,
|
|
||||||
verified_key: None,
|
|
||||||
verified_key_fingerprint: None,
|
|
||||||
verifier: None,
|
|
||||||
secondary_verified_key: None,
|
|
||||||
secondary_verified_key_fingerprint: None,
|
|
||||||
secondary_verifier: None,
|
|
||||||
backward_verified_key_id: None,
|
|
||||||
fingerprint_changed: false,
|
|
||||||
};
|
|
||||||
assert!(
|
|
||||||
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
|
|
||||||
"failed to save peerstate"
|
|
||||||
);
|
|
||||||
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
if let Qr::FprMismatch { contact_id, .. } = qr {
|
|
||||||
assert_eq!(contact_id, Some(alice_contact_id));
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR code type");
|
|
||||||
}
|
|
||||||
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
&format!(
|
|
||||||
"OPENPGP4FPR:{}#a=alice@example.org",
|
|
||||||
pub_key.dc_fingerprint()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
if let Qr::FprOk { contact_id, .. } = qr {
|
|
||||||
assert_eq!(contact_id, alice_contact_id);
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR code type");
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=bob@example.org",
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
Qr::FprMismatch { contact_id: None }
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_openpgp_without_addr() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"OPENPGP4FPR:1234567890123456789012345678901234567890",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::FprWithoutAddr {
|
|
||||||
fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
|
||||||
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"openpgp4fpr:1234567890123456789012345678901234567890",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::FprWithoutAddr {
|
|
||||||
fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890").await;
|
|
||||||
assert!(res.is_err());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_withdraw_verifycontact() -> Result<()> {
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
let qr = get_securejoin_qr(&alice, None).await?;
|
|
||||||
|
|
||||||
// scanning own verify-contact code offers withdrawing
|
|
||||||
assert!(matches!(
|
|
||||||
check_qr(&alice, &qr).await?,
|
|
||||||
Qr::WithdrawVerifyContact { .. }
|
|
||||||
));
|
|
||||||
set_config_from_qr(&alice, &qr).await?;
|
|
||||||
|
|
||||||
// scanning withdrawn verify-contact code offers reviving
|
|
||||||
assert!(matches!(
|
|
||||||
check_qr(&alice, &qr).await?,
|
|
||||||
Qr::ReviveVerifyContact { .. }
|
|
||||||
));
|
|
||||||
set_config_from_qr(&alice, &qr).await?;
|
|
||||||
assert!(matches!(
|
|
||||||
check_qr(&alice, &qr).await?,
|
|
||||||
Qr::WithdrawVerifyContact { .. }
|
|
||||||
));
|
|
||||||
|
|
||||||
// someone else always scans as ask-verify-contact
|
|
||||||
let bob = TestContext::new_bob().await;
|
|
||||||
assert!(matches!(
|
|
||||||
check_qr(&bob, &qr).await?,
|
|
||||||
Qr::AskVerifyContact { .. }
|
|
||||||
));
|
|
||||||
assert!(set_config_from_qr(&bob, &qr).await.is_err());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_withdraw_verifygroup() -> Result<()> {
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
|
||||||
let qr = get_securejoin_qr(&alice, Some(chat_id)).await?;
|
|
||||||
|
|
||||||
// scanning own verify-group code offers withdrawing
|
|
||||||
if let Qr::WithdrawVerifyGroup { grpname, .. } = check_qr(&alice, &qr).await? {
|
|
||||||
assert_eq!(grpname, "foo");
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR type, expected WithdrawVerifyGroup");
|
|
||||||
}
|
|
||||||
set_config_from_qr(&alice, &qr).await?;
|
|
||||||
|
|
||||||
// scanning withdrawn verify-group code offers reviving
|
|
||||||
if let Qr::ReviveVerifyGroup { grpname, .. } = check_qr(&alice, &qr).await? {
|
|
||||||
assert_eq!(grpname, "foo");
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR type, expected ReviveVerifyGroup");
|
|
||||||
}
|
|
||||||
|
|
||||||
// someone else always scans as ask-verify-group
|
|
||||||
let bob = TestContext::new_bob().await;
|
|
||||||
if let Qr::AskVerifyGroup { grpname, .. } = check_qr(&bob, &qr).await? {
|
|
||||||
assert_eq!(grpname, "foo");
|
|
||||||
} else {
|
|
||||||
bail!("Wrong QR type, expected AskVerifyGroup");
|
|
||||||
}
|
|
||||||
assert!(set_config_from_qr(&bob, &qr).await.is_err());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_and_apply_dclogin() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let result = check_qr(&ctx.ctx, "dclogin:usename+extension@host?p=1234&v=1").await?;
|
|
||||||
if let Qr::Login { address, options } = result {
|
|
||||||
assert_eq!(address, "usename+extension@host".to_owned());
|
|
||||||
|
|
||||||
if let LoginOptions::V1 { mail_pw, .. } = options {
|
|
||||||
assert_eq!(mail_pw, "1234".to_owned());
|
|
||||||
} else {
|
|
||||||
bail!("wrong type")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bail!("wrong type")
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(ctx.ctx.get_config(Config::Addr).await?.is_none());
|
|
||||||
assert!(ctx.ctx.get_config(Config::MailPw).await?.is_none());
|
|
||||||
|
|
||||||
set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&v=1").await?;
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::Addr).await?,
|
|
||||||
Some("username+extension@host".to_owned())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::MailPw).await?,
|
|
||||||
Some("1234".to_owned())
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_and_apply_dclogin_advanced_options() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&spw=4321&sh=send.host&sp=7273&su=SendUser&ih=host.tld&ip=4343&iu=user&ipw=password&is=ssl&ic=1&sc=3&ss=plain&v=1").await?;
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::Addr).await?,
|
|
||||||
Some("username+extension@host".to_owned())
|
|
||||||
);
|
|
||||||
|
|
||||||
// `p=1234` is ignored, because `ipw=password` is set
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::MailServer).await?,
|
|
||||||
Some("host.tld".to_owned())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::MailPort).await?,
|
|
||||||
Some("4343".to_owned())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::MailUser).await?,
|
|
||||||
Some("user".to_owned())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::MailPw).await?,
|
|
||||||
Some("password".to_owned())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::MailSecurity).await?,
|
|
||||||
Some("1".to_owned()) // ssl
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::ImapCertificateChecks).await?,
|
|
||||||
Some("1".to_owned())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::SendPw).await?,
|
|
||||||
Some("4321".to_owned())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::SendServer).await?,
|
|
||||||
Some("send.host".to_owned())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::SendPort).await?,
|
|
||||||
Some("7273".to_owned())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::SendUser).await?,
|
|
||||||
Some("SendUser".to_owned())
|
|
||||||
);
|
|
||||||
|
|
||||||
// `sc` option is actually ignored and `ic` is used instead
|
|
||||||
// because `smtp_certificate_checks` is deprecated.
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::SmtpCertificateChecks).await?,
|
|
||||||
Some("1".to_owned())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::SendSecurity).await?,
|
|
||||||
Some("3".to_owned()) // plain
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_account() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Account {
|
|
||||||
domain: "example.org".to_string()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test it again with lowercased "dcaccount:" uri scheme
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"dcaccount:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Account {
|
|
||||||
domain: "example.org".to_string()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_webrtc_instance() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://basicurl.com/$ROOM").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::WebrtcInstance {
|
|
||||||
domain: "basicurl.com".to_string(),
|
|
||||||
instance_pattern: "basicwebrtc:https://basicurl.com/$ROOM".to_string()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test it again with mixcased "dcWebRTC:" uri scheme
|
|
||||||
let qr = check_qr(&ctx.ctx, "dcWebRTC:https://example.org/").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::WebrtcInstance {
|
|
||||||
domain: "example.org".to_string(),
|
|
||||||
instance_pattern: "https://example.org/".to_string()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_tg_socks_proxy() -> Result<()> {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(&t, "https://t.me/socks?server=84.53.239.95&port=4145").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Proxy {
|
|
||||||
url: "socks5://84.53.239.95:4145".to_string(),
|
|
||||||
host: "84.53.239.95".to_string(),
|
|
||||||
port: 4145,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let qr = check_qr(&t, "https://t.me/socks?server=foo.bar&port=123").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Proxy {
|
|
||||||
url: "socks5://foo.bar:123".to_string(),
|
|
||||||
host: "foo.bar".to_string(),
|
|
||||||
port: 123,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let qr = check_qr(&t, "https://t.me/socks?server=foo.baz").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Proxy {
|
|
||||||
url: "socks5://foo.baz:1080".to_string(),
|
|
||||||
host: "foo.baz".to_string(),
|
|
||||||
port: 1080,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let qr = check_qr(
|
|
||||||
&t,
|
|
||||||
"https://t.me/socks?server=foo.baz&port=12345&user=ada&pass=ms%21%2F%24",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Proxy {
|
|
||||||
url: "socks5://ada:ms%21%2F%24@foo.baz:12345".to_string(),
|
|
||||||
host: "foo.baz".to_string(),
|
|
||||||
port: 12345,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// wrong domain results in Qr:Url instead of Qr::Socks5Proxy
|
|
||||||
let qr = check_qr(&t, "https://not.me/socks?noserver=84.53.239.95&port=4145").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Url {
|
|
||||||
url: "https://not.me/socks?noserver=84.53.239.95&port=4145".to_string()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let qr = check_qr(&t, "https://t.me/socks?noserver=84.53.239.95&port=4145").await;
|
|
||||||
assert!(qr.is_err());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_account_bad_scheme() {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
let res = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"DCACCOUNT:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert!(res.is_err());
|
|
||||||
|
|
||||||
// Test it again with lowercased "dcaccount:" uri scheme
|
|
||||||
let res = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"dcaccount:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert!(res.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_set_webrtc_instance_config_from_qr() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
|
|
||||||
|
|
||||||
let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await;
|
|
||||||
assert!(res.is_err());
|
|
||||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
|
|
||||||
|
|
||||||
let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
|
|
||||||
assert!(res.is_ok());
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(),
|
|
||||||
"https://example.org/"
|
|
||||||
);
|
|
||||||
|
|
||||||
let res =
|
|
||||||
set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await;
|
|
||||||
assert!(res.is_ok());
|
|
||||||
assert_eq!(
|
|
||||||
ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(),
|
|
||||||
"basicwebrtc:https://foo.bar/?$ROOM&test"
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_set_proxy_config_from_qr() -> Result<()> {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
|
|
||||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, false);
|
|
||||||
|
|
||||||
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
|
|
||||||
assert!(res.is_ok());
|
|
||||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
|
||||||
assert_eq!(
|
|
||||||
t.get_config(Config::ProxyUrl).await?,
|
|
||||||
Some("socks5://foo:666".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test URL without port.
|
|
||||||
//
|
|
||||||
// Also check that whitespace is trimmed.
|
|
||||||
let res = set_config_from_qr(&t, " https://t.me/socks?server=1.2.3.4\n").await;
|
|
||||||
assert!(res.is_ok());
|
|
||||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
|
||||||
assert_eq!(
|
|
||||||
t.get_config(Config::ProxyUrl).await?,
|
|
||||||
Some("socks5://1.2.3.4:1080\nsocks5://foo:666".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
// make sure, user&password are set when specified in the URL
|
|
||||||
// Password is an URL-encoded "x&%$X".
|
|
||||||
let res =
|
|
||||||
set_config_from_qr(&t, "https://t.me/socks?server=jau&user=Da&pass=x%26%25%24X").await;
|
|
||||||
assert!(res.is_ok());
|
|
||||||
assert_eq!(
|
|
||||||
t.get_config(Config::ProxyUrl).await?,
|
|
||||||
Some(
|
|
||||||
"socks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080\nsocks5://foo:666"
|
|
||||||
.to_string()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Scanning existing proxy brings it to the top in the list.
|
|
||||||
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
|
|
||||||
assert!(res.is_ok());
|
|
||||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
|
||||||
assert_eq!(
|
|
||||||
t.get_config(Config::ProxyUrl).await?,
|
|
||||||
Some(
|
|
||||||
"socks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080"
|
|
||||||
.to_string()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
set_config_from_qr(
|
|
||||||
&t,
|
|
||||||
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
t.get_config(Config::ProxyUrl).await?,
|
|
||||||
Some(
|
|
||||||
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080"
|
|
||||||
.to_string()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// SOCKS5 config does not have port 1080 explicitly specified,
|
|
||||||
// but should bring `socks5://1.2.3.4:1080` to the top instead of creating another entry.
|
|
||||||
set_config_from_qr(&t, "socks5://1.2.3.4").await?;
|
|
||||||
assert_eq!(
|
|
||||||
t.get_config(Config::ProxyUrl).await?,
|
|
||||||
Some(
|
|
||||||
"socks5://1.2.3.4:1080\nss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080"
|
|
||||||
.to_string()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_shadowsocks() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(
|
|
||||||
&ctx.ctx,
|
|
||||||
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Proxy {
|
|
||||||
url: "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1".to_string(),
|
|
||||||
host: "192.168.100.1".to_string(),
|
|
||||||
port: 8888,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_socks5() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(&ctx.ctx, "socks5://127.0.0.1:9050").await?;
|
|
||||||
assert_eq!(
|
|
||||||
qr,
|
|
||||||
Qr::Proxy {
|
|
||||||
url: "socks5://127.0.0.1:9050".to_string(),
|
|
||||||
host: "127.0.0.1".to_string(),
|
|
||||||
port: 9050,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure that `DCBACKUP2` QR code does not fail to deserialize
|
|
||||||
/// because iroh changes the format of `NodeAddr`
|
|
||||||
/// as happened between iroh 0.29 and iroh 0.30 before.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_decode_backup() -> Result<()> {
|
|
||||||
let ctx = TestContext::new().await;
|
|
||||||
|
|
||||||
let qr = check_qr(&ctx, r#"DCBACKUP2:TWSv6ZjDPa5eoxkocj7xMi8r&{"node_id":"9afc1ea5b4f543e5cdd7b7a21cd26aee7c0b1e1c2af26790896fbd8932a06e1e","relay_url":null,"direct_addresses":["192.168.1.10:12345"]}"#).await?;
|
|
||||||
assert!(matches!(qr, Qr::Backup2 { .. }));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
918
src/qr/qr_tests.rs
Normal file
918
src/qr/qr_tests.rs
Normal file
@@ -0,0 +1,918 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::aheader::EncryptPreference;
|
||||||
|
use crate::chat::{create_group_chat, ProtectionStatus};
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::key::DcKey;
|
||||||
|
use crate::securejoin::get_securejoin_qr;
|
||||||
|
use crate::test_utils::{alice_keypair, TestContext};
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_http() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(&ctx.ctx, "http://www.hello.com:80").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Proxy {
|
||||||
|
url: "http://www.hello.com:80".to_string(),
|
||||||
|
host: "www.hello.com".to_string(),
|
||||||
|
port: 80
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// If it has no explicit port, then it is not a proxy.
|
||||||
|
let qr = check_qr(&ctx.ctx, "http://www.hello.com").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Url {
|
||||||
|
url: "http://www.hello.com".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// If it has a path, then it is not a proxy.
|
||||||
|
let qr = check_qr(&ctx.ctx, "http://www.hello.com/").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Url {
|
||||||
|
url: "http://www.hello.com/".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let qr = check_qr(&ctx.ctx, "http://www.hello.com/hello").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Url {
|
||||||
|
url: "http://www.hello.com/hello".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test that QR code whitespace is stripped.
|
||||||
|
// Users can copy-paste QR code contents and "scan"
|
||||||
|
// from the clipboard.
|
||||||
|
let qr = check_qr(&ctx.ctx, " \thttp://www.hello.com/hello \n\t \r\n ").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Url {
|
||||||
|
url: "http://www.hello.com/hello".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_https() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(&ctx.ctx, "https://www.hello.com:443").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Proxy {
|
||||||
|
url: "https://www.hello.com:443".to_string(),
|
||||||
|
host: "www.hello.com".to_string(),
|
||||||
|
port: 443
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// If it has no explicit port, then it is not a proxy.
|
||||||
|
let qr = check_qr(&ctx.ctx, "https://www.hello.com").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Url {
|
||||||
|
url: "https://www.hello.com".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// If it has a path, then it is not a proxy.
|
||||||
|
let qr = check_qr(&ctx.ctx, "https://www.hello.com/").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Url {
|
||||||
|
url: "https://www.hello.com/".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let qr = check_qr(&ctx.ctx, "https://www.hello.com/hello").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Url {
|
||||||
|
url: "https://www.hello.com/hello".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_text() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(&ctx.ctx, "I am so cool").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Text {
|
||||||
|
text: "I am so cool".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_vcard() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"BEGIN:VCARD\nVERSION:3.0\nN:Last;First\nEMAIL;TYPE=INTERNET:stress@test.local\nEND:VCARD",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Qr::Addr { contact_id, draft } = qr {
|
||||||
|
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||||
|
assert_eq!(contact.get_addr(), "stress@test.local");
|
||||||
|
assert_eq!(contact.get_name(), "First Last");
|
||||||
|
assert_eq!(contact.get_authname(), "");
|
||||||
|
assert_eq!(contact.get_display_name(), "First Last");
|
||||||
|
assert!(draft.is_none());
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR code type");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_matmsg() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"MATMSG:TO:\n\nstress@test.local ; \n\nSUB:\n\nSubject here\n\nBODY:\n\nhelloworld\n;;",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Qr::Addr { contact_id, draft } = qr {
|
||||||
|
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||||
|
assert_eq!(contact.get_addr(), "stress@test.local");
|
||||||
|
assert!(draft.is_none());
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR code type");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_mailto() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"mailto:stress@test.local?subject=hello&body=beautiful+world",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if let Qr::Addr { contact_id, draft } = qr {
|
||||||
|
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||||
|
assert_eq!(contact.get_addr(), "stress@test.local");
|
||||||
|
assert_eq!(draft.unwrap(), "hello\nbeautiful world");
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR code type");
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = check_qr(&ctx.ctx, "mailto:no-questionmark@example.org").await?;
|
||||||
|
if let Qr::Addr { contact_id, draft } = res {
|
||||||
|
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||||
|
assert_eq!(contact.get_addr(), "no-questionmark@example.org");
|
||||||
|
assert!(draft.is_none());
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR code type");
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = check_qr(&ctx.ctx, "mailto:no-addr").await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_smtp() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
if let Qr::Addr { contact_id, draft } =
|
||||||
|
check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld").await?
|
||||||
|
{
|
||||||
|
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||||
|
assert_eq!(contact.get_addr(), "stress@test.local");
|
||||||
|
assert!(draft.is_none());
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR code type");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_ideltachat_link() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"https://i.delta.chat/#79252762C34C5096AF57958F4FC3D21A81B0F0A7&a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||||
|
).await?;
|
||||||
|
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
|
||||||
|
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"https://i.delta.chat#79252762C34C5096AF57958F4FC3D21A81B0F0A7&a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||||
|
).await?;
|
||||||
|
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too.
|
||||||
|
// see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_openpgp_tolerance_for_issue_1969() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7%23a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_openpgp_group() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||||
|
).await?;
|
||||||
|
if let Qr::AskVerifyGroup {
|
||||||
|
contact_id,
|
||||||
|
grpname,
|
||||||
|
..
|
||||||
|
} = qr
|
||||||
|
{
|
||||||
|
assert_ne!(contact_id, ContactId::UNDEFINED);
|
||||||
|
assert_eq!(grpname, "test ? test !");
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR code type");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||||
|
).await?;
|
||||||
|
if let Qr::AskVerifyGroup {
|
||||||
|
contact_id,
|
||||||
|
grpname,
|
||||||
|
..
|
||||||
|
} = qr
|
||||||
|
{
|
||||||
|
assert_ne!(contact_id, ContactId::UNDEFINED);
|
||||||
|
assert_eq!(grpname, "test ? test !");
|
||||||
|
|
||||||
|
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||||
|
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR code type");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_openpgp_invalid_token() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
// Token cannot contain "/"
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL/cxRL"
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
assert!(matches!(qr, Qr::FprMismatch { .. }));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_openpgp_secure_join() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
||||||
|
assert_ne!(contact_id, ContactId::UNDEFINED);
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR code type");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
||||||
|
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||||
|
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||||
|
assert_eq!(contact.get_authname(), "Jörn P. P.");
|
||||||
|
assert_eq!(contact.get_name(), "");
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR code type");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regression test
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
||||||
|
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||||
|
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||||
|
assert_eq!(contact.get_authname(), "");
|
||||||
|
assert_eq!(contact.get_name(), "");
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR code type");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_openpgp_fingerprint() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let alice_contact_id = Contact::create(&ctx, "Alice", "alice@example.org")
|
||||||
|
.await
|
||||||
|
.context("failed to create contact")?;
|
||||||
|
let pub_key = alice_keypair().public;
|
||||||
|
let peerstate = Peerstate {
|
||||||
|
addr: "alice@example.org".to_string(),
|
||||||
|
last_seen: 1,
|
||||||
|
last_seen_autocrypt: 1,
|
||||||
|
prefer_encrypt: EncryptPreference::Mutual,
|
||||||
|
public_key: Some(pub_key.clone()),
|
||||||
|
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||||
|
gossip_key: None,
|
||||||
|
gossip_timestamp: 0,
|
||||||
|
gossip_key_fingerprint: None,
|
||||||
|
verified_key: None,
|
||||||
|
verified_key_fingerprint: None,
|
||||||
|
verifier: None,
|
||||||
|
secondary_verified_key: None,
|
||||||
|
secondary_verified_key_fingerprint: None,
|
||||||
|
secondary_verifier: None,
|
||||||
|
backward_verified_key_id: None,
|
||||||
|
fingerprint_changed: false,
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
|
||||||
|
"failed to save peerstate"
|
||||||
|
);
|
||||||
|
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if let Qr::FprMismatch { contact_id, .. } = qr {
|
||||||
|
assert_eq!(contact_id, Some(alice_contact_id));
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR code type");
|
||||||
|
}
|
||||||
|
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
&format!(
|
||||||
|
"OPENPGP4FPR:{}#a=alice@example.org",
|
||||||
|
pub_key.dc_fingerprint()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if let Qr::FprOk { contact_id, .. } = qr {
|
||||||
|
assert_eq!(contact_id, alice_contact_id);
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR code type");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=bob@example.org",
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
Qr::FprMismatch { contact_id: None }
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_openpgp_without_addr() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"OPENPGP4FPR:1234567890123456789012345678901234567890",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::FprWithoutAddr {
|
||||||
|
fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||||
|
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"openpgp4fpr:1234567890123456789012345678901234567890",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::FprWithoutAddr {
|
||||||
|
fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890").await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_withdraw_verifycontact() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let qr = get_securejoin_qr(&alice, None).await?;
|
||||||
|
|
||||||
|
// scanning own verify-contact code offers withdrawing
|
||||||
|
assert!(matches!(
|
||||||
|
check_qr(&alice, &qr).await?,
|
||||||
|
Qr::WithdrawVerifyContact { .. }
|
||||||
|
));
|
||||||
|
set_config_from_qr(&alice, &qr).await?;
|
||||||
|
|
||||||
|
// scanning withdrawn verify-contact code offers reviving
|
||||||
|
assert!(matches!(
|
||||||
|
check_qr(&alice, &qr).await?,
|
||||||
|
Qr::ReviveVerifyContact { .. }
|
||||||
|
));
|
||||||
|
set_config_from_qr(&alice, &qr).await?;
|
||||||
|
assert!(matches!(
|
||||||
|
check_qr(&alice, &qr).await?,
|
||||||
|
Qr::WithdrawVerifyContact { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
// someone else always scans as ask-verify-contact
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
assert!(matches!(
|
||||||
|
check_qr(&bob, &qr).await?,
|
||||||
|
Qr::AskVerifyContact { .. }
|
||||||
|
));
|
||||||
|
assert!(set_config_from_qr(&bob, &qr).await.is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_withdraw_verifygroup() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||||
|
let qr = get_securejoin_qr(&alice, Some(chat_id)).await?;
|
||||||
|
|
||||||
|
// scanning own verify-group code offers withdrawing
|
||||||
|
if let Qr::WithdrawVerifyGroup { grpname, .. } = check_qr(&alice, &qr).await? {
|
||||||
|
assert_eq!(grpname, "foo");
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR type, expected WithdrawVerifyGroup");
|
||||||
|
}
|
||||||
|
set_config_from_qr(&alice, &qr).await?;
|
||||||
|
|
||||||
|
// scanning withdrawn verify-group code offers reviving
|
||||||
|
if let Qr::ReviveVerifyGroup { grpname, .. } = check_qr(&alice, &qr).await? {
|
||||||
|
assert_eq!(grpname, "foo");
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR type, expected ReviveVerifyGroup");
|
||||||
|
}
|
||||||
|
|
||||||
|
// someone else always scans as ask-verify-group
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
if let Qr::AskVerifyGroup { grpname, .. } = check_qr(&bob, &qr).await? {
|
||||||
|
assert_eq!(grpname, "foo");
|
||||||
|
} else {
|
||||||
|
bail!("Wrong QR type, expected AskVerifyGroup");
|
||||||
|
}
|
||||||
|
assert!(set_config_from_qr(&bob, &qr).await.is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_and_apply_dclogin() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let result = check_qr(&ctx.ctx, "dclogin:usename+extension@host?p=1234&v=1").await?;
|
||||||
|
if let Qr::Login { address, options } = result {
|
||||||
|
assert_eq!(address, "usename+extension@host".to_owned());
|
||||||
|
|
||||||
|
if let LoginOptions::V1 { mail_pw, .. } = options {
|
||||||
|
assert_eq!(mail_pw, "1234".to_owned());
|
||||||
|
} else {
|
||||||
|
bail!("wrong type")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bail!("wrong type")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(ctx.ctx.get_config(Config::Addr).await?.is_none());
|
||||||
|
assert!(ctx.ctx.get_config(Config::MailPw).await?.is_none());
|
||||||
|
|
||||||
|
set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&v=1").await?;
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::Addr).await?,
|
||||||
|
Some("username+extension@host".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::MailPw).await?,
|
||||||
|
Some("1234".to_owned())
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_and_apply_dclogin_advanced_options() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&spw=4321&sh=send.host&sp=7273&su=SendUser&ih=host.tld&ip=4343&iu=user&ipw=password&is=ssl&ic=1&sc=3&ss=plain&v=1").await?;
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::Addr).await?,
|
||||||
|
Some("username+extension@host".to_owned())
|
||||||
|
);
|
||||||
|
|
||||||
|
// `p=1234` is ignored, because `ipw=password` is set
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::MailServer).await?,
|
||||||
|
Some("host.tld".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::MailPort).await?,
|
||||||
|
Some("4343".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::MailUser).await?,
|
||||||
|
Some("user".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::MailPw).await?,
|
||||||
|
Some("password".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::MailSecurity).await?,
|
||||||
|
Some("1".to_owned()) // ssl
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::ImapCertificateChecks).await?,
|
||||||
|
Some("1".to_owned())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::SendPw).await?,
|
||||||
|
Some("4321".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::SendServer).await?,
|
||||||
|
Some("send.host".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::SendPort).await?,
|
||||||
|
Some("7273".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::SendUser).await?,
|
||||||
|
Some("SendUser".to_owned())
|
||||||
|
);
|
||||||
|
|
||||||
|
// `sc` option is actually ignored and `ic` is used instead
|
||||||
|
// because `smtp_certificate_checks` is deprecated.
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::SmtpCertificateChecks).await?,
|
||||||
|
Some("1".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::SendSecurity).await?,
|
||||||
|
Some("3".to_owned()) // plain
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_account() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Account {
|
||||||
|
domain: "example.org".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test it again with lowercased "dcaccount:" uri scheme
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"dcaccount:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Account {
|
||||||
|
domain: "example.org".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_webrtc_instance() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://basicurl.com/$ROOM").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::WebrtcInstance {
|
||||||
|
domain: "basicurl.com".to_string(),
|
||||||
|
instance_pattern: "basicwebrtc:https://basicurl.com/$ROOM".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test it again with mixcased "dcWebRTC:" uri scheme
|
||||||
|
let qr = check_qr(&ctx.ctx, "dcWebRTC:https://example.org/").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::WebrtcInstance {
|
||||||
|
domain: "example.org".to_string(),
|
||||||
|
instance_pattern: "https://example.org/".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_tg_socks_proxy() -> Result<()> {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(&t, "https://t.me/socks?server=84.53.239.95&port=4145").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Proxy {
|
||||||
|
url: "socks5://84.53.239.95:4145".to_string(),
|
||||||
|
host: "84.53.239.95".to_string(),
|
||||||
|
port: 4145,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let qr = check_qr(&t, "https://t.me/socks?server=foo.bar&port=123").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Proxy {
|
||||||
|
url: "socks5://foo.bar:123".to_string(),
|
||||||
|
host: "foo.bar".to_string(),
|
||||||
|
port: 123,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let qr = check_qr(&t, "https://t.me/socks?server=foo.baz").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Proxy {
|
||||||
|
url: "socks5://foo.baz:1080".to_string(),
|
||||||
|
host: "foo.baz".to_string(),
|
||||||
|
port: 1080,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let qr = check_qr(
|
||||||
|
&t,
|
||||||
|
"https://t.me/socks?server=foo.baz&port=12345&user=ada&pass=ms%21%2F%24",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Proxy {
|
||||||
|
url: "socks5://ada:ms%21%2F%24@foo.baz:12345".to_string(),
|
||||||
|
host: "foo.baz".to_string(),
|
||||||
|
port: 12345,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// wrong domain results in Qr:Url instead of Qr::Socks5Proxy
|
||||||
|
let qr = check_qr(&t, "https://not.me/socks?noserver=84.53.239.95&port=4145").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Url {
|
||||||
|
url: "https://not.me/socks?noserver=84.53.239.95&port=4145".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let qr = check_qr(&t, "https://t.me/socks?noserver=84.53.239.95&port=4145").await;
|
||||||
|
assert!(qr.is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_account_bad_scheme() {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
let res = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"DCACCOUNT:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
|
||||||
|
// Test it again with lowercased "dcaccount:" uri scheme
|
||||||
|
let res = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"dcaccount:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_set_webrtc_instance_config_from_qr() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
|
||||||
|
|
||||||
|
let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
|
||||||
|
|
||||||
|
let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(),
|
||||||
|
"https://example.org/"
|
||||||
|
);
|
||||||
|
|
||||||
|
let res =
|
||||||
|
set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(),
|
||||||
|
"basicwebrtc:https://foo.bar/?$ROOM&test"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_set_proxy_config_from_qr() -> Result<()> {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
|
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, false);
|
||||||
|
|
||||||
|
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
||||||
|
assert_eq!(
|
||||||
|
t.get_config(Config::ProxyUrl).await?,
|
||||||
|
Some("socks5://foo:666".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test URL without port.
|
||||||
|
//
|
||||||
|
// Also check that whitespace is trimmed.
|
||||||
|
let res = set_config_from_qr(&t, " https://t.me/socks?server=1.2.3.4\n").await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
||||||
|
assert_eq!(
|
||||||
|
t.get_config(Config::ProxyUrl).await?,
|
||||||
|
Some("socks5://1.2.3.4:1080\nsocks5://foo:666".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
// make sure, user&password are set when specified in the URL
|
||||||
|
// Password is an URL-encoded "x&%$X".
|
||||||
|
let res =
|
||||||
|
set_config_from_qr(&t, "https://t.me/socks?server=jau&user=Da&pass=x%26%25%24X").await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
t.get_config(Config::ProxyUrl).await?,
|
||||||
|
Some(
|
||||||
|
"socks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080\nsocks5://foo:666".to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Scanning existing proxy brings it to the top in the list.
|
||||||
|
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
||||||
|
assert_eq!(
|
||||||
|
t.get_config(Config::ProxyUrl).await?,
|
||||||
|
Some(
|
||||||
|
"socks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080".to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
set_config_from_qr(
|
||||||
|
&t,
|
||||||
|
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
t.get_config(Config::ProxyUrl).await?,
|
||||||
|
Some(
|
||||||
|
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080"
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// SOCKS5 config does not have port 1080 explicitly specified,
|
||||||
|
// but should bring `socks5://1.2.3.4:1080` to the top instead of creating another entry.
|
||||||
|
set_config_from_qr(&t, "socks5://1.2.3.4").await?;
|
||||||
|
assert_eq!(
|
||||||
|
t.get_config(Config::ProxyUrl).await?,
|
||||||
|
Some(
|
||||||
|
"socks5://1.2.3.4:1080\nss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080"
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_shadowsocks() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Proxy {
|
||||||
|
url: "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1".to_string(),
|
||||||
|
host: "192.168.100.1".to_string(),
|
||||||
|
port: 8888,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_socks5() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(&ctx.ctx, "socks5://127.0.0.1:9050").await?;
|
||||||
|
assert_eq!(
|
||||||
|
qr,
|
||||||
|
Qr::Proxy {
|
||||||
|
url: "socks5://127.0.0.1:9050".to_string(),
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: 9050,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that `DCBACKUP2` QR code does not fail to deserialize
|
||||||
|
/// because iroh changes the format of `NodeAddr`
|
||||||
|
/// as happened between iroh 0.29 and iroh 0.30 before.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_decode_backup() -> Result<()> {
|
||||||
|
let ctx = TestContext::new().await;
|
||||||
|
|
||||||
|
let qr = check_qr(&ctx, r#"DCBACKUP2:TWSv6ZjDPa5eoxkocj7xMi8r&{"node_id":"9afc1ea5b4f543e5cdd7b7a21cd26aee7c0b1e1c2af26790896fbd8932a06e1e","relay_url":null,"direct_addresses":["192.168.1.10:12345"]}"#).await?;
|
||||||
|
assert!(matches!(qr, Qr::Backup2 { .. }));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -744,850 +744,4 @@ fn encrypted_and_signed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod securejoin_tests;
|
||||||
use deltachat_contact_tools::{ContactAddress, EmailAddress};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::chat::{remove_contact_from_chat, CantSendReason};
|
|
||||||
use crate::chatlist::Chatlist;
|
|
||||||
use crate::constants::{self, Chattype};
|
|
||||||
use crate::imex::{imex, ImexMode};
|
|
||||||
use crate::receive_imf::receive_imf;
|
|
||||||
use crate::stock_str::{self, chat_protection_enabled};
|
|
||||||
use crate::test_utils::{get_chat_msg, TimeShiftFalsePositiveNote};
|
|
||||||
use crate::test_utils::{TestContext, TestContextManager};
|
|
||||||
use crate::tools::SystemTime;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
enum SetupContactCase {
|
|
||||||
Normal,
|
|
||||||
CheckProtectionTimestamp,
|
|
||||||
WrongAliceGossip,
|
|
||||||
SecurejoinWaitTimeout,
|
|
||||||
AliceIsBot,
|
|
||||||
AliceHasName,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_setup_contact() {
|
|
||||||
test_setup_contact_ex(SetupContactCase::Normal).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_setup_contact_protection_timestamp() {
|
|
||||||
test_setup_contact_ex(SetupContactCase::CheckProtectionTimestamp).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_setup_contact_wrong_alice_gossip() {
|
|
||||||
test_setup_contact_ex(SetupContactCase::WrongAliceGossip).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_setup_contact_wait_timeout() {
|
|
||||||
test_setup_contact_ex(SetupContactCase::SecurejoinWaitTimeout).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_setup_contact_alice_is_bot() {
|
|
||||||
test_setup_contact_ex(SetupContactCase::AliceIsBot).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_setup_contact_alice_has_name() {
|
|
||||||
test_setup_contact_ex(SetupContactCase::AliceHasName).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn test_setup_contact_ex(case: SetupContactCase) {
|
|
||||||
let _n = TimeShiftFalsePositiveNote;
|
|
||||||
|
|
||||||
let mut tcm = TestContextManager::new();
|
|
||||||
let alice = tcm.alice().await;
|
|
||||||
let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap();
|
|
||||||
if case == SetupContactCase::AliceHasName {
|
|
||||||
alice
|
|
||||||
.set_config(Config::Displayname, Some("Alice"))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
let bob = tcm.bob().await;
|
|
||||||
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let alice_auto_submitted_hdr;
|
|
||||||
match case {
|
|
||||||
SetupContactCase::AliceIsBot => {
|
|
||||||
alice.set_config_bool(Config::Bot, true).await.unwrap();
|
|
||||||
alice_auto_submitted_hdr = "Auto-Submitted: auto-generated";
|
|
||||||
}
|
|
||||||
_ => alice_auto_submitted_hdr = "Auto-Submitted: auto-replied",
|
|
||||||
};
|
|
||||||
for t in [&alice, &bob] {
|
|
||||||
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Chatlist::try_load(&alice, 0, None, None)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.len(),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
|
||||||
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
|
||||||
// We want Bob to learn Alice's name from their messages, not from the QR code.
|
|
||||||
alice
|
|
||||||
.set_config(Config::Displayname, Some("Alice Exampleorg"))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Step 2: Bob scans QR-code, sends vc-request
|
|
||||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
|
||||||
.await
|
|
||||||
.expect("Error looking up contact")
|
|
||||||
.expect("Contact not found");
|
|
||||||
let sent = bob.pop_sent_msg().await;
|
|
||||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
|
||||||
assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap());
|
|
||||||
let msg = alice.parse_msg(&sent).await;
|
|
||||||
assert!(!msg.was_encrypted());
|
|
||||||
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
|
|
||||||
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
|
|
||||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
|
||||||
|
|
||||||
// Step 3: Alice receives vc-request, sends vc-auth-required
|
|
||||||
alice.recv_msg_trash(&sent).await;
|
|
||||||
assert_eq!(
|
|
||||||
Chatlist::try_load(&alice, 0, None, None)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.len(),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
let sent = alice.pop_sent_msg().await;
|
|
||||||
assert!(sent.payload.contains(alice_auto_submitted_hdr));
|
|
||||||
assert!(!sent.payload.contains("Alice Exampleorg"));
|
|
||||||
let msg = bob.parse_msg(&sent).await;
|
|
||||||
assert!(msg.was_encrypted());
|
|
||||||
assert_eq!(
|
|
||||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
||||||
"vc-auth-required"
|
|
||||||
);
|
|
||||||
let bob_chat = bob.create_chat(&alice).await;
|
|
||||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), false);
|
|
||||||
assert_eq!(
|
|
||||||
bob_chat.why_cant_send(&bob).await.unwrap(),
|
|
||||||
Some(CantSendReason::SecurejoinWait)
|
|
||||||
);
|
|
||||||
if case == SetupContactCase::SecurejoinWaitTimeout {
|
|
||||||
SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT));
|
|
||||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
|
|
||||||
bob.recv_msg_trash(&sent).await;
|
|
||||||
let bob_chat = bob.create_chat(&alice).await;
|
|
||||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
|
||||||
|
|
||||||
// Check Bob emitted the JoinerProgress event.
|
|
||||||
let event = bob
|
|
||||||
.evtracker
|
|
||||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
|
||||||
.await;
|
|
||||||
match event {
|
|
||||||
EventType::SecurejoinJoinerProgress {
|
|
||||||
contact_id,
|
|
||||||
progress,
|
|
||||||
} => {
|
|
||||||
let alice_contact_id =
|
|
||||||
Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
|
||||||
.await
|
|
||||||
.expect("Error looking up contact")
|
|
||||||
.expect("Contact not found");
|
|
||||||
assert_eq!(contact_id, alice_contact_id);
|
|
||||||
assert_eq!(progress, 400);
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Bob sent the right message.
|
|
||||||
let sent = bob.pop_sent_msg().await;
|
|
||||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
|
||||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
|
||||||
let mut msg = alice.parse_msg(&sent).await;
|
|
||||||
let vc_request_with_auth_ts_sent = msg
|
|
||||||
.get_header(HeaderDef::Date)
|
|
||||||
.and_then(|value| mailparse::dateparse(value).ok())
|
|
||||||
.unwrap();
|
|
||||||
assert!(msg.was_encrypted());
|
|
||||||
assert_eq!(
|
|
||||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
||||||
"vc-request-with-auth"
|
|
||||||
);
|
|
||||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
|
||||||
let bob_fp = load_self_public_key(&bob.ctx)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.dc_fingerprint();
|
|
||||||
assert_eq!(
|
|
||||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
|
||||||
bob_fp.hex()
|
|
||||||
);
|
|
||||||
|
|
||||||
if case == SetupContactCase::WrongAliceGossip {
|
|
||||||
let wrong_pubkey = load_self_public_key(&bob).await.unwrap();
|
|
||||||
let alice_pubkey = msg
|
|
||||||
.gossiped_keys
|
|
||||||
.insert(alice_addr.to_string(), wrong_pubkey)
|
|
||||||
.unwrap();
|
|
||||||
let contact_bob = alice.add_or_lookup_contact(&bob).await;
|
|
||||||
let handshake_msg = handle_securejoin_handshake(&alice, &msg, contact_bob.id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
|
||||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false);
|
|
||||||
|
|
||||||
msg.gossiped_keys
|
|
||||||
.insert(alice_addr.to_string(), alice_pubkey)
|
|
||||||
.unwrap();
|
|
||||||
let handshake_msg = handle_securejoin_handshake(&alice, &msg, contact_bob.id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
|
||||||
assert!(contact_bob.is_verified(&alice.ctx).await.unwrap());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alice should not yet have Bob verified
|
|
||||||
let contact_bob_id =
|
|
||||||
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
|
||||||
.await
|
|
||||||
.expect("Error looking up contact")
|
|
||||||
.expect("Contact not found");
|
|
||||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false);
|
|
||||||
assert_eq!(contact_bob.get_authname(), "");
|
|
||||||
|
|
||||||
if case == SetupContactCase::CheckProtectionTimestamp {
|
|
||||||
SystemTime::shift(Duration::from_secs(3600));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
|
|
||||||
alice.recv_msg_trash(&sent).await;
|
|
||||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true);
|
|
||||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
|
|
||||||
assert!(contact_bob.get_name().is_empty());
|
|
||||||
assert_eq!(contact_bob.is_bot(), false);
|
|
||||||
|
|
||||||
// exactly one one-to-one chat should be visible for both now
|
|
||||||
// (check this before calling alice.create_chat() explicitly below)
|
|
||||||
assert_eq!(
|
|
||||||
Chatlist::try_load(&alice, 0, None, None)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.len(),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check Alice got the verified message in her 1:1 chat.
|
|
||||||
{
|
|
||||||
let chat = alice.create_chat(&bob).await;
|
|
||||||
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
|
|
||||||
assert!(msg.is_info());
|
|
||||||
let expected_text = chat_protection_enabled(&alice).await;
|
|
||||||
assert_eq!(msg.get_text(), expected_text);
|
|
||||||
if case == SetupContactCase::CheckProtectionTimestamp {
|
|
||||||
assert_eq!(msg.timestamp_sort, vc_request_with_auth_ts_sent + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure Alice hasn't yet sent their name to Bob.
|
|
||||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
match case {
|
|
||||||
SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"),
|
|
||||||
_ => assert_eq!(contact_alice.get_authname(), ""),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check Alice sent the right message to Bob.
|
|
||||||
let sent = alice.pop_sent_msg().await;
|
|
||||||
assert!(sent.payload.contains(alice_auto_submitted_hdr));
|
|
||||||
assert!(!sent.payload.contains("Alice Exampleorg"));
|
|
||||||
let msg = bob.parse_msg(&sent).await;
|
|
||||||
assert!(msg.was_encrypted());
|
|
||||||
assert_eq!(
|
|
||||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
||||||
"vc-contact-confirm"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bob should not yet have Alice verified
|
|
||||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), false);
|
|
||||||
|
|
||||||
// Step 7: Bob receives vc-contact-confirm
|
|
||||||
bob.recv_msg_trash(&sent).await;
|
|
||||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true);
|
|
||||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(contact_alice.get_authname(), "Alice Exampleorg");
|
|
||||||
assert!(contact_alice.get_name().is_empty());
|
|
||||||
assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot);
|
|
||||||
|
|
||||||
if case != SetupContactCase::SecurejoinWaitTimeout {
|
|
||||||
// Later we check that the timeout message isn't added to the already protected chat.
|
|
||||||
SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT + 1));
|
|
||||||
assert_eq!(
|
|
||||||
bob_chat
|
|
||||||
.check_securejoin_wait(&bob, constants::SECUREJOIN_WAIT_TIMEOUT)
|
|
||||||
.await
|
|
||||||
.unwrap(),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Bob got expected info messages in his 1:1 chat.
|
|
||||||
let msg_cnt: usize = match case {
|
|
||||||
SetupContactCase::SecurejoinWaitTimeout => 3,
|
|
||||||
_ => 2,
|
|
||||||
};
|
|
||||||
let mut i = 0..msg_cnt;
|
|
||||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
|
||||||
assert!(msg.is_info());
|
|
||||||
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
|
|
||||||
if case == SetupContactCase::SecurejoinWaitTimeout {
|
|
||||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
|
||||||
assert!(msg.is_info());
|
|
||||||
assert_eq!(
|
|
||||||
msg.get_text(),
|
|
||||||
stock_str::securejoin_wait_timeout(&bob).await
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
|
||||||
assert!(msg.is_info());
|
|
||||||
assert_eq!(msg.get_text(), chat_protection_enabled(&bob).await);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_setup_contact_bad_qr() {
|
|
||||||
let bob = TestContext::new_bob().await;
|
|
||||||
let ret = join_securejoin(&bob.ctx, "not a qr code").await;
|
|
||||||
assert!(ret.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
|
||||||
let mut tcm = TestContextManager::new();
|
|
||||||
let alice = tcm.alice().await;
|
|
||||||
let bob = tcm.bob().await;
|
|
||||||
|
|
||||||
// Ensure Bob knows Alice_FP
|
|
||||||
let alice_pubkey = load_self_public_key(&alice.ctx).await?;
|
|
||||||
let peerstate = Peerstate {
|
|
||||||
addr: "alice@example.org".into(),
|
|
||||||
last_seen: 10,
|
|
||||||
last_seen_autocrypt: 10,
|
|
||||||
prefer_encrypt: EncryptPreference::Mutual,
|
|
||||||
public_key: Some(alice_pubkey.clone()),
|
|
||||||
public_key_fingerprint: Some(alice_pubkey.dc_fingerprint()),
|
|
||||||
gossip_key: Some(alice_pubkey.clone()),
|
|
||||||
gossip_timestamp: 10,
|
|
||||||
gossip_key_fingerprint: Some(alice_pubkey.dc_fingerprint()),
|
|
||||||
verified_key: None,
|
|
||||||
verified_key_fingerprint: None,
|
|
||||||
verifier: None,
|
|
||||||
secondary_verified_key: None,
|
|
||||||
secondary_verified_key_fingerprint: None,
|
|
||||||
secondary_verifier: None,
|
|
||||||
backward_verified_key_id: None,
|
|
||||||
fingerprint_changed: false,
|
|
||||||
};
|
|
||||||
peerstate.save_to_db(&bob.ctx.sql).await?;
|
|
||||||
|
|
||||||
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
|
||||||
let qr = get_securejoin_qr(&alice.ctx, None).await?;
|
|
||||||
|
|
||||||
// Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request
|
|
||||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
|
||||||
|
|
||||||
// Check Bob emitted the JoinerProgress event.
|
|
||||||
let event = bob
|
|
||||||
.evtracker
|
|
||||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
|
||||||
.await;
|
|
||||||
match event {
|
|
||||||
EventType::SecurejoinJoinerProgress {
|
|
||||||
contact_id,
|
|
||||||
progress,
|
|
||||||
} => {
|
|
||||||
let alice_contact_id =
|
|
||||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
|
||||||
.await
|
|
||||||
.expect("Error looking up contact")
|
|
||||||
.expect("Contact not found");
|
|
||||||
assert_eq!(contact_id, alice_contact_id);
|
|
||||||
assert_eq!(progress, 400);
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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());
|
|
||||||
assert_eq!(
|
|
||||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
||||||
"vc-request-with-auth"
|
|
||||||
);
|
|
||||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
|
||||||
let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint();
|
|
||||||
assert_eq!(
|
|
||||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
|
||||||
bob_fp.hex()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Alice should not yet have Bob verified
|
|
||||||
let (contact_bob_id, _modified) = Contact::add_or_lookup(
|
|
||||||
&alice.ctx,
|
|
||||||
"",
|
|
||||||
&ContactAddress::new("bob@example.net")?,
|
|
||||||
Origin::ManuallyCreated,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?;
|
|
||||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false);
|
|
||||||
|
|
||||||
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
|
|
||||||
alice.recv_msg_trash(&sent).await;
|
|
||||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true);
|
|
||||||
|
|
||||||
let sent = alice.pop_sent_msg().await;
|
|
||||||
let msg = bob.parse_msg(&sent).await;
|
|
||||||
assert!(msg.was_encrypted());
|
|
||||||
assert_eq!(
|
|
||||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
||||||
"vc-contact-confirm"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bob should not yet have Alice verified
|
|
||||||
let contact_alice_id =
|
|
||||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
|
||||||
.await
|
|
||||||
.expect("Error looking up contact")
|
|
||||||
.expect("Contact not found");
|
|
||||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
|
|
||||||
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
|
|
||||||
|
|
||||||
// Step 7: Bob receives vc-contact-confirm
|
|
||||||
bob.recv_msg_trash(&sent).await;
|
|
||||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_setup_contact_concurrent_calls() -> Result<()> {
|
|
||||||
let mut tcm = TestContextManager::new();
|
|
||||||
let alice = tcm.alice().await;
|
|
||||||
let bob = tcm.bob().await;
|
|
||||||
|
|
||||||
// do a scan that is not working as claire is never responding
|
|
||||||
let qr_stale = "OPENPGP4FPR:1234567890123456789012345678901234567890#a=claire%40foo.de&n=&i=12345678901&s=23456789012";
|
|
||||||
let claire_id = join_securejoin(&bob, qr_stale).await?;
|
|
||||||
let chat = Chat::load_from_db(&bob, claire_id).await?;
|
|
||||||
assert!(!claire_id.is_special());
|
|
||||||
assert_eq!(chat.typ, Chattype::Single);
|
|
||||||
assert!(bob.pop_sent_msg().await.payload().contains("claire@foo.de"));
|
|
||||||
|
|
||||||
// subsequent scans shall abort existing ones or run concurrently -
|
|
||||||
// but they must not fail as otherwise the whole qr scanning becomes unusable until restart.
|
|
||||||
let qr = get_securejoin_qr(&alice, None).await?;
|
|
||||||
let alice_id = join_securejoin(&bob, &qr).await?;
|
|
||||||
let chat = Chat::load_from_db(&bob, alice_id).await?;
|
|
||||||
assert!(!alice_id.is_special());
|
|
||||||
assert_eq!(chat.typ, Chattype::Single);
|
|
||||||
assert_ne!(claire_id, alice_id);
|
|
||||||
assert!(bob
|
|
||||||
.pop_sent_msg()
|
|
||||||
.await
|
|
||||||
.payload()
|
|
||||||
.contains("alice@example.org"));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_secure_join() -> Result<()> {
|
|
||||||
let mut tcm = TestContextManager::new();
|
|
||||||
let alice = tcm.alice().await;
|
|
||||||
let bob = tcm.bob().await;
|
|
||||||
|
|
||||||
// We start with empty chatlists.
|
|
||||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
|
||||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
|
||||||
|
|
||||||
let alice_chatid =
|
|
||||||
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
|
|
||||||
|
|
||||||
// Step 1: Generate QR-code, secure-join implied by chatid
|
|
||||||
let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Step 2: Bob scans QR-code, sends vg-request
|
|
||||||
let bob_chatid = join_securejoin(&bob.ctx, &qr).await?;
|
|
||||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
|
||||||
|
|
||||||
let sent = bob.pop_sent_msg().await;
|
|
||||||
assert_eq!(
|
|
||||||
sent.recipient(),
|
|
||||||
EmailAddress::new("alice@example.org").unwrap()
|
|
||||||
);
|
|
||||||
let msg = alice.parse_msg(&sent).await;
|
|
||||||
assert!(!msg.was_encrypted());
|
|
||||||
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
|
|
||||||
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
|
|
||||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
|
||||||
|
|
||||||
// Old Delta Chat core sent `Secure-Join-Group` header in `vg-request`,
|
|
||||||
// but it was only used by Alice in `vg-request-with-auth`.
|
|
||||||
// New Delta Chat versions do not use `Secure-Join-Group` header at all
|
|
||||||
// and it is deprecated.
|
|
||||||
// Now `Secure-Join-Group` header
|
|
||||||
// is only sent in `vg-request-with-auth` for compatibility.
|
|
||||||
assert!(msg.get_header(HeaderDef::SecureJoinGroup).is_none());
|
|
||||||
|
|
||||||
// Step 3: Alice receives vg-request, sends vg-auth-required
|
|
||||||
alice.recv_msg_trash(&sent).await;
|
|
||||||
|
|
||||||
let sent = alice.pop_sent_msg().await;
|
|
||||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
|
||||||
let msg = bob.parse_msg(&sent).await;
|
|
||||||
assert!(msg.was_encrypted());
|
|
||||||
assert_eq!(
|
|
||||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
||||||
"vg-auth-required"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 4: Bob receives vg-auth-required, sends vg-request-with-auth
|
|
||||||
bob.recv_msg_trash(&sent).await;
|
|
||||||
let sent = bob.pop_sent_msg().await;
|
|
||||||
|
|
||||||
// Check Bob emitted the JoinerProgress event.
|
|
||||||
let event = bob
|
|
||||||
.evtracker
|
|
||||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
|
||||||
.await;
|
|
||||||
match event {
|
|
||||||
EventType::SecurejoinJoinerProgress {
|
|
||||||
contact_id,
|
|
||||||
progress,
|
|
||||||
} => {
|
|
||||||
let alice_contact_id =
|
|
||||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
|
||||||
.await
|
|
||||||
.expect("Error looking up contact")
|
|
||||||
.expect("Contact not found");
|
|
||||||
assert_eq!(contact_id, alice_contact_id);
|
|
||||||
assert_eq!(progress, 400);
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Bob sent the right handshake message.
|
|
||||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
|
||||||
let msg = alice.parse_msg(&sent).await;
|
|
||||||
assert!(msg.was_encrypted());
|
|
||||||
assert_eq!(
|
|
||||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
||||||
"vg-request-with-auth"
|
|
||||||
);
|
|
||||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
|
||||||
let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint();
|
|
||||||
assert_eq!(
|
|
||||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
|
||||||
bob_fp.hex()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Alice should not yet have Bob verified
|
|
||||||
let contact_bob_id =
|
|
||||||
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
|
||||||
.await?
|
|
||||||
.expect("Contact not found");
|
|
||||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?;
|
|
||||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false);
|
|
||||||
|
|
||||||
// Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added
|
|
||||||
alice.recv_msg_trash(&sent).await;
|
|
||||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true);
|
|
||||||
|
|
||||||
let sent = alice.pop_sent_msg().await;
|
|
||||||
let msg = bob.parse_msg(&sent).await;
|
|
||||||
assert!(msg.was_encrypted());
|
|
||||||
assert_eq!(
|
|
||||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
|
||||||
"vg-member-added"
|
|
||||||
);
|
|
||||||
// Formally this message is auto-submitted, but as the member addition is a result of an
|
|
||||||
// explicit user action, the Auto-Submitted header shouldn't be present. Otherwise it would
|
|
||||||
// be strange to have it in "member-added" messages of verified groups only.
|
|
||||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
|
||||||
// This is a two-member group, but Alice must Autocrypt-gossip to her other devices.
|
|
||||||
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some());
|
|
||||||
|
|
||||||
{
|
|
||||||
// Now Alice's chat with Bob should still be hidden, the verified message should
|
|
||||||
// appear in the group chat.
|
|
||||||
|
|
||||||
let chat = alice.get_chat(&bob).await;
|
|
||||||
assert_eq!(
|
|
||||||
chat.blocked,
|
|
||||||
Blocked::Yes,
|
|
||||||
"Alice's 1:1 chat with Bob is not hidden"
|
|
||||||
);
|
|
||||||
// There should be 3 messages in the chat:
|
|
||||||
// - The ChatProtectionEnabled message
|
|
||||||
// - You added member bob@example.net
|
|
||||||
let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await;
|
|
||||||
assert!(msg.is_info());
|
|
||||||
let expected_text = chat_protection_enabled(&alice).await;
|
|
||||||
assert_eq!(msg.get_text(), expected_text);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bob should not yet have Alice verified
|
|
||||||
let contact_alice_id =
|
|
||||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
|
||||||
.await
|
|
||||||
.expect("Error looking up contact")
|
|
||||||
.expect("Contact not found");
|
|
||||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
|
|
||||||
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
|
|
||||||
|
|
||||||
// Step 7: Bob receives vg-member-added
|
|
||||||
bob.recv_msg(&sent).await;
|
|
||||||
{
|
|
||||||
// Bob has Alice verified, message shows up in the group chat.
|
|
||||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true);
|
|
||||||
let chat = bob.get_chat(&alice).await;
|
|
||||||
assert_eq!(
|
|
||||||
chat.blocked,
|
|
||||||
Blocked::Yes,
|
|
||||||
"Bob's 1:1 chat with Alice is not hidden"
|
|
||||||
);
|
|
||||||
for item in chat::get_chat_msgs(&bob.ctx, bob_chatid).await.unwrap() {
|
|
||||||
if let chat::ChatItem::Message { msg_id } = item {
|
|
||||||
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
|
|
||||||
let text = msg.get_text();
|
|
||||||
println!("msg {msg_id} text: {text}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
|
|
||||||
assert!(bob_chat.is_protected());
|
|
||||||
assert!(bob_chat.typ == Chattype::Group);
|
|
||||||
|
|
||||||
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
|
|
||||||
// The one-to-one chats are used internally for the hidden handshake messages,
|
|
||||||
// however, should not be visible in the UIs.
|
|
||||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
|
||||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
|
||||||
|
|
||||||
// If Bob then sends a direct message to alice, however, the one-to-one with Alice should appear.
|
|
||||||
let bobs_chat_with_alice = bob.create_chat(&alice).await;
|
|
||||||
let sent = bob.send_text(bobs_chat_with_alice.id, "Hello").await;
|
|
||||||
alice.recv_msg(&sent).await;
|
|
||||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 2);
|
|
||||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 2);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_adhoc_group_no_qr() -> Result<()> {
|
|
||||||
let alice = TestContext::new_alice().await;
|
|
||||||
|
|
||||||
let mime = br#"Subject: First thread
|
|
||||||
Message-ID: first@example.org
|
|
||||||
To: Alice <alice@example.org>, Bob <bob@example.net>
|
|
||||||
From: Claire <claire@example.org>
|
|
||||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
|
||||||
|
|
||||||
First thread."#;
|
|
||||||
|
|
||||||
receive_imf(&alice, mime, false).await?;
|
|
||||||
let msg = alice.get_last_msg().await;
|
|
||||||
let chat_id = msg.chat_id;
|
|
||||||
|
|
||||||
assert!(get_securejoin_qr(&alice, Some(chat_id)).await.is_err());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_unknown_sender() -> Result<()> {
|
|
||||||
let mut tcm = TestContextManager::new();
|
|
||||||
let alice = tcm.alice().await;
|
|
||||||
let bob = tcm.bob().await;
|
|
||||||
|
|
||||||
tcm.execute_securejoin(&alice, &bob).await;
|
|
||||||
|
|
||||||
let alice_chat_id = alice
|
|
||||||
.create_group_with_members(ProtectionStatus::Protected, "Group with Bob", &[&bob])
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let sent = alice.send_text(alice_chat_id, "Hi!").await;
|
|
||||||
let bob_chat_id = bob.recv_msg(&sent).await.chat_id;
|
|
||||||
|
|
||||||
let sent = bob.send_text(bob_chat_id, "Hi hi!").await;
|
|
||||||
|
|
||||||
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
|
||||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
|
||||||
alice.pop_sent_msg().await;
|
|
||||||
|
|
||||||
// The message from Bob is delivered late, Bob is already removed.
|
|
||||||
let msg = alice.recv_msg(&sent).await;
|
|
||||||
assert_eq!(msg.text, "Hi hi!");
|
|
||||||
assert_eq!(msg.error.unwrap(), "Unknown sender for this chat.");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tests that Bob gets Alice as verified
|
|
||||||
/// if `vc-contact-confirm` is lost but Alice then sends
|
|
||||||
/// a message to Bob in a verified 1:1 chat with a `Chat-Verified` header.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_lost_contact_confirm() {
|
|
||||||
let mut tcm = TestContextManager::new();
|
|
||||||
let alice = tcm.alice().await;
|
|
||||||
let bob = tcm.bob().await;
|
|
||||||
for t in [&alice, &bob] {
|
|
||||||
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
|
||||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
|
||||||
|
|
||||||
// vc-request
|
|
||||||
let sent = bob.pop_sent_msg().await;
|
|
||||||
alice.recv_msg_trash(&sent).await;
|
|
||||||
|
|
||||||
// vc-auth-required
|
|
||||||
let sent = alice.pop_sent_msg().await;
|
|
||||||
bob.recv_msg_trash(&sent).await;
|
|
||||||
|
|
||||||
// vc-request-with-auth
|
|
||||||
let sent = bob.pop_sent_msg().await;
|
|
||||||
alice.recv_msg_trash(&sent).await;
|
|
||||||
|
|
||||||
// Alice has Bob verified now.
|
|
||||||
let contact_bob_id =
|
|
||||||
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
|
||||||
.await
|
|
||||||
.expect("Error looking up contact")
|
|
||||||
.expect("Contact not found");
|
|
||||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true);
|
|
||||||
|
|
||||||
// Alice sends vc-contact-confirm, but it gets lost.
|
|
||||||
let _sent_vc_contact_confirm = alice.pop_sent_msg().await;
|
|
||||||
|
|
||||||
// Bob should not yet have Alice verified
|
|
||||||
let contact_alice_id =
|
|
||||||
Contact::lookup_id_by_addr(&bob, "alice@example.org", Origin::Unknown)
|
|
||||||
.await
|
|
||||||
.expect("Error looking up contact")
|
|
||||||
.expect("Contact not found");
|
|
||||||
let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap();
|
|
||||||
assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), false);
|
|
||||||
|
|
||||||
// Alice sends a text message to Bob.
|
|
||||||
let received_hello = tcm.send_recv(&alice, &bob, "Hello!").await;
|
|
||||||
let chat_id = received_hello.chat_id;
|
|
||||||
let chat = Chat::load_from_db(&bob, chat_id).await.unwrap();
|
|
||||||
assert_eq!(chat.is_protected(), true);
|
|
||||||
|
|
||||||
// Received text message in a verified 1:1 chat results in backward verification
|
|
||||||
// and Bob now marks alice as verified.
|
|
||||||
let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap();
|
|
||||||
assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An unencrypted message with already known Autocrypt key, but sent from another address,
|
|
||||||
/// means that it's rather a new contact sharing the same key than the existing one changed its
|
|
||||||
/// address, otherwise it would already have our key to encrypt.
|
|
||||||
///
|
|
||||||
/// This is a regression test for a bug where DC wrongly executed AEAP in this case.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_shared_bobs_key() -> Result<()> {
|
|
||||||
let mut tcm = TestContextManager::new();
|
|
||||||
let alice = &tcm.alice().await;
|
|
||||||
let bob = &tcm.bob().await;
|
|
||||||
let bob_addr = &bob.get_config(Config::Addr).await?.unwrap();
|
|
||||||
|
|
||||||
tcm.execute_securejoin(bob, alice).await;
|
|
||||||
|
|
||||||
let export_dir = tempfile::tempdir().unwrap();
|
|
||||||
imex(bob, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
|
|
||||||
let bob2 = &TestContext::new().await;
|
|
||||||
let bob2_addr = "bob2@example.net";
|
|
||||||
bob2.configure_addr(bob2_addr).await;
|
|
||||||
imex(bob2, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
|
|
||||||
|
|
||||||
tcm.execute_securejoin(bob2, alice).await;
|
|
||||||
|
|
||||||
let bob3 = &TestContext::new().await;
|
|
||||||
let bob3_addr = "bob3@example.net";
|
|
||||||
bob3.configure_addr(bob3_addr).await;
|
|
||||||
imex(bob3, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
|
|
||||||
tcm.send_recv(bob3, alice, "hi Alice!").await;
|
|
||||||
let msg = tcm.send_recv(alice, bob3, "hi Bob3!").await;
|
|
||||||
assert!(msg.get_showpadlock());
|
|
||||||
|
|
||||||
let mut bob_ids = HashSet::new();
|
|
||||||
bob_ids.insert(
|
|
||||||
Contact::lookup_id_by_addr(alice, bob_addr, Origin::Unknown)
|
|
||||||
.await?
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
bob_ids.insert(
|
|
||||||
Contact::lookup_id_by_addr(alice, bob2_addr, Origin::Unknown)
|
|
||||||
.await?
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
bob_ids.insert(
|
|
||||||
Contact::lookup_id_by_addr(alice, bob3_addr, Origin::Unknown)
|
|
||||||
.await?
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
assert_eq!(bob_ids.len(), 3);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
841
src/securejoin/securejoin_tests.rs
Normal file
841
src/securejoin/securejoin_tests.rs
Normal file
@@ -0,0 +1,841 @@
|
|||||||
|
use deltachat_contact_tools::{ContactAddress, EmailAddress};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::chat::{remove_contact_from_chat, CantSendReason};
|
||||||
|
use crate::chatlist::Chatlist;
|
||||||
|
use crate::constants::{self, Chattype};
|
||||||
|
use crate::imex::{imex, ImexMode};
|
||||||
|
use crate::receive_imf::receive_imf;
|
||||||
|
use crate::stock_str::{self, chat_protection_enabled};
|
||||||
|
use crate::test_utils::{get_chat_msg, TimeShiftFalsePositiveNote};
|
||||||
|
use crate::test_utils::{TestContext, TestContextManager};
|
||||||
|
use crate::tools::SystemTime;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
enum SetupContactCase {
|
||||||
|
Normal,
|
||||||
|
CheckProtectionTimestamp,
|
||||||
|
WrongAliceGossip,
|
||||||
|
SecurejoinWaitTimeout,
|
||||||
|
AliceIsBot,
|
||||||
|
AliceHasName,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_setup_contact() {
|
||||||
|
test_setup_contact_ex(SetupContactCase::Normal).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_setup_contact_protection_timestamp() {
|
||||||
|
test_setup_contact_ex(SetupContactCase::CheckProtectionTimestamp).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_setup_contact_wrong_alice_gossip() {
|
||||||
|
test_setup_contact_ex(SetupContactCase::WrongAliceGossip).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_setup_contact_wait_timeout() {
|
||||||
|
test_setup_contact_ex(SetupContactCase::SecurejoinWaitTimeout).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_setup_contact_alice_is_bot() {
|
||||||
|
test_setup_contact_ex(SetupContactCase::AliceIsBot).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_setup_contact_alice_has_name() {
|
||||||
|
test_setup_contact_ex(SetupContactCase::AliceHasName).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||||
|
let _n = TimeShiftFalsePositiveNote;
|
||||||
|
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = tcm.alice().await;
|
||||||
|
let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap();
|
||||||
|
if case == SetupContactCase::AliceHasName {
|
||||||
|
alice
|
||||||
|
.set_config(Config::Displayname, Some("Alice"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let bob = tcm.bob().await;
|
||||||
|
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let alice_auto_submitted_hdr;
|
||||||
|
match case {
|
||||||
|
SetupContactCase::AliceIsBot => {
|
||||||
|
alice.set_config_bool(Config::Bot, true).await.unwrap();
|
||||||
|
alice_auto_submitted_hdr = "Auto-Submitted: auto-generated";
|
||||||
|
}
|
||||||
|
_ => alice_auto_submitted_hdr = "Auto-Submitted: auto-replied",
|
||||||
|
};
|
||||||
|
for t in [&alice, &bob] {
|
||||||
|
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Chatlist::try_load(&alice, 0, None, None)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
||||||
|
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
||||||
|
// We want Bob to learn Alice's name from their messages, not from the QR code.
|
||||||
|
alice
|
||||||
|
.set_config(Config::Displayname, Some("Alice Exampleorg"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Step 2: Bob scans QR-code, sends vc-request
|
||||||
|
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
||||||
|
.await
|
||||||
|
.expect("Error looking up contact")
|
||||||
|
.expect("Contact not found");
|
||||||
|
let sent = bob.pop_sent_msg().await;
|
||||||
|
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||||
|
assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap());
|
||||||
|
let msg = alice.parse_msg(&sent).await;
|
||||||
|
assert!(!msg.was_encrypted());
|
||||||
|
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
|
||||||
|
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
|
||||||
|
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||||
|
|
||||||
|
// Step 3: Alice receives vc-request, sends vc-auth-required
|
||||||
|
alice.recv_msg_trash(&sent).await;
|
||||||
|
assert_eq!(
|
||||||
|
Chatlist::try_load(&alice, 0, None, None)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
assert!(sent.payload.contains(alice_auto_submitted_hdr));
|
||||||
|
assert!(!sent.payload.contains("Alice Exampleorg"));
|
||||||
|
let msg = bob.parse_msg(&sent).await;
|
||||||
|
assert!(msg.was_encrypted());
|
||||||
|
assert_eq!(
|
||||||
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||||
|
"vc-auth-required"
|
||||||
|
);
|
||||||
|
let bob_chat = bob.create_chat(&alice).await;
|
||||||
|
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), false);
|
||||||
|
assert_eq!(
|
||||||
|
bob_chat.why_cant_send(&bob).await.unwrap(),
|
||||||
|
Some(CantSendReason::SecurejoinWait)
|
||||||
|
);
|
||||||
|
if case == SetupContactCase::SecurejoinWaitTimeout {
|
||||||
|
SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT));
|
||||||
|
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
|
||||||
|
bob.recv_msg_trash(&sent).await;
|
||||||
|
let bob_chat = bob.create_chat(&alice).await;
|
||||||
|
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||||
|
|
||||||
|
// Check Bob emitted the JoinerProgress event.
|
||||||
|
let event = bob
|
||||||
|
.evtracker
|
||||||
|
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||||
|
.await;
|
||||||
|
match event {
|
||||||
|
EventType::SecurejoinJoinerProgress {
|
||||||
|
contact_id,
|
||||||
|
progress,
|
||||||
|
} => {
|
||||||
|
let alice_contact_id =
|
||||||
|
Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
||||||
|
.await
|
||||||
|
.expect("Error looking up contact")
|
||||||
|
.expect("Contact not found");
|
||||||
|
assert_eq!(contact_id, alice_contact_id);
|
||||||
|
assert_eq!(progress, 400);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Bob sent the right message.
|
||||||
|
let sent = bob.pop_sent_msg().await;
|
||||||
|
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||||
|
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||||
|
let mut msg = alice.parse_msg(&sent).await;
|
||||||
|
let vc_request_with_auth_ts_sent = msg
|
||||||
|
.get_header(HeaderDef::Date)
|
||||||
|
.and_then(|value| mailparse::dateparse(value).ok())
|
||||||
|
.unwrap();
|
||||||
|
assert!(msg.was_encrypted());
|
||||||
|
assert_eq!(
|
||||||
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||||
|
"vc-request-with-auth"
|
||||||
|
);
|
||||||
|
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||||
|
let bob_fp = load_self_public_key(&bob.ctx)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.dc_fingerprint();
|
||||||
|
assert_eq!(
|
||||||
|
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||||
|
bob_fp.hex()
|
||||||
|
);
|
||||||
|
|
||||||
|
if case == SetupContactCase::WrongAliceGossip {
|
||||||
|
let wrong_pubkey = load_self_public_key(&bob).await.unwrap();
|
||||||
|
let alice_pubkey = msg
|
||||||
|
.gossiped_keys
|
||||||
|
.insert(alice_addr.to_string(), wrong_pubkey)
|
||||||
|
.unwrap();
|
||||||
|
let contact_bob = alice.add_or_lookup_contact(&bob).await;
|
||||||
|
let handshake_msg = handle_securejoin_handshake(&alice, &msg, contact_bob.id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
||||||
|
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false);
|
||||||
|
|
||||||
|
msg.gossiped_keys
|
||||||
|
.insert(alice_addr.to_string(), alice_pubkey)
|
||||||
|
.unwrap();
|
||||||
|
let handshake_msg = handle_securejoin_handshake(&alice, &msg, contact_bob.id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
||||||
|
assert!(contact_bob.is_verified(&alice.ctx).await.unwrap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice should not yet have Bob verified
|
||||||
|
let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
||||||
|
.await
|
||||||
|
.expect("Error looking up contact")
|
||||||
|
.expect("Contact not found");
|
||||||
|
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false);
|
||||||
|
assert_eq!(contact_bob.get_authname(), "");
|
||||||
|
|
||||||
|
if case == SetupContactCase::CheckProtectionTimestamp {
|
||||||
|
SystemTime::shift(Duration::from_secs(3600));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
|
||||||
|
alice.recv_msg_trash(&sent).await;
|
||||||
|
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true);
|
||||||
|
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
|
||||||
|
assert!(contact_bob.get_name().is_empty());
|
||||||
|
assert_eq!(contact_bob.is_bot(), false);
|
||||||
|
|
||||||
|
// exactly one one-to-one chat should be visible for both now
|
||||||
|
// (check this before calling alice.create_chat() explicitly below)
|
||||||
|
assert_eq!(
|
||||||
|
Chatlist::try_load(&alice, 0, None, None)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check Alice got the verified message in her 1:1 chat.
|
||||||
|
{
|
||||||
|
let chat = alice.create_chat(&bob).await;
|
||||||
|
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
|
||||||
|
assert!(msg.is_info());
|
||||||
|
let expected_text = chat_protection_enabled(&alice).await;
|
||||||
|
assert_eq!(msg.get_text(), expected_text);
|
||||||
|
if case == SetupContactCase::CheckProtectionTimestamp {
|
||||||
|
assert_eq!(msg.timestamp_sort, vc_request_with_auth_ts_sent + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure Alice hasn't yet sent their name to Bob.
|
||||||
|
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
match case {
|
||||||
|
SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"),
|
||||||
|
_ => assert_eq!(contact_alice.get_authname(), ""),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check Alice sent the right message to Bob.
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
assert!(sent.payload.contains(alice_auto_submitted_hdr));
|
||||||
|
assert!(!sent.payload.contains("Alice Exampleorg"));
|
||||||
|
let msg = bob.parse_msg(&sent).await;
|
||||||
|
assert!(msg.was_encrypted());
|
||||||
|
assert_eq!(
|
||||||
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||||
|
"vc-contact-confirm"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bob should not yet have Alice verified
|
||||||
|
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), false);
|
||||||
|
|
||||||
|
// Step 7: Bob receives vc-contact-confirm
|
||||||
|
bob.recv_msg_trash(&sent).await;
|
||||||
|
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true);
|
||||||
|
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(contact_alice.get_authname(), "Alice Exampleorg");
|
||||||
|
assert!(contact_alice.get_name().is_empty());
|
||||||
|
assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot);
|
||||||
|
|
||||||
|
if case != SetupContactCase::SecurejoinWaitTimeout {
|
||||||
|
// Later we check that the timeout message isn't added to the already protected chat.
|
||||||
|
SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT + 1));
|
||||||
|
assert_eq!(
|
||||||
|
bob_chat
|
||||||
|
.check_securejoin_wait(&bob, constants::SECUREJOIN_WAIT_TIMEOUT)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Bob got expected info messages in his 1:1 chat.
|
||||||
|
let msg_cnt: usize = match case {
|
||||||
|
SetupContactCase::SecurejoinWaitTimeout => 3,
|
||||||
|
_ => 2,
|
||||||
|
};
|
||||||
|
let mut i = 0..msg_cnt;
|
||||||
|
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||||
|
assert!(msg.is_info());
|
||||||
|
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
|
||||||
|
if case == SetupContactCase::SecurejoinWaitTimeout {
|
||||||
|
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||||
|
assert!(msg.is_info());
|
||||||
|
assert_eq!(
|
||||||
|
msg.get_text(),
|
||||||
|
stock_str::securejoin_wait_timeout(&bob).await
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||||
|
assert!(msg.is_info());
|
||||||
|
assert_eq!(msg.get_text(), chat_protection_enabled(&bob).await);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_setup_contact_bad_qr() {
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
let ret = join_securejoin(&bob.ctx, "not a qr code").await;
|
||||||
|
assert!(ret.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = tcm.alice().await;
|
||||||
|
let bob = tcm.bob().await;
|
||||||
|
|
||||||
|
// Ensure Bob knows Alice_FP
|
||||||
|
let alice_pubkey = load_self_public_key(&alice.ctx).await?;
|
||||||
|
let peerstate = Peerstate {
|
||||||
|
addr: "alice@example.org".into(),
|
||||||
|
last_seen: 10,
|
||||||
|
last_seen_autocrypt: 10,
|
||||||
|
prefer_encrypt: EncryptPreference::Mutual,
|
||||||
|
public_key: Some(alice_pubkey.clone()),
|
||||||
|
public_key_fingerprint: Some(alice_pubkey.dc_fingerprint()),
|
||||||
|
gossip_key: Some(alice_pubkey.clone()),
|
||||||
|
gossip_timestamp: 10,
|
||||||
|
gossip_key_fingerprint: Some(alice_pubkey.dc_fingerprint()),
|
||||||
|
verified_key: None,
|
||||||
|
verified_key_fingerprint: None,
|
||||||
|
verifier: None,
|
||||||
|
secondary_verified_key: None,
|
||||||
|
secondary_verified_key_fingerprint: None,
|
||||||
|
secondary_verifier: None,
|
||||||
|
backward_verified_key_id: None,
|
||||||
|
fingerprint_changed: false,
|
||||||
|
};
|
||||||
|
peerstate.save_to_db(&bob.ctx.sql).await?;
|
||||||
|
|
||||||
|
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
||||||
|
let qr = get_securejoin_qr(&alice.ctx, None).await?;
|
||||||
|
|
||||||
|
// Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request
|
||||||
|
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||||
|
|
||||||
|
// Check Bob emitted the JoinerProgress event.
|
||||||
|
let event = bob
|
||||||
|
.evtracker
|
||||||
|
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||||
|
.await;
|
||||||
|
match event {
|
||||||
|
EventType::SecurejoinJoinerProgress {
|
||||||
|
contact_id,
|
||||||
|
progress,
|
||||||
|
} => {
|
||||||
|
let alice_contact_id =
|
||||||
|
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||||
|
.await
|
||||||
|
.expect("Error looking up contact")
|
||||||
|
.expect("Contact not found");
|
||||||
|
assert_eq!(contact_id, alice_contact_id);
|
||||||
|
assert_eq!(progress, 400);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
assert_eq!(
|
||||||
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||||
|
"vc-request-with-auth"
|
||||||
|
);
|
||||||
|
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||||
|
let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint();
|
||||||
|
assert_eq!(
|
||||||
|
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||||
|
bob_fp.hex()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Alice should not yet have Bob verified
|
||||||
|
let (contact_bob_id, _modified) = Contact::add_or_lookup(
|
||||||
|
&alice.ctx,
|
||||||
|
"",
|
||||||
|
&ContactAddress::new("bob@example.net")?,
|
||||||
|
Origin::ManuallyCreated,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?;
|
||||||
|
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false);
|
||||||
|
|
||||||
|
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
|
||||||
|
alice.recv_msg_trash(&sent).await;
|
||||||
|
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true);
|
||||||
|
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
let msg = bob.parse_msg(&sent).await;
|
||||||
|
assert!(msg.was_encrypted());
|
||||||
|
assert_eq!(
|
||||||
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||||
|
"vc-contact-confirm"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bob should not yet have Alice verified
|
||||||
|
let contact_alice_id =
|
||||||
|
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||||
|
.await
|
||||||
|
.expect("Error looking up contact")
|
||||||
|
.expect("Contact not found");
|
||||||
|
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
|
||||||
|
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
|
||||||
|
|
||||||
|
// Step 7: Bob receives vc-contact-confirm
|
||||||
|
bob.recv_msg_trash(&sent).await;
|
||||||
|
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_setup_contact_concurrent_calls() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = tcm.alice().await;
|
||||||
|
let bob = tcm.bob().await;
|
||||||
|
|
||||||
|
// do a scan that is not working as claire is never responding
|
||||||
|
let qr_stale = "OPENPGP4FPR:1234567890123456789012345678901234567890#a=claire%40foo.de&n=&i=12345678901&s=23456789012";
|
||||||
|
let claire_id = join_securejoin(&bob, qr_stale).await?;
|
||||||
|
let chat = Chat::load_from_db(&bob, claire_id).await?;
|
||||||
|
assert!(!claire_id.is_special());
|
||||||
|
assert_eq!(chat.typ, Chattype::Single);
|
||||||
|
assert!(bob.pop_sent_msg().await.payload().contains("claire@foo.de"));
|
||||||
|
|
||||||
|
// subsequent scans shall abort existing ones or run concurrently -
|
||||||
|
// but they must not fail as otherwise the whole qr scanning becomes unusable until restart.
|
||||||
|
let qr = get_securejoin_qr(&alice, None).await?;
|
||||||
|
let alice_id = join_securejoin(&bob, &qr).await?;
|
||||||
|
let chat = Chat::load_from_db(&bob, alice_id).await?;
|
||||||
|
assert!(!alice_id.is_special());
|
||||||
|
assert_eq!(chat.typ, Chattype::Single);
|
||||||
|
assert_ne!(claire_id, alice_id);
|
||||||
|
assert!(bob
|
||||||
|
.pop_sent_msg()
|
||||||
|
.await
|
||||||
|
.payload()
|
||||||
|
.contains("alice@example.org"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_secure_join() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = tcm.alice().await;
|
||||||
|
let bob = tcm.bob().await;
|
||||||
|
|
||||||
|
// We start with empty chatlists.
|
||||||
|
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
||||||
|
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||||
|
|
||||||
|
let alice_chatid =
|
||||||
|
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
|
||||||
|
|
||||||
|
// Step 1: Generate QR-code, secure-join implied by chatid
|
||||||
|
let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Step 2: Bob scans QR-code, sends vg-request
|
||||||
|
let bob_chatid = join_securejoin(&bob.ctx, &qr).await?;
|
||||||
|
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||||
|
|
||||||
|
let sent = bob.pop_sent_msg().await;
|
||||||
|
assert_eq!(
|
||||||
|
sent.recipient(),
|
||||||
|
EmailAddress::new("alice@example.org").unwrap()
|
||||||
|
);
|
||||||
|
let msg = alice.parse_msg(&sent).await;
|
||||||
|
assert!(!msg.was_encrypted());
|
||||||
|
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
|
||||||
|
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
|
||||||
|
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||||
|
|
||||||
|
// Old Delta Chat core sent `Secure-Join-Group` header in `vg-request`,
|
||||||
|
// but it was only used by Alice in `vg-request-with-auth`.
|
||||||
|
// New Delta Chat versions do not use `Secure-Join-Group` header at all
|
||||||
|
// and it is deprecated.
|
||||||
|
// Now `Secure-Join-Group` header
|
||||||
|
// is only sent in `vg-request-with-auth` for compatibility.
|
||||||
|
assert!(msg.get_header(HeaderDef::SecureJoinGroup).is_none());
|
||||||
|
|
||||||
|
// Step 3: Alice receives vg-request, sends vg-auth-required
|
||||||
|
alice.recv_msg_trash(&sent).await;
|
||||||
|
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||||
|
let msg = bob.parse_msg(&sent).await;
|
||||||
|
assert!(msg.was_encrypted());
|
||||||
|
assert_eq!(
|
||||||
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||||
|
"vg-auth-required"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 4: Bob receives vg-auth-required, sends vg-request-with-auth
|
||||||
|
bob.recv_msg_trash(&sent).await;
|
||||||
|
let sent = bob.pop_sent_msg().await;
|
||||||
|
|
||||||
|
// Check Bob emitted the JoinerProgress event.
|
||||||
|
let event = bob
|
||||||
|
.evtracker
|
||||||
|
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||||
|
.await;
|
||||||
|
match event {
|
||||||
|
EventType::SecurejoinJoinerProgress {
|
||||||
|
contact_id,
|
||||||
|
progress,
|
||||||
|
} => {
|
||||||
|
let alice_contact_id =
|
||||||
|
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||||
|
.await
|
||||||
|
.expect("Error looking up contact")
|
||||||
|
.expect("Contact not found");
|
||||||
|
assert_eq!(contact_id, alice_contact_id);
|
||||||
|
assert_eq!(progress, 400);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Bob sent the right handshake message.
|
||||||
|
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||||
|
let msg = alice.parse_msg(&sent).await;
|
||||||
|
assert!(msg.was_encrypted());
|
||||||
|
assert_eq!(
|
||||||
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||||
|
"vg-request-with-auth"
|
||||||
|
);
|
||||||
|
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||||
|
let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint();
|
||||||
|
assert_eq!(
|
||||||
|
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||||
|
bob_fp.hex()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Alice should not yet have Bob verified
|
||||||
|
let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
||||||
|
.await?
|
||||||
|
.expect("Contact not found");
|
||||||
|
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?;
|
||||||
|
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false);
|
||||||
|
|
||||||
|
// Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added
|
||||||
|
alice.recv_msg_trash(&sent).await;
|
||||||
|
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true);
|
||||||
|
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
let msg = bob.parse_msg(&sent).await;
|
||||||
|
assert!(msg.was_encrypted());
|
||||||
|
assert_eq!(
|
||||||
|
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||||
|
"vg-member-added"
|
||||||
|
);
|
||||||
|
// Formally this message is auto-submitted, but as the member addition is a result of an
|
||||||
|
// explicit user action, the Auto-Submitted header shouldn't be present. Otherwise it would
|
||||||
|
// be strange to have it in "member-added" messages of verified groups only.
|
||||||
|
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||||
|
// This is a two-member group, but Alice must Autocrypt-gossip to her other devices.
|
||||||
|
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some());
|
||||||
|
|
||||||
|
{
|
||||||
|
// Now Alice's chat with Bob should still be hidden, the verified message should
|
||||||
|
// appear in the group chat.
|
||||||
|
|
||||||
|
let chat = alice.get_chat(&bob).await;
|
||||||
|
assert_eq!(
|
||||||
|
chat.blocked,
|
||||||
|
Blocked::Yes,
|
||||||
|
"Alice's 1:1 chat with Bob is not hidden"
|
||||||
|
);
|
||||||
|
// There should be 3 messages in the chat:
|
||||||
|
// - The ChatProtectionEnabled message
|
||||||
|
// - You added member bob@example.net
|
||||||
|
let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await;
|
||||||
|
assert!(msg.is_info());
|
||||||
|
let expected_text = chat_protection_enabled(&alice).await;
|
||||||
|
assert_eq!(msg.get_text(), expected_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob should not yet have Alice verified
|
||||||
|
let contact_alice_id =
|
||||||
|
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||||
|
.await
|
||||||
|
.expect("Error looking up contact")
|
||||||
|
.expect("Contact not found");
|
||||||
|
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
|
||||||
|
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
|
||||||
|
|
||||||
|
// Step 7: Bob receives vg-member-added
|
||||||
|
bob.recv_msg(&sent).await;
|
||||||
|
{
|
||||||
|
// Bob has Alice verified, message shows up in the group chat.
|
||||||
|
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true);
|
||||||
|
let chat = bob.get_chat(&alice).await;
|
||||||
|
assert_eq!(
|
||||||
|
chat.blocked,
|
||||||
|
Blocked::Yes,
|
||||||
|
"Bob's 1:1 chat with Alice is not hidden"
|
||||||
|
);
|
||||||
|
for item in chat::get_chat_msgs(&bob.ctx, bob_chatid).await.unwrap() {
|
||||||
|
if let chat::ChatItem::Message { msg_id } = item {
|
||||||
|
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
|
||||||
|
let text = msg.get_text();
|
||||||
|
println!("msg {msg_id} text: {text}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
|
||||||
|
assert!(bob_chat.is_protected());
|
||||||
|
assert!(bob_chat.typ == Chattype::Group);
|
||||||
|
|
||||||
|
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
|
||||||
|
// The one-to-one chats are used internally for the hidden handshake messages,
|
||||||
|
// however, should not be visible in the UIs.
|
||||||
|
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
||||||
|
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||||
|
|
||||||
|
// If Bob then sends a direct message to alice, however, the one-to-one with Alice should appear.
|
||||||
|
let bobs_chat_with_alice = bob.create_chat(&alice).await;
|
||||||
|
let sent = bob.send_text(bobs_chat_with_alice.id, "Hello").await;
|
||||||
|
alice.recv_msg(&sent).await;
|
||||||
|
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 2);
|
||||||
|
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 2);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_adhoc_group_no_qr() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
|
||||||
|
let mime = br#"Subject: First thread
|
||||||
|
Message-ID: first@example.org
|
||||||
|
To: Alice <alice@example.org>, Bob <bob@example.net>
|
||||||
|
From: Claire <claire@example.org>
|
||||||
|
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||||
|
|
||||||
|
First thread."#;
|
||||||
|
|
||||||
|
receive_imf(&alice, mime, false).await?;
|
||||||
|
let msg = alice.get_last_msg().await;
|
||||||
|
let chat_id = msg.chat_id;
|
||||||
|
|
||||||
|
assert!(get_securejoin_qr(&alice, Some(chat_id)).await.is_err());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_unknown_sender() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = tcm.alice().await;
|
||||||
|
let bob = tcm.bob().await;
|
||||||
|
|
||||||
|
tcm.execute_securejoin(&alice, &bob).await;
|
||||||
|
|
||||||
|
let alice_chat_id = alice
|
||||||
|
.create_group_with_members(ProtectionStatus::Protected, "Group with Bob", &[&bob])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let sent = alice.send_text(alice_chat_id, "Hi!").await;
|
||||||
|
let bob_chat_id = bob.recv_msg(&sent).await.chat_id;
|
||||||
|
|
||||||
|
let sent = bob.send_text(bob_chat_id, "Hi hi!").await;
|
||||||
|
|
||||||
|
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||||
|
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||||
|
alice.pop_sent_msg().await;
|
||||||
|
|
||||||
|
// The message from Bob is delivered late, Bob is already removed.
|
||||||
|
let msg = alice.recv_msg(&sent).await;
|
||||||
|
assert_eq!(msg.text, "Hi hi!");
|
||||||
|
assert_eq!(msg.error.unwrap(), "Unknown sender for this chat.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that Bob gets Alice as verified
|
||||||
|
/// if `vc-contact-confirm` is lost but Alice then sends
|
||||||
|
/// a message to Bob in a verified 1:1 chat with a `Chat-Verified` header.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_lost_contact_confirm() {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = tcm.alice().await;
|
||||||
|
let bob = tcm.bob().await;
|
||||||
|
for t in [&alice, &bob] {
|
||||||
|
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
||||||
|
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||||
|
|
||||||
|
// vc-request
|
||||||
|
let sent = bob.pop_sent_msg().await;
|
||||||
|
alice.recv_msg_trash(&sent).await;
|
||||||
|
|
||||||
|
// vc-auth-required
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
bob.recv_msg_trash(&sent).await;
|
||||||
|
|
||||||
|
// vc-request-with-auth
|
||||||
|
let sent = bob.pop_sent_msg().await;
|
||||||
|
alice.recv_msg_trash(&sent).await;
|
||||||
|
|
||||||
|
// Alice has Bob verified now.
|
||||||
|
let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
||||||
|
.await
|
||||||
|
.expect("Error looking up contact")
|
||||||
|
.expect("Contact not found");
|
||||||
|
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true);
|
||||||
|
|
||||||
|
// Alice sends vc-contact-confirm, but it gets lost.
|
||||||
|
let _sent_vc_contact_confirm = alice.pop_sent_msg().await;
|
||||||
|
|
||||||
|
// Bob should not yet have Alice verified
|
||||||
|
let contact_alice_id = Contact::lookup_id_by_addr(&bob, "alice@example.org", Origin::Unknown)
|
||||||
|
.await
|
||||||
|
.expect("Error looking up contact")
|
||||||
|
.expect("Contact not found");
|
||||||
|
let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap();
|
||||||
|
assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), false);
|
||||||
|
|
||||||
|
// Alice sends a text message to Bob.
|
||||||
|
let received_hello = tcm.send_recv(&alice, &bob, "Hello!").await;
|
||||||
|
let chat_id = received_hello.chat_id;
|
||||||
|
let chat = Chat::load_from_db(&bob, chat_id).await.unwrap();
|
||||||
|
assert_eq!(chat.is_protected(), true);
|
||||||
|
|
||||||
|
// Received text message in a verified 1:1 chat results in backward verification
|
||||||
|
// and Bob now marks alice as verified.
|
||||||
|
let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap();
|
||||||
|
assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An unencrypted message with already known Autocrypt key, but sent from another address,
|
||||||
|
/// means that it's rather a new contact sharing the same key than the existing one changed its
|
||||||
|
/// address, otherwise it would already have our key to encrypt.
|
||||||
|
///
|
||||||
|
/// This is a regression test for a bug where DC wrongly executed AEAP in this case.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_shared_bobs_key() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
let bob = &tcm.bob().await;
|
||||||
|
let bob_addr = &bob.get_config(Config::Addr).await?.unwrap();
|
||||||
|
|
||||||
|
tcm.execute_securejoin(bob, alice).await;
|
||||||
|
|
||||||
|
let export_dir = tempfile::tempdir().unwrap();
|
||||||
|
imex(bob, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
|
||||||
|
let bob2 = &TestContext::new().await;
|
||||||
|
let bob2_addr = "bob2@example.net";
|
||||||
|
bob2.configure_addr(bob2_addr).await;
|
||||||
|
imex(bob2, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
|
||||||
|
|
||||||
|
tcm.execute_securejoin(bob2, alice).await;
|
||||||
|
|
||||||
|
let bob3 = &TestContext::new().await;
|
||||||
|
let bob3_addr = "bob3@example.net";
|
||||||
|
bob3.configure_addr(bob3_addr).await;
|
||||||
|
imex(bob3, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
|
||||||
|
tcm.send_recv(bob3, alice, "hi Alice!").await;
|
||||||
|
let msg = tcm.send_recv(alice, bob3, "hi Bob3!").await;
|
||||||
|
assert!(msg.get_showpadlock());
|
||||||
|
|
||||||
|
let mut bob_ids = HashSet::new();
|
||||||
|
bob_ids.insert(
|
||||||
|
Contact::lookup_id_by_addr(alice, bob_addr, Origin::Unknown)
|
||||||
|
.await?
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
bob_ids.insert(
|
||||||
|
Contact::lookup_id_by_addr(alice, bob2_addr, Origin::Unknown)
|
||||||
|
.await?
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
bob_ids.insert(
|
||||||
|
Contact::lookup_id_by_addr(alice, bob3_addr, Origin::Unknown)
|
||||||
|
.await?
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
assert_eq!(bob_ids.len(), 3);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
375
src/sql.rs
375
src/sql.rs
@@ -1044,377 +1044,4 @@ async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod sql_tests;
|
||||||
use super::*;
|
|
||||||
use crate::{test_utils::TestContext, EventType};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_maybe_add_file() {
|
|
||||||
let mut files = Default::default();
|
|
||||||
maybe_add_file(&mut files, "$BLOBDIR/hello");
|
|
||||||
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
|
|
||||||
maybe_add_file(&mut files, "world2.txt");
|
|
||||||
maybe_add_file(&mut files, "$BLOBDIR");
|
|
||||||
|
|
||||||
assert!(files.contains("hello"));
|
|
||||||
assert!(files.contains("world.txt"));
|
|
||||||
assert!(!files.contains("world2.txt"));
|
|
||||||
assert!(!files.contains("$BLOBDIR"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_is_file_in_use() {
|
|
||||||
let mut files = Default::default();
|
|
||||||
maybe_add_file(&mut files, "$BLOBDIR/hello");
|
|
||||||
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
|
|
||||||
maybe_add_file(&mut files, "world2.txt");
|
|
||||||
|
|
||||||
assert!(is_file_in_use(&files, None, "hello"));
|
|
||||||
assert!(!is_file_in_use(&files, Some(".txt"), "hello"));
|
|
||||||
assert!(is_file_in_use(&files, Some("-suffix"), "world.txt-suffix"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_table_exists() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
assert!(t.ctx.sql.table_exists("msgs").await.unwrap());
|
|
||||||
assert!(!t.ctx.sql.table_exists("foobar").await.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_col_exists() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
assert!(t.ctx.sql.col_exists("msgs", "mime_modified").await.unwrap());
|
|
||||||
assert!(!t.ctx.sql.col_exists("msgs", "foobar").await.unwrap());
|
|
||||||
assert!(!t.ctx.sql.col_exists("foobar", "foobar").await.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tests that auto_vacuum is enabled for new databases.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_auto_vacuum() -> Result<()> {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
|
|
||||||
let query_only = true;
|
|
||||||
let auto_vacuum = t
|
|
||||||
.sql
|
|
||||||
.call(query_only, |conn| {
|
|
||||||
let auto_vacuum = conn.pragma_query_value(None, "auto_vacuum", |row| {
|
|
||||||
let auto_vacuum: i32 = row.get(0)?;
|
|
||||||
Ok(auto_vacuum)
|
|
||||||
})?;
|
|
||||||
Ok(auto_vacuum)
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// auto_vacuum=2 is the same as auto_vacuum=INCREMENTAL
|
|
||||||
assert_eq!(auto_vacuum, 2);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_housekeeping_db_closed() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
|
|
||||||
let avatar_src = t.dir.path().join("avatar.png");
|
|
||||||
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
|
||||||
tokio::fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
|
||||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let event_source = t.get_event_emitter();
|
|
||||||
|
|
||||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
|
||||||
assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]);
|
|
||||||
|
|
||||||
t.sql.close().await;
|
|
||||||
housekeeping(&t).await.unwrap(); // housekeeping should emit warnings but not fail
|
|
||||||
t.sql.open(&t, "".to_string()).await.unwrap();
|
|
||||||
|
|
||||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
|
||||||
assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]);
|
|
||||||
|
|
||||||
while let Ok(event) = event_source.try_recv() {
|
|
||||||
match event.typ {
|
|
||||||
EventType::Info(s) => assert!(
|
|
||||||
!s.contains("Keeping new unreferenced file"),
|
|
||||||
"File {s} was almost deleted, only reason it was kept is that it was created recently (as the tests don't run for a long time)"
|
|
||||||
),
|
|
||||||
EventType::Error(s) => panic!("{}", s),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Regression test for a bug where housekeeping deleted drafts since their
|
|
||||||
/// `hidden` flag is set.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_housekeeping_dont_delete_drafts() {
|
|
||||||
let t = TestContext::new_alice().await;
|
|
||||||
|
|
||||||
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
|
|
||||||
let mut new_draft = Message::new_text("This is my draft".to_string());
|
|
||||||
chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap();
|
|
||||||
|
|
||||||
housekeeping(&t).await.unwrap();
|
|
||||||
|
|
||||||
let loaded_draft = chat.id.get_draft(&t).await.unwrap();
|
|
||||||
assert_eq!(loaded_draft.unwrap().text, "This is my draft");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tests that `housekeeping` deletes the blobs backup dir which is created normally by
|
|
||||||
/// `imex::import_backup`.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_housekeeping_delete_blobs_backup_dir() {
|
|
||||||
let t = TestContext::new_alice().await;
|
|
||||||
let dir = t.get_blobdir().join(BLOBS_BACKUP_NAME);
|
|
||||||
tokio::fs::create_dir(&dir).await.unwrap();
|
|
||||||
tokio::fs::write(dir.join("f"), "").await.unwrap();
|
|
||||||
housekeeping(&t).await.unwrap();
|
|
||||||
tokio::fs::create_dir(&dir).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Regression test.
|
|
||||||
///
|
|
||||||
/// Previously the code checking for existence of `config` table
|
|
||||||
/// checked it with `PRAGMA table_info("config")` but did not
|
|
||||||
/// drain `SqlitePool.fetch` result, only using the first row
|
|
||||||
/// returned. As a result, prepared statement for `PRAGMA` was not
|
|
||||||
/// finalized early enough, leaving reader connection in a broken
|
|
||||||
/// state after reopening the database, when `config` table
|
|
||||||
/// existed and `PRAGMA` returned non-empty result.
|
|
||||||
///
|
|
||||||
/// Statements were not finalized due to a bug in sqlx:
|
|
||||||
/// <https://github.com/launchbadge/sqlx/issues/1147>
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_db_reopen() -> Result<()> {
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
// The context is used only for logging.
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
|
|
||||||
// Create a separate empty database for testing.
|
|
||||||
let dir = tempdir()?;
|
|
||||||
let dbfile = dir.path().join("testdb.sqlite");
|
|
||||||
let sql = Sql::new(dbfile);
|
|
||||||
|
|
||||||
// Create database with all the tables.
|
|
||||||
sql.open(&t, "".to_string()).await.unwrap();
|
|
||||||
sql.close().await;
|
|
||||||
|
|
||||||
// Reopen the database
|
|
||||||
sql.open(&t, "".to_string()).await?;
|
|
||||||
sql.execute(
|
|
||||||
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
|
||||||
("foo", "bar"),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let value: Option<String> = sql
|
|
||||||
.query_get_value("SELECT value FROM config WHERE keyname=?;", ("foo",))
|
|
||||||
.await?;
|
|
||||||
assert_eq!(value.unwrap(), "bar");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_migration_flags() -> Result<()> {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
t.evtracker.get_info_contains("Opened database").await;
|
|
||||||
|
|
||||||
// as migrations::run() was already executed on context creation,
|
|
||||||
// another call should not result in any action needed.
|
|
||||||
// this test catches some bugs where dbversion was forgotten to be persisted.
|
|
||||||
let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) =
|
|
||||||
migrations::run(&t, &t.sql).await?;
|
|
||||||
assert!(!recalc_fingerprints);
|
|
||||||
assert!(!update_icons);
|
|
||||||
assert!(!disable_server_delete);
|
|
||||||
assert!(!recode_avatar);
|
|
||||||
|
|
||||||
info!(&t, "test_migration_flags: XXX END MARKER");
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let evt = t
|
|
||||||
.evtracker
|
|
||||||
.get_matching(|evt| matches!(evt, EventType::Info(_)))
|
|
||||||
.await;
|
|
||||||
match evt {
|
|
||||||
EventType::Info(msg) => {
|
|
||||||
assert!(
|
|
||||||
!msg.contains("[migration]"),
|
|
||||||
"Migrations were run twice, you probably forgot to update the db version"
|
|
||||||
);
|
|
||||||
if msg.contains("test_migration_flags: XXX END MARKER") {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_check_passphrase() -> Result<()> {
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
// The context is used only for logging.
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
|
|
||||||
// Create a separate empty database for testing.
|
|
||||||
let dir = tempdir()?;
|
|
||||||
let dbfile = dir.path().join("testdb.sqlite");
|
|
||||||
let sql = Sql::new(dbfile.clone());
|
|
||||||
|
|
||||||
sql.check_passphrase("foo".to_string()).await?;
|
|
||||||
sql.open(&t, "foo".to_string())
|
|
||||||
.await
|
|
||||||
.context("failed to open the database first time")?;
|
|
||||||
sql.close().await;
|
|
||||||
|
|
||||||
// Reopen the database
|
|
||||||
let sql = Sql::new(dbfile);
|
|
||||||
|
|
||||||
// Test that we can't open encrypted database without a passphrase.
|
|
||||||
assert!(sql.open(&t, "".to_string()).await.is_err());
|
|
||||||
|
|
||||||
// Now open the database with passpharse, it should succeed.
|
|
||||||
sql.check_passphrase("foo".to_string()).await?;
|
|
||||||
sql.open(&t, "foo".to_string())
|
|
||||||
.await
|
|
||||||
.context("failed to open the database second time")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_sql_change_passphrase() -> Result<()> {
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
// The context is used only for logging.
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
|
|
||||||
// Create a separate empty database for testing.
|
|
||||||
let dir = tempdir()?;
|
|
||||||
let dbfile = dir.path().join("testdb.sqlite");
|
|
||||||
let sql = Sql::new(dbfile.clone());
|
|
||||||
|
|
||||||
sql.open(&t, "foo".to_string())
|
|
||||||
.await
|
|
||||||
.context("failed to open the database first time")?;
|
|
||||||
sql.close().await;
|
|
||||||
|
|
||||||
// Change the passphrase from "foo" to "bar".
|
|
||||||
let sql = Sql::new(dbfile.clone());
|
|
||||||
sql.open(&t, "foo".to_string())
|
|
||||||
.await
|
|
||||||
.context("failed to open the database second time")?;
|
|
||||||
sql.change_passphrase("bar".to_string())
|
|
||||||
.await
|
|
||||||
.context("failed to change passphrase")?;
|
|
||||||
|
|
||||||
// Test that at least two connections are still working.
|
|
||||||
// This ensures that not only the connection which changed the password is working,
|
|
||||||
// but other connections as well.
|
|
||||||
{
|
|
||||||
let lock = sql.pool.read().await;
|
|
||||||
let pool = lock.as_ref().unwrap();
|
|
||||||
let query_only = true;
|
|
||||||
let conn1 = pool.get(query_only).await?;
|
|
||||||
let conn2 = pool.get(query_only).await?;
|
|
||||||
conn1
|
|
||||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
|
||||||
.unwrap();
|
|
||||||
conn2
|
|
||||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
sql.close().await;
|
|
||||||
|
|
||||||
let sql = Sql::new(dbfile);
|
|
||||||
|
|
||||||
// Test that old passphrase is not working.
|
|
||||||
assert!(sql.open(&t, "foo".to_string()).await.is_err());
|
|
||||||
|
|
||||||
// Open the database with the new passphrase.
|
|
||||||
sql.check_passphrase("bar".to_string()).await?;
|
|
||||||
sql.open(&t, "bar".to_string())
|
|
||||||
.await
|
|
||||||
.context("failed to open the database third time")?;
|
|
||||||
sql.close().await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_query_only() -> Result<()> {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
|
|
||||||
// `query_row` does not acquire write lock
|
|
||||||
// and operates on read-only connection.
|
|
||||||
// Using it to `INSERT` should fail.
|
|
||||||
let res = t
|
|
||||||
.sql
|
|
||||||
.query_row(
|
|
||||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 1",
|
|
||||||
("xyz", "ijk"),
|
|
||||||
|row| {
|
|
||||||
let res: u32 = row.get(0)?;
|
|
||||||
Ok(res)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert!(res.is_err());
|
|
||||||
|
|
||||||
// If you want to `INSERT` and get value via `RETURNING`,
|
|
||||||
// use `call_write` or `transaction`.
|
|
||||||
|
|
||||||
let res: Result<u32> = t
|
|
||||||
.sql
|
|
||||||
.call_write(|conn| {
|
|
||||||
let val = conn.query_row(
|
|
||||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 2",
|
|
||||||
("foo", "bar"),
|
|
||||||
|row| {
|
|
||||||
let res: u32 = row.get(0)?;
|
|
||||||
Ok(res)
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
Ok(val)
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
assert_eq!(res.unwrap(), 2);
|
|
||||||
|
|
||||||
let res = t
|
|
||||||
.sql
|
|
||||||
.transaction(|t| {
|
|
||||||
let val = t.query_row(
|
|
||||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 3",
|
|
||||||
("abc", "def"),
|
|
||||||
|row| {
|
|
||||||
let res: u32 = row.get(0)?;
|
|
||||||
Ok(res)
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
Ok(val)
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
assert_eq!(res.unwrap(), 3);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tests that incremental_vacuum does not fail.
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_incremental_vacuum() -> Result<()> {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
|
|
||||||
incremental_vacuum(&t).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
372
src/sql/sql_tests.rs
Normal file
372
src/sql/sql_tests.rs
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::{test_utils::TestContext, EventType};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_maybe_add_file() {
|
||||||
|
let mut files = Default::default();
|
||||||
|
maybe_add_file(&mut files, "$BLOBDIR/hello");
|
||||||
|
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
|
||||||
|
maybe_add_file(&mut files, "world2.txt");
|
||||||
|
maybe_add_file(&mut files, "$BLOBDIR");
|
||||||
|
|
||||||
|
assert!(files.contains("hello"));
|
||||||
|
assert!(files.contains("world.txt"));
|
||||||
|
assert!(!files.contains("world2.txt"));
|
||||||
|
assert!(!files.contains("$BLOBDIR"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_file_in_use() {
|
||||||
|
let mut files = Default::default();
|
||||||
|
maybe_add_file(&mut files, "$BLOBDIR/hello");
|
||||||
|
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
|
||||||
|
maybe_add_file(&mut files, "world2.txt");
|
||||||
|
|
||||||
|
assert!(is_file_in_use(&files, None, "hello"));
|
||||||
|
assert!(!is_file_in_use(&files, Some(".txt"), "hello"));
|
||||||
|
assert!(is_file_in_use(&files, Some("-suffix"), "world.txt-suffix"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_table_exists() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
assert!(t.ctx.sql.table_exists("msgs").await.unwrap());
|
||||||
|
assert!(!t.ctx.sql.table_exists("foobar").await.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_col_exists() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
assert!(t.ctx.sql.col_exists("msgs", "mime_modified").await.unwrap());
|
||||||
|
assert!(!t.ctx.sql.col_exists("msgs", "foobar").await.unwrap());
|
||||||
|
assert!(!t.ctx.sql.col_exists("foobar", "foobar").await.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that auto_vacuum is enabled for new databases.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_auto_vacuum() -> Result<()> {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
|
let query_only = true;
|
||||||
|
let auto_vacuum = t
|
||||||
|
.sql
|
||||||
|
.call(query_only, |conn| {
|
||||||
|
let auto_vacuum = conn.pragma_query_value(None, "auto_vacuum", |row| {
|
||||||
|
let auto_vacuum: i32 = row.get(0)?;
|
||||||
|
Ok(auto_vacuum)
|
||||||
|
})?;
|
||||||
|
Ok(auto_vacuum)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// auto_vacuum=2 is the same as auto_vacuum=INCREMENTAL
|
||||||
|
assert_eq!(auto_vacuum, 2);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_housekeeping_db_closed() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
|
let avatar_src = t.dir.path().join("avatar.png");
|
||||||
|
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||||
|
tokio::fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||||
|
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let event_source = t.get_event_emitter();
|
||||||
|
|
||||||
|
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||||
|
assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]);
|
||||||
|
|
||||||
|
t.sql.close().await;
|
||||||
|
housekeeping(&t).await.unwrap(); // housekeeping should emit warnings but not fail
|
||||||
|
t.sql.open(&t, "".to_string()).await.unwrap();
|
||||||
|
|
||||||
|
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||||
|
assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]);
|
||||||
|
|
||||||
|
while let Ok(event) = event_source.try_recv() {
|
||||||
|
match event.typ {
|
||||||
|
EventType::Info(s) => assert!(
|
||||||
|
!s.contains("Keeping new unreferenced file"),
|
||||||
|
"File {s} was almost deleted, only reason it was kept is that it was created recently (as the tests don't run for a long time)"
|
||||||
|
),
|
||||||
|
EventType::Error(s) => panic!("{}", s),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Regression test for a bug where housekeeping deleted drafts since their
|
||||||
|
/// `hidden` flag is set.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_housekeeping_dont_delete_drafts() {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
|
||||||
|
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
|
||||||
|
let mut new_draft = Message::new_text("This is my draft".to_string());
|
||||||
|
chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap();
|
||||||
|
|
||||||
|
housekeeping(&t).await.unwrap();
|
||||||
|
|
||||||
|
let loaded_draft = chat.id.get_draft(&t).await.unwrap();
|
||||||
|
assert_eq!(loaded_draft.unwrap().text, "This is my draft");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that `housekeeping` deletes the blobs backup dir which is created normally by
|
||||||
|
/// `imex::import_backup`.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_housekeeping_delete_blobs_backup_dir() {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
let dir = t.get_blobdir().join(BLOBS_BACKUP_NAME);
|
||||||
|
tokio::fs::create_dir(&dir).await.unwrap();
|
||||||
|
tokio::fs::write(dir.join("f"), "").await.unwrap();
|
||||||
|
housekeeping(&t).await.unwrap();
|
||||||
|
tokio::fs::create_dir(&dir).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Regression test.
|
||||||
|
///
|
||||||
|
/// Previously the code checking for existence of `config` table
|
||||||
|
/// checked it with `PRAGMA table_info("config")` but did not
|
||||||
|
/// drain `SqlitePool.fetch` result, only using the first row
|
||||||
|
/// returned. As a result, prepared statement for `PRAGMA` was not
|
||||||
|
/// finalized early enough, leaving reader connection in a broken
|
||||||
|
/// state after reopening the database, when `config` table
|
||||||
|
/// existed and `PRAGMA` returned non-empty result.
|
||||||
|
///
|
||||||
|
/// Statements were not finalized due to a bug in sqlx:
|
||||||
|
/// <https://github.com/launchbadge/sqlx/issues/1147>
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_db_reopen() -> Result<()> {
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
// The context is used only for logging.
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
|
// Create a separate empty database for testing.
|
||||||
|
let dir = tempdir()?;
|
||||||
|
let dbfile = dir.path().join("testdb.sqlite");
|
||||||
|
let sql = Sql::new(dbfile);
|
||||||
|
|
||||||
|
// Create database with all the tables.
|
||||||
|
sql.open(&t, "".to_string()).await.unwrap();
|
||||||
|
sql.close().await;
|
||||||
|
|
||||||
|
// Reopen the database
|
||||||
|
sql.open(&t, "".to_string()).await?;
|
||||||
|
sql.execute(
|
||||||
|
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
||||||
|
("foo", "bar"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let value: Option<String> = sql
|
||||||
|
.query_get_value("SELECT value FROM config WHERE keyname=?;", ("foo",))
|
||||||
|
.await?;
|
||||||
|
assert_eq!(value.unwrap(), "bar");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_migration_flags() -> Result<()> {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
t.evtracker.get_info_contains("Opened database").await;
|
||||||
|
|
||||||
|
// as migrations::run() was already executed on context creation,
|
||||||
|
// another call should not result in any action needed.
|
||||||
|
// this test catches some bugs where dbversion was forgotten to be persisted.
|
||||||
|
let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) =
|
||||||
|
migrations::run(&t, &t.sql).await?;
|
||||||
|
assert!(!recalc_fingerprints);
|
||||||
|
assert!(!update_icons);
|
||||||
|
assert!(!disable_server_delete);
|
||||||
|
assert!(!recode_avatar);
|
||||||
|
|
||||||
|
info!(&t, "test_migration_flags: XXX END MARKER");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let evt = t
|
||||||
|
.evtracker
|
||||||
|
.get_matching(|evt| matches!(evt, EventType::Info(_)))
|
||||||
|
.await;
|
||||||
|
match evt {
|
||||||
|
EventType::Info(msg) => {
|
||||||
|
assert!(
|
||||||
|
!msg.contains("[migration]"),
|
||||||
|
"Migrations were run twice, you probably forgot to update the db version"
|
||||||
|
);
|
||||||
|
if msg.contains("test_migration_flags: XXX END MARKER") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_check_passphrase() -> Result<()> {
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
// The context is used only for logging.
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
|
// Create a separate empty database for testing.
|
||||||
|
let dir = tempdir()?;
|
||||||
|
let dbfile = dir.path().join("testdb.sqlite");
|
||||||
|
let sql = Sql::new(dbfile.clone());
|
||||||
|
|
||||||
|
sql.check_passphrase("foo".to_string()).await?;
|
||||||
|
sql.open(&t, "foo".to_string())
|
||||||
|
.await
|
||||||
|
.context("failed to open the database first time")?;
|
||||||
|
sql.close().await;
|
||||||
|
|
||||||
|
// Reopen the database
|
||||||
|
let sql = Sql::new(dbfile);
|
||||||
|
|
||||||
|
// Test that we can't open encrypted database without a passphrase.
|
||||||
|
assert!(sql.open(&t, "".to_string()).await.is_err());
|
||||||
|
|
||||||
|
// Now open the database with passpharse, it should succeed.
|
||||||
|
sql.check_passphrase("foo".to_string()).await?;
|
||||||
|
sql.open(&t, "foo".to_string())
|
||||||
|
.await
|
||||||
|
.context("failed to open the database second time")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_sql_change_passphrase() -> Result<()> {
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
// The context is used only for logging.
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
|
// Create a separate empty database for testing.
|
||||||
|
let dir = tempdir()?;
|
||||||
|
let dbfile = dir.path().join("testdb.sqlite");
|
||||||
|
let sql = Sql::new(dbfile.clone());
|
||||||
|
|
||||||
|
sql.open(&t, "foo".to_string())
|
||||||
|
.await
|
||||||
|
.context("failed to open the database first time")?;
|
||||||
|
sql.close().await;
|
||||||
|
|
||||||
|
// Change the passphrase from "foo" to "bar".
|
||||||
|
let sql = Sql::new(dbfile.clone());
|
||||||
|
sql.open(&t, "foo".to_string())
|
||||||
|
.await
|
||||||
|
.context("failed to open the database second time")?;
|
||||||
|
sql.change_passphrase("bar".to_string())
|
||||||
|
.await
|
||||||
|
.context("failed to change passphrase")?;
|
||||||
|
|
||||||
|
// Test that at least two connections are still working.
|
||||||
|
// This ensures that not only the connection which changed the password is working,
|
||||||
|
// but other connections as well.
|
||||||
|
{
|
||||||
|
let lock = sql.pool.read().await;
|
||||||
|
let pool = lock.as_ref().unwrap();
|
||||||
|
let query_only = true;
|
||||||
|
let conn1 = pool.get(query_only).await?;
|
||||||
|
let conn2 = pool.get(query_only).await?;
|
||||||
|
conn1
|
||||||
|
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||||
|
.unwrap();
|
||||||
|
conn2
|
||||||
|
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
sql.close().await;
|
||||||
|
|
||||||
|
let sql = Sql::new(dbfile);
|
||||||
|
|
||||||
|
// Test that old passphrase is not working.
|
||||||
|
assert!(sql.open(&t, "foo".to_string()).await.is_err());
|
||||||
|
|
||||||
|
// Open the database with the new passphrase.
|
||||||
|
sql.check_passphrase("bar".to_string()).await?;
|
||||||
|
sql.open(&t, "bar".to_string())
|
||||||
|
.await
|
||||||
|
.context("failed to open the database third time")?;
|
||||||
|
sql.close().await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_query_only() -> Result<()> {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
|
// `query_row` does not acquire write lock
|
||||||
|
// and operates on read-only connection.
|
||||||
|
// Using it to `INSERT` should fail.
|
||||||
|
let res = t
|
||||||
|
.sql
|
||||||
|
.query_row(
|
||||||
|
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 1",
|
||||||
|
("xyz", "ijk"),
|
||||||
|
|row| {
|
||||||
|
let res: u32 = row.get(0)?;
|
||||||
|
Ok(res)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
|
||||||
|
// If you want to `INSERT` and get value via `RETURNING`,
|
||||||
|
// use `call_write` or `transaction`.
|
||||||
|
|
||||||
|
let res: Result<u32> = t
|
||||||
|
.sql
|
||||||
|
.call_write(|conn| {
|
||||||
|
let val = conn.query_row(
|
||||||
|
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 2",
|
||||||
|
("foo", "bar"),
|
||||||
|
|row| {
|
||||||
|
let res: u32 = row.get(0)?;
|
||||||
|
Ok(res)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
Ok(val)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.unwrap(), 2);
|
||||||
|
|
||||||
|
let res = t
|
||||||
|
.sql
|
||||||
|
.transaction(|t| {
|
||||||
|
let val = t.query_row(
|
||||||
|
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 3",
|
||||||
|
("abc", "def"),
|
||||||
|
|row| {
|
||||||
|
let res: u32 = row.get(0)?;
|
||||||
|
Ok(res)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
Ok(val)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.unwrap(), 3);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that incremental_vacuum does not fail.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_incremental_vacuum() -> Result<()> {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
|
||||||
|
incremental_vacuum(&t).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
185
src/stock_str.rs
185
src/stock_str.rs
@@ -1439,187 +1439,4 @@ impl Accounts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod stock_str_tests;
|
||||||
use num_traits::ToPrimitive;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::chat::delete_and_reset_all_device_msgs;
|
|
||||||
use crate::chatlist::Chatlist;
|
|
||||||
use crate::test_utils::TestContext;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_enum_mapping() {
|
|
||||||
assert_eq!(StockMessage::NoMessages.to_usize().unwrap(), 1);
|
|
||||||
assert_eq!(StockMessage::SelfMsg.to_usize().unwrap(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fallback() {
|
|
||||||
assert_eq!(StockMessage::NoMessages.fallback(), "No messages.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_set_stock_translation() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
t.set_stock_translation(StockMessage::NoMessages, "xyz".to_string())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(no_messages(&t).await, "xyz")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_set_stock_translation_wrong_replacements() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
assert!(t
|
|
||||||
.ctx
|
|
||||||
.set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string())
|
|
||||||
.await
|
|
||||||
.is_err());
|
|
||||||
assert!(t
|
|
||||||
.ctx
|
|
||||||
.set_stock_translation(StockMessage::NoMessages, "xyz %2$s ".to_string())
|
|
||||||
.await
|
|
||||||
.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_stock_str() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
assert_eq!(no_messages(&t).await, "No messages.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_stock_string_repl_str() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let contact_id = Contact::create(&t.ctx, "Someone", "someone@example.org")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let contact = Contact::get_by_id(&t.ctx, contact_id).await.unwrap();
|
|
||||||
// uses %1$s substitution
|
|
||||||
assert_eq!(
|
|
||||||
contact_verified(&t, &contact).await,
|
|
||||||
"Someone (someone@example.org) verified."
|
|
||||||
);
|
|
||||||
// We have no string using %1$d to test...
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_stock_system_msg_simple() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
assert_eq!(
|
|
||||||
msg_location_enabled(&t).await,
|
|
||||||
"Location streaming enabled."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_stock_system_msg_add_member_by_me() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
assert_eq!(
|
|
||||||
msg_add_member_remote(&t, "alice@example.org").await,
|
|
||||||
"I added member alice@example.org."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
|
|
||||||
"You added member alice@example.org."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_stock_system_msg_add_member_by_me_with_displayname() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
Contact::create(&t, "Alice", "alice@example.org")
|
|
||||||
.await
|
|
||||||
.expect("failed to create contact");
|
|
||||||
assert_eq!(
|
|
||||||
msg_add_member_remote(&t, "alice@example.org").await,
|
|
||||||
"I added member alice@example.org."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
|
|
||||||
"You added member Alice (alice@example.org)."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_stock_system_msg_add_member_by_other_with_displayname() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let contact_id = {
|
|
||||||
Contact::create(&t, "Alice", "alice@example.org")
|
|
||||||
.await
|
|
||||||
.expect("Failed to create contact Alice");
|
|
||||||
Contact::create(&t, "Bob", "bob@example.com")
|
|
||||||
.await
|
|
||||||
.expect("failed to create bob")
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
msg_add_member_local(&t, "alice@example.org", contact_id,).await,
|
|
||||||
"Member Alice (alice@example.org) added by Bob (bob@example.com)."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_quota_exceeding_stock_str() -> Result<()> {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let str = quota_exceeding(&t, 81).await;
|
|
||||||
assert!(str.contains("81% "));
|
|
||||||
assert!(str.contains("100% "));
|
|
||||||
assert!(!str.contains("%%"));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_partial_download_msg_body() -> Result<()> {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
let str = partial_download_msg_body(&t, 1024 * 1024).await;
|
|
||||||
assert_eq!(str, "1 MiB message");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_update_device_chats() {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
t.update_device_chats().await.ok();
|
|
||||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
|
||||||
assert_eq!(chats.len(), 2);
|
|
||||||
|
|
||||||
let chat0 = Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let (self_talk_id, device_chat_id) = if chat0.is_self_talk() {
|
|
||||||
(chats.get_chat_id(0).unwrap(), chats.get_chat_id(1).unwrap())
|
|
||||||
} else {
|
|
||||||
(chats.get_chat_id(1).unwrap(), chats.get_chat_id(0).unwrap())
|
|
||||||
};
|
|
||||||
|
|
||||||
// delete self-talk first; this adds a message to device-chat about how self-talk can be restored
|
|
||||||
let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id).await.unwrap().len();
|
|
||||||
self_talk_id.delete(&t).await.ok();
|
|
||||||
assert_eq!(
|
|
||||||
chat::get_chat_msgs(&t, device_chat_id).await.unwrap().len(),
|
|
||||||
device_chat_msgs_before + 1
|
|
||||||
);
|
|
||||||
|
|
||||||
// delete device chat
|
|
||||||
device_chat_id.delete(&t).await.ok();
|
|
||||||
|
|
||||||
// check, that the chatlist is empty
|
|
||||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
|
||||||
assert_eq!(chats.len(), 0);
|
|
||||||
|
|
||||||
// a subsequent call to update_device_chats() must not re-add manually deleted messages or chats
|
|
||||||
t.update_device_chats().await.unwrap();
|
|
||||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
|
||||||
assert_eq!(chats.len(), 0);
|
|
||||||
|
|
||||||
// Reset all device messages. This normally happens due to account export and import.
|
|
||||||
// Check that update_device_chats() does not add welcome message for imported account.
|
|
||||||
delete_and_reset_all_device_msgs(&t).await.unwrap();
|
|
||||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
|
||||||
assert_eq!(chats.len(), 0);
|
|
||||||
|
|
||||||
t.update_device_chats().await.unwrap();
|
|
||||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
|
||||||
assert_eq!(chats.len(), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
182
src/stock_str/stock_str_tests.rs
Normal file
182
src/stock_str/stock_str_tests.rs
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
use num_traits::ToPrimitive;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::chat::delete_and_reset_all_device_msgs;
|
||||||
|
use crate::chatlist::Chatlist;
|
||||||
|
use crate::test_utils::TestContext;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enum_mapping() {
|
||||||
|
assert_eq!(StockMessage::NoMessages.to_usize().unwrap(), 1);
|
||||||
|
assert_eq!(StockMessage::SelfMsg.to_usize().unwrap(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fallback() {
|
||||||
|
assert_eq!(StockMessage::NoMessages.fallback(), "No messages.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_set_stock_translation() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
t.set_stock_translation(StockMessage::NoMessages, "xyz".to_string())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(no_messages(&t).await, "xyz")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_set_stock_translation_wrong_replacements() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
assert!(t
|
||||||
|
.ctx
|
||||||
|
.set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string())
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
assert!(t
|
||||||
|
.ctx
|
||||||
|
.set_stock_translation(StockMessage::NoMessages, "xyz %2$s ".to_string())
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_stock_str() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
assert_eq!(no_messages(&t).await, "No messages.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_stock_string_repl_str() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let contact_id = Contact::create(&t.ctx, "Someone", "someone@example.org")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let contact = Contact::get_by_id(&t.ctx, contact_id).await.unwrap();
|
||||||
|
// uses %1$s substitution
|
||||||
|
assert_eq!(
|
||||||
|
contact_verified(&t, &contact).await,
|
||||||
|
"Someone (someone@example.org) verified."
|
||||||
|
);
|
||||||
|
// We have no string using %1$d to test...
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_stock_system_msg_simple() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
assert_eq!(
|
||||||
|
msg_location_enabled(&t).await,
|
||||||
|
"Location streaming enabled."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_stock_system_msg_add_member_by_me() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
assert_eq!(
|
||||||
|
msg_add_member_remote(&t, "alice@example.org").await,
|
||||||
|
"I added member alice@example.org."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
|
||||||
|
"You added member alice@example.org."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_stock_system_msg_add_member_by_me_with_displayname() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
Contact::create(&t, "Alice", "alice@example.org")
|
||||||
|
.await
|
||||||
|
.expect("failed to create contact");
|
||||||
|
assert_eq!(
|
||||||
|
msg_add_member_remote(&t, "alice@example.org").await,
|
||||||
|
"I added member alice@example.org."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
|
||||||
|
"You added member Alice (alice@example.org)."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_stock_system_msg_add_member_by_other_with_displayname() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let contact_id = {
|
||||||
|
Contact::create(&t, "Alice", "alice@example.org")
|
||||||
|
.await
|
||||||
|
.expect("Failed to create contact Alice");
|
||||||
|
Contact::create(&t, "Bob", "bob@example.com")
|
||||||
|
.await
|
||||||
|
.expect("failed to create bob")
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
msg_add_member_local(&t, "alice@example.org", contact_id,).await,
|
||||||
|
"Member Alice (alice@example.org) added by Bob (bob@example.com)."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_quota_exceeding_stock_str() -> Result<()> {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let str = quota_exceeding(&t, 81).await;
|
||||||
|
assert!(str.contains("81% "));
|
||||||
|
assert!(str.contains("100% "));
|
||||||
|
assert!(!str.contains("%%"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_partial_download_msg_body() -> Result<()> {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let str = partial_download_msg_body(&t, 1024 * 1024).await;
|
||||||
|
assert_eq!(str, "1 MiB message");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_update_device_chats() {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
t.update_device_chats().await.ok();
|
||||||
|
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||||
|
assert_eq!(chats.len(), 2);
|
||||||
|
|
||||||
|
let chat0 = Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let (self_talk_id, device_chat_id) = if chat0.is_self_talk() {
|
||||||
|
(chats.get_chat_id(0).unwrap(), chats.get_chat_id(1).unwrap())
|
||||||
|
} else {
|
||||||
|
(chats.get_chat_id(1).unwrap(), chats.get_chat_id(0).unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
// delete self-talk first; this adds a message to device-chat about how self-talk can be restored
|
||||||
|
let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id).await.unwrap().len();
|
||||||
|
self_talk_id.delete(&t).await.ok();
|
||||||
|
assert_eq!(
|
||||||
|
chat::get_chat_msgs(&t, device_chat_id).await.unwrap().len(),
|
||||||
|
device_chat_msgs_before + 1
|
||||||
|
);
|
||||||
|
|
||||||
|
// delete device chat
|
||||||
|
device_chat_id.delete(&t).await.ok();
|
||||||
|
|
||||||
|
// check, that the chatlist is empty
|
||||||
|
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||||
|
assert_eq!(chats.len(), 0);
|
||||||
|
|
||||||
|
// a subsequent call to update_device_chats() must not re-add manually deleted messages or chats
|
||||||
|
t.update_device_chats().await.unwrap();
|
||||||
|
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||||
|
assert_eq!(chats.len(), 0);
|
||||||
|
|
||||||
|
// Reset all device messages. This normally happens due to account export and import.
|
||||||
|
// Check that update_device_chats() does not add welcome message for imported account.
|
||||||
|
delete_and_reset_all_device_msgs(&t).await.unwrap();
|
||||||
|
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||||
|
assert_eq!(chats.len(), 0);
|
||||||
|
|
||||||
|
t.update_device_chats().await.unwrap();
|
||||||
|
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||||
|
assert_eq!(chats.len(), 0);
|
||||||
|
}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
Return-Path: <bob@example.org>
|
|
||||||
User-Agent: K-9 Mail for Android
|
|
||||||
In-Reply-To: <hasnihae@gmx.de>
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/alternative; boundary="----MLV7YOLJ7ED4UZKNGQYQ63O0RJGHU0"
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
Subject: Re: Test
|
|
||||||
To: Alice <alice@example.org>
|
|
||||||
From: Bob <bob@example.org>
|
|
||||||
Message-ID: <haeisnr@example.org>
|
|
||||||
|
|
||||||
------MLV7YOLJ7ED4UZKNGQYQ63O0RJGHU0
|
|
||||||
Content-Type: text/definitelynotplainthiswouldbetooeasy;
|
|
||||||
charset=utf-8
|
|
||||||
Content-Transfer-Encoding: quoted-printable
|
|
||||||
|
|
||||||
Hi Alice,
|
|
||||||
|
|
||||||
some text.
|
|
||||||
|
|
||||||
Am 21=2E Juni 2020 10:38:44 MESZ schrieb Alice <alice@example=2Eorg>:
|
|
||||||
>Dear Bob,
|
|
||||||
>
|
|
||||||
>let's meet
|
|
||||||
>
|
|
||||||
>Alice
|
|
||||||
|
|
||||||
--=20
|
|
||||||
Diese Nachricht wurde von meinem Android-Ger=C3=A4t mit K-9 Mail gesendet=
|
|
||||||
=2E
|
|
||||||
------MLV7YOLJ7ED4UZKNGQYQ63O0RJGHU0
|
|
||||||
Content-Type: text/html;
|
|
||||||
charset=utf-8
|
|
||||||
Content-Transfer-Encoding: quoted-printable
|
|
||||||
|
|
||||||
<html><head></head><body>Hi Alice,<br><br>some text.<br><br>
|
|
||||||
<div class=3D"gmail_quote">Am 21=2E Juni 2020 10:38:44 M=
|
|
||||||
ESZ schrieb Alice <jonathanschmiederer@gmx=2Ede>:<bloc=
|
|
||||||
kquote class=3D"gmail_quote" style=3D"margin: 0pt 0pt 0pt 0=2E8ex; border-l=
|
|
||||||
eft: 1px solid rgb(204, 204, 204); padding-left: 1ex;">
|
|
||||||
<pre class=3D"k9mail">Sehr geehrte/r Frau/Herr Brenner,<br><br>ich habe in=
|
|
||||||
meinen JuFo-Unterlagen den angeh=C3=A4ngten Gutschein gefunden=2E<br>Ist e=
|
|
||||||
s noch m=C3=B6glich, diesen einzul=C3=B6sen?<br><br>Mit freundlichen Gr=C3=
|
|
||||||
=BC=C3=9Fen<br>Alice<br></pre></blockquote></div><br>-- <br>=
|
|
||||||
Diese Nachricht wurde von meinem Android-Ger=C3=A4t mit K-9 Mail gesendet=
|
|
||||||
=2E</body></html>
|
|
||||||
------MLV7YOLJ7ED4UZKNGQYQ63O0RJGHU0--
|
|
||||||
Reference in New Issue
Block a user