v0.0.5
RustBox CI / rustbox (push) Waiting to run

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-30 12:06:44 +01:00
1 parent 96a32ea8fb
commit a643e7e3ab
106 files changed
+2223 -4669

No files matched your search

+10 -47
View File
@@ -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();