Skip to main content

rustlavel_mail/
fake.rs

1//! Faking outbound mail.
2//!
3//! This is `Mail::fake()`. A test must never put a message on a real mail
4//! server: it is slow, it needs a server nobody has, and the day the address
5//! in a fixture is a real one it emails a real person.
6
7use crate::message::Message;
8use crate::transport::{BoxFuture, Transport};
9use rustlavel_core::Result;
10use std::sync::Mutex;
11
12/// A transport that records instead of sending.
13#[derive(Default)]
14pub struct Fake {
15    sent: Mutex<Vec<Message>>,
16}
17
18impl Fake {
19    pub fn new() -> Fake {
20        Fake::default()
21    }
22
23    /// Every message that would have been sent, in order.
24    pub fn sent(&self) -> Vec<Message> {
25        self.messages().clone()
26    }
27
28    /// The messages addressed to one recipient — To, Cc or Bcc.
29    pub fn sent_to(&self, address: &str) -> Vec<Message> {
30        self.messages()
31            .iter()
32            .filter(|message| {
33                message.envelope_recipients().iter().any(|recipient| recipient == address)
34            })
35            .cloned()
36            .collect()
37    }
38
39    pub fn count(&self) -> usize {
40        self.messages().len()
41    }
42
43    /// Forget everything recorded so far.
44    pub fn clear(&self) {
45        self.messages().clear();
46    }
47
48    #[track_caller]
49    pub fn assert_sent_to(&self, address: &str) {
50        assert!(
51            !self.sent_to(address).is_empty(),
52            "expected a message to {address}; {}",
53            self.summary()
54        );
55    }
56
57    #[track_caller]
58    pub fn assert_not_sent_to(&self, address: &str) {
59        assert!(
60            self.sent_to(address).is_empty(),
61            "did not expect a message to {address}; {}",
62            self.summary()
63        );
64    }
65
66    #[track_caller]
67    pub fn assert_sent_times(&self, expected: usize) {
68        assert_eq!(self.count(), expected, "unexpected number of messages; {}", self.summary());
69    }
70
71    #[track_caller]
72    pub fn assert_nothing_sent(&self) {
73        assert!(self.count() == 0, "expected no mail at all; {}", self.summary());
74    }
75
76    /// What a failed assertion prints: enough to see what actually happened
77    /// without printing whole message bodies into a test log.
78    fn summary(&self) -> String {
79        let messages = self.messages();
80        if messages.is_empty() {
81            return "no messages were sent".to_string();
82        }
83        let lines: Vec<String> = messages
84            .iter()
85            .map(|message| {
86                format!(
87                    "`{}` to {}",
88                    message.subject_text(),
89                    message.envelope_recipients().join(", ")
90                )
91            })
92            .collect();
93        format!("sent: {}", lines.join("; "))
94    }
95
96    fn messages(&self) -> std::sync::MutexGuard<'_, Vec<Message>> {
97        self.sent.lock().expect("mail fake lock poisoned")
98    }
99}
100
101impl Transport for Fake {
102    fn name(&self) -> &'static str {
103        "fake"
104    }
105
106    fn send<'a>(&'a self, message: &'a Message) -> BoxFuture<'a, Result<()>> {
107        Box::pin(async move {
108            // Rendered even though nothing is sent: a message that cannot be
109            // turned into MIME must fail in the test, not in production.
110            message.to_mime()?;
111            self.messages().push(message.clone());
112            Ok(())
113        })
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::Mail;
121    use crate::message::Attachment;
122
123    fn message() -> Message {
124        Message::new()
125            .from("no-reply@example.com")
126            .to("ada@example.com")
127            .subject("Your receipt")
128            .text("Thank you.")
129            .html("<p>Thank you.</p>")
130    }
131
132    #[tokio::test]
133    async fn nothing_is_sent_and_everything_is_recorded() {
134        let mailer = Mail::fake();
135
136        mailer.send(message()).await.unwrap();
137        mailer.send(message().to("grace@example.com").subject("Second")).await.unwrap();
138
139        let fake = mailer.fake().expect("a faking mailer");
140        fake.assert_sent_to("ada@example.com");
141        fake.assert_sent_to("grace@example.com");
142        fake.assert_not_sent_to("nobody@example.com");
143        fake.assert_sent_times(2);
144
145        assert_eq!(mailer.transport_name(), "fake");
146    }
147
148    #[tokio::test]
149    async fn a_test_can_read_the_subject_and_the_bodies_it_recorded() {
150        let mailer = Mail::fake();
151        mailer
152            .send(message().attach(Attachment::new("receipt.pdf", b"%PDF".to_vec())))
153            .await
154            .unwrap();
155
156        let recorded = mailer.fake().unwrap().sent();
157        let sent = &recorded[0];
158
159        assert_eq!(sent.subject_text(), "Your receipt");
160        assert_eq!(sent.text_body(), Some("Thank you."));
161        assert_eq!(sent.html_body(), Some("<p>Thank you.</p>"));
162        assert_eq!(sent.attachments()[0].filename, "receipt.pdf");
163        // The full MIME is available too, for a test that wants to be sure.
164        assert!(sent.to_mime().unwrap().contains("multipart/mixed"));
165    }
166
167    #[tokio::test]
168    async fn assertions_are_also_reachable_on_the_mailer_itself() {
169        let mailer = Mail::fake();
170        mailer.assert_nothing_sent();
171
172        mailer.send(message()).await.unwrap();
173
174        mailer.assert_sent_to("ada@example.com");
175        mailer.assert_sent_times(1);
176    }
177
178    #[tokio::test]
179    async fn a_message_that_cannot_be_rendered_fails_in_the_test() {
180        let mailer = Mail::fake();
181
182        let error = mailer
183            .send(Message::new().from("no-reply@example.com").to("nonsense"))
184            .await
185            .unwrap_err()
186            .to_string();
187
188        assert!(error.contains("nonsense"), "{error}");
189        mailer.assert_nothing_sent();
190    }
191
192    #[tokio::test]
193    async fn a_failed_assertion_says_what_was_actually_sent() {
194        let fake = Fake::new();
195        fake.send(&message()).await.unwrap();
196
197        let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
198            fake.assert_sent_to("nobody@example.com")
199        }))
200        .unwrap_err();
201
202        let text = panicked
203            .downcast_ref::<String>()
204            .cloned()
205            .unwrap_or_else(|| "not a string".to_string());
206
207        assert!(text.contains("nobody@example.com"), "{text}");
208        assert!(text.contains("`Your receipt` to ada@example.com"), "{text}");
209    }
210
211    #[tokio::test]
212    async fn recorded_messages_can_be_cleared_between_phases_of_a_test() {
213        let fake = Fake::new();
214        fake.send(&message()).await.unwrap();
215        fake.assert_sent_times(1);
216
217        fake.clear();
218        fake.assert_nothing_sent();
219    }
220}