Skip to main content

max / makenotwork

3.9 KB · 134 lines History Blame Raw
1 //! Mock email transport for integration tests.
2 //!
3 //! Records all sent emails so tests can assert on recipients, subjects, and bodies
4 //! without hitting any external service.
5
6 use super::faults::Faults;
7 use makenotwork::email::EmailTransport;
8 use makenotwork::error::Result;
9 use std::sync::Mutex;
10
11 /// A sent email record.
12 #[derive(Debug, Clone)]
13 #[allow(dead_code)]
14 pub(crate) struct SentEmail {
15 pub to: String,
16 pub subject: String,
17 pub body: String,
18 pub unsub_url: Option<String>,
19 pub stream: Option<String>,
20 /// The fan-out this mail was attributed to (`93f23f00`). Recorded rather
21 /// than ignored, because "the id went out with the mail" is the half of the
22 /// attribution a test can see -- the other half is Postmark handing it
23 /// back.
24 pub send: Option<String>,
25 }
26
27 /// In-memory email transport that records all sent emails.
28 pub(crate) struct MockEmailTransport {
29 sent: Mutex<Vec<SentEmail>>,
30 /// Injected send failures. Empty by default.
31 faults: Faults,
32 }
33
34 #[allow(dead_code)]
35 impl MockEmailTransport {
36 pub(crate) fn new() -> Self {
37 MockEmailTransport {
38 sent: Mutex::new(Vec::new()),
39 faults: Faults::new(),
40 }
41 }
42
43 /// The failure policy. All four send methods share the operation name
44 /// `send_email`, because a caller picks one by what it needs to include and
45 /// a transport outage takes out all of them together.
46 pub(crate) fn faults(&self) -> &Faults {
47 &self.faults
48 }
49
50 /// Return all emails sent so far.
51 pub(crate) fn sent(&self) -> Vec<SentEmail> {
52 self.sent.lock().unwrap().clone()
53 }
54
55 /// Return emails sent to a specific address.
56 pub(crate) fn sent_to(&self, address: &str) -> Vec<SentEmail> {
57 self.sent
58 .lock()
59 .unwrap()
60 .iter()
61 .filter(|e| e.to == address)
62 .cloned()
63 .collect()
64 }
65
66 /// Clear the sent email log.
67 pub(crate) fn clear(&self) {
68 self.sent.lock().unwrap().clear();
69 }
70
71 /// Return the number of emails sent.
72 pub(crate) fn count(&self) -> usize {
73 self.sent.lock().unwrap().len()
74 }
75 }
76
77 #[async_trait::async_trait]
78 impl EmailTransport for MockEmailTransport {
79 async fn send_email(&self, to: &str, subject: &str, body: &str) -> Result<()> {
80 self.faults.check("send_email")?;
81 self.sent.lock().unwrap().push(SentEmail {
82 to: to.to_string(),
83 subject: subject.to_string(),
84 body: body.to_string(),
85 unsub_url: None,
86 stream: None,
87 // Transactional mail belongs to no fan-out.
88 send: None,
89 });
90 Ok(())
91 }
92
93 async fn send_email_with_headers_and_unsub(
94 &self,
95 to: &str,
96 subject: &str,
97 body: &str,
98 _extra_headers: &[(&str, String)],
99 unsub_url: Option<&str>,
100 ) -> Result<()> {
101 self.faults.check("send_email")?;
102 self.sent.lock().unwrap().push(SentEmail {
103 to: to.to_string(),
104 subject: subject.to_string(),
105 body: body.to_string(),
106 unsub_url: unsub_url.map(std::string::ToString::to_string),
107 stream: None,
108 // Transactional mail belongs to no fan-out.
109 send: None,
110 });
111 Ok(())
112 }
113
114 async fn send_email_broadcast_with_unsub(
115 &self,
116 to: &str,
117 subject: &str,
118 body: &str,
119 unsub_url: Option<&str>,
120 send: Option<makenotwork::db::EmailSendId>,
121 ) -> Result<()> {
122 self.faults.check("send_email")?;
123 self.sent.lock().unwrap().push(SentEmail {
124 to: to.to_string(),
125 subject: subject.to_string(),
126 body: body.to_string(),
127 unsub_url: unsub_url.map(std::string::ToString::to_string),
128 stream: Some("broadcast".to_string()),
129 send: send.map(|id| id.to_string()),
130 });
131 Ok(())
132 }
133 }
134