Skip to main content

forge_core/testing/
mock_email.rs

1//! Mock email sender for testing.
2
3use 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/// Records sent emails for assertion in tests.
13#[derive(Debug, Clone, Default)]
14pub struct MockEmailSender {
15    sent: Arc<Mutex<Vec<SentEmail>>>,
16}
17
18/// A recorded email send.
19#[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    /// Assert that exactly one email was sent to the given address.
37    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    /// Assert that no emails were sent.
51    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}