1use crate::message::Message;
8use crate::transport::{BoxFuture, Transport};
9use rustlavel_core::Result;
10use std::sync::Mutex;
11
12#[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 pub fn sent(&self) -> Vec<Message> {
25 self.messages().clone()
26 }
27
28 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 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 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 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 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}