diff --git a/Cargo.lock b/Cargo.lock index b76539f..c41f4bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -701,6 +701,25 @@ dependencies = [ "syn", ] +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "fake" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6479fa2c7e83ddf8be7d435421e093b072ca891b99a49bc84eba098f4044f818" +dependencies = [ + "rand", +] + [[package]] name = "flate2" version = "1.0.19" @@ -1609,6 +1628,29 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quickcheck" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" +dependencies = [ + "env_logger", + "log", + "rand", + "rand_core", +] + +[[package]] +name = "quickcheck_macros" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608c156fd8e97febc07dc9c2e2c80bf74cfc6ef26893eae3daf8bc2bc94a4b7f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.7" @@ -2921,7 +2963,10 @@ dependencies = [ "chrono", "claim", "config", + "fake", "lazy_static", + "quickcheck", + "quickcheck_macros", "reqwest", "serde", "serde-aux", diff --git a/Cargo.toml b/Cargo.toml index fe8f6bb..1199da8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,8 +30,11 @@ tracing-actix-web = "0.2.0" serde-aux = "1.0.1" unicode-segmentation = "1.7.1" validator = "0.12.0" +quickcheck_macros = "0.9.1" [dev-dependencies] reqwest = { version = "0.10.7", features = ["json"] } lazy_static = "1.4.0" claim = "0.4.0" +quickcheck = "0.9.2" +fake = "2.3.0" diff --git a/src/domain/mod.rs b/src/domain/mod.rs index d59765a..e771a2e 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -1,6 +1,7 @@ -mod subscriber_name; -mod subscriber_email; mod new_subscriber; +mod subscriber_email; +mod subscriber_name; -pub use subscriber_name::SubscriberName; pub use new_subscriber::NewSubscriber; +pub use subscriber_email::SubscriberEmail; +pub use subscriber_name::SubscriberName; diff --git a/src/domain/new_subscriber.rs b/src/domain/new_subscriber.rs index d422b03..8af796b 100644 --- a/src/domain/new_subscriber.rs +++ b/src/domain/new_subscriber.rs @@ -1,6 +1,8 @@ use crate::domain::subscriber_name::SubscriberName; +use crate::domain::subscriber_email::SubscriberEmail; pub struct NewSubscriber { - pub email: String, + // We are not using `String` anymore! + pub email: SubscriberEmail, pub name: SubscriberName, } diff --git a/src/domain/subscriber_email.rs b/src/domain/subscriber_email.rs index 91f30b9..e6612df 100644 --- a/src/domain/subscriber_email.rs +++ b/src/domain/subscriber_email.rs @@ -23,6 +23,8 @@ impl AsRef for SubscriberEmail { mod tests { use super::SubscriberEmail; use claim::assert_err; + use fake::Fake; + use fake::faker::internet::en::SafeEmail; #[test] fn empty_string_is_rejected() { @@ -41,4 +43,19 @@ mod tests { let email = "@domain.com".to_string(); assert_err!(SubscriberEmail::parse(email)); } + + #[derive(Debug, Clone)] + struct ValidEmailFixture(pub String); + + impl quickcheck::Arbitrary for ValidEmailFixture { + fn arbitrary(g: &mut G) -> Self { + let email = SafeEmail().fake_with_rng(g); + Self(email) + } + } + + #[quickcheck_macros::quickcheck] + fn valid_emails_are_parsed_successfully(valid_email: ValidEmailFixture) -> bool { + SubscriberEmail::parse(valid_email.0).is_ok() + } } \ No newline at end of file diff --git a/src/routes/subscriptions.rs b/src/routes/subscriptions.rs index 7e55663..8ae915c 100644 --- a/src/routes/subscriptions.rs +++ b/src/routes/subscriptions.rs @@ -1,7 +1,8 @@ -use crate::domain::{NewSubscriber, SubscriberName}; +use crate::domain::{NewSubscriber, SubscriberEmail, SubscriberName}; use actix_web::{web, HttpResponse}; use chrono::Utc; use sqlx::PgPool; +use std::convert::TryInto; use uuid::Uuid; #[derive(serde::Deserialize)] @@ -10,6 +11,16 @@ pub struct FormData { name: String, } +impl TryInto for FormData { + type Error = String; + + fn try_into(self) -> Result { + let name = SubscriberName::parse(self.name)?; + let email = SubscriberEmail::parse(self.email)?; + Ok(NewSubscriber { email, name }) + } +} + #[tracing::instrument( name = "Adding a new subscriber", skip(form, pool), @@ -22,12 +33,10 @@ pub async fn subscribe( form: web::Form, pool: web::Data, ) -> Result { - let name = - SubscriberName::parse(form.0.name).map_err(|_| HttpResponse::BadRequest().finish())?; - let new_subscriber = NewSubscriber { - email: form.0.email, - name, - }; + let new_subscriber = form + .0 + .try_into() + .map_err(|_| HttpResponse::BadRequest().finish())?; insert_subscriber(&pool, &new_subscriber) .await .map_err(|_| HttpResponse::InternalServerError().finish())?; @@ -48,7 +57,7 @@ pub async fn insert_subscriber( VALUES ($1, $2, $3, $4) "#, Uuid::new_v4(), - new_subscriber.email, + new_subscriber.email.as_ref(), new_subscriber.name.as_ref(), Utc::now() )