198 lines
5.7 KiB
Rust
198 lines
5.7 KiB
Rust
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<Self, anyhow::Error> {
|
|
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: self.sender.as_ref(),
|
|
to: 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: &'a str,
|
|
to: &'a str,
|
|
subject: &'a str,
|
|
text: &'a str,
|
|
html: &'a str,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
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 std::time::Duration;
|
|
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::Value, _> = 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);
|
|
}
|
|
}
|