mirror of
https://github.com/spartanz51/tutabridge.git
synced 2026-06-24 10:54:32 +02:00
test(imap): cover on-demand failure path; bound the cooldown map
Add a MockBackend error mode and a test asserting a failed on-demand body fetch returns NO [UNAVAILABLE] (not a placeholder body) and arms the cooldown, and that a cooled-down mail then short-circuits to NO without touching the backend. Also prune elapsed entries when arming a cooldown so the map stays bounded by the mails currently cooling down rather than every mail that has ever failed.
This commit is contained in:
@@ -1640,6 +1640,7 @@ mod tests {
|
|||||||
unread_calls: Mutex<Vec<(Vec<IdTupleGenerated>, bool)>>,
|
unread_calls: Mutex<Vec<(Vec<IdTupleGenerated>, bool)>>,
|
||||||
sent: Mutex<Vec<ParsedMessage>>,
|
sent: Mutex<Vec<ParsedMessage>>,
|
||||||
moved: Mutex<Vec<(Vec<IdTupleGenerated>, String)>>,
|
moved: Mutex<Vec<(Vec<IdTupleGenerated>, String)>>,
|
||||||
|
fail_details: Mutex<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockBackend {
|
impl MockBackend {
|
||||||
@@ -1651,9 +1652,14 @@ mod tests {
|
|||||||
unread_calls: Mutex::new(Vec::new()),
|
unread_calls: Mutex::new(Vec::new()),
|
||||||
sent: Mutex::new(Vec::new()),
|
sent: Mutex::new(Vec::new()),
|
||||||
moved: Mutex::new(Vec::new()),
|
moved: Mutex::new(Vec::new()),
|
||||||
|
fail_details: Mutex::new(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_fail_details(&self, v: bool) {
|
||||||
|
*self.fail_details.lock().unwrap() = v;
|
||||||
|
}
|
||||||
|
|
||||||
fn with_mails(mails: Vec<Mail>) -> Self {
|
fn with_mails(mails: Vec<Mail>) -> Self {
|
||||||
let m = Self::new();
|
let m = Self::new();
|
||||||
*m.mails.lock().unwrap() = mails;
|
*m.mails.lock().unwrap() = mails;
|
||||||
@@ -1736,6 +1742,48 @@ mod tests {
|
|||||||
assert_eq!(moved[0].0.len(), 1);
|
assert_eq!(moved[0].0.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn on_demand_body_fetch_failure_returns_no_and_cooldowns() {
|
||||||
|
let mail = make_mail("ed", "subj", false);
|
||||||
|
let backend = Arc::new(MockBackend::with_mails(vec![mail.clone()]));
|
||||||
|
backend.set_fail_details(true);
|
||||||
|
let store = MailStore::new();
|
||||||
|
store.set_folder_list(vec![inbox_folder()]).await;
|
||||||
|
store
|
||||||
|
.set_folder(
|
||||||
|
"inbox",
|
||||||
|
vec![StoredMail {
|
||||||
|
mail,
|
||||||
|
details: None,
|
||||||
|
rfc2822: None,
|
||||||
|
uid: 1,
|
||||||
|
attachments_pending: false,
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let mut session = ImapSession::new(store.clone(), backend.clone(), None, None);
|
||||||
|
session.handle_command("a LOGIN u p").await;
|
||||||
|
session.handle_command("b SELECT INBOX").await;
|
||||||
|
|
||||||
|
// A failed on-demand body fetch returns NO [UNAVAILABLE] (not a fake
|
||||||
|
// body) and arms the shared cooldown.
|
||||||
|
let resp = session.handle_command("c UID FETCH 1 (BODY[])").await;
|
||||||
|
assert!(
|
||||||
|
resp.last().unwrap().contains("NO [UNAVAILABLE]"),
|
||||||
|
"expected NO on failure, got {resp:?}"
|
||||||
|
);
|
||||||
|
assert!(store.body_fetch_on_cooldown("ed"));
|
||||||
|
|
||||||
|
// While cooling down, a repeat fetch is short-circuited to NO without
|
||||||
|
// touching the backend, even though it would now succeed.
|
||||||
|
backend.set_fail_details(false);
|
||||||
|
let resp2 = session.handle_command("d UID FETCH 1 (BODY[])").await;
|
||||||
|
assert!(
|
||||||
|
resp2.last().unwrap().contains("NO [UNAVAILABLE]"),
|
||||||
|
"cooldown should keep returning NO, got {resp2:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn copy_is_rejected() {
|
async fn copy_is_rejected() {
|
||||||
let backend = Arc::new(MockBackend::with_mails(vec![]));
|
let backend = Arc::new(MockBackend::with_mails(vec![]));
|
||||||
@@ -1860,6 +1908,9 @@ mod tests {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
async fn load_mail_details(&self, mail: &Mail) -> Result<Option<MailDetails>, String> {
|
async fn load_mail_details(&self, mail: &Mail) -> Result<Option<MailDetails>, String> {
|
||||||
|
if *self.fail_details.lock().unwrap() {
|
||||||
|
return Err("simulated Too Many Requests".to_string());
|
||||||
|
}
|
||||||
let key = mail
|
let key = mail
|
||||||
._id
|
._id
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|||||||
@@ -74,8 +74,12 @@ impl MailStore {
|
|||||||
/// Arm a cooldown after a failed on-demand body fetch so the bridge does not
|
/// Arm a cooldown after a failed on-demand body fetch so the bridge does not
|
||||||
/// immediately re-hit a throttled or erroring server for the same mail.
|
/// immediately re-hit a throttled or erroring server for the same mail.
|
||||||
pub(crate) fn mark_body_fetch_failed(&self, element_id: &str) {
|
pub(crate) fn mark_body_fetch_failed(&self, element_id: &str) {
|
||||||
crate::util::lock_recover(&self.body_fetch_cooldown)
|
let mut map = crate::util::lock_recover(&self.body_fetch_cooldown);
|
||||||
.insert(element_id.to_string(), Instant::now() + BODY_FETCH_COOLDOWN);
|
let now = Instant::now();
|
||||||
|
// Drop elapsed entries so the map stays bounded by the number of mails
|
||||||
|
// currently cooling down, not every mail that has ever failed.
|
||||||
|
map.retain(|_, until| *until > now);
|
||||||
|
map.insert(element_id.to_string(), now + BODY_FETCH_COOLDOWN);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscribe(&self) -> watch::Receiver<u64> {
|
pub fn subscribe(&self) -> watch::Receiver<u64> {
|
||||||
|
|||||||
Reference in New Issue
Block a user