use crate::domain::SubscriberEmail; use reqwest::Client; pub struct EmailClient { http_client: Client, base_url: String, sender: SubscriberEmail, authorization_token: String, } impl EmailClient { pub fn new(base_url: String, sender: SubscriberEmail, authorization_token: String) -> Self { Self { http_client: Client::new(), base_url, sender, authorization_token, } } pub async fn send_email( &self, recipient: SubscriberEmail, subject: &str, html_content: &str, text_content: &str, ) -> Result<(), reqwest::Error> { let url = format!("{}/email", self.base_url); let request_body = SendEmailRequest { from: self.sender.as_ref(), to: recipient.as_ref(), subject, html_body: html_content, text_body: text_content, }; self.http_client .post(&url) .header("X-Postmark-Server-Token", &self.authorization_token) .json(&request_body) .send() .await? .error_for_status()?; Ok(()) } } #[derive(serde::Serialize)] #[serde(rename_all = "PascalCase")] struct SendEmailRequest<'a> { from: &'a str, to: &'a str, subject: &'a str, html_body: &'a str, text_body: &'a str, } #[cfg(test)] mod tests { use crate::domain::SubscriberEmail; use crate::email_client::EmailClient; use claim::{assert_err, assert_ok}; use fake::faker::internet::en::SafeEmail; use fake::faker::lorem::en::{Paragraph, Sentence}; use fake::{Fake, Faker}; use wiremock::matchers::{any, header, header_exists, method, path}; use wiremock::{Mock, MockServer, Request, ResponseTemplate}; struct SendEmailBodyMatcher; impl wiremock::Match for SendEmailBodyMatcher { fn matches(&self, request: &Request) -> bool { let result: Result = serde_json::from_slice(&request.body); if let Ok(body) = result { body.get("From").is_some() && body.get("To").is_some() && body.get("Subject").is_some() && body.get("HtmlBody").is_some() && body.get("TextBody").is_some() } else { false } } } #[actix_rt::test] async fn send_email_sends_the_expected_request() { // Arrange let mock_server = MockServer::start().await; let sender = SubscriberEmail::parse(SafeEmail().fake()).unwrap(); let email_client = EmailClient::new(mock_server.uri(), sender, Faker.fake()); let subscriber_email = SubscriberEmail::parse(SafeEmail().fake()).unwrap(); let subject: String = Sentence(1..2).fake(); let content: String = Paragraph(1..10).fake(); Mock::given(header_exists("X-Postmark-Server-Token")) .and(header("Content-Type", "application/json")) .and(path("/email")) .and(method("POST")) .and(SendEmailBodyMatcher) .respond_with(ResponseTemplate::new(200)) .expect(1) .mount(&mock_server) .await; // Act let _ = email_client .send_email(subscriber_email, &subject, &content, &content) .await; // Assert } #[actix_rt::test] async fn send_email_succeeds_if_the_server_returns_200() { // Arrange let mock_server = MockServer::start().await; let sender = SubscriberEmail::parse(SafeEmail().fake()).unwrap(); let email_client = EmailClient::new(mock_server.uri(), sender, Faker.fake()); let subscriber_email = SubscriberEmail::parse(SafeEmail().fake()).unwrap(); let subject: String = Sentence(1..2).fake(); let content: String = Paragraph(1..10).fake(); Mock::given(any()) .respond_with(ResponseTemplate::new(200)) .expect(1) .mount(&mock_server) .await; // Act let outcome = email_client .send_email(subscriber_email, &subject, &content, &content) .await; // Assert assert_ok!(outcome); } #[actix_rt::test] async fn send_email_fails_if_the_server_returns_500() { // Arrange let mock_server = MockServer::start().await; let sender = SubscriberEmail::parse(SafeEmail().fake()).unwrap(); let email_client = EmailClient::new(mock_server.uri(), sender, Faker.fake()); let subscriber_email = SubscriberEmail::parse(SafeEmail().fake()).unwrap(); let subject: String = Sentence(1..2).fake(); let content: String = Paragraph(1..10).fake(); Mock::given(any()) // Not a 200 anymore! .respond_with(ResponseTemplate::new(500)) .expect(1) .mount(&mock_server) .await; // Act let outcome = email_client .send_email(subscriber_email, &subject, &content, &content) .await; // Assert assert_err!(outcome); } }