diff --git a/src/contact.rs b/src/contact.rs index 3e7eacb7a..a27b8e305 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1980,1278 +1980,4 @@ impl RecentlySeenLoop { } #[cfg(test)] -mod tests { - use deltachat_contact_tools::may_be_valid_addr; - - use super::*; - use crate::chat::{get_chat_contacts, send_text_msg, Chat}; - use crate::chatlist::Chatlist; - use crate::receive_imf::receive_imf; - use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote}; - - #[test] - fn test_contact_id_values() { - // Some FFI users need to have the values of these fixed, how naughty. But let's - // make sure we don't modify them anyway. - assert_eq!(ContactId::UNDEFINED.to_u32(), 0); - assert_eq!(ContactId::SELF.to_u32(), 1); - assert_eq!(ContactId::INFO.to_u32(), 2); - assert_eq!(ContactId::DEVICE.to_u32(), 5); - assert_eq!(ContactId::LAST_SPECIAL.to_u32(), 9); - } - - #[test] - fn test_may_be_valid_addr() { - assert_eq!(may_be_valid_addr(""), false); - assert_eq!(may_be_valid_addr("user@domain.tld"), true); - assert_eq!(may_be_valid_addr("uuu"), false); - assert_eq!(may_be_valid_addr("dd.tt"), false); - assert_eq!(may_be_valid_addr("tt.dd@uu"), true); - assert_eq!(may_be_valid_addr("u@d"), true); - assert_eq!(may_be_valid_addr("u@d."), false); - assert_eq!(may_be_valid_addr("u@d.t"), true); - assert_eq!(may_be_valid_addr("u@d.tt"), true); - assert_eq!(may_be_valid_addr("u@.tt"), true); - assert_eq!(may_be_valid_addr("@d.tt"), false); - assert_eq!(may_be_valid_addr(""), false); - assert_eq!(may_be_valid_addr("as@sd.de>"), false); - assert_eq!(may_be_valid_addr("ask dkl@dd.tt"), false); - assert_eq!(may_be_valid_addr("user@domain.tld."), false); - } - - #[test] - fn test_normalize_addr() { - assert_eq!(addr_normalize("mailto:john@doe.com"), "john@doe.com"); - assert_eq!(addr_normalize(" hello@world.com "), "hello@world.com"); - assert_eq!(addr_normalize("John@Doe.com"), "john@doe.com"); - } - - #[test] - fn test_split_address_book() { - let book = "Name one\nAddress one\nName two\nAddress two\nrest name"; - let list = split_address_book(book); - assert_eq!( - list, - vec![("Name one", "Address one"), ("Name two", "Address two")] - ) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_contacts() -> Result<()> { - let context = TestContext::new().await; - - assert!(context.get_all_self_addrs().await?.is_empty()); - - // Bob is not in the contacts yet. - let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?; - assert_eq!(contacts.len(), 0); - - let (id, _modified) = Contact::add_or_lookup( - &context.ctx, - "bob", - &ContactAddress::new("user@example.org")?, - Origin::IncomingReplyTo, - ) - .await?; - assert_ne!(id, ContactId::UNDEFINED); - - let contact = Contact::get_by_id(&context.ctx, id).await.unwrap(); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_authname(), "bob"); - assert_eq!(contact.get_display_name(), "bob"); - - // Search by name. - let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?; - assert_eq!(contacts.len(), 1); - assert_eq!(contacts.first(), Some(&id)); - - // Search by address. - let contacts = Contact::get_all(&context.ctx, 0, Some("user")).await?; - assert_eq!(contacts.len(), 1); - assert_eq!(contacts.first(), Some(&id)); - - let contacts = Contact::get_all(&context.ctx, 0, Some("alice")).await?; - assert_eq!(contacts.len(), 0); - - // Set Bob name to "someone" manually. - let (contact_bob_id, modified) = Contact::add_or_lookup( - &context.ctx, - "someone", - &ContactAddress::new("user@example.org")?, - Origin::ManuallyCreated, - ) - .await?; - assert_eq!(contact_bob_id, id); - assert_eq!(modified, Modifier::Modified); - let contact = Contact::get_by_id(&context.ctx, id).await.unwrap(); - assert_eq!(contact.get_name(), "someone"); - assert_eq!(contact.get_authname(), "bob"); - assert_eq!(contact.get_display_name(), "someone"); - - // Not searchable by authname, because it is not displayed. - let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?; - assert_eq!(contacts.len(), 0); - - // Search by display name (same as manually set name). - let contacts = Contact::get_all(&context.ctx, 0, Some("someone")).await?; - assert_eq!(contacts.len(), 1); - assert_eq!(contacts.first(), Some(&id)); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_is_self_addr() -> Result<()> { - let t = TestContext::new().await; - assert_eq!(t.is_self_addr("me@me.org").await?, false); - - t.configure_addr("you@you.net").await; - assert_eq!(t.is_self_addr("me@me.org").await?, false); - assert_eq!(t.is_self_addr("you@you.net").await?, true); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_add_or_lookup() { - // add some contacts, this also tests add_address_book() - let t = TestContext::new().await; - let book = concat!( - " Name one \n one@eins.org \n", - "Name two\ntwo@deux.net\n", - "Invalid\n+1234567890\n", // invalid, should be ignored - "\nthree@drei.sam\n", - "Name two\ntwo@deux.net\n", // should not be added again - "\nWonderland, Alice \n", - ); - assert_eq!(Contact::add_address_book(&t, book).await.unwrap(), 4); - - // check first added contact, this modifies authname because it is empty - let (contact_id, sth_modified) = Contact::add_or_lookup( - &t, - "bla foo", - &ContactAddress::new("one@eins.org").unwrap(), - Origin::IncomingUnknownTo, - ) - .await - .unwrap(); - assert!(!contact_id.is_special()); - assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_id(), contact_id); - assert_eq!(contact.get_name(), "Name one"); - assert_eq!(contact.get_authname(), "bla foo"); - assert_eq!(contact.get_display_name(), "Name one"); - assert_eq!(contact.get_addr(), "one@eins.org"); - assert_eq!(contact.get_name_n_addr(), "Name one (one@eins.org)"); - - // modify first added contact - let (contact_id_test, sth_modified) = Contact::add_or_lookup( - &t, - "Real one", - &ContactAddress::new(" one@eins.org ").unwrap(), - Origin::ManuallyCreated, - ) - .await - .unwrap(); - assert_eq!(contact_id, contact_id_test); - assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_name(), "Real one"); - assert_eq!(contact.get_addr(), "one@eins.org"); - assert!(!contact.is_blocked()); - - // check third added contact (contact without name) - let (contact_id, sth_modified) = Contact::add_or_lookup( - &t, - "", - &ContactAddress::new("three@drei.sam").unwrap(), - Origin::IncomingUnknownTo, - ) - .await - .unwrap(); - assert!(!contact_id.is_special()); - assert_eq!(sth_modified, Modifier::None); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_display_name(), "three@drei.sam"); - assert_eq!(contact.get_addr(), "three@drei.sam"); - assert_eq!(contact.get_name_n_addr(), "three@drei.sam"); - - // add name to third contact from incoming message (this becomes authorized name) - let (contact_id_test, sth_modified) = Contact::add_or_lookup( - &t, - "m. serious", - &ContactAddress::new("three@drei.sam").unwrap(), - Origin::IncomingUnknownFrom, - ) - .await - .unwrap(); - assert_eq!(contact_id, contact_id_test); - assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_name_n_addr(), "m. serious (three@drei.sam)"); - assert!(!contact.is_blocked()); - - // manually edit name of third contact (does not changed authorized name) - let (contact_id_test, sth_modified) = Contact::add_or_lookup( - &t, - "schnucki", - &ContactAddress::new("three@drei.sam").unwrap(), - Origin::ManuallyCreated, - ) - .await - .unwrap(); - assert_eq!(contact_id, contact_id_test); - assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_authname(), "m. serious"); - assert_eq!(contact.get_name_n_addr(), "schnucki (three@drei.sam)"); - assert!(!contact.is_blocked()); - - // Fourth contact: - let (contact_id, sth_modified) = Contact::add_or_lookup( - &t, - "", - &ContactAddress::new("alice@w.de").unwrap(), - Origin::IncomingUnknownTo, - ) - .await - .unwrap(); - assert!(!contact_id.is_special()); - assert_eq!(sth_modified, Modifier::None); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_name(), "Wonderland, Alice"); - assert_eq!(contact.get_display_name(), "Wonderland, Alice"); - assert_eq!(contact.get_addr(), "alice@w.de"); - assert_eq!(contact.get_name_n_addr(), "Wonderland, Alice (alice@w.de)"); - - // check SELF - let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap(); - assert_eq!(contact.get_name(), stock_str::self_msg(&t).await); - assert_eq!(contact.get_addr(), ""); // we're not configured - assert!(!contact.is_blocked()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_contact_name_changes() -> Result<()> { - let t = TestContext::new_alice().await; - - // first message creates contact and one-to-one-chat without name set - receive_imf( - &t, - b"From: f@example.org\n\ - To: alice@example.org\n\ - Subject: foo\n\ - Message-ID: <1234-1@example.org>\n\ - Chat-Version: 1.0\n\ - Date: Sun, 29 May 2022 08:37:57 +0000\n\ - \n\ - hello one\n", - false, - ) - .await?; - let chat_id = t.get_last_msg().await.get_chat_id(); - chat_id.accept(&t).await?; - assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "f@example.org"); - let chatlist = Chatlist::try_load(&t, 0, Some("f@example.org"), None).await?; - assert_eq!(chatlist.len(), 1); - let contacts = get_chat_contacts(&t, chat_id).await?; - let contact_id = contacts.first().unwrap(); - let contact = Contact::get_by_id(&t, *contact_id).await?; - assert_eq!(contact.get_authname(), ""); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_display_name(), "f@example.org"); - assert_eq!(contact.get_name_n_addr(), "f@example.org"); - let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?; - assert_eq!(contacts.len(), 1); - - // second message inits the name - receive_imf( - &t, - b"From: Flobbyfoo \n\ - To: alice@example.org\n\ - Subject: foo\n\ - Message-ID: <1234-2@example.org>\n\ - Chat-Version: 1.0\n\ - Date: Sun, 29 May 2022 08:38:57 +0000\n\ - \n\ - hello two\n", - false, - ) - .await?; - let chat_id = t.get_last_msg().await.get_chat_id(); - assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Flobbyfoo"); - let chatlist = Chatlist::try_load(&t, 0, Some("flobbyfoo"), None).await?; - assert_eq!(chatlist.len(), 1); - let contact = Contact::get_by_id(&t, *contact_id).await?; - assert_eq!(contact.get_authname(), "Flobbyfoo"); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_display_name(), "Flobbyfoo"); - assert_eq!(contact.get_name_n_addr(), "Flobbyfoo (f@example.org)"); - let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?; - assert_eq!(contacts.len(), 1); - let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?; - assert_eq!(contacts.len(), 1); - - // third message changes the name - receive_imf( - &t, - b"From: Foo Flobby \n\ - To: alice@example.org\n\ - Subject: foo\n\ - Message-ID: <1234-3@example.org>\n\ - Chat-Version: 1.0\n\ - Date: Sun, 29 May 2022 08:39:57 +0000\n\ - \n\ - hello three\n", - false, - ) - .await?; - let chat_id = t.get_last_msg().await.get_chat_id(); - assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Foo Flobby"); - let chatlist = Chatlist::try_load(&t, 0, Some("Flobbyfoo"), None).await?; - assert_eq!(chatlist.len(), 0); - let chatlist = Chatlist::try_load(&t, 0, Some("Foo Flobby"), None).await?; - assert_eq!(chatlist.len(), 1); - let contact = Contact::get_by_id(&t, *contact_id).await?; - assert_eq!(contact.get_authname(), "Foo Flobby"); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_display_name(), "Foo Flobby"); - assert_eq!(contact.get_name_n_addr(), "Foo Flobby (f@example.org)"); - let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?; - assert_eq!(contacts.len(), 1); - let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?; - assert_eq!(contacts.len(), 0); - let contacts = Contact::get_all(&t, 0, Some("Foo Flobby")).await?; - assert_eq!(contacts.len(), 1); - - // change name manually - let test_id = Contact::create(&t, "Falk", "f@example.org").await?; - assert_eq!(*contact_id, test_id); - assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Falk"); - let chatlist = Chatlist::try_load(&t, 0, Some("Falk"), None).await?; - assert_eq!(chatlist.len(), 1); - let contact = Contact::get_by_id(&t, *contact_id).await?; - assert_eq!(contact.get_authname(), "Foo Flobby"); - assert_eq!(contact.get_name(), "Falk"); - assert_eq!(contact.get_display_name(), "Falk"); - assert_eq!(contact.get_name_n_addr(), "Falk (f@example.org)"); - let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?; - assert_eq!(contacts.len(), 1); - let contacts = Contact::get_all(&t, 0, Some("falk")).await?; - assert_eq!(contacts.len(), 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_delete() -> Result<()> { - let alice = TestContext::new_alice().await; - - assert!(Contact::delete(&alice, ContactId::SELF).await.is_err()); - - // Create Bob contact - let (contact_id, _) = Contact::add_or_lookup( - &alice, - "Bob", - &ContactAddress::new("bob@example.net")?, - Origin::ManuallyCreated, - ) - .await?; - let chat = alice - .create_chat_with_contact("Bob", "bob@example.net") - .await; - assert_eq!( - Contact::get_all(&alice, 0, Some("bob@example.net")) - .await? - .len(), - 1 - ); - - // If a contact has ongoing chats, contact is only hidden on deletion - Contact::delete(&alice, contact_id).await?; - let contact = Contact::get_by_id(&alice, contact_id).await?; - assert_eq!(contact.origin, Origin::Hidden); - assert_eq!( - Contact::get_all(&alice, 0, Some("bob@example.net")) - .await? - .len(), - 0 - ); - - // Delete chat. - chat.get_id().delete(&alice).await?; - - // Can delete contact physically now - Contact::delete(&alice, contact_id).await?; - assert!(Contact::get_by_id(&alice, contact_id).await.is_err()); - assert_eq!( - Contact::get_all(&alice, 0, Some("bob@example.net")) - .await? - .len(), - 0 - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_delete_and_recreate_contact() -> Result<()> { - let t = TestContext::new_alice().await; - - // test recreation after physical deletion - let contact_id1 = Contact::create(&t, "Foo", "foo@bar.de").await?; - assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1); - Contact::delete(&t, contact_id1).await?; - assert!(Contact::get_by_id(&t, contact_id1).await.is_err()); - assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0); - let contact_id2 = Contact::create(&t, "Foo", "foo@bar.de").await?; - assert_ne!(contact_id2, contact_id1); - assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1); - - // test recreation after hiding - t.create_chat_with_contact("Foo", "foo@bar.de").await; - Contact::delete(&t, contact_id2).await?; - let contact = Contact::get_by_id(&t, contact_id2).await?; - assert_eq!(contact.origin, Origin::Hidden); - assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0); - - let contact_id3 = Contact::create(&t, "Foo", "foo@bar.de").await?; - let contact = Contact::get_by_id(&t, contact_id3).await?; - assert_eq!(contact.origin, Origin::ManuallyCreated); - assert_eq!(contact_id3, contact_id2); - assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_remote_authnames() { - let t = TestContext::new().await; - - // incoming mail `From: bob1 ` - this should init authname - let (contact_id, sth_modified) = Contact::add_or_lookup( - &t, - "bob1", - &ContactAddress::new("bob@example.org").unwrap(), - Origin::IncomingUnknownFrom, - ) - .await - .unwrap(); - assert!(!contact_id.is_special()); - assert_eq!(sth_modified, Modifier::Created); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_authname(), "bob1"); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_display_name(), "bob1"); - - // incoming mail `From: bob2 ` - this should update authname - let (contact_id, sth_modified) = Contact::add_or_lookup( - &t, - "bob2", - &ContactAddress::new("bob@example.org").unwrap(), - Origin::IncomingUnknownFrom, - ) - .await - .unwrap(); - assert!(!contact_id.is_special()); - assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_authname(), "bob2"); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_display_name(), "bob2"); - - // manually edit name to "bob3" - authname should be still be "bob2" as given in `From:` above - let contact_id = Contact::create(&t, "bob3", "bob@example.org") - .await - .unwrap(); - assert!(!contact_id.is_special()); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_authname(), "bob2"); - assert_eq!(contact.get_name(), "bob3"); - assert_eq!(contact.get_display_name(), "bob3"); - - // incoming mail `From: bob4 ` - this should update authname, manually given name is still "bob3" - let (contact_id, sth_modified) = Contact::add_or_lookup( - &t, - "bob4", - &ContactAddress::new("bob@example.org").unwrap(), - Origin::IncomingUnknownFrom, - ) - .await - .unwrap(); - assert!(!contact_id.is_special()); - assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_authname(), "bob4"); - assert_eq!(contact.get_name(), "bob3"); - assert_eq!(contact.get_display_name(), "bob3"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_remote_authnames_create_empty() { - let t = TestContext::new().await; - - // manually create "claire@example.org" without a given name - let contact_id = Contact::create(&t, "", "claire@example.org").await.unwrap(); - assert!(!contact_id.is_special()); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_authname(), ""); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_display_name(), "claire@example.org"); - - // incoming mail `From: claire1 ` - this should update authname - let (contact_id_same, sth_modified) = Contact::add_or_lookup( - &t, - "claire1", - &ContactAddress::new("claire@example.org").unwrap(), - Origin::IncomingUnknownFrom, - ) - .await - .unwrap(); - assert_eq!(contact_id, contact_id_same); - assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_authname(), "claire1"); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_display_name(), "claire1"); - - // incoming mail `From: claire2 ` - this should update authname - let (contact_id_same, sth_modified) = Contact::add_or_lookup( - &t, - "claire2", - &ContactAddress::new("claire@example.org").unwrap(), - Origin::IncomingUnknownFrom, - ) - .await - .unwrap(); - assert_eq!(contact_id, contact_id_same); - assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_authname(), "claire2"); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_display_name(), "claire2"); - } - - /// Regression test. - /// - /// In the past, "Not Bob" name was stuck until "Bob" changed the name to "Not Bob" and back in - /// the "From:" field or user set the name to empty string manually. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_remote_authnames_update_to() -> Result<()> { - let t = TestContext::new().await; - - // Incoming message from Bob. - let (contact_id, sth_modified) = Contact::add_or_lookup( - &t, - "Bob", - &ContactAddress::new("bob@example.org")?, - Origin::IncomingUnknownFrom, - ) - .await?; - assert_eq!(sth_modified, Modifier::Created); - let contact = Contact::get_by_id(&t, contact_id).await?; - assert_eq!(contact.get_display_name(), "Bob"); - - // Incoming message from someone else with "Not Bob" in the "To:" field. - let (contact_id_same, sth_modified) = Contact::add_or_lookup( - &t, - "Not Bob", - &ContactAddress::new("bob@example.org")?, - Origin::IncomingUnknownTo, - ) - .await?; - assert_eq!(contact_id, contact_id_same); - assert_eq!(sth_modified, Modifier::Modified); - let contact = Contact::get_by_id(&t, contact_id).await?; - assert_eq!(contact.get_display_name(), "Not Bob"); - - // Incoming message from Bob, changing the name back. - let (contact_id_same, sth_modified) = Contact::add_or_lookup( - &t, - "Bob", - &ContactAddress::new("bob@example.org")?, - Origin::IncomingUnknownFrom, - ) - .await?; - assert_eq!(contact_id, contact_id_same); - assert_eq!(sth_modified, Modifier::Modified); // This was None until the bugfix - let contact = Contact::get_by_id(&t, contact_id).await?; - assert_eq!(contact.get_display_name(), "Bob"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_remote_authnames_edit_empty() { - let t = TestContext::new().await; - - // manually create "dave@example.org" - let contact_id = Contact::create(&t, "dave1", "dave@example.org") - .await - .unwrap(); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_authname(), ""); - assert_eq!(contact.get_name(), "dave1"); - assert_eq!(contact.get_display_name(), "dave1"); - - // incoming mail `From: dave2 ` - this should update authname - Contact::add_or_lookup( - &t, - "dave2", - &ContactAddress::new("dave@example.org").unwrap(), - Origin::IncomingUnknownFrom, - ) - .await - .unwrap(); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_authname(), "dave2"); - assert_eq!(contact.get_name(), "dave1"); - assert_eq!(contact.get_display_name(), "dave1"); - - // manually clear the name - Contact::create(&t, "", "dave@example.org").await.unwrap(); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_authname(), "dave2"); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_display_name(), "dave2"); - } - - #[test] - fn test_addr_cmp() { - assert!(addr_cmp("AA@AA.ORG", "aa@aa.ORG")); - assert!(addr_cmp(" aa@aa.ORG ", "AA@AA.ORG")); - assert!(addr_cmp(" mailto:AA@AA.ORG", "Aa@Aa.orG")); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_name_in_address() { - let t = TestContext::new().await; - - let contact_id = Contact::create(&t, "", "").await.unwrap(); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_addr(), "dave@example.org"); - - let contact_id = Contact::create(&t, "", "Mueller, Dave ") - .await - .unwrap(); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_name(), "Mueller, Dave"); - assert_eq!(contact.get_addr(), "dave@example.org"); - - let contact_id = Contact::create(&t, "name1", "name2 ") - .await - .unwrap(); - let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); - assert_eq!(contact.get_name(), "name1"); - assert_eq!(contact.get_addr(), "dave@example.org"); - - assert!(Contact::create(&t, "", "dslk@sadklj.dk>") - .await - .is_err()); - assert!(Contact::create(&t, "", "dskjfdslksadklj.dk").await.is_err()); - assert!(Contact::create(&t, "", "dskjfdslk@sadklj.dk>") - .await - .is_err()); - assert!(Contact::create(&t, "", "dskjf dslk@d.e").await.is_err()); - assert!(Contact::create(&t, "", " Result<()> { - let t = TestContext::new().await; - let contact_id = Contact::create(&t, "name", "name@example.net").await?; - let color1 = Contact::get_by_id(&t, contact_id).await?.get_color(); - assert_eq!(color1, 0xA739FF); - - let t = TestContext::new().await; - let contact_id = Contact::create(&t, "prename name", "name@example.net").await?; - let color2 = Contact::get_by_id(&t, contact_id).await?.get_color(); - assert_eq!(color2, color1); - - let t = TestContext::new().await; - let contact_id = Contact::create(&t, "Name", "nAme@exAmple.NET").await?; - let color3 = Contact::get_by_id(&t, contact_id).await?.get_color(); - assert_eq!(color3, color1); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_contact_get_encrinfo() -> Result<()> { - let alice = TestContext::new_alice().await; - - // Return error for special IDs - let encrinfo = Contact::get_encrinfo(&alice, ContactId::SELF).await; - assert!(encrinfo.is_err()); - let encrinfo = Contact::get_encrinfo(&alice, ContactId::DEVICE).await; - assert!(encrinfo.is_err()); - - let (contact_bob_id, _modified) = Contact::add_or_lookup( - &alice, - "Bob", - &ContactAddress::new("bob@example.net")?, - Origin::ManuallyCreated, - ) - .await?; - - let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?; - assert_eq!(encrinfo, "No encryption"); - let contact = Contact::get_by_id(&alice, contact_bob_id).await?; - assert!(!contact.e2ee_avail(&alice).await?); - - let bob = TestContext::new_bob().await; - let chat_alice = bob - .create_chat_with_contact("Alice", "alice@example.org") - .await; - send_text_msg(&bob, chat_alice.id, "Hello".to_string()).await?; - let msg = bob.pop_sent_msg().await; - alice.recv_msg(&msg).await; - - let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?; - assert_eq!( - encrinfo, - "End-to-end encryption preferred. -Fingerprints: - -alice@example.org: -2E6F A2CB 23B5 32D7 2863 -4B58 64B0 8F61 A9ED 9443 - -bob@example.net: -CCCB 5AA9 F6E1 141C 9431 -65F1 DB18 B18C BCF7 0487" - ); - let contact = Contact::get_by_id(&alice, contact_bob_id).await?; - assert!(contact.e2ee_avail(&alice).await?); - Ok(()) - } - - /// Tests that status is synchronized when sending encrypted BCC-self messages and not - /// synchronized when the message is not encrypted. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_synchronize_status() -> Result<()> { - // Alice has two devices. - let alice1 = TestContext::new_alice().await; - let alice2 = TestContext::new_alice().await; - - // Bob has one device. - let bob = TestContext::new_bob().await; - - let default_status = alice1.get_config(Config::Selfstatus).await?; - - alice1 - .set_config(Config::Selfstatus, Some("New status")) - .await?; - let chat = alice1 - .create_chat_with_contact("Bob", "bob@example.net") - .await; - - // Alice sends a message to Bob from the first device. - send_text_msg(&alice1, chat.id, "Hello".to_string()).await?; - let sent_msg = alice1.pop_sent_msg().await; - - // Message is not encrypted. - let message = sent_msg.load_from_db().await; - assert!(!message.get_showpadlock()); - - // Alice's second devices receives a copy of outgoing message. - alice2.recv_msg(&sent_msg).await; - - // Bob receives message. - bob.recv_msg(&sent_msg).await; - - // Message was not encrypted, so status is not copied. - assert_eq!(alice2.get_config(Config::Selfstatus).await?, default_status); - - // Bob replies. - let chat = bob - .create_chat_with_contact("Alice", "alice@example.org") - .await; - - send_text_msg(&bob, chat.id, "Reply".to_string()).await?; - let sent_msg = bob.pop_sent_msg().await; - alice1.recv_msg(&sent_msg).await; - alice2.recv_msg(&sent_msg).await; - - // Alice sends second message. - send_text_msg(&alice1, chat.id, "Hello".to_string()).await?; - let sent_msg = alice1.pop_sent_msg().await; - - // Second message is encrypted. - let message = sent_msg.load_from_db().await; - assert!(message.get_showpadlock()); - - // Alice's second devices receives a copy of second outgoing message. - alice2.recv_msg(&sent_msg).await; - - assert_eq!( - alice2.get_config(Config::Selfstatus).await?, - Some("New status".to_string()) - ); - - Ok(()) - } - - /// Tests that DC_EVENT_SELFAVATAR_CHANGED is emitted on avatar changes. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_selfavatar_changed_event() -> Result<()> { - // Alice has two devices. - let alice1 = TestContext::new_alice().await; - let alice2 = TestContext::new_alice().await; - - // Bob has one device. - let bob = TestContext::new_bob().await; - - assert_eq!(alice1.get_config(Config::Selfavatar).await?, None); - - let avatar_src = alice1.get_blobdir().join("avatar.png"); - tokio::fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES).await?; - - alice1 - .set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) - .await?; - - alice1 - .evtracker - .get_matching(|e| matches!(e, EventType::SelfavatarChanged)) - .await; - - // Bob sends a message so that Alice can encrypt to him. - let chat = bob - .create_chat_with_contact("Alice", "alice@example.org") - .await; - - send_text_msg(&bob, chat.id, "Reply".to_string()).await?; - let sent_msg = bob.pop_sent_msg().await; - alice1.recv_msg(&sent_msg).await; - alice2.recv_msg(&sent_msg).await; - - // Alice sends a message. - let alice1_chat_id = alice1.get_last_msg().await.chat_id; - alice1_chat_id.accept(&alice1).await?; - send_text_msg(&alice1, alice1_chat_id, "Hello".to_string()).await?; - let sent_msg = alice1.pop_sent_msg().await; - - // The message is encrypted. - let message = sent_msg.load_from_db().await; - assert!(message.get_showpadlock()); - - // Alice's second device receives a copy of the outgoing message. - alice2.recv_msg(&sent_msg).await; - - // Alice's second device applies the selfavatar. - assert!(alice2.get_config(Config::Selfavatar).await?.is_some()); - alice2 - .evtracker - .get_matching(|e| matches!(e, EventType::SelfavatarChanged)) - .await; - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_last_seen() -> Result<()> { - let alice = TestContext::new_alice().await; - - let (contact_id, _) = Contact::add_or_lookup( - &alice, - "Bob", - &ContactAddress::new("bob@example.net")?, - Origin::ManuallyCreated, - ) - .await?; - let contact = Contact::get_by_id(&alice, contact_id).await?; - assert_eq!(contact.last_seen(), 0); - - let mime = br#"Subject: Hello -Message-ID: message@example.net -To: Alice -From: Bob -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Chat-Version: 1.0 -Date: Sun, 22 Mar 2020 22:37:55 +0000 - -Hi."#; - receive_imf(&alice, mime, false).await?; - let msg = alice.get_last_msg().await; - - let timestamp = msg.get_timestamp(); - assert!(timestamp > 0); - let contact = Contact::get_by_id(&alice, contact_id).await?; - assert_eq!(contact.last_seen(), timestamp); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_was_seen_recently() -> Result<()> { - let _n = TimeShiftFalsePositiveNote; - - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - let chat = alice.create_chat(&bob).await; - let sent_msg = alice.send_text(chat.id, "moin").await; - - let chat = bob.create_chat(&alice).await; - let contacts = chat::get_chat_contacts(&bob, chat.id).await?; - let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?; - assert!(!contact.was_seen_recently()); - - bob.recv_msg(&sent_msg).await; - let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?; - - assert!(contact.was_seen_recently()); - - let self_contact = Contact::get_by_id(&bob, ContactId::SELF).await?; - assert!(!self_contact.was_seen_recently()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_was_seen_recently_event() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - let recently_seen_loop = RecentlySeenLoop::new(bob.ctx.clone()); - let chat = bob.create_chat(&alice).await; - let contacts = chat::get_chat_contacts(&bob, chat.id).await?; - - for _ in 0..2 { - let chat = alice.create_chat(&bob).await; - let sent_msg = alice.send_text(chat.id, "moin").await; - let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?; - assert!(!contact.was_seen_recently()); - bob.evtracker.clear_events(); - bob.recv_msg(&sent_msg).await; - let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?; - assert!(contact.was_seen_recently()); - bob.evtracker - .get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. })) - .await; - recently_seen_loop - .interrupt(contact.id, contact.last_seen) - .await; - - // Wait for `was_seen_recently()` to turn off. - bob.evtracker.clear_events(); - SystemTime::shift(Duration::from_secs(SEEN_RECENTLY_SECONDS as u64 * 2)); - recently_seen_loop.interrupt(ContactId::UNDEFINED, 0).await; - let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?; - assert!(!contact.was_seen_recently()); - bob.evtracker - .get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. })) - .await; - } - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_verified_by_none() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - let contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?; - let contact = Contact::get_by_id(&alice, contact_id).await?; - assert!(contact.get_verifier_id(&alice).await?.is_none()); - - // Receive a message from Bob to create a peerstate. - let chat = bob.create_chat(&alice).await; - let sent_msg = bob.send_text(chat.id, "moin").await; - alice.recv_msg(&sent_msg).await; - - let contact = Contact::get_by_id(&alice, contact_id).await?; - assert!(contact.get_verifier_id(&alice).await?.is_none()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_sync_create() -> Result<()> { - let alice0 = &TestContext::new_alice().await; - let alice1 = &TestContext::new_alice().await; - for a in [alice0, alice1] { - a.set_config_bool(Config::SyncMsgs, true).await?; - } - - Contact::create(alice0, "Bob", "bob@example.net").await?; - test_utils::sync(alice0, alice1).await; - let a1b_contact_id = - Contact::lookup_id_by_addr(alice1, "bob@example.net", Origin::ManuallyCreated) - .await? - .unwrap(); - let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?; - assert_eq!(a1b_contact.name, "Bob"); - - Contact::create(alice0, "Bob Renamed", "bob@example.net").await?; - test_utils::sync(alice0, alice1).await; - let id = Contact::lookup_id_by_addr(alice1, "bob@example.net", Origin::ManuallyCreated) - .await? - .unwrap(); - assert_eq!(id, a1b_contact_id); - let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?; - assert_eq!(a1b_contact.name, "Bob Renamed"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_make_n_import_vcard() -> Result<()> { - let alice = &TestContext::new_alice().await; - let bob = &TestContext::new_bob().await; - bob.set_config(Config::Displayname, Some("Bob")).await?; - let avatar_path = bob.dir.path().join("avatar.png"); - let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png"); - let avatar_base64 = base64::engine::general_purpose::STANDARD.encode(avatar_bytes); - tokio::fs::write(&avatar_path, avatar_bytes).await?; - bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap())) - .await?; - let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); - let chat = bob.create_chat(alice).await; - let sent_msg = bob.send_text(chat.id, "moin").await; - alice.recv_msg(&sent_msg).await; - let bob_id = Contact::create(alice, "Some Bob", &bob_addr).await?; - let key_base64 = Peerstate::from_addr(alice, &bob_addr) - .await? - .unwrap() - .peek_key(false) - .unwrap() - .to_base64(); - let fiona_id = Contact::create(alice, "Fiona", "fiona@example.net").await?; - - assert_eq!(make_vcard(alice, &[]).await?, "".to_string()); - - let t0 = time(); - let vcard = make_vcard(alice, &[bob_id, fiona_id]).await?; - let t1 = time(); - // Just test that it's parsed as expected, `deltachat_contact_tools` crate has tests on the - // exact format. - let contacts = contact_tools::parse_vcard(&vcard); - assert_eq!(contacts.len(), 2); - assert_eq!(contacts[0].addr, bob_addr); - assert_eq!(contacts[0].authname, "Bob".to_string()); - assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64); - assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64); - let timestamp = *contacts[0].timestamp.as_ref().unwrap(); - assert!(t0 <= timestamp && timestamp <= t1); - assert_eq!(contacts[1].addr, "fiona@example.net".to_string()); - assert_eq!(contacts[1].authname, "".to_string()); - assert_eq!(contacts[1].key, None); - assert_eq!(contacts[1].profile_image, None); - let timestamp = *contacts[1].timestamp.as_ref().unwrap(); - assert!(t0 <= timestamp && timestamp <= t1); - - let alice = &TestContext::new_alice().await; - alice.evtracker.clear_events(); - let contact_ids = import_vcard(alice, &vcard).await?; - assert_eq!(contact_ids.len(), 2); - for _ in 0..contact_ids.len() { - alice - .evtracker - .get_matching(|evt| matches!(evt, EventType::ContactsChanged(Some(_)))) - .await; - } - - let vcard = make_vcard(alice, &[contact_ids[0], contact_ids[1]]).await?; - // This should be the same vCard except timestamps, check that roughly. - let contacts = contact_tools::parse_vcard(&vcard); - assert_eq!(contacts.len(), 2); - assert_eq!(contacts[0].addr, bob_addr); - assert_eq!(contacts[0].authname, "Bob".to_string()); - assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64); - assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64); - assert!(contacts[0].timestamp.is_ok()); - assert_eq!(contacts[1].addr, "fiona@example.net".to_string()); - - let chat_id = ChatId::create_for_contact(alice, contact_ids[0]).await?; - let sent_msg = alice.send_text(chat_id, "moin").await; - let msg = bob.recv_msg(&sent_msg).await; - assert!(msg.get_showpadlock()); - - // Bob only actually imports Fiona, though `ContactId::SELF` is also returned. - bob.evtracker.clear_events(); - let contact_ids = import_vcard(bob, &vcard).await?; - bob.emit_event(EventType::Test); - assert_eq!(contact_ids.len(), 2); - assert_eq!(contact_ids[0], ContactId::SELF); - let ev = bob - .evtracker - .get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. })) - .await; - assert_eq!(ev, EventType::ContactsChanged(Some(contact_ids[1]))); - let ev = bob - .evtracker - .get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. } | EventType::Test)) - .await; - assert_eq!(ev, EventType::Test); - let vcard = make_vcard(bob, &[contact_ids[1]]).await?; - let contacts = contact_tools::parse_vcard(&vcard); - assert_eq!(contacts.len(), 1); - assert_eq!(contacts[0].addr, "fiona@example.net"); - assert_eq!(contacts[0].authname, "".to_string()); - assert_eq!(contacts[0].key, None); - assert_eq!(contacts[0].profile_image, None); - assert!(contacts[0].timestamp.is_ok()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_import_vcard_updates_only_key() -> Result<()> { - let alice = &TestContext::new_alice().await; - let bob = &TestContext::new_bob().await; - let bob_addr = &bob.get_config(Config::Addr).await?.unwrap(); - bob.set_config(Config::Displayname, Some("Bob")).await?; - let vcard = make_vcard(bob, &[ContactId::SELF]).await?; - alice.evtracker.clear_events(); - let alice_bob_id = import_vcard(alice, &vcard).await?[0]; - let ev = alice - .evtracker - .get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. })) - .await; - assert_eq!(ev, EventType::ContactsChanged(Some(alice_bob_id))); - let chat_id = ChatId::create_for_contact(alice, alice_bob_id).await?; - let sent_msg = alice.send_text(chat_id, "moin").await; - let msg = bob.recv_msg(&sent_msg).await; - assert!(msg.get_showpadlock()); - - let bob = &TestContext::new().await; - bob.configure_addr(bob_addr).await; - bob.set_config(Config::Displayname, Some("Not Bob")).await?; - let avatar_path = bob.dir.path().join("avatar.png"); - let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png"); - tokio::fs::write(&avatar_path, avatar_bytes).await?; - bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap())) - .await?; - SystemTime::shift(Duration::from_secs(1)); - let vcard1 = make_vcard(bob, &[ContactId::SELF]).await?; - assert_eq!(import_vcard(alice, &vcard1).await?, vec![alice_bob_id]); - let alice_bob_contact = Contact::get_by_id(alice, alice_bob_id).await?; - assert_eq!(alice_bob_contact.get_authname(), "Bob"); - assert_eq!(alice_bob_contact.get_profile_image(alice).await?, None); - let msg = alice.get_last_msg_in(chat_id).await; - assert!(msg.is_info()); - assert_eq!( - msg.get_text(), - stock_str::contact_setup_changed(alice, bob_addr).await - ); - let sent_msg = alice.send_text(chat_id, "moin").await; - let msg = bob.recv_msg(&sent_msg).await; - assert!(msg.get_showpadlock()); - - // The old vCard is imported, but doesn't change Bob's key for Alice. - import_vcard(alice, &vcard).await?.first().unwrap(); - let sent_msg = alice.send_text(chat_id, "moin").await; - let msg = bob.recv_msg(&sent_msg).await; - assert!(msg.get_showpadlock()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_reset_encryption() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - let msg = tcm.send_recv_accept(alice, bob, "Hello!").await; - assert_eq!(msg.get_showpadlock(), false); - - let msg = tcm.send_recv(bob, alice, "Hi!").await; - assert_eq!(msg.get_showpadlock(), true); - let alice_bob_contact_id = msg.from_id; - - alice_bob_contact_id.reset_encryption(alice).await?; - - let msg = tcm.send_recv(alice, bob, "Unencrypted").await; - assert_eq!(msg.get_showpadlock(), false); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_reset_verified_encryption() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - tcm.execute_securejoin(bob, alice).await; - - let msg = tcm.send_recv(bob, alice, "Encrypted").await; - assert_eq!(msg.get_showpadlock(), true); - - let alice_bob_chat_id = msg.chat_id; - let alice_bob_contact_id = msg.from_id; - alice_bob_contact_id.reset_encryption(alice).await?; - - // Check that the contact is still verified after resetting encryption. - let alice_bob_contact = Contact::get_by_id(alice, alice_bob_contact_id).await?; - assert_eq!(alice_bob_contact.is_verified(alice).await?, true); - - // 1:1 chat and profile is no longer verified. - assert_eq!(alice_bob_contact.is_profile_verified(alice).await?, false); - - let info_msg = alice.get_last_msg_in(alice_bob_chat_id).await; - assert_eq!( - info_msg.text, - "bob@example.net sent a message from another device." - ); - - let msg = tcm.send_recv(alice, bob, "Unencrypted").await; - assert_eq!(msg.get_showpadlock(), false); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_self_is_verified() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - - let contact = Contact::get_by_id(&alice, ContactId::SELF).await?; - assert_eq!(contact.is_verified(&alice).await?, true); - assert!(contact.is_profile_verified(&alice).await?); - assert!(contact.get_verifier_id(&alice).await?.is_none()); - - let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?; - assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected); - - Ok(()) - } -} +mod contact_tests; diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs new file mode 100644 index 000000000..3691d5752 --- /dev/null +++ b/src/contact/contact_tests.rs @@ -0,0 +1,1273 @@ +use deltachat_contact_tools::may_be_valid_addr; + +use super::*; +use crate::chat::{get_chat_contacts, send_text_msg, Chat}; +use crate::chatlist::Chatlist; +use crate::receive_imf::receive_imf; +use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote}; + +#[test] +fn test_contact_id_values() { + // Some FFI users need to have the values of these fixed, how naughty. But let's + // make sure we don't modify them anyway. + assert_eq!(ContactId::UNDEFINED.to_u32(), 0); + assert_eq!(ContactId::SELF.to_u32(), 1); + assert_eq!(ContactId::INFO.to_u32(), 2); + assert_eq!(ContactId::DEVICE.to_u32(), 5); + assert_eq!(ContactId::LAST_SPECIAL.to_u32(), 9); +} + +#[test] +fn test_may_be_valid_addr() { + assert_eq!(may_be_valid_addr(""), false); + assert_eq!(may_be_valid_addr("user@domain.tld"), true); + assert_eq!(may_be_valid_addr("uuu"), false); + assert_eq!(may_be_valid_addr("dd.tt"), false); + assert_eq!(may_be_valid_addr("tt.dd@uu"), true); + assert_eq!(may_be_valid_addr("u@d"), true); + assert_eq!(may_be_valid_addr("u@d."), false); + assert_eq!(may_be_valid_addr("u@d.t"), true); + assert_eq!(may_be_valid_addr("u@d.tt"), true); + assert_eq!(may_be_valid_addr("u@.tt"), true); + assert_eq!(may_be_valid_addr("@d.tt"), false); + assert_eq!(may_be_valid_addr(""), false); + assert_eq!(may_be_valid_addr("as@sd.de>"), false); + assert_eq!(may_be_valid_addr("ask dkl@dd.tt"), false); + assert_eq!(may_be_valid_addr("user@domain.tld."), false); +} + +#[test] +fn test_normalize_addr() { + assert_eq!(addr_normalize("mailto:john@doe.com"), "john@doe.com"); + assert_eq!(addr_normalize(" hello@world.com "), "hello@world.com"); + assert_eq!(addr_normalize("John@Doe.com"), "john@doe.com"); +} + +#[test] +fn test_split_address_book() { + let book = "Name one\nAddress one\nName two\nAddress two\nrest name"; + let list = split_address_book(book); + assert_eq!( + list, + vec![("Name one", "Address one"), ("Name two", "Address two")] + ) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_contacts() -> Result<()> { + let context = TestContext::new().await; + + assert!(context.get_all_self_addrs().await?.is_empty()); + + // Bob is not in the contacts yet. + let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?; + assert_eq!(contacts.len(), 0); + + let (id, _modified) = Contact::add_or_lookup( + &context.ctx, + "bob", + &ContactAddress::new("user@example.org")?, + Origin::IncomingReplyTo, + ) + .await?; + assert_ne!(id, ContactId::UNDEFINED); + + let contact = Contact::get_by_id(&context.ctx, id).await.unwrap(); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_authname(), "bob"); + assert_eq!(contact.get_display_name(), "bob"); + + // Search by name. + let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?; + assert_eq!(contacts.len(), 1); + assert_eq!(contacts.first(), Some(&id)); + + // Search by address. + let contacts = Contact::get_all(&context.ctx, 0, Some("user")).await?; + assert_eq!(contacts.len(), 1); + assert_eq!(contacts.first(), Some(&id)); + + let contacts = Contact::get_all(&context.ctx, 0, Some("alice")).await?; + assert_eq!(contacts.len(), 0); + + // Set Bob name to "someone" manually. + let (contact_bob_id, modified) = Contact::add_or_lookup( + &context.ctx, + "someone", + &ContactAddress::new("user@example.org")?, + Origin::ManuallyCreated, + ) + .await?; + assert_eq!(contact_bob_id, id); + assert_eq!(modified, Modifier::Modified); + let contact = Contact::get_by_id(&context.ctx, id).await.unwrap(); + assert_eq!(contact.get_name(), "someone"); + assert_eq!(contact.get_authname(), "bob"); + assert_eq!(contact.get_display_name(), "someone"); + + // Not searchable by authname, because it is not displayed. + let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?; + assert_eq!(contacts.len(), 0); + + // Search by display name (same as manually set name). + let contacts = Contact::get_all(&context.ctx, 0, Some("someone")).await?; + assert_eq!(contacts.len(), 1); + assert_eq!(contacts.first(), Some(&id)); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_is_self_addr() -> Result<()> { + let t = TestContext::new().await; + assert_eq!(t.is_self_addr("me@me.org").await?, false); + + t.configure_addr("you@you.net").await; + assert_eq!(t.is_self_addr("me@me.org").await?, false); + assert_eq!(t.is_self_addr("you@you.net").await?, true); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_add_or_lookup() { + // add some contacts, this also tests add_address_book() + let t = TestContext::new().await; + let book = concat!( + " Name one \n one@eins.org \n", + "Name two\ntwo@deux.net\n", + "Invalid\n+1234567890\n", // invalid, should be ignored + "\nthree@drei.sam\n", + "Name two\ntwo@deux.net\n", // should not be added again + "\nWonderland, Alice \n", + ); + assert_eq!(Contact::add_address_book(&t, book).await.unwrap(), 4); + + // check first added contact, this modifies authname because it is empty + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "bla foo", + &ContactAddress::new("one@eins.org").unwrap(), + Origin::IncomingUnknownTo, + ) + .await + .unwrap(); + assert!(!contact_id.is_special()); + assert_eq!(sth_modified, Modifier::Modified); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_id(), contact_id); + assert_eq!(contact.get_name(), "Name one"); + assert_eq!(contact.get_authname(), "bla foo"); + assert_eq!(contact.get_display_name(), "Name one"); + assert_eq!(contact.get_addr(), "one@eins.org"); + assert_eq!(contact.get_name_n_addr(), "Name one (one@eins.org)"); + + // modify first added contact + let (contact_id_test, sth_modified) = Contact::add_or_lookup( + &t, + "Real one", + &ContactAddress::new(" one@eins.org ").unwrap(), + Origin::ManuallyCreated, + ) + .await + .unwrap(); + assert_eq!(contact_id, contact_id_test); + assert_eq!(sth_modified, Modifier::Modified); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_name(), "Real one"); + assert_eq!(contact.get_addr(), "one@eins.org"); + assert!(!contact.is_blocked()); + + // check third added contact (contact without name) + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "", + &ContactAddress::new("three@drei.sam").unwrap(), + Origin::IncomingUnknownTo, + ) + .await + .unwrap(); + assert!(!contact_id.is_special()); + assert_eq!(sth_modified, Modifier::None); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_display_name(), "three@drei.sam"); + assert_eq!(contact.get_addr(), "three@drei.sam"); + assert_eq!(contact.get_name_n_addr(), "three@drei.sam"); + + // add name to third contact from incoming message (this becomes authorized name) + let (contact_id_test, sth_modified) = Contact::add_or_lookup( + &t, + "m. serious", + &ContactAddress::new("three@drei.sam").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); + assert_eq!(contact_id, contact_id_test); + assert_eq!(sth_modified, Modifier::Modified); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_name_n_addr(), "m. serious (three@drei.sam)"); + assert!(!contact.is_blocked()); + + // manually edit name of third contact (does not changed authorized name) + let (contact_id_test, sth_modified) = Contact::add_or_lookup( + &t, + "schnucki", + &ContactAddress::new("three@drei.sam").unwrap(), + Origin::ManuallyCreated, + ) + .await + .unwrap(); + assert_eq!(contact_id, contact_id_test); + assert_eq!(sth_modified, Modifier::Modified); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_authname(), "m. serious"); + assert_eq!(contact.get_name_n_addr(), "schnucki (three@drei.sam)"); + assert!(!contact.is_blocked()); + + // Fourth contact: + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "", + &ContactAddress::new("alice@w.de").unwrap(), + Origin::IncomingUnknownTo, + ) + .await + .unwrap(); + assert!(!contact_id.is_special()); + assert_eq!(sth_modified, Modifier::None); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_name(), "Wonderland, Alice"); + assert_eq!(contact.get_display_name(), "Wonderland, Alice"); + assert_eq!(contact.get_addr(), "alice@w.de"); + assert_eq!(contact.get_name_n_addr(), "Wonderland, Alice (alice@w.de)"); + + // check SELF + let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap(); + assert_eq!(contact.get_name(), stock_str::self_msg(&t).await); + assert_eq!(contact.get_addr(), ""); // we're not configured + assert!(!contact.is_blocked()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_contact_name_changes() -> Result<()> { + let t = TestContext::new_alice().await; + + // first message creates contact and one-to-one-chat without name set + receive_imf( + &t, + b"From: f@example.org\n\ + To: alice@example.org\n\ + Subject: foo\n\ + Message-ID: <1234-1@example.org>\n\ + Chat-Version: 1.0\n\ + Date: Sun, 29 May 2022 08:37:57 +0000\n\ + \n\ + hello one\n", + false, + ) + .await?; + let chat_id = t.get_last_msg().await.get_chat_id(); + chat_id.accept(&t).await?; + assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "f@example.org"); + let chatlist = Chatlist::try_load(&t, 0, Some("f@example.org"), None).await?; + assert_eq!(chatlist.len(), 1); + let contacts = get_chat_contacts(&t, chat_id).await?; + let contact_id = contacts.first().unwrap(); + let contact = Contact::get_by_id(&t, *contact_id).await?; + assert_eq!(contact.get_authname(), ""); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_display_name(), "f@example.org"); + assert_eq!(contact.get_name_n_addr(), "f@example.org"); + let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?; + assert_eq!(contacts.len(), 1); + + // second message inits the name + receive_imf( + &t, + b"From: Flobbyfoo \n\ + To: alice@example.org\n\ + Subject: foo\n\ + Message-ID: <1234-2@example.org>\n\ + Chat-Version: 1.0\n\ + Date: Sun, 29 May 2022 08:38:57 +0000\n\ + \n\ + hello two\n", + false, + ) + .await?; + let chat_id = t.get_last_msg().await.get_chat_id(); + assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Flobbyfoo"); + let chatlist = Chatlist::try_load(&t, 0, Some("flobbyfoo"), None).await?; + assert_eq!(chatlist.len(), 1); + let contact = Contact::get_by_id(&t, *contact_id).await?; + assert_eq!(contact.get_authname(), "Flobbyfoo"); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_display_name(), "Flobbyfoo"); + assert_eq!(contact.get_name_n_addr(), "Flobbyfoo (f@example.org)"); + let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?; + assert_eq!(contacts.len(), 1); + let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?; + assert_eq!(contacts.len(), 1); + + // third message changes the name + receive_imf( + &t, + b"From: Foo Flobby \n\ + To: alice@example.org\n\ + Subject: foo\n\ + Message-ID: <1234-3@example.org>\n\ + Chat-Version: 1.0\n\ + Date: Sun, 29 May 2022 08:39:57 +0000\n\ + \n\ + hello three\n", + false, + ) + .await?; + let chat_id = t.get_last_msg().await.get_chat_id(); + assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Foo Flobby"); + let chatlist = Chatlist::try_load(&t, 0, Some("Flobbyfoo"), None).await?; + assert_eq!(chatlist.len(), 0); + let chatlist = Chatlist::try_load(&t, 0, Some("Foo Flobby"), None).await?; + assert_eq!(chatlist.len(), 1); + let contact = Contact::get_by_id(&t, *contact_id).await?; + assert_eq!(contact.get_authname(), "Foo Flobby"); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_display_name(), "Foo Flobby"); + assert_eq!(contact.get_name_n_addr(), "Foo Flobby (f@example.org)"); + let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?; + assert_eq!(contacts.len(), 1); + let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?; + assert_eq!(contacts.len(), 0); + let contacts = Contact::get_all(&t, 0, Some("Foo Flobby")).await?; + assert_eq!(contacts.len(), 1); + + // change name manually + let test_id = Contact::create(&t, "Falk", "f@example.org").await?; + assert_eq!(*contact_id, test_id); + assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Falk"); + let chatlist = Chatlist::try_load(&t, 0, Some("Falk"), None).await?; + assert_eq!(chatlist.len(), 1); + let contact = Contact::get_by_id(&t, *contact_id).await?; + assert_eq!(contact.get_authname(), "Foo Flobby"); + assert_eq!(contact.get_name(), "Falk"); + assert_eq!(contact.get_display_name(), "Falk"); + assert_eq!(contact.get_name_n_addr(), "Falk (f@example.org)"); + let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?; + assert_eq!(contacts.len(), 1); + let contacts = Contact::get_all(&t, 0, Some("falk")).await?; + assert_eq!(contacts.len(), 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_delete() -> Result<()> { + let alice = TestContext::new_alice().await; + + assert!(Contact::delete(&alice, ContactId::SELF).await.is_err()); + + // Create Bob contact + let (contact_id, _) = Contact::add_or_lookup( + &alice, + "Bob", + &ContactAddress::new("bob@example.net")?, + Origin::ManuallyCreated, + ) + .await?; + let chat = alice + .create_chat_with_contact("Bob", "bob@example.net") + .await; + assert_eq!( + Contact::get_all(&alice, 0, Some("bob@example.net")) + .await? + .len(), + 1 + ); + + // If a contact has ongoing chats, contact is only hidden on deletion + Contact::delete(&alice, contact_id).await?; + let contact = Contact::get_by_id(&alice, contact_id).await?; + assert_eq!(contact.origin, Origin::Hidden); + assert_eq!( + Contact::get_all(&alice, 0, Some("bob@example.net")) + .await? + .len(), + 0 + ); + + // Delete chat. + chat.get_id().delete(&alice).await?; + + // Can delete contact physically now + Contact::delete(&alice, contact_id).await?; + assert!(Contact::get_by_id(&alice, contact_id).await.is_err()); + assert_eq!( + Contact::get_all(&alice, 0, Some("bob@example.net")) + .await? + .len(), + 0 + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_delete_and_recreate_contact() -> Result<()> { + let t = TestContext::new_alice().await; + + // test recreation after physical deletion + let contact_id1 = Contact::create(&t, "Foo", "foo@bar.de").await?; + assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1); + Contact::delete(&t, contact_id1).await?; + assert!(Contact::get_by_id(&t, contact_id1).await.is_err()); + assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0); + let contact_id2 = Contact::create(&t, "Foo", "foo@bar.de").await?; + assert_ne!(contact_id2, contact_id1); + assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1); + + // test recreation after hiding + t.create_chat_with_contact("Foo", "foo@bar.de").await; + Contact::delete(&t, contact_id2).await?; + let contact = Contact::get_by_id(&t, contact_id2).await?; + assert_eq!(contact.origin, Origin::Hidden); + assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0); + + let contact_id3 = Contact::create(&t, "Foo", "foo@bar.de").await?; + let contact = Contact::get_by_id(&t, contact_id3).await?; + assert_eq!(contact.origin, Origin::ManuallyCreated); + assert_eq!(contact_id3, contact_id2); + assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_remote_authnames() { + let t = TestContext::new().await; + + // incoming mail `From: bob1 ` - this should init authname + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "bob1", + &ContactAddress::new("bob@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); + assert!(!contact_id.is_special()); + assert_eq!(sth_modified, Modifier::Created); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_authname(), "bob1"); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_display_name(), "bob1"); + + // incoming mail `From: bob2 ` - this should update authname + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "bob2", + &ContactAddress::new("bob@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); + assert!(!contact_id.is_special()); + assert_eq!(sth_modified, Modifier::Modified); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_authname(), "bob2"); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_display_name(), "bob2"); + + // manually edit name to "bob3" - authname should be still be "bob2" as given in `From:` above + let contact_id = Contact::create(&t, "bob3", "bob@example.org") + .await + .unwrap(); + assert!(!contact_id.is_special()); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_authname(), "bob2"); + assert_eq!(contact.get_name(), "bob3"); + assert_eq!(contact.get_display_name(), "bob3"); + + // incoming mail `From: bob4 ` - this should update authname, manually given name is still "bob3" + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "bob4", + &ContactAddress::new("bob@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); + assert!(!contact_id.is_special()); + assert_eq!(sth_modified, Modifier::Modified); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_authname(), "bob4"); + assert_eq!(contact.get_name(), "bob3"); + assert_eq!(contact.get_display_name(), "bob3"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_remote_authnames_create_empty() { + let t = TestContext::new().await; + + // manually create "claire@example.org" without a given name + let contact_id = Contact::create(&t, "", "claire@example.org").await.unwrap(); + assert!(!contact_id.is_special()); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_authname(), ""); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_display_name(), "claire@example.org"); + + // incoming mail `From: claire1 ` - this should update authname + let (contact_id_same, sth_modified) = Contact::add_or_lookup( + &t, + "claire1", + &ContactAddress::new("claire@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); + assert_eq!(contact_id, contact_id_same); + assert_eq!(sth_modified, Modifier::Modified); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_authname(), "claire1"); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_display_name(), "claire1"); + + // incoming mail `From: claire2 ` - this should update authname + let (contact_id_same, sth_modified) = Contact::add_or_lookup( + &t, + "claire2", + &ContactAddress::new("claire@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); + assert_eq!(contact_id, contact_id_same); + assert_eq!(sth_modified, Modifier::Modified); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_authname(), "claire2"); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_display_name(), "claire2"); +} + +/// Regression test. +/// +/// In the past, "Not Bob" name was stuck until "Bob" changed the name to "Not Bob" and back in +/// the "From:" field or user set the name to empty string manually. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_remote_authnames_update_to() -> Result<()> { + let t = TestContext::new().await; + + // Incoming message from Bob. + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "Bob", + &ContactAddress::new("bob@example.org")?, + Origin::IncomingUnknownFrom, + ) + .await?; + assert_eq!(sth_modified, Modifier::Created); + let contact = Contact::get_by_id(&t, contact_id).await?; + assert_eq!(contact.get_display_name(), "Bob"); + + // Incoming message from someone else with "Not Bob" in the "To:" field. + let (contact_id_same, sth_modified) = Contact::add_or_lookup( + &t, + "Not Bob", + &ContactAddress::new("bob@example.org")?, + Origin::IncomingUnknownTo, + ) + .await?; + assert_eq!(contact_id, contact_id_same); + assert_eq!(sth_modified, Modifier::Modified); + let contact = Contact::get_by_id(&t, contact_id).await?; + assert_eq!(contact.get_display_name(), "Not Bob"); + + // Incoming message from Bob, changing the name back. + let (contact_id_same, sth_modified) = Contact::add_or_lookup( + &t, + "Bob", + &ContactAddress::new("bob@example.org")?, + Origin::IncomingUnknownFrom, + ) + .await?; + assert_eq!(contact_id, contact_id_same); + assert_eq!(sth_modified, Modifier::Modified); // This was None until the bugfix + let contact = Contact::get_by_id(&t, contact_id).await?; + assert_eq!(contact.get_display_name(), "Bob"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_remote_authnames_edit_empty() { + let t = TestContext::new().await; + + // manually create "dave@example.org" + let contact_id = Contact::create(&t, "dave1", "dave@example.org") + .await + .unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_authname(), ""); + assert_eq!(contact.get_name(), "dave1"); + assert_eq!(contact.get_display_name(), "dave1"); + + // incoming mail `From: dave2 ` - this should update authname + Contact::add_or_lookup( + &t, + "dave2", + &ContactAddress::new("dave@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_authname(), "dave2"); + assert_eq!(contact.get_name(), "dave1"); + assert_eq!(contact.get_display_name(), "dave1"); + + // manually clear the name + Contact::create(&t, "", "dave@example.org").await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_authname(), "dave2"); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_display_name(), "dave2"); +} + +#[test] +fn test_addr_cmp() { + assert!(addr_cmp("AA@AA.ORG", "aa@aa.ORG")); + assert!(addr_cmp(" aa@aa.ORG ", "AA@AA.ORG")); + assert!(addr_cmp(" mailto:AA@AA.ORG", "Aa@Aa.orG")); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_name_in_address() { + let t = TestContext::new().await; + + let contact_id = Contact::create(&t, "", "").await.unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_addr(), "dave@example.org"); + + let contact_id = Contact::create(&t, "", "Mueller, Dave ") + .await + .unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_name(), "Mueller, Dave"); + assert_eq!(contact.get_addr(), "dave@example.org"); + + let contact_id = Contact::create(&t, "name1", "name2 ") + .await + .unwrap(); + let contact = Contact::get_by_id(&t, contact_id).await.unwrap(); + assert_eq!(contact.get_name(), "name1"); + assert_eq!(contact.get_addr(), "dave@example.org"); + + assert!(Contact::create(&t, "", "dslk@sadklj.dk>") + .await + .is_err()); + assert!(Contact::create(&t, "", "dskjfdslksadklj.dk").await.is_err()); + assert!(Contact::create(&t, "", "dskjfdslk@sadklj.dk>") + .await + .is_err()); + assert!(Contact::create(&t, "", "dskjf dslk@d.e").await.is_err()); + assert!(Contact::create(&t, "", " Result<()> { + let t = TestContext::new().await; + let contact_id = Contact::create(&t, "name", "name@example.net").await?; + let color1 = Contact::get_by_id(&t, contact_id).await?.get_color(); + assert_eq!(color1, 0xA739FF); + + let t = TestContext::new().await; + let contact_id = Contact::create(&t, "prename name", "name@example.net").await?; + let color2 = Contact::get_by_id(&t, contact_id).await?.get_color(); + assert_eq!(color2, color1); + + let t = TestContext::new().await; + let contact_id = Contact::create(&t, "Name", "nAme@exAmple.NET").await?; + let color3 = Contact::get_by_id(&t, contact_id).await?.get_color(); + assert_eq!(color3, color1); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_contact_get_encrinfo() -> Result<()> { + let alice = TestContext::new_alice().await; + + // Return error for special IDs + let encrinfo = Contact::get_encrinfo(&alice, ContactId::SELF).await; + assert!(encrinfo.is_err()); + let encrinfo = Contact::get_encrinfo(&alice, ContactId::DEVICE).await; + assert!(encrinfo.is_err()); + + let (contact_bob_id, _modified) = Contact::add_or_lookup( + &alice, + "Bob", + &ContactAddress::new("bob@example.net")?, + Origin::ManuallyCreated, + ) + .await?; + + let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?; + assert_eq!(encrinfo, "No encryption"); + let contact = Contact::get_by_id(&alice, contact_bob_id).await?; + assert!(!contact.e2ee_avail(&alice).await?); + + let bob = TestContext::new_bob().await; + let chat_alice = bob + .create_chat_with_contact("Alice", "alice@example.org") + .await; + send_text_msg(&bob, chat_alice.id, "Hello".to_string()).await?; + let msg = bob.pop_sent_msg().await; + alice.recv_msg(&msg).await; + + let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?; + assert_eq!( + encrinfo, + "End-to-end encryption preferred. +Fingerprints: + +alice@example.org: +2E6F A2CB 23B5 32D7 2863 +4B58 64B0 8F61 A9ED 9443 + +bob@example.net: +CCCB 5AA9 F6E1 141C 9431 +65F1 DB18 B18C BCF7 0487" + ); + let contact = Contact::get_by_id(&alice, contact_bob_id).await?; + assert!(contact.e2ee_avail(&alice).await?); + Ok(()) +} + +/// Tests that status is synchronized when sending encrypted BCC-self messages and not +/// synchronized when the message is not encrypted. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_synchronize_status() -> Result<()> { + // Alice has two devices. + let alice1 = TestContext::new_alice().await; + let alice2 = TestContext::new_alice().await; + + // Bob has one device. + let bob = TestContext::new_bob().await; + + let default_status = alice1.get_config(Config::Selfstatus).await?; + + alice1 + .set_config(Config::Selfstatus, Some("New status")) + .await?; + let chat = alice1 + .create_chat_with_contact("Bob", "bob@example.net") + .await; + + // Alice sends a message to Bob from the first device. + send_text_msg(&alice1, chat.id, "Hello".to_string()).await?; + let sent_msg = alice1.pop_sent_msg().await; + + // Message is not encrypted. + let message = sent_msg.load_from_db().await; + assert!(!message.get_showpadlock()); + + // Alice's second devices receives a copy of outgoing message. + alice2.recv_msg(&sent_msg).await; + + // Bob receives message. + bob.recv_msg(&sent_msg).await; + + // Message was not encrypted, so status is not copied. + assert_eq!(alice2.get_config(Config::Selfstatus).await?, default_status); + + // Bob replies. + let chat = bob + .create_chat_with_contact("Alice", "alice@example.org") + .await; + + send_text_msg(&bob, chat.id, "Reply".to_string()).await?; + let sent_msg = bob.pop_sent_msg().await; + alice1.recv_msg(&sent_msg).await; + alice2.recv_msg(&sent_msg).await; + + // Alice sends second message. + send_text_msg(&alice1, chat.id, "Hello".to_string()).await?; + let sent_msg = alice1.pop_sent_msg().await; + + // Second message is encrypted. + let message = sent_msg.load_from_db().await; + assert!(message.get_showpadlock()); + + // Alice's second devices receives a copy of second outgoing message. + alice2.recv_msg(&sent_msg).await; + + assert_eq!( + alice2.get_config(Config::Selfstatus).await?, + Some("New status".to_string()) + ); + + Ok(()) +} + +/// Tests that DC_EVENT_SELFAVATAR_CHANGED is emitted on avatar changes. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_selfavatar_changed_event() -> Result<()> { + // Alice has two devices. + let alice1 = TestContext::new_alice().await; + let alice2 = TestContext::new_alice().await; + + // Bob has one device. + let bob = TestContext::new_bob().await; + + assert_eq!(alice1.get_config(Config::Selfavatar).await?, None); + + let avatar_src = alice1.get_blobdir().join("avatar.png"); + tokio::fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES).await?; + + alice1 + .set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) + .await?; + + alice1 + .evtracker + .get_matching(|e| matches!(e, EventType::SelfavatarChanged)) + .await; + + // Bob sends a message so that Alice can encrypt to him. + let chat = bob + .create_chat_with_contact("Alice", "alice@example.org") + .await; + + send_text_msg(&bob, chat.id, "Reply".to_string()).await?; + let sent_msg = bob.pop_sent_msg().await; + alice1.recv_msg(&sent_msg).await; + alice2.recv_msg(&sent_msg).await; + + // Alice sends a message. + let alice1_chat_id = alice1.get_last_msg().await.chat_id; + alice1_chat_id.accept(&alice1).await?; + send_text_msg(&alice1, alice1_chat_id, "Hello".to_string()).await?; + let sent_msg = alice1.pop_sent_msg().await; + + // The message is encrypted. + let message = sent_msg.load_from_db().await; + assert!(message.get_showpadlock()); + + // Alice's second device receives a copy of the outgoing message. + alice2.recv_msg(&sent_msg).await; + + // Alice's second device applies the selfavatar. + assert!(alice2.get_config(Config::Selfavatar).await?.is_some()); + alice2 + .evtracker + .get_matching(|e| matches!(e, EventType::SelfavatarChanged)) + .await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_last_seen() -> Result<()> { + let alice = TestContext::new_alice().await; + + let (contact_id, _) = Contact::add_or_lookup( + &alice, + "Bob", + &ContactAddress::new("bob@example.net")?, + Origin::ManuallyCreated, + ) + .await?; + let contact = Contact::get_by_id(&alice, contact_id).await?; + assert_eq!(contact.last_seen(), 0); + + let mime = br#"Subject: Hello +Message-ID: message@example.net +To: Alice +From: Bob +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no +Chat-Version: 1.0 +Date: Sun, 22 Mar 2020 22:37:55 +0000 + +Hi."#; + receive_imf(&alice, mime, false).await?; + let msg = alice.get_last_msg().await; + + let timestamp = msg.get_timestamp(); + assert!(timestamp > 0); + let contact = Contact::get_by_id(&alice, contact_id).await?; + assert_eq!(contact.last_seen(), timestamp); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_was_seen_recently() -> Result<()> { + let _n = TimeShiftFalsePositiveNote; + + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + let chat = alice.create_chat(&bob).await; + let sent_msg = alice.send_text(chat.id, "moin").await; + + let chat = bob.create_chat(&alice).await; + let contacts = chat::get_chat_contacts(&bob, chat.id).await?; + let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?; + assert!(!contact.was_seen_recently()); + + bob.recv_msg(&sent_msg).await; + let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?; + + assert!(contact.was_seen_recently()); + + let self_contact = Contact::get_by_id(&bob, ContactId::SELF).await?; + assert!(!self_contact.was_seen_recently()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_was_seen_recently_event() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let recently_seen_loop = RecentlySeenLoop::new(bob.ctx.clone()); + let chat = bob.create_chat(&alice).await; + let contacts = chat::get_chat_contacts(&bob, chat.id).await?; + + for _ in 0..2 { + let chat = alice.create_chat(&bob).await; + let sent_msg = alice.send_text(chat.id, "moin").await; + let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?; + assert!(!contact.was_seen_recently()); + bob.evtracker.clear_events(); + bob.recv_msg(&sent_msg).await; + let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?; + assert!(contact.was_seen_recently()); + bob.evtracker + .get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. })) + .await; + recently_seen_loop + .interrupt(contact.id, contact.last_seen) + .await; + + // Wait for `was_seen_recently()` to turn off. + bob.evtracker.clear_events(); + SystemTime::shift(Duration::from_secs(SEEN_RECENTLY_SECONDS as u64 * 2)); + recently_seen_loop.interrupt(ContactId::UNDEFINED, 0).await; + let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?; + assert!(!contact.was_seen_recently()); + bob.evtracker + .get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. })) + .await; + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_verified_by_none() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + let contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?; + let contact = Contact::get_by_id(&alice, contact_id).await?; + assert!(contact.get_verifier_id(&alice).await?.is_none()); + + // Receive a message from Bob to create a peerstate. + let chat = bob.create_chat(&alice).await; + let sent_msg = bob.send_text(chat.id, "moin").await; + alice.recv_msg(&sent_msg).await; + + let contact = Contact::get_by_id(&alice, contact_id).await?; + assert!(contact.get_verifier_id(&alice).await?.is_none()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync_create() -> Result<()> { + let alice0 = &TestContext::new_alice().await; + let alice1 = &TestContext::new_alice().await; + for a in [alice0, alice1] { + a.set_config_bool(Config::SyncMsgs, true).await?; + } + + Contact::create(alice0, "Bob", "bob@example.net").await?; + test_utils::sync(alice0, alice1).await; + let a1b_contact_id = + Contact::lookup_id_by_addr(alice1, "bob@example.net", Origin::ManuallyCreated) + .await? + .unwrap(); + let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?; + assert_eq!(a1b_contact.name, "Bob"); + + Contact::create(alice0, "Bob Renamed", "bob@example.net").await?; + test_utils::sync(alice0, alice1).await; + let id = Contact::lookup_id_by_addr(alice1, "bob@example.net", Origin::ManuallyCreated) + .await? + .unwrap(); + assert_eq!(id, a1b_contact_id); + let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?; + assert_eq!(a1b_contact.name, "Bob Renamed"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_make_n_import_vcard() -> Result<()> { + let alice = &TestContext::new_alice().await; + let bob = &TestContext::new_bob().await; + bob.set_config(Config::Displayname, Some("Bob")).await?; + let avatar_path = bob.dir.path().join("avatar.png"); + let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + let avatar_base64 = base64::engine::general_purpose::STANDARD.encode(avatar_bytes); + tokio::fs::write(&avatar_path, avatar_bytes).await?; + bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap())) + .await?; + let bob_addr = bob.get_config(Config::Addr).await?.unwrap(); + let chat = bob.create_chat(alice).await; + let sent_msg = bob.send_text(chat.id, "moin").await; + alice.recv_msg(&sent_msg).await; + let bob_id = Contact::create(alice, "Some Bob", &bob_addr).await?; + let key_base64 = Peerstate::from_addr(alice, &bob_addr) + .await? + .unwrap() + .peek_key(false) + .unwrap() + .to_base64(); + let fiona_id = Contact::create(alice, "Fiona", "fiona@example.net").await?; + + assert_eq!(make_vcard(alice, &[]).await?, "".to_string()); + + let t0 = time(); + let vcard = make_vcard(alice, &[bob_id, fiona_id]).await?; + let t1 = time(); + // Just test that it's parsed as expected, `deltachat_contact_tools` crate has tests on the + // exact format. + let contacts = contact_tools::parse_vcard(&vcard); + assert_eq!(contacts.len(), 2); + assert_eq!(contacts[0].addr, bob_addr); + assert_eq!(contacts[0].authname, "Bob".to_string()); + assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64); + assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64); + let timestamp = *contacts[0].timestamp.as_ref().unwrap(); + assert!(t0 <= timestamp && timestamp <= t1); + assert_eq!(contacts[1].addr, "fiona@example.net".to_string()); + assert_eq!(contacts[1].authname, "".to_string()); + assert_eq!(contacts[1].key, None); + assert_eq!(contacts[1].profile_image, None); + let timestamp = *contacts[1].timestamp.as_ref().unwrap(); + assert!(t0 <= timestamp && timestamp <= t1); + + let alice = &TestContext::new_alice().await; + alice.evtracker.clear_events(); + let contact_ids = import_vcard(alice, &vcard).await?; + assert_eq!(contact_ids.len(), 2); + for _ in 0..contact_ids.len() { + alice + .evtracker + .get_matching(|evt| matches!(evt, EventType::ContactsChanged(Some(_)))) + .await; + } + + let vcard = make_vcard(alice, &[contact_ids[0], contact_ids[1]]).await?; + // This should be the same vCard except timestamps, check that roughly. + let contacts = contact_tools::parse_vcard(&vcard); + assert_eq!(contacts.len(), 2); + assert_eq!(contacts[0].addr, bob_addr); + assert_eq!(contacts[0].authname, "Bob".to_string()); + assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64); + assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64); + assert!(contacts[0].timestamp.is_ok()); + assert_eq!(contacts[1].addr, "fiona@example.net".to_string()); + + let chat_id = ChatId::create_for_contact(alice, contact_ids[0]).await?; + let sent_msg = alice.send_text(chat_id, "moin").await; + let msg = bob.recv_msg(&sent_msg).await; + assert!(msg.get_showpadlock()); + + // Bob only actually imports Fiona, though `ContactId::SELF` is also returned. + bob.evtracker.clear_events(); + let contact_ids = import_vcard(bob, &vcard).await?; + bob.emit_event(EventType::Test); + assert_eq!(contact_ids.len(), 2); + assert_eq!(contact_ids[0], ContactId::SELF); + let ev = bob + .evtracker + .get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. })) + .await; + assert_eq!(ev, EventType::ContactsChanged(Some(contact_ids[1]))); + let ev = bob + .evtracker + .get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. } | EventType::Test)) + .await; + assert_eq!(ev, EventType::Test); + let vcard = make_vcard(bob, &[contact_ids[1]]).await?; + let contacts = contact_tools::parse_vcard(&vcard); + assert_eq!(contacts.len(), 1); + assert_eq!(contacts[0].addr, "fiona@example.net"); + assert_eq!(contacts[0].authname, "".to_string()); + assert_eq!(contacts[0].key, None); + assert_eq!(contacts[0].profile_image, None); + assert!(contacts[0].timestamp.is_ok()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_import_vcard_updates_only_key() -> Result<()> { + let alice = &TestContext::new_alice().await; + let bob = &TestContext::new_bob().await; + let bob_addr = &bob.get_config(Config::Addr).await?.unwrap(); + bob.set_config(Config::Displayname, Some("Bob")).await?; + let vcard = make_vcard(bob, &[ContactId::SELF]).await?; + alice.evtracker.clear_events(); + let alice_bob_id = import_vcard(alice, &vcard).await?[0]; + let ev = alice + .evtracker + .get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. })) + .await; + assert_eq!(ev, EventType::ContactsChanged(Some(alice_bob_id))); + let chat_id = ChatId::create_for_contact(alice, alice_bob_id).await?; + let sent_msg = alice.send_text(chat_id, "moin").await; + let msg = bob.recv_msg(&sent_msg).await; + assert!(msg.get_showpadlock()); + + let bob = &TestContext::new().await; + bob.configure_addr(bob_addr).await; + bob.set_config(Config::Displayname, Some("Not Bob")).await?; + let avatar_path = bob.dir.path().join("avatar.png"); + let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png"); + tokio::fs::write(&avatar_path, avatar_bytes).await?; + bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap())) + .await?; + SystemTime::shift(Duration::from_secs(1)); + let vcard1 = make_vcard(bob, &[ContactId::SELF]).await?; + assert_eq!(import_vcard(alice, &vcard1).await?, vec![alice_bob_id]); + let alice_bob_contact = Contact::get_by_id(alice, alice_bob_id).await?; + assert_eq!(alice_bob_contact.get_authname(), "Bob"); + assert_eq!(alice_bob_contact.get_profile_image(alice).await?, None); + let msg = alice.get_last_msg_in(chat_id).await; + assert!(msg.is_info()); + assert_eq!( + msg.get_text(), + stock_str::contact_setup_changed(alice, bob_addr).await + ); + let sent_msg = alice.send_text(chat_id, "moin").await; + let msg = bob.recv_msg(&sent_msg).await; + assert!(msg.get_showpadlock()); + + // The old vCard is imported, but doesn't change Bob's key for Alice. + import_vcard(alice, &vcard).await?.first().unwrap(); + let sent_msg = alice.send_text(chat_id, "moin").await; + let msg = bob.recv_msg(&sent_msg).await; + assert!(msg.get_showpadlock()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_reset_encryption() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let msg = tcm.send_recv_accept(alice, bob, "Hello!").await; + assert_eq!(msg.get_showpadlock(), false); + + let msg = tcm.send_recv(bob, alice, "Hi!").await; + assert_eq!(msg.get_showpadlock(), true); + let alice_bob_contact_id = msg.from_id; + + alice_bob_contact_id.reset_encryption(alice).await?; + + let msg = tcm.send_recv(alice, bob, "Unencrypted").await; + assert_eq!(msg.get_showpadlock(), false); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_reset_verified_encryption() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + tcm.execute_securejoin(bob, alice).await; + + let msg = tcm.send_recv(bob, alice, "Encrypted").await; + assert_eq!(msg.get_showpadlock(), true); + + let alice_bob_chat_id = msg.chat_id; + let alice_bob_contact_id = msg.from_id; + alice_bob_contact_id.reset_encryption(alice).await?; + + // Check that the contact is still verified after resetting encryption. + let alice_bob_contact = Contact::get_by_id(alice, alice_bob_contact_id).await?; + assert_eq!(alice_bob_contact.is_verified(alice).await?, true); + + // 1:1 chat and profile is no longer verified. + assert_eq!(alice_bob_contact.is_profile_verified(alice).await?, false); + + let info_msg = alice.get_last_msg_in(alice_bob_chat_id).await; + assert_eq!( + info_msg.text, + "bob@example.net sent a message from another device." + ); + + let msg = tcm.send_recv(alice, bob, "Unencrypted").await; + assert_eq!(msg.get_showpadlock(), false); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_self_is_verified() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + + let contact = Contact::get_by_id(&alice, ContactId::SELF).await?; + assert_eq!(contact.is_verified(&alice).await?, true); + assert!(contact.is_profile_verified(&alice).await?); + assert!(contact.get_verifier_id(&alice).await?.is_none()); + + let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?; + assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected); + + Ok(()) +} diff --git a/src/message.rs b/src/message.rs index 99993cbeb..cfe01b717 100644 --- a/src/message.rs +++ b/src/message.rs @@ -2245,763 +2245,4 @@ pub(crate) fn normalize_text(text: &str) -> Option { } #[cfg(test)] -mod tests { - use num_traits::FromPrimitive; - - use super::*; - use crate::chat::{ - self, add_contact_to_chat, forward_msgs, marknoticed_chat, save_msgs, send_text_msg, - ChatItem, ProtectionStatus, - }; - use crate::chatlist::Chatlist; - use crate::config::Config; - use crate::reaction::send_reaction; - use crate::receive_imf::receive_imf; - use crate::test_utils as test; - use crate::test_utils::{TestContext, TestContextManager}; - - #[test] - fn test_guess_msgtype_from_suffix() { - assert_eq!( - guess_msgtype_from_path_suffix(Path::new("foo/bar-sth.mp3")), - Some((Viewtype::Audio, "audio/mpeg")) - ); - assert_eq!( - guess_msgtype_from_path_suffix(Path::new("foo/file.html")), - Some((Viewtype::File, "text/html")) - ); - assert_eq!( - guess_msgtype_from_path_suffix(Path::new("foo/file.xdc")), - Some((Viewtype::Webxdc, "application/webxdc+zip")) - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_webrtc_instance() { - let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar"); - assert_eq!(webrtc_type, VideochatType::BasicWebrtc); - assert_eq!(url, "https://foo/bar"); - - let (webrtc_type, url) = Message::parse_webrtc_instance("bAsIcwEbrTc:url"); - assert_eq!(webrtc_type, VideochatType::BasicWebrtc); - assert_eq!(url, "url"); - - let (webrtc_type, url) = Message::parse_webrtc_instance("https://foo/bar?key=val#key=val"); - assert_eq!(webrtc_type, VideochatType::Unknown); - assert_eq!(url, "https://foo/bar?key=val#key=val"); - - let (webrtc_type, url) = Message::parse_webrtc_instance("jitsi:https://j.si/foo"); - assert_eq!(webrtc_type, VideochatType::Jitsi); - assert_eq!(url, "https://j.si/foo"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_create_webrtc_instance() { - // webrtc_instance may come from an input field of the ui, be pretty tolerant on input - let instance = Message::create_webrtc_instance("https://meet.jit.si/", "123"); - assert_eq!(instance, "https://meet.jit.si/123"); - - let instance = Message::create_webrtc_instance("https://meet.jit.si", "456"); - assert_eq!(instance, "https://meet.jit.si/456"); - - let instance = Message::create_webrtc_instance("meet.jit.si", "789"); - assert_eq!(instance, "https://meet.jit.si/789"); - - let instance = Message::create_webrtc_instance("bla.foo?", "123"); - assert_eq!(instance, "https://bla.foo?123"); - - let instance = Message::create_webrtc_instance("jitsi:bla.foo#", "456"); - assert_eq!(instance, "jitsi:https://bla.foo#456"); - - let instance = Message::create_webrtc_instance("bla.foo#room=", "789"); - assert_eq!(instance, "https://bla.foo#room=789"); - - let instance = Message::create_webrtc_instance("https://bla.foo#room", "123"); - assert_eq!(instance, "https://bla.foo#room/123"); - - let instance = Message::create_webrtc_instance("bla.foo#room$ROOM", "123"); - assert_eq!(instance, "https://bla.foo#room123"); - - let instance = Message::create_webrtc_instance("bla.foo#room=$ROOM&after=cont", "234"); - assert_eq!(instance, "https://bla.foo#room=234&after=cont"); - - let instance = Message::create_webrtc_instance(" meet.jit .si ", "789"); - assert_eq!(instance, "https://meet.jit.si/789"); - - let instance = Message::create_webrtc_instance(" basicwebrtc: basic . stuff\n ", "12345ab"); - assert_eq!(instance, "basicwebrtc:https://basic.stuff/12345ab"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_create_webrtc_instance_noroom() { - // webrtc_instance may come from an input field of the ui, be pretty tolerant on input - let instance = Message::create_webrtc_instance("bla.foo$NOROOM", "123"); - assert_eq!(instance, "https://bla.foo"); - - let instance = Message::create_webrtc_instance(" bla . foo $NOROOM ", "456"); - assert_eq!(instance, "https://bla.foo"); - - let instance = Message::create_webrtc_instance(" $NOROOM bla . foo ", "789"); - assert_eq!(instance, "https://bla.foo"); - - let instance = Message::create_webrtc_instance(" bla.foo / $NOROOM ? a = b ", "123"); - assert_eq!(instance, "https://bla.foo/?a=b"); - - // $ROOM has a higher precedence - let instance = Message::create_webrtc_instance("bla.foo/?$NOROOM=$ROOM", "123"); - assert_eq!(instance, "https://bla.foo/?$NOROOM=123"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_width_height() { - let t = test::TestContext::new().await; - - // test that get_width() and get_height() are returning some dimensions for images; - // (as the device-chat contains a welcome-images, we check that) - t.update_device_chats().await.ok(); - let device_chat_id = ChatId::get_for_contact(&t, ContactId::DEVICE) - .await - .unwrap(); - - let mut has_image = false; - let chatitems = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); - for chatitem in chatitems { - if let ChatItem::Message { msg_id } = chatitem { - if let Ok(msg) = Message::load_from_db(&t, msg_id).await { - if msg.get_viewtype() == Viewtype::Image { - has_image = true; - // just check that width/height are inside some reasonable ranges - assert!(msg.get_width() > 100); - assert!(msg.get_height() > 100); - assert!(msg.get_width() < 4000); - assert!(msg.get_height() < 4000); - } - } - } - } - assert!(has_image); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_quote() { - let d = test::TestContext::new().await; - let ctx = &d.ctx; - - ctx.set_config(Config::ConfiguredAddr, Some("self@example.com")) - .await - .unwrap(); - - let chat = d.create_chat_with_contact("", "dest@example.com").await; - - let mut msg = Message::new_text("Quoted message".to_string()); - - // Send message, so it gets a Message-Id. - assert!(msg.rfc724_mid.is_empty()); - let msg_id = chat::send_msg(ctx, chat.id, &mut msg).await.unwrap(); - let msg = Message::load_from_db(ctx, msg_id).await.unwrap(); - assert!(!msg.rfc724_mid.is_empty()); - - let mut msg2 = Message::new(Viewtype::Text); - msg2.set_quote(ctx, Some(&msg)) - .await - .expect("can't set quote"); - assert_eq!(msg2.quoted_text().unwrap(), msg.get_text()); - - let quoted_msg = msg2 - .quoted_message(ctx) - .await - .expect("error while retrieving quoted message") - .expect("quoted message not found"); - assert_eq!(quoted_msg.get_text(), msg2.quoted_text().unwrap()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_no_quote() { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - tcm.send_recv_accept(alice, bob, "Hi!").await; - let msg = tcm - .send_recv( - alice, - bob, - "On 2024-08-28, Alice wrote:\n> A quote.\nNot really.", - ) - .await; - - assert!(msg.quoted_text().is_none()); - assert!(msg.quoted_message(bob).await.unwrap().is_none()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_unencrypted_quote_encrypted_message() -> Result<()> { - let mut tcm = TestContextManager::new(); - - let alice = &tcm.alice().await; - let bob = &tcm.bob().await; - - let alice_group = alice - .create_group_with_members(ProtectionStatus::Unprotected, "Group chat", &[bob]) - .await; - let sent = alice.send_text(alice_group, "Hi! I created a group").await; - let bob_received_message = bob.recv_msg(&sent).await; - - let bob_group = bob_received_message.chat_id; - bob_group.accept(bob).await?; - let sent = bob.send_text(bob_group, "Encrypted message").await; - let alice_received_message = alice.recv_msg(&sent).await; - assert!(alice_received_message.get_showpadlock()); - - // Alice adds contact without key so chat becomes unencrypted. - let alice_flubby_contact_id = - Contact::create(alice, "Flubby", "flubby@example.org").await?; - add_contact_to_chat(alice, alice_group, alice_flubby_contact_id).await?; - - // Alice quotes encrypted message in unencrypted chat. - let mut msg = Message::new_text("unencrypted".to_string()); - msg.set_quote(alice, Some(&alice_received_message)).await?; - chat::send_msg(alice, alice_group, &mut msg).await?; - - let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await; - assert_eq!(bob_received_message.quoted_text().unwrap(), "..."); - assert_eq!(bob_received_message.get_showpadlock(), false); - - // Alice replaces a quote of encrypted message with a quote of unencrypted one. - let mut msg1 = Message::new(Viewtype::Text); - msg1.set_quote(alice, Some(&alice_received_message)).await?; - msg1.set_quote(alice, Some(&msg)).await?; - chat::send_msg(alice, alice_group, &mut msg1).await?; - - let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await; - assert_eq!(bob_received_message.quoted_text().unwrap(), "unencrypted"); - assert_eq!(bob_received_message.get_showpadlock(), false); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_chat_id() { - // Alice receives a message that pops up as a contact request - let alice = TestContext::new_alice().await; - receive_imf( - &alice, - b"From: Bob \n\ - To: alice@example.org\n\ - Chat-Version: 1.0\n\ - Message-ID: <123@example.com>\n\ - Date: Fri, 29 Jan 2021 21:37:55 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - - // check chat-id of this message - let msg = alice.get_last_msg().await; - assert!(!msg.get_chat_id().is_special()); - assert_eq!(msg.get_text(), "hello".to_string()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_override_sender_name() { - // send message with overridden sender name - let alice = TestContext::new_alice().await; - let alice2 = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let chat = alice.create_chat(&bob).await; - let contact_id = *chat::get_chat_contacts(&alice, chat.id) - .await - .unwrap() - .first() - .unwrap(); - let contact = Contact::get_by_id(&alice, contact_id).await.unwrap(); - - let mut msg = Message::new_text("bla blubb".to_string()); - msg.set_override_sender_name(Some("over ride".to_string())); - assert_eq!( - msg.get_override_sender_name(), - Some("over ride".to_string()) - ); - assert_eq!(msg.get_sender_name(&contact), "over ride".to_string()); - assert_ne!(contact.get_display_name(), "over ride".to_string()); - chat::send_msg(&alice, chat.id, &mut msg).await.unwrap(); - let sent_msg = alice.pop_sent_msg().await; - - // bob receives that message - let chat = bob.create_chat(&alice).await; - let contact_id = *chat::get_chat_contacts(&bob, chat.id) - .await - .unwrap() - .first() - .unwrap(); - let contact = Contact::get_by_id(&bob, contact_id).await.unwrap(); - let msg = bob.recv_msg(&sent_msg).await; - assert_eq!(msg.chat_id, chat.id); - assert_eq!(msg.text, "bla blubb"); - assert_eq!( - msg.get_override_sender_name(), - Some("over ride".to_string()) - ); - assert_eq!(msg.get_sender_name(&contact), "over ride".to_string()); - assert_ne!(contact.get_display_name(), "over ride".to_string()); - - // explicitly check that the message does not create a mailing list - // (mailing lists may also use `Sender:`-header) - let chat = Chat::load_from_db(&bob, msg.chat_id).await.unwrap(); - assert_ne!(chat.typ, Chattype::Mailinglist); - - // Alice receives message on another device. - let msg = alice2.recv_msg(&sent_msg).await; - assert_eq!( - msg.get_override_sender_name(), - Some("over ride".to_string()) - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_original_msg_id() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - // normal sending of messages does not have an original ID - let one2one_chat = alice.create_chat(&bob).await; - let sent = alice.send_text(one2one_chat.id, "foo").await; - let orig_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?; - assert!(orig_msg.get_original_msg_id(&alice).await?.is_none()); - assert!(orig_msg.parent(&alice).await?.is_none()); - assert!(orig_msg.quoted_message(&alice).await?.is_none()); - - // forwarding to "Saved Messages", the message gets the original ID attached - let self_chat = alice.get_self_chat().await; - save_msgs(&alice, &[sent.sender_msg_id]).await?; - let saved_msg = alice.get_last_msg_in(self_chat.get_id()).await; - assert_ne!(saved_msg.get_id(), orig_msg.get_id()); - assert_eq!( - saved_msg.get_original_msg_id(&alice).await?.unwrap(), - orig_msg.get_id() - ); - assert!(saved_msg.parent(&alice).await?.is_none()); - assert!(saved_msg.quoted_message(&alice).await?.is_none()); - - // forwarding from "Saved Messages" back to another chat, detaches original ID - forward_msgs(&alice, &[saved_msg.get_id()], one2one_chat.get_id()).await?; - let forwarded_msg = alice.get_last_msg_in(one2one_chat.get_id()).await; - assert_ne!(forwarded_msg.get_id(), saved_msg.get_id()); - assert_ne!(forwarded_msg.get_id(), orig_msg.get_id()); - assert!(forwarded_msg.get_original_msg_id(&alice).await?.is_none()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_markseen_msgs() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat = alice.create_chat(&bob).await; - let mut msg = Message::new_text("this is the text!".to_string()); - - // alice sends to bob, - assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0); - let sent1 = alice.send_msg(alice_chat.id, &mut msg).await; - let msg1 = bob.recv_msg(&sent1).await; - let bob_chat_id = msg1.chat_id; - let sent2 = alice.send_msg(alice_chat.id, &mut msg).await; - let msg2 = bob.recv_msg(&sent2).await; - assert_eq!(msg1.chat_id, msg2.chat_id); - let chats = Chatlist::try_load(&bob, 0, None, None).await?; - assert_eq!(chats.len(), 1); - let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?; - assert_eq!(msgs.len(), 2); - assert_eq!(bob.get_fresh_msgs().await?.len(), 0); - - // that has no effect in contact request - markseen_msgs(&bob, vec![msg1.id, msg2.id]).await?; - - assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1); - let bob_chat = Chat::load_from_db(&bob, bob_chat_id).await?; - assert_eq!(bob_chat.blocked, Blocked::Request); - - let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?; - assert_eq!(msgs.len(), 2); - bob_chat_id.accept(&bob).await.unwrap(); - - // bob sends to alice, - // alice knows bob and messages appear in normal chat - let msg1 = alice - .recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await) - .await; - let msg2 = alice - .recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await) - .await; - let chats = Chatlist::try_load(&alice, 0, None, None).await?; - assert_eq!(chats.len(), 1); - assert_eq!(chats.get_chat_id(0)?, alice_chat.id); - assert_eq!(chats.get_chat_id(0)?, msg1.chat_id); - assert_eq!(chats.get_chat_id(0)?, msg2.chat_id); - assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 2); - assert_eq!(alice.get_fresh_msgs().await?.len(), 2); - - // no message-ids, that should have no effect - markseen_msgs(&alice, vec![]).await?; - - // bad message-id, that should have no effect - markseen_msgs(&alice, vec![MsgId::new(123456)]).await?; - - assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 2); - assert_eq!(alice.get_fresh_msgs().await?.len(), 2); - - // mark the most recent as seen - markseen_msgs(&alice, vec![msg2.id]).await?; - - assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 1); - assert_eq!(alice.get_fresh_msgs().await?.len(), 1); - - // user scrolled up - mark both as seen - markseen_msgs(&alice, vec![msg1.id, msg2.id]).await?; - - assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 0); - assert_eq!(alice.get_fresh_msgs().await?.len(), 0); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_markseen_not_downloaded_msg() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - alice.set_config(Config::DownloadLimit, Some("1")).await?; - let bob = &tcm.bob().await; - let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id; - - let file_bytes = include_bytes!("../test-data/image/screenshot.png"); - let mut msg = Message::new(Viewtype::Image); - msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?; - let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await; - let msg = alice.recv_msg(&sent_msg).await; - assert_eq!(msg.download_state, DownloadState::Available); - assert!(!msg.param.get_bool(Param::WantsMdn).unwrap_or_default()); - assert_eq!(msg.state, MessageState::InFresh); - markseen_msgs(alice, vec![msg.id]).await?; - // A not downloaded message can be seen only if it's seen on another device. - assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed); - // Marking the message as seen again is a no op. - markseen_msgs(alice, vec![msg.id]).await?; - assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed); - - msg.id - .update_download_state(alice, DownloadState::InProgress) - .await?; - markseen_msgs(alice, vec![msg.id]).await?; - assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed); - msg.id - .update_download_state(alice, DownloadState::Failure) - .await?; - markseen_msgs(alice, vec![msg.id]).await?; - assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed); - msg.id - .update_download_state(alice, DownloadState::Undecipherable) - .await?; - markseen_msgs(alice, vec![msg.id]).await?; - assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed); - - assert!( - !alice - .sql - .exists("SELECT COUNT(*) FROM smtp_mdns", ()) - .await? - ); - - alice.set_config(Config::DownloadLimit, None).await?; - // Let's assume that Alice and Bob resolved the problem with encryption. - let old_msg = msg; - let msg = alice.recv_msg(&sent_msg).await; - assert_eq!(msg.chat_id, old_msg.chat_id); - assert_eq!(msg.download_state, DownloadState::Done); - assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default()); - assert!(msg.get_showpadlock()); - // The message state mustn't be downgraded to `InFresh`. - assert_eq!(msg.state, MessageState::InNoticed); - markseen_msgs(alice, vec![msg.id]).await?; - let msg = Message::load_from_db(alice, msg.id).await?; - assert_eq!(msg.state, MessageState::InSeen); - assert_eq!( - alice - .sql - .count("SELECT COUNT(*) FROM smtp_mdns", ()) - .await?, - 1 - ); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_msg_seen_on_imap_when_downloaded() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; - alice.set_config(Config::DownloadLimit, Some("1")).await?; - let bob = &tcm.bob().await; - let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id; - - let file_bytes = include_bytes!("../test-data/image/screenshot.png"); - let mut msg = Message::new(Viewtype::Image); - msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?; - let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await; - let msg = alice.recv_msg(&sent_msg).await; - assert_eq!(msg.download_state, DownloadState::Available); - assert_eq!(msg.state, MessageState::InFresh); - - alice.set_config(Config::DownloadLimit, None).await?; - let seen = true; - let rcvd_msg = receive_imf(alice, sent_msg.payload().as_bytes(), seen) - .await - .unwrap() - .unwrap(); - assert_eq!(rcvd_msg.chat_id, msg.chat_id); - let msg = Message::load_from_db(alice, *rcvd_msg.msg_ids.last().unwrap()) - .await - .unwrap(); - assert_eq!(msg.download_state, DownloadState::Done); - assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default()); - assert!(msg.get_showpadlock()); - assert_eq!(msg.state, MessageState::InSeen); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_state() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat = alice.create_chat(&bob).await; - let bob_chat = bob.create_chat(&alice).await; - - // check both get_state() functions, - // the one requiring a id and the one requiring an object - async fn assert_state(t: &Context, msg_id: MsgId, state: MessageState) { - assert_eq!(msg_id.get_state(t).await.unwrap(), state); - assert_eq!( - Message::load_from_db(t, msg_id).await.unwrap().get_state(), - state - ); - } - - // check outgoing messages states on sender side - let mut alice_msg = Message::new_text("hi!".to_string()); - assert_eq!(alice_msg.get_state(), MessageState::Undefined); // message not yet in db, assert_state() won't work - - alice_chat - .id - .set_draft(&alice, Some(&mut alice_msg)) - .await?; - let mut alice_msg = alice_chat.id.get_draft(&alice).await?.unwrap(); - assert_state(&alice, alice_msg.id, MessageState::OutDraft).await; - - let msg_id = chat::send_msg(&alice, alice_chat.id, &mut alice_msg).await?; - assert_eq!(msg_id, alice_msg.id); - assert_state(&alice, alice_msg.id, MessageState::OutPending).await; - - let payload = alice.pop_sent_msg().await; - assert_state(&alice, alice_msg.id, MessageState::OutDelivered).await; - - set_msg_failed(&alice, &mut alice_msg, "badly failed").await?; - assert_state(&alice, alice_msg.id, MessageState::OutFailed).await; - - // check incoming message states on receiver side - let bob_msg = bob.recv_msg(&payload).await; - assert_eq!(bob_chat.id, bob_msg.chat_id); - assert_state(&bob, bob_msg.id, MessageState::InFresh).await; - - marknoticed_chat(&bob, bob_msg.chat_id).await?; - assert_state(&bob, bob_msg.id, MessageState::InNoticed).await; - - markseen_msgs(&bob, vec![bob_msg.id]).await?; - assert_state(&bob, bob_msg.id, MessageState::InSeen).await; - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_is_bot() -> Result<()> { - let alice = TestContext::new_alice().await; - - // Alice receives an auto-generated non-chat message. - // - // This could be a holiday notice, - // in which case the message should be marked as bot-generated, - // but the contact should not. - receive_imf( - &alice, - b"From: Claire \n\ - To: alice@example.org\n\ - Message-ID: <789@example.com>\n\ - Auto-Submitted: auto-generated\n\ - Date: Fri, 29 Jan 2021 21:37:55 +0000\n\ - \n\ - hello\n", - false, - ) - .await?; - let msg = alice.get_last_msg().await; - assert_eq!(msg.get_text(), "hello".to_string()); - assert!(msg.is_bot()); - let contact = Contact::get_by_id(&alice, msg.from_id).await?; - assert!(!contact.is_bot()); - - // Alice receives a message from Bob the bot. - receive_imf( - &alice, - b"From: Bob \n\ - To: alice@example.org\n\ - Chat-Version: 1.0\n\ - Message-ID: <123@example.com>\n\ - Auto-Submitted: auto-generated\n\ - Date: Fri, 29 Jan 2021 21:37:55 +0000\n\ - \n\ - hello\n", - false, - ) - .await?; - let msg = alice.get_last_msg().await; - assert_eq!(msg.get_text(), "hello".to_string()); - assert!(msg.is_bot()); - let contact = Contact::get_by_id(&alice, msg.from_id).await?; - assert!(contact.is_bot()); - - // Alice receives a message from Bob who is not the bot anymore. - receive_imf( - &alice, - b"From: Bob \n\ - To: alice@example.org\n\ - Chat-Version: 1.0\n\ - Message-ID: <456@example.com>\n\ - Date: Fri, 29 Jan 2021 21:37:55 +0000\n\ - \n\ - hello again\n", - false, - ) - .await?; - let msg = alice.get_last_msg().await; - assert_eq!(msg.get_text(), "hello again".to_string()); - assert!(!msg.is_bot()); - let contact = Contact::get_by_id(&alice, msg.from_id).await?; - assert!(!contact.is_bot()); - - Ok(()) - } - - #[test] - fn test_viewtype_derive_display_works_as_expected() { - assert_eq!(format!("{}", Viewtype::Audio), "Audio"); - } - - #[test] - fn test_viewtype_values() { - // values may be written to disk and must not change - assert_eq!(Viewtype::Unknown, Viewtype::default()); - assert_eq!(Viewtype::Unknown, Viewtype::from_i32(0).unwrap()); - assert_eq!(Viewtype::Text, Viewtype::from_i32(10).unwrap()); - assert_eq!(Viewtype::Image, Viewtype::from_i32(20).unwrap()); - assert_eq!(Viewtype::Gif, Viewtype::from_i32(21).unwrap()); - assert_eq!(Viewtype::Sticker, Viewtype::from_i32(23).unwrap()); - assert_eq!(Viewtype::Audio, Viewtype::from_i32(40).unwrap()); - assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap()); - assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap()); - assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap()); - assert_eq!( - Viewtype::VideochatInvitation, - Viewtype::from_i32(70).unwrap() - ); - assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap()); - assert_eq!(Viewtype::Vcard, Viewtype::from_i32(90).unwrap()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_send_quotes() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let chat = alice.create_chat(&bob).await; - - let sent = alice.send_text(chat.id, "> First quote").await; - let received = bob.recv_msg(&sent).await; - assert_eq!(received.text, "> First quote"); - assert!(received.quoted_text().is_none()); - assert!(received.quoted_message(&bob).await?.is_none()); - - let sent = alice.send_text(chat.id, "> Second quote").await; - let received = bob.recv_msg(&sent).await; - assert_eq!(received.text, "> Second quote"); - assert!(received.quoted_text().is_none()); - assert!(received.quoted_message(&bob).await?.is_none()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_message_summary_text() -> Result<()> { - let t = TestContext::new_alice().await; - let chat = t.get_self_chat().await; - let msg_id = send_text_msg(&t, chat.id, "foo".to_string()).await?; - let msg = Message::load_from_db(&t, msg_id).await?; - let summary = msg.get_summary(&t, None).await?; - assert_eq!(summary.text, "foo"); - - // message summary does not change when reactions are applied (in contrast to chatlist summary) - send_reaction(&t, msg_id, "🫵").await?; - let msg = Message::load_from_db(&t, msg_id).await?; - let summary = msg.get_summary(&t, None).await?; - assert_eq!(summary.text, "foo"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_format_flowed_round_trip() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - let chat = alice.create_chat(&bob).await; - - let text = " Foo bar"; - let sent = alice.send_text(chat.id, text).await; - let received = bob.recv_msg(&sent).await; - assert_eq!(received.text, text); - - let text = "Foo bar baz"; - let sent = alice.send_text(chat.id, text).await; - let received = bob.recv_msg(&sent).await; - assert_eq!(received.text, text); - - let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A"; - let sent = alice.send_text(chat.id, text).await; - let received = bob.recv_msg(&sent).await; - assert_eq!(received.text, text); - - let python_program = "\ -def hello(): - return 'Hello, world!'"; - let sent = alice.send_text(chat.id, python_program).await; - let received = bob.recv_msg(&sent).await; - assert_eq!(received.text, python_program); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_delete_msgs_offline() -> Result<()> { - let alice = TestContext::new_alice().await; - let chat = alice - .create_chat_with_contact("Bob", "bob@example.org") - .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?); - delete_msgs(&alice, &[msg.id]).await?; - assert!(!alice.sql.exists(stmt, (msg.id,)).await?); - - Ok(()) - } -} +mod message_tests; diff --git a/src/message/message_tests.rs b/src/message/message_tests.rs new file mode 100644 index 000000000..5a2118e1a --- /dev/null +++ b/src/message/message_tests.rs @@ -0,0 +1,757 @@ +use num_traits::FromPrimitive; + +use super::*; +use crate::chat::{ + self, add_contact_to_chat, forward_msgs, marknoticed_chat, save_msgs, send_text_msg, ChatItem, + ProtectionStatus, +}; +use crate::chatlist::Chatlist; +use crate::config::Config; +use crate::reaction::send_reaction; +use crate::receive_imf::receive_imf; +use crate::test_utils as test; +use crate::test_utils::{TestContext, TestContextManager}; + +#[test] +fn test_guess_msgtype_from_suffix() { + assert_eq!( + guess_msgtype_from_path_suffix(Path::new("foo/bar-sth.mp3")), + Some((Viewtype::Audio, "audio/mpeg")) + ); + assert_eq!( + guess_msgtype_from_path_suffix(Path::new("foo/file.html")), + Some((Viewtype::File, "text/html")) + ); + assert_eq!( + guess_msgtype_from_path_suffix(Path::new("foo/file.xdc")), + Some((Viewtype::Webxdc, "application/webxdc+zip")) + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_webrtc_instance() { + let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar"); + assert_eq!(webrtc_type, VideochatType::BasicWebrtc); + assert_eq!(url, "https://foo/bar"); + + let (webrtc_type, url) = Message::parse_webrtc_instance("bAsIcwEbrTc:url"); + assert_eq!(webrtc_type, VideochatType::BasicWebrtc); + assert_eq!(url, "url"); + + let (webrtc_type, url) = Message::parse_webrtc_instance("https://foo/bar?key=val#key=val"); + assert_eq!(webrtc_type, VideochatType::Unknown); + assert_eq!(url, "https://foo/bar?key=val#key=val"); + + let (webrtc_type, url) = Message::parse_webrtc_instance("jitsi:https://j.si/foo"); + assert_eq!(webrtc_type, VideochatType::Jitsi); + assert_eq!(url, "https://j.si/foo"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_create_webrtc_instance() { + // webrtc_instance may come from an input field of the ui, be pretty tolerant on input + let instance = Message::create_webrtc_instance("https://meet.jit.si/", "123"); + assert_eq!(instance, "https://meet.jit.si/123"); + + let instance = Message::create_webrtc_instance("https://meet.jit.si", "456"); + assert_eq!(instance, "https://meet.jit.si/456"); + + let instance = Message::create_webrtc_instance("meet.jit.si", "789"); + assert_eq!(instance, "https://meet.jit.si/789"); + + let instance = Message::create_webrtc_instance("bla.foo?", "123"); + assert_eq!(instance, "https://bla.foo?123"); + + let instance = Message::create_webrtc_instance("jitsi:bla.foo#", "456"); + assert_eq!(instance, "jitsi:https://bla.foo#456"); + + let instance = Message::create_webrtc_instance("bla.foo#room=", "789"); + assert_eq!(instance, "https://bla.foo#room=789"); + + let instance = Message::create_webrtc_instance("https://bla.foo#room", "123"); + assert_eq!(instance, "https://bla.foo#room/123"); + + let instance = Message::create_webrtc_instance("bla.foo#room$ROOM", "123"); + assert_eq!(instance, "https://bla.foo#room123"); + + let instance = Message::create_webrtc_instance("bla.foo#room=$ROOM&after=cont", "234"); + assert_eq!(instance, "https://bla.foo#room=234&after=cont"); + + let instance = Message::create_webrtc_instance(" meet.jit .si ", "789"); + assert_eq!(instance, "https://meet.jit.si/789"); + + let instance = Message::create_webrtc_instance(" basicwebrtc: basic . stuff\n ", "12345ab"); + assert_eq!(instance, "basicwebrtc:https://basic.stuff/12345ab"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_create_webrtc_instance_noroom() { + // webrtc_instance may come from an input field of the ui, be pretty tolerant on input + let instance = Message::create_webrtc_instance("bla.foo$NOROOM", "123"); + assert_eq!(instance, "https://bla.foo"); + + let instance = Message::create_webrtc_instance(" bla . foo $NOROOM ", "456"); + assert_eq!(instance, "https://bla.foo"); + + let instance = Message::create_webrtc_instance(" $NOROOM bla . foo ", "789"); + assert_eq!(instance, "https://bla.foo"); + + let instance = Message::create_webrtc_instance(" bla.foo / $NOROOM ? a = b ", "123"); + assert_eq!(instance, "https://bla.foo/?a=b"); + + // $ROOM has a higher precedence + let instance = Message::create_webrtc_instance("bla.foo/?$NOROOM=$ROOM", "123"); + assert_eq!(instance, "https://bla.foo/?$NOROOM=123"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_width_height() { + let t = test::TestContext::new().await; + + // test that get_width() and get_height() are returning some dimensions for images; + // (as the device-chat contains a welcome-images, we check that) + t.update_device_chats().await.ok(); + let device_chat_id = ChatId::get_for_contact(&t, ContactId::DEVICE) + .await + .unwrap(); + + let mut has_image = false; + let chatitems = chat::get_chat_msgs(&t, device_chat_id).await.unwrap(); + for chatitem in chatitems { + if let ChatItem::Message { msg_id } = chatitem { + if let Ok(msg) = Message::load_from_db(&t, msg_id).await { + if msg.get_viewtype() == Viewtype::Image { + has_image = true; + // just check that width/height are inside some reasonable ranges + assert!(msg.get_width() > 100); + assert!(msg.get_height() > 100); + assert!(msg.get_width() < 4000); + assert!(msg.get_height() < 4000); + } + } + } + } + assert!(has_image); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_quote() { + let d = test::TestContext::new().await; + let ctx = &d.ctx; + + ctx.set_config(Config::ConfiguredAddr, Some("self@example.com")) + .await + .unwrap(); + + let chat = d.create_chat_with_contact("", "dest@example.com").await; + + let mut msg = Message::new_text("Quoted message".to_string()); + + // Send message, so it gets a Message-Id. + assert!(msg.rfc724_mid.is_empty()); + let msg_id = chat::send_msg(ctx, chat.id, &mut msg).await.unwrap(); + let msg = Message::load_from_db(ctx, msg_id).await.unwrap(); + assert!(!msg.rfc724_mid.is_empty()); + + let mut msg2 = Message::new(Viewtype::Text); + msg2.set_quote(ctx, Some(&msg)) + .await + .expect("can't set quote"); + assert_eq!(msg2.quoted_text().unwrap(), msg.get_text()); + + let quoted_msg = msg2 + .quoted_message(ctx) + .await + .expect("error while retrieving quoted message") + .expect("quoted message not found"); + assert_eq!(quoted_msg.get_text(), msg2.quoted_text().unwrap()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_no_quote() { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + tcm.send_recv_accept(alice, bob, "Hi!").await; + let msg = tcm + .send_recv( + alice, + bob, + "On 2024-08-28, Alice wrote:\n> A quote.\nNot really.", + ) + .await; + + assert!(msg.quoted_text().is_none()); + assert!(msg.quoted_message(bob).await.unwrap().is_none()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_unencrypted_quote_encrypted_message() -> Result<()> { + let mut tcm = TestContextManager::new(); + + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let alice_group = alice + .create_group_with_members(ProtectionStatus::Unprotected, "Group chat", &[bob]) + .await; + let sent = alice.send_text(alice_group, "Hi! I created a group").await; + let bob_received_message = bob.recv_msg(&sent).await; + + let bob_group = bob_received_message.chat_id; + bob_group.accept(bob).await?; + let sent = bob.send_text(bob_group, "Encrypted message").await; + let alice_received_message = alice.recv_msg(&sent).await; + assert!(alice_received_message.get_showpadlock()); + + // Alice adds contact without key so chat becomes unencrypted. + let alice_flubby_contact_id = Contact::create(alice, "Flubby", "flubby@example.org").await?; + add_contact_to_chat(alice, alice_group, alice_flubby_contact_id).await?; + + // Alice quotes encrypted message in unencrypted chat. + let mut msg = Message::new_text("unencrypted".to_string()); + msg.set_quote(alice, Some(&alice_received_message)).await?; + chat::send_msg(alice, alice_group, &mut msg).await?; + + let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await; + assert_eq!(bob_received_message.quoted_text().unwrap(), "..."); + assert_eq!(bob_received_message.get_showpadlock(), false); + + // Alice replaces a quote of encrypted message with a quote of unencrypted one. + let mut msg1 = Message::new(Viewtype::Text); + msg1.set_quote(alice, Some(&alice_received_message)).await?; + msg1.set_quote(alice, Some(&msg)).await?; + chat::send_msg(alice, alice_group, &mut msg1).await?; + + let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await; + assert_eq!(bob_received_message.quoted_text().unwrap(), "unencrypted"); + assert_eq!(bob_received_message.get_showpadlock(), false); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_chat_id() { + // Alice receives a message that pops up as a contact request + let alice = TestContext::new_alice().await; + receive_imf( + &alice, + b"From: Bob \n\ + To: alice@example.org\n\ + Chat-Version: 1.0\n\ + Message-ID: <123@example.com>\n\ + Date: Fri, 29 Jan 2021 21:37:55 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + + // check chat-id of this message + let msg = alice.get_last_msg().await; + assert!(!msg.get_chat_id().is_special()); + assert_eq!(msg.get_text(), "hello".to_string()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_set_override_sender_name() { + // send message with overridden sender name + let alice = TestContext::new_alice().await; + let alice2 = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat = alice.create_chat(&bob).await; + let contact_id = *chat::get_chat_contacts(&alice, chat.id) + .await + .unwrap() + .first() + .unwrap(); + let contact = Contact::get_by_id(&alice, contact_id).await.unwrap(); + + let mut msg = Message::new_text("bla blubb".to_string()); + msg.set_override_sender_name(Some("over ride".to_string())); + assert_eq!( + msg.get_override_sender_name(), + Some("over ride".to_string()) + ); + assert_eq!(msg.get_sender_name(&contact), "over ride".to_string()); + assert_ne!(contact.get_display_name(), "over ride".to_string()); + chat::send_msg(&alice, chat.id, &mut msg).await.unwrap(); + let sent_msg = alice.pop_sent_msg().await; + + // bob receives that message + let chat = bob.create_chat(&alice).await; + let contact_id = *chat::get_chat_contacts(&bob, chat.id) + .await + .unwrap() + .first() + .unwrap(); + let contact = Contact::get_by_id(&bob, contact_id).await.unwrap(); + let msg = bob.recv_msg(&sent_msg).await; + assert_eq!(msg.chat_id, chat.id); + assert_eq!(msg.text, "bla blubb"); + assert_eq!( + msg.get_override_sender_name(), + Some("over ride".to_string()) + ); + assert_eq!(msg.get_sender_name(&contact), "over ride".to_string()); + assert_ne!(contact.get_display_name(), "over ride".to_string()); + + // explicitly check that the message does not create a mailing list + // (mailing lists may also use `Sender:`-header) + let chat = Chat::load_from_db(&bob, msg.chat_id).await.unwrap(); + assert_ne!(chat.typ, Chattype::Mailinglist); + + // Alice receives message on another device. + let msg = alice2.recv_msg(&sent_msg).await; + assert_eq!( + msg.get_override_sender_name(), + Some("over ride".to_string()) + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_original_msg_id() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + // normal sending of messages does not have an original ID + let one2one_chat = alice.create_chat(&bob).await; + let sent = alice.send_text(one2one_chat.id, "foo").await; + let orig_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?; + assert!(orig_msg.get_original_msg_id(&alice).await?.is_none()); + assert!(orig_msg.parent(&alice).await?.is_none()); + assert!(orig_msg.quoted_message(&alice).await?.is_none()); + + // forwarding to "Saved Messages", the message gets the original ID attached + let self_chat = alice.get_self_chat().await; + save_msgs(&alice, &[sent.sender_msg_id]).await?; + let saved_msg = alice.get_last_msg_in(self_chat.get_id()).await; + assert_ne!(saved_msg.get_id(), orig_msg.get_id()); + assert_eq!( + saved_msg.get_original_msg_id(&alice).await?.unwrap(), + orig_msg.get_id() + ); + assert!(saved_msg.parent(&alice).await?.is_none()); + assert!(saved_msg.quoted_message(&alice).await?.is_none()); + + // forwarding from "Saved Messages" back to another chat, detaches original ID + forward_msgs(&alice, &[saved_msg.get_id()], one2one_chat.get_id()).await?; + let forwarded_msg = alice.get_last_msg_in(one2one_chat.get_id()).await; + assert_ne!(forwarded_msg.get_id(), saved_msg.get_id()); + assert_ne!(forwarded_msg.get_id(), orig_msg.get_id()); + assert!(forwarded_msg.get_original_msg_id(&alice).await?.is_none()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_markseen_msgs() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + let mut msg = Message::new_text("this is the text!".to_string()); + + // alice sends to bob, + assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0); + let sent1 = alice.send_msg(alice_chat.id, &mut msg).await; + let msg1 = bob.recv_msg(&sent1).await; + let bob_chat_id = msg1.chat_id; + let sent2 = alice.send_msg(alice_chat.id, &mut msg).await; + let msg2 = bob.recv_msg(&sent2).await; + assert_eq!(msg1.chat_id, msg2.chat_id); + let chats = Chatlist::try_load(&bob, 0, None, None).await?; + assert_eq!(chats.len(), 1); + let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?; + assert_eq!(msgs.len(), 2); + assert_eq!(bob.get_fresh_msgs().await?.len(), 0); + + // that has no effect in contact request + markseen_msgs(&bob, vec![msg1.id, msg2.id]).await?; + + assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1); + let bob_chat = Chat::load_from_db(&bob, bob_chat_id).await?; + assert_eq!(bob_chat.blocked, Blocked::Request); + + let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?; + assert_eq!(msgs.len(), 2); + bob_chat_id.accept(&bob).await.unwrap(); + + // bob sends to alice, + // alice knows bob and messages appear in normal chat + let msg1 = alice + .recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await) + .await; + let msg2 = alice + .recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await) + .await; + let chats = Chatlist::try_load(&alice, 0, None, None).await?; + assert_eq!(chats.len(), 1); + assert_eq!(chats.get_chat_id(0)?, alice_chat.id); + assert_eq!(chats.get_chat_id(0)?, msg1.chat_id); + assert_eq!(chats.get_chat_id(0)?, msg2.chat_id); + assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 2); + assert_eq!(alice.get_fresh_msgs().await?.len(), 2); + + // no message-ids, that should have no effect + markseen_msgs(&alice, vec![]).await?; + + // bad message-id, that should have no effect + markseen_msgs(&alice, vec![MsgId::new(123456)]).await?; + + assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 2); + assert_eq!(alice.get_fresh_msgs().await?.len(), 2); + + // mark the most recent as seen + markseen_msgs(&alice, vec![msg2.id]).await?; + + assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 1); + assert_eq!(alice.get_fresh_msgs().await?.len(), 1); + + // user scrolled up - mark both as seen + markseen_msgs(&alice, vec![msg1.id, msg2.id]).await?; + + assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 0); + assert_eq!(alice.get_fresh_msgs().await?.len(), 0); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_markseen_not_downloaded_msg() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + alice.set_config(Config::DownloadLimit, Some("1")).await?; + let bob = &tcm.bob().await; + let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id; + + let file_bytes = include_bytes!("../../test-data/image/screenshot.png"); + let mut msg = Message::new(Viewtype::Image); + msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?; + let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await; + let msg = alice.recv_msg(&sent_msg).await; + assert_eq!(msg.download_state, DownloadState::Available); + assert!(!msg.param.get_bool(Param::WantsMdn).unwrap_or_default()); + assert_eq!(msg.state, MessageState::InFresh); + markseen_msgs(alice, vec![msg.id]).await?; + // A not downloaded message can be seen only if it's seen on another device. + assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed); + // Marking the message as seen again is a no op. + markseen_msgs(alice, vec![msg.id]).await?; + assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed); + + msg.id + .update_download_state(alice, DownloadState::InProgress) + .await?; + markseen_msgs(alice, vec![msg.id]).await?; + assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed); + msg.id + .update_download_state(alice, DownloadState::Failure) + .await?; + markseen_msgs(alice, vec![msg.id]).await?; + assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed); + msg.id + .update_download_state(alice, DownloadState::Undecipherable) + .await?; + markseen_msgs(alice, vec![msg.id]).await?; + assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed); + + assert!( + !alice + .sql + .exists("SELECT COUNT(*) FROM smtp_mdns", ()) + .await? + ); + + alice.set_config(Config::DownloadLimit, None).await?; + // Let's assume that Alice and Bob resolved the problem with encryption. + let old_msg = msg; + let msg = alice.recv_msg(&sent_msg).await; + assert_eq!(msg.chat_id, old_msg.chat_id); + assert_eq!(msg.download_state, DownloadState::Done); + assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default()); + assert!(msg.get_showpadlock()); + // The message state mustn't be downgraded to `InFresh`. + assert_eq!(msg.state, MessageState::InNoticed); + markseen_msgs(alice, vec![msg.id]).await?; + let msg = Message::load_from_db(alice, msg.id).await?; + assert_eq!(msg.state, MessageState::InSeen); + assert_eq!( + alice + .sql + .count("SELECT COUNT(*) FROM smtp_mdns", ()) + .await?, + 1 + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_msg_seen_on_imap_when_downloaded() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + alice.set_config(Config::DownloadLimit, Some("1")).await?; + let bob = &tcm.bob().await; + let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id; + + let file_bytes = include_bytes!("../../test-data/image/screenshot.png"); + let mut msg = Message::new(Viewtype::Image); + msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?; + let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await; + let msg = alice.recv_msg(&sent_msg).await; + assert_eq!(msg.download_state, DownloadState::Available); + assert_eq!(msg.state, MessageState::InFresh); + + alice.set_config(Config::DownloadLimit, None).await?; + let seen = true; + let rcvd_msg = receive_imf(alice, sent_msg.payload().as_bytes(), seen) + .await + .unwrap() + .unwrap(); + assert_eq!(rcvd_msg.chat_id, msg.chat_id); + let msg = Message::load_from_db(alice, *rcvd_msg.msg_ids.last().unwrap()) + .await + .unwrap(); + assert_eq!(msg.download_state, DownloadState::Done); + assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default()); + assert!(msg.get_showpadlock()); + assert_eq!(msg.state, MessageState::InSeen); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_state() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + let bob_chat = bob.create_chat(&alice).await; + + // check both get_state() functions, + // the one requiring a id and the one requiring an object + async fn assert_state(t: &Context, msg_id: MsgId, state: MessageState) { + assert_eq!(msg_id.get_state(t).await.unwrap(), state); + assert_eq!( + Message::load_from_db(t, msg_id).await.unwrap().get_state(), + state + ); + } + + // check outgoing messages states on sender side + let mut alice_msg = Message::new_text("hi!".to_string()); + assert_eq!(alice_msg.get_state(), MessageState::Undefined); // message not yet in db, assert_state() won't work + + alice_chat + .id + .set_draft(&alice, Some(&mut alice_msg)) + .await?; + let mut alice_msg = alice_chat.id.get_draft(&alice).await?.unwrap(); + assert_state(&alice, alice_msg.id, MessageState::OutDraft).await; + + let msg_id = chat::send_msg(&alice, alice_chat.id, &mut alice_msg).await?; + assert_eq!(msg_id, alice_msg.id); + assert_state(&alice, alice_msg.id, MessageState::OutPending).await; + + let payload = alice.pop_sent_msg().await; + assert_state(&alice, alice_msg.id, MessageState::OutDelivered).await; + + set_msg_failed(&alice, &mut alice_msg, "badly failed").await?; + assert_state(&alice, alice_msg.id, MessageState::OutFailed).await; + + // check incoming message states on receiver side + let bob_msg = bob.recv_msg(&payload).await; + assert_eq!(bob_chat.id, bob_msg.chat_id); + assert_state(&bob, bob_msg.id, MessageState::InFresh).await; + + marknoticed_chat(&bob, bob_msg.chat_id).await?; + assert_state(&bob, bob_msg.id, MessageState::InNoticed).await; + + markseen_msgs(&bob, vec![bob_msg.id]).await?; + assert_state(&bob, bob_msg.id, MessageState::InSeen).await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_is_bot() -> Result<()> { + let alice = TestContext::new_alice().await; + + // Alice receives an auto-generated non-chat message. + // + // This could be a holiday notice, + // in which case the message should be marked as bot-generated, + // but the contact should not. + receive_imf( + &alice, + b"From: Claire \n\ + To: alice@example.org\n\ + Message-ID: <789@example.com>\n\ + Auto-Submitted: auto-generated\n\ + Date: Fri, 29 Jan 2021 21:37:55 +0000\n\ + \n\ + hello\n", + false, + ) + .await?; + let msg = alice.get_last_msg().await; + assert_eq!(msg.get_text(), "hello".to_string()); + assert!(msg.is_bot()); + let contact = Contact::get_by_id(&alice, msg.from_id).await?; + assert!(!contact.is_bot()); + + // Alice receives a message from Bob the bot. + receive_imf( + &alice, + b"From: Bob \n\ + To: alice@example.org\n\ + Chat-Version: 1.0\n\ + Message-ID: <123@example.com>\n\ + Auto-Submitted: auto-generated\n\ + Date: Fri, 29 Jan 2021 21:37:55 +0000\n\ + \n\ + hello\n", + false, + ) + .await?; + let msg = alice.get_last_msg().await; + assert_eq!(msg.get_text(), "hello".to_string()); + assert!(msg.is_bot()); + let contact = Contact::get_by_id(&alice, msg.from_id).await?; + assert!(contact.is_bot()); + + // Alice receives a message from Bob who is not the bot anymore. + receive_imf( + &alice, + b"From: Bob \n\ + To: alice@example.org\n\ + Chat-Version: 1.0\n\ + Message-ID: <456@example.com>\n\ + Date: Fri, 29 Jan 2021 21:37:55 +0000\n\ + \n\ + hello again\n", + false, + ) + .await?; + let msg = alice.get_last_msg().await; + assert_eq!(msg.get_text(), "hello again".to_string()); + assert!(!msg.is_bot()); + let contact = Contact::get_by_id(&alice, msg.from_id).await?; + assert!(!contact.is_bot()); + + Ok(()) +} + +#[test] +fn test_viewtype_derive_display_works_as_expected() { + assert_eq!(format!("{}", Viewtype::Audio), "Audio"); +} + +#[test] +fn test_viewtype_values() { + // values may be written to disk and must not change + assert_eq!(Viewtype::Unknown, Viewtype::default()); + assert_eq!(Viewtype::Unknown, Viewtype::from_i32(0).unwrap()); + assert_eq!(Viewtype::Text, Viewtype::from_i32(10).unwrap()); + assert_eq!(Viewtype::Image, Viewtype::from_i32(20).unwrap()); + assert_eq!(Viewtype::Gif, Viewtype::from_i32(21).unwrap()); + assert_eq!(Viewtype::Sticker, Viewtype::from_i32(23).unwrap()); + assert_eq!(Viewtype::Audio, Viewtype::from_i32(40).unwrap()); + assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap()); + assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap()); + assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap()); + assert_eq!( + Viewtype::VideochatInvitation, + Viewtype::from_i32(70).unwrap() + ); + assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap()); + assert_eq!(Viewtype::Vcard, Viewtype::from_i32(90).unwrap()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_send_quotes() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat = alice.create_chat(&bob).await; + + let sent = alice.send_text(chat.id, "> First quote").await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text, "> First quote"); + assert!(received.quoted_text().is_none()); + assert!(received.quoted_message(&bob).await?.is_none()); + + let sent = alice.send_text(chat.id, "> Second quote").await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text, "> Second quote"); + assert!(received.quoted_text().is_none()); + assert!(received.quoted_message(&bob).await?.is_none()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_message_summary_text() -> Result<()> { + let t = TestContext::new_alice().await; + let chat = t.get_self_chat().await; + let msg_id = send_text_msg(&t, chat.id, "foo".to_string()).await?; + let msg = Message::load_from_db(&t, msg_id).await?; + let summary = msg.get_summary(&t, None).await?; + assert_eq!(summary.text, "foo"); + + // message summary does not change when reactions are applied (in contrast to chatlist summary) + send_reaction(&t, msg_id, "🫵").await?; + let msg = Message::load_from_db(&t, msg_id).await?; + let summary = msg.get_summary(&t, None).await?; + assert_eq!(summary.text, "foo"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_format_flowed_round_trip() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let chat = alice.create_chat(&bob).await; + + let text = " Foo bar"; + let sent = alice.send_text(chat.id, text).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text, text); + + let text = "Foo bar baz"; + let sent = alice.send_text(chat.id, text).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text, text); + + let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A"; + let sent = alice.send_text(chat.id, text).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text, text); + + let python_program = "\ +def hello(): + return 'Hello, world!'"; + let sent = alice.send_text(chat.id, python_program).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text, python_program); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_delete_msgs_offline() -> Result<()> { + let alice = TestContext::new_alice().await; + let chat = alice + .create_chat_with_contact("Bob", "bob@example.org") + .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?); + delete_msgs(&alice, &[msg.id]).await?; + assert!(!alice.sql.exists(stmt, (msg.id,)).await?); + + Ok(()) +} diff --git a/src/webxdc.rs b/src/webxdc.rs index 1311dd0eb..4993def12 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -982,2208 +982,4 @@ impl Message { } #[cfg(test)] -mod tests { - use std::time::Duration; - - use regex::Regex; - use serde_json::json; - - use super::*; - use crate::chat::{ - add_contact_to_chat, create_broadcast_list, create_group_chat, forward_msgs, - remove_contact_from_chat, resend_msgs, send_msg, send_text_msg, ChatId, ProtectionStatus, - }; - use crate::chatlist::Chatlist; - use crate::config::Config; - use crate::contact::Contact; - use crate::download::DownloadState; - use crate::ephemeral; - use crate::receive_imf::{receive_imf, receive_imf_from_inbox}; - use crate::test_utils::{TestContext, TestContextManager}; - use crate::tools::{self, SystemTime}; - use crate::{message, sql}; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_is_webxdc_file() -> Result<()> { - let t = TestContext::new().await; - assert!( - !t.is_webxdc_file( - "bad-ext-no-zip.txt", - include_bytes!("../test-data/message/issue_523.txt") - ) - .await? - ); - assert!( - !t.is_webxdc_file( - "bad-ext-good-zip.txt", - include_bytes!("../test-data/webxdc/minimal.xdc") - ) - .await? - ); - assert!( - !t.is_webxdc_file( - "good-ext-no-zip.xdc", - include_bytes!("../test-data/message/issue_523.txt") - ) - .await? - ); - assert!( - !t.is_webxdc_file( - "good-ext-no-index-html.xdc", - include_bytes!("../test-data/webxdc/no-index-html.xdc") - ) - .await? - ); - assert!( - t.is_webxdc_file( - "good-ext-good-zip.xdc", - include_bytes!("../test-data/webxdc/minimal.xdc") - ) - .await? - ); - Ok(()) - } - - fn create_webxdc_instance(t: &TestContext, name: &str, bytes: &[u8]) -> Result { - let mut instance = Message::new(Viewtype::File); - instance.set_file_from_bytes(t, name, bytes, None)?; - Ok(instance) - } - - async fn send_webxdc_instance(t: &TestContext, chat_id: ChatId) -> Result { - let mut instance = create_webxdc_instance( - t, - "minimal.xdc", - include_bytes!("../test-data/webxdc/minimal.xdc"), - )?; - let instance_msg_id = send_msg(t, chat_id, &mut instance).await?; - assert_eq!(instance.viewtype, Viewtype::Webxdc); - Message::load_from_db(t, instance_msg_id).await - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_send_webxdc_instance() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - - // send as .xdc file - let instance = send_webxdc_instance(&t, chat_id).await?; - assert_eq!(instance.viewtype, Viewtype::Webxdc); - assert_eq!(instance.get_filename(), Some("minimal.xdc".to_string())); - assert_eq!(instance.chat_id, chat_id); - - // sending using bad extension is not working, even when setting Viewtype to webxdc - let mut instance = Message::new(Viewtype::Webxdc); - instance.set_file_from_bytes(&t, "index.html", b"ola!", None)?; - assert!(send_msg(&t, chat_id, &mut instance).await.is_err()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_send_invalid_webxdc() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - - // sending invalid .xdc as file is possible, but must not result in Viewtype::Webxdc - let mut instance = create_webxdc_instance( - &t, - "invalid-no-zip-but-7z.xdc", - include_bytes!("../test-data/webxdc/invalid-no-zip-but-7z.xdc"), - )?; - let instance_id = send_msg(&t, chat_id, &mut instance).await?; - assert_eq!(instance.viewtype, Viewtype::File); - let test = Message::load_from_db(&t, instance_id).await?; - assert_eq!(test.viewtype, Viewtype::File); - - // sending invalid .xdc as Viewtype::Webxdc should fail already on sending - let mut instance = Message::new(Viewtype::Webxdc); - instance.set_file_from_bytes( - &t, - "invalid2.xdc", - include_bytes!("../test-data/webxdc/invalid-no-zip-but-7z.xdc"), - None, - )?; - assert!(send_msg(&t, chat_id, &mut instance).await.is_err()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_send_special_webxdc_format() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - - // chess.xdc is failing for some zip-versions, see #3476, if we know more details about why, we can have a nicer name for the test :) - let mut instance = create_webxdc_instance( - &t, - "chess.xdc", - include_bytes!("../test-data/webxdc/chess.xdc"), - )?; - let instance_id = send_msg(&t, chat_id, &mut instance).await?; - let instance = Message::load_from_db(&t, instance_id).await?; - assert_eq!(instance.viewtype, Viewtype::Webxdc); - - let info = instance.get_webxdc_info(&t).await?; - assert_eq!(info.name, "Chess Board"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_forward_webxdc_instance() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let instance = send_webxdc_instance(&t, chat_id).await?; - t.send_webxdc_status_update( - instance.id, - r#"{"info": "foo", "summary":"bar", "document":"doc", "payload": 42}"#, - ) - .await?; - assert!(!instance.is_forwarded()); - assert_eq!( - t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":42,"info":"foo","document":"doc","summary":"bar","serial":1,"max_serial":1}]"# - ); - assert_eq!(chat_id.get_msg_cnt(&t).await?, 2); // instance and info - let info = Message::load_from_db(&t, instance.id) - .await? - .get_webxdc_info(&t) - .await?; - assert_eq!(info.summary, "bar".to_string()); - assert_eq!(info.document, "doc".to_string()); - - // forwarding an instance creates a fresh instance; updates etc. are not forwarded - forward_msgs(&t, &[instance.get_id()], chat_id).await?; - let instance2 = t.get_last_msg_in(chat_id).await; - assert!(instance2.is_forwarded()); - assert_eq!( - t.get_webxdc_status_updates(instance2.id, StatusUpdateSerial(0)) - .await?, - "[]" - ); - assert_eq!(chat_id.get_msg_cnt(&t).await?, 3); // two instances, only one info - let info = Message::load_from_db(&t, instance2.id) - .await? - .get_webxdc_info(&t) - .await?; - assert_eq!(info.summary, "".to_string()); - assert_eq!(info.document, "".to_string()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_resend_webxdc_instance_and_info() -> Result<()> { - let mut tcm = TestContextManager::new(); - - // Alice uses webxdc in a group - let alice = tcm.alice().await; - alice.set_config_bool(Config::BccSelf, false).await?; - let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; - let alice_instance = send_webxdc_instance(&alice, alice_grp).await?; - assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 1); - alice - .send_webxdc_status_update( - alice_instance.id, - r#"{"payload":7,"info": "i","summary":"s"}"#, - ) - .await?; - assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 2); - assert!(alice.get_last_msg_in(alice_grp).await.is_info()); - - // Alice adds Bob and resends already used webxdc - add_contact_to_chat( - &alice, - alice_grp, - Contact::create(&alice, "", "bob@example.net").await?, - ) - .await?; - assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 3); - resend_msgs(&alice, &[alice_instance.id]).await?; - let sent1 = alice.pop_sent_msg().await; - alice.flush_status_updates().await?; - let sent2 = alice.pop_sent_msg().await; - - // Bob receives webxdc, legacy info-messages updates are received and added to the chat. - let bob = tcm.bob().await; - let bob_instance = bob.recv_msg(&sent1).await; - bob.recv_msg_trash(&sent2).await; - assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); - assert!(!bob_instance.is_info()); - assert_eq!( - bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":7,"info":"i","summary":"s","serial":1,"max_serial":1}]"# - ); - let bob_grp = bob_instance.chat_id; - assert_eq!(bob.get_last_msg_in(bob_grp).await.id, bob_instance.id); - assert_eq!(bob_grp.get_msg_cnt(&bob).await?, 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_receive_webxdc_instance() -> Result<()> { - let t = TestContext::new_alice().await; - receive_imf( - &t, - include_bytes!("../test-data/message/webxdc_good_extension.eml"), - false, - ) - .await?; - let instance = t.get_last_msg().await; - assert_eq!(instance.viewtype, Viewtype::Webxdc); - assert_eq!(instance.get_filename().unwrap(), "minimal.xdc"); - - receive_imf( - &t, - include_bytes!("../test-data/message/webxdc_bad_extension.eml"), - false, - ) - .await?; - let instance = t.get_last_msg().await; - assert_eq!(instance.viewtype, Viewtype::File); // we require the correct extension, only a mime type is not sufficient - assert_eq!(instance.get_filename().unwrap(), "index.html"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_contact_request() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - // Alice sends an webxdc instance to Bob - let alice_chat = alice.create_chat(&bob).await; - let _alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; - bob.recv_msg(&alice.pop_sent_msg().await).await; - - // Bob can start the webxdc from a contact request (get index.html) - // but cannot send updates to contact requests - let bob_instance = bob.get_last_msg().await; - let bob_chat = Chat::load_from_db(&bob, bob_instance.chat_id).await?; - assert!(bob_chat.is_contact_request()); - assert!(bob_instance - .get_webxdc_blob(&bob, "index.html") - .await - .is_ok()); - assert!(bob - .send_webxdc_status_update(bob_instance.id, r#"{"payload":42}"#) - .await - .is_err()); - assert_eq!( - bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) - .await?, - "[]" - ); - - // Once the contact request is accepted, Bob can send updates - bob_chat.id.accept(&bob).await?; - assert!(bob - .send_webxdc_status_update(bob_instance.id, r#"{"payload":42}"#) - .await - .is_ok()); - assert_eq!( - bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":42,"serial":1,"max_serial":1}]"# - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_update_for_not_downloaded_instance() -> Result<()> { - // Alice sends a larger instance and an update - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let chat = alice.create_chat(&bob).await; - bob.set_config(Config::DownloadLimit, Some("40000")).await?; - let mut alice_instance = create_webxdc_instance( - &alice, - "chess.xdc", - include_bytes!("../test-data/webxdc/chess.xdc"), - )?; - let sent1 = alice.send_msg(chat.id, &mut alice_instance).await; - let alice_instance = sent1.load_from_db().await; - alice - .send_webxdc_status_update( - alice_instance.id, - r#"{"payload": 7, "summary":"sum", "document":"doc"}"#, - ) - .await?; - alice.flush_status_updates().await?; - let sent2 = alice.pop_sent_msg().await; - - // Bob does not download instance but already receives update - receive_imf_from_inbox( - &bob, - &alice_instance.rfc724_mid, - sent1.payload().as_bytes(), - false, - Some(70790), - false, - ) - .await?; - let bob_instance = bob.get_last_msg().await; - bob_instance.chat_id.accept(&bob).await?; - bob.recv_msg_trash(&sent2).await; - assert_eq!(bob_instance.download_state, DownloadState::Available); - - // Bob downloads instance, updates should be assigned correctly - let received_msg = receive_imf_from_inbox( - &bob, - &alice_instance.rfc724_mid, - sent1.payload().as_bytes(), - false, - None, - false, - ) - .await? - .unwrap(); - assert_eq!(*received_msg.msg_ids.first().unwrap(), bob_instance.id); - let bob_instance = bob.get_last_msg().await; - assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); - assert_eq!(bob_instance.download_state, DownloadState::Done); - assert_eq!( - bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":7,"document":"doc","summary":"sum","serial":1,"max_serial":1}]"# - ); - let info = bob_instance.get_webxdc_info(&bob).await?; - assert_eq!(info.document, "doc"); - assert_eq!(info.summary, "sum"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_delete_webxdc_instance() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let instance = send_webxdc_instance(&t, chat_id).await?; - let now = tools::time(); - t.receive_status_update( - ContactId::SELF, - &instance, - now, - true, - r#"{"updates":[{"payload":1}]}"#, - ) - .await?; - assert_eq!( - t.sql - .count("SELECT COUNT(*) FROM msgs_status_updates;", ()) - .await?, - 1 - ); - - message::delete_msgs(&t, &[instance.id]).await?; - sql::housekeeping(&t).await?; - assert_eq!( - t.sql - .count("SELECT COUNT(*) FROM msgs_status_updates;", ()) - .await?, - 0 - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_delete_chat_with_webxdc() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let instance = send_webxdc_instance(&t, chat_id).await?; - let now = tools::time(); - t.receive_status_update( - ContactId::SELF, - &instance, - now, - true, - r#"{"updates":[{"payload":1}, {"payload":2}]}"#, - ) - .await?; - assert_eq!( - t.sql - .count("SELECT COUNT(*) FROM msgs_status_updates;", ()) - .await?, - 2 - ); - - chat_id.delete(&t).await?; - sql::housekeeping(&t).await?; - assert_eq!( - t.sql - .count("SELECT COUNT(*) FROM msgs_status_updates;", ()) - .await?, - 0 - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_delete_webxdc_draft() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - - let mut instance = create_webxdc_instance( - &t, - "minimal.xdc", - include_bytes!("../test-data/webxdc/minimal.xdc"), - )?; - chat_id.set_draft(&t, Some(&mut instance)).await?; - let instance = chat_id.get_draft(&t).await?.unwrap(); - t.send_webxdc_status_update(instance.id, r#"{"payload": 42}"#) - .await?; - assert_eq!( - t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":42,"serial":1,"max_serial":1}]"#.to_string() - ); - - // set_draft(None) deletes the message without the need to simulate network - chat_id.set_draft(&t, None).await?; - assert_eq!( - t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) - .await?, - "[]".to_string() - ); - assert_eq!( - t.sql - .count("SELECT COUNT(*) FROM msgs_status_updates;", ()) - .await?, - 0 - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_create_status_update_record() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let instance = send_webxdc_instance(&t, chat_id).await?; - - assert_eq!( - t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) - .await?, - "[]" - ); - - let update_id1 = t - .create_status_update_record( - &instance, - StatusUpdateItem { - payload: json!({"foo": "bar"}), - info: None, - href: None, - document: None, - summary: None, - uid: Some("iecie2Ze".to_string()), - notify: None, - }, - 1640178619, - true, - ContactId::SELF, - ) - .await? - .unwrap(); - assert_eq!( - t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"# - ); - - // Update with duplicate update ID is received. - // Whatever the payload is, update should be ignored just because ID is duplicate. - let update_id1_duplicate = t - .create_status_update_record( - &instance, - StatusUpdateItem { - payload: json!({"nothing": "this should be ignored"}), - info: None, - href: None, - document: None, - summary: None, - uid: Some("iecie2Ze".to_string()), - notify: None, - }, - 1640178619, - true, - ContactId::SELF, - ) - .await?; - assert_eq!(update_id1_duplicate, None); - - assert!(t - .send_webxdc_status_update(instance.id, "\n\n\n") - .await - .is_err()); - - assert!(t - .send_webxdc_status_update(instance.id, "bad json") - .await - .is_err()); - - assert_eq!( - t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"# - ); - - let update_id2 = t - .create_status_update_record( - &instance, - StatusUpdateItem { - payload: json!({"foo2": "bar2"}), - info: None, - href: None, - document: None, - summary: None, - uid: None, - notify: None, - }, - 1640178619, - true, - ContactId::SELF, - ) - .await? - .unwrap(); - assert_eq!( - t.get_webxdc_status_updates(instance.id, update_id1).await?, - r#"[{"payload":{"foo2":"bar2"},"serial":3,"max_serial":3}]"# - ); - t.create_status_update_record( - &instance, - StatusUpdateItem { - payload: Value::Bool(true), - info: None, - href: None, - document: None, - summary: None, - uid: None, - notify: None, - }, - 1640178619, - true, - ContactId::SELF, - ) - .await?; - assert_eq!( - t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":4}, -{"payload":{"foo2":"bar2"},"serial":3,"max_serial":4}, -{"payload":true,"serial":4,"max_serial":4}]"# - ); - - t.send_webxdc_status_update( - instance.id, - r#"{"payload" : 1, "sender": "that is not used"}"#, - ) - .await?; - assert_eq!( - t.get_webxdc_status_updates(instance.id, update_id2).await?, - r#"[{"payload":true,"serial":4,"max_serial":5}, -{"payload":1,"serial":5,"max_serial":5}]"# - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_receive_status_update() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let instance = send_webxdc_instance(&t, chat_id).await?; - let now = tools::time(); - - assert!(t - .receive_status_update(ContactId::SELF, &instance, now, true, r#"foo: bar"#) - .await - .is_err()); // no json - assert!(t - .receive_status_update( - ContactId::SELF, - &instance, - now, - true, - r#"{"updada":[{"payload":{"foo":"bar"}}]}"# - ) - .await - .is_err()); // "updates" object missing - assert!(t - .receive_status_update( - ContactId::SELF, - &instance, - now, - true, - r#"{"updates":[{"foo":"bar"}]}"# - ) - .await - .is_err()); // "payload" field missing - assert!(t - .receive_status_update( - ContactId::SELF, - &instance, - now, - true, - r#"{"updates":{"payload":{"foo":"bar"}}}"# - ) - .await - .is_err()); // not an array - - t.receive_status_update( - ContactId::SELF, - &instance, - now, - true, - r#"{"updates":[{"payload":{"foo":"bar"}, "someTrash": "definitely TrAsH"}]}"#, - ) - .await?; - assert_eq!( - t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"# - ); - - t.receive_status_update( - ContactId::SELF, - &instance, - now, - true, - r#" {"updates": [ {"payload" :42} , {"payload": 23} ] } "#, - ) - .await?; - assert_eq!( - t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":3}, -{"payload":42,"serial":2,"max_serial":3}, -{"payload":23,"serial":3,"max_serial":3}]"# - ); - - t.receive_status_update( - ContactId::SELF, - &instance, - now, - true, - r#" {"updates": [ {"payload" :"ok", "future_item": "test"} ], "from": "future" } "#, - ) - .await?; // ignore members that may be added in the future - assert_eq!( - t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":4}, -{"payload":42,"serial":2,"max_serial":4}, -{"payload":23,"serial":3,"max_serial":4}, -{"payload":"ok","serial":4,"max_serial":4}]"# - ); - - Ok(()) - } - - async fn expect_status_update_event(t: &TestContext, instance_id: MsgId) -> Result<()> { - let event = t - .evtracker - .get_matching(|evt| matches!(evt, EventType::WebxdcStatusUpdate { .. })) - .await; - match event { - EventType::WebxdcStatusUpdate { - msg_id, - status_update_serial: _, - } => { - assert_eq!(msg_id, instance_id); - } - _ => unreachable!(), - } - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_send_webxdc_status_update() -> Result<()> { - let alice = TestContext::new_alice().await; - alice.set_config_bool(Config::BccSelf, true).await?; - let bob = TestContext::new_bob().await; - - // Alice sends an webxdc instance and a status update - let alice_chat = alice.create_chat(&bob).await; - let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; - let sent1 = &alice.pop_sent_msg().await; - assert_eq!(alice_instance.viewtype, Viewtype::Webxdc); - assert!(!sent1.payload().contains("report-type=status-update")); - - alice - .send_webxdc_status_update(alice_instance.id, r#"{"payload" : {"foo":"bar"}}"#) - .await?; - alice.flush_status_updates().await?; - expect_status_update_event(&alice, alice_instance.id).await?; - let sent2 = &alice.pop_sent_msg().await; - let alice_update = sent2.load_from_db().await; - assert!(alice_update.hidden); - assert_eq!(alice_update.viewtype, Viewtype::Text); - assert_eq!(alice_update.get_filename(), None); - assert_eq!(alice_update.text, BODY_DESCR.to_string()); - assert_eq!(alice_update.chat_id, alice_instance.chat_id); - assert_eq!( - alice_update.parent(&alice).await?.unwrap().id, - alice_instance.id - ); - assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1); - assert!(sent2.payload().contains("report-type=status-update")); - assert!(sent2.payload().contains(BODY_DESCR)); - assert_eq!( - alice - .get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"# - ); - - alice - .send_webxdc_status_update(alice_instance.id, r#"{"payload":{"snipp":"snapp"}}"#) - .await?; - assert_eq!( - alice - .get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2}, -{"payload":{"snipp":"snapp"},"serial":2,"max_serial":2}]"# - ); - - // Bob receives all messages - let bob_instance = bob.recv_msg(sent1).await; - let bob_chat_id = bob_instance.chat_id; - assert_eq!(bob_instance.rfc724_mid, alice_instance.rfc724_mid); - assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); - assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1); - - let bob_received_update = bob.recv_msg_opt(sent2).await; - assert!(bob_received_update.is_none()); - expect_status_update_event(&bob, bob_instance.id).await?; - assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1); - - assert_eq!( - bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"# - ); - - // Alice has a second device and also receives messages there - let alice2 = TestContext::new_alice().await; - alice2.recv_msg(sent1).await; - alice2.recv_msg_trash(sent2).await; - let alice2_instance = alice2.get_last_msg().await; - let alice2_chat_id = alice2_instance.chat_id; - assert_eq!(alice2_instance.viewtype, Viewtype::Webxdc); - assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 1); - - // To support the second device, Alice has enabled bcc_self and will receive their own messages; - // these messages, however, should be ignored - alice.recv_msg_opt(sent1).await; - alice.recv_msg_opt(sent2).await; - assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1); - assert_eq!( - alice - .get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2}, -{"payload":{"snipp":"snapp"},"serial":2,"max_serial":2}]"# - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_send_big_webxdc_status_update() -> Result<()> { - let alice = TestContext::new_alice().await; - alice.set_config_bool(Config::BccSelf, true).await?; - let bob = TestContext::new_bob().await; - - let alice_chat = alice.create_chat(&bob).await; - let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; - let sent1 = &alice.pop_sent_msg().await; - assert_eq!(alice_instance.viewtype, Viewtype::Webxdc); - assert!(!sent1.payload().contains("report-type=status-update")); - - let update1_str = r#"{"payload":{"foo":""#.to_string() - + &String::from_utf8(vec![b'a'; STATUS_UPDATE_SIZE_MAX])? - + r#""}"#; - alice - .send_webxdc_status_update(alice_instance.id, &(update1_str.clone() + "}")) - .await?; - alice - .send_webxdc_status_update(alice_instance.id, r#"{"payload" : {"foo":"bar2"}}"#) - .await?; - alice - .send_webxdc_status_update(alice_instance.id, r#"{"payload" : {"foo":"bar3"}}"#) - .await?; - alice.flush_status_updates().await?; - - // There's the message stack, so we pop messages in the reverse order. - let sent3 = &alice.pop_sent_msg().await; - let alice_update = sent3.load_from_db().await; - assert_eq!(alice_update.text, BODY_DESCR.to_string()); - let sent2 = &alice.pop_sent_msg().await; - let alice_update = sent2.load_from_db().await; - assert_eq!(alice_update.text, BODY_DESCR.to_string()); - assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1); - - // Bob receives the instance. - let bob_instance = bob.recv_msg(sent1).await; - let bob_chat_id = bob_instance.chat_id; - assert_eq!(bob_instance.rfc724_mid, alice_instance.rfc724_mid); - assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); - assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1); - - // Bob receives the status updates. - bob.recv_msg_trash(sent2).await; - expect_status_update_event(&bob, bob_instance.id).await?; - assert_eq!( - bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) - .await?, - "[".to_string() + &update1_str + r#","serial":1,"max_serial":1}]"# - ); - bob.recv_msg_trash(sent3).await; - for _ in 0..2 { - expect_status_update_event(&bob, bob_instance.id).await?; - } - assert_eq!( - bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(1)) - .await?, - r#"[{"payload":{"foo":"bar2"},"serial":2,"max_serial":3}, -{"payload":{"foo":"bar3"},"serial":3,"max_serial":3}]"# - ); - assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_render_webxdc_status_update_object() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?; - let mut instance = create_webxdc_instance( - &t, - "minimal.xdc", - include_bytes!("../test-data/webxdc/minimal.xdc"), - )?; - chat_id.set_draft(&t, Some(&mut instance)).await?; - let (first, last) = (StatusUpdateSerial(1), StatusUpdateSerial::MAX); - assert_eq!( - t.render_webxdc_status_update_object(instance.id, first, last, None) - .await?, - (None, StatusUpdateSerial(u32::MAX)) - ); - - t.send_webxdc_status_update(instance.id, r#"{"payload": 1}"#) - .await?; - let (object, first_new) = t - .render_webxdc_status_update_object(instance.id, first, last, None) - .await?; - assert!(object.is_some()); - assert_eq!(first_new, StatusUpdateSerial(u32::MAX)); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_render_webxdc_status_update_object_range() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?; - let instance = send_webxdc_instance(&t, chat_id).await?; - t.send_webxdc_status_update(instance.id, r#"{"payload": 1}"#) - .await?; - t.send_webxdc_status_update(instance.id, r#"{"payload": 2}"#) - .await?; - t.send_webxdc_status_update(instance.id, r#"{"payload": 3}"#) - .await?; - t.send_webxdc_status_update(instance.id, r#"{"payload": 4}"#) - .await?; - let (json, first_new) = t - .render_webxdc_status_update_object( - instance.id, - StatusUpdateSerial(2), - StatusUpdateSerial(3), - None, - ) - .await?; - let json = json.unwrap(); - assert_eq!(first_new, StatusUpdateSerial(4)); - let json = Regex::new(r#""uid":"[^"]*""#) - .unwrap() - .replace_all(&json, "XXX"); - assert_eq!( - json, - "{\"updates\":[{\"payload\":2,XXX},\n{\"payload\":3,XXX}]}" - ); - - assert_eq!( - t.sql - .count("SELECT COUNT(*) FROM smtp_status_updates", ()) - .await?, - 1 - ); - t.flush_status_updates().await?; - assert_eq!( - t.sql - .count("SELECT COUNT(*) FROM smtp_status_updates", ()) - .await?, - 0 - ); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_pop_status_update() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?; - let instance1 = send_webxdc_instance(&t, chat_id).await?; - let instance2 = send_webxdc_instance(&t, chat_id).await?; - let instance3 = send_webxdc_instance(&t, chat_id).await?; - assert!(t.smtp_status_update_get().await?.is_none()); - - t.send_webxdc_status_update(instance1.id, r#"{"payload": "1a"}"#) - .await?; - t.send_webxdc_status_update(instance2.id, r#"{"payload": "2a"}"#) - .await?; - t.send_webxdc_status_update(instance2.id, r#"{"payload": "2b"}"#) - .await?; - t.send_webxdc_status_update(instance3.id, r#"{"payload": "3a"}"#) - .await?; - t.send_webxdc_status_update(instance3.id, r#"{"payload": "3b"}"#) - .await?; - t.send_webxdc_status_update(instance3.id, r#"{"payload": "3c"}"#) - .await?; - assert_eq!( - t.sql - .count("SELECT COUNT(*) FROM smtp_status_updates", ()) - .await?, - 3 - ); - - // order of smtp_status_update_get() is not defined, therefore the more complicated test - let mut instances_checked = 0; - for i in 0..3 { - let (instance, min_ser, max_ser) = t.smtp_status_update_get().await?.unwrap(); - t.smtp_status_update_pop_serials( - instance, - min_ser, - StatusUpdateSerial::new(max_ser.to_u32().checked_add(1).unwrap()), - ) - .await?; - let min_ser: u32 = min_ser.try_into()?; - if instance == instance1.id { - assert_eq!(min_ser, max_ser.to_u32()); - - instances_checked += 1; - } else if instance == instance2.id { - assert_eq!(min_ser, max_ser.to_u32() - 1); - - instances_checked += 1; - } else if instance == instance3.id { - assert_eq!(min_ser, max_ser.to_u32() - 2); - instances_checked += 1; - } else { - bail!("unexpected instance"); - } - assert_eq!( - t.sql - .count("SELECT COUNT(*) FROM smtp_status_updates", ()) - .await?, - 2 - i - ); - } - assert_eq!(instances_checked, 3); - assert!(t.smtp_status_update_get().await?.is_none()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_draft_and_send_webxdc_status_update() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat_id = alice.create_chat(&bob).await.id; - - // prepare webxdc instance, - // status updates are not sent for drafts, therefore send_webxdc_status_update() returns Ok(None) - let mut alice_instance = create_webxdc_instance( - &alice, - "minimal.xdc", - include_bytes!("../test-data/webxdc/minimal.xdc"), - )?; - alice_chat_id - .set_draft(&alice, Some(&mut alice_instance)) - .await?; - let mut alice_instance = alice_chat_id.get_draft(&alice).await?.unwrap(); - - alice - .send_webxdc_status_update(alice_instance.id, r#"{"payload": {"foo":"bar"}}"#) - .await?; - expect_status_update_event(&alice, alice_instance.id).await?; - alice - .send_webxdc_status_update(alice_instance.id, r#"{"payload":42, "info":"i"}"#) - .await?; - expect_status_update_event(&alice, alice_instance.id).await?; - assert_eq!( - alice - .sql - .count("SELECT COUNT(*) FROM smtp_status_updates", ()) - .await?, - 0 - ); - assert!(!alice.get_last_msg().await.is_info()); // 'info: "i"' message not added in draft mode - - // send webxdc instance, - // the initial status updates are sent together in the same message - let alice_instance_id = send_msg(&alice, alice_chat_id, &mut alice_instance).await?; - let sent1 = alice.pop_sent_msg().await; - let alice_instance = Message::load_from_db(&alice, alice_instance_id).await?; - assert_eq!(alice_instance.viewtype, Viewtype::Webxdc); - assert_eq!( - alice_instance.get_filename(), - Some("minimal.xdc".to_string()) - ); - assert_eq!(alice_instance.chat_id, alice_chat_id); - - // bob receives the instance together with the initial updates in a single message - let bob_instance = bob.recv_msg(&sent1).await; - assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); - assert_eq!(bob_instance.get_filename().unwrap(), "minimal.xdc"); - assert!(sent1.payload().contains("Content-Type: application/json")); - assert!(sent1.payload().contains("status-update.json")); - assert_eq!( - bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2}, -{"payload":42,"info":"i","serial":2,"max_serial":2}]"# - ); - assert!(!bob.get_last_msg().await.is_info()); // 'info: "i"' message not added in draft mode - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_send_webxdc_status_update_to_non_webxdc() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let msg_id = send_text_msg(&t, chat_id, "ho!".to_string()).await?; - assert!(t - .send_webxdc_status_update(msg_id, r#"{"foo":"bar"}"#) - .await - .is_err()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_webxdc_blob() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let instance = send_webxdc_instance(&t, chat_id).await?; - - let buf = instance.get_webxdc_blob(&t, "index.html").await?; - assert_eq!(buf.len(), 188); - assert!(String::from_utf8_lossy(&buf).contains("document.write")); - - assert!(instance - .get_webxdc_blob(&t, "not-existent.html") - .await - .is_err()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_webxdc_blob_default_icon() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let instance = send_webxdc_instance(&t, chat_id).await?; - - let buf = instance.get_webxdc_blob(&t, WEBXDC_DEFAULT_ICON).await?; - assert!(buf.len() > 100); - assert!(String::from_utf8_lossy(&buf).contains("PNG\r\n")); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_webxdc_blob_with_absolute_paths() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let instance = send_webxdc_instance(&t, chat_id).await?; - - let buf = instance.get_webxdc_blob(&t, "/index.html").await?; - assert!(String::from_utf8_lossy(&buf).contains("document.write")); - - assert!(instance.get_webxdc_blob(&t, "/not-there").await.is_err()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_webxdc_blob_with_subdirs() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let mut instance = create_webxdc_instance( - &t, - "some-files.xdc", - include_bytes!("../test-data/webxdc/some-files.xdc"), - )?; - chat_id.set_draft(&t, Some(&mut instance)).await?; - - let buf = instance.get_webxdc_blob(&t, "index.html").await?; - assert_eq!(buf.len(), 65); - assert!(String::from_utf8_lossy(&buf).contains("many files")); - - let buf = instance.get_webxdc_blob(&t, "subdir/bla.txt").await?; - assert_eq!(buf.len(), 4); - assert!(String::from_utf8_lossy(&buf).starts_with("bla")); - - let buf = instance - .get_webxdc_blob(&t, "subdir/subsubdir/text.md") - .await?; - assert_eq!(buf.len(), 24); - assert!(String::from_utf8_lossy(&buf).starts_with("this is a markdown file")); - - let buf = instance - .get_webxdc_blob(&t, "subdir/subsubdir/text2.md") - .await?; - assert_eq!(buf.len(), 22); - assert!(String::from_utf8_lossy(&buf).starts_with("another markdown")); - - let buf = instance - .get_webxdc_blob(&t, "anotherdir/anothersubsubdir/foo.txt") - .await?; - assert_eq!(buf.len(), 4); - assert!(String::from_utf8_lossy(&buf).starts_with("foo")); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_webxdc_manifest() -> Result<()> { - let result = parse_webxdc_manifest(r#"key = syntax error"#.as_bytes()); - assert!(result.is_err()); - - let manifest = parse_webxdc_manifest(r#"no_name = "no name, no icon""#.as_bytes())?; - assert_eq!(manifest.name, None); - - let manifest = parse_webxdc_manifest(r#"name = "name, no icon""#.as_bytes())?; - assert_eq!(manifest.name, Some("name, no icon".to_string())); - - let manifest = parse_webxdc_manifest( - r#"name = "foo" -icon = "bar""# - .as_bytes(), - )?; - assert_eq!(manifest.name, Some("foo".to_string())); - - let manifest = parse_webxdc_manifest( - r#"name = "foz" -icon = "baz" -add_item = "that should be just ignored" - -[section] -sth_for_the = "future""# - .as_bytes(), - )?; - assert_eq!(manifest.name, Some("foz".to_string())); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_webxdc_manifest_min_api() -> Result<()> { - let manifest = parse_webxdc_manifest(r#"min_api = 3"#.as_bytes())?; - assert_eq!(manifest.min_api, Some(3)); - - let result = parse_webxdc_manifest(r#"min_api = "1""#.as_bytes()); - assert!(result.is_err()); - - let result = parse_webxdc_manifest(r#"min_api = 1.2"#.as_bytes()); - assert!(result.is_err()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_webxdc_manifest_source_code_url() -> Result<()> { - let result = parse_webxdc_manifest(r#"source_code_url = 3"#.as_bytes()); - assert!(result.is_err()); - - let manifest = parse_webxdc_manifest(r#"source_code_url = "https://foo.bar""#.as_bytes())?; - assert_eq!( - manifest.source_code_url, - Some("https://foo.bar".to_string()) - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_min_api_too_large() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?; - let mut instance = create_webxdc_instance( - &t, - "with-min-api-1001.xdc", - include_bytes!("../test-data/webxdc/with-min-api-1001.xdc"), - )?; - send_msg(&t, chat_id, &mut instance).await?; - - let instance = t.get_last_msg().await; - let html = instance.get_webxdc_blob(&t, "index.html").await?; - assert!(String::from_utf8_lossy(&html).contains("requires a newer Delta Chat version")); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_webxdc_info() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - - let instance = send_webxdc_instance(&t, chat_id).await?; - let info = instance.get_webxdc_info(&t).await?; - assert_eq!(info.name, "minimal.xdc"); - assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string()); - assert_eq!(info.send_update_interval, 10000); - assert_eq!(info.send_update_max_size, RECOMMENDED_FILE_SIZE as usize); - - let mut instance = create_webxdc_instance( - &t, - "with-manifest-empty-name.xdc", - include_bytes!("../test-data/webxdc/with-manifest-empty-name.xdc"), - )?; - chat_id.set_draft(&t, Some(&mut instance)).await?; - let info = instance.get_webxdc_info(&t).await?; - assert_eq!(info.name, "with-manifest-empty-name.xdc"); - assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string()); - - let mut instance = create_webxdc_instance( - &t, - "with-manifest-no-name.xdc", - include_bytes!("../test-data/webxdc/with-manifest-no-name.xdc"), - )?; - chat_id.set_draft(&t, Some(&mut instance)).await?; - let info = instance.get_webxdc_info(&t).await?; - assert_eq!(info.name, "with-manifest-no-name.xdc"); - assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string()); - - let mut instance = create_webxdc_instance( - &t, - "with-minimal-manifest.xdc", - include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc"), - )?; - chat_id.set_draft(&t, Some(&mut instance)).await?; - let info = instance.get_webxdc_info(&t).await?; - assert_eq!(info.name, "nice app!"); - assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string()); - - let mut instance = create_webxdc_instance( - &t, - "with-manifest-and-png-icon.xdc", - include_bytes!("../test-data/webxdc/with-manifest-and-png-icon.xdc"), - )?; - chat_id.set_draft(&t, Some(&mut instance)).await?; - let info = instance.get_webxdc_info(&t).await?; - assert_eq!(info.name, "with some icon"); - assert_eq!(info.icon, "icon.png"); - - let mut instance = create_webxdc_instance( - &t, - "with-png-icon.xdc", - include_bytes!("../test-data/webxdc/with-png-icon.xdc"), - )?; - chat_id.set_draft(&t, Some(&mut instance)).await?; - let info = instance.get_webxdc_info(&t).await?; - assert_eq!(info.name, "with-png-icon.xdc"); - assert_eq!(info.icon, "icon.png"); - - let mut instance = create_webxdc_instance( - &t, - "with-jpg-icon.xdc", - include_bytes!("../test-data/webxdc/with-jpg-icon.xdc"), - )?; - chat_id.set_draft(&t, Some(&mut instance)).await?; - let info = instance.get_webxdc_info(&t).await?; - assert_eq!(info.name, "with-jpg-icon.xdc"); - assert_eq!(info.icon, "icon.jpg"); - - let msg_id = send_text_msg(&t, chat_id, "foo".to_string()).await?; - let msg = Message::load_from_db(&t, msg_id).await?; - let result = msg.get_webxdc_info(&t).await; - assert!(result.is_err()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_webxdc_self_addr() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - - let instance = send_webxdc_instance(&t, chat_id).await?; - let info1 = instance.get_webxdc_info(&t).await?; - let instance = send_webxdc_instance(&t, chat_id).await?; - let info2 = instance.get_webxdc_info(&t).await?; - - let real_addr = t.get_primary_self_addr().await?; - assert!(!info1.self_addr.contains(&real_addr)); - assert_ne!(info1.self_addr, info2.self_addr); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_info_summary() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - // Alice creates an webxdc instance and updates summary - let alice_chat = alice.create_chat(&bob).await; - let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; - let sent_instance = &alice.pop_sent_msg().await; - let info = alice_instance.get_webxdc_info(&alice).await?; - assert_eq!(info.summary, "".to_string()); - - alice - .send_webxdc_status_update(alice_instance.id, r#"{"summary":"sum: 1", "payload":1}"#) - .await?; - alice.flush_status_updates().await?; - let sent_update1 = &alice.pop_sent_msg().await; - let info = Message::load_from_db(&alice, alice_instance.id) - .await? - .get_webxdc_info(&alice) - .await?; - assert_eq!(info.summary, "sum: 1".to_string()); - - alice - .send_webxdc_status_update(alice_instance.id, r#"{"summary":"sum: 2", "payload":2}"#) - .await?; - alice.flush_status_updates().await?; - let sent_update2 = &alice.pop_sent_msg().await; - let info = Message::load_from_db(&alice, alice_instance.id) - .await? - .get_webxdc_info(&alice) - .await?; - assert_eq!(info.summary, "sum: 2".to_string()); - - // Bob receives the updates - let bob_instance = bob.recv_msg(sent_instance).await; - bob.recv_msg_trash(sent_update1).await; - bob.recv_msg_trash(sent_update2).await; - let info = Message::load_from_db(&bob, bob_instance.id) - .await? - .get_webxdc_info(&bob) - .await?; - assert_eq!(info.summary, "sum: 2".to_string()); - - // Alice has a second device and also receives the updates there - let alice2 = TestContext::new_alice().await; - let alice2_instance = alice2.recv_msg(sent_instance).await; - alice2.recv_msg_trash(sent_update1).await; - alice2.recv_msg_trash(sent_update2).await; - let info = Message::load_from_db(&alice2, alice2_instance.id) - .await? - .get_webxdc_info(&alice2) - .await?; - assert_eq!(info.summary, "sum: 2".to_string()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_document_name() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - // Alice creates an webxdc instance and updates document name - let alice_chat = alice.create_chat(&bob).await; - let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; - let sent_instance = &alice.pop_sent_msg().await; - let info = alice_instance.get_webxdc_info(&alice).await?; - assert_eq!(info.document, "".to_string()); - assert_eq!(info.summary, "".to_string()); - - alice - .send_webxdc_status_update( - alice_instance.id, - r#"{"document":"my file", "payload":1337}"#, - ) - .await?; - alice.flush_status_updates().await?; - let sent_update1 = &alice.pop_sent_msg().await; - let info = Message::load_from_db(&alice, alice_instance.id) - .await? - .get_webxdc_info(&alice) - .await?; - assert_eq!(info.document, "my file".to_string()); - assert_eq!(info.summary, "".to_string()); - - // Bob receives the updates - let bob_instance = bob.recv_msg(sent_instance).await; - bob.recv_msg_trash(sent_update1).await; - let info = Message::load_from_db(&bob, bob_instance.id) - .await? - .get_webxdc_info(&bob) - .await?; - assert_eq!(info.document, "my file".to_string()); - assert_eq!(info.summary, "".to_string()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_info_msg() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - // Alice sends update with an info message - let alice_chat = alice.create_chat(&bob).await; - let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; - let sent1 = &alice.pop_sent_msg().await; - assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1); - - alice - .send_webxdc_status_update( - alice_instance.id, - r#"{"info":"this appears in-chat", "payload":"sth. else"}"#, - ) - .await?; - alice.flush_status_updates().await?; - let sent2 = &alice.pop_sent_msg().await; - assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2); - let info_msg = alice.get_last_msg().await; - assert!(info_msg.is_info()); - assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage); - assert_eq!(info_msg.from_id, ContactId::SELF); - assert_eq!(info_msg.get_text(), "this appears in-chat"); - assert_eq!( - info_msg.parent(&alice).await?.unwrap().id, - alice_instance.id - ); - assert!(info_msg.quoted_message(&alice).await?.is_none()); - assert_eq!( - alice - .get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"# - ); - - // Bob receives all messages - let bob_instance = bob.recv_msg(sent1).await; - let bob_chat_id = bob_instance.chat_id; - bob.recv_msg_trash(sent2).await; - assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2); - let info_msg = bob.get_last_msg().await; - assert!(info_msg.is_info()); - assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage); - assert!(!info_msg.from_id.is_special()); - assert_eq!(info_msg.get_text(), "this appears in-chat"); - assert_eq!(info_msg.parent(&bob).await?.unwrap().id, bob_instance.id); - assert!(info_msg.quoted_message(&bob).await?.is_none()); - assert_eq!( - bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"# - ); - - // Alice has a second device and also receives the info message there - let alice2 = TestContext::new_alice().await; - let alice2_instance = alice2.recv_msg(sent1).await; - let alice2_chat_id = alice2_instance.chat_id; - alice2.recv_msg_trash(sent2).await; - assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 2); - let info_msg = alice2.get_last_msg().await; - assert!(info_msg.is_info()); - assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage); - assert_eq!(info_msg.from_id, ContactId::SELF); - assert_eq!(info_msg.get_text(), "this appears in-chat"); - assert_eq!( - info_msg.parent(&alice2).await?.unwrap().id, - alice2_instance.id - ); - assert!(info_msg.quoted_message(&alice2).await?.is_none()); - assert_eq!( - alice2 - .get_webxdc_status_updates(alice2_instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"# - ); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_info_msg_cleanup_series() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat = alice.create_chat(&bob).await; - let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; - let sent1 = &alice.pop_sent_msg().await; - - // Alice sends two info messages in a row; - // the second one removes the first one as there is nothing in between - alice - .send_webxdc_status_update(alice_instance.id, r#"{"info":"i1", "payload":1}"#) - .await?; - alice.flush_status_updates().await?; - let sent2 = &alice.pop_sent_msg().await; - assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2); - alice - .send_webxdc_status_update(alice_instance.id, r#"{"info":"i2", "payload":2}"#) - .await?; - alice.flush_status_updates().await?; - let sent3 = &alice.pop_sent_msg().await; - assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2); - let info_msg = alice.get_last_msg().await; - assert_eq!(info_msg.get_text(), "i2"); - - // When Bob receives the messages, they should be cleaned up as well - let bob_instance = bob.recv_msg(sent1).await; - let bob_chat_id = bob_instance.chat_id; - bob.recv_msg_trash(sent2).await; - assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2); - bob.recv_msg_trash(sent3).await; - assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2); - let info_msg = bob.get_last_msg().await; - assert_eq!(info_msg.get_text(), "i2"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_info_msg_no_cleanup_on_interrupted_series() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "c").await?; - let instance = send_webxdc_instance(&t, chat_id).await?; - - t.send_webxdc_status_update(instance.id, r#"{"info":"i1", "payload":1}"#) - .await?; - assert_eq!(chat_id.get_msg_cnt(&t).await?, 2); - send_text_msg(&t, chat_id, "msg between info".to_string()).await?; - assert_eq!(chat_id.get_msg_cnt(&t).await?, 3); - t.send_webxdc_status_update(instance.id, r#"{"info":"i2", "payload":2}"#) - .await?; - assert_eq!(chat_id.get_msg_cnt(&t).await?, 4); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_opportunistic_encryption() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - // Bob sends sth. to Alice, Alice has Bob's key - let bob_chat_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "chat").await?; - add_contact_to_chat( - &bob, - bob_chat_id, - Contact::create(&bob, "", "alice@example.org").await?, - ) - .await?; - send_text_msg(&bob, bob_chat_id, "populate".to_string()).await?; - alice.recv_msg(&bob.pop_sent_msg().await).await; - - // Alice sends instance+update to Bob - let alice_chat_id = alice.get_last_msg().await.chat_id; - alice_chat_id.accept(&alice).await?; - let alice_instance = send_webxdc_instance(&alice, alice_chat_id).await?; - let sent1 = &alice.pop_sent_msg().await; - alice - .send_webxdc_status_update(alice_instance.id, r#"{"payload":42}"#) - .await?; - alice.flush_status_updates().await?; - let sent2 = &alice.pop_sent_msg().await; - let update_msg = sent2.load_from_db().await; - assert!(alice_instance.get_showpadlock()); - assert!(update_msg.get_showpadlock()); - - // Bob receives instance+update - let bob_instance = bob.recv_msg(sent1).await; - bob.recv_msg_trash(sent2).await; - assert!(bob_instance.get_showpadlock()); - - // Bob adds Claire with unknown key, update to Alice+Claire cannot be encrypted - add_contact_to_chat( - &bob, - bob_chat_id, - Contact::create(&bob, "", "claire@example.org").await?, - ) - .await?; - bob.send_webxdc_status_update(bob_instance.id, r#"{"payload":43}"#) - .await?; - bob.flush_status_updates().await?; - let sent3 = bob.pop_sent_msg().await; - let update_msg = sent3.load_from_db().await; - assert!(!update_msg.get_showpadlock()); - - Ok(()) - } - - // check that `info.internet_access` is not set for normal, non-integrated webxdc - - // even if they use the deprecated option `request_internet_access` in manifest.toml - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_no_internet_access() -> Result<()> { - let t = TestContext::new_alice().await; - let self_id = t.get_self_chat().await.id; - let single_id = t.create_chat_with_contact("bob", "bob@e.com").await.id; - let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?; - let broadcast_id = create_broadcast_list(&t).await?; - - for e2ee in ["1", "0"] { - t.set_config(Config::E2eeEnabled, Some(e2ee)).await?; - for chat_id in [self_id, single_id, group_id, broadcast_id] { - for internet_xdc in [true, false] { - let mut instance = create_webxdc_instance( - &t, - "foo.xdc", - if internet_xdc { - include_bytes!("../test-data/webxdc/request-internet-access.xdc") - } else { - include_bytes!("../test-data/webxdc/minimal.xdc") - }, - )?; - let instance_id = send_msg(&t, chat_id, &mut instance).await?; - t.send_webxdc_status_update( - instance_id, - r#"{"summary":"real summary", "payload": 42}"#, - ) - .await?; - let instance = Message::load_from_db(&t, instance_id).await?; - let info = instance.get_webxdc_info(&t).await?; - assert_eq!(info.internet_access, false); - } - } - } - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_chatlist_summary() -> Result<()> { - let t = TestContext::new_alice().await; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?; - let mut instance = create_webxdc_instance( - &t, - "with-minimal-manifest.xdc", - include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc"), - )?; - send_msg(&t, chat_id, &mut instance).await?; - - let chatlist = Chatlist::try_load(&t, 0, None, None).await?; - assert_eq!(chatlist.len(), 1); - let summary = chatlist.get_summary(&t, 0, None).await?; - assert_eq!(summary.text, "nice app!".to_string()); - assert_eq!(summary.thumbnail_path.unwrap(), "webxdc-icon://last-msg-id"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_and_text() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - - // Alice sends instance and adds some text - let alice_chat = alice.create_chat(&bob).await; - let mut alice_instance = create_webxdc_instance( - &alice, - "minimal.xdc", - include_bytes!("../test-data/webxdc/minimal.xdc"), - )?; - alice_instance.set_text("user added text".to_string()); - send_msg(&alice, alice_chat.id, &mut alice_instance).await?; - let alice_instance = alice.get_last_msg().await; - assert_eq!(alice_instance.get_text(), "user added text"); - - // Bob receives that instance - let sent1 = alice.pop_sent_msg().await; - let bob_instance = bob.recv_msg(&sent1).await; - assert_eq!(bob_instance.get_text(), "user added text"); - - // Alice's second device receives the instance as well - let alice2 = TestContext::new_alice().await; - let alice2_instance = alice2.recv_msg(&sent1).await; - assert_eq!(alice2_instance.get_text(), "user added text"); - - Ok(()) - } - - async fn helper_send_receive_status_update( - bob: &TestContext, - alice: &TestContext, - bob_instance: &Message, - alice_instance: &Message, - ) -> Result { - bob.send_webxdc_status_update( - bob_instance.id, - r#"{"payload":7,"info": "i","summary":"s"}"#, - ) - .await?; - bob.flush_status_updates().await?; - let msg = bob.pop_sent_msg().await; - alice.recv_msg_trash(&msg).await; - alice - .get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0)) - .await - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_reject_updates_from_non_groupmembers() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let contact_bob = Contact::create(&alice, "Bob", "bob@example.net").await?; - let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; - add_contact_to_chat(&alice, chat_id, contact_bob).await?; - let instance = send_webxdc_instance(&alice, chat_id).await?; - bob.recv_msg(&alice.pop_sent_msg().await).await; - let bob_instance = bob.get_last_msg().await; - Chat::load_from_db(&bob, bob_instance.chat_id) - .await? - .id - .accept(&bob) - .await?; - - let status = - helper_send_receive_status_update(&bob, &alice, &bob_instance, &instance).await?; - assert_eq!( - status, - r#"[{"payload":7,"info":"i","summary":"s","serial":1,"max_serial":1}]"# - ); - - remove_contact_from_chat(&alice, chat_id, contact_bob).await?; - alice.pop_sent_msg().await; - let status = - helper_send_receive_status_update(&bob, &alice, &bob_instance, &instance).await?; - - assert_eq!( - status, - r#"[{"payload":7,"info":"i","summary":"s","serial":1,"max_serial":1}]"# - ); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_delete_event() -> Result<()> { - let alice = TestContext::new_alice().await; - let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; - let instance = send_webxdc_instance(&alice, chat_id).await?; - message::delete_msgs(&alice, &[instance.id]).await?; - alice - .evtracker - .get_matching(|evt| matches!(evt, EventType::WebxdcInstanceDeleted { .. })) - .await; - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn change_logging_webxdc() -> Result<()> { - let alice = TestContext::new_alice().await; - let chat_id = ChatId::create_for_contact(&alice, ContactId::SELF).await?; - - assert_eq!( - alice - .sql - .count("SELECT COUNT(*) FROM msgs_status_updates;", ()) - .await?, - 0 - ); - - let mut instance = create_webxdc_instance( - &alice, - "debug_logging.xdc", - include_bytes!("../test-data/webxdc/minimal.xdc"), - )?; - assert!(alice.debug_logging.read().unwrap().is_none()); - send_msg(&alice, chat_id, &mut instance).await?; - assert!(alice.debug_logging.read().unwrap().is_some()); - - alice.emit_event(EventType::Info("hi".to_string())); - alice - .evtracker - .get_matching(|ev| matches!(*ev, EventType::WebxdcStatusUpdate { .. })) - .await; - assert!( - alice - .sql - .count("SELECT COUNT(*) FROM msgs_status_updates;", ()) - .await? - > 0 - ); - Ok(()) - } - - /// Tests extensibility of WebXDC updates. - /// - /// If an update sent by WebXDC contains unknown properties, - /// such as `aNewUnknownProperty` or a reserved property - /// like `serial` or `max_serial`, - /// they are silently dropped and are not sent over the wire. - /// - /// This ensures new WebXDC can try to send new properties - /// added in later revisions of the WebXDC API - /// and this will not result in a failure to send the whole update. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_send_webxdc_status_update_extensibility() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat = alice.create_chat(&bob).await; - let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; - - let bob_instance = bob.recv_msg(&alice.pop_sent_msg().await).await; - - alice - .send_webxdc_status_update( - alice_instance.id, - r#"{"payload":"p","info":"i","aNewUnknownProperty":"x","max_serial":123}"#, - ) - .await?; - alice.flush_status_updates().await?; - let received_update = bob.recv_msg_opt(&alice.pop_sent_msg().await).await; - assert!(received_update.is_none()); - - assert_eq!( - bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":"p","info":"i","serial":1,"max_serial":1}]"# - ); - - Ok(()) - } - - // NB: This test also checks that a contact is not marked as bot after receiving from it a - // webxdc instance and status updates. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_status_update_vs_delete_device_after() -> Result<()> { - let alice = &TestContext::new_alice().await; - let bob = &TestContext::new_bob().await; - bob.set_config(Config::DeleteDeviceAfter, Some("3600")) - .await?; - let alice_chat = alice.create_chat(bob).await; - let alice_instance = send_webxdc_instance(alice, alice_chat.id).await?; - let bob_instance = bob.recv_msg(&alice.pop_sent_msg().await).await; - assert_eq!(bob.add_or_lookup_contact(alice).await.is_bot(), false); - - SystemTime::shift(Duration::from_secs(1800)); - let mut update = Message { - chat_id: alice_chat.id, - viewtype: Viewtype::Text, - text: "I'm an update".to_string(), - hidden: true, - ..Default::default() - }; - update.param.set_cmd(SystemMessage::WebxdcStatusUpdate); - update - .param - .set(Param::Arg, r#"{"updates":[{"payload":{"foo":"bar"}}]}"#); - update.set_quote(alice, Some(&alice_instance)).await?; - let sent_msg = alice.send_msg(alice_chat.id, &mut update).await; - bob.recv_msg_trash(&sent_msg).await; - assert_eq!(bob.add_or_lookup_contact(alice).await.is_bot(), false); - assert_eq!( - bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"# - ); - assert_eq!(bob.add_or_lookup_contact(alice).await.is_bot(), false); - - SystemTime::shift(Duration::from_secs(2700)); - ephemeral::delete_expired_messages(bob, tools::time()).await?; - let bob_instance = Message::load_from_db(bob, bob_instance.id).await?; - assert_eq!(bob_instance.chat_id.is_trash(), false); - - Ok(()) - } - - async fn has_incoming_webxdc_event( - t: &TestContext, - expected_msg: Message, - expected_text: &str, - ) -> bool { - t.evtracker - .get_matching_opt(t, |evt| { - if let EventType::IncomingWebxdcNotify { msg_id, text, .. } = evt { - *msg_id == expected_msg.id && text == expected_text - } else { - false - } - }) - .await - .is_some() - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_notify_one() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - let fiona = tcm.fiona().await; - - let grp_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) - .await; - let alice_instance = send_webxdc_instance(&alice, grp_id).await?; - let sent1 = alice.pop_sent_msg().await; - let bob_instance = bob.recv_msg(&sent1).await; - let _fiona_instance = fiona.recv_msg(&sent1).await; - - alice - .send_webxdc_status_update( - alice_instance.id, - &format!( - "{{\"payload\":7,\"info\": \"Alice moved\",\"notify\":{{\"{}\": \"Your move!\"}} }}", - bob_instance.get_webxdc_self_addr(&bob).await? - ), - ) - .await?; - alice.flush_status_updates().await?; - let sent2 = alice.pop_sent_msg().await; - let info_msg = alice.get_last_msg().await; - assert!(info_msg.is_info()); - assert_eq!(info_msg.text, "Alice moved"); - assert!(!has_incoming_webxdc_event(&alice, info_msg, "").await); - - bob.recv_msg_trash(&sent2).await; - let info_msg = bob.get_last_msg().await; - assert!(info_msg.is_info()); - assert_eq!(info_msg.text, "Alice moved"); - assert!(has_incoming_webxdc_event(&bob, info_msg, "Your move!").await); - - fiona.recv_msg_trash(&sent2).await; - let info_msg = fiona.get_last_msg().await; - assert!(info_msg.is_info()); - assert_eq!(info_msg.text, "Alice moved"); - assert!(!has_incoming_webxdc_event(&fiona, info_msg, "").await); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_notify_multiple() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - let fiona = tcm.fiona().await; - - let grp_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) - .await; - let alice_instance = send_webxdc_instance(&alice, grp_id).await?; - let sent1 = alice.pop_sent_msg().await; - let bob_instance = bob.recv_msg(&sent1).await; - let fiona_instance = fiona.recv_msg(&sent1).await; - - alice - .send_webxdc_status_update( - alice_instance.id, - &format!( - "{{\"payload\":7,\"info\": \"moved\", \"summary\": \"move summary\", \"notify\":{{\"{}\":\"move, Bob\",\"{}\":\"move, Fiona\"}} }}", - bob_instance.get_webxdc_self_addr(&bob).await?, - fiona_instance.get_webxdc_self_addr(&fiona).await? - ), - - ) - .await?; - alice.flush_status_updates().await?; - let sent2 = alice.pop_sent_msg().await; - let info_msg = alice.get_last_msg().await; - assert!(info_msg.is_info()); - assert!(!has_incoming_webxdc_event(&alice, info_msg, "").await); - - bob.recv_msg_trash(&sent2).await; - let info_msg = bob.get_last_msg().await; - assert!(info_msg.is_info()); - assert!(has_incoming_webxdc_event(&bob, info_msg, "move, Bob").await); - - fiona.recv_msg_trash(&sent2).await; - let info_msg = fiona.get_last_msg().await; - assert!(info_msg.is_info()); - assert!(has_incoming_webxdc_event(&fiona, info_msg, "move, Fiona").await); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_no_notify_self() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let alice2 = tcm.alice().await; - - let grp_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[]) - .await; - let alice_instance = send_webxdc_instance(&alice, grp_id).await?; - let sent1 = alice.pop_sent_msg().await; - let alice2_instance = alice2.recv_msg(&sent1).await; - assert_eq!( - alice_instance.get_webxdc_self_addr(&alice).await?, - alice2_instance.get_webxdc_self_addr(&alice2).await? - ); - - alice - .send_webxdc_status_update( - alice_instance.id, - &format!( - "{{\"payload\":7,\"info\": \"moved\", \"notify\":{{\"{}\": \"bla\"}} }}", - alice2_instance.get_webxdc_self_addr(&alice2).await? - ), - ) - .await?; - alice.flush_status_updates().await?; - let sent2 = alice.pop_sent_msg().await; - let info_msg = alice.get_last_msg().await; - assert!(info_msg.is_info()); - assert!(!has_incoming_webxdc_event(&alice, info_msg, "").await); - - alice2.recv_msg_trash(&sent2).await; - let info_msg = alice2.get_last_msg().await; - assert!(info_msg.is_info()); - assert!(!has_incoming_webxdc_event(&alice2, info_msg, "").await); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_notify_all() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - let fiona = tcm.fiona().await; - - let grp_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) - .await; - let alice_instance = send_webxdc_instance(&alice, grp_id).await?; - let sent1 = alice.pop_sent_msg().await; - bob.recv_msg(&sent1).await; - fiona.recv_msg(&sent1).await; - - alice - .send_webxdc_status_update( - alice_instance.id, - "{\"payload\":7,\"info\": \"go\", \"notify\":{\"*\":\"notify all\"} }", - ) - .await?; - alice.flush_status_updates().await?; - let sent2 = alice.pop_sent_msg().await; - let info_msg = alice.get_last_msg().await; - assert_eq!(info_msg.text, "go"); - assert!(!has_incoming_webxdc_event(&alice, info_msg, "").await); - - bob.recv_msg_trash(&sent2).await; - let info_msg = bob.get_last_msg().await; - assert_eq!(info_msg.text, "go"); - assert!(has_incoming_webxdc_event(&bob, info_msg, "notify all").await); - - fiona.recv_msg_trash(&sent2).await; - let info_msg = fiona.get_last_msg().await; - assert_eq!(info_msg.text, "go"); - assert!(has_incoming_webxdc_event(&fiona, info_msg, "notify all").await); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_notify_bob_and_all() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - let fiona = tcm.fiona().await; - - let grp_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) - .await; - let alice_instance = send_webxdc_instance(&alice, grp_id).await?; - let sent1 = alice.pop_sent_msg().await; - let bob_instance = bob.recv_msg(&sent1).await; - let fiona_instance = fiona.recv_msg(&sent1).await; - - alice - .send_webxdc_status_update( - alice_instance.id, - &format!( - "{{\"payload\":7, \"notify\":{{\"{}\": \"notify bob\",\"*\": \"notify all\"}} }}", - bob_instance.get_webxdc_self_addr(&bob).await? - ), - ) - .await?; - alice.flush_status_updates().await?; - let sent2 = alice.pop_sent_msg().await; - bob.recv_msg_trash(&sent2).await; - fiona.recv_msg_trash(&sent2).await; - assert!(has_incoming_webxdc_event(&bob, bob_instance, "notify bob").await); - assert!(has_incoming_webxdc_event(&fiona, fiona_instance, "notify all").await); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_notify_all_and_bob() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - let fiona = tcm.fiona().await; - - let grp_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) - .await; - let alice_instance = send_webxdc_instance(&alice, grp_id).await?; - let sent1 = alice.pop_sent_msg().await; - let bob_instance = bob.recv_msg(&sent1).await; - let fiona_instance = fiona.recv_msg(&sent1).await; - - alice - .send_webxdc_status_update( - alice_instance.id, - &format!( - "{{\"payload\":7, \"notify\":{{\"*\": \"notify all\", \"{}\": \"notify bob\"}} }}", - bob_instance.get_webxdc_self_addr(&bob).await? - ), - ) - .await?; - alice.flush_status_updates().await?; - let sent2 = alice.pop_sent_msg().await; - bob.recv_msg_trash(&sent2).await; - fiona.recv_msg_trash(&sent2).await; - assert!(has_incoming_webxdc_event(&bob, bob_instance, "notify bob").await); - assert!(has_incoming_webxdc_event(&fiona, fiona_instance, "notify all").await); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_webxdc_href() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - let grp_id = alice - .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob]) - .await; - let instance = send_webxdc_instance(&alice, grp_id).await?; - let sent1 = alice.pop_sent_msg().await; - - alice - .send_webxdc_status_update( - instance.id, - r##"{"payload": "my deeplink data", "info": "my move!", "href": "#foobar"}"##, - ) - .await?; - alice.flush_status_updates().await?; - let sent2 = alice.pop_sent_msg().await; - let info_msg = alice.get_last_msg().await; - assert!(info_msg.is_info()); - assert_eq!(info_msg.get_webxdc_href(), Some("#foobar".to_string())); - - bob.recv_msg(&sent1).await; - bob.recv_msg_trash(&sent2).await; - let info_msg = bob.get_last_msg().await; - assert!(info_msg.is_info()); - assert_eq!(info_msg.get_webxdc_href(), Some("#foobar".to_string())); - - Ok(()) - } -} +mod webxdc_tests; diff --git a/src/webxdc/webxdc_tests.rs b/src/webxdc/webxdc_tests.rs new file mode 100644 index 000000000..9586082d9 --- /dev/null +++ b/src/webxdc/webxdc_tests.rs @@ -0,0 +1,2201 @@ +use std::time::Duration; + +use regex::Regex; +use serde_json::json; + +use super::*; +use crate::chat::{ + add_contact_to_chat, create_broadcast_list, create_group_chat, forward_msgs, + remove_contact_from_chat, resend_msgs, send_msg, send_text_msg, ChatId, ProtectionStatus, +}; +use crate::chatlist::Chatlist; +use crate::config::Config; +use crate::contact::Contact; +use crate::download::DownloadState; +use crate::ephemeral; +use crate::receive_imf::{receive_imf, receive_imf_from_inbox}; +use crate::test_utils::{TestContext, TestContextManager}; +use crate::tools::{self, SystemTime}; +use crate::{message, sql}; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_is_webxdc_file() -> Result<()> { + let t = TestContext::new().await; + assert!( + !t.is_webxdc_file( + "bad-ext-no-zip.txt", + include_bytes!("../../test-data/message/issue_523.txt") + ) + .await? + ); + assert!( + !t.is_webxdc_file( + "bad-ext-good-zip.txt", + include_bytes!("../../test-data/webxdc/minimal.xdc") + ) + .await? + ); + assert!( + !t.is_webxdc_file( + "good-ext-no-zip.xdc", + include_bytes!("../../test-data/message/issue_523.txt") + ) + .await? + ); + assert!( + !t.is_webxdc_file( + "good-ext-no-index-html.xdc", + include_bytes!("../../test-data/webxdc/no-index-html.xdc") + ) + .await? + ); + assert!( + t.is_webxdc_file( + "good-ext-good-zip.xdc", + include_bytes!("../../test-data/webxdc/minimal.xdc") + ) + .await? + ); + Ok(()) +} + +fn create_webxdc_instance(t: &TestContext, name: &str, bytes: &[u8]) -> Result { + let mut instance = Message::new(Viewtype::File); + instance.set_file_from_bytes(t, name, bytes, None)?; + Ok(instance) +} + +async fn send_webxdc_instance(t: &TestContext, chat_id: ChatId) -> Result { + let mut instance = create_webxdc_instance( + t, + "minimal.xdc", + include_bytes!("../../test-data/webxdc/minimal.xdc"), + )?; + let instance_msg_id = send_msg(t, chat_id, &mut instance).await?; + assert_eq!(instance.viewtype, Viewtype::Webxdc); + Message::load_from_db(t, instance_msg_id).await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_send_webxdc_instance() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + + // send as .xdc file + let instance = send_webxdc_instance(&t, chat_id).await?; + assert_eq!(instance.viewtype, Viewtype::Webxdc); + assert_eq!(instance.get_filename(), Some("minimal.xdc".to_string())); + assert_eq!(instance.chat_id, chat_id); + + // sending using bad extension is not working, even when setting Viewtype to webxdc + let mut instance = Message::new(Viewtype::Webxdc); + instance.set_file_from_bytes(&t, "index.html", b"ola!", None)?; + assert!(send_msg(&t, chat_id, &mut instance).await.is_err()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_send_invalid_webxdc() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + + // sending invalid .xdc as file is possible, but must not result in Viewtype::Webxdc + let mut instance = create_webxdc_instance( + &t, + "invalid-no-zip-but-7z.xdc", + include_bytes!("../../test-data/webxdc/invalid-no-zip-but-7z.xdc"), + )?; + let instance_id = send_msg(&t, chat_id, &mut instance).await?; + assert_eq!(instance.viewtype, Viewtype::File); + let test = Message::load_from_db(&t, instance_id).await?; + assert_eq!(test.viewtype, Viewtype::File); + + // sending invalid .xdc as Viewtype::Webxdc should fail already on sending + let mut instance = Message::new(Viewtype::Webxdc); + instance.set_file_from_bytes( + &t, + "invalid2.xdc", + include_bytes!("../../test-data/webxdc/invalid-no-zip-but-7z.xdc"), + None, + )?; + assert!(send_msg(&t, chat_id, &mut instance).await.is_err()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_send_special_webxdc_format() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + + // chess.xdc is failing for some zip-versions, see #3476, if we know more details about why, we can have a nicer name for the test :) + let mut instance = create_webxdc_instance( + &t, + "chess.xdc", + include_bytes!("../../test-data/webxdc/chess.xdc"), + )?; + let instance_id = send_msg(&t, chat_id, &mut instance).await?; + let instance = Message::load_from_db(&t, instance_id).await?; + assert_eq!(instance.viewtype, Viewtype::Webxdc); + + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "Chess Board"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_forward_webxdc_instance() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let instance = send_webxdc_instance(&t, chat_id).await?; + t.send_webxdc_status_update( + instance.id, + r#"{"info": "foo", "summary":"bar", "document":"doc", "payload": 42}"#, + ) + .await?; + assert!(!instance.is_forwarded()); + assert_eq!( + t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":42,"info":"foo","document":"doc","summary":"bar","serial":1,"max_serial":1}]"# + ); + assert_eq!(chat_id.get_msg_cnt(&t).await?, 2); // instance and info + let info = Message::load_from_db(&t, instance.id) + .await? + .get_webxdc_info(&t) + .await?; + assert_eq!(info.summary, "bar".to_string()); + assert_eq!(info.document, "doc".to_string()); + + // forwarding an instance creates a fresh instance; updates etc. are not forwarded + forward_msgs(&t, &[instance.get_id()], chat_id).await?; + let instance2 = t.get_last_msg_in(chat_id).await; + assert!(instance2.is_forwarded()); + assert_eq!( + t.get_webxdc_status_updates(instance2.id, StatusUpdateSerial(0)) + .await?, + "[]" + ); + assert_eq!(chat_id.get_msg_cnt(&t).await?, 3); // two instances, only one info + let info = Message::load_from_db(&t, instance2.id) + .await? + .get_webxdc_info(&t) + .await?; + assert_eq!(info.summary, "".to_string()); + assert_eq!(info.document, "".to_string()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_resend_webxdc_instance_and_info() -> Result<()> { + let mut tcm = TestContextManager::new(); + + // Alice uses webxdc in a group + let alice = tcm.alice().await; + alice.set_config_bool(Config::BccSelf, false).await?; + let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; + let alice_instance = send_webxdc_instance(&alice, alice_grp).await?; + assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 1); + alice + .send_webxdc_status_update( + alice_instance.id, + r#"{"payload":7,"info": "i","summary":"s"}"#, + ) + .await?; + assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 2); + assert!(alice.get_last_msg_in(alice_grp).await.is_info()); + + // Alice adds Bob and resends already used webxdc + add_contact_to_chat( + &alice, + alice_grp, + Contact::create(&alice, "", "bob@example.net").await?, + ) + .await?; + assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 3); + resend_msgs(&alice, &[alice_instance.id]).await?; + let sent1 = alice.pop_sent_msg().await; + alice.flush_status_updates().await?; + let sent2 = alice.pop_sent_msg().await; + + // Bob receives webxdc, legacy info-messages updates are received and added to the chat. + let bob = tcm.bob().await; + let bob_instance = bob.recv_msg(&sent1).await; + bob.recv_msg_trash(&sent2).await; + assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); + assert!(!bob_instance.is_info()); + assert_eq!( + bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":7,"info":"i","summary":"s","serial":1,"max_serial":1}]"# + ); + let bob_grp = bob_instance.chat_id; + assert_eq!(bob.get_last_msg_in(bob_grp).await.id, bob_instance.id); + assert_eq!(bob_grp.get_msg_cnt(&bob).await?, 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_receive_webxdc_instance() -> Result<()> { + let t = TestContext::new_alice().await; + receive_imf( + &t, + include_bytes!("../../test-data/message/webxdc_good_extension.eml"), + false, + ) + .await?; + let instance = t.get_last_msg().await; + assert_eq!(instance.viewtype, Viewtype::Webxdc); + assert_eq!(instance.get_filename().unwrap(), "minimal.xdc"); + + receive_imf( + &t, + include_bytes!("../../test-data/message/webxdc_bad_extension.eml"), + false, + ) + .await?; + let instance = t.get_last_msg().await; + assert_eq!(instance.viewtype, Viewtype::File); // we require the correct extension, only a mime type is not sufficient + assert_eq!(instance.get_filename().unwrap(), "index.html"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_contact_request() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + // Alice sends an webxdc instance to Bob + let alice_chat = alice.create_chat(&bob).await; + let _alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; + bob.recv_msg(&alice.pop_sent_msg().await).await; + + // Bob can start the webxdc from a contact request (get index.html) + // but cannot send updates to contact requests + let bob_instance = bob.get_last_msg().await; + let bob_chat = Chat::load_from_db(&bob, bob_instance.chat_id).await?; + assert!(bob_chat.is_contact_request()); + assert!(bob_instance + .get_webxdc_blob(&bob, "index.html") + .await + .is_ok()); + assert!(bob + .send_webxdc_status_update(bob_instance.id, r#"{"payload":42}"#) + .await + .is_err()); + assert_eq!( + bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) + .await?, + "[]" + ); + + // Once the contact request is accepted, Bob can send updates + bob_chat.id.accept(&bob).await?; + assert!(bob + .send_webxdc_status_update(bob_instance.id, r#"{"payload":42}"#) + .await + .is_ok()); + assert_eq!( + bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":42,"serial":1,"max_serial":1}]"# + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_update_for_not_downloaded_instance() -> Result<()> { + // Alice sends a larger instance and an update + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat = alice.create_chat(&bob).await; + bob.set_config(Config::DownloadLimit, Some("40000")).await?; + let mut alice_instance = create_webxdc_instance( + &alice, + "chess.xdc", + include_bytes!("../../test-data/webxdc/chess.xdc"), + )?; + let sent1 = alice.send_msg(chat.id, &mut alice_instance).await; + let alice_instance = sent1.load_from_db().await; + alice + .send_webxdc_status_update( + alice_instance.id, + r#"{"payload": 7, "summary":"sum", "document":"doc"}"#, + ) + .await?; + alice.flush_status_updates().await?; + let sent2 = alice.pop_sent_msg().await; + + // Bob does not download instance but already receives update + receive_imf_from_inbox( + &bob, + &alice_instance.rfc724_mid, + sent1.payload().as_bytes(), + false, + Some(70790), + false, + ) + .await?; + let bob_instance = bob.get_last_msg().await; + bob_instance.chat_id.accept(&bob).await?; + bob.recv_msg_trash(&sent2).await; + assert_eq!(bob_instance.download_state, DownloadState::Available); + + // Bob downloads instance, updates should be assigned correctly + let received_msg = receive_imf_from_inbox( + &bob, + &alice_instance.rfc724_mid, + sent1.payload().as_bytes(), + false, + None, + false, + ) + .await? + .unwrap(); + assert_eq!(*received_msg.msg_ids.first().unwrap(), bob_instance.id); + let bob_instance = bob.get_last_msg().await; + assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); + assert_eq!(bob_instance.download_state, DownloadState::Done); + assert_eq!( + bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":7,"document":"doc","summary":"sum","serial":1,"max_serial":1}]"# + ); + let info = bob_instance.get_webxdc_info(&bob).await?; + assert_eq!(info.document, "doc"); + assert_eq!(info.summary, "sum"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_delete_webxdc_instance() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let instance = send_webxdc_instance(&t, chat_id).await?; + let now = tools::time(); + t.receive_status_update( + ContactId::SELF, + &instance, + now, + true, + r#"{"updates":[{"payload":1}]}"#, + ) + .await?; + assert_eq!( + t.sql + .count("SELECT COUNT(*) FROM msgs_status_updates;", ()) + .await?, + 1 + ); + + message::delete_msgs(&t, &[instance.id]).await?; + sql::housekeeping(&t).await?; + assert_eq!( + t.sql + .count("SELECT COUNT(*) FROM msgs_status_updates;", ()) + .await?, + 0 + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_delete_chat_with_webxdc() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let instance = send_webxdc_instance(&t, chat_id).await?; + let now = tools::time(); + t.receive_status_update( + ContactId::SELF, + &instance, + now, + true, + r#"{"updates":[{"payload":1}, {"payload":2}]}"#, + ) + .await?; + assert_eq!( + t.sql + .count("SELECT COUNT(*) FROM msgs_status_updates;", ()) + .await?, + 2 + ); + + chat_id.delete(&t).await?; + sql::housekeeping(&t).await?; + assert_eq!( + t.sql + .count("SELECT COUNT(*) FROM msgs_status_updates;", ()) + .await?, + 0 + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_delete_webxdc_draft() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + + let mut instance = create_webxdc_instance( + &t, + "minimal.xdc", + include_bytes!("../../test-data/webxdc/minimal.xdc"), + )?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + let instance = chat_id.get_draft(&t).await?.unwrap(); + t.send_webxdc_status_update(instance.id, r#"{"payload": 42}"#) + .await?; + assert_eq!( + t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":42,"serial":1,"max_serial":1}]"#.to_string() + ); + + // set_draft(None) deletes the message without the need to simulate network + chat_id.set_draft(&t, None).await?; + assert_eq!( + t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) + .await?, + "[]".to_string() + ); + assert_eq!( + t.sql + .count("SELECT COUNT(*) FROM msgs_status_updates;", ()) + .await?, + 0 + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_create_status_update_record() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let instance = send_webxdc_instance(&t, chat_id).await?; + + assert_eq!( + t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) + .await?, + "[]" + ); + + let update_id1 = t + .create_status_update_record( + &instance, + StatusUpdateItem { + payload: json!({"foo": "bar"}), + info: None, + href: None, + document: None, + summary: None, + uid: Some("iecie2Ze".to_string()), + notify: None, + }, + 1640178619, + true, + ContactId::SELF, + ) + .await? + .unwrap(); + assert_eq!( + t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"# + ); + + // Update with duplicate update ID is received. + // Whatever the payload is, update should be ignored just because ID is duplicate. + let update_id1_duplicate = t + .create_status_update_record( + &instance, + StatusUpdateItem { + payload: json!({"nothing": "this should be ignored"}), + info: None, + href: None, + document: None, + summary: None, + uid: Some("iecie2Ze".to_string()), + notify: None, + }, + 1640178619, + true, + ContactId::SELF, + ) + .await?; + assert_eq!(update_id1_duplicate, None); + + assert!(t + .send_webxdc_status_update(instance.id, "\n\n\n") + .await + .is_err()); + + assert!(t + .send_webxdc_status_update(instance.id, "bad json") + .await + .is_err()); + + assert_eq!( + t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"# + ); + + let update_id2 = t + .create_status_update_record( + &instance, + StatusUpdateItem { + payload: json!({"foo2": "bar2"}), + info: None, + href: None, + document: None, + summary: None, + uid: None, + notify: None, + }, + 1640178619, + true, + ContactId::SELF, + ) + .await? + .unwrap(); + assert_eq!( + t.get_webxdc_status_updates(instance.id, update_id1).await?, + r#"[{"payload":{"foo2":"bar2"},"serial":3,"max_serial":3}]"# + ); + t.create_status_update_record( + &instance, + StatusUpdateItem { + payload: Value::Bool(true), + info: None, + href: None, + document: None, + summary: None, + uid: None, + notify: None, + }, + 1640178619, + true, + ContactId::SELF, + ) + .await?; + assert_eq!( + t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":4}, +{"payload":{"foo2":"bar2"},"serial":3,"max_serial":4}, +{"payload":true,"serial":4,"max_serial":4}]"# + ); + + t.send_webxdc_status_update( + instance.id, + r#"{"payload" : 1, "sender": "that is not used"}"#, + ) + .await?; + assert_eq!( + t.get_webxdc_status_updates(instance.id, update_id2).await?, + r#"[{"payload":true,"serial":4,"max_serial":5}, +{"payload":1,"serial":5,"max_serial":5}]"# + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_receive_status_update() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let instance = send_webxdc_instance(&t, chat_id).await?; + let now = tools::time(); + + assert!(t + .receive_status_update(ContactId::SELF, &instance, now, true, r#"foo: bar"#) + .await + .is_err()); // no json + assert!(t + .receive_status_update( + ContactId::SELF, + &instance, + now, + true, + r#"{"updada":[{"payload":{"foo":"bar"}}]}"# + ) + .await + .is_err()); // "updates" object missing + assert!(t + .receive_status_update( + ContactId::SELF, + &instance, + now, + true, + r#"{"updates":[{"foo":"bar"}]}"# + ) + .await + .is_err()); // "payload" field missing + assert!(t + .receive_status_update( + ContactId::SELF, + &instance, + now, + true, + r#"{"updates":{"payload":{"foo":"bar"}}}"# + ) + .await + .is_err()); // not an array + + t.receive_status_update( + ContactId::SELF, + &instance, + now, + true, + r#"{"updates":[{"payload":{"foo":"bar"}, "someTrash": "definitely TrAsH"}]}"#, + ) + .await?; + assert_eq!( + t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"# + ); + + t.receive_status_update( + ContactId::SELF, + &instance, + now, + true, + r#" {"updates": [ {"payload" :42} , {"payload": 23} ] } "#, + ) + .await?; + assert_eq!( + t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":3}, +{"payload":42,"serial":2,"max_serial":3}, +{"payload":23,"serial":3,"max_serial":3}]"# + ); + + t.receive_status_update( + ContactId::SELF, + &instance, + now, + true, + r#" {"updates": [ {"payload" :"ok", "future_item": "test"} ], "from": "future" } "#, + ) + .await?; // ignore members that may be added in the future + assert_eq!( + t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":4}, +{"payload":42,"serial":2,"max_serial":4}, +{"payload":23,"serial":3,"max_serial":4}, +{"payload":"ok","serial":4,"max_serial":4}]"# + ); + + Ok(()) +} + +async fn expect_status_update_event(t: &TestContext, instance_id: MsgId) -> Result<()> { + let event = t + .evtracker + .get_matching(|evt| matches!(evt, EventType::WebxdcStatusUpdate { .. })) + .await; + match event { + EventType::WebxdcStatusUpdate { + msg_id, + status_update_serial: _, + } => { + assert_eq!(msg_id, instance_id); + } + _ => unreachable!(), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_send_webxdc_status_update() -> Result<()> { + let alice = TestContext::new_alice().await; + alice.set_config_bool(Config::BccSelf, true).await?; + let bob = TestContext::new_bob().await; + + // Alice sends an webxdc instance and a status update + let alice_chat = alice.create_chat(&bob).await; + let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; + let sent1 = &alice.pop_sent_msg().await; + assert_eq!(alice_instance.viewtype, Viewtype::Webxdc); + assert!(!sent1.payload().contains("report-type=status-update")); + + alice + .send_webxdc_status_update(alice_instance.id, r#"{"payload" : {"foo":"bar"}}"#) + .await?; + alice.flush_status_updates().await?; + expect_status_update_event(&alice, alice_instance.id).await?; + let sent2 = &alice.pop_sent_msg().await; + let alice_update = sent2.load_from_db().await; + assert!(alice_update.hidden); + assert_eq!(alice_update.viewtype, Viewtype::Text); + assert_eq!(alice_update.get_filename(), None); + assert_eq!(alice_update.text, BODY_DESCR.to_string()); + assert_eq!(alice_update.chat_id, alice_instance.chat_id); + assert_eq!( + alice_update.parent(&alice).await?.unwrap().id, + alice_instance.id + ); + assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1); + assert!(sent2.payload().contains("report-type=status-update")); + assert!(sent2.payload().contains(BODY_DESCR)); + assert_eq!( + alice + .get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"# + ); + + alice + .send_webxdc_status_update(alice_instance.id, r#"{"payload":{"snipp":"snapp"}}"#) + .await?; + assert_eq!( + alice + .get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2}, +{"payload":{"snipp":"snapp"},"serial":2,"max_serial":2}]"# + ); + + // Bob receives all messages + let bob_instance = bob.recv_msg(sent1).await; + let bob_chat_id = bob_instance.chat_id; + assert_eq!(bob_instance.rfc724_mid, alice_instance.rfc724_mid); + assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); + assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1); + + let bob_received_update = bob.recv_msg_opt(sent2).await; + assert!(bob_received_update.is_none()); + expect_status_update_event(&bob, bob_instance.id).await?; + assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1); + + assert_eq!( + bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"# + ); + + // Alice has a second device and also receives messages there + let alice2 = TestContext::new_alice().await; + alice2.recv_msg(sent1).await; + alice2.recv_msg_trash(sent2).await; + let alice2_instance = alice2.get_last_msg().await; + let alice2_chat_id = alice2_instance.chat_id; + assert_eq!(alice2_instance.viewtype, Viewtype::Webxdc); + assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 1); + + // To support the second device, Alice has enabled bcc_self and will receive their own messages; + // these messages, however, should be ignored + alice.recv_msg_opt(sent1).await; + alice.recv_msg_opt(sent2).await; + assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1); + assert_eq!( + alice + .get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2}, +{"payload":{"snipp":"snapp"},"serial":2,"max_serial":2}]"# + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_send_big_webxdc_status_update() -> Result<()> { + let alice = TestContext::new_alice().await; + alice.set_config_bool(Config::BccSelf, true).await?; + let bob = TestContext::new_bob().await; + + let alice_chat = alice.create_chat(&bob).await; + let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; + let sent1 = &alice.pop_sent_msg().await; + assert_eq!(alice_instance.viewtype, Viewtype::Webxdc); + assert!(!sent1.payload().contains("report-type=status-update")); + + let update1_str = r#"{"payload":{"foo":""#.to_string() + + &String::from_utf8(vec![b'a'; STATUS_UPDATE_SIZE_MAX])? + + r#""}"#; + alice + .send_webxdc_status_update(alice_instance.id, &(update1_str.clone() + "}")) + .await?; + alice + .send_webxdc_status_update(alice_instance.id, r#"{"payload" : {"foo":"bar2"}}"#) + .await?; + alice + .send_webxdc_status_update(alice_instance.id, r#"{"payload" : {"foo":"bar3"}}"#) + .await?; + alice.flush_status_updates().await?; + + // There's the message stack, so we pop messages in the reverse order. + let sent3 = &alice.pop_sent_msg().await; + let alice_update = sent3.load_from_db().await; + assert_eq!(alice_update.text, BODY_DESCR.to_string()); + let sent2 = &alice.pop_sent_msg().await; + let alice_update = sent2.load_from_db().await; + assert_eq!(alice_update.text, BODY_DESCR.to_string()); + assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1); + + // Bob receives the instance. + let bob_instance = bob.recv_msg(sent1).await; + let bob_chat_id = bob_instance.chat_id; + assert_eq!(bob_instance.rfc724_mid, alice_instance.rfc724_mid); + assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); + assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1); + + // Bob receives the status updates. + bob.recv_msg_trash(sent2).await; + expect_status_update_event(&bob, bob_instance.id).await?; + assert_eq!( + bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) + .await?, + "[".to_string() + &update1_str + r#","serial":1,"max_serial":1}]"# + ); + bob.recv_msg_trash(sent3).await; + for _ in 0..2 { + expect_status_update_event(&bob, bob_instance.id).await?; + } + assert_eq!( + bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(1)) + .await?, + r#"[{"payload":{"foo":"bar2"},"serial":2,"max_serial":3}, +{"payload":{"foo":"bar3"},"serial":3,"max_serial":3}]"# + ); + assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_render_webxdc_status_update_object() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?; + let mut instance = create_webxdc_instance( + &t, + "minimal.xdc", + include_bytes!("../../test-data/webxdc/minimal.xdc"), + )?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + let (first, last) = (StatusUpdateSerial(1), StatusUpdateSerial::MAX); + assert_eq!( + t.render_webxdc_status_update_object(instance.id, first, last, None) + .await?, + (None, StatusUpdateSerial(u32::MAX)) + ); + + t.send_webxdc_status_update(instance.id, r#"{"payload": 1}"#) + .await?; + let (object, first_new) = t + .render_webxdc_status_update_object(instance.id, first, last, None) + .await?; + assert!(object.is_some()); + assert_eq!(first_new, StatusUpdateSerial(u32::MAX)); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_render_webxdc_status_update_object_range() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?; + let instance = send_webxdc_instance(&t, chat_id).await?; + t.send_webxdc_status_update(instance.id, r#"{"payload": 1}"#) + .await?; + t.send_webxdc_status_update(instance.id, r#"{"payload": 2}"#) + .await?; + t.send_webxdc_status_update(instance.id, r#"{"payload": 3}"#) + .await?; + t.send_webxdc_status_update(instance.id, r#"{"payload": 4}"#) + .await?; + let (json, first_new) = t + .render_webxdc_status_update_object( + instance.id, + StatusUpdateSerial(2), + StatusUpdateSerial(3), + None, + ) + .await?; + let json = json.unwrap(); + assert_eq!(first_new, StatusUpdateSerial(4)); + let json = Regex::new(r#""uid":"[^"]*""#) + .unwrap() + .replace_all(&json, "XXX"); + assert_eq!( + json, + "{\"updates\":[{\"payload\":2,XXX},\n{\"payload\":3,XXX}]}" + ); + + assert_eq!( + t.sql + .count("SELECT COUNT(*) FROM smtp_status_updates", ()) + .await?, + 1 + ); + t.flush_status_updates().await?; + assert_eq!( + t.sql + .count("SELECT COUNT(*) FROM smtp_status_updates", ()) + .await?, + 0 + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_pop_status_update() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?; + let instance1 = send_webxdc_instance(&t, chat_id).await?; + let instance2 = send_webxdc_instance(&t, chat_id).await?; + let instance3 = send_webxdc_instance(&t, chat_id).await?; + assert!(t.smtp_status_update_get().await?.is_none()); + + t.send_webxdc_status_update(instance1.id, r#"{"payload": "1a"}"#) + .await?; + t.send_webxdc_status_update(instance2.id, r#"{"payload": "2a"}"#) + .await?; + t.send_webxdc_status_update(instance2.id, r#"{"payload": "2b"}"#) + .await?; + t.send_webxdc_status_update(instance3.id, r#"{"payload": "3a"}"#) + .await?; + t.send_webxdc_status_update(instance3.id, r#"{"payload": "3b"}"#) + .await?; + t.send_webxdc_status_update(instance3.id, r#"{"payload": "3c"}"#) + .await?; + assert_eq!( + t.sql + .count("SELECT COUNT(*) FROM smtp_status_updates", ()) + .await?, + 3 + ); + + // order of smtp_status_update_get() is not defined, therefore the more complicated test + let mut instances_checked = 0; + for i in 0..3 { + let (instance, min_ser, max_ser) = t.smtp_status_update_get().await?.unwrap(); + t.smtp_status_update_pop_serials( + instance, + min_ser, + StatusUpdateSerial::new(max_ser.to_u32().checked_add(1).unwrap()), + ) + .await?; + let min_ser: u32 = min_ser.try_into()?; + if instance == instance1.id { + assert_eq!(min_ser, max_ser.to_u32()); + + instances_checked += 1; + } else if instance == instance2.id { + assert_eq!(min_ser, max_ser.to_u32() - 1); + + instances_checked += 1; + } else if instance == instance3.id { + assert_eq!(min_ser, max_ser.to_u32() - 2); + instances_checked += 1; + } else { + bail!("unexpected instance"); + } + assert_eq!( + t.sql + .count("SELECT COUNT(*) FROM smtp_status_updates", ()) + .await?, + 2 - i + ); + } + assert_eq!(instances_checked, 3); + assert!(t.smtp_status_update_get().await?.is_none()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_draft_and_send_webxdc_status_update() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat_id = alice.create_chat(&bob).await.id; + + // prepare webxdc instance, + // status updates are not sent for drafts, therefore send_webxdc_status_update() returns Ok(None) + let mut alice_instance = create_webxdc_instance( + &alice, + "minimal.xdc", + include_bytes!("../../test-data/webxdc/minimal.xdc"), + )?; + alice_chat_id + .set_draft(&alice, Some(&mut alice_instance)) + .await?; + let mut alice_instance = alice_chat_id.get_draft(&alice).await?.unwrap(); + + alice + .send_webxdc_status_update(alice_instance.id, r#"{"payload": {"foo":"bar"}}"#) + .await?; + expect_status_update_event(&alice, alice_instance.id).await?; + alice + .send_webxdc_status_update(alice_instance.id, r#"{"payload":42, "info":"i"}"#) + .await?; + expect_status_update_event(&alice, alice_instance.id).await?; + assert_eq!( + alice + .sql + .count("SELECT COUNT(*) FROM smtp_status_updates", ()) + .await?, + 0 + ); + assert!(!alice.get_last_msg().await.is_info()); // 'info: "i"' message not added in draft mode + + // send webxdc instance, + // the initial status updates are sent together in the same message + let alice_instance_id = send_msg(&alice, alice_chat_id, &mut alice_instance).await?; + let sent1 = alice.pop_sent_msg().await; + let alice_instance = Message::load_from_db(&alice, alice_instance_id).await?; + assert_eq!(alice_instance.viewtype, Viewtype::Webxdc); + assert_eq!( + alice_instance.get_filename(), + Some("minimal.xdc".to_string()) + ); + assert_eq!(alice_instance.chat_id, alice_chat_id); + + // bob receives the instance together with the initial updates in a single message + let bob_instance = bob.recv_msg(&sent1).await; + assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); + assert_eq!(bob_instance.get_filename().unwrap(), "minimal.xdc"); + assert!(sent1.payload().contains("Content-Type: application/json")); + assert!(sent1.payload().contains("status-update.json")); + assert_eq!( + bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2}, +{"payload":42,"info":"i","serial":2,"max_serial":2}]"# + ); + assert!(!bob.get_last_msg().await.is_info()); // 'info: "i"' message not added in draft mode + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_send_webxdc_status_update_to_non_webxdc() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let msg_id = send_text_msg(&t, chat_id, "ho!".to_string()).await?; + assert!(t + .send_webxdc_status_update(msg_id, r#"{"foo":"bar"}"#) + .await + .is_err()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_webxdc_blob() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let instance = send_webxdc_instance(&t, chat_id).await?; + + let buf = instance.get_webxdc_blob(&t, "index.html").await?; + assert_eq!(buf.len(), 188); + assert!(String::from_utf8_lossy(&buf).contains("document.write")); + + assert!(instance + .get_webxdc_blob(&t, "not-existent.html") + .await + .is_err()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_webxdc_blob_default_icon() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let instance = send_webxdc_instance(&t, chat_id).await?; + + let buf = instance.get_webxdc_blob(&t, WEBXDC_DEFAULT_ICON).await?; + assert!(buf.len() > 100); + assert!(String::from_utf8_lossy(&buf).contains("PNG\r\n")); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_webxdc_blob_with_absolute_paths() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let instance = send_webxdc_instance(&t, chat_id).await?; + + let buf = instance.get_webxdc_blob(&t, "/index.html").await?; + assert!(String::from_utf8_lossy(&buf).contains("document.write")); + + assert!(instance.get_webxdc_blob(&t, "/not-there").await.is_err()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_webxdc_blob_with_subdirs() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let mut instance = create_webxdc_instance( + &t, + "some-files.xdc", + include_bytes!("../../test-data/webxdc/some-files.xdc"), + )?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + + let buf = instance.get_webxdc_blob(&t, "index.html").await?; + assert_eq!(buf.len(), 65); + assert!(String::from_utf8_lossy(&buf).contains("many files")); + + let buf = instance.get_webxdc_blob(&t, "subdir/bla.txt").await?; + assert_eq!(buf.len(), 4); + assert!(String::from_utf8_lossy(&buf).starts_with("bla")); + + let buf = instance + .get_webxdc_blob(&t, "subdir/subsubdir/text.md") + .await?; + assert_eq!(buf.len(), 24); + assert!(String::from_utf8_lossy(&buf).starts_with("this is a markdown file")); + + let buf = instance + .get_webxdc_blob(&t, "subdir/subsubdir/text2.md") + .await?; + assert_eq!(buf.len(), 22); + assert!(String::from_utf8_lossy(&buf).starts_with("another markdown")); + + let buf = instance + .get_webxdc_blob(&t, "anotherdir/anothersubsubdir/foo.txt") + .await?; + assert_eq!(buf.len(), 4); + assert!(String::from_utf8_lossy(&buf).starts_with("foo")); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_webxdc_manifest() -> Result<()> { + let result = parse_webxdc_manifest(r#"key = syntax error"#.as_bytes()); + assert!(result.is_err()); + + let manifest = parse_webxdc_manifest(r#"no_name = "no name, no icon""#.as_bytes())?; + assert_eq!(manifest.name, None); + + let manifest = parse_webxdc_manifest(r#"name = "name, no icon""#.as_bytes())?; + assert_eq!(manifest.name, Some("name, no icon".to_string())); + + let manifest = parse_webxdc_manifest( + r#"name = "foo" +icon = "bar""# + .as_bytes(), + )?; + assert_eq!(manifest.name, Some("foo".to_string())); + + let manifest = parse_webxdc_manifest( + r#"name = "foz" +icon = "baz" +add_item = "that should be just ignored" + +[section] +sth_for_the = "future""# + .as_bytes(), + )?; + assert_eq!(manifest.name, Some("foz".to_string())); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_webxdc_manifest_min_api() -> Result<()> { + let manifest = parse_webxdc_manifest(r#"min_api = 3"#.as_bytes())?; + assert_eq!(manifest.min_api, Some(3)); + + let result = parse_webxdc_manifest(r#"min_api = "1""#.as_bytes()); + assert!(result.is_err()); + + let result = parse_webxdc_manifest(r#"min_api = 1.2"#.as_bytes()); + assert!(result.is_err()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_webxdc_manifest_source_code_url() -> Result<()> { + let result = parse_webxdc_manifest(r#"source_code_url = 3"#.as_bytes()); + assert!(result.is_err()); + + let manifest = parse_webxdc_manifest(r#"source_code_url = "https://foo.bar""#.as_bytes())?; + assert_eq!( + manifest.source_code_url, + Some("https://foo.bar".to_string()) + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_min_api_too_large() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?; + let mut instance = create_webxdc_instance( + &t, + "with-min-api-1001.xdc", + include_bytes!("../../test-data/webxdc/with-min-api-1001.xdc"), + )?; + send_msg(&t, chat_id, &mut instance).await?; + + let instance = t.get_last_msg().await; + let html = instance.get_webxdc_blob(&t, "index.html").await?; + assert!(String::from_utf8_lossy(&html).contains("requires a newer Delta Chat version")); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_webxdc_info() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + + let instance = send_webxdc_instance(&t, chat_id).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "minimal.xdc"); + assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string()); + assert_eq!(info.send_update_interval, 10000); + assert_eq!(info.send_update_max_size, RECOMMENDED_FILE_SIZE as usize); + + let mut instance = create_webxdc_instance( + &t, + "with-manifest-empty-name.xdc", + include_bytes!("../../test-data/webxdc/with-manifest-empty-name.xdc"), + )?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "with-manifest-empty-name.xdc"); + assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string()); + + let mut instance = create_webxdc_instance( + &t, + "with-manifest-no-name.xdc", + include_bytes!("../../test-data/webxdc/with-manifest-no-name.xdc"), + )?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "with-manifest-no-name.xdc"); + assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string()); + + let mut instance = create_webxdc_instance( + &t, + "with-minimal-manifest.xdc", + include_bytes!("../../test-data/webxdc/with-minimal-manifest.xdc"), + )?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "nice app!"); + assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string()); + + let mut instance = create_webxdc_instance( + &t, + "with-manifest-and-png-icon.xdc", + include_bytes!("../../test-data/webxdc/with-manifest-and-png-icon.xdc"), + )?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "with some icon"); + assert_eq!(info.icon, "icon.png"); + + let mut instance = create_webxdc_instance( + &t, + "with-png-icon.xdc", + include_bytes!("../../test-data/webxdc/with-png-icon.xdc"), + )?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "with-png-icon.xdc"); + assert_eq!(info.icon, "icon.png"); + + let mut instance = create_webxdc_instance( + &t, + "with-jpg-icon.xdc", + include_bytes!("../../test-data/webxdc/with-jpg-icon.xdc"), + )?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "with-jpg-icon.xdc"); + assert_eq!(info.icon, "icon.jpg"); + + let msg_id = send_text_msg(&t, chat_id, "foo".to_string()).await?; + let msg = Message::load_from_db(&t, msg_id).await?; + let result = msg.get_webxdc_info(&t).await; + assert!(result.is_err()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_webxdc_self_addr() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + + let instance = send_webxdc_instance(&t, chat_id).await?; + let info1 = instance.get_webxdc_info(&t).await?; + let instance = send_webxdc_instance(&t, chat_id).await?; + let info2 = instance.get_webxdc_info(&t).await?; + + let real_addr = t.get_primary_self_addr().await?; + assert!(!info1.self_addr.contains(&real_addr)); + assert_ne!(info1.self_addr, info2.self_addr); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_info_summary() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + // Alice creates an webxdc instance and updates summary + let alice_chat = alice.create_chat(&bob).await; + let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; + let sent_instance = &alice.pop_sent_msg().await; + let info = alice_instance.get_webxdc_info(&alice).await?; + assert_eq!(info.summary, "".to_string()); + + alice + .send_webxdc_status_update(alice_instance.id, r#"{"summary":"sum: 1", "payload":1}"#) + .await?; + alice.flush_status_updates().await?; + let sent_update1 = &alice.pop_sent_msg().await; + let info = Message::load_from_db(&alice, alice_instance.id) + .await? + .get_webxdc_info(&alice) + .await?; + assert_eq!(info.summary, "sum: 1".to_string()); + + alice + .send_webxdc_status_update(alice_instance.id, r#"{"summary":"sum: 2", "payload":2}"#) + .await?; + alice.flush_status_updates().await?; + let sent_update2 = &alice.pop_sent_msg().await; + let info = Message::load_from_db(&alice, alice_instance.id) + .await? + .get_webxdc_info(&alice) + .await?; + assert_eq!(info.summary, "sum: 2".to_string()); + + // Bob receives the updates + let bob_instance = bob.recv_msg(sent_instance).await; + bob.recv_msg_trash(sent_update1).await; + bob.recv_msg_trash(sent_update2).await; + let info = Message::load_from_db(&bob, bob_instance.id) + .await? + .get_webxdc_info(&bob) + .await?; + assert_eq!(info.summary, "sum: 2".to_string()); + + // Alice has a second device and also receives the updates there + let alice2 = TestContext::new_alice().await; + let alice2_instance = alice2.recv_msg(sent_instance).await; + alice2.recv_msg_trash(sent_update1).await; + alice2.recv_msg_trash(sent_update2).await; + let info = Message::load_from_db(&alice2, alice2_instance.id) + .await? + .get_webxdc_info(&alice2) + .await?; + assert_eq!(info.summary, "sum: 2".to_string()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_document_name() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + // Alice creates an webxdc instance and updates document name + let alice_chat = alice.create_chat(&bob).await; + let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; + let sent_instance = &alice.pop_sent_msg().await; + let info = alice_instance.get_webxdc_info(&alice).await?; + assert_eq!(info.document, "".to_string()); + assert_eq!(info.summary, "".to_string()); + + alice + .send_webxdc_status_update( + alice_instance.id, + r#"{"document":"my file", "payload":1337}"#, + ) + .await?; + alice.flush_status_updates().await?; + let sent_update1 = &alice.pop_sent_msg().await; + let info = Message::load_from_db(&alice, alice_instance.id) + .await? + .get_webxdc_info(&alice) + .await?; + assert_eq!(info.document, "my file".to_string()); + assert_eq!(info.summary, "".to_string()); + + // Bob receives the updates + let bob_instance = bob.recv_msg(sent_instance).await; + bob.recv_msg_trash(sent_update1).await; + let info = Message::load_from_db(&bob, bob_instance.id) + .await? + .get_webxdc_info(&bob) + .await?; + assert_eq!(info.document, "my file".to_string()); + assert_eq!(info.summary, "".to_string()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_info_msg() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + // Alice sends update with an info message + let alice_chat = alice.create_chat(&bob).await; + let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; + let sent1 = &alice.pop_sent_msg().await; + assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1); + + alice + .send_webxdc_status_update( + alice_instance.id, + r#"{"info":"this appears in-chat", "payload":"sth. else"}"#, + ) + .await?; + alice.flush_status_updates().await?; + let sent2 = &alice.pop_sent_msg().await; + assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2); + let info_msg = alice.get_last_msg().await; + assert!(info_msg.is_info()); + assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage); + assert_eq!(info_msg.from_id, ContactId::SELF); + assert_eq!(info_msg.get_text(), "this appears in-chat"); + assert_eq!( + info_msg.parent(&alice).await?.unwrap().id, + alice_instance.id + ); + assert!(info_msg.quoted_message(&alice).await?.is_none()); + assert_eq!( + alice + .get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"# + ); + + // Bob receives all messages + let bob_instance = bob.recv_msg(sent1).await; + let bob_chat_id = bob_instance.chat_id; + bob.recv_msg_trash(sent2).await; + assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2); + let info_msg = bob.get_last_msg().await; + assert!(info_msg.is_info()); + assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage); + assert!(!info_msg.from_id.is_special()); + assert_eq!(info_msg.get_text(), "this appears in-chat"); + assert_eq!(info_msg.parent(&bob).await?.unwrap().id, bob_instance.id); + assert!(info_msg.quoted_message(&bob).await?.is_none()); + assert_eq!( + bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"# + ); + + // Alice has a second device and also receives the info message there + let alice2 = TestContext::new_alice().await; + let alice2_instance = alice2.recv_msg(sent1).await; + let alice2_chat_id = alice2_instance.chat_id; + alice2.recv_msg_trash(sent2).await; + assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 2); + let info_msg = alice2.get_last_msg().await; + assert!(info_msg.is_info()); + assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage); + assert_eq!(info_msg.from_id, ContactId::SELF); + assert_eq!(info_msg.get_text(), "this appears in-chat"); + assert_eq!( + info_msg.parent(&alice2).await?.unwrap().id, + alice2_instance.id + ); + assert!(info_msg.quoted_message(&alice2).await?.is_none()); + assert_eq!( + alice2 + .get_webxdc_status_updates(alice2_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"# + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_info_msg_cleanup_series() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; + let sent1 = &alice.pop_sent_msg().await; + + // Alice sends two info messages in a row; + // the second one removes the first one as there is nothing in between + alice + .send_webxdc_status_update(alice_instance.id, r#"{"info":"i1", "payload":1}"#) + .await?; + alice.flush_status_updates().await?; + let sent2 = &alice.pop_sent_msg().await; + assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2); + alice + .send_webxdc_status_update(alice_instance.id, r#"{"info":"i2", "payload":2}"#) + .await?; + alice.flush_status_updates().await?; + let sent3 = &alice.pop_sent_msg().await; + assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2); + let info_msg = alice.get_last_msg().await; + assert_eq!(info_msg.get_text(), "i2"); + + // When Bob receives the messages, they should be cleaned up as well + let bob_instance = bob.recv_msg(sent1).await; + let bob_chat_id = bob_instance.chat_id; + bob.recv_msg_trash(sent2).await; + assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2); + bob.recv_msg_trash(sent3).await; + assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2); + let info_msg = bob.get_last_msg().await; + assert_eq!(info_msg.get_text(), "i2"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_info_msg_no_cleanup_on_interrupted_series() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "c").await?; + let instance = send_webxdc_instance(&t, chat_id).await?; + + t.send_webxdc_status_update(instance.id, r#"{"info":"i1", "payload":1}"#) + .await?; + assert_eq!(chat_id.get_msg_cnt(&t).await?, 2); + send_text_msg(&t, chat_id, "msg between info".to_string()).await?; + assert_eq!(chat_id.get_msg_cnt(&t).await?, 3); + t.send_webxdc_status_update(instance.id, r#"{"info":"i2", "payload":2}"#) + .await?; + assert_eq!(chat_id.get_msg_cnt(&t).await?, 4); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_opportunistic_encryption() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + // Bob sends sth. to Alice, Alice has Bob's key + let bob_chat_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "chat").await?; + add_contact_to_chat( + &bob, + bob_chat_id, + Contact::create(&bob, "", "alice@example.org").await?, + ) + .await?; + send_text_msg(&bob, bob_chat_id, "populate".to_string()).await?; + alice.recv_msg(&bob.pop_sent_msg().await).await; + + // Alice sends instance+update to Bob + let alice_chat_id = alice.get_last_msg().await.chat_id; + alice_chat_id.accept(&alice).await?; + let alice_instance = send_webxdc_instance(&alice, alice_chat_id).await?; + let sent1 = &alice.pop_sent_msg().await; + alice + .send_webxdc_status_update(alice_instance.id, r#"{"payload":42}"#) + .await?; + alice.flush_status_updates().await?; + let sent2 = &alice.pop_sent_msg().await; + let update_msg = sent2.load_from_db().await; + assert!(alice_instance.get_showpadlock()); + assert!(update_msg.get_showpadlock()); + + // Bob receives instance+update + let bob_instance = bob.recv_msg(sent1).await; + bob.recv_msg_trash(sent2).await; + assert!(bob_instance.get_showpadlock()); + + // Bob adds Claire with unknown key, update to Alice+Claire cannot be encrypted + add_contact_to_chat( + &bob, + bob_chat_id, + Contact::create(&bob, "", "claire@example.org").await?, + ) + .await?; + bob.send_webxdc_status_update(bob_instance.id, r#"{"payload":43}"#) + .await?; + bob.flush_status_updates().await?; + let sent3 = bob.pop_sent_msg().await; + let update_msg = sent3.load_from_db().await; + assert!(!update_msg.get_showpadlock()); + + Ok(()) +} + +// check that `info.internet_access` is not set for normal, non-integrated webxdc - +// even if they use the deprecated option `request_internet_access` in manifest.toml +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_no_internet_access() -> Result<()> { + let t = TestContext::new_alice().await; + let self_id = t.get_self_chat().await.id; + let single_id = t.create_chat_with_contact("bob", "bob@e.com").await.id; + let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?; + let broadcast_id = create_broadcast_list(&t).await?; + + for e2ee in ["1", "0"] { + t.set_config(Config::E2eeEnabled, Some(e2ee)).await?; + for chat_id in [self_id, single_id, group_id, broadcast_id] { + for internet_xdc in [true, false] { + let mut instance = create_webxdc_instance( + &t, + "foo.xdc", + if internet_xdc { + include_bytes!("../../test-data/webxdc/request-internet-access.xdc") + } else { + include_bytes!("../../test-data/webxdc/minimal.xdc") + }, + )?; + let instance_id = send_msg(&t, chat_id, &mut instance).await?; + t.send_webxdc_status_update( + instance_id, + r#"{"summary":"real summary", "payload": 42}"#, + ) + .await?; + let instance = Message::load_from_db(&t, instance_id).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.internet_access, false); + } + } + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_chatlist_summary() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?; + let mut instance = create_webxdc_instance( + &t, + "with-minimal-manifest.xdc", + include_bytes!("../../test-data/webxdc/with-minimal-manifest.xdc"), + )?; + send_msg(&t, chat_id, &mut instance).await?; + + let chatlist = Chatlist::try_load(&t, 0, None, None).await?; + assert_eq!(chatlist.len(), 1); + let summary = chatlist.get_summary(&t, 0, None).await?; + assert_eq!(summary.text, "nice app!".to_string()); + assert_eq!(summary.thumbnail_path.unwrap(), "webxdc-icon://last-msg-id"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_and_text() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + // Alice sends instance and adds some text + let alice_chat = alice.create_chat(&bob).await; + let mut alice_instance = create_webxdc_instance( + &alice, + "minimal.xdc", + include_bytes!("../../test-data/webxdc/minimal.xdc"), + )?; + alice_instance.set_text("user added text".to_string()); + send_msg(&alice, alice_chat.id, &mut alice_instance).await?; + let alice_instance = alice.get_last_msg().await; + assert_eq!(alice_instance.get_text(), "user added text"); + + // Bob receives that instance + let sent1 = alice.pop_sent_msg().await; + let bob_instance = bob.recv_msg(&sent1).await; + assert_eq!(bob_instance.get_text(), "user added text"); + + // Alice's second device receives the instance as well + let alice2 = TestContext::new_alice().await; + let alice2_instance = alice2.recv_msg(&sent1).await; + assert_eq!(alice2_instance.get_text(), "user added text"); + + Ok(()) +} + +async fn helper_send_receive_status_update( + bob: &TestContext, + alice: &TestContext, + bob_instance: &Message, + alice_instance: &Message, +) -> Result { + bob.send_webxdc_status_update( + bob_instance.id, + r#"{"payload":7,"info": "i","summary":"s"}"#, + ) + .await?; + bob.flush_status_updates().await?; + let msg = bob.pop_sent_msg().await; + alice.recv_msg_trash(&msg).await; + alice + .get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0)) + .await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_reject_updates_from_non_groupmembers() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let contact_bob = Contact::create(&alice, "Bob", "bob@example.net").await?; + let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?; + add_contact_to_chat(&alice, chat_id, contact_bob).await?; + let instance = send_webxdc_instance(&alice, chat_id).await?; + bob.recv_msg(&alice.pop_sent_msg().await).await; + let bob_instance = bob.get_last_msg().await; + Chat::load_from_db(&bob, bob_instance.chat_id) + .await? + .id + .accept(&bob) + .await?; + + let status = helper_send_receive_status_update(&bob, &alice, &bob_instance, &instance).await?; + assert_eq!( + status, + r#"[{"payload":7,"info":"i","summary":"s","serial":1,"max_serial":1}]"# + ); + + remove_contact_from_chat(&alice, chat_id, contact_bob).await?; + alice.pop_sent_msg().await; + let status = helper_send_receive_status_update(&bob, &alice, &bob_instance, &instance).await?; + + assert_eq!( + status, + r#"[{"payload":7,"info":"i","summary":"s","serial":1,"max_serial":1}]"# + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_delete_event() -> Result<()> { + let alice = TestContext::new_alice().await; + let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?; + let instance = send_webxdc_instance(&alice, chat_id).await?; + message::delete_msgs(&alice, &[instance.id]).await?; + alice + .evtracker + .get_matching(|evt| matches!(evt, EventType::WebxdcInstanceDeleted { .. })) + .await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn change_logging_webxdc() -> Result<()> { + let alice = TestContext::new_alice().await; + let chat_id = ChatId::create_for_contact(&alice, ContactId::SELF).await?; + + assert_eq!( + alice + .sql + .count("SELECT COUNT(*) FROM msgs_status_updates;", ()) + .await?, + 0 + ); + + let mut instance = create_webxdc_instance( + &alice, + "debug_logging.xdc", + include_bytes!("../../test-data/webxdc/minimal.xdc"), + )?; + assert!(alice.debug_logging.read().unwrap().is_none()); + send_msg(&alice, chat_id, &mut instance).await?; + assert!(alice.debug_logging.read().unwrap().is_some()); + + alice.emit_event(EventType::Info("hi".to_string())); + alice + .evtracker + .get_matching(|ev| matches!(*ev, EventType::WebxdcStatusUpdate { .. })) + .await; + assert!( + alice + .sql + .count("SELECT COUNT(*) FROM msgs_status_updates;", ()) + .await? + > 0 + ); + Ok(()) +} + +/// Tests extensibility of WebXDC updates. +/// +/// If an update sent by WebXDC contains unknown properties, +/// such as `aNewUnknownProperty` or a reserved property +/// like `serial` or `max_serial`, +/// they are silently dropped and are not sent over the wire. +/// +/// This ensures new WebXDC can try to send new properties +/// added in later revisions of the WebXDC API +/// and this will not result in a failure to send the whole update. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_send_webxdc_status_update_extensibility() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; + + let bob_instance = bob.recv_msg(&alice.pop_sent_msg().await).await; + + alice + .send_webxdc_status_update( + alice_instance.id, + r#"{"payload":"p","info":"i","aNewUnknownProperty":"x","max_serial":123}"#, + ) + .await?; + alice.flush_status_updates().await?; + let received_update = bob.recv_msg_opt(&alice.pop_sent_msg().await).await; + assert!(received_update.is_none()); + + assert_eq!( + bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":"p","info":"i","serial":1,"max_serial":1}]"# + ); + + Ok(()) +} + +// NB: This test also checks that a contact is not marked as bot after receiving from it a +// webxdc instance and status updates. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_status_update_vs_delete_device_after() -> Result<()> { + let alice = &TestContext::new_alice().await; + let bob = &TestContext::new_bob().await; + bob.set_config(Config::DeleteDeviceAfter, Some("3600")) + .await?; + let alice_chat = alice.create_chat(bob).await; + let alice_instance = send_webxdc_instance(alice, alice_chat.id).await?; + let bob_instance = bob.recv_msg(&alice.pop_sent_msg().await).await; + assert_eq!(bob.add_or_lookup_contact(alice).await.is_bot(), false); + + SystemTime::shift(Duration::from_secs(1800)); + let mut update = Message { + chat_id: alice_chat.id, + viewtype: Viewtype::Text, + text: "I'm an update".to_string(), + hidden: true, + ..Default::default() + }; + update.param.set_cmd(SystemMessage::WebxdcStatusUpdate); + update + .param + .set(Param::Arg, r#"{"updates":[{"payload":{"foo":"bar"}}]}"#); + update.set_quote(alice, Some(&alice_instance)).await?; + let sent_msg = alice.send_msg(alice_chat.id, &mut update).await; + bob.recv_msg_trash(&sent_msg).await; + assert_eq!(bob.add_or_lookup_contact(alice).await.is_bot(), false); + assert_eq!( + bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"# + ); + assert_eq!(bob.add_or_lookup_contact(alice).await.is_bot(), false); + + SystemTime::shift(Duration::from_secs(2700)); + ephemeral::delete_expired_messages(bob, tools::time()).await?; + let bob_instance = Message::load_from_db(bob, bob_instance.id).await?; + assert_eq!(bob_instance.chat_id.is_trash(), false); + + Ok(()) +} + +async fn has_incoming_webxdc_event( + t: &TestContext, + expected_msg: Message, + expected_text: &str, +) -> bool { + t.evtracker + .get_matching_opt(t, |evt| { + if let EventType::IncomingWebxdcNotify { msg_id, text, .. } = evt { + *msg_id == expected_msg.id && text == expected_text + } else { + false + } + }) + .await + .is_some() +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_notify_one() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let fiona = tcm.fiona().await; + + let grp_id = alice + .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) + .await; + let alice_instance = send_webxdc_instance(&alice, grp_id).await?; + let sent1 = alice.pop_sent_msg().await; + let bob_instance = bob.recv_msg(&sent1).await; + let _fiona_instance = fiona.recv_msg(&sent1).await; + + alice + .send_webxdc_status_update( + alice_instance.id, + &format!( + "{{\"payload\":7,\"info\": \"Alice moved\",\"notify\":{{\"{}\": \"Your move!\"}} }}", + bob_instance.get_webxdc_self_addr(&bob).await? + ), + ) + .await?; + alice.flush_status_updates().await?; + let sent2 = alice.pop_sent_msg().await; + let info_msg = alice.get_last_msg().await; + assert!(info_msg.is_info()); + assert_eq!(info_msg.text, "Alice moved"); + assert!(!has_incoming_webxdc_event(&alice, info_msg, "").await); + + bob.recv_msg_trash(&sent2).await; + let info_msg = bob.get_last_msg().await; + assert!(info_msg.is_info()); + assert_eq!(info_msg.text, "Alice moved"); + assert!(has_incoming_webxdc_event(&bob, info_msg, "Your move!").await); + + fiona.recv_msg_trash(&sent2).await; + let info_msg = fiona.get_last_msg().await; + assert!(info_msg.is_info()); + assert_eq!(info_msg.text, "Alice moved"); + assert!(!has_incoming_webxdc_event(&fiona, info_msg, "").await); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_notify_multiple() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let fiona = tcm.fiona().await; + + let grp_id = alice + .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) + .await; + let alice_instance = send_webxdc_instance(&alice, grp_id).await?; + let sent1 = alice.pop_sent_msg().await; + let bob_instance = bob.recv_msg(&sent1).await; + let fiona_instance = fiona.recv_msg(&sent1).await; + + alice + .send_webxdc_status_update( + alice_instance.id, + &format!( + "{{\"payload\":7,\"info\": \"moved\", \"summary\": \"move summary\", \"notify\":{{\"{}\":\"move, Bob\",\"{}\":\"move, Fiona\"}} }}", + bob_instance.get_webxdc_self_addr(&bob).await?, + fiona_instance.get_webxdc_self_addr(&fiona).await? + ), + + ) + .await?; + alice.flush_status_updates().await?; + let sent2 = alice.pop_sent_msg().await; + let info_msg = alice.get_last_msg().await; + assert!(info_msg.is_info()); + assert!(!has_incoming_webxdc_event(&alice, info_msg, "").await); + + bob.recv_msg_trash(&sent2).await; + let info_msg = bob.get_last_msg().await; + assert!(info_msg.is_info()); + assert!(has_incoming_webxdc_event(&bob, info_msg, "move, Bob").await); + + fiona.recv_msg_trash(&sent2).await; + let info_msg = fiona.get_last_msg().await; + assert!(info_msg.is_info()); + assert!(has_incoming_webxdc_event(&fiona, info_msg, "move, Fiona").await); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_no_notify_self() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let alice2 = tcm.alice().await; + + let grp_id = alice + .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[]) + .await; + let alice_instance = send_webxdc_instance(&alice, grp_id).await?; + let sent1 = alice.pop_sent_msg().await; + let alice2_instance = alice2.recv_msg(&sent1).await; + assert_eq!( + alice_instance.get_webxdc_self_addr(&alice).await?, + alice2_instance.get_webxdc_self_addr(&alice2).await? + ); + + alice + .send_webxdc_status_update( + alice_instance.id, + &format!( + "{{\"payload\":7,\"info\": \"moved\", \"notify\":{{\"{}\": \"bla\"}} }}", + alice2_instance.get_webxdc_self_addr(&alice2).await? + ), + ) + .await?; + alice.flush_status_updates().await?; + let sent2 = alice.pop_sent_msg().await; + let info_msg = alice.get_last_msg().await; + assert!(info_msg.is_info()); + assert!(!has_incoming_webxdc_event(&alice, info_msg, "").await); + + alice2.recv_msg_trash(&sent2).await; + let info_msg = alice2.get_last_msg().await; + assert!(info_msg.is_info()); + assert!(!has_incoming_webxdc_event(&alice2, info_msg, "").await); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_notify_all() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let fiona = tcm.fiona().await; + + let grp_id = alice + .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) + .await; + let alice_instance = send_webxdc_instance(&alice, grp_id).await?; + let sent1 = alice.pop_sent_msg().await; + bob.recv_msg(&sent1).await; + fiona.recv_msg(&sent1).await; + + alice + .send_webxdc_status_update( + alice_instance.id, + "{\"payload\":7,\"info\": \"go\", \"notify\":{\"*\":\"notify all\"} }", + ) + .await?; + alice.flush_status_updates().await?; + let sent2 = alice.pop_sent_msg().await; + let info_msg = alice.get_last_msg().await; + assert_eq!(info_msg.text, "go"); + assert!(!has_incoming_webxdc_event(&alice, info_msg, "").await); + + bob.recv_msg_trash(&sent2).await; + let info_msg = bob.get_last_msg().await; + assert_eq!(info_msg.text, "go"); + assert!(has_incoming_webxdc_event(&bob, info_msg, "notify all").await); + + fiona.recv_msg_trash(&sent2).await; + let info_msg = fiona.get_last_msg().await; + assert_eq!(info_msg.text, "go"); + assert!(has_incoming_webxdc_event(&fiona, info_msg, "notify all").await); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_notify_bob_and_all() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let fiona = tcm.fiona().await; + + let grp_id = alice + .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) + .await; + let alice_instance = send_webxdc_instance(&alice, grp_id).await?; + let sent1 = alice.pop_sent_msg().await; + let bob_instance = bob.recv_msg(&sent1).await; + let fiona_instance = fiona.recv_msg(&sent1).await; + + alice + .send_webxdc_status_update( + alice_instance.id, + &format!( + "{{\"payload\":7, \"notify\":{{\"{}\": \"notify bob\",\"*\": \"notify all\"}} }}", + bob_instance.get_webxdc_self_addr(&bob).await? + ), + ) + .await?; + alice.flush_status_updates().await?; + let sent2 = alice.pop_sent_msg().await; + bob.recv_msg_trash(&sent2).await; + fiona.recv_msg_trash(&sent2).await; + assert!(has_incoming_webxdc_event(&bob, bob_instance, "notify bob").await); + assert!(has_incoming_webxdc_event(&fiona, fiona_instance, "notify all").await); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_notify_all_and_bob() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let fiona = tcm.fiona().await; + + let grp_id = alice + .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) + .await; + let alice_instance = send_webxdc_instance(&alice, grp_id).await?; + let sent1 = alice.pop_sent_msg().await; + let bob_instance = bob.recv_msg(&sent1).await; + let fiona_instance = fiona.recv_msg(&sent1).await; + + alice + .send_webxdc_status_update( + alice_instance.id, + &format!( + "{{\"payload\":7, \"notify\":{{\"*\": \"notify all\", \"{}\": \"notify bob\"}} }}", + bob_instance.get_webxdc_self_addr(&bob).await? + ), + ) + .await?; + alice.flush_status_updates().await?; + let sent2 = alice.pop_sent_msg().await; + bob.recv_msg_trash(&sent2).await; + fiona.recv_msg_trash(&sent2).await; + assert!(has_incoming_webxdc_event(&bob, bob_instance, "notify bob").await); + assert!(has_incoming_webxdc_event(&fiona, fiona_instance, "notify all").await); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_href() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + let grp_id = alice + .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob]) + .await; + let instance = send_webxdc_instance(&alice, grp_id).await?; + let sent1 = alice.pop_sent_msg().await; + + alice + .send_webxdc_status_update( + instance.id, + r##"{"payload": "my deeplink data", "info": "my move!", "href": "#foobar"}"##, + ) + .await?; + alice.flush_status_updates().await?; + let sent2 = alice.pop_sent_msg().await; + let info_msg = alice.get_last_msg().await; + assert!(info_msg.is_info()); + assert_eq!(info_msg.get_webxdc_href(), Some("#foobar".to_string())); + + bob.recv_msg(&sent1).await; + bob.recv_msg_trash(&sent2).await; + let info_msg = bob.get_last_msg().await; + assert!(info_msg.is_info()); + assert_eq!(info_msg.get_webxdc_href(), Some("#foobar".to_string())); + + Ok(()) +}