247 lines
8.0 KiB
Rust
247 lines
8.0 KiB
Rust
use super::{
|
|
AttachmentData, SendableMessage, SmtpClient, SmtpCredentials, SmtpError, SmtpSendResult,
|
|
};
|
|
use async_trait::async_trait;
|
|
use std::sync::Mutex;
|
|
|
|
pub struct MockSmtpClient {
|
|
should_fail: Mutex<Option<SmtpError>>,
|
|
sent_messages: Mutex<Vec<SendableMessage>>,
|
|
}
|
|
|
|
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<SendableMessage> {
|
|
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<SmtpSendResult, SmtpError> {
|
|
if let Some(ref err) = *self.should_fail.lock().unwrap() {
|
|
return Err(clone_error(err));
|
|
}
|
|
let message_id = format!("<mock-{}>", 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("<p>Please read.</p>".to_string()),
|
|
in_reply_to: Some("<original@example.com>".to_string()),
|
|
references: Some("<original@example.com>".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("<p>Please read.</p>"));
|
|
assert_eq!(
|
|
captured.in_reply_to.as_deref(),
|
|
Some("<original@example.com>")
|
|
);
|
|
assert_eq!(
|
|
captured.references.as_deref(),
|
|
Some("<original@example.com>")
|
|
);
|
|
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);
|
|
}
|
|
}
|
|
}
|