Skip to content

Commit

Permalink
feat: User validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Yag000 committed Sep 6, 2023
1 parent 22fac1e commit 405029a
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 10 deletions.
20 changes: 15 additions & 5 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 @@ -28,6 +28,8 @@ once_cell = "1"
secrecy = { version = "0.8", features = ["serde"] }
tracing-actix-web = "0.7"
serde-aux = "4"
unicode-segmentation = "1"
claim = "0.5.0"

[dev-dependencies]
reqwest = { version = "0.11", features = ["json"] }
Expand Down
79 changes: 79 additions & 0 deletions src/domain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use unicode_segmentation::UnicodeSegmentation;

pub struct NewSubscriber {
pub email: String,
pub name: SubscriberName,
}

#[derive(Debug)]
pub struct SubscriberName(String);

impl SubscriberName {
/// Returns an instance of `SubscriberName` if the input string
/// contains valid characters only, and `None` otherwise.
pub fn parse(s: String) -> Result<SubscriberName, String> {
let is_empty_or_whitespace = s.trim().is_empty();

let is_too_long = s.graphemes(true).count() > 256;

let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];

let contains_forbidden_characters = s.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", s))
} else {
Ok(Self(s))
}
}
}

impl AsRef<str> for SubscriberName {
fn as_ref(&self) -> &str {
&self.0
}
}

#[cfg(test)]
mod tests {
use crate::domain::SubscriberName;
use claim::{assert_err, assert_ok};

#[test]
fn a_256_grapheme_long_name_is_valid() {
let name = "a".repeat(256);
assert_ok!(SubscriberName::parse(name));
}

#[test]
fn a_name_longer_than_256_graphemes_is_rejected() {
let name = "a".repeat(257);
assert_err!(SubscriberName::parse(name));
}

#[test]
fn whitespace_only_names_are_rejected() {
let name = " ".to_string();
assert_err!(SubscriberName::parse(name));
}

#[test]
fn empty_string_is_rejected() {
let name = "".to_string();
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));
}
}

#[test]
fn a_valid_name_is_parsed_successfully() {
let name = "Ursula Le Guin".to_string();
assert_ok!(SubscriberName::parse(name));
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod configurations;
pub mod domain;
pub mod routes;
pub mod startup;
pub mod telemetry;
39 changes: 34 additions & 5 deletions src/routes/subscriptions.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use actix_web::{web, HttpResponse};
use chrono::Utc;
use sqlx::PgPool;
use unicode_segmentation::UnicodeSegmentation;
use uuid::Uuid;

use crate::domain::{NewSubscriber, SubscriberName};

#[allow(dead_code)]
#[derive(serde::Deserialize)]
pub struct FormData {
Expand All @@ -19,25 +22,37 @@ pub struct FormData {
)
)]
pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>) -> HttpResponse {
match insert_subscriber(&form, &pool).await {
let name = match SubscriberName::parse(form.name.clone()) {
Ok(name) => name,
Err(_) => return HttpResponse::BadRequest().finish(),
};
let new_subscriber = NewSubscriber {
email: form.email.clone(),
name,
};

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

#[tracing::instrument(
name = "Saving new subscriber details in the database",
skip(form, pool)
skip(new_subscriber, pool)
)]
pub async fn insert_subscriber(form: &FormData, pool: &PgPool) -> Result<(), sqlx::Error> {
pub async fn insert_subscriber(
new_subscriber: &NewSubscriber,
pool: &PgPool,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO subscriptions (id, email, name, subscribed_at)
VALUES ($1, $2, $3, $4)
"#,
Uuid::new_v4(),
form.email,
form.name,
new_subscriber.email,
new_subscriber.name.as_ref(),
Utc::now()
)
.execute(pool)
Expand All @@ -48,3 +63,17 @@ pub async fn insert_subscriber(form: &FormData, pool: &PgPool) -> Result<(), sql
})?;
Ok(())
}

/// Returns `true` if the input satisfies all our validation constraints
/// on subscriber names, and `false` otherwise.
pub fn is_valid_name(s: &str) -> bool {
let is_empty_or_whitespace = s.trim().is_empty();

let is_too_long = s.graphemes(true).count() > 256;

let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];

let contains_forbidden_characters = s.chars().any(|g| forbidden_characters.contains(&g));

!(is_empty_or_whitespace || is_too_long || contains_forbidden_characters)
}
29 changes: 29 additions & 0 deletions tests/health_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,32 @@ async fn subscribe_returns_a_400_when_data_is_missing() {
);
}
}

#[tokio::test]
async fn subscribe_returns_a_200_when_fields_are_present_but_empty() {
// Arrange
let app = spawn_app().await;
let client = reqwest::Client::new();
let test_cases = vec![
("name=&email=ursula_le_guin%40gmail.com", "empty name"),
("name=Ursula&email=", "empty email"),
("name=Ursula&email=definitely-not-an-email", "invalid email"),
];
for (body, description) in test_cases {
// Act
let response = client
.post(&format!("{}/subscriptions", &app.address))
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body)
.send()
.await
.expect("Failed to execute request.");
// Assert
assert_eq!(
400,
response.status().as_u16(),
"The API did not return a 200 OK when the payload was {}.",
description
);
}
}

0 comments on commit 405029a

Please sign in to comment.