bazzar/crates/account_manager/src/db/accounts.rs

442 lines
12 KiB
Rust

use model::{AccountId, AccountState, Email, FullAccount, Login, PassHash, Role};
pub type Result<T> = std::result::Result<T, Error>;
#[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 {
pub limit: i32,
pub offset: i32,
}
impl AllAccounts {
pub async fn run(
self,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<FullAccount>> {
sqlx::query_as(
r#"
SELECT id, email, login, pass_hash, role, customer_id, state
FROM accounts
LIMIT $1 OFFSET $2
"#,
)
.bind(self.limit)
.bind(self.offset)
.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<FullAccount> {
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<PassHash>,
pub role: Role,
pub state: AccountState,
}
impl UpdateAccount {
pub async fn run(
self,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<FullAccount> {
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<FullAccount> {
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<Login>,
pub email: Option<Email>,
}
impl AccountByIdentity {
pub async fn run(
self,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<FullAccount> {
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::*;
use crate::db::Database;
pub struct NoOpts;
impl UpdateConfig for NoOpts {}
async fn test_create_account(
t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
login: Option<String>,
email: Option<String>,
hash: Option<String>,
) -> 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()
}
#[tokio::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);
}
#[tokio::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<FullAccount> = AllAccounts {
limit: 200,
offset: 0,
}
.run(&mut t)
.await
.unwrap();
testx::db_rollback!(t);
assert!(v.len() >= 3);
}
#[tokio::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);
}
#[tokio::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);
}
#[tokio::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);
}
#[tokio::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);
}
#[tokio::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);
}
}