feat: identity: register_user service

This commit is contained in:
Aravinth Manivannan 2024-05-17 23:39:58 +05:30
parent 8c136cb46c
commit 4b5f8733ce
Signed by: realaravinth
GPG key ID: F8F50389936984FF
3 changed files with 166 additions and 19 deletions

View file

@ -1,11 +1,18 @@
use derive_getters::Getters; // SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder; use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd)] #[derive(
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
)]
pub struct UserRegisteredEvent { pub struct UserRegisteredEvent {
username: String, username: String,
email: String, email: String,
hashed_password: String, hashed_password: String,
is_verified: bool, is_verified: bool,
is_admin: bool,
} }

View file

@ -7,11 +7,12 @@ pub mod error;
pub mod events; pub mod events;
pub mod service; pub mod service;
use super::errors::*;
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait RegisterUserUseCase { pub trait RegisterUserUseCase: Send + Sync {
async fn register_user( async fn register_user(
&self, &self,
cmd: command::RegisterUserCommand, cmd: command::RegisterUserCommand,
//) -> errors::ProcessAuthorizationServiceResult<String>; ) -> IdentityResult<events::UserRegisteredEvent>;
) -> events::UserRegisteredEvent;
} }

View file

@ -5,27 +5,81 @@
// - Hash password // - Hash password
// - Add user record // - Add user record
// - Send verification email // - Send verification email
// - If UID == 0; set admin // - TODO: If UID == 0; set admin
use derive_builder::Builder;
use super::*; use super::*;
use crate::identity::application::port::output::{
db::{create_verification_secret::*, email_exists::*, username_exists::*},
mailer::account_validation_link::*,
};
use crate::utils::random_string::*;
struct RegisterUserService; pub const SECRET_LEN: usize = 20;
pub const REGISTRATION_SECRET_PURPOSE: &str = "account_validation";
#[derive(Builder)]
pub struct RegisterUserService {
db_email_exists_adapter: EmailExistsOutDBPortObj,
db_username_exists_adapter: UsernameExistsOutDBPortObj,
db_create_verification_secret_adapter: CreateVerificationSecretOutDBPortObj,
mailer_account_validation_link_adapter: AccountValidationLinkOutMailerPortObj,
random_string_adapter: GenerateRandomStringInterfaceObj,
}
#[async_trait::async_trait] #[async_trait::async_trait]
impl RegisterUserUseCase for RegisterUserService { impl RegisterUserUseCase for RegisterUserService {
async fn register_user( async fn register_user(
&self, &self,
cmd: command::RegisterUserCommand, cmd: command::RegisterUserCommand,
//) -> errors::ProcessAuthorizationServiceResult<String>; ) -> IdentityResult<events::UserRegisteredEvent> {
) -> events::UserRegisteredEvent { if self
.db_username_exists_adapter
.username_exists(cmd.username())
.await
.unwrap()
{
return Err(IdentityError::UsernameExists);
}
events::UserRegisteredEventBuilder::default() if self
.db_email_exists_adapter
.email_exists(cmd.email())
.await
.unwrap()
{
return Err(IdentityError::EmailExists);
}
let secret = self.random_string_adapter.get_random(SECRET_LEN);
self.db_create_verification_secret_adapter
.create_verification_secret(
CreateSecretMsgBuilder::default()
.secret(secret.clone())
.purpose(REGISTRATION_SECRET_PURPOSE.into())
.username(cmd.username().into())
.build()
.unwrap(),
)
.await
.unwrap();
self.mailer_account_validation_link_adapter
.account_validation_link(cmd.email(), cmd.username(), &secret)
.await
.unwrap();
// TODO: send mail verification link
Ok(events::UserRegisteredEventBuilder::default()
.username(cmd.username().into()) .username(cmd.username().into())
.email(cmd.email().into()) .email(cmd.email().into())
.hashed_password(cmd.hashed_password().into()) .hashed_password(cmd.hashed_password().into())
.is_verified(false) .is_verified(false)
.build().unwrap() .is_admin(false) // TODO: if UID == 0; set true
.build()
.unwrap())
} }
} }
@ -33,6 +87,9 @@ impl RegisterUserUseCase for RegisterUserService {
mod tests { mod tests {
use super::*; use super::*;
use crate::tests::bdd::*;
use crate::utils::random_string::tests::*;
#[actix_rt::test] #[actix_rt::test]
async fn test_service() { async fn test_service() {
let username = "realaravinth"; let username = "realaravinth";
@ -45,13 +102,95 @@ mod tests {
password.into(), password.into(),
password.into(), password.into(),
&config, &config,
); )
.unwrap();
let s = RegisterUserService; // happy case
let res = s.register_user(cmd.clone()).await; {
assert_eq!(res.username(), cmd.username()); let s = RegisterUserServiceBuilder::default()
assert_eq!(res.email(), cmd.email()); .db_username_exists_adapter(mock_username_exists_db_port(
assert!(argon2_creds::Config::verify(res.hashed_password(), password).unwrap()) IS_CALLED_ONLY_ONCE,
RETURNS_FALSE,
))
.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.register_user(cmd.clone()).await.unwrap();
assert_eq!(res.username(), cmd.username());
assert_eq!(res.email(), cmd.email());
assert!(!res.is_admin());
assert!(argon2_creds::Config::verify(res.hashed_password(), password).unwrap())
}
// username exists
{
let s = RegisterUserServiceBuilder::default()
.db_username_exists_adapter(mock_username_exists_db_port(
IS_CALLED_ONLY_ONCE,
RETURNS_TRUE,
))
.db_email_exists_adapter(mock_email_exists_db_port(IS_NEVER_CALLED, RETURNS_FALSE))
.db_create_verification_secret_adapter(mock_create_verification_secret_db_port(
IS_NEVER_CALLED,
))
.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.register_user(cmd.clone()).await.err(),
Some(IdentityError::UsernameExists)
);
}
// email exists
{
let s = RegisterUserServiceBuilder::default()
.db_username_exists_adapter(mock_username_exists_db_port(
IS_CALLED_ONLY_ONCE,
RETURNS_FALSE,
))
.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.register_user(cmd.clone()).await.err(),
Some(IdentityError::EmailExists)
);
}
} }
} }