feat: add input validation
This commit is contained in:
parent
d7d37341ba
commit
419be581b3
10 changed files with 271 additions and 122 deletions
78
Cargo.lock
generated
78
Cargo.lock
generated
|
|
@ -245,6 +245,15 @@ dependencies = [
|
||||||
"windows-targets 0.48.1",
|
"windows-targets 0.48.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "claims"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6995bbe186456c36307f8ea36be3eefe42f49d106896414e18efc4fb2f846b5"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "config"
|
name = "config"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
|
@ -395,6 +404,12 @@ dependencies = [
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deunicode"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ae2a35373c5c74340b79ae6780b498b2b183915ec5dacf263aac5a099bf485a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
|
|
@ -440,6 +455,16 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_logger"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -484,6 +509,16 @@ version = "2.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fake"
|
||||||
|
version = "2.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1c25829bde82205da46e1823b2259db6273379f626fc211f126f65654a2669be"
|
||||||
|
dependencies = [
|
||||||
|
"deunicode",
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
|
|
@ -1040,9 +1075,14 @@ version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"claims",
|
||||||
"config",
|
"config",
|
||||||
|
"fake",
|
||||||
"hyper 1.1.0",
|
"hyper 1.1.0",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"quickcheck",
|
||||||
|
"quickcheck_macros",
|
||||||
|
"rand",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"secrecy",
|
"secrecy",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -1059,6 +1099,7 @@ dependencies = [
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"validator",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1538,6 +1579,28 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quickcheck"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
|
||||||
|
dependencies = [
|
||||||
|
"env_logger",
|
||||||
|
"log",
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quickcheck_macros"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.29"
|
version = "1.0.29"
|
||||||
|
|
@ -2723,6 +2786,21 @@ dependencies = [
|
||||||
"rand",
|
"rand",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "validator"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd"
|
||||||
|
dependencies = [
|
||||||
|
"idna",
|
||||||
|
"lazy_static",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ tracing-log = "0.2"
|
||||||
secrecy = { version = "0.8", features = ["serde"] }
|
secrecy = { version = "0.8", features = ["serde"] }
|
||||||
unicode-segmentation = "1"
|
unicode-segmentation = "1"
|
||||||
strum_macros = "0.26"
|
strum_macros = "0.26"
|
||||||
|
validator = "0.16"
|
||||||
|
|
||||||
# async-trait = "0.1"
|
# async-trait = "0.1"
|
||||||
# strum_macros = "0.25"
|
# strum_macros = "0.25"
|
||||||
|
|
||||||
|
|
@ -54,3 +56,8 @@ features = [
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
reqwest = "0.11"
|
reqwest = "0.11"
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
|
claims = "0.7"
|
||||||
|
fake = "2.9.2"
|
||||||
|
quickcheck = "1.0.3"
|
||||||
|
quickcheck_macros = "1.0.0"
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
|
||||||
7
src/domain/mod.rs
Normal file
7
src/domain/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
mod new_subscriber;
|
||||||
|
mod subscriber_email;
|
||||||
|
mod subscriber_name;
|
||||||
|
|
||||||
|
pub use new_subscriber::NewSubscriber;
|
||||||
|
pub use subscriber_email::SubscriberEmail;
|
||||||
|
pub use subscriber_name::SubscriberName;
|
||||||
7
src/domain/new_subscriber.rs
Normal file
7
src/domain/new_subscriber.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
use crate::domain::subscriber_email::SubscriberEmail;
|
||||||
|
use crate::domain::subscriber_name::SubscriberName;
|
||||||
|
|
||||||
|
pub struct NewSubscriber {
|
||||||
|
pub email: SubscriberEmail,
|
||||||
|
pub name: SubscriberName,
|
||||||
|
}
|
||||||
76
src/domain/subscriber_email.rs
Normal file
76
src/domain/subscriber_email.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
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 subscriber email.", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for SubscriberEmail {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::SubscriberEmail;
|
||||||
|
use claims::assert_err;
|
||||||
|
use fake::faker::internet::en::SafeEmail;
|
||||||
|
use fake::Fake;
|
||||||
|
use rand::{rngs::StdRng, SeedableRng};
|
||||||
|
|
||||||
|
#[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 {
|
||||||
|
dbg!(&valid_email.0);
|
||||||
|
SubscriberEmail::parse(valid_email.0).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 = "ursuladomain.com".to_string();
|
||||||
|
assert_err!(SubscriberEmail::parse(email));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn email_missing_subject_is_rejected() {
|
||||||
|
let email = "@domain.com".to_string();
|
||||||
|
assert_err!(SubscriberEmail::parse(email));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn email_sharp_s_prefix_is_rejected() {
|
||||||
|
let email = "joe.ßchmidt@example.com".to_string();
|
||||||
|
assert_err!(SubscriberEmail::parse(email));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn email_with_invalid_characters_is_rejected() {
|
||||||
|
let email = "joe.smith@ex ample.com".to_string();
|
||||||
|
assert_err!(SubscriberEmail::parse(email));
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/domain/subscriber_name.rs
Normal file
70
src/domain/subscriber_name.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SubscriberName(String);
|
||||||
|
impl SubscriberName {
|
||||||
|
/// Returns an instance of `SubscriberName` if the input satisfies all
|
||||||
|
/// our validation constraints on subscriber names.
|
||||||
|
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 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 = "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));
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/error.rs
78
src/error.rs
|
|
@ -1,78 +0,0 @@
|
||||||
use axum::http::StatusCode;
|
|
||||||
use axum::response::{IntoResponse, Response};
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, strum_macros::AsRefStr)]
|
|
||||||
#[serde(tag = "type", content = "data")]
|
|
||||||
pub enum Error {
|
|
||||||
LoginFail,
|
|
||||||
|
|
||||||
// -- Auth errors.
|
|
||||||
AuthFailNoAuthTokenCookie,
|
|
||||||
AuthFailTokenWrongFormat,
|
|
||||||
AuthFailCtxNotInRequestExt,
|
|
||||||
|
|
||||||
// -- Model errors.
|
|
||||||
// FIXME: Delete this:
|
|
||||||
PropertyDeleteFailIdNotFound { id: u64 },
|
|
||||||
|
|
||||||
// -- Service errors.
|
|
||||||
SubscriptionFailInvalidName,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error {
|
|
||||||
pub fn client_status_and_error(&self) -> (StatusCode, ClientError) {
|
|
||||||
match self {
|
|
||||||
// -- Login.
|
|
||||||
Self::LoginFail => (StatusCode::UNAUTHORIZED, ClientError::LOGIN_FAIL),
|
|
||||||
|
|
||||||
// -- Auth.
|
|
||||||
Self::AuthFailNoAuthTokenCookie
|
|
||||||
| Self::AuthFailTokenWrongFormat
|
|
||||||
| Self::AuthFailCtxNotInRequestExt => (StatusCode::FORBIDDEN, ClientError::NO_AUTH),
|
|
||||||
|
|
||||||
// -- Model.
|
|
||||||
// FIXME: Delete this:
|
|
||||||
Self::PropertyDeleteFailIdNotFound { .. } => {
|
|
||||||
(StatusCode::BAD_REQUEST, ClientError::INVALID_PARAMS)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Subscription.
|
|
||||||
Self::SubscriptionFailInvalidName => {
|
|
||||||
(StatusCode::BAD_REQUEST, ClientError::INVALID_PARAMS)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Fallback.
|
|
||||||
#[allow(unreachable_patterns)]
|
|
||||||
_ => (
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
ClientError::SERVICE_ERROR,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoResponse for Error {
|
|
||||||
fn into_response(self) -> Response {
|
|
||||||
// TODO: trace something here maybe.
|
|
||||||
|
|
||||||
// Create a placeholder Axum response.
|
|
||||||
let mut response = StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
|
||||||
|
|
||||||
// Insert the Error into the response.
|
|
||||||
response.extensions_mut().insert(self);
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, strum_macros::AsRefStr)]
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
pub enum ClientError {
|
|
||||||
LOGIN_FAIL,
|
|
||||||
NO_AUTH,
|
|
||||||
INVALID_PARAMS,
|
|
||||||
SERVICE_ERROR,
|
|
||||||
}
|
|
||||||
|
|
@ -2,10 +2,8 @@
|
||||||
//!
|
//!
|
||||||
//! This is an example documentation string for the root of the crate.
|
//! This is an example documentation string for the root of the crate.
|
||||||
|
|
||||||
pub use self::error::{Error, Result};
|
|
||||||
|
|
||||||
pub mod configuration;
|
pub mod configuration;
|
||||||
pub mod error;
|
pub mod domain;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
pub mod startup;
|
pub mod startup;
|
||||||
pub mod telemetry;
|
pub mod telemetry;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::Result;
|
use crate::domain::SubscriberEmail;
|
||||||
|
use crate::domain::{NewSubscriber, SubscriberName};
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::routing::post;
|
use axum::routing::post;
|
||||||
use axum::Form;
|
use axum::Form;
|
||||||
|
|
@ -10,7 +11,6 @@ use axum::{
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -19,32 +19,6 @@ struct FormData {
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the input satisfies all our validation constraints
|
|
||||||
/// on subscriber names, `false` otherwise.
|
|
||||||
pub fn is_valid_name(s: &str) -> bool {
|
|
||||||
// `.trim()` returns a view over the input `s` without trailing
|
|
||||||
// whitespace-like characters.
|
|
||||||
// `.is_empty` checks if the view contains any character.
|
|
||||||
let is_empty_or_whitespace = s.trim().is_empty();
|
|
||||||
|
|
||||||
// A grapheme is defined by the Unicode standard as a "user-perceived"
|
|
||||||
// character: `å` is a single grapheme, but it is composed of two characters
|
|
||||||
// (`a` and `̊`).
|
|
||||||
//
|
|
||||||
// `graphemes` returns an iterator over the graphemes in the input `s`.
|
|
||||||
// `true` specifies that we want to use the extended grapheme definition set,
|
|
||||||
// the recommended one.
|
|
||||||
let is_too_long = s.graphemes(true).count() > 256;
|
|
||||||
|
|
||||||
// Iterate over all characters in the input `s` to check if any of them matches
|
|
||||||
// one of the characters in the forbidden array.
|
|
||||||
let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
|
|
||||||
let contains_forbidden_characters = s.chars().any(|g| forbidden_characters.contains(&g));
|
|
||||||
|
|
||||||
// Return `false` if any of our conditions have been violated
|
|
||||||
!(is_empty_or_whitespace || is_too_long || contains_forbidden_characters)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
name = "Adding a new subscriber",
|
name = "Adding a new subscriber",
|
||||||
skip(form, pool),
|
skip(form, pool),
|
||||||
|
|
@ -55,17 +29,24 @@ pub fn is_valid_name(s: &str) -> bool {
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
pub async fn subscribe(State(pool): State<PgPool>, Form(form): Form<FormData>) -> Response {
|
pub async fn subscribe(State(pool): State<PgPool>, Form(form): Form<FormData>) -> Response {
|
||||||
if !is_valid_name(&form.name) {
|
let name = match SubscriberName::parse(form.name) {
|
||||||
tracing::error!("Failed to add subscriber to the database");
|
Ok(name) => name,
|
||||||
return (StatusCode::BAD_REQUEST, "Invalid name").into_response();
|
Err(_) => {
|
||||||
}
|
return (StatusCode::BAD_REQUEST, "Invalid name").into_response();
|
||||||
match insert_subscriber(&pool, &form).await {
|
}
|
||||||
|
};
|
||||||
|
let email = match SubscriberEmail::parse(form.email) {
|
||||||
|
Ok(email) => email,
|
||||||
|
Err(_) => {
|
||||||
|
return (StatusCode::BAD_REQUEST, "Invalid email address").into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let new_subscriber = NewSubscriber { email, name };
|
||||||
|
match insert_subscriber(&pool, &new_subscriber).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tracing::info!("Subscriber added to the database");
|
|
||||||
return (StatusCode::OK,).into_response();
|
return (StatusCode::OK,).into_response();
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
tracing::error!("Failed to add subscriber to the database");
|
|
||||||
return (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong").into_response();
|
return (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong").into_response();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,17 +54,20 @@ pub async fn subscribe(State(pool): State<PgPool>, Form(form): Form<FormData>) -
|
||||||
|
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
name = "Saving new subscriber details in the database",
|
name = "Saving new subscriber details in the database",
|
||||||
skip(form, pool)
|
skip(new_subscriber, pool)
|
||||||
)]
|
)]
|
||||||
pub async fn insert_subscriber(pool: &PgPool, form: &FormData) -> Result<()> {
|
pub async fn insert_subscriber(
|
||||||
|
pool: &PgPool,
|
||||||
|
new_subscriber: &NewSubscriber,
|
||||||
|
) -> Result<(), sqlx::Error> {
|
||||||
let _ = sqlx::query!(
|
let _ = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO subscriptions (id, email, name, subscribed_at)
|
INSERT INTO subscriptions (id, email, name, subscribed_at)
|
||||||
VALUES ($1, $2, $3, $4)
|
VALUES ($1, $2, $3, $4)
|
||||||
"#,
|
"#,
|
||||||
Uuid::new_v4(),
|
Uuid::new_v4(),
|
||||||
form.email,
|
new_subscriber.email.as_ref(),
|
||||||
form.name,
|
new_subscriber.name.as_ref(),
|
||||||
Utc::now()
|
Utc::now()
|
||||||
)
|
)
|
||||||
// We use `get_ref` to get an immutable reference to the `PgConnection`
|
// We use `get_ref` to get an immutable reference to the `PgConnection`
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ async fn subscribe_returns_a_200_for_valid_form_data() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn subscribe_returns_a_200_when_fields_are_present_but_empty() {
|
async fn subscribe_returns_a_400_when_fields_are_present_but_invalid() {
|
||||||
// Arrange
|
// Arrange
|
||||||
let app = spawn_app().await;
|
let app = spawn_app().await;
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|
@ -127,7 +127,7 @@ async fn subscribe_returns_a_200_when_fields_are_present_but_empty() {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
200,
|
400,
|
||||||
response.status().as_u16(),
|
response.status().as_u16(),
|
||||||
"The API did not return a 200 OK when the payload was {}.",
|
"The API did not return a 200 OK when the payload was {}.",
|
||||||
description
|
description
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue