forge_core/testing/
mock_email.rs1use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6
7use tokio::sync::Mutex;
8
9use crate::email::{Email, EmailSender};
10use crate::error::Result;
11
12#[derive(Debug, Clone, Default)]
14pub struct MockEmailSender {
15 sent: Arc<Mutex<Vec<SentEmail>>>,
16}
17
18#[derive(Debug, Clone)]
20pub struct SentEmail {
21 pub to: Vec<String>,
22 pub subject: String,
23 pub text: Option<String>,
24 pub html: Option<String>,
25}
26
27impl MockEmailSender {
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 pub async fn sent(&self) -> Vec<SentEmail> {
33 self.sent.lock().await.clone()
34 }
35
36 pub async fn assert_sent_to(&self, address: &str) {
38 let sent = self.sent.lock().await;
39 let matching: Vec<_> = sent
40 .iter()
41 .filter(|e| e.to.contains(&address.to_string()))
42 .collect();
43 assert!(
44 matching.len() == 1,
45 "Expected 1 email to {address}, found {}",
46 matching.len()
47 );
48 }
49
50 pub async fn assert_none_sent(&self) {
52 let sent = self.sent.lock().await;
53 assert!(sent.is_empty(), "Expected no emails, found {}", sent.len());
54 }
55}
56
57impl EmailSender for MockEmailSender {
58 fn send<'a>(
59 &'a self,
60 email: &'a Email,
61 ) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>> {
62 Box::pin(async move {
63 self.sent.lock().await.push(SentEmail {
64 to: email.to.clone(),
65 subject: email.subject.clone(),
66 text: email.text.clone(),
67 html: email.html.clone(),
68 });
69 Ok(format!("mock-{}", uuid::Uuid::new_v4()))
70 })
71 }
72}