fix: check for duplicate email and send confirmation link while updating email

This commit is contained in:
Aravinth Manivannan 2024-05-19 00:32:03 +05:30
parent 956b94f97b
commit 434436b81f
Signed by: realaravinth
GPG key ID: F8F50389936984FF
4 changed files with 130 additions and 19 deletions

View file

@ -10,10 +10,12 @@ use super::*;
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
pub struct UpdateEmailCommand {
new_email: String,
username: String,
}
impl UpdateEmailCommand {
pub fn new(
username: String,
new_email: String,
supplied_password: String,
actual_password_hash: &str,
@ -24,7 +26,10 @@ impl UpdateEmailCommand {
}
config.email(&new_email)?;
Ok(Self { new_email })
Ok(Self {
username,
new_email,
})
}
}
@ -41,6 +46,7 @@ mod tests {
let hashed_password = config.password(password).unwrap();
assert_eq!(
UpdateEmailCommand::new(
username.into(),
new_email.clone(),
password.into(),
&hashed_password,
@ -53,15 +59,27 @@ mod tests {
// email is not valid email
assert_eq!(
UpdateEmailCommand::new(username.into(), password.into(), &hashed_password, &config)
.err(),
UpdateEmailCommand::new(
username.into(),
username.into(),
password.into(),
&hashed_password,
&config
)
.err(),
Some(IdentityCommandError::BadEmail)
);
// wrong password
assert_eq!(
UpdateEmailCommand::new(username.into(), username.into(), &hashed_password, &config)
.err(),
UpdateEmailCommand::new(
username.into(),
username.into(),
username.into(),
&hashed_password,
&config
)
.err(),
Some(IdentityCommandError::WrongPassword)
);
}

View file

@ -7,11 +7,11 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd)]
pub struct EmailUpdatedEvent {
email: String,
new_email: String,
}
impl EmailUpdatedEvent {
pub fn new(email: String) -> Self {
Self { email }
pub fn new(new_email: String) -> Self {
Self { new_email }
}
}

View file

@ -9,10 +9,11 @@ pub mod service;
use super::errors::*;
#[async_trait::async_trait]
pub trait UpdateEmailUseCase {
pub trait UpdateEmailUseCase: Send + Sync {
async fn update_email(
&self,
cmd: command::UpdateEmailCommand,
//) -> errors::ProcessAuthorizationServiceResult<String>;
) -> events::EmailUpdatedEvent;
) -> IdentityResult<events::EmailUpdatedEvent>;
}
pub type UpdateEmailServiceObj = std::sync::Arc<dyn UpdateEmailUseCase>;

View file

@ -2,25 +2,70 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::*;
use derive_builder::Builder;
struct UpdateEmailService;
use super::*;
use crate::identity::application::port::output::{
db::{create_verification_secret::*, email_exists::*},
mailer::account_validation_link::*,
};
use crate::utils::random_string::*;
use crate::identity::application::services::register_user::service::SECRET_LEN;
#[derive(Builder)]
pub struct UpdateEmailService {
db_email_exists_adapter: EmailExistsOutDBPortObj,
// TODO: update email must use special PURPOSE
// TODO: User must have email_verified field
db_create_verification_secret_adapter: CreateVerificationSecretOutDBPortObj,
mailer_account_validation_link_adapter: AccountValidationLinkOutMailerPortObj,
random_string_adapter: GenerateRandomStringInterfaceObj,
}
#[async_trait::async_trait]
impl UpdateEmailUseCase for UpdateEmailService {
async fn update_email(
&self,
cmd: command::UpdateEmailCommand,
//) -> errors::ProcessAuthorizationServiceResult<String>;
) -> events::EmailUpdatedEvent {
// TODO: check if email exists in DB
events::EmailUpdatedEvent::new(cmd.new_email().into())
) -> IdentityResult<events::EmailUpdatedEvent> {
if self
.db_email_exists_adapter
.email_exists(cmd.new_email())
.await
.unwrap()
{
return Err(IdentityError::DuplicateEmail);
}
let secret = self.random_string_adapter.get_random(SECRET_LEN);
self.db_create_verification_secret_adapter
.create_verification_secret(
CreateSecretMsgBuilder::default()
.secret(secret.clone())
.username(cmd.username().into())
.build()
.unwrap(),
)
.await
.unwrap();
self.mailer_account_validation_link_adapter
.account_validation_link(cmd.new_email(), cmd.username(), &secret)
.await
.unwrap();
Ok(events::EmailUpdatedEvent::new(cmd.new_email().into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::random_string::tests::*;
use crate::tests::bdd::*;
#[actix_rt::test]
async fn test_service() {
@ -31,6 +76,7 @@ mod tests {
let hashed_password = config.password(password).unwrap();
let cmd = command::UpdateEmailCommand::new(
username.into(),
new_email.clone(),
password.into(),
&hashed_password,
@ -38,8 +84,54 @@ mod tests {
)
.unwrap();
let s = UpdateEmailService;
let res = s.update_email(cmd.clone()).await;
assert_eq!(res.email(), cmd.new_email());
// happy case
{
let s = UpdateEmailServiceBuilder::default()
.db_create_verification_secret_adapter(mock_create_verification_secret_db_port(
IS_CALLED_ONLY_ONCE,
))
.db_email_exists_adapter(mock_email_exists_db_port(
IS_CALLED_ONLY_ONCE,
RETURNS_FALSE,
))
.random_string_adapter(mock_generate_random_string(
IS_CALLED_ONLY_ONCE,
RETURNS_RANDOM_STRING.into(),
))
.mailer_account_validation_link_adapter(mock_account_validation_link_db_port(
IS_CALLED_ONLY_ONCE,
))
.build()
.unwrap();
let res = s.update_email(cmd.clone()).await.unwrap();
assert_eq!(res.new_email(), cmd.new_email());
}
// email exists
{
let s = UpdateEmailServiceBuilder::default()
.db_create_verification_secret_adapter(mock_create_verification_secret_db_port(
IS_NEVER_CALLED,
))
.db_email_exists_adapter(mock_email_exists_db_port(
IS_CALLED_ONLY_ONCE,
RETURNS_TRUE,
))
.random_string_adapter(mock_generate_random_string(
IS_NEVER_CALLED,
RETURNS_RANDOM_STRING.into(),
))
.mailer_account_validation_link_adapter(mock_account_validation_link_db_port(
IS_NEVER_CALLED,
))
.build()
.unwrap();
assert_eq!(
s.update_email(cmd.clone()).await.err(),
Some(IdentityError::DuplicateEmail)
);
}
}
}