#[cfg(feature = "dummy")] use fake::Fake; use model::{AccountId, AccountState, Email, FullAccount, Login, PassHash, Role}; use sqlx::PgPool; use crate::{db_async_handler, Result}; #[derive(Debug, Copy, Clone, 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); pub(crate) async fn all_accounts(_msg: AllAccounts, pool: PgPool) -> Result> { sqlx::query_as( r#" SELECT id, email, login, pass_hash, role, customer_id, state FROM accounts "#, ) .fetch_all(&pool) .await .map_err(|e| { log::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); pub(crate) async fn create_account(msg: CreateAccount, db: PgPool) -> 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(&db) .await .map_err(|e| { log::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); pub(crate) async fn update_account(msg: UpdateAccount, db: PgPool) -> 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(&db) .await .map_err(|e| { log::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); pub(crate) async fn find_account(msg: FindAccount, db: PgPool) -> 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(&db) .await .map_err(|e| { log::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); pub(crate) async fn account_by_identity(msg: AccountByIdentity, db: PgPool) -> 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(&db) .await .map_err(|e| { log::error!("{e:?}"); super::Error::Account(Error::CantCreate) }) } #[cfg(test)] mod tests { use actix::Addr; use config::UpdateConfig; use fake::Fake; use model::*; use crate::*; pub struct NoOpts; impl UpdateConfig for NoOpts {} async fn test_create_account( db: Addr, 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()); db.send(CreateAccount { email: Email::new(email), login: Login::new(login), pass_hash: PassHash::new(hash), role: Role::Admin, }) .await .unwrap() .unwrap() } #[actix::test] async fn create_account() { let config = config::default_load(&mut NoOpts); config .lock() .database_mut() .set_url("postgres://postgres@localhost/bazzar_test"); let db = Database::build(config).await.start(); 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 = db .send(CreateAccount { email: Email::new(&email), login: Login::new(&login), pass_hash: PassHash::new(&hash), role: Role::Admin, }) .await .unwrap() .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, }; assert_eq!(account, expected); } #[actix::test] async fn all_accounts() { let config = config::default_load(&mut NoOpts); config .lock() .database_mut() .set_url("postgres://postgres@localhost/bazzar_test"); let db = Database::build(config).await.start(); test_create_account(db.clone(), None, None, None).await; test_create_account(db.clone(), None, None, None).await; test_create_account(db.clone(), None, None, None).await; let v: Vec = db.send(AllAccounts).await.unwrap().unwrap(); assert!(v.len() >= 3); } #[actix::test] async fn update_account_without_pass() { let config = config::default_load(&mut NoOpts); config .lock() .database_mut() .set_url("postgres://postgres@localhost/bazzar_test"); let db = Database::build(config).await.start(); 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( db.clone(), 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 = db .send(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, }) .await .unwrap() .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, }; assert_ne!(original_account, expected); assert_eq!(updated_account, expected); } #[actix::test] async fn update_account_with_pass() { let config = config::default_load(&mut NoOpts); config .lock() .database_mut() .set_url("postgres://postgres@localhost/bazzar_test"); let db = Database::build(config).await.start(); 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( db.clone(), 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 = db .send(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, }) .await .unwrap() .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, }; assert_ne!(original_account, expected); assert_eq!(updated_account, expected); } #[actix::test] async fn find() { let config = config::default_load(&mut NoOpts); config .lock() .database_mut() .set_url("postgres://postgres@localhost/bazzar_test"); let db = Database::build(config).await.start(); let account = test_create_account(db.clone(), None, None, None).await; let res: FullAccount = db .send(FindAccount { account_id: account.id, }) .await .unwrap() .unwrap(); assert_eq!(account, res); } #[actix::test] async fn find_identity_email() { let config = config::default_load(&mut NoOpts); config .lock() .database_mut() .set_url("postgres://postgres@localhost/bazzar_test"); let db = Database::build(config).await.start(); let account = test_create_account(db.clone(), None, None, None).await; let res: FullAccount = db .send(AccountByIdentity { email: Some(account.email.clone()), login: None, }) .await .unwrap() .unwrap(); assert_eq!(account, res); } #[actix::test] async fn find_identity_login() { let config = config::default_load(&mut NoOpts); config .lock() .database_mut() .set_url("postgres://postgres@localhost/bazzar_test"); let db = Database::build(config).await.start(); let account = test_create_account(db.clone(), None, None, None).await; let res: FullAccount = db .send(AccountByIdentity { login: Some(account.login.clone()), email: None, }) .await .unwrap() .unwrap(); assert_eq!(account, res); } }