zero-to-production/src/email_client.rs

210 lines
5.7 KiB
Rust
Raw Normal View History

2021-01-10 17:27:54 +00:00
use crate::domain::SubscriberEmail;
2021-01-11 09:37:04 +00:00
use reqwest::Client;
2021-12-27 12:24:24 +00:00
use secrecy::{ExposeSecret, Secret};
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,
2021-12-27 12:24:24 +00:00
authorization_token: Secret<String>,
2021-01-11 09:37:04 +00:00
}
2021-01-10 17:27:54 +00:00
impl EmailClient {
pub fn new(
base_url: String,
sender: SubscriberEmail,
2021-12-27 12:24:24 +00:00
authorization_token: Secret<String>,
timeout: std::time::Duration,
) -> Self {
let http_client = Client::builder().timeout(timeout).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,
2021-08-01 13:57:02 +00:00
recipient: &SubscriberEmail,
2021-01-10 17:27:54 +00:00
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)
2021-12-27 12:24:24 +00:00
.header(
"X-Postmark-Server-Token",
self.authorization_token.expose_secret(),
)
2021-01-16 23:01:08 +00:00
.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;
use claims::{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-12-27 12:24:24 +00:00
use secrecy::Secret;
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 {
EmailClient::new(
base_url,
email(),
2021-12-27 12:24:24 +00:00
Secret::new(Faker.fake()),
std::time::Duration::from_millis(200),
)
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-08-01 13:57:02 +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-08-01 13:57:02 +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-08-01 13:57:02 +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-08-01 13:57:02 +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
}