Skip to content

Commit 701495c

Browse files
committed
chore: extended input validation to email
1 parent 20c6718 commit 701495c

File tree

6 files changed

+115
-32
lines changed

6 files changed

+115
-32
lines changed

src/domain.rs

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/domain/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
mod new_subscriber;
2+
mod subscriber_email;
3+
mod subscriber_name;
4+
5+
pub use new_subscriber::NewSubscriber;
6+
pub use subscriber_name::SubscriberName;

src/domain/new_subscriber.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
use crate::domain::subscriber_name::SubscriberName;
2+
pub struct NewSubscriber {
3+
pub email: String,
4+
pub name: SubscriberName,
5+
}

src/domain/subscriber_email.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

src/domain/subscriber_name.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#[derive(Debug)]
2+
pub struct SubscriberName(String);
3+
4+
impl AsRef<str> for SubscriberName {
5+
fn as_ref(&self) -> &str {
6+
&self.0
7+
}
8+
}
9+
10+
impl SubscriberName {
11+
pub fn parse(s: String) -> Result<SubscriberName, String> {
12+
let name = s.trim().to_string();
13+
let is_empty_or_whitespace = name.trim().is_empty();
14+
let is_too_long = name.len() > 256;
15+
let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
16+
let contains_forbidden_characters = name
17+
.chars()
18+
.any(|char| forbidden_characters.contains(&char));
19+
if is_empty_or_whitespace || is_too_long || contains_forbidden_characters {
20+
Err(format!(
21+
"`{}` is not a valid subscriber name. Subscriber name cannot be empty, more than 256 characters long, or contain the following characters: {:?}",
22+
name,
23+
forbidden_characters,
24+
))
25+
} else {
26+
Ok(Self(name))
27+
}
28+
}
29+
}
30+
31+
#[cfg(test)]
32+
mod tests {
33+
use super::*;
34+
use claim::{assert_err, assert_ok};
35+
36+
#[test]
37+
fn a_256_graheme_long_name_is_valid() {
38+
let name = "a".repeat(256);
39+
assert_ok!(SubscriberName::parse(name));
40+
}
41+
42+
#[test]
43+
fn a_name_longer_than_256_graphemes_is_rejected() {
44+
let name = "a".repeat(257);
45+
assert_err!(SubscriberName::parse(name));
46+
}
47+
48+
#[test]
49+
fn empty_string_is_rejected() {
50+
let name = "".to_string();
51+
assert_err!(SubscriberName::parse(name));
52+
}
53+
54+
#[test]
55+
fn whitespace_only_name_is_rejected() {
56+
let name = " ".to_string();
57+
assert_err!(SubscriberName::parse(name));
58+
}
59+
60+
#[test]
61+
fn a_name_containing_a_invalid_character_is_rejected() {
62+
let invalid_characters = vec!['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
63+
for character in invalid_characters {
64+
let name = format!("name{}", character);
65+
assert_err!(SubscriberName::parse(name));
66+
}
67+
}
68+
69+
#[test]
70+
fn a_name_containing_a_valid_character_is_valid() {
71+
let valid_characters = vec!['a', 'A', '1', '-', '.', '_'];
72+
for character in valid_characters {
73+
let name = format!("name{}", character);
74+
assert_ok!(SubscriberName::parse(name));
75+
}
76+
}
77+
78+
#[test]
79+
fn a_valid_name_is_trimmed() {
80+
let name = " John Doe ".to_string();
81+
let parsed_name = SubscriberName::parse(name).unwrap();
82+
assert_eq!(parsed_name.as_ref(), "John Doe");
83+
}
84+
85+
#[test]
86+
fn a_name_with_valid_characters_is_valid() {
87+
let name = "John Doe".to_string();
88+
assert_ok!(SubscriberName::parse(name));
89+
}
90+
}

src/routes/subscriptions.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
use crate::domain::{NewSubscriber, SubscriberName};
12
use actix_web::{web, HttpResponse};
23
use chrono::Utc;
34
use sqlx::types::Uuid;
45
use sqlx::PgPool;
5-
use crate::domain::{NewSubscriber, SubscriberName};
66

77
#[allow(dead_code)]
88
#[derive(serde::Deserialize)]
@@ -21,10 +21,16 @@ pub struct FormData {
2121
)]
2222

2323
pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>) -> HttpResponse {
24+
let subscriber_name = match SubscriberName::parse(form.0.name) {
25+
Ok(name) => name,
26+
Err(_) => return HttpResponse::BadRequest().finish(),
27+
};
28+
2429
let new_subscriber = NewSubscriber {
2530
email: form.0.email,
26-
name: SubscriberName::parse(form.0.name),
31+
name: subscriber_name,
2732
};
33+
2834
match insert_subscriber(&new_subscriber, &pool).await {
2935
Ok(_) => HttpResponse::Ok().finish(),
3036
Err(e) => {
@@ -38,7 +44,10 @@ pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>) -> Ht
3844
name = "Saving a new subscriber details in the database",
3945
skip(new_subscriber, pool)
4046
)]
41-
async fn insert_subscriber(new_subscriber: &NewSubscriber, pool: &PgPool) -> Result<(), sqlx::Error> {
47+
async fn insert_subscriber(
48+
new_subscriber: &NewSubscriber,
49+
pool: &PgPool,
50+
) -> Result<(), sqlx::Error> {
4251
let subscribe_id = Uuid::new_v4();
4352
sqlx::query!(
4453
r#"
@@ -47,7 +56,7 @@ async fn insert_subscriber(new_subscriber: &NewSubscriber, pool: &PgPool) -> Res
4756
"#,
4857
subscribe_id,
4958
new_subscriber.email,
50-
new_subscriber.name.inner_ref(),
59+
new_subscriber.name.as_ref(),
5160
Utc::now()
5261
)
5362
.execute(pool)

0 commit comments

Comments
 (0)