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)>>,
|
||||
sent: Mutex<Vec<ParsedMessage>>,
|
||||
moved: Mutex<Vec<(Vec<IdTupleGenerated>, String)>>,
|
||||
fail_details: Mutex<bool>,
|
||||
}
|
||||
|
||||
impl MockBackend {
|
||||
@@ -1651,9 +1652,14 @@ mod tests {
|
||||
unread_calls: Mutex::new(Vec::new()),
|
||||
sent: 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 {
|
||||
let m = Self::new();
|
||||
*m.mails.lock().unwrap() = mails;
|
||||
@@ -1736,6 +1742,48 @@ mod tests {
|
||||
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]
|
||||
async fn copy_is_rejected() {
|
||||
let backend = Arc::new(MockBackend::with_mails(vec![]));
|
||||
@@ -1860,6 +1908,9 @@ mod tests {
|
||||
Ok(None)
|
||||
}
|
||||
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
|
||||
._id
|
||||
.as_ref()
|
||||
|
||||
@@ -74,8 +74,12 @@ impl MailStore {
|
||||
/// 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.
|
||||
pub(crate) fn mark_body_fetch_failed(&self, element_id: &str) {
|
||||
crate::util::lock_recover(&self.body_fetch_cooldown)
|
||||
.insert(element_id.to_string(), Instant::now() + BODY_FETCH_COOLDOWN);
|
||||
let mut map = crate::util::lock_recover(&self.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> {
|
||||
|
||||
Reference in New Issue
Block a user