1 parent
96a32ea8fb
commit
a643e7e3ab
106 files changed
+2223
-4669
No files matched your search
+10
-47
@@ -1,11 +1,6 @@
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// A mock IMAP client that returns pre-loaded data.
|
||||
///
|
||||
/// Uses interior mutability (`Mutex`) so it can be shared behind `&self`.
|
||||
/// Build it up with the `.with_*()` builder methods, then pass it wherever
|
||||
/// an `&dyn ImapClient` is needed.
|
||||
pub struct MockImapClient {
|
||||
folders: Mutex<Vec<ImapFolder>>,
|
||||
headers: Mutex<Vec<ImapMessageHeader>>,
|
||||
@@ -15,12 +10,11 @@ pub struct MockImapClient {
|
||||
should_fail: Mutex<Option<ImapError>>,
|
||||
pub appended: Mutex<Vec<(String, Vec<u8>)>>,
|
||||
pub header_requests: Mutex<Vec<(String, String)>>,
|
||||
/// One-shot gate after taking a header snapshot, before returning it.
|
||||
|
||||
pub header_fetch_gate: Option<(tokio::sync::Notify, tokio::sync::Notify)>,
|
||||
}
|
||||
|
||||
impl MockImapClient {
|
||||
/// Create a new empty mock.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
folders: Mutex::new(Vec::new()),
|
||||
@@ -35,47 +29,39 @@ impl MockImapClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre-load folders that `list_folders` will return.
|
||||
pub fn with_folders(self, folders: Vec<ImapFolder>) -> Self {
|
||||
*self.folders.lock().unwrap() = folders;
|
||||
self
|
||||
}
|
||||
|
||||
/// Pre-load message headers that `fetch_headers` will return.
|
||||
pub fn with_headers(self, headers: Vec<ImapMessageHeader>) -> Self {
|
||||
*self.headers.lock().unwrap() = headers;
|
||||
self
|
||||
}
|
||||
|
||||
/// Pre-load a folder status that `folder_status` will return.
|
||||
#[allow(dead_code)]
|
||||
pub fn with_folder_status(self, status: FolderStatus) -> Self {
|
||||
*self.folder_status.lock().unwrap() = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
/// Pre-load an extended folder status that `folder_status_extended` will return.
|
||||
#[allow(dead_code)]
|
||||
pub fn with_folder_status_extended(self, status: FolderStatusExtended) -> Self {
|
||||
*self.folder_status_ext.lock().unwrap() = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
/// Pre-load message bodies that `fetch_body` will match against by UID.
|
||||
pub fn with_bodies(self, bodies: Vec<ImapMessageBody>) -> Self {
|
||||
*self.bodies.lock().unwrap() = bodies;
|
||||
self
|
||||
}
|
||||
|
||||
/// Make every subsequent call return this error.
|
||||
pub fn with_error(self, error: ImapError) -> Self {
|
||||
*self.should_fail.lock().unwrap() = Some(error);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to clone an `ImapError` for the mock (the real errors are not
|
||||
/// `Clone`, so we reconstruct them).
|
||||
fn clone_error(err: &ImapError) -> ImapError {
|
||||
match err {
|
||||
ImapError::ConnectionFailed(msg) => ImapError::ConnectionFailed(msg.clone()),
|
||||
@@ -127,7 +113,7 @@ impl ImapClient for MockImapClient {
|
||||
return Ok(s.clone());
|
||||
}
|
||||
}
|
||||
// Derive from headers (separate lock scope).
|
||||
|
||||
let headers = self.headers.lock().unwrap();
|
||||
let exists = headers.len() as u32;
|
||||
let uid_next = headers.iter().map(|h| h.uid).max().unwrap_or(0) + 1;
|
||||
@@ -255,11 +241,7 @@ impl ImapClient for MockImapClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn empty_folder(
|
||||
&self,
|
||||
_creds: &ImapCredentials,
|
||||
_folder: &str,
|
||||
) -> Result<(), ImapError> {
|
||||
async fn empty_folder(&self, _creds: &ImapCredentials, _folder: &str) -> Result<(), ImapError> {
|
||||
if let Some(ref err) = *self.should_fail.lock().unwrap() {
|
||||
return Err(clone_error(err));
|
||||
}
|
||||
@@ -390,7 +372,7 @@ impl ImapClient for MockImapClient {
|
||||
return Ok(s.clone());
|
||||
}
|
||||
}
|
||||
// Derive from headers.
|
||||
|
||||
let headers = self.headers.lock().unwrap();
|
||||
let exists = headers.len() as u32;
|
||||
let uid_next = headers.iter().map(|h| h.uid).max().unwrap_or(0) + 1;
|
||||
@@ -412,7 +394,7 @@ impl ImapClient for MockImapClient {
|
||||
if let Some(ref err) = *self.should_fail.lock().unwrap() {
|
||||
return Err(clone_error(err));
|
||||
}
|
||||
// In mock, return all headers as "changed" with modseq 0.
|
||||
|
||||
let headers = self.headers.lock().unwrap();
|
||||
let items: Vec<(u32, Vec<String>)> =
|
||||
headers.iter().map(|h| (h.uid, h.flags.clone())).collect();
|
||||
@@ -438,15 +420,10 @@ impl ImapClient for MockImapClient {
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Convenience helper to build dummy credentials for tests.
|
||||
fn test_creds() -> ImapCredentials {
|
||||
ImapCredentials {
|
||||
host: "imap.example.com".to_string(),
|
||||
@@ -636,7 +613,7 @@ mod tests {
|
||||
let creds = test_creds();
|
||||
|
||||
let err = client.list_folders(&creds).await.unwrap_err();
|
||||
// With a fake host the connection should fail.
|
||||
|
||||
assert!(
|
||||
matches!(err, ImapError::ConnectionFailed(_)),
|
||||
"Expected ConnectionFailed, got: {err}"
|
||||
@@ -760,19 +737,6 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Integration tests (run manually against a real IMAP server)
|
||||
// -------------------------------------------------------------------
|
||||
//
|
||||
// cargo test real_imap -- --ignored
|
||||
//
|
||||
// Required env vars:
|
||||
// TEST_IMAP_HOST (e.g. "imap.gmail.com")
|
||||
// TEST_IMAP_PORT (e.g. "993")
|
||||
// TEST_IMAP_EMAIL (e.g. "you@gmail.com")
|
||||
// TEST_IMAP_PASSWORD (e.g. "app-password")
|
||||
// TEST_IMAP_TLS (e.g. "true")
|
||||
|
||||
fn real_creds() -> Option<ImapCredentials> {
|
||||
let host = std::env::var("TEST_IMAP_HOST").ok()?;
|
||||
let port: u16 = std::env::var("TEST_IMAP_PORT").ok()?.parse().ok()?;
|
||||
@@ -793,7 +757,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Run manually: cargo test real_imap_list_folders -- --ignored
|
||||
#[ignore]
|
||||
async fn real_imap_list_folders() {
|
||||
let creds = real_creds().expect("TEST_IMAP_* env vars required");
|
||||
let client = RealImapClient::default();
|
||||
@@ -807,24 +771,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Run manually: cargo test real_imap_fetch_headers -- --ignored
|
||||
#[ignore]
|
||||
async fn real_imap_fetch_headers() {
|
||||
let creds = real_creds().expect("TEST_IMAP_* env vars required");
|
||||
let client = RealImapClient::default();
|
||||
let headers = client.fetch_headers(&creds, "INBOX", "1:5").await.unwrap();
|
||||
// The mailbox might be empty, so we just check it doesn't error.
|
||||
|
||||
for h in &headers {
|
||||
assert!(h.uid > 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Run manually: cargo test real_imap_fetch_body -- --ignored
|
||||
#[ignore]
|
||||
async fn real_imap_fetch_body() {
|
||||
let creds = real_creds().expect("TEST_IMAP_* env vars required");
|
||||
let client = RealImapClient::default();
|
||||
|
||||
// First fetch headers to find a UID.
|
||||
let headers = client.fetch_headers(&creds, "INBOX", "1:1").await.unwrap();
|
||||
if let Some(h) = headers.first() {
|
||||
let body = client.fetch_body(&creds, "INBOX", h.uid).await.unwrap();
|
||||
|
||||
Reference in new issue
Block a user