use super::{ AttachmentData, SendableMessage, SmtpClient, SmtpCredentials, SmtpError, SmtpSendResult, }; use async_trait::async_trait; use std::sync::Mutex; pub struct MockSmtpClient { should_fail: Mutex>, sent_messages: Mutex>, } impl MockSmtpClient { pub fn new() -> Self { Self { should_fail: Mutex::new(None), sent_messages: Mutex::new(Vec::new()), } } pub fn with_error(self, error: SmtpError) -> Self { *self.should_fail.lock().unwrap() = Some(error); self } pub fn sent_count(&self) -> usize { self.sent_messages.lock().unwrap().len() } pub fn last_sent(&self) -> Option { self.sent_messages.lock().unwrap().last().cloned() } } impl Default for MockSmtpClient { fn default() -> Self { Self::new() } } fn clone_error(err: &SmtpError) -> SmtpError { match err { SmtpError::ConnectionFailed(msg) => SmtpError::ConnectionFailed(msg.clone()), SmtpError::AuthenticationFailed => SmtpError::AuthenticationFailed, SmtpError::SendFailed(msg) => SmtpError::SendFailed(msg.clone()), SmtpError::MessageTooLarge { actual, limit } => SmtpError::MessageTooLarge { actual: *actual, limit: *limit, }, } } #[async_trait] impl SmtpClient for MockSmtpClient { async fn send_message( &self, _creds: &SmtpCredentials, message: &SendableMessage, ) -> Result { if let Some(ref err) = *self.should_fail.lock().unwrap() { return Err(clone_error(err)); } let message_id = format!("", uuid::Uuid::new_v4()); let rfc822 = crate::smtp::builder::build_rfc822(message, &message_id) .map_err(SmtpError::SendFailed)?; if message.max_message_size_bytes > 0 && rfc822.len() > message.max_message_size_bytes { return Err(SmtpError::MessageTooLarge { actual: rfc822.len(), limit: message.max_message_size_bytes, }); } self.sent_messages.lock().unwrap().push(message.clone()); Ok(SmtpSendResult { message_id, rfc822 }) } } #[cfg(test)] mod tests { use super::*; fn test_creds() -> SmtpCredentials { SmtpCredentials { host: "smtp.example.com".to_string(), port: 587, tls: true, tls_verify: true, email: "user@example.com".to_string(), password: "hunter2".to_string(), } } fn test_message() -> SendableMessage { SendableMessage { from: "user@example.com".to_string(), to: vec!["recipient@example.com".to_string()], cc: vec![], bcc: vec![], reply_to: None, organization: None, subject: "Test subject".to_string(), text_body: "Hello, world!".to_string(), html_body: None, in_reply_to: None, references: None, read_receipt_to: None, priority: "normal".to_string(), extra_headers: vec![], attachments: vec![], pgp: None, max_message_size_bytes: 50 * 1024 * 1024, } } #[tokio::test] async fn mock_send_succeeds_without_error() { let mock = MockSmtpClient::new(); let result = mock.send_message(&test_creds(), &test_message()).await; assert!(result.is_ok()); let result = result.unwrap(); assert!(!result.message_id.is_empty()); assert!(!result.rfc822.is_empty()); } #[tokio::test] async fn mock_rejects_message_larger_than_final_mime_limit() { let mock = MockSmtpClient::new(); let mut msg = test_message(); msg.max_message_size_bytes = 1; let err = mock.send_message(&test_creds(), &msg).await.unwrap_err(); assert!(matches!(err, SmtpError::MessageTooLarge { .. })); assert_eq!(mock.sent_count(), 0); } #[tokio::test] async fn mock_send_captures_message() { let mock = MockSmtpClient::new(); let msg = SendableMessage { from: "sender@example.com".to_string(), to: vec!["alice@example.com".to_string()], cc: vec!["bob@example.com".to_string()], bcc: vec![], reply_to: None, organization: None, subject: "Important".to_string(), text_body: "Please read.".to_string(), html_body: Some("

Please read.

".to_string()), in_reply_to: Some("".to_string()), references: Some("".to_string()), read_receipt_to: Some("sender@example.com".to_string()), priority: "high".to_string(), extra_headers: vec![], attachments: vec![AttachmentData { filename: "notes.txt".to_string(), content_type: "text/plain".to_string(), data: b"some content".to_vec(), content_id: None, }], pgp: None, max_message_size_bytes: 50 * 1024 * 1024, }; let result = mock.send_message(&test_creds(), &msg).await; assert!(result.is_ok()); assert_eq!(mock.sent_count(), 1); let captured = mock.last_sent().unwrap(); assert_eq!(captured.from, "sender@example.com"); assert_eq!(captured.to, vec!["alice@example.com"]); assert_eq!(captured.cc, vec!["bob@example.com"]); assert_eq!(captured.subject, "Important"); assert_eq!(captured.text_body, "Please read."); assert_eq!(captured.html_body.as_deref(), Some("

Please read.

")); assert_eq!( captured.in_reply_to.as_deref(), Some("") ); assert_eq!( captured.references.as_deref(), Some("") ); assert_eq!( captured.read_receipt_to.as_deref(), Some("sender@example.com") ); assert_eq!(captured.priority, "high"); assert_eq!(captured.attachments.len(), 1); assert_eq!(captured.attachments[0].filename, "notes.txt"); } #[tokio::test] async fn mock_with_error_returns_error() { let mock = MockSmtpClient::new().with_error(SmtpError::AuthenticationFailed); let err = mock .send_message(&test_creds(), &test_message()) .await .unwrap_err(); assert!(matches!(err, SmtpError::AuthenticationFailed)); assert_eq!(mock.sent_count(), 0); } #[tokio::test] async fn real_smtp_connection_fails_with_bad_host() { use crate::smtp::client::RealSmtpClient; let client = RealSmtpClient; let creds = SmtpCredentials { host: "invalid.host.test".to_string(), port: 587, tls: true, tls_verify: true, email: "user@invalid.host.test".to_string(), password: "password".to_string(), }; let msg = test_message(); let err = client.send_message(&creds, &msg).await.unwrap_err(); assert!( matches!( err, SmtpError::ConnectionFailed(_) | SmtpError::SendFailed(_) ), "Expected ConnectionFailed or SendFailed, got: {err}" ); } #[tokio::test] async fn smtp_error_display_formats_correctly() { let cases: Vec<(SmtpError, &str)> = vec![ ( SmtpError::ConnectionFailed("timeout".to_string()), "Connection failed: timeout", ), (SmtpError::AuthenticationFailed, "Authentication failed"), ( SmtpError::SendFailed("rejected by server".to_string()), "Send failed: rejected by server", ), ]; for (err, expected) in cases { assert_eq!(err.to_string(), expected); } } }