Skip to content

Commit

Permalink
add subscriber data validation
Browse files Browse the repository at this point in the history
  • Loading branch information
alexsavio committed Apr 2, 2024
1 parent e92fd1f commit 1283d33
Show file tree
Hide file tree
Showing 11 changed files with 297 additions and 11 deletions.
95 changes: 92 additions & 3 deletions Cargo.lock

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

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ tracing-actix-web = "0.7"
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
tracing-bunyan-formatter = "0.3"
serde-aux = "4.5.0"
unicode-segmentation = "1.11.0"
validator = "0.16.0"

[dependencies.sqlx]
version = "0.7"
Expand All @@ -41,3 +43,8 @@ features = [
[dev-dependencies]
reqwest = "0.12.2"
once_cell = "1"
claims = "0.7"
fake = "2.9.2"
quickcheck = "1.0.3"
quickcheck_macros = "1.0.0"
rand = "0.8.5"
1 change: 1 addition & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ testv:

## Run the linter
lint:
cargo check
cargo clippy

## Authenticate against DigitalOcean
Expand Down
3 changes: 0 additions & 3 deletions spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ services:
routes:
- path: /
envs:
- key: APP_APPLICATION__BASE_URL
scope: RUN_TIME
value: ${APP_URL}
- key: APP_DATABASE__USERNAME
scope: RUN_TIME
value: ${newsletter.USERNAME}
Expand Down
7 changes: 7 additions & 0 deletions src/domain/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod subscriber_email;
mod subscriber_name;
mod new_subscriber;

pub use subscriber_email::SubscriberEmail;
pub use subscriber_name::SubscriberName;
pub use new_subscriber::NewSubscriber;
8 changes: 8 additions & 0 deletions src/domain/new_subscriber.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

use crate::domain::subscriber_name::SubscriberName;
use crate::domain::subscriber_email::SubscriberEmail;

pub struct NewSubscriber {
pub email: SubscriberEmail,
pub name: SubscriberName,
}
64 changes: 64 additions & 0 deletions src/domain/subscriber_email.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use validator::validate_email;

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

impl SubscriberEmail {
pub fn parse(s: String) -> Result<SubscriberEmail, String> {
if validate_email(&s) {
Ok(Self(s))
}
else {
Err(format!("{} is not a valid email address.", s))
}
}
}

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

#[cfg(test)]
mod tests {
use fake::faker::internet::en::SafeEmail;
use fake::Fake;
use rand::{rngs::StdRng, SeedableRng};
use claims::assert_err;
use crate::domain::SubscriberEmail;

#[test]
fn empty_string_is_rejected() {
let email = "".to_string();
assert_err!(SubscriberEmail::parse(email));
}

#[test]
fn email_missing_at_symbol_is_rejected() {
let email = "name.example.com".to_string();
assert_err!(SubscriberEmail::parse(email));
}

#[test]
fn email_missing_subject_is_rejected() {
let email = "@example.com".to_string();
assert_err!(SubscriberEmail::parse(email));
}

#[derive(Debug, Clone)]
struct ValidEmailFixture(pub String);

impl quickcheck::Arbitrary for ValidEmailFixture {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let mut rng = StdRng::seed_from_u64(u64::arbitrary(g));
let email = SafeEmail().fake_with_rng(&mut rng);
Self(email)
}
}

#[quickcheck_macros::quickcheck]
fn valid_emails_are_parsed_successfully(valid_email: ValidEmailFixture) -> bool {
SubscriberEmail::parse(valid_email.0).is_ok()
}
}
70 changes: 70 additions & 0 deletions src/domain/subscriber_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use unicode_segmentation::UnicodeSegmentation;

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

impl SubscriberName {
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(|c| forbidden_characters.contains(&c));
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 claims::{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_257_grapheme_long_name_is_rejected() {
let name = "a".repeat(257);
assert_err!(SubscriberName::parse(name));
}

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

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

#[test]
fn an_empty_name_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 = format!("a{}b", name);
assert_err!(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 configuration;
pub mod domain;
pub mod routes;
pub mod startup;
pub mod telemetry;
Loading

0 comments on commit 1283d33

Please sign in to comment.