mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
refactor: extract email validation and PII utils to common_utils crate (#72)
This commit is contained in:
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -897,11 +897,16 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"error-stack",
|
"error-stack",
|
||||||
|
"fake",
|
||||||
"masking",
|
"masking",
|
||||||
|
"once_cell",
|
||||||
|
"proptest",
|
||||||
|
"regex",
|
||||||
"router_env",
|
"router_env",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2625,7 +2630,6 @@ dependencies = [
|
|||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"error-stack",
|
"error-stack",
|
||||||
"fake",
|
|
||||||
"fred",
|
"fred",
|
||||||
"futures",
|
"futures",
|
||||||
"hex",
|
"hex",
|
||||||
@ -2637,7 +2641,6 @@ dependencies = [
|
|||||||
"mime",
|
"mime",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proptest",
|
|
||||||
"rand",
|
"rand",
|
||||||
"redis_interface",
|
"redis_interface",
|
||||||
"regex",
|
"regex",
|
||||||
|
|||||||
@ -7,11 +7,18 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
bytes = "1.2.1"
|
bytes = "1.2.1"
|
||||||
error-stack = "0.2.1"
|
error-stack = "0.2.1"
|
||||||
|
once_cell = "1.16.0"
|
||||||
|
regex = "1.7.0"
|
||||||
serde = { version = "1.0.145", features = ["derive"] }
|
serde = { version = "1.0.145", features = ["derive"] }
|
||||||
serde_json = "1.0.85"
|
serde_json = "1.0.85"
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
|
thiserror = "1.0.37"
|
||||||
time = { version = "0.3.17", features = ["serde", "serde-well-known", "std"] }
|
time = { version = "0.3.17", features = ["serde", "serde-well-known", "std"] }
|
||||||
|
|
||||||
# First party crates
|
# First party crates
|
||||||
masking = { version = "0.1.0", path = "../masking" }
|
masking = { version = "0.1.0", path = "../masking" }
|
||||||
router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] }
|
router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
fake = "2.5.0"
|
||||||
|
proptest = "1.0.0"
|
||||||
@ -1,9 +1,8 @@
|
|||||||
//!
|
//! Errors and error specific types for universal use
|
||||||
//! errors and error specific types for universal use
|
|
||||||
|
|
||||||
/// Custom Result
|
/// Custom Result
|
||||||
/// A custom datatype that wraps the error variant <E> into a report, allowing
|
/// A custom datatype that wraps the error variant <E> into a report, allowing
|
||||||
/// error_stack::Report<E> specific extendability
|
/// error_stack::Report<E> specific extendability
|
||||||
///
|
///
|
||||||
/// Effectively, equivalent to `Result<T, error_stack::Report<E>>`
|
/// Effectively, equivalent to `Result<T, error_stack::Report<E>>`
|
||||||
///
|
///
|
||||||
@ -38,3 +37,20 @@ macro_rules! impl_error_type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl_error_type!(ParsingError, "Parsing error");
|
impl_error_type!(ParsingError, "Parsing error");
|
||||||
|
|
||||||
|
/// Validation errors.
|
||||||
|
#[allow(missing_docs)] // Only to prevent warnings about struct fields not being documented
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ValidationError {
|
||||||
|
/// The provided input is missing a required field.
|
||||||
|
#[error("Missing required field: {field_name}")]
|
||||||
|
MissingRequiredField { field_name: String },
|
||||||
|
|
||||||
|
/// An incorrect value was provided for the field specified by `field_name`.
|
||||||
|
#[error("Incorrect value provided for field: {field_name}")]
|
||||||
|
IncorrectValueProvided { field_name: &'static str },
|
||||||
|
|
||||||
|
/// An invalid input was provided.
|
||||||
|
#[error("{message}")]
|
||||||
|
InvalidValue { message: String },
|
||||||
|
}
|
||||||
|
|||||||
@ -16,6 +16,8 @@
|
|||||||
pub mod custom_serde;
|
pub mod custom_serde;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod ext_traits;
|
pub mod ext_traits;
|
||||||
|
pub mod pii;
|
||||||
|
pub mod validation;
|
||||||
|
|
||||||
/// Date-time utilities.
|
/// Date-time utilities.
|
||||||
pub mod date_time {
|
pub mod date_time {
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
//!
|
|
||||||
//! Personal Identifiable Information protection.
|
//! Personal Identifiable Information protection.
|
||||||
//!
|
|
||||||
|
|
||||||
use std::{convert::AsRef, fmt};
|
use std::{convert::AsRef, fmt};
|
||||||
|
|
||||||
#[doc(inline)]
|
use masking::{Strategy, WithType};
|
||||||
pub use masking::*;
|
|
||||||
|
|
||||||
use crate::utils::validate_email;
|
use crate::validation::validate_email;
|
||||||
|
|
||||||
|
/// Card number
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct CardNumber;
|
pub struct CardNumber;
|
||||||
|
|
||||||
impl<T> Strategy<T> for CardNumber
|
impl<T> Strategy<T> for CardNumber
|
||||||
@ -22,32 +21,42 @@ where
|
|||||||
return WithType::fmt(val, f);
|
return WithType::fmt(val, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(f, "{}{}", &val_str[..6], "*".repeat(val_str.len() - 6))
|
f.write_str(&format!(
|
||||||
|
"{}{}",
|
||||||
|
&val_str[..6],
|
||||||
|
"*".repeat(val_str.len() - 6)
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//pub struct PhoneNumber;
|
/*
|
||||||
|
/// Phone number
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PhoneNumber;
|
||||||
|
|
||||||
//impl<T> Strategy<T> for PhoneNumber
|
impl<T> Strategy<T> for PhoneNumber
|
||||||
//where
|
where
|
||||||
//T: AsRef<str>,
|
T: AsRef<str>,
|
||||||
//{
|
{
|
||||||
//fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
//let val_str: &str = val.as_ref();
|
let val_str: &str = val.as_ref();
|
||||||
|
|
||||||
//if val_str.len() < 10 || val_str.len() > 12 {
|
if val_str.len() < 10 || val_str.len() > 12 {
|
||||||
//return WithType::fmt(val, f);
|
return WithType::fmt(val, f);
|
||||||
//}
|
}
|
||||||
|
|
||||||
//f.write_str(&format!(
|
f.write_str(&format!(
|
||||||
//"{}{}{}",
|
"{}{}{}",
|
||||||
//&val_str[..2],
|
&val_str[..2],
|
||||||
//"*".repeat(val_str.len() - 5),
|
"*".repeat(val_str.len() - 5),
|
||||||
//&val_str[(val_str.len() - 3)..]
|
&val_str[(val_str.len() - 3)..]
|
||||||
//))
|
))
|
||||||
//}
|
}
|
||||||
//}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// Email address
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Email;
|
pub struct Email;
|
||||||
|
|
||||||
impl<T> Strategy<T> for Email
|
impl<T> Strategy<T> for Email
|
||||||
@ -62,14 +71,17 @@ where
|
|||||||
return WithType::fmt(val, f);
|
return WithType::fmt(val, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((a, b)) = val_str.split_once('@') {
|
let parts: Vec<&str> = val_str.split('@').collect();
|
||||||
write!(f, "{}@{}", "*".repeat(a.len()), b)
|
if parts.len() != 2 {
|
||||||
} else {
|
return WithType::fmt(val, f);
|
||||||
WithType::fmt(val, f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
f.write_str(&format!("{}@{}", "*".repeat(parts[0].len()), parts[1]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// IP address
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct IpAddress;
|
pub struct IpAddress;
|
||||||
|
|
||||||
impl<T> Strategy<T> for IpAddress
|
impl<T> Strategy<T> for IpAddress
|
||||||
@ -90,13 +102,15 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(f, "{}.**.**.**", segments[0])
|
f.write_str(&format!("{}.**.**.**", segments[0]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod pii_masking_strategy_tests {
|
mod pii_masking_strategy_tests {
|
||||||
use super::{CardNumber, Email, IpAddress, Secret};
|
use masking::Secret;
|
||||||
|
|
||||||
|
use super::{CardNumber, Email, IpAddress};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_valid_card_number_masking() {
|
fn test_valid_card_number_masking() {
|
||||||
@ -110,7 +124,8 @@ mod pii_masking_strategy_tests {
|
|||||||
assert_eq!("123456****", &format!("{:?}", secret));
|
assert_eq!("123456****", &format!("{:?}", secret));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* #[test]
|
/*
|
||||||
|
#[test]
|
||||||
fn test_valid_phone_number_masking() {
|
fn test_valid_phone_number_masking() {
|
||||||
let secret: Secret<String, PhoneNumber> = Secret::new("9922992299".to_string());
|
let secret: Secret<String, PhoneNumber> = Secret::new("9922992299".to_string());
|
||||||
assert_eq!("99*****299", &format!("{}", secret));
|
assert_eq!("99*****299", &format!("{}", secret));
|
||||||
@ -123,7 +138,8 @@ mod pii_masking_strategy_tests {
|
|||||||
|
|
||||||
let secret: Secret<String, PhoneNumber> = Secret::new("9922992299229922".to_string());
|
let secret: Secret<String, PhoneNumber> = Secret::new("9922992299229922".to_string());
|
||||||
assert_eq!("*** alloc::string::String ***", &format!("{}", secret));
|
assert_eq!("*** alloc::string::String ***", &format!("{}", secret));
|
||||||
} */
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_valid_email_masking() {
|
fn test_valid_email_masking() {
|
||||||
102
crates/common_utils/src/validation.rs
Normal file
102
crates/common_utils/src/validation.rs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
//! Custom validations for some shared types.
|
||||||
|
|
||||||
|
use error_stack::report;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
|
use router_env::logger;
|
||||||
|
|
||||||
|
use crate::errors::{CustomResult, ValidationError};
|
||||||
|
|
||||||
|
/// Performs a simple validation against a provided email address.
|
||||||
|
pub fn validate_email(email: &str) -> CustomResult<(), ValidationError> {
|
||||||
|
#[deny(clippy::invalid_regex)]
|
||||||
|
static EMAIL_REGEX: Lazy<Option<Regex>> = Lazy::new(|| {
|
||||||
|
match Regex::new(
|
||||||
|
r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
|
||||||
|
) {
|
||||||
|
Ok(regex) => Some(regex),
|
||||||
|
Err(error) => {
|
||||||
|
logger::error!(?error);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let email_regex = match EMAIL_REGEX.as_ref() {
|
||||||
|
Some(regex) => Ok(regex),
|
||||||
|
None => Err(report!(ValidationError::InvalidValue {
|
||||||
|
message: "Invalid regex expression".into()
|
||||||
|
})),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
const EMAIL_MAX_LENGTH: usize = 319;
|
||||||
|
if email.is_empty() || email.chars().count() > EMAIL_MAX_LENGTH {
|
||||||
|
return Err(report!(ValidationError::InvalidValue {
|
||||||
|
message: "Email address is either empty or exceeds maximum allowed length".into()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !email_regex.is_match(email) {
|
||||||
|
return Err(report!(ValidationError::InvalidValue {
|
||||||
|
message: "Invalid email address format".into()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use fake::{faker::internet::en::SafeEmail, Fake};
|
||||||
|
use proptest::{
|
||||||
|
prop_assert,
|
||||||
|
strategy::{Just, NewTree, Strategy},
|
||||||
|
test_runner::TestRunner,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ValidEmail;
|
||||||
|
|
||||||
|
impl Strategy for ValidEmail {
|
||||||
|
type Tree = Just<String>;
|
||||||
|
type Value = String;
|
||||||
|
|
||||||
|
fn new_tree(&self, _runner: &mut TestRunner) -> NewTree<Self> {
|
||||||
|
Ok(Just(SafeEmail().fake()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_email() {
|
||||||
|
let result = validate_email("abc@example.com");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let result = validate_email("abc+123@example.com");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let result = validate_email("");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest::proptest! {
|
||||||
|
/// Example of unit test
|
||||||
|
#[test]
|
||||||
|
fn proptest_valid_fake_email(email in ValidEmail) {
|
||||||
|
prop_assert!(validate_email(&email).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Example of unit test
|
||||||
|
#[test]
|
||||||
|
fn proptest_invalid_data_email(email in "\\PC*") {
|
||||||
|
prop_assert!(validate_email(&email).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make maybe unit test working
|
||||||
|
// minimal failing input: email = "+@a"
|
||||||
|
// #[test]
|
||||||
|
// fn proptest_invalid_email(email in "[.+]@(.+)") {
|
||||||
|
// prop_assert!(validate_email(&email).is_err());
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -75,15 +75,13 @@ router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra
|
|||||||
router_env = { version = "0.1.0", path = "../router_env", default-features = false, features = ["vergen"] }
|
router_env = { version = "0.1.0", path = "../router_env", default-features = false, features = ["vergen"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
actix-http = "3.2.2"
|
||||||
awc = { version = "3.0.1", features = ["rustls"] }
|
awc = { version = "3.0.1", features = ["rustls"] }
|
||||||
|
derive_deref = "1.1.1"
|
||||||
|
rand = "0.8.5"
|
||||||
time = { version = "0.3.14", features = ["macros"] }
|
time = { version = "0.3.14", features = ["macros"] }
|
||||||
tokio = "1.21.2"
|
tokio = "1.21.2"
|
||||||
toml = "0.5.9"
|
toml = "0.5.9"
|
||||||
derive_deref = "1.1.1"
|
|
||||||
actix-http = "3.2.2"
|
|
||||||
proptest = "1.0"
|
|
||||||
fake = "2.5"
|
|
||||||
rand = "0.8"
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "router"
|
name = "router"
|
||||||
|
|||||||
@ -40,7 +40,7 @@ pub struct Card {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&types::PaymentsAuthorizeRouterData> for BraintreePaymentsRequest {
|
impl TryFrom<&types::PaymentsAuthorizeRouterData> for BraintreePaymentsRequest {
|
||||||
type Error = error_stack::Report<errors::ValidateError>;
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||||
match item.request.payment_method_data {
|
match item.request.payment_method_data {
|
||||||
api::PaymentMethod::Card(ref ccard) => {
|
api::PaymentMethod::Card(ref ccard) => {
|
||||||
@ -62,7 +62,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BraintreePaymentsRequest {
|
|||||||
transaction: braintree_payment_request,
|
transaction: braintree_payment_request,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => Err(errors::ValidateError.into()),
|
_ => Err(errors::ConnectorError::RequestEncodingFailed.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ pub(crate) mod utils;
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use actix_web::{body::BoxBody, http::StatusCode, HttpResponse, ResponseError};
|
use actix_web::{body::BoxBody, http::StatusCode, HttpResponse, ResponseError};
|
||||||
pub use common_utils::errors::{CustomResult, ParsingError};
|
pub use common_utils::errors::{CustomResult, ParsingError, ValidationError};
|
||||||
use config::ConfigError;
|
use config::ConfigError;
|
||||||
use error_stack;
|
use error_stack;
|
||||||
pub use redis_interface::errors::RedisError;
|
pub use redis_interface::errors::RedisError;
|
||||||
@ -409,16 +409,6 @@ error_to_process_tracker_error!(
|
|||||||
ProcessTrackerError::EValidationError(error_stack::Report<ValidationError>)
|
ProcessTrackerError::EValidationError(error_stack::Report<ValidationError>)
|
||||||
);
|
);
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ValidationError {
|
|
||||||
#[error("Missing required field: {field_name}")]
|
|
||||||
MissingRequiredField { field_name: String },
|
|
||||||
#[error("Incorrect value provided for field: {field_name}")]
|
|
||||||
IncorrectValueProvided { field_name: &'static str },
|
|
||||||
#[error("{message}")]
|
|
||||||
InvalidValue { message: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum WebhooksFlowError {
|
pub enum WebhooksFlowError {
|
||||||
#[error("Merchant webhook config not found")]
|
#[error("Merchant webhook config not found")]
|
||||||
|
|||||||
@ -27,7 +27,6 @@ pub mod core;
|
|||||||
pub mod cors;
|
pub mod cors;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod env;
|
pub mod env;
|
||||||
pub mod pii;
|
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
pub mod scheduler;
|
pub mod scheduler;
|
||||||
|
|
||||||
@ -61,6 +60,14 @@ pub mod headers {
|
|||||||
pub const X_API_VERSION: &str = "X-ApiVersion";
|
pub const X_API_VERSION: &str = "X-ApiVersion";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod pii {
|
||||||
|
//! Personal Identifiable Information protection.
|
||||||
|
|
||||||
|
pub(crate) use common_utils::pii::{CardNumber, Email, IpAddress};
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use masking::*;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mk_app(
|
pub fn mk_app(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
request_body_limit: usize,
|
request_body_limit: usize,
|
||||||
|
|||||||
@ -5,11 +5,14 @@ mod fp_utils;
|
|||||||
#[cfg(feature = "kv_store")]
|
#[cfg(feature = "kv_store")]
|
||||||
pub(crate) mod storage_partitioning;
|
pub(crate) mod storage_partitioning;
|
||||||
|
|
||||||
pub(crate) use common_utils::ext_traits::{ByteSliceExt, BytesExt, Encode, StringExt, ValueExt};
|
pub(crate) use common_utils::{
|
||||||
|
ext_traits::{ByteSliceExt, BytesExt, Encode, StringExt, ValueExt},
|
||||||
|
validation::validate_email,
|
||||||
|
};
|
||||||
use nanoid::nanoid;
|
use nanoid::nanoid;
|
||||||
|
|
||||||
pub(crate) use self::{
|
pub(crate) use self::{
|
||||||
ext_traits::{validate_address, validate_email, OptionExt, ValidateCall},
|
ext_traits::{validate_address, OptionExt, ValidateCall},
|
||||||
fp_utils::when,
|
fp_utils::when,
|
||||||
};
|
};
|
||||||
use crate::consts;
|
use crate::consts;
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
use common_utils::ext_traits::ValueExt;
|
use common_utils::ext_traits::ValueExt;
|
||||||
use error_stack::{report, IntoReport, Report, ResultExt};
|
use error_stack::{report, IntoReport, Report, ResultExt};
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::errors::{self, ApiErrorResponse, CustomResult, RouterResult},
|
core::errors::{self, ApiErrorResponse, CustomResult, RouterResult},
|
||||||
logger,
|
|
||||||
types::api::AddressDetails,
|
types::api::AddressDetails,
|
||||||
utils::when,
|
utils::when,
|
||||||
};
|
};
|
||||||
@ -132,42 +129,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_email(email: &str) -> CustomResult<(), errors::ValidationError> {
|
|
||||||
#[deny(clippy::invalid_regex)]
|
|
||||||
static EMAIL_REGEX: Lazy<Option<Regex>> = Lazy::new(|| {
|
|
||||||
match Regex::new(
|
|
||||||
r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
|
|
||||||
) {
|
|
||||||
Ok(regex) => Some(regex),
|
|
||||||
Err(error) => {
|
|
||||||
logger::error!(?error);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let email_regex = match EMAIL_REGEX.as_ref() {
|
|
||||||
Some(regex) => Ok(regex),
|
|
||||||
None => Err(report!(errors::ValidationError::InvalidValue {
|
|
||||||
message: "Invalid regex expression".into()
|
|
||||||
})),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
const EMAIL_MAX_LENGTH: usize = 319;
|
|
||||||
if email.is_empty() || email.chars().count() > EMAIL_MAX_LENGTH {
|
|
||||||
return Err(report!(errors::ValidationError::InvalidValue {
|
|
||||||
message: "Email address is either empty or exceeds maximum allowed length".into()
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !email_regex.is_match(email) {
|
|
||||||
return Err(report!(errors::ValidationError::InvalidValue {
|
|
||||||
message: "Invalid email address format".into()
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_address(address: &serde_json::Value) -> CustomResult<(), errors::ValidationError> {
|
pub fn validate_address(address: &serde_json::Value) -> CustomResult<(), errors::ValidationError> {
|
||||||
if let Err(err) = serde_json::from_value::<AddressDetails>(address.clone()) {
|
if let Err(err) = serde_json::from_value::<AddressDetails>(address.clone()) {
|
||||||
return Err(report!(errors::ValidationError::InvalidValue {
|
return Err(report!(errors::ValidationError::InvalidValue {
|
||||||
@ -176,62 +137,3 @@ pub fn validate_address(address: &serde_json::Value) -> CustomResult<(), errors:
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use fake::{faker::internet::en::SafeEmail, Fake};
|
|
||||||
use proptest::{
|
|
||||||
prop_assert,
|
|
||||||
strategy::{Just, NewTree, Strategy},
|
|
||||||
test_runner::TestRunner,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ValidEmail;
|
|
||||||
|
|
||||||
impl Strategy for ValidEmail {
|
|
||||||
type Tree = Just<String>;
|
|
||||||
type Value = String;
|
|
||||||
|
|
||||||
fn new_tree(&self, _runner: &mut TestRunner) -> NewTree<Self> {
|
|
||||||
Ok(Just(SafeEmail().fake()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validate_email() {
|
|
||||||
let result = validate_email("abc@example.com");
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let result = validate_email("abc+123@example.com");
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let result = validate_email("");
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
proptest::proptest! {
|
|
||||||
/// Example of unit test
|
|
||||||
/// Kind of test: output-based testing
|
|
||||||
#[test]
|
|
||||||
fn proptest_valid_fake_email(email in ValidEmail) {
|
|
||||||
prop_assert!(validate_email(&email).is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Example of unit test
|
|
||||||
/// Kind of test: output-based testing
|
|
||||||
#[test]
|
|
||||||
fn proptest_invalid_data_email(email in "\\PC*") {
|
|
||||||
prop_assert!(validate_email(&email).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make maybe unit test working
|
|
||||||
// minimal failing input: email = "+@a"
|
|
||||||
// #[test]
|
|
||||||
// fn proptest_invalid_email(email in "[.+]@(.+)") {
|
|
||||||
// prop_assert!(validate_email(&email).is_err());
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user