Skip to main content

max / makenotwork

3.4 KB · 123 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 }
21
22 /// In-memory email transport that records all sent emails.
23 pub(crate) struct MockEmailTransport {
24 sent: Mutex<Vec<SentEmail>>,
25 /// Injected send failures. Empty by default.
26 faults: Faults,
27 }
28
29 #[allow(dead_code)]
30 impl MockEmailTransport {
31 pub(crate) fn new() -> Self {
32 MockEmailTransport {
33 sent: Mutex::new(Vec::new()),
34 faults: Faults::new(),
35 }
36 }
37
38 /// The failure policy. All four send methods share the operation name
39 /// `send_email`, because a caller picks one by what it needs to include and
40 /// a transport outage takes out all of them together.
41 pub(crate) fn faults(&self) -> &Faults {
42 &self.faults
43 }
44
45 /// Return all emails sent so far.
46 pub(crate) fn sent(&self) -> Vec<SentEmail> {
47 self.sent.lock().unwrap().clone()
48 }
49
50 /// Return emails sent to a specific address.
51 pub(crate) fn sent_to(&self, address: &str) -> Vec<SentEmail> {
52 self.sent
53 .lock()
54 .unwrap()
55 .iter()
56 .filter(|e| e.to == address)
57 .cloned()
58 .collect()
59 }
60
61 /// Clear the sent email log.
62 pub(crate) fn clear(&self) {
63 self.sent.lock().unwrap().clear();
64 }
65
66 /// Return the number of emails sent.
67 pub(crate) fn count(&self) -> usize {
68 self.sent.lock().unwrap().len()
69 }
70 }
71
72 #[async_trait::async_trait]
73 impl EmailTransport for MockEmailTransport {
74 async fn send_email(&self, to: &str, subject: &str, body: &str) -> Result<()> {
75 self.faults.check("send_email")?;
76 self.sent.lock().unwrap().push(SentEmail {
77 to: to.to_string(),
78 subject: subject.to_string(),
79 body: body.to_string(),
80 unsub_url: None,
81 stream: None,
82 });
83 Ok(())
84 }
85
86 async fn send_email_with_headers_and_unsub(
87 &self,
88 to: &str,
89 subject: &str,
90 body: &str,
91 _extra_headers: &[(&str, String)],
92 unsub_url: Option<&str>,
93 ) -> Result<()> {
94 self.faults.check("send_email")?;
95 self.sent.lock().unwrap().push(SentEmail {
96 to: to.to_string(),
97 subject: subject.to_string(),
98 body: body.to_string(),
99 unsub_url: unsub_url.map(std::string::ToString::to_string),
100 stream: None,
101 });
102 Ok(())
103 }
104
105 async fn send_email_broadcast_with_unsub(
106 &self,
107 to: &str,
108 subject: &str,
109 body: &str,
110 unsub_url: Option<&str>,
111 ) -> Result<()> {
112 self.faults.check("send_email")?;
113 self.sent.lock().unwrap().push(SentEmail {
114 to: to.to_string(),
115 subject: subject.to_string(),
116 body: body.to_string(),
117 unsub_url: unsub_url.map(std::string::ToString::to_string),
118 stream: Some("broadcast".to_string()),
119 });
120 Ok(())
121 }
122 }
123