use model::{AccountId, AccountState, Email, FullAccount, Login, PassHash, Role}; pub type Result = std::result::Result; #[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] pub enum Error { #[error("Can't create account")] CantCreate, #[error("Can't find account does to lack of identity")] NoIdentity, #[error("Account does not exists")] NotExists, #[error("Failed to load all accounts")] All, #[error("Can't update account")] CantUpdate, } #[derive(Debug)] pub struct AllAccounts; impl AllAccounts { pub async fn run( _msg: AllAccounts, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result> { sqlx::query_as( r#" SELECT id, email, login, pass_hash, role, customer_id, state FROM accounts "#, ) .fetch_all(pool) .await .map_err(|e| { tracing::error!("{e:?}"); Error::All }) } } #[derive(Debug)] pub struct CreateAccount { pub email: Email, pub login: Login, pub pass_hash: PassHash, pub role: Role, } impl CreateAccount { pub async fn run( self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { sqlx::query_as( r#" INSERT INTO accounts (login, email, role, pass_hash) VALUES ($1, $2, $3, $4) RETURNING id, email, login, pass_hash, role, customer_id, state "#, ) .bind(self.login) .bind(self.email) .bind(self.role) .bind(self.pass_hash) .fetch_one(pool) .await .map_err(|e| { tracing::error!("{e:?}"); Error::CantCreate }) } } #[derive(Debug)] pub struct UpdateAccount { pub id: AccountId, pub email: Email, pub login: Login, pub pass_hash: Option, pub role: Role, pub state: AccountState, } impl UpdateAccount { pub async fn run( self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { match self.pass_hash { Some(hash) => sqlx::query_as( r#" UPDATE accounts SET login = $2, email = $3, role = $4, pass_hash = $5, state = $6 WHERE id = $1 RETURNING id, email, login, pass_hash, role, customer_id, state "#, ) .bind(self.id) .bind(self.login) .bind(self.email) .bind(self.role) .bind(hash) .bind(self.state), None => sqlx::query_as( r#" UPDATE accounts SET login = $2, email = $3, role = $4, state = $5 WHERE id = $1 RETURNING id, email, login, pass_hash, role, customer_id, state "#, ) .bind(self.id) .bind(self.login) .bind(self.email) .bind(self.role) .bind(self.state), } .fetch_one(pool) .await .map_err(|e| { tracing::error!("{e:?}"); Error::CantUpdate }) } } #[derive(Debug)] pub struct FindAccount { pub account_id: AccountId, } impl FindAccount { pub async fn run( self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { sqlx::query_as( r#" SELECT id, email, login, pass_hash, role, customer_id, state FROM accounts WHERE id = $1 "#, ) .bind(self.account_id) .fetch_one(pool) .await .map_err(|e| { tracing::error!("{e:?}"); Error::NotExists }) } } #[derive(Debug)] pub struct AccountByIdentity { pub login: Option, pub email: Option, } impl AccountByIdentity { pub async fn run( self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { match (self.login, self.email) { (Some(login), None) => sqlx::query_as( r#" SELECT id, email, login, pass_hash, role, customer_id, state FROM accounts WHERE login = $1 "#, ) .bind(login), (None, Some(email)) => sqlx::query_as( r#" SELECT id, email, login, pass_hash, role, customer_id, state FROM accounts WHERE email = $1 "#, ) .bind(email), (Some(login), Some(email)) => sqlx::query_as( r#" SELECT id, email, login, pass_hash, role, customer_id, state FROM accounts WHERE login = $1 AND email = $2 "#, ) .bind(login) .bind(email), _ => return Err(Error::NoIdentity), } .fetch_one(pool) .await .map_err(|e| { tracing::error!("{e:?}"); Error::CantCreate }) } } #[cfg(test)] mod tests { use config::UpdateConfig; use fake::Fake; use model::*; use super::*; pub struct NoOpts; impl UpdateConfig for NoOpts {} async fn test_create_account( t: &mut sqlx::Transaction<'_, sqlx::Postgres>, login: Option, email: Option, hash: Option, ) -> FullAccount { use fake::faker::internet::en; let login: String = login.unwrap_or_else(|| en::Username().fake()); let email: String = email.unwrap_or_else(|| en::FreeEmail().fake()); let hash: String = hash.unwrap_or_else(|| en::Password(10..20).fake()); CreateAccount { email: Email::new(email), login: Login::new(login), pass_hash: PassHash::new(hash), role: Role::Admin, } .run(t) .await .unwrap() } #[actix::test] async fn create_account() { testx::db_t_ref!(t); let login: String = fake::faker::internet::en::Username().fake(); let email: String = fake::faker::internet::en::FreeEmail().fake(); let hash: String = fake::faker::internet::en::Password(10..20).fake(); let account: FullAccount = CreateAccount { email: Email::new(&email), login: Login::new(&login), pass_hash: PassHash::new(&hash), role: Role::Admin, } .run(&mut t) .await .unwrap(); let expected = FullAccount { login: Login::new(login), email: Email::new(email), pass_hash: PassHash::new(&hash), role: Role::Admin, customer_id: account.customer_id, id: account.id, state: AccountState::Active, }; t.rollback().await.unwrap(); assert_eq!(account, expected); } #[actix::test] async fn all_accounts() { testx::db_t_ref!(t); test_create_account(&mut t, None, None, None).await; test_create_account(&mut t, None, None, None).await; test_create_account(&mut t, None, None, None).await; let v: Vec = AllAccounts.run(&mut t).await.unwrap(); testx::db_rollback!(t); assert!(v.len() >= 3); } #[actix::test] async fn update_account_without_pass() { testx::db_t_ref!(t); let original_login: String = fake::faker::internet::en::Username().fake(); let original_email: String = fake::faker::internet::en::FreeEmail().fake(); let original_hash: String = fake::faker::internet::en::Password(10..20).fake(); let original_account = test_create_account( &mut t, Some(original_login.clone()), Some(original_email.clone()), Some(original_hash.clone()), ) .await; let updated_login: String = fake::faker::internet::en::Username().fake(); let updated_email: String = fake::faker::internet::en::FreeEmail().fake(); let updated_account: FullAccount = UpdateAccount { id: original_account.id, email: Email::new(updated_email.clone()), login: Login::new(updated_login.clone()), pass_hash: None, role: Role::Admin, state: AccountState::Active, } .run(&mut t) .await .unwrap(); let expected = FullAccount { id: original_account.id, email: Email::new(updated_email), login: Login::new(updated_login), pass_hash: PassHash::new(original_hash), role: Role::Admin, customer_id: original_account.customer_id, state: AccountState::Active, }; testx::db_rollback!(t); assert_ne!(original_account, expected); assert_eq!(updated_account, expected); } #[actix::test] async fn update_account_with_pass() { testx::db_t_ref!(t); let original_login: String = fake::faker::internet::en::Username().fake(); let original_email: String = fake::faker::internet::en::FreeEmail().fake(); let original_hash: String = fake::faker::internet::en::Password(10..20).fake(); let original_account = test_create_account( &mut t, Some(original_login.clone()), Some(original_email.clone()), Some(original_hash.clone()), ) .await; let updated_login: String = fake::faker::internet::en::Username().fake(); let updated_email: String = fake::faker::internet::en::FreeEmail().fake(); let updated_hash: String = fake::faker::internet::en::Password(10..20).fake(); let updated_account: FullAccount = UpdateAccount { id: original_account.id, email: Email::new(updated_email.clone()), login: Login::new(updated_login.clone()), pass_hash: Some(PassHash::new(updated_hash.clone())), role: Role::Admin, state: AccountState::Active, } .run(&mut t) .await .unwrap(); let expected = FullAccount { id: original_account.id, email: Email::new(updated_email), login: Login::new(updated_login), pass_hash: PassHash::new(updated_hash), role: Role::Admin, customer_id: original_account.customer_id, state: AccountState::Active, }; testx::db_rollback!(t); assert_ne!(original_account, expected); assert_eq!(updated_account, expected); } #[actix::test] async fn find() { testx::db_t_ref!(t); let account = test_create_account(&mut t, None, None, None).await; let res: FullAccount = FindAccount { account_id: account.id, } .run(&mut t) .await .unwrap(); testx::db_rollback!(t); assert_eq!(account, res); } #[actix::test] async fn find_identity_email() { testx::db_t_ref!(t); let account = test_create_account(&mut t, None, None, None).await; let res: FullAccount = AccountByIdentity { email: Some(account.email.clone()), login: None, } .run(&mut t) .await .unwrap(); testx::db_rollback!(t); assert_eq!(account, res); } #[actix::test] async fn find_identity_login() { testx::db_t_ref!(t); let account = test_create_account(&mut t, None, None, None).await; let res: FullAccount = AccountByIdentity { login: Some(account.login.clone()), email: None, } .run(&mut t) .await .unwrap(); testx::db_rollback!(t); assert_eq!(account, res); } }