1use crate::error::AuthResult;
2use async_trait::async_trait;
3use std::io::Write;
4
5#[async_trait]
8pub trait EmailProvider: Send + Sync {
9 async fn send(&self, to: &str, subject: &str, html: &str, text: &str) -> AuthResult<()>;
16}
17
18#[derive(Debug)]
22pub struct ConsoleEmailProvider;
23
24#[async_trait]
25impl EmailProvider for ConsoleEmailProvider {
26 async fn send(&self, to: &str, subject: &str, _html: &str, text: &str) -> AuthResult<()> {
27 _ = writeln!(
28 std::io::stderr().lock(),
29 "[EMAIL] To: {to} | Subject: {subject} | Body: {text}"
30 );
31 Ok(())
32 }
33}
34
35#[cfg(test)]
37mod tests {
38 use super::*;
39 use std::sync::{Arc, Mutex};
40
41 type SentMessages = Arc<Mutex<Vec<(String, String, String, String)>>>;
42
43 struct MockEmailProvider {
45 sent: SentMessages,
46 }
47
48 impl MockEmailProvider {
49 fn new() -> (Self, SentMessages) {
50 let sent = Arc::new(Mutex::new(Vec::new()));
51 (
52 Self {
53 sent: Arc::clone(&sent),
54 },
55 sent,
56 )
57 }
58 }
59
60 #[async_trait]
61 impl EmailProvider for MockEmailProvider {
62 async fn send(&self, to: &str, subject: &str, html: &str, text: &str) -> AuthResult<()> {
63 self.sent.lock().unwrap().push((
64 to.to_owned(),
65 subject.to_owned(),
66 html.to_owned(),
67 text.to_owned(),
68 ));
69 Ok(())
70 }
71 }
72
73 #[tokio::test]
75 async fn test_console_email_provider_send() {
76 let provider = ConsoleEmailProvider;
77 let result = provider
78 .send("user@example.com", "Test Subject", "<h1>Hi</h1>", "Hi")
79 .await;
80 assert!(result.is_ok());
81 }
82
83 #[tokio::test]
85 async fn test_mock_email_provider_records_sends() {
86 let (provider, sent) = MockEmailProvider::new();
87 provider
88 .send("a@b.com", "Sub", "<p>html</p>", "text")
89 .await
90 .unwrap();
91 provider.send("c@d.com", "Sub2", "", "text2").await.unwrap();
92
93 let messages = sent.lock().unwrap();
94 assert_eq!(messages.len(), 2);
95 assert_eq!(
96 (messages)
97 .first()
98 .expect("fixture contains the requested index")
99 .0,
100 "a@b.com"
101 );
102 assert_eq!(
103 (messages)
104 .get(1)
105 .expect("fixture contains the requested index")
106 .0,
107 "c@d.com"
108 );
109 }
110
111 #[tokio::test]
113 async fn test_trait_object_works() {
114 let provider: Box<dyn EmailProvider> = Box::new(ConsoleEmailProvider);
115 let result = provider.send("user@example.com", "Test", "", "body").await;
116 assert!(result.is_ok());
117 }
118
119 #[tokio::test]
121 async fn test_missing_provider_returns_error() {
122 use crate::plugin::AuthContext;
123 use crate::test_store::{test_config, test_database};
124
125 let config = test_config();
126 let database = test_database().await;
127 let ctx = AuthContext::new(config, database);
128
129 let result = ctx.email_provider();
130 assert!(result.is_err());
131 }
132}
133