Skip to content

Commit

Permalink
Add new subscriber validations from form data
Browse files Browse the repository at this point in the history
  • Loading branch information
zachkirlew committed Apr 3, 2024
1 parent e2e7d51 commit c9a7b01
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 48 deletions.
7 changes: 3 additions & 4 deletions src/domain/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
mod new_subscriber;
mod subscriber_name;
mod subscriber_email;
mod subscriber_name;


pub use subscriber_name::SubscriberName;
pub use subscriber_email::SubscriberEmail;
pub use new_subscriber::NewSubscriber;
pub use subscriber_email::SubscriberEmail;
pub use subscriber_name::SubscriberName;
2 changes: 1 addition & 1 deletion src/domain/new_subscriber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ use crate::domain::{SubscriberEmail, SubscriberName};
pub struct NewSubscriber {
pub email: SubscriberEmail,
pub name: SubscriberName,
}
}
3 changes: 1 addition & 2 deletions src/domain/subscriber_email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ impl AsRef<str> for SubscriberEmail {
}
}


#[cfg(test)]
mod tests {
use super::SubscriberEmail;
Expand Down Expand Up @@ -61,4 +60,4 @@ mod tests {
let email = "@domain.com".to_string();
assert_err!(SubscriberEmail::parse(email));
}
}
}
25 changes: 14 additions & 11 deletions src/domain/subscriber_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,17 @@ use unicode_segmentation::UnicodeSegmentation;
pub struct SubscriberName(String);

impl SubscriberName {
pub fn parse(name: String) -> Result<SubscriberName,String> {
pub fn parse(name: String) -> Result<SubscriberName, String> {
let is_empty_or_whitespace = name.trim().is_empty();
let is_too_long = name.graphemes(true).count() > 256;
let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
let contains_forbidden_characters = name
.chars()
.any(|g| forbidden_characters.contains(&g));
let contains_forbidden_characters = name.chars().any(|g| forbidden_characters.contains(&g));
if is_empty_or_whitespace || is_too_long || contains_forbidden_characters {
Err(format!("{} is not a valid subscriber name.", name))
} else {
Ok(Self(name))
}
}

}

impl AsRef<str> for SubscriberName {
Expand Down Expand Up @@ -46,17 +43,23 @@ mod tests {
#[test]
fn whitespace_only_names_are_rejected() {
let name = " ".to_string();
assert_err!(SubscriberName::parse(name)); }
assert_err!(SubscriberName::parse(name));
}
#[test]
fn empty_string_is_rejected() {
let name = "".to_string();
assert_err!(SubscriberName::parse(name)); }
assert_err!(SubscriberName::parse(name));
}
#[test]
fn names_containing_an_invalid_character_are_rejected() {
for name in &['/', '(', ')', '"', '<', '>', '\\', '{', '}'] { let name = name.to_string(); assert_err!(SubscriberName::parse(name));
} }
for name in &['/', '(', ')', '"', '<', '>', '\\', '{', '}'] {
let name = name.to_string();
assert_err!(SubscriberName::parse(name));
}
}
#[test]
fn a_valid_name_is_parsed_successfully() {
let name = "Ursula Le Guin".to_string();
assert_ok!(SubscriberName::parse(name)); }
}
assert_ok!(SubscriberName::parse(name));
}
}
37 changes: 14 additions & 23 deletions src/routes/subscriptions.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::domain::{NewSubscriber, SubscriberEmail, SubscriberName};
use actix_web::web::Form;
use actix_web::{web, HttpResponse, Responder};
use chrono::Utc;
use serde::Deserialize;
use sqlx::PgPool;
use crate::domain::{NewSubscriber, SubscriberEmail, SubscriberName};

#[derive(Deserialize)]
pub struct FormData {
Expand All @@ -26,14 +26,13 @@ pub async fn subscribe(form: Form<FormData>, db_pool: web::Data<PgPool>) -> impl
Err(_) => return HttpResponse::BadRequest().finish(),
};


match insert_subscriber(&db_pool, &new_subscriber).await {
Ok(_) => HttpResponse::Ok().finish(),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}

impl TryFrom<FormData> for NewSubscriber{
impl TryFrom<FormData> for NewSubscriber {
type Error = ();

fn try_from(value: FormData) -> Result<Self, Self::Error> {
Expand All @@ -43,22 +42,14 @@ impl TryFrom<FormData> for NewSubscriber{
}
}

pub fn parse_subscriber(form: Form<FormData>) -> Result<NewSubscriber, String> {
let name = SubscriberName::parse(form.name.clone())?;

let email = SubscriberEmail::parse(form.email.clone())?;

Ok(NewSubscriber {
name,
email,
})
}

#[tracing::instrument(
name = "Saving new subscriber details in the database",
skip(db_pool, new_subscriber)
name = "Saving new subscriber details in the database",
skip(db_pool, new_subscriber)
)]
pub async fn insert_subscriber(db_pool: &PgPool, new_subscriber: &NewSubscriber) -> Result<(), sqlx::Error> {
pub async fn insert_subscriber(
db_pool: &PgPool,
new_subscriber: &NewSubscriber,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO subscriptions (name, email, subscribed_at) VALUES ($1, $2, $3)
Expand All @@ -67,11 +58,11 @@ pub async fn insert_subscriber(db_pool: &PgPool, new_subscriber: &NewSubscriber)
new_subscriber.email.as_ref(),
Utc::now()
)
.execute(db_pool)
.await
.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
})?;
.execute(db_pool)
.await
.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
})?;
Ok(())
}
15 changes: 8 additions & 7 deletions tests/health_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ async fn subscribe_returns_a_400_when_data_is_missing(pool: PgPool) {

#[sqlx::test]
async fn subscribe_returns_a_400_when_fields_are_present_but_invalid(pool: PgPool) {
// Arrange
// Arrange
let app = spawn_app(pool).await;
let client = reqwest::Client::new();
let test_cases = vec![
Expand All @@ -85,17 +85,18 @@ async fn subscribe_returns_a_400_when_fields_are_present_but_invalid(pool: PgPoo
("name=Ursula&email=definitely-not-an-email", "invalid email"),
];
for (body, description) in test_cases {

let response = client
.post(&format!("{}/subscriptions", &app.address)).header("Content-Type", "application/x-www-form-urlencoded")
.post(&format!("{}/subscriptions", &app.address))
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body)
.send()
.await
.expect("Failed to execute request.");
assert_eq!(400,
response.status().as_u16(),
"The API did not return a 400 Bad Request when the payload was {}.",
description
assert_eq!(
400,
response.status().as_u16(),
"The API did not return a 400 Bad Request when the payload was {}.",
description
);
}
}
Expand Down

0 comments on commit c9a7b01

Please sign in to comment.