Remove name from subscriptions table
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE subscriptions DROP COLUMN name;
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
mod new_subscriber;
|
mod new_subscriber;
|
||||||
mod subscriber_email;
|
mod subscriber_email;
|
||||||
mod subscriber_name;
|
|
||||||
|
|
||||||
pub use new_subscriber::NewSubscriber;
|
pub use new_subscriber::NewSubscriber;
|
||||||
pub use subscriber_email::SubscriberEmail;
|
pub use subscriber_email::SubscriberEmail;
|
||||||
pub use subscriber_name::SubscriberName;
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::domain::{SubscriberName, subscriber_email::SubscriberEmail};
|
use crate::domain::subscriber_email::SubscriberEmail;
|
||||||
|
|
||||||
pub struct NewSubscriber {
|
pub struct NewSubscriber {
|
||||||
pub email: SubscriberEmail,
|
pub email: SubscriberEmail,
|
||||||
pub name: SubscriberName,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
use unicode_segmentation::UnicodeSegmentation;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SubscriberName(String);
|
|
||||||
|
|
||||||
impl SubscriberName {
|
|
||||||
pub fn parse(s: String) -> Result<Self, 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.as_str()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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 = "ê".repeat(256);
|
|
||||||
assert_ok!(SubscriberName::parse(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_name_longer_than_256_graphemes_is_rejected() {
|
|
||||||
let name = "ê".repeat(257);
|
|
||||||
assert_err!(SubscriberName::parse(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_whitespace_only_name_is_rejected() {
|
|
||||||
let name = "\n \t ".to_string();
|
|
||||||
assert_err!(SubscriberName::parse(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_string_is_rejected() {
|
|
||||||
let name = "".to_string();
|
|
||||||
assert_err!(SubscriberName::parse(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_name_containing_invalid_character_is_rejected() {
|
|
||||||
for name in ['/', '(', ')', '"', '<', '>', '\\', '{', '}'] {
|
|
||||||
let name = name.to_string();
|
|
||||||
assert_err!(SubscriberName::parse(name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_valid_name_is_parsed_successfully() {
|
|
||||||
let name = "Alphonse".to_string();
|
|
||||||
assert_ok!(SubscriberName::parse(name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
domain::{NewSubscriber, SubscriberEmail, SubscriberName},
|
domain::{NewSubscriber, SubscriberEmail},
|
||||||
email_client::EmailClient,
|
email_client::EmailClient,
|
||||||
startup::AppState,
|
startup::AppState,
|
||||||
};
|
};
|
||||||
@@ -82,7 +82,6 @@ impl IntoResponse for SubscribeError {
|
|||||||
skip(messages, connection_pool, email_client, base_url, form),
|
skip(messages, connection_pool, email_client, base_url, form),
|
||||||
fields(
|
fields(
|
||||||
subscriber_email = %form.email,
|
subscriber_email = %form.email,
|
||||||
subscriber_name = %form.name
|
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
pub async fn subscribe(
|
pub async fn subscribe(
|
||||||
@@ -140,12 +139,11 @@ pub async fn insert_subscriber(
|
|||||||
let subscriber_id = Uuid::new_v4();
|
let subscriber_id = Uuid::new_v4();
|
||||||
let query = sqlx::query!(
|
let query = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO subscriptions (id, email, name, subscribed_at, status)
|
INSERT INTO subscriptions (id, email, subscribed_at, status)
|
||||||
VALUES ($1, $2, $3, $4, 'pending_confirmation')
|
VALUES ($1, $2, $3, 'pending_confirmation')
|
||||||
"#,
|
"#,
|
||||||
subscriber_id,
|
subscriber_id,
|
||||||
new_subscriber.email.as_ref(),
|
new_subscriber.email.as_ref(),
|
||||||
new_subscriber.name.as_ref(),
|
|
||||||
Utc::now()
|
Utc::now()
|
||||||
);
|
);
|
||||||
transaction.execute(query).await?;
|
transaction.execute(query).await?;
|
||||||
@@ -209,20 +207,14 @@ Click <a href=\"{}\">here</a> to confirm your subscription.",
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct SubscriptionFormData {
|
pub struct SubscriptionFormData {
|
||||||
name: String,
|
|
||||||
email: String,
|
email: String,
|
||||||
email_check: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<SubscriptionFormData> for NewSubscriber {
|
impl TryFrom<SubscriptionFormData> for NewSubscriber {
|
||||||
type Error = String;
|
type Error = String;
|
||||||
|
|
||||||
fn try_from(value: SubscriptionFormData) -> Result<Self, Self::Error> {
|
fn try_from(value: SubscriptionFormData) -> Result<Self, Self::Error> {
|
||||||
let name = SubscriberName::parse(value.name)?;
|
|
||||||
if value.email != value.email_check {
|
|
||||||
return Err("Email addresses don't match.".into());
|
|
||||||
}
|
|
||||||
let email = SubscriberEmail::parse(value.email)?;
|
let email = SubscriberEmail::parse(value.email)?;
|
||||||
Ok(Self { name, email })
|
Ok(Self { email })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ block content %}
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
hx-post="/api/newsletter/subscribe"
|
hx-post="/subscriptions"
|
||||||
hx-target="#subscription-result"
|
hx-target="#subscription-result"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
class="max-w-md mx-auto"
|
class="max-w-md mx-auto"
|
||||||
|
|||||||
@@ -34,20 +34,19 @@ async fn subscribe_persists_the_new_subscriber() {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let email = "alphonse.paix@outlook.com";
|
let email = "alphonse.paix@outlook.com";
|
||||||
let body = format!("name=Alphonse&email={0}&email_check={0}", email);
|
let body = format!("email={email}");
|
||||||
let response = app.post_subscriptions(body).await;
|
let response = app.post_subscriptions(body).await;
|
||||||
|
|
||||||
assert_is_redirect_to(&response, "/register");
|
assert_is_redirect_to(&response, "/register");
|
||||||
let page_html = app.get_register_html().await;
|
let page_html = app.get_register_html().await;
|
||||||
assert!(page_html.contains("A confirmation email has been sent"));
|
assert!(page_html.contains("A confirmation email has been sent"));
|
||||||
|
|
||||||
let saved = sqlx::query!("SELECT email, name, status FROM subscriptions")
|
let saved = sqlx::query!("SELECT email, status FROM subscriptions")
|
||||||
.fetch_one(&app.connection_pool)
|
.fetch_one(&app.connection_pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to fetch saved subscription");
|
.expect("Failed to fetch saved subscription");
|
||||||
|
|
||||||
assert_eq!(saved.email, "alphonse.paix@outlook.com");
|
assert_eq!(saved.email, "alphonse.paix@outlook.com");
|
||||||
assert_eq!(saved.name, "Alphonse");
|
|
||||||
assert_eq!(saved.status, "pending_confirmation");
|
assert_eq!(saved.status, "pending_confirmation");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,22 +54,14 @@ async fn subscribe_persists_the_new_subscriber() {
|
|||||||
async fn subscribe_returns_a_422_when_data_is_missing() {
|
async fn subscribe_returns_a_422_when_data_is_missing() {
|
||||||
let app = TestApp::spawn().await;
|
let app = TestApp::spawn().await;
|
||||||
|
|
||||||
let test_cases = [
|
let response = app.post_subscriptions(String::new()).await;
|
||||||
("name=Alphonse", "missing the email"),
|
|
||||||
("email=alphonse.paix%40outlook.com", "missing the name"),
|
|
||||||
("", "missing both name and email"),
|
|
||||||
];
|
|
||||||
for (invalid_body, error_message) in test_cases {
|
|
||||||
let response = app.post_subscriptions(invalid_body.into()).await;
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
422,
|
422,
|
||||||
response.status().as_u16(),
|
response.status().as_u16(),
|
||||||
"the API did not fail with 422 Unprocessable Entity when the payload was {}.",
|
"the API did not fail with 422 Unprocessable Entity when the payload was missing the email"
|
||||||
error_message
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn subscribe_shows_an_error_message_when_fields_are_present_but_invalid() {
|
async fn subscribe_shows_an_error_message_when_fields_are_present_but_invalid() {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ async fn clicking_on_the_confirmation_link_confirms_a_subscriber() {
|
|||||||
let app = TestApp::spawn().await;
|
let app = TestApp::spawn().await;
|
||||||
|
|
||||||
let email = "alphonse.paix@outlook.com";
|
let email = "alphonse.paix@outlook.com";
|
||||||
let body = format!("name=Alphonse&email={email}&email_check={email}");
|
let body = format!("email={email}");
|
||||||
|
|
||||||
Mock::given(path("v1/email"))
|
Mock::given(path("v1/email"))
|
||||||
.and(method("POST"))
|
.and(method("POST"))
|
||||||
@@ -67,12 +67,11 @@ async fn clicking_on_the_confirmation_link_confirms_a_subscriber() {
|
|||||||
.error_for_status()
|
.error_for_status()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let saved = sqlx::query!("SELECT email, name, status FROM subscriptions")
|
let saved = sqlx::query!("SELECT email, status FROM subscriptions")
|
||||||
.fetch_one(&app.connection_pool)
|
.fetch_one(&app.connection_pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to fetch saved subscription");
|
.expect("Failed to fetch saved subscription");
|
||||||
|
|
||||||
assert_eq!(saved.email, "alphonse.paix@outlook.com");
|
assert_eq!(saved.email, "alphonse.paix@outlook.com");
|
||||||
assert_eq!(saved.name, "Alphonse");
|
|
||||||
assert_eq!(saved.status, "confirmed");
|
assert_eq!(saved.status, "confirmed");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user