meli/accounts: add mailbox_by_path() tests

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
This commit is contained in:
Manos Pitsidianakis 2024-11-28 12:35:43 +02:00
parent 18e9d5c148
commit de65eec3a9
No known key found for this signature in database
GPG key ID: 7729C7707F7E09D0
3 changed files with 483 additions and 18 deletions

View file

@ -1337,16 +1337,23 @@ impl Account {
.list_mailboxes()
.into_iter()
.map(|n| (n.hash, n.depth))
.collect::<BTreeMap<MailboxHash, usize>>();
.collect::<IndexMap<MailboxHash, usize>>();
let mut entries = self
.mailbox_entries
.iter()
.map(|(h, f)| (h, f.ref_mailbox.path()))
.map(|(h, f)| (h, f.ref_mailbox.name(), f.ref_mailbox.path()))
.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 mut potential_matches = IndexSet::new();
for (_, haystack) in entries {
for (_, _, haystack) in &entries {
let ac = AhoCorasick::builder()
.ascii_case_insensitive(true)
.build(patterns)

View file

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

View file

@ -20,13 +20,21 @@
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
use melib::{
backends::{Mailbox, MailboxHash},
error::Result,
MailboxPermissions, SpecialUsageMailbox,
};
use std::path::PathBuf;
use crate::accounts::{FileMailboxConf, MailboxEntry, MailboxStatus};
use melib::{
backends::{prelude::*, Mailbox, MailboxHash},
error::Result,
maildir::MaildirType,
smol, MailboxPermissions, SpecialUsageMailbox,
};
use tempfile::TempDir;
use crate::{
accounts::{AccountConf, FileMailboxConf, MailboxEntry, MailboxStatus},
command::actions::MailboxOperation,
utilities::tests::{eprint_step_fn, eprintln_ok_fn},
};
#[test]
fn test_mailbox_utf7() {
@ -110,3 +118,448 @@ fn test_mailbox_utf7() {
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();
}
}