Browse Source

meli/accounts: add mailbox_by_path() tests

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
Manos Pitsidianakis 7 months ago
parent
commit
de65eec3
3 changed files with 480 additions and 15 deletions
  1. 11 4
      meli/src/accounts.rs
  2. 13 8
      meli/src/accounts/mailbox_ops.rs
  3. 456 3
      meli/src/accounts/tests.rs

+ 11 - 4
meli/src/accounts.rs

@@ -1337,16 +1337,23 @@ impl Account {
                 .list_mailboxes()
                 .list_mailboxes()
                 .into_iter()
                 .into_iter()
                 .map(|n| (n.hash, n.depth))
                 .map(|n| (n.hash, n.depth))
-                .collect::<BTreeMap<MailboxHash, usize>>();
+                .collect::<IndexMap<MailboxHash, usize>>();
             let mut entries = self
             let mut entries = self
                 .mailbox_entries
                 .mailbox_entries
                 .iter()
                 .iter()
-                .map(|(h, f)| (h, f.ref_mailbox.path()))
+                .map(|(h, f)| (h, f.ref_mailbox.name(), f.ref_mailbox.path()))
                 .collect::<Vec<_>>();
                 .collect::<Vec<_>>();
-            entries.sort_by_cached_key(|(h, _)| nodes.get(h).cloned().unwrap_or(usize::MAX));
+            entries.sort_by_cached_key(|(h, n, p)| {
+                (
+                    n.len(),
+                    nodes.get(*h).cloned().unwrap_or(usize::MAX),
+                    *p,
+                    *n,
+                )
+            });
             let patterns = &[path.trim_matches('/')];
             let patterns = &[path.trim_matches('/')];
             let mut potential_matches = IndexSet::new();
             let mut potential_matches = IndexSet::new();
-            for (_, haystack) in entries {
+            for (_, _, haystack) in &entries {
                 let ac = AhoCorasick::builder()
                 let ac = AhoCorasick::builder()
                     .ascii_case_insensitive(true)
                     .ascii_case_insensitive(true)
                     .build(patterns)
                     .build(patterns)

+ 13 - 8
meli/src/accounts/mailbox_ops.rs

@@ -26,7 +26,7 @@ use super::*;
 use crate::command::actions::MailboxOperation;
 use crate::command::actions::MailboxOperation;
 
 
 impl Account {
 impl Account {
-    pub fn mailbox_operation(&mut self, op: MailboxOperation) -> Result<()> {
+    pub fn mailbox_operation(&mut self, op: MailboxOperation) -> Result<JobId> {
         if self.settings.account.read_only {
         if self.settings.account.read_only {
             return Err(Error::new("Account is read-only."));
             return Err(Error::new("Account is read-only."));
         }
         }
@@ -42,11 +42,12 @@ impl Account {
                     job,
                     job,
                     self.is_async(),
                     self.is_async(),
                 );
                 );
+                let job_id = handle.job_id;
                 self.insert_job(
                 self.insert_job(
                     handle.job_id,
                     handle.job_id,
                     JobRequest::Mailbox(MailboxJobRequest::CreateMailbox { path, handle }),
                     JobRequest::Mailbox(MailboxJobRequest::CreateMailbox { path, handle }),
                 );
                 );
-                Ok(())
+                Ok(job_id)
             }
             }
             MailboxOperation::Delete(path) => {
             MailboxOperation::Delete(path) => {
                 if self.mailbox_entries.len() == 1 {
                 if self.mailbox_entries.len() == 1 {
@@ -60,6 +61,7 @@ impl Account {
                     job,
                     job,
                     self.is_async(),
                     self.is_async(),
                 );
                 );
+                let job_id = handle.job_id;
                 self.insert_job(
                 self.insert_job(
                     handle.job_id,
                     handle.job_id,
                     JobRequest::Mailbox(MailboxJobRequest::DeleteMailbox {
                     JobRequest::Mailbox(MailboxJobRequest::DeleteMailbox {
@@ -67,7 +69,7 @@ impl Account {
                         handle,
                         handle,
                     }),
                     }),
                 );
                 );
-                Ok(())
+                Ok(job_id)
             }
             }
             MailboxOperation::Subscribe(path) => {
             MailboxOperation::Subscribe(path) => {
                 let mailbox_hash = self.mailbox_by_path(&path)?;
                 let mailbox_hash = self.mailbox_by_path(&path)?;
@@ -81,6 +83,7 @@ impl Account {
                     job,
                     job,
                     self.is_async(),
                     self.is_async(),
                 );
                 );
+                let job_id = handle.job_id;
                 self.insert_job(
                 self.insert_job(
                     handle.job_id,
                     handle.job_id,
                     JobRequest::Mailbox(MailboxJobRequest::SetMailboxSubscription {
                     JobRequest::Mailbox(MailboxJobRequest::SetMailboxSubscription {
@@ -89,7 +92,7 @@ impl Account {
                         handle,
                         handle,
                     }),
                     }),
                 );
                 );
-                Ok(())
+                Ok(job_id)
             }
             }
             MailboxOperation::Unsubscribe(path) => {
             MailboxOperation::Unsubscribe(path) => {
                 let mailbox_hash = self.mailbox_by_path(&path)?;
                 let mailbox_hash = self.mailbox_by_path(&path)?;
@@ -103,15 +106,16 @@ impl Account {
                     job,
                     job,
                     self.is_async(),
                     self.is_async(),
                 );
                 );
+                let job_id = handle.job_id;
                 self.insert_job(
                 self.insert_job(
-                    handle.job_id,
+                    job_id,
                     JobRequest::Mailbox(MailboxJobRequest::SetMailboxSubscription {
                     JobRequest::Mailbox(MailboxJobRequest::SetMailboxSubscription {
                         mailbox_hash,
                         mailbox_hash,
                         new_value: false,
                         new_value: false,
                         handle,
                         handle,
                     }),
                     }),
                 );
                 );
-                Ok(())
+                Ok(job_id)
             }
             }
             MailboxOperation::Rename(path, new_path) => {
             MailboxOperation::Rename(path, new_path) => {
                 let mailbox_hash = self.mailbox_by_path(&path)?;
                 let mailbox_hash = self.mailbox_by_path(&path)?;
@@ -125,15 +129,16 @@ impl Account {
                     job,
                     job,
                     self.is_async(),
                     self.is_async(),
                 );
                 );
+                let job_id = handle.job_id;
                 self.insert_job(
                 self.insert_job(
-                    handle.job_id,
+                    job_id,
                     JobRequest::Mailbox(MailboxJobRequest::RenameMailbox {
                     JobRequest::Mailbox(MailboxJobRequest::RenameMailbox {
                         handle,
                         handle,
                         mailbox_hash,
                         mailbox_hash,
                         new_path,
                         new_path,
                     }),
                     }),
                 );
                 );
-                Ok(())
+                Ok(job_id)
             }
             }
             MailboxOperation::SetPermissions(_) => Err(Error::new("Not implemented.")),
             MailboxOperation::SetPermissions(_) => Err(Error::new("Not implemented.")),
         }
         }

+ 456 - 3
meli/src/accounts/tests.rs

@@ -20,13 +20,21 @@
 //
 //
 // SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
 // SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
 
 
+use std::path::PathBuf;
+
 use melib::{
 use melib::{
-    backends::{Mailbox, MailboxHash},
+    backends::{prelude::*, Mailbox, MailboxHash},
     error::Result,
     error::Result,
-    MailboxPermissions, SpecialUsageMailbox,
+    maildir::MaildirType,
+    smol, MailboxPermissions, SpecialUsageMailbox,
 };
 };
+use tempfile::TempDir;
 
 
-use crate::accounts::{FileMailboxConf, MailboxEntry, MailboxStatus};
+use crate::{
+    accounts::{AccountConf, FileMailboxConf, MailboxEntry, MailboxStatus},
+    command::actions::MailboxOperation,
+    utilities::tests::{eprint_step_fn, eprintln_ok_fn},
+};
 
 
 #[test]
 #[test]
 fn test_mailbox_utf7() {
 fn test_mailbox_utf7() {
@@ -110,3 +118,448 @@ fn test_mailbox_utf7() {
         assert_eq!(&entry.path, d);
         assert_eq!(&entry.path, d);
     }
     }
 }
 }
+
+fn new_maildir_backend(
+    temp_dir: &TempDir,
+    acc_name: &str,
+    event_consumer: BackendEventConsumer,
+    with_root_mailbox: bool,
+) -> Result<(PathBuf, AccountConf, Box<MaildirType>)> {
+    let root_mailbox = temp_dir.path().join("inbox");
+    {
+        std::fs::create_dir(&root_mailbox).expect("Could not create root mailbox directory.");
+        if with_root_mailbox {
+            for d in &["cur", "new", "tmp"] {
+                std::fs::create_dir(root_mailbox.join(d))
+                    .expect("Could not create root mailbox directory contents.");
+            }
+        }
+    }
+    let subscribed_mailboxes = if with_root_mailbox {
+        vec!["inbox".into()]
+    } else {
+        vec![]
+    };
+    let mailboxes = if with_root_mailbox {
+        vec![(
+            "inbox".into(),
+            melib::conf::MailboxConf {
+                extra: indexmap::indexmap! {
+                    "path".into() => root_mailbox.display().to_string(),
+                },
+                ..Default::default()
+            },
+        )]
+        .into_iter()
+        .collect()
+    } else {
+        indexmap::indexmap! {}
+    };
+    let extra = if with_root_mailbox {
+        indexmap::indexmap! {
+            "root_mailbox".into() => root_mailbox.display().to_string(),
+        }
+    } else {
+        indexmap::indexmap! {}
+    };
+
+    let account_conf = melib::AccountSettings {
+        name: acc_name.to_string(),
+        root_mailbox: root_mailbox.display().to_string(),
+        format: "maildir".to_string(),
+        identity: "user@localhost".to_string(),
+        extra_identities: vec![],
+        read_only: false,
+        display_name: None,
+        order: Default::default(),
+        subscribed_mailboxes,
+        mailboxes,
+        manual_refresh: true,
+        extra,
+    };
+
+    let maildir = MaildirType::new(&account_conf, Default::default(), event_consumer)?;
+    Ok((root_mailbox, account_conf.into(), maildir))
+}
+
+#[test]
+fn test_accounts_mailbox_by_path_error_msg() {
+    const ACCOUNT_NAME: &str = "test";
+
+    let eprintln_ok = eprintln_ok_fn();
+    let mut eprint_step_closure = eprint_step_fn();
+    macro_rules! eprint_step {
+        ($($arg:tt)+) => {{
+            eprint_step_closure(format_args!($($arg)+));
+        }};
+    }
+    let temp_dir = TempDir::new().unwrap();
+    {
+        eprint_step!(
+            "Create maildir backend with a root mailbox, \"inbox\" which will be a valid maildir \
+             folder because it will contain cur, new, tmp subdirectories..."
+        );
+        let mut ctx = crate::Context::new_mock(&temp_dir);
+        let backend_event_queue = Arc::new(std::sync::Mutex::new(
+            std::collections::VecDeque::with_capacity(16),
+        ));
+
+        let backend_event_consumer = {
+            let backend_event_queue = Arc::clone(&backend_event_queue);
+
+            BackendEventConsumer::new(Arc::new(move |ah, be| {
+                backend_event_queue.lock().unwrap().push_back((ah, be));
+            }))
+        };
+
+        let (root_mailbox, settings, maildir) =
+            new_maildir_backend(&temp_dir, ACCOUNT_NAME, backend_event_consumer, true).unwrap();
+        eprintln_ok();
+        let name = maildir.account_name.to_string();
+        let account_hash = maildir.account_hash;
+        let backend = maildir as Box<dyn MailBackend>;
+        let ref_mailboxes = smol::block_on(backend.mailboxes().unwrap()).unwrap();
+        let contacts = melib::contacts::Contacts::new(name.to_string());
+
+        let mut account = super::Account {
+            hash: account_hash,
+            name: name.into(),
+            is_online: super::IsOnline::True,
+            mailbox_entries: Default::default(),
+            mailboxes_order: Default::default(),
+            tree: Default::default(),
+            contacts,
+            collection: backend.collection(),
+            settings,
+            main_loop_handler: ctx.main_loop_handler.clone(),
+            active_jobs: HashMap::default(),
+            active_job_instants: std::collections::BTreeMap::default(),
+            event_queue: IndexMap::default(),
+            backend_capabilities: backend.capabilities(),
+            backend: Arc::new(std::sync::RwLock::new(backend)),
+        };
+        account.init(ref_mailboxes).unwrap();
+        while let Ok(thread_event) = ctx.receiver.try_recv() {
+            if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
+                if !account.process_event(&job_id) {
+                    assert!(
+                        ctx.accounts[0].process_event(&job_id),
+                        "unclaimed job id: {:?}",
+                        job_id
+                    );
+                }
+            }
+        }
+        eprint_step!("Assert that mailbox_by_path(\"inbox\") returns the root mailbox...");
+        account.mailbox_by_path("inbox").unwrap();
+        eprintln_ok();
+        eprint_step!(
+            "Assert that mailbox_by_path(\"box\") returns an error mentioning the root mailbox..."
+        );
+        assert_eq!(
+            account.mailbox_by_path("box").unwrap_err().to_string(),
+            Error {
+                summary: "Mailbox with that path not found.".into(),
+                details: Some(
+                    "Some matching paths that were found: [\"inbox\"]. You can inspect the list \
+                     of mailbox paths of an account with the manage-mailboxes command."
+                        .into()
+                ),
+                source: None,
+                inner: None,
+                related_path: None,
+                kind: ErrorKind::NotFound
+            }
+            .to_string()
+        );
+        eprintln_ok();
+
+        macro_rules! wait_for_job {
+            ($job_id:expr) => {{
+                let wait_for = $job_id;
+                while let Ok(thread_event) = ctx.receiver.recv() {
+                    if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
+                        if !account.process_event(&job_id) {
+                            assert!(
+                                ctx.accounts[0].process_event(&job_id),
+                                "unclaimed job id: {:?}",
+                                job_id
+                            );
+                        } else if job_id == wait_for {
+                            break;
+                        }
+                    }
+                }
+            }};
+        }
+        eprint_step!(
+            "Create new mailboxes: \"Sent\", \"Trash\", \"Drafts\", \"Archive\", \"Outbox\", \
+             \"Archive/Archive (old)\"..."
+        );
+        wait_for_job!(account
+            .mailbox_operation(MailboxOperation::Create("Sent".to_string()))
+            .unwrap());
+        wait_for_job!(account
+            .mailbox_operation(MailboxOperation::Create("Trash".to_string()))
+            .unwrap());
+        wait_for_job!(account
+            .mailbox_operation(MailboxOperation::Create("Drafts".to_string()))
+            .unwrap());
+        wait_for_job!(account
+            .mailbox_operation(MailboxOperation::Create("Archive".to_string()))
+            .unwrap());
+        wait_for_job!(account
+            .mailbox_operation(MailboxOperation::Create("Outbox".to_string()))
+            .unwrap());
+        wait_for_job!(account
+            .mailbox_operation(MailboxOperation::Create(
+                "inbox/Archive/Archive (old)".to_string(),
+            ))
+            .unwrap());
+        eprintln_ok();
+        eprint_step!(
+            "Assert that mailbox_by_path(\"rchive\") returns an error and mentions matching \
+             archives with mailboxes with the least depth in the tree hierarchy of mailboxes \
+             mentioned first..."
+        );
+        assert_eq!(
+            account.mailbox_by_path("rchive").unwrap_err().to_string(),
+            Error {
+                summary: "Mailbox with that path not found.".into(),
+                details: Some(
+                    "Some matching paths that were found: [\"inbox/Archive\", \
+                     \"inbox/Archive/Archive (old)\"]. You can inspect the list of mailbox paths \
+                     of an account with the manage-mailboxes command."
+                        .into()
+                ),
+                source: None,
+                inner: None,
+                related_path: None,
+                kind: ErrorKind::NotFound
+            }
+            .to_string()
+        );
+        eprintln_ok();
+        eprint_step!("Create \"inbox/Archive/Archive{{1,2,3,4,5,6,7,8,9,10}}\" mailboxes...");
+        for i in 1..=10 {
+            wait_for_job!(account
+                .mailbox_operation(MailboxOperation::Create(format!(
+                    "inbox/Archive/Archive{i}"
+                )))
+                .unwrap());
+        }
+        eprintln_ok();
+        eprint_step!(
+            "Assert that mailbox_by_path(\"inbox/Archive/Archive{{n}}\") works, i.e. we have to \
+             specify the root prefix \"inbox\"..."
+        );
+        for i in 1..=10 {
+            account
+                .mailbox_by_path(&format!("inbox/Archive/Archive{i}"))
+                .unwrap();
+            account
+                .mailbox_by_path(&format!("Archive/Archive{i}"))
+                .unwrap_err();
+        }
+        eprintln_ok();
+        eprint_step!(
+            "Assert that mailbox_by_path(\"rchive\") returns and error and truncates the matching \
+             mailbox paths to 5 maximum..."
+        );
+        assert_eq!(
+            account.mailbox_by_path("rchive").unwrap_err().to_string(),
+            Error {
+                summary: "Mailbox with that path not found.".into(),
+                details: Some(
+                    "Some matching paths that were found: [\"inbox/Archive\", \
+                     \"inbox/Archive/Archive1\", \"inbox/Archive/Archive2\", \
+                     \"inbox/Archive/Archive3\", \"inbox/Archive/Archive4\"] and 7 others. You \
+                     can inspect the list of mailbox paths of an account with the \
+                     manage-mailboxes command."
+                        .into()
+                ),
+                source: None,
+                inner: None,
+                related_path: None,
+                kind: ErrorKind::NotFound
+            }
+            .to_string()
+        );
+        eprintln_ok();
+        eprint_step!(
+            "Assert that mailbox_by_path(\"inbox/Archive\") returns a valid result (since the \
+             root mailbox is a valid maildir folder)..."
+        );
+        account.mailbox_by_path("inbox/Archive").unwrap();
+        eprintln_ok();
+
+        eprint_step!("Cleanup maildir account with valid root mailbox...");
+        std::fs::remove_dir_all(root_mailbox).unwrap();
+        eprintln_ok();
+    }
+
+    {
+        eprint_step!(
+            "Create maildir backend with a root mailbox, \"inbox\" which will NOT be a valid \
+             maildir folder because it will NOT contain cur, new, tmp subdirectories..."
+        );
+        let mut ctx = crate::Context::new_mock(&temp_dir);
+        let backend_event_queue = Arc::new(std::sync::Mutex::new(
+            std::collections::VecDeque::with_capacity(16),
+        ));
+
+        let backend_event_consumer = {
+            let backend_event_queue = Arc::clone(&backend_event_queue);
+
+            BackendEventConsumer::new(Arc::new(move |ah, be| {
+                backend_event_queue.lock().unwrap().push_back((ah, be));
+            }))
+        };
+
+        let (_root_mailbox, settings, maildir) =
+            new_maildir_backend(&temp_dir, ACCOUNT_NAME, backend_event_consumer, false).unwrap();
+        eprintln_ok();
+        let name = maildir.account_name.to_string();
+        let account_hash = maildir.account_hash;
+        let backend = maildir as Box<dyn MailBackend>;
+        let ref_mailboxes = smol::block_on(backend.mailboxes().unwrap()).unwrap();
+        eprint_step!("Assert that created account has no mailboxes at all...");
+        assert!(
+            ref_mailboxes.is_empty(),
+            "ref_mailboxes were not empty: {:?}",
+            ref_mailboxes
+        );
+        eprintln_ok();
+        let contacts = melib::contacts::Contacts::new(name.to_string());
+
+        let mut account = super::Account {
+            hash: account_hash,
+            name: name.into(),
+            is_online: super::IsOnline::True,
+            mailbox_entries: Default::default(),
+            mailboxes_order: Default::default(),
+            tree: Default::default(),
+            contacts,
+            collection: backend.collection(),
+            settings,
+            main_loop_handler: ctx.main_loop_handler.clone(),
+            active_jobs: HashMap::default(),
+            active_job_instants: std::collections::BTreeMap::default(),
+            event_queue: IndexMap::default(),
+            backend_capabilities: backend.capabilities(),
+            backend: Arc::new(std::sync::RwLock::new(backend)),
+        };
+        account.init(ref_mailboxes).unwrap();
+        while let Ok(thread_event) = ctx.receiver.try_recv() {
+            if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
+                if !account.process_event(&job_id) {
+                    assert!(
+                        ctx.accounts[0].process_event(&job_id),
+                        "unclaimed job id: {:?}",
+                        job_id
+                    );
+                }
+            }
+        }
+        eprint_step!(
+            "Assert that mailbox_by_path(\"inbox\") does not return a valid result (there are no \
+             mailboxes)..."
+        );
+        assert_eq!(
+            account.mailbox_by_path("inbox").unwrap_err().to_string(),
+            Error {
+                summary: "Mailbox with that path not found.".into(),
+                details: Some(
+                    "You can inspect the list of mailbox paths of an account with the \
+                     manage-mailboxes command."
+                        .into()
+                ),
+                source: None,
+                inner: None,
+                related_path: None,
+                kind: ErrorKind::NotFound
+            }
+            .to_string()
+        );
+        eprintln_ok();
+        eprint_step!(
+            "Create multiple maildir folders \"inbox/Archive{{1,2,3,4,5,6,7,8,9,10}}\"..."
+        );
+        macro_rules! wait_for_job {
+            ($job_id:expr) => {{
+                let wait_for = $job_id;
+                while let Ok(thread_event) = ctx.receiver.recv() {
+                    if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
+                        if !account.process_event(&job_id) {
+                            assert!(
+                                ctx.accounts[0].process_event(&job_id),
+                                "unclaimed job id: {:?}",
+                                job_id
+                            );
+                        } else if job_id == wait_for {
+                            break;
+                        }
+                    }
+                }
+            }};
+        }
+        for i in 1..=10 {
+            wait_for_job!(account
+                .mailbox_operation(MailboxOperation::Create(format!("inbox/Archive{i}")))
+                .unwrap());
+        }
+        eprintln_ok();
+        eprint_step!(
+            "Assert that mailbox_by_path(\"Archive{{n}}\") works, and that we don't have to \
+             specify the root prefix \"inbox\"..."
+        );
+        for i in 1..=10 {
+            account.mailbox_by_path(&format!("Archive{i}")).unwrap();
+        }
+        eprintln_ok();
+        eprint_step!(
+            "Assert that mailbox_by_path(\"rchive\") returns an error message with matches..."
+        );
+        assert_eq!(
+            account.mailbox_by_path("rchive").unwrap_err().to_string(),
+            Error {
+                summary: "Mailbox with that path not found.".into(),
+                details: Some(
+                    "Some matching paths that were found: [\"Archive1\", \"Archive2\", \
+                     \"Archive3\", \"Archive4\", \"Archive5\"] and 5 others. You can inspect the \
+                     list of mailbox paths of an account with the manage-mailboxes command."
+                        .into()
+                ),
+                source: None,
+                inner: None,
+                related_path: None,
+                kind: ErrorKind::NotFound
+            }
+            .to_string()
+        );
+        eprintln_ok();
+        eprint_step!(
+            "Assert that mailbox_by_path(\"inbox/Archive{{n}}\") does not return a valid result..."
+        );
+        assert_eq!(
+            account
+                .mailbox_by_path("inbox/Archive1")
+                .unwrap_err()
+                .to_string(),
+            Error {
+                summary: "Mailbox with that path not found.".into(),
+                details: Some(
+                    "You can inspect the list of mailbox paths of an account with the \
+                     manage-mailboxes command."
+                        .into()
+                ),
+                source: None,
+                inner: None,
+                related_path: None,
+                kind: ErrorKind::NotFound
+            }
+            .to_string()
+        );
+        eprintln_ok();
+    }
+}