Skip to content

Commit

Permalink
finish Chapter 8
Browse files Browse the repository at this point in the history
  • Loading branch information
josemoura212 committed Jun 22, 2024
1 parent 83a853e commit ba617b4
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 114 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ name = "zero2prod"

[dependencies]
actix-web = "4.5.1"
anyhow = "1.0.86"
chrono = "0.4.38"
config = "0.14.0"
rand = { version = "0.8.5", features = ["std_rng"] }
reqwest = { version = "0.12.4", default-features = false, features = ["json", "rustls-tls"] }
secrecy = { version = "0.8.0", features = ["serde"] }
serde = { version = "1.0.203", features = ["derive"] }
serde-aux = "4.5.0"
thiserror = "1.0.61"
tokio = {version = "1.37.0",features = ["macros", "rt-multi-thread"]}
tracing = { version = "0.1.40", features = ["log"] }
tracing-actix-web = "0.7.11"
Expand Down
181 changes: 96 additions & 85 deletions src/routes/subscriptions.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::domain::{NewSubscriber, SubscriberEmail, SubscriberName};
use crate::email_client::EmailClient;
use crate::startup::ApplicationBaseUrl;
use actix_web::ResponseError;
use actix_web::{web, HttpResponse};
use actix_web::http::StatusCode;
use actix_web::{web, HttpResponse, ResponseError};
use anyhow::Context;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use sqlx::{Executor, PgPool, Postgres, Transaction};
Expand All @@ -23,13 +24,27 @@ impl TryFrom<FormData> for NewSubscriber {
Ok(NewSubscriber { email, name })
}
}
#[derive(thiserror::Error)]
pub enum SubscribeError {
#[error("{0}")]
ValidationError(String),
#[error(transparent)]
UnexpectedError(#[from] anyhow::Error),
}

fn generate_subscription_token() -> String {
let mut rng = thread_rng();
std::iter::repeat_with(|| rng.sample(Alphanumeric))
.map(char::from)
.take(25)
.collect()
impl std::fmt::Debug for SubscribeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}

impl ResponseError for SubscribeError {
fn status_code(&self) -> StatusCode {
match self {
SubscribeError::ValidationError(_) => StatusCode::BAD_REQUEST,
SubscribeError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

#[tracing::instrument(
Expand All @@ -46,43 +61,47 @@ pub async fn subscribe(
pool: web::Data<PgPool>,
email_client: web::Data<EmailClient>,
base_url: web::Data<ApplicationBaseUrl>,
) -> Result<HttpResponse, actix_web::Error> {
let new_subscriber = match form.0.try_into() {
Ok(form) => form,
Err(_) => return Ok(HttpResponse::BadRequest().finish()),
};

let mut transaction = match pool.begin().await {
Ok(transaction) => transaction,
Err(_) => return Ok(HttpResponse::InternalServerError().finish()),
};

let subscriber_id = match insert_subscriber(&mut transaction, &new_subscriber).await {
Ok(subscriber_id) => subscriber_id,
Err(_) => return Ok(HttpResponse::InternalServerError().finish()),
};
) -> Result<HttpResponse, SubscribeError> {
let new_subscriber = form.0.try_into().map_err(SubscribeError::ValidationError)?;

let mut transaction = pool
.begin()
.await
.context("Failed to acquire a Postgres connection from the pool")?;
let subscriber_id = insert_subscriber(&mut transaction, &new_subscriber)
.await
.context("Failed to insert new subscriber in the database.")?;

let subscription_token = generate_subscription_token();
store_token(&mut transaction, subscriber_id, &subscription_token).await?;
store_token(&mut transaction, subscriber_id, &subscription_token)
.await
.context("Failed to store the confirmation token for a new subscriber.")?;

if transaction.commit().await.is_err() {
return Ok(HttpResponse::InternalServerError().finish());
}
transaction
.commit()
.await
.context("Failed to commit SQL transaction to store a new subscriber.")?;

if send_confirmation_email(
send_confirmation_email(
&email_client,
new_subscriber,
&base_url.0,
&subscription_token,
)
.await
.is_err()
{
return Ok(HttpResponse::InternalServerError().finish());
}
.context("Failed to send a confirmation email.")?;

Ok(HttpResponse::Ok().finish())
}

fn generate_subscription_token() -> String {
let mut rng = thread_rng();
std::iter::repeat_with(|| rng.sample(Alphanumeric))
.map(char::from)
.take(25)
.collect()
}

#[tracing::instrument(
name = "Saving new subscriber details in the database",
skip(new_subscriber, transaction)
Expand All @@ -102,13 +121,39 @@ pub async fn insert_subscriber(
new_subscriber.name.as_ref(),
);

transaction.execute(query).await.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
})?;
transaction.execute(query).await?;
Ok(subscriber_id)
}

#[tracing::instrument(
name = "Send a confirmation email to a new subscriber",
skip(email_client, new_subscriber, base_url, subscription_token)
)]
pub async fn send_confirmation_email(
email_client: &EmailClient,
new_subscriber: NewSubscriber,
base_url: &str,
subscription_token: &str,
) -> Result<(), reqwest::Error> {
let confirmation_link = format!(
"{}/subscriptions/confirm?subscription_token={}",
base_url, subscription_token
);

let plain_body = format!(
"Welcome to our newsletter!\nVisit {} to confirm your subscription.",
confirmation_link
);
let html_body = format!(
"Welcome to our newsletter!<br /> \
Click <a href=\"{}\">here</a> to confirm your subscription.",
confirmation_link,
);
email_client
.send_email(new_subscriber.email, "Welcome!", &html_body, &plain_body)
.await
}

#[tracing::instrument(
name = "Storing subscription token in the database",
skip(transaction, subscription_token)
Expand All @@ -127,21 +172,31 @@ pub async fn store_token(
subscriber_id
);

transaction.execute(query).await.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
StoreTokenError(e)
})?;
transaction.execute(query).await.map_err(StoreTokenError)?;
Ok(())
}

#[allow(unused)]
pub struct StoreTokenError(sqlx::Error);

impl std::error::Error for StoreTokenError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
// The compiler transparently casts `&sqlx::Error` into a `&dyn Error`
Some(&self.0)
}
}

impl std::fmt::Debug for StoreTokenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}

impl std::fmt::Display for StoreTokenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"A database error occurred when storing a subscription token"
"A database failure was encountered while trying to store a subscription token."
)
}
}
Expand All @@ -158,47 +213,3 @@ fn error_chain_fmt(
}
Ok(())
}

impl std::error::Error for StoreTokenError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
// The compiler transparently casts `&sqlx::Error` into a `&dyn Error`
Some(&self.0)
}
}

impl std::fmt::Debug for StoreTokenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}

impl ResponseError for StoreTokenError {}

#[tracing::instrument(
name = "Send a confirmation email to a new subscriber",
skip(email_client, new_subscriber, base_url, subscription_token)
)]
pub async fn send_confirmation_email(
email_client: &EmailClient,
new_subscriber: NewSubscriber,
base_url: &str,
subscription_token: &str,
) -> Result<(), reqwest::Error> {
let confirmation_link = format!(
"{}/subscriptions/confirm?subscription_token={}",
base_url, subscription_token
);

let plain_body = format!(
"Welcome to our newsletter!\nVisit {} to confirm your subscription.",
confirmation_link
);
let html_body = format!(
"Welcome to our newsletter!<br /> \
Click <a href=\"{}\">here</a> to confirm your subscription.",
confirmation_link,
);
email_client
.send_email(new_subscriber.email, "Welcome!", &html_body, &plain_body)
.await
}
13 changes: 0 additions & 13 deletions src/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,6 @@ use tracing_log::LogTracer;
use tracing_subscriber::fmt::MakeWriter;
use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry};

/// Compose multiple layers into a `tracing`'s subscriber.
///
/// # Implementation Notes
///
/// We are using `impl Subscriber` as return type to avoid having to
/// spell out the actual type of the returned subscriber, which is
/// indeed quite complex.
/// We need to explicitly call out that the returned subscriber is
/// `Send` and `Sync` to make it possible to pass it to `init_subscriber`
/// later on.
pub fn get_subscriber<Sink>(
name: String,
env_filter: String,
Expand All @@ -34,9 +24,6 @@ where
.with(formatting_layer)
}

/// Register a subscriber as global default to process span data.
///
/// It should only be called once!
pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) {
LogTracer::init().expect("Failed to set logger");
set_global_default(subscriber).expect("Failed to set subscriber");
Expand Down
31 changes: 15 additions & 16 deletions tests/api/subscriptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,19 +134,18 @@ async fn subscribe_sends_a_confirmation_email_with_a_link() {

#[tokio::test]
async fn subscribe_fails_if_there_is_a_fatal_database_error() {
// Arrange
let app = spawn_app().await;
let body = "name=le%20guin&email=ursula_le_guin%40gmail.com";
// Sabotage the database
sqlx::query!("ALTER TABLE subscription_tokens DROP COLUMN subscription_token;",)
.execute(&app.db_pool)
.await
.unwrap();

// Act
let response = app.post_subscriptions(body.into()).await;

// Assert
assert_eq!(response.status().as_u16(), 500);

}
// Arrange
let app = spawn_app().await;
let body = "name=le%20guin&email=ursula_le_guin%40gmail.com";
// Sabotage the database
sqlx::query!("ALTER TABLE subscriptions DROP COLUMN email;",)
.execute(&app.db_pool)
.await
.unwrap();

// Act
let response = app.post_subscriptions(body.into()).await;

// Assert
assert_eq!(response.status().as_u16(), 500);
}

0 comments on commit ba617b4

Please sign in to comment.