mirror of
https://github.com/chatmail/core.git
synced 2026-04-26 01:46:34 +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)]
|
||||
mod 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(())
|
||||
}
|
||||
}
|
||||
mod blob_tests;
|
||||
|
||||
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)]
|
||||
mod 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(())
|
||||
}
|
||||
}
|
||||
mod context_tests;
|
||||
|
||||
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)]
|
||||
mod 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(())
|
||||
}
|
||||
}
|
||||
mod ephemeral_tests;
|
||||
|
||||
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)]
|
||||
mod 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)"
|
||||
);
|
||||
}
|
||||
}
|
||||
mod imap_tests;
|
||||
|
||||
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)]
|
||||
mod 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(())
|
||||
}
|
||||
}
|
||||
mod qr_tests;
|
||||
|
||||
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)]
|
||||
mod 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(())
|
||||
}
|
||||
}
|
||||
mod securejoin_tests;
|
||||
|
||||
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)]
|
||||
mod 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(())
|
||||
}
|
||||
}
|
||||
mod sql_tests;
|
||||
|
||||
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)]
|
||||
mod 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);
|
||||
}
|
||||
}
|
||||
mod stock_str_tests;
|
||||
|
||||
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