use crate::{configuration::EmailClientSettings, domain::SubscriberEmail}; use reqwest::Client; use secrecy::{ExposeSecret, SecretString}; use std::time::Duration; pub struct EmailClient { http_client: Client, base_url: reqwest::Url, sender: SubscriberEmail, authorization_token: SecretString, } impl EmailClient { pub fn build(config: EmailClientSettings) -> Result { let client = Self { http_client: Client::builder() .timeout(Duration::from_millis(config.timeout_milliseconds)) .build() .unwrap(), base_url: reqwest::Url::parse(&config.base_url)?, sender: config.sender().map_err(|e| anyhow::anyhow!(e))?, authorization_token: config.authorization_token, }; Ok(client) } pub async fn send_email( &self, recipient: &SubscriberEmail, subject: &str, html_content: &str, text_content: &str, ) -> Result<(), reqwest::Error> { let url = self.base_url.join("email").unwrap(); let request_body = SendEmailRequest { from: EmailField { email: self.sender.as_ref(), }, to: vec![EmailField { email: recipient.as_ref(), }], subject, text: text_content, html: html_content, }; self.http_client .post(url) .header("X-Requested-With", "XMLHttpRequest") .header( "Authorization", format!("Bearer {}", self.authorization_token.expose_secret()), ) .json(&request_body) .send() .await? .error_for_status()?; Ok(()) } } #[derive(serde::Serialize)] struct SendEmailRequest<'a> { from: EmailField<'a>, to: Vec>, subject: &'a str, text: &'a str, html: &'a str, } #[derive(serde::Serialize)] struct EmailField<'a> { email: &'a str, } #[cfg(test)] mod tests { use std::time::Duration; use crate::{ configuration::EmailClientSettings, domain::SubscriberEmail, email_client::EmailClient, }; use claims::{assert_err, assert_ok}; use fake::{ Fake, Faker, faker::{ internet::en::SafeEmail, lorem::en::{Paragraph, Sentence}, }, }; use wiremock::{ Mock, MockServer, ResponseTemplate, matchers::{any, header, header_exists, method, path}, }; struct SendEmailBodyMatcher; impl wiremock::Match for SendEmailBodyMatcher { fn matches(&self, request: &wiremock::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("html").is_some() && body.get("text").is_some() } else { false } } } fn subject() -> String { Sentence(1..2).fake() } fn content() -> String { Paragraph(1..10).fake() } fn email() -> SubscriberEmail { SubscriberEmail::parse(SafeEmail().fake()).unwrap() } fn email_client(base_url: String) -> EmailClient { let sender_email = SafeEmail().fake(); let token: String = Faker.fake(); let settings = EmailClientSettings::new(base_url, sender_email, token, 200); EmailClient::build(settings).unwrap() } #[tokio::test] async fn send_email_sends_the_expected_request() { let mock_server = MockServer::start().await; let email_client = email_client(mock_server.uri()); Mock::given(header_exists("Authorization")) .and(header("Content-Type", "application/json")) .and(header("X-Requested-With", "XMLHttpRequest")) .and(path("email")) .and(method("POST")) .and(SendEmailBodyMatcher) .respond_with(ResponseTemplate::new(200)) .expect(1) .mount(&mock_server) .await; email_client .send_email(&email(), &subject(), &content(), &content()) .await .unwrap(); } #[tokio::test] async fn send_email_succeeds_if_the_server_returns_200() { let mock_server = MockServer::start().await; let email_client = email_client(mock_server.uri()); Mock::given(any()) .respond_with(ResponseTemplate::new(200)) .expect(1) .mount(&mock_server) .await; let response = email_client .send_email(&email(), &subject(), &content(), &content()) .await; assert_ok!(response); } #[tokio::test] async fn send_email_fails_if_the_server_retuns_500() { let mock_server = MockServer::start().await; let email_client = email_client(mock_server.uri()); Mock::given(any()) .respond_with(ResponseTemplate::new(500)) .expect(1) .mount(&mock_server) .await; let response = email_client .send_email(&email(), &subject(), &content(), &content()) .await; assert_err!(response); } #[tokio::test] async fn send_email_times_out_if_the_server_takes_too_long() { let mock_server = MockServer::start().await; let email_client = email_client(mock_server.uri()); Mock::given(any()) .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(180))) .expect(1) .mount(&mock_server) .await; let response = email_client .send_email(&email(), &subject(), &content(), &content()) .await; assert_err!(response); } }