Finished chapter 7.2

This commit is contained in:
Kristofers Solo 2024-08-24 17:28:38 +03:00
parent 2ebb54ef0f
commit d8fb203a2f
7 changed files with 608 additions and 372 deletions

859
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@ axum = "0.7"
chrono = { version = "0.4", features = ["serde", "clock"] } chrono = { version = "0.4", features = ["serde", "clock"] }
config = { version = "0.14", features = ["toml"], default-features = false } config = { version = "0.14", features = ["toml"], default-features = false }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.7", default-features = false, features = [ sqlx = { version = "0.8", default-features = false, features = [
"runtime-tokio", "runtime-tokio",
"tls-rustls", "tls-rustls",
"macros", "macros",

View File

@ -12,3 +12,4 @@ database_name = "newsletter"
base_url = "localhost" base_url = "localhost"
sender_email = "test@gmail.com" sender_email = "test@gmail.com"
auth_token = "super-secret-token" auth_token = "super-secret-token"
timeout_milliseconds = 10000

View File

@ -1,4 +1,4 @@
use std::fmt::Display; use std::{fmt::Display, time::Duration};
use secrecy::{ExposeSecret, Secret}; use secrecy::{ExposeSecret, Secret};
use serde::Deserialize; use serde::Deserialize;
@ -45,12 +45,17 @@ pub struct EmailClientSettings {
pub base_url: String, pub base_url: String,
pub sender_email: String, pub sender_email: String,
pub auth_token: Secret<String>, pub auth_token: Secret<String>,
pub timeout_milliseconds: u64,
} }
impl EmailClientSettings { impl EmailClientSettings {
pub fn sender(&self) -> Result<SubscriberEmail, String> { pub fn sender(&self) -> Result<SubscriberEmail, String> {
self.sender_email.clone().try_into() self.sender_email.clone().try_into()
} }
pub fn timeout(&self) -> Duration {
Duration::from_millis(self.timeout_milliseconds)
}
} }
pub fn get_config() -> Result<Settings, config::ConfigError> { pub fn get_config() -> Result<Settings, config::ConfigError> {

View File

@ -1,3 +1,5 @@
use std::time::Duration;
use reqwest::{Client, Url}; use reqwest::{Client, Url};
use secrecy::{ExposeSecret, Secret}; use secrecy::{ExposeSecret, Secret};
use serde::Serialize; use serde::Serialize;
@ -13,14 +15,21 @@ pub struct EmailClient {
} }
impl EmailClient { impl EmailClient {
pub fn new(base_url: String, sender: SubscriberEmail, auth_token: Secret<String>) -> Self { pub fn new(
base_url: String,
sender: SubscriberEmail,
auth_token: Secret<String>,
timeout: Duration,
) -> Self {
let http_client = Client::builder().timeout(timeout).build().unwrap();
Self { Self {
http_client: Client::new(), http_client,
base_url, base_url,
sender, sender,
auth_token, auth_token,
} }
} }
pub async fn send_email( pub async fn send_email(
&self, &self,
recipient: SubscriberEmail, recipient: SubscriberEmail,
@ -38,13 +47,13 @@ impl EmailClient {
html_body: html_content, html_body: html_content,
text_body: text_content, text_body: text_content,
}; };
let builder = self self.http_client
.http_client
.post(url) .post(url)
.header("X-Postmark-Server-Token", self.auth_token.expose_secret()) .header("X-Postmark-Server-Token", self.auth_token.expose_secret())
.json(&request_body) .json(&request_body)
.send() .send()
.await?; .await?
.error_for_status()?;
Ok(()) Ok(())
} }
} }
@ -62,6 +71,9 @@ struct SendEmailRequest<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{str::FromStr, time::Duration};
use claims::{assert_err, assert_ok};
use fake::{ use fake::{
faker::{ faker::{
internet::en::SafeEmail, internet::en::SafeEmail,
@ -71,7 +83,7 @@ mod tests {
}; };
use serde_json; use serde_json;
use wiremock::{ use wiremock::{
matchers::{header, header_exists, method, path}, matchers::{any, header, header_exists, method, path},
Match, Mock, MockServer, ResponseTemplate, Match, Mock, MockServer, ResponseTemplate,
}; };
@ -93,12 +105,32 @@ mod tests {
} }
} }
fn subject() -> String {
Sentence(1..2).fake()
}
fn content() -> String {
Paragraph(1..10).fake()
}
fn email() -> SubscriberEmail {
SubscriberEmail::from_str(&SafeEmail().fake::<String>()).unwrap()
}
fn email_client(base_url: String) -> EmailClient {
EmailClient::new(
base_url,
email(),
Secret::new(Faker.fake()),
Duration::from_millis(200),
)
}
use super::*; use super::*;
#[tokio::test] #[tokio::test]
async fn send_email_sends_the_expectred_request() { async fn send_email_sends_the_expectred_request() {
let mock_server = MockServer::start().await; let mock_server = MockServer::start().await;
let sender = SafeEmail().fake::<String>().try_into().unwrap(); let email_client = email_client(mock_server.uri());
let email_client = EmailClient::new(mock_server.uri(), sender, Secret::new(Faker.fake()));
Mock::given(header_exists("X-Postmark-Server-Token")) Mock::given(header_exists("X-Postmark-Server-Token"))
.and(header("Content-Type", "application/json")) .and(header("Content-Type", "application/json"))
@ -110,12 +142,61 @@ mod tests {
.mount(&mock_server) .mount(&mock_server)
.await; .await;
let subscriber_email = SafeEmail().fake::<String>().try_into().unwrap();
let subject = Sentence(1..2).fake::<String>();
dbg!(&subject);
let content = Paragraph(1..10).fake::<String>();
let _ = email_client let _ = email_client
.send_email(subscriber_email, &subject, &content, &content) .send_email(email(), &subject(), &content(), &content())
.await; .await;
} }
#[tokio::test]
async fn send_email_succeds_if_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 outcome = email_client
.send_email(email(), &subject(), &content(), &content())
.await;
assert_ok!(outcome);
}
#[tokio::test]
async fn send_email_fails_if_server_returns_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 outcome = email_client
.send_email(email(), &subject(), &content(), &content())
.await;
assert_err!(outcome);
}
#[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());
let response = ResponseTemplate::new(200).set_delay(Duration::from_secs(180));
Mock::given(any())
.respond_with(response)
.expect(1)
.mount(&mock_server)
.await;
let outcome = email_client
.send_email(email(), &subject(), &content(), &content())
.await;
assert_err!(outcome);
}
} }

View File

@ -22,10 +22,12 @@ async fn main() -> Result<(), std::io::Error> {
.email_client .email_client
.sender() .sender()
.expect("Invalid sender email adress"); .expect("Invalid sender email adress");
let timeout = config.email_client.timeout();
let email_client = EmailClient::new( let email_client = EmailClient::new(
config.email_client.base_url, config.email_client.base_url,
sender_email, sender_email,
config.email_client.auth_token, config.email_client.auth_token,
timeout,
); );
axum::serve(listener, route(pool, email_client)).await axum::serve(listener, route(pool, email_client)).await

View File

@ -146,10 +146,12 @@ async fn spawn_app() -> TestApp {
.email_client .email_client
.sender() .sender()
.expect("Invalid sender email adress"); .expect("Invalid sender email adress");
let timeout = config.email_client.timeout();
let email_client = EmailClient::new( let email_client = EmailClient::new(
config.email_client.base_url, config.email_client.base_url,
sender_email, sender_email,
config.email_client.auth_token, config.email_client.auth_token,
timeout,
); );
let _ = tokio::spawn(async move { let _ = tokio::spawn(async move {