#[cfg(feature = "dummy")] use fake::Fake; use model::*; use crate::{db_async_handler, Result}; #[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)] #[serde(rename_all = "kebab-case")] 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(actix::Message)] #[rtype(result = "Result>")] pub struct AllAccounts; db_async_handler!( AllAccounts, all_accounts, Vec, inner_all_accounts ); pub(crate) async fn all_accounts( _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:?}"); super::Error::Account(Error::All) }) } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[derive(actix::Message, Debug)] #[rtype(result = "Result")] pub struct CreateAccount { pub email: Email, pub login: Login, pub pass_hash: PassHash, pub role: Role, } db_async_handler!( CreateAccount, create_account, FullAccount, inner_create_account ); pub async fn create_account( msg: CreateAccount, 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(msg.login) .bind(msg.email) .bind(msg.role) .bind(msg.pass_hash) .fetch_one(pool) .await .map_err(|e| { tracing::error!("{e:?}"); super::Error::Account(Error::CantCreate) }) } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[derive(actix::Message)] #[rtype(result = "Result")] pub struct UpdateAccount { pub id: AccountId, pub email: Email, pub login: Login, pub pass_hash: Option, pub role: Role, pub state: AccountState, } db_async_handler!( UpdateAccount, update_account, FullAccount, inner_update_account ); pub(crate) async fn update_account( msg: UpdateAccount, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { match msg.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(msg.id) .bind(msg.login) .bind(msg.email) .bind(msg.role) .bind(hash) .bind(msg.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(msg.id) .bind(msg.login) .bind(msg.email) .bind(msg.role) .bind(msg.state), } .fetch_one(pool) .await .map_err(|e| { tracing::error!("{e:?}"); super::Error::Account(Error::CantUpdate) }) } #[derive(actix::Message)] #[rtype(result = "Result")] pub struct FindAccount { pub account_id: AccountId, } db_async_handler!(FindAccount, find_account, FullAccount, inner_find_account); pub(crate) async fn find_account( msg: FindAccount, 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(msg.account_id) .fetch_one(pool) .await .map_err(|e| { tracing::error!("{e:?}"); super::Error::Account(Error::NotExists) }) } #[derive(actix::Message)] #[rtype(result = "Result")] pub struct AccountByIdentity { pub login: Option, pub email: Option, } db_async_handler!( AccountByIdentity, account_by_identity, FullAccount, inner_account_by_identity ); pub(crate) async fn account_by_identity( msg: AccountByIdentity, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { match (msg.login, msg.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(super::Error::Account(Error::NoIdentity)), } .fetch_one(pool) .await .map_err(|e| { tracing::error!("{e:?}"); super::Error::Account(Error::CantCreate) }) } #[cfg(test)] mod tests { use config::UpdateConfig; use fake::Fake; use model::*; use crate::*; 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()); super::create_account( CreateAccount { email: Email::new(email), login: Login::new(login), pass_hash: PassHash::new(hash), role: Role::Admin, }, 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 = super::create_account( CreateAccount { email: Email::new(&email), login: Login::new(&login), pass_hash: PassHash::new(&hash), role: Role::Admin, }, &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 = super::all_accounts(AllAccounts, &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 = super::update_account( 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, }, &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 = super::update_account( 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, }, &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 = super::find_account( FindAccount { account_id: account.id, }, &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 = super::account_by_identity( AccountByIdentity { email: Some(account.email.clone()), login: None, }, &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 = super::account_by_identity( AccountByIdentity { login: Some(account.login.clone()), email: None, }, &mut t, ) .await .unwrap(); testx::db_rollback!(t); assert_eq!(account, res); } }