2021-01-10 17:27:54 +00:00
|
|
|
use crate::domain::SubscriberEmail;
|
2021-01-11 09:37:04 +00:00
|
|
|
use reqwest::Client;
|
2021-01-10 17:27:54 +00:00
|
|
|
|
2021-01-11 09:37:04 +00:00
|
|
|
pub struct EmailClient {
|
|
|
|
http_client: Client,
|
2021-01-16 15:11:17 +00:00
|
|
|
base_url: String,
|
2021-01-16 23:01:08 +00:00
|
|
|
sender: SubscriberEmail,
|
|
|
|
authorization_token: String,
|
2021-01-11 09:37:04 +00:00
|
|
|
}
|
2021-01-10 17:27:54 +00:00
|
|
|
|
|
|
|
impl EmailClient {
|
2021-01-16 23:01:08 +00:00
|
|
|
pub fn new(base_url: String, sender: SubscriberEmail, authorization_token: String) -> Self {
|
2021-01-17 00:26:30 +00:00
|
|
|
let http_client = Client::builder()
|
|
|
|
.timeout(std::time::Duration::from_secs(10))
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
2021-01-11 09:37:04 +00:00
|
|
|
Self {
|
2021-01-17 00:26:30 +00:00
|
|
|
http_client,
|
2021-01-16 15:11:17 +00:00
|
|
|
base_url,
|
2021-01-16 23:01:08 +00:00
|
|
|
sender,
|
|
|
|
authorization_token,
|
2021-01-11 09:37:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-10 17:27:54 +00:00
|
|
|
pub async fn send_email(
|
|
|
|
&self,
|
|
|
|
recipient: SubscriberEmail,
|
|
|
|
subject: &str,
|
2021-01-16 15:59:41 +00:00
|
|
|
html_content: &str,
|
|
|
|
text_content: &str,
|
2021-01-16 23:01:08 +00:00
|
|
|
) -> Result<(), reqwest::Error> {
|
|
|
|
let url = format!("{}/email", self.base_url);
|
|
|
|
let request_body = SendEmailRequest {
|
2021-01-16 23:54:21 +00:00
|
|
|
from: self.sender.as_ref(),
|
|
|
|
to: recipient.as_ref(),
|
|
|
|
subject,
|
|
|
|
html_body: html_content,
|
|
|
|
text_body: text_content,
|
2021-01-16 23:01:08 +00:00
|
|
|
};
|
|
|
|
self.http_client
|
|
|
|
.post(&url)
|
|
|
|
.header("X-Postmark-Server-Token", &self.authorization_token)
|
|
|
|
.json(&request_body)
|
|
|
|
.send()
|
2021-01-16 23:57:57 +00:00
|
|
|
.await?
|
|
|
|
.error_for_status()?;
|
2021-01-16 23:01:08 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(serde::Serialize)]
|
|
|
|
#[serde(rename_all = "PascalCase")]
|
2021-01-16 23:54:21 +00:00
|
|
|
struct SendEmailRequest<'a> {
|
|
|
|
from: &'a str,
|
|
|
|
to: &'a str,
|
|
|
|
subject: &'a str,
|
|
|
|
html_body: &'a str,
|
|
|
|
text_body: &'a str,
|
2021-01-16 23:01:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use crate::domain::SubscriberEmail;
|
|
|
|
use crate::email_client::EmailClient;
|
2021-01-16 23:54:21 +00:00
|
|
|
use claim::{assert_err, assert_ok};
|
2021-01-16 23:01:08 +00:00
|
|
|
use fake::faker::internet::en::SafeEmail;
|
|
|
|
use fake::faker::lorem::en::{Paragraph, Sentence};
|
|
|
|
use fake::{Fake, Faker};
|
2021-01-16 23:54:21 +00:00
|
|
|
use wiremock::matchers::{any, header, header_exists, method, path};
|
2021-01-16 23:01:08 +00:00
|
|
|
use wiremock::{Mock, MockServer, Request, ResponseTemplate};
|
|
|
|
|
|
|
|
struct SendEmailBodyMatcher;
|
|
|
|
|
|
|
|
impl wiremock::Match for SendEmailBodyMatcher {
|
|
|
|
fn matches(&self, request: &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("HtmlBody").is_some()
|
|
|
|
&& body.get("TextBody").is_some()
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-17 00:43:04 +00:00
|
|
|
/// Generate a random email subject
|
|
|
|
fn subject() -> String {
|
|
|
|
Sentence(1..2).fake()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Generate a random email content
|
|
|
|
fn content() -> String {
|
|
|
|
Paragraph(1..10).fake()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Generate a random subscriber email
|
|
|
|
fn email() -> SubscriberEmail {
|
|
|
|
SubscriberEmail::parse(SafeEmail().fake()).unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get a test instance of `EmailClient`.
|
|
|
|
fn email_client(base_url: String) -> EmailClient {
|
2021-01-17 11:46:40 +00:00
|
|
|
EmailClient::new(base_url, email(), Faker.fake())
|
2021-01-17 00:43:04 +00:00
|
|
|
}
|
|
|
|
|
2021-01-17 00:27:03 +00:00
|
|
|
#[tokio::test]
|
2021-01-16 23:54:21 +00:00
|
|
|
async fn send_email_sends_the_expected_request() {
|
2021-01-16 23:01:08 +00:00
|
|
|
// Arrange
|
|
|
|
let mock_server = MockServer::start().await;
|
2021-01-17 00:43:04 +00:00
|
|
|
let email_client = email_client(mock_server.uri());
|
2021-01-16 23:01:08 +00:00
|
|
|
|
|
|
|
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
|
2021-01-17 11:46:40 +00:00
|
|
|
.send_email(email(), &subject(), &content(), &content())
|
2021-01-16 23:01:08 +00:00
|
|
|
.await;
|
|
|
|
|
|
|
|
// Assert
|
2021-01-10 17:27:54 +00:00
|
|
|
}
|
2021-01-16 23:54:21 +00:00
|
|
|
|
2021-01-17 00:27:03 +00:00
|
|
|
#[tokio::test]
|
2021-01-16 23:54:21 +00:00
|
|
|
async fn send_email_succeeds_if_the_server_returns_200() {
|
|
|
|
// Arrange
|
|
|
|
let mock_server = MockServer::start().await;
|
2021-01-17 00:43:04 +00:00
|
|
|
let email_client = email_client(mock_server.uri());
|
2021-01-16 23:54:21 +00:00
|
|
|
|
|
|
|
Mock::given(any())
|
|
|
|
.respond_with(ResponseTemplate::new(200))
|
|
|
|
.expect(1)
|
|
|
|
.mount(&mock_server)
|
|
|
|
.await;
|
|
|
|
|
|
|
|
// Act
|
|
|
|
let outcome = email_client
|
2021-01-17 11:46:40 +00:00
|
|
|
.send_email(email(), &subject(), &content(), &content())
|
2021-01-16 23:54:21 +00:00
|
|
|
.await;
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
assert_ok!(outcome);
|
|
|
|
}
|
|
|
|
|
2021-01-17 00:27:03 +00:00
|
|
|
#[tokio::test]
|
2021-01-16 23:54:21 +00:00
|
|
|
async fn send_email_fails_if_the_server_returns_500() {
|
|
|
|
// Arrange
|
|
|
|
let mock_server = MockServer::start().await;
|
2021-01-17 00:43:04 +00:00
|
|
|
let email_client = email_client(mock_server.uri());
|
2021-01-16 23:54:21 +00:00
|
|
|
|
|
|
|
Mock::given(any())
|
|
|
|
// Not a 200 anymore!
|
|
|
|
.respond_with(ResponseTemplate::new(500))
|
|
|
|
.expect(1)
|
|
|
|
.mount(&mock_server)
|
|
|
|
.await;
|
|
|
|
|
|
|
|
// Act
|
|
|
|
let outcome = email_client
|
2021-01-17 11:46:40 +00:00
|
|
|
.send_email(email(), &subject(), &content(), &content())
|
2021-01-16 23:54:21 +00:00
|
|
|
.await;
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
assert_err!(outcome);
|
|
|
|
}
|
2021-01-17 00:26:30 +00:00
|
|
|
|
2021-02-11 09:23:13 +00:00
|
|
|
#[tokio::test]
|
2021-01-17 00:26:30 +00:00
|
|
|
async fn send_email_times_out_if_the_server_takes_too_long() {
|
|
|
|
// Arrange
|
|
|
|
let mock_server = MockServer::start().await;
|
2021-01-17 00:43:04 +00:00
|
|
|
let email_client = email_client(mock_server.uri());
|
2021-01-17 00:26:30 +00:00
|
|
|
|
|
|
|
let response = ResponseTemplate::new(200).set_delay(std::time::Duration::from_secs(180));
|
|
|
|
Mock::given(any())
|
|
|
|
.respond_with(response)
|
|
|
|
.expect(1)
|
|
|
|
.mount(&mock_server)
|
|
|
|
.await;
|
|
|
|
|
|
|
|
// Act
|
|
|
|
let outcome = email_client
|
2021-01-17 11:46:40 +00:00
|
|
|
.send_email(email(), &subject(), &content(), &content())
|
2021-01-17 00:26:30 +00:00
|
|
|
.await;
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
assert_err!(outcome);
|
|
|
|
}
|
2021-01-10 17:27:54 +00:00
|
|
|
}
|