mirror of
https://github.com/kristoferssolo/zero2prod.git
synced 2025-10-21 20:10:40 +00:00
Finished chapter 7.2
This commit is contained in:
parent
2ebb54ef0f
commit
d8fb203a2f
859
Cargo.lock
generated
859
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user