use crate::message::Message;
use crate::transport::{BoxFuture, Transport};
use rustlavel_core::Result;
use std::sync::Mutex;
#[derive(Default)]
pub struct Fake {
sent: Mutex<Vec<Message>>,
}
impl Fake {
pub fn new() -> Fake {
Fake::default()
}
pub fn sent(&self) -> Vec<Message> {
self.messages().clone()
}
pub fn sent_to(&self, address: &str) -> Vec<Message> {
self.messages()
.iter()
.filter(|message| {
message.envelope_recipients().iter().any(|recipient| recipient == address)
})
.cloned()
.collect()
}
pub fn count(&self) -> usize {
self.messages().len()
}
pub fn clear(&self) {
self.messages().clear();
}
#[track_caller]
pub fn assert_sent_to(&self, address: &str) {
assert!(
!self.sent_to(address).is_empty(),
"expected a message to {address}; {}",
self.summary()
);
}
#[track_caller]
pub fn assert_not_sent_to(&self, address: &str) {
assert!(
self.sent_to(address).is_empty(),
"did not expect a message to {address}; {}",
self.summary()
);
}
#[track_caller]
pub fn assert_sent_times(&self, expected: usize) {
assert_eq!(self.count(), expected, "unexpected number of messages; {}", self.summary());
}
#[track_caller]
pub fn assert_nothing_sent(&self) {
assert!(self.count() == 0, "expected no mail at all; {}", self.summary());
}
fn summary(&self) -> String {
let messages = self.messages();
if messages.is_empty() {
return "no messages were sent".to_string();
}
let lines: Vec<String> = messages
.iter()
.map(|message| {
format!(
"`{}` to {}",
message.subject_text(),
message.envelope_recipients().join(", ")
)
})
.collect();
format!("sent: {}", lines.join("; "))
}
fn messages(&self) -> std::sync::MutexGuard<'_, Vec<Message>> {
self.sent.lock().expect("mail fake lock poisoned")
}
}
impl Transport for Fake {
fn name(&self) -> &'static str {
"fake"
}
fn send<'a>(&'a self, message: &'a Message) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
message.to_mime()?;
self.messages().push(message.clone());
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Mail;
use crate::message::Attachment;
fn message() -> Message {
Message::new()
.from("no-reply@example.com")
.to("ada@example.com")
.subject("Your receipt")
.text("Thank you.")
.html("<p>Thank you.</p>")
}
#[tokio::test]
async fn nothing_is_sent_and_everything_is_recorded() {
let mailer = Mail::fake();
mailer.send(message()).await.unwrap();
mailer.send(message().to("grace@example.com").subject("Second")).await.unwrap();
let fake = mailer.fake().expect("a faking mailer");
fake.assert_sent_to("ada@example.com");
fake.assert_sent_to("grace@example.com");
fake.assert_not_sent_to("nobody@example.com");
fake.assert_sent_times(2);
assert_eq!(mailer.transport_name(), "fake");
}
#[tokio::test]
async fn a_test_can_read_the_subject_and_the_bodies_it_recorded() {
let mailer = Mail::fake();
mailer
.send(message().attach(Attachment::new("receipt.pdf", b"%PDF".to_vec())))
.await
.unwrap();
let recorded = mailer.fake().unwrap().sent();
let sent = &recorded[0];
assert_eq!(sent.subject_text(), "Your receipt");
assert_eq!(sent.text_body(), Some("Thank you."));
assert_eq!(sent.html_body(), Some("<p>Thank you.</p>"));
assert_eq!(sent.attachments()[0].filename, "receipt.pdf");
assert!(sent.to_mime().unwrap().contains("multipart/mixed"));
}
#[tokio::test]
async fn assertions_are_also_reachable_on_the_mailer_itself() {
let mailer = Mail::fake();
mailer.assert_nothing_sent();
mailer.send(message()).await.unwrap();
mailer.assert_sent_to("ada@example.com");
mailer.assert_sent_times(1);
}
#[tokio::test]
async fn a_message_that_cannot_be_rendered_fails_in_the_test() {
let mailer = Mail::fake();
let error = mailer
.send(Message::new().from("no-reply@example.com").to("nonsense"))
.await
.unwrap_err()
.to_string();
assert!(error.contains("nonsense"), "{error}");
mailer.assert_nothing_sent();
}
#[tokio::test]
async fn a_failed_assertion_says_what_was_actually_sent() {
let fake = Fake::new();
fake.send(&message()).await.unwrap();
let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
fake.assert_sent_to("nobody@example.com")
}))
.unwrap_err();
let text = panicked
.downcast_ref::<String>()
.cloned()
.unwrap_or_else(|| "not a string".to_string());
assert!(text.contains("nobody@example.com"), "{text}");
assert!(text.contains("`Your receipt` to ada@example.com"), "{text}");
}
#[tokio::test]
async fn recorded_messages_can_be_cleared_between_phases_of_a_test() {
let fake = Fake::new();
fake.send(&message()).await.unwrap();
fake.assert_sent_times(1);
fake.clear();
fake.assert_nothing_sent();
}
}