From 434436b81f2958a368bb87406057916562d1442b Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Sun, 19 May 2024 00:32:03 +0530 Subject: [PATCH] fix: check for duplicate email and send confirmation link while updating email --- .../services/update_email/command.rs | 28 ++++- .../services/update_email/events.rs | 6 +- .../application/services/update_email/mod.rs | 5 +- .../services/update_email/service.rs | 110 ++++++++++++++++-- 4 files changed, 130 insertions(+), 19 deletions(-) diff --git a/src/identity/application/services/update_email/command.rs b/src/identity/application/services/update_email/command.rs index 4bb13ef..19e229f 100644 --- a/src/identity/application/services/update_email/command.rs +++ b/src/identity/application/services/update_email/command.rs @@ -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) ); } diff --git a/src/identity/application/services/update_email/events.rs b/src/identity/application/services/update_email/events.rs index cc6e08a..26a4f7c 100644 --- a/src/identity/application/services/update_email/events.rs +++ b/src/identity/application/services/update_email/events.rs @@ -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 } } } diff --git a/src/identity/application/services/update_email/mod.rs b/src/identity/application/services/update_email/mod.rs index 9561be2..12dde84 100644 --- a/src/identity/application/services/update_email/mod.rs +++ b/src/identity/application/services/update_email/mod.rs @@ -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; - ) -> events::EmailUpdatedEvent; + ) -> IdentityResult; } +pub type UpdateEmailServiceObj = std::sync::Arc; diff --git a/src/identity/application/services/update_email/service.rs b/src/identity/application/services/update_email/service.rs index f436679..f5090d5 100644 --- a/src/identity/application/services/update_email/service.rs +++ b/src/identity/application/services/update_email/service.rs @@ -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; - ) -> events::EmailUpdatedEvent { - // TODO: check if email exists in DB - events::EmailUpdatedEvent::new(cmd.new_email().into()) + ) -> IdentityResult { + 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) + ); + } } }