Fix creating address, move to transactional tests

This commit is contained in:
eraden 2022-06-05 17:44:47 +02:00
parent 93c66d33bf
commit cf464beb27
5 changed files with 352 additions and 156 deletions

View File

@ -70,8 +70,6 @@ WHERE account_id = $1 AND id = $2
.map_err(|_| Error::AccountAddresses.into()) .map_err(|_| Error::AccountAddresses.into())
} }
/////
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<model::AccountAddress>")] #[rtype(result = "Result<model::AccountAddress>")]
pub struct DefaultAccountAddress { pub struct DefaultAccountAddress {
@ -139,14 +137,15 @@ WHERE account_id = $1
.fetch_all(&mut *pool) .fetch_all(&mut *pool)
.await .await
{ {
log::error!("{}", e); log::error!("{e}");
eprintln!("{e}")
} }
} }
sqlx::query_as( sqlx::query_as(
r#" r#"
INSERT INTO account_addresses ( name, email, phone, street, city, country, zip, account_id ) INSERT INTO account_addresses ( name, email, phone, street, city, country, zip, account_id, is_default)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id, name, email, phone, street, city, country, zip, account_id, is_default RETURNING id, name, email, phone, street, city, country, zip, account_id, is_default
"#, "#,
) )
@ -161,7 +160,11 @@ RETURNING id, name, email, phone, street, city, country, zip, account_id, is_def
.bind(msg.is_default) .bind(msg.is_default)
.fetch_one(pool) .fetch_one(pool)
.await .await
.map_err(|_| Error::CreateAccountAddress.into()) .map_err(|e| {
log::error!("{e}");
eprintln!("{e}");
Error::CreateAccountAddress.into()
})
} }
#[derive(actix::Message)] #[derive(actix::Message)]
@ -215,7 +218,6 @@ RETURNING id, name, email, phone, street, city, country, zip, account_id, is_def
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use actix::{Actor, Addr};
use config::*; use config::*;
use fake::Fake; use fake::Fake;
use model::*; use model::*;
@ -226,20 +228,22 @@ mod test {
impl UpdateConfig for NoOpts {} impl UpdateConfig for NoOpts {}
async fn test_create_account(db: Addr<Database>) -> FullAccount { async fn test_create_account(pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> FullAccount {
let login: String = fake::faker::internet::en::Username().fake(); let login: String = fake::faker::internet::en::Username().fake();
let email: String = fake::faker::internet::en::FreeEmail().fake(); let email: String = fake::faker::internet::en::FreeEmail().fake();
let hash: String = fake::faker::internet::en::Password(10..20).fake(); let hash: String = fake::faker::internet::en::Password(10..20).fake();
db.send(CreateAccount { crate::create_account(
email: Email::new(email), CreateAccount {
login: Login::new(login), email: Email::new(email),
pass_hash: PassHash::new(hash), login: Login::new(login),
role: Role::Admin, pass_hash: PassHash::new(hash),
}) role: Role::Admin,
},
pool,
)
.await .await
.unwrap() .unwrap()
.unwrap()
} }
#[actix::test] #[actix::test]
@ -250,10 +254,12 @@ mod test {
.database_mut() .database_mut()
.set_url("postgres://postgres@localhost/bazzar_test"); .set_url("postgres://postgres@localhost/bazzar_test");
let db = Database::build(config).await.start(); let db = Database::build(config).await;
let pool = db.pool();
let mut t = pool.begin().await.unwrap();
// account // account
let account = test_create_account(db.clone()).await; let account = test_create_account(&mut t).await;
// address // address
let mut address: AccountAddress = { let mut address: AccountAddress = {
@ -265,35 +271,36 @@ mod test {
let country: String = fake::faker::address::en::CountryName().fake(); let country: String = fake::faker::address::en::CountryName().fake();
let zip: String = fake::faker::address::en::ZipCode().fake(); let zip: String = fake::faker::address::en::ZipCode().fake();
let account_id = Some(account.id); let account_id = Some(account.id);
let is_default: bool = fake::faker::boolean::en::Boolean(6).fake(); let is_default: bool = true;
let address = db let address = super::create_address(
.send(CreateAccountAddress { CreateAccountAddress {
name: model::Name::new(name.clone()), name: Name::new(name.clone()),
email: model::Email::new(email.clone()), email: Email::new(email.clone()),
phone: model::Phone::new(phone.clone()), phone: Phone::new(phone.clone()),
street: model::Street::new(street.clone()), street: Street::new(street.clone()),
city: model::City::new(city.clone()), city: City::new(city.clone()),
country: model::Country::new(country.clone()), country: Country::new(country.clone()),
zip: model::Zip::new(zip.clone()), zip: Zip::new(zip.clone()),
account_id: account_id.clone(), account_id: account_id.clone(),
is_default: is_default.clone(), is_default,
}) },
.await &mut t,
.unwrap() )
.unwrap(); .await
.unwrap();
assert_eq!( assert_eq!(
address, address,
model::AccountAddress { AccountAddress {
id: address.id, id: address.id,
name: model::Name::new(name.clone()), name: Name::new(name.clone()),
email: model::Email::new(email.clone()), email: Email::new(email.clone()),
phone: model::Phone::new(phone.clone()), phone: Phone::new(phone.clone()),
street: model::Street::new(street.clone()), street: Street::new(street.clone()),
city: model::City::new(city.clone()), city: City::new(city.clone()),
country: model::Country::new(country.clone()), country: Country::new(country.clone()),
zip: model::Zip::new(zip.clone()), zip: Zip::new(zip.clone()),
account_id: account.id, account_id: account.id,
is_default, is_default,
} }
@ -301,19 +308,20 @@ mod test {
address address
}; };
let found = db let found = super::find_account_address(
.send(FindAccountAddress { FindAccountAddress {
account_id: account.id, account_id: account.id,
address_id: address.id, address_id: address.id,
}) },
.await &mut t,
.unwrap() )
.unwrap(); .await
.unwrap();
assert_eq!(found, address); assert_eq!(found, address);
let changed = db let changed = super::update_account_address(
.send(UpdateAccountAddress { UpdateAccountAddress {
id: address.id, id: address.id,
name: address.name.clone(), name: address.name.clone(),
email: address.email.clone(), email: address.email.clone(),
@ -324,23 +332,26 @@ mod test {
zip: address.zip.clone(), zip: address.zip.clone(),
account_id: address.account_id.clone(), account_id: address.account_id.clone(),
is_default: true, is_default: true,
}) },
.await &mut t,
.unwrap() )
.unwrap(); .await
.unwrap();
address.is_default = true; address.is_default = true;
assert_eq!(changed, address); assert_eq!(changed, address);
let default_address = db let default_address = super::default_account_address(
.send(DefaultAccountAddress { DefaultAccountAddress {
account_id: account.id, account_id: account.id,
}) },
.await &mut t,
.unwrap() )
.unwrap(); .await
.unwrap();
t.rollback().await.unwrap();
assert_eq!(default_address, address); assert_eq!(default_address, address);
} }
} }

View File

@ -1,7 +1,6 @@
#[cfg(feature = "dummy")] #[cfg(feature = "dummy")]
use fake::Fake; use fake::Fake;
use model::{AccountId, AccountState, Email, FullAccount, Login, PassHash, Role}; use model::*;
use sqlx::PgPool;
use crate::{db_async_handler, Result}; use crate::{db_async_handler, Result};
@ -24,16 +23,24 @@ pub enum Error {
#[rtype(result = "Result<Vec<FullAccount>>")] #[rtype(result = "Result<Vec<FullAccount>>")]
pub struct AllAccounts; pub struct AllAccounts;
db_async_handler!(AllAccounts, all_accounts, Vec<FullAccount>); db_async_handler!(
AllAccounts,
all_accounts,
Vec<FullAccount>,
inner_all_accounts
);
pub(crate) async fn all_accounts(_msg: AllAccounts, pool: PgPool) -> Result<Vec<FullAccount>> { pub(crate) async fn all_accounts(
_msg: AllAccounts,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<FullAccount>> {
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT id, email, login, pass_hash, role, customer_id, state SELECT id, email, login, pass_hash, role, customer_id, state
FROM accounts FROM accounts
"#, "#,
) )
.fetch_all(&pool) .fetch_all(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -51,9 +58,17 @@ pub struct CreateAccount {
pub role: Role, pub role: Role,
} }
db_async_handler!(CreateAccount, create_account, FullAccount); db_async_handler!(
CreateAccount,
create_account,
FullAccount,
inner_create_account
);
pub(crate) async fn create_account(msg: CreateAccount, db: PgPool) -> Result<FullAccount> { pub(crate) async fn create_account(
msg: CreateAccount,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<FullAccount> {
sqlx::query_as( sqlx::query_as(
r#" r#"
INSERT INTO accounts (login, email, role, pass_hash) INSERT INTO accounts (login, email, role, pass_hash)
@ -65,7 +80,7 @@ RETURNING id, email, login, pass_hash, role, customer_id, state
.bind(msg.email) .bind(msg.email)
.bind(msg.role) .bind(msg.role)
.bind(msg.pass_hash) .bind(msg.pass_hash)
.fetch_one(&db) .fetch_one(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -85,9 +100,17 @@ pub struct UpdateAccount {
pub state: AccountState, pub state: AccountState,
} }
db_async_handler!(UpdateAccount, update_account, FullAccount); db_async_handler!(
UpdateAccount,
update_account,
FullAccount,
inner_update_account
);
pub(crate) async fn update_account(msg: UpdateAccount, db: PgPool) -> Result<FullAccount> { pub(crate) async fn update_account(
msg: UpdateAccount,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<FullAccount> {
match msg.pass_hash { match msg.pass_hash {
Some(hash) => sqlx::query_as( Some(hash) => sqlx::query_as(
r#" r#"
@ -117,7 +140,7 @@ RETURNING id, email, login, pass_hash, role, customer_id, state
.bind(msg.role) .bind(msg.role)
.bind(msg.state), .bind(msg.state),
} }
.fetch_one(&db) .fetch_one(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -131,9 +154,12 @@ pub struct FindAccount {
pub account_id: AccountId, pub account_id: AccountId,
} }
db_async_handler!(FindAccount, find_account, FullAccount); db_async_handler!(FindAccount, find_account, FullAccount, inner_find_account);
pub(crate) async fn find_account(msg: FindAccount, db: PgPool) -> Result<FullAccount> { pub(crate) async fn find_account(
msg: FindAccount,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<FullAccount> {
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT id, email, login, pass_hash, role, customer_id, state SELECT id, email, login, pass_hash, role, customer_id, state
@ -142,7 +168,7 @@ WHERE id = $1
"#, "#,
) )
.bind(msg.account_id) .bind(msg.account_id)
.fetch_one(&db) .fetch_one(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -157,9 +183,17 @@ pub struct AccountByIdentity {
pub email: Option<Email>, pub email: Option<Email>,
} }
db_async_handler!(AccountByIdentity, account_by_identity, FullAccount); db_async_handler!(
AccountByIdentity,
account_by_identity,
FullAccount,
inner_account_by_identity
);
pub(crate) async fn account_by_identity(msg: AccountByIdentity, db: PgPool) -> Result<FullAccount> { pub(crate) async fn account_by_identity(
msg: AccountByIdentity,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<FullAccount> {
match (msg.login, msg.email) { match (msg.login, msg.email) {
(Some(login), None) => sqlx::query_as( (Some(login), None) => sqlx::query_as(
r#" r#"
@ -188,7 +222,7 @@ WHERE login = $1 AND email = $2
.bind(email), .bind(email),
_ => return Err(super::Error::Account(Error::NoIdentity)), _ => return Err(super::Error::Account(Error::NoIdentity)),
} }
.fetch_one(&db) .fetch_one(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -198,7 +232,6 @@ WHERE login = $1 AND email = $2
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix::Addr;
use config::UpdateConfig; use config::UpdateConfig;
use fake::Fake; use fake::Fake;
use model::*; use model::*;
@ -210,7 +243,7 @@ mod tests {
impl UpdateConfig for NoOpts {} impl UpdateConfig for NoOpts {}
async fn test_create_account( async fn test_create_account(
db: Addr<Database>, t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
login: Option<String>, login: Option<String>,
email: Option<String>, email: Option<String>,
hash: Option<String>, hash: Option<String>,
@ -220,15 +253,17 @@ mod tests {
let email: String = email.unwrap_or_else(|| en::FreeEmail().fake()); let email: String = email.unwrap_or_else(|| en::FreeEmail().fake());
let hash: String = hash.unwrap_or_else(|| en::Password(10..20).fake()); let hash: String = hash.unwrap_or_else(|| en::Password(10..20).fake());
db.send(CreateAccount { super::create_account(
email: Email::new(email), CreateAccount {
login: Login::new(login), email: Email::new(email),
pass_hash: PassHash::new(hash), login: Login::new(login),
role: Role::Admin, pass_hash: PassHash::new(hash),
}) role: Role::Admin,
},
t,
)
.await .await
.unwrap() .unwrap()
.unwrap()
} }
#[actix::test] #[actix::test]
@ -239,22 +274,25 @@ mod tests {
.database_mut() .database_mut()
.set_url("postgres://postgres@localhost/bazzar_test"); .set_url("postgres://postgres@localhost/bazzar_test");
let db = Database::build(config).await.start(); let db = Database::build(config).await;
let pool = db.pool();
let mut t = pool.begin().await.unwrap();
let login: String = fake::faker::internet::en::Username().fake(); let login: String = fake::faker::internet::en::Username().fake();
let email: String = fake::faker::internet::en::FreeEmail().fake(); let email: String = fake::faker::internet::en::FreeEmail().fake();
let hash: String = fake::faker::internet::en::Password(10..20).fake(); let hash: String = fake::faker::internet::en::Password(10..20).fake();
let account: FullAccount = db let account: FullAccount = super::create_account(
.send(CreateAccount { CreateAccount {
email: Email::new(&email), email: Email::new(&email),
login: Login::new(&login), login: Login::new(&login),
pass_hash: PassHash::new(&hash), pass_hash: PassHash::new(&hash),
role: Role::Admin, role: Role::Admin,
}) },
.await &mut t,
.unwrap() )
.unwrap(); .await
.unwrap();
let expected = FullAccount { let expected = FullAccount {
login: Login::new(login), login: Login::new(login),
@ -266,6 +304,7 @@ mod tests {
state: AccountState::Active, state: AccountState::Active,
}; };
t.rollback().await.unwrap();
assert_eq!(account, expected); assert_eq!(account, expected);
} }
@ -277,14 +316,17 @@ mod tests {
.database_mut() .database_mut()
.set_url("postgres://postgres@localhost/bazzar_test"); .set_url("postgres://postgres@localhost/bazzar_test");
let db = Database::build(config).await.start(); let db = Database::build(config).await;
let pool = db.pool();
let mut t = pool.begin().await.unwrap();
test_create_account(db.clone(), None, None, None).await; test_create_account(&mut t, None, None, None).await;
test_create_account(db.clone(), None, None, None).await; test_create_account(&mut t, None, None, None).await;
test_create_account(db.clone(), None, None, None).await; test_create_account(&mut t, None, None, None).await;
let v: Vec<FullAccount> = db.send(AllAccounts).await.unwrap().unwrap(); let v: Vec<FullAccount> = super::all_accounts(AllAccounts, &mut t).await.unwrap();
assert!(v.len() >= 3); assert!(v.len() >= 3);
t.rollback().await.unwrap();
} }
#[actix::test] #[actix::test]
@ -295,14 +337,16 @@ mod tests {
.database_mut() .database_mut()
.set_url("postgres://postgres@localhost/bazzar_test"); .set_url("postgres://postgres@localhost/bazzar_test");
let db = Database::build(config).await.start(); let db = Database::build(config).await;
let pool = db.pool();
let mut t = pool.begin().await.unwrap();
let original_login: String = fake::faker::internet::en::Username().fake(); let original_login: String = fake::faker::internet::en::Username().fake();
let original_email: String = fake::faker::internet::en::FreeEmail().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_hash: String = fake::faker::internet::en::Password(10..20).fake();
let original_account = test_create_account( let original_account = test_create_account(
db.clone(), &mut t,
Some(original_login.clone()), Some(original_login.clone()),
Some(original_email.clone()), Some(original_email.clone()),
Some(original_hash.clone()), Some(original_hash.clone()),
@ -312,18 +356,19 @@ mod tests {
let updated_login: String = fake::faker::internet::en::Username().fake(); let updated_login: String = fake::faker::internet::en::Username().fake();
let updated_email: String = fake::faker::internet::en::FreeEmail().fake(); let updated_email: String = fake::faker::internet::en::FreeEmail().fake();
let updated_account: FullAccount = db let updated_account: FullAccount = super::update_account(
.send(UpdateAccount { UpdateAccount {
id: original_account.id, id: original_account.id,
email: Email::new(updated_email.clone()), email: Email::new(updated_email.clone()),
login: Login::new(updated_login.clone()), login: Login::new(updated_login.clone()),
pass_hash: None, pass_hash: None,
role: Role::Admin, role: Role::Admin,
state: AccountState::Active, state: AccountState::Active,
}) },
.await &mut t,
.unwrap() )
.unwrap(); .await
.unwrap();
let expected = FullAccount { let expected = FullAccount {
id: original_account.id, id: original_account.id,
@ -335,6 +380,7 @@ mod tests {
state: AccountState::Active, state: AccountState::Active,
}; };
t.rollback().await.unwrap();
assert_ne!(original_account, expected); assert_ne!(original_account, expected);
assert_eq!(updated_account, expected); assert_eq!(updated_account, expected);
} }
@ -347,14 +393,16 @@ mod tests {
.database_mut() .database_mut()
.set_url("postgres://postgres@localhost/bazzar_test"); .set_url("postgres://postgres@localhost/bazzar_test");
let db = Database::build(config).await.start(); let db = Database::build(config).await;
let pool = db.pool();
let mut t = pool.begin().await.unwrap();
let original_login: String = fake::faker::internet::en::Username().fake(); let original_login: String = fake::faker::internet::en::Username().fake();
let original_email: String = fake::faker::internet::en::FreeEmail().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_hash: String = fake::faker::internet::en::Password(10..20).fake();
let original_account = test_create_account( let original_account = test_create_account(
db.clone(), &mut t,
Some(original_login.clone()), Some(original_login.clone()),
Some(original_email.clone()), Some(original_email.clone()),
Some(original_hash.clone()), Some(original_hash.clone()),
@ -365,18 +413,19 @@ mod tests {
let updated_email: String = fake::faker::internet::en::FreeEmail().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_hash: String = fake::faker::internet::en::Password(10..20).fake();
let updated_account: FullAccount = db let updated_account: FullAccount = super::update_account(
.send(UpdateAccount { UpdateAccount {
id: original_account.id, id: original_account.id,
email: Email::new(updated_email.clone()), email: Email::new(updated_email.clone()),
login: Login::new(updated_login.clone()), login: Login::new(updated_login.clone()),
pass_hash: Some(PassHash::new(updated_hash.clone())), pass_hash: Some(PassHash::new(updated_hash.clone())),
role: Role::Admin, role: Role::Admin,
state: AccountState::Active, state: AccountState::Active,
}) },
.await &mut t,
.unwrap() )
.unwrap(); .await
.unwrap();
let expected = FullAccount { let expected = FullAccount {
id: original_account.id, id: original_account.id,
@ -388,6 +437,7 @@ mod tests {
state: AccountState::Active, state: AccountState::Active,
}; };
t.rollback().await.unwrap();
assert_ne!(original_account, expected); assert_ne!(original_account, expected);
assert_eq!(updated_account, expected); assert_eq!(updated_account, expected);
} }
@ -400,18 +450,22 @@ mod tests {
.database_mut() .database_mut()
.set_url("postgres://postgres@localhost/bazzar_test"); .set_url("postgres://postgres@localhost/bazzar_test");
let db = Database::build(config).await.start(); let db = Database::build(config).await;
let pool = db.pool();
let mut t = pool.begin().await.unwrap();
let account = test_create_account(db.clone(), None, None, None).await; let account = test_create_account(&mut t, None, None, None).await;
let res: FullAccount = db let res: FullAccount = super::find_account(
.send(FindAccount { FindAccount {
account_id: account.id, account_id: account.id,
}) },
.await &mut t,
.unwrap() )
.unwrap(); .await
.unwrap();
t.rollback().await.unwrap();
assert_eq!(account, res); assert_eq!(account, res);
} }
@ -423,19 +477,23 @@ mod tests {
.database_mut() .database_mut()
.set_url("postgres://postgres@localhost/bazzar_test"); .set_url("postgres://postgres@localhost/bazzar_test");
let db = Database::build(config).await.start(); let db = Database::build(config).await;
let pool = db.pool();
let mut t = pool.begin().await.unwrap();
let account = test_create_account(db.clone(), None, None, None).await; let account = test_create_account(&mut t, None, None, None).await;
let res: FullAccount = db let res: FullAccount = super::account_by_identity(
.send(AccountByIdentity { AccountByIdentity {
email: Some(account.email.clone()), email: Some(account.email.clone()),
login: None, login: None,
}) },
.await &mut t,
.unwrap() )
.unwrap(); .await
.unwrap();
t.rollback().await.unwrap();
assert_eq!(account, res); assert_eq!(account, res);
} }
@ -447,19 +505,23 @@ mod tests {
.database_mut() .database_mut()
.set_url("postgres://postgres@localhost/bazzar_test"); .set_url("postgres://postgres@localhost/bazzar_test");
let db = Database::build(config).await.start(); let db = Database::build(config).await;
let pool = db.pool();
let mut t = pool.begin().await.unwrap();
let account = test_create_account(db.clone(), None, None, None).await; let account = test_create_account(&mut t, None, None, None).await;
let res: FullAccount = db let res: FullAccount = super::account_by_identity(
.send(AccountByIdentity { AccountByIdentity {
login: Some(account.login.clone()), login: Some(account.login.clone()),
email: None, email: None,
}) },
.await &mut t,
.unwrap() )
.unwrap(); .await
.unwrap();
t.rollback().await.unwrap();
assert_eq!(account, res); assert_eq!(account, res);
} }
} }

View File

@ -133,6 +133,7 @@ RETURNING id,
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
eprintln!("{e:?}");
crate::Error::Product(Error::Create) crate::Error::Product(Error::Create)
}) })
} }

View File

@ -1,6 +1,5 @@
use actix::Message; use actix::Message;
use model::{ProductId, Quantity, QuantityUnit, Stock, StockId}; use model::{ProductId, Quantity, QuantityUnit, Stock, StockId};
use sqlx::PgPool;
use crate::{MultiLoad, Result}; use crate::{MultiLoad, Result};
@ -22,16 +21,19 @@ pub enum Error {
#[rtype(result = "Result<Vec<model::Stock>>")] #[rtype(result = "Result<Vec<model::Stock>>")]
pub struct AllStocks; pub struct AllStocks;
crate::db_async_handler!(AllStocks, all_stocks, Vec<Stock>); crate::db_async_handler!(AllStocks, all_stocks, Vec<Stock>, inner_all_stocks);
async fn all_stocks(_msg: AllStocks, pool: PgPool) -> Result<Vec<model::Stock>> { async fn all_stocks(
_msg: AllStocks,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<Stock>> {
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT id, product_id, quantity, quantity_unit SELECT id, product_id, quantity, quantity_unit
FROM stocks FROM stocks
"#, "#,
) )
.fetch_all(&pool) .fetch_all(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -47,9 +49,12 @@ pub struct CreateStock {
pub quantity_unit: QuantityUnit, pub quantity_unit: QuantityUnit,
} }
crate::db_async_handler!(CreateStock, create_stock, Stock); crate::db_async_handler!(CreateStock, create_stock, Stock, inner_create_stock);
async fn create_stock(msg: CreateStock, pool: PgPool) -> Result<model::Stock> { async fn create_stock(
msg: CreateStock,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Stock> {
sqlx::query_as( sqlx::query_as(
r#" r#"
INSERT INTO stocks (product_id, quantity, quantity_unit) INSERT INTO stocks (product_id, quantity, quantity_unit)
@ -60,10 +65,11 @@ RETURNING id, product_id, quantity, quantity_unit
.bind(msg.product_id) .bind(msg.product_id)
.bind(msg.quantity) .bind(msg.quantity)
.bind(msg.quantity_unit) .bind(msg.quantity_unit)
.fetch_one(&pool) .fetch_one(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
eprintln!("{e:?}");
crate::Error::Stock(Error::Create) crate::Error::Stock(Error::Create)
}) })
} }
@ -77,9 +83,12 @@ pub struct UpdateStock {
pub quantity_unit: QuantityUnit, pub quantity_unit: QuantityUnit,
} }
crate::db_async_handler!(UpdateStock, update_stock, Stock); crate::db_async_handler!(UpdateStock, update_stock, Stock, inner_update_stock);
async fn update_stock(msg: UpdateStock, pool: PgPool) -> Result<model::Stock> { async fn update_stock(
msg: UpdateStock,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Stock> {
sqlx::query_as( sqlx::query_as(
r#" r#"
UPDATE stocks UPDATE stocks
@ -94,7 +103,7 @@ RETURNING id, product_id, quantity, quantity_unit
.bind(msg.quantity) .bind(msg.quantity)
.bind(msg.quantity_unit) .bind(msg.quantity_unit)
.bind(msg.id) .bind(msg.id)
.fetch_one(&pool) .fetch_one(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -108,9 +117,17 @@ pub struct DeleteStock {
pub stock_id: StockId, pub stock_id: StockId,
} }
crate::db_async_handler!(DeleteStock, delete_stock, Option<model::Stock>); crate::db_async_handler!(
DeleteStock,
delete_stock,
Option<model::Stock>,
inner_delete_stock
);
async fn delete_stock(msg: DeleteStock, pool: PgPool) -> Result<Option<Stock>> { async fn delete_stock(
msg: DeleteStock,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<Stock>> {
sqlx::query_as( sqlx::query_as(
r#" r#"
DELETE FROM stocks DELETE FROM stocks
@ -119,7 +136,7 @@ RETURNING id, product_id, quantity, quantity_unit
"#, "#,
) )
.bind(msg.stock_id) .bind(msg.stock_id)
.fetch_optional(&pool) .fetch_optional(pool)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
@ -143,7 +160,7 @@ crate::db_async_handler!(
async fn product_stock( async fn product_stock(
msg: ProductsStock, msg: ProductsStock,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<model::Stock>> { ) -> Result<Vec<Stock>> {
Ok(MultiLoad::new( Ok(MultiLoad::new(
pool, pool,
r#" r#"
@ -160,3 +177,102 @@ async fn product_stock(
) )
.await?) .await?)
} }
#[cfg(test)]
mod tests {
use config::UpdateConfig;
use fake::faker::lorem::en as lorem;
use fake::Fake;
use model::*;
use uuid::Uuid;
pub struct NoOpts;
impl UpdateConfig for NoOpts {}
use crate::*;
async fn test_product(pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Product {
create_product(
CreateProduct {
name: ProductName::new(format!("db stocks test product {}", Uuid::new_v4())),
short_description: ProductShortDesc::new(lorem::Paragraph(1..2).fake::<String>()),
long_description: ProductLongDesc::new(lorem::Paragraph(4..5).fake::<String>()),
category: None,
price: Price::from_u32(12321),
deliver_days_flag: Days(vec![Day::Friday, Day::Sunday]),
},
pool,
)
.await
.unwrap()
}
async fn test_stock(
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
product_id: Option<ProductId>,
quantity: Option<Quantity>,
quantity_unit: Option<QuantityUnit>,
) -> Stock {
let product_id = match product_id {
Some(id) => id,
_ => test_product(&mut *pool).await.id,
};
let quantity = quantity.unwrap_or_else(|| Quantity::from_u32(345));
let quantity_unit = quantity_unit.unwrap_or_else(|| QuantityUnit::Piece);
super::create_stock(
CreateStock {
product_id,
quantity_unit,
quantity,
},
&mut *pool,
)
.await
.unwrap()
}
#[actix::test]
async fn create_stock() {
let config = config::default_load(&mut NoOpts);
config
.lock()
.database_mut()
.set_url("postgres://postgres@localhost/bazzar_test");
let db = Database::build(config).await;
let pool = db.pool();
let mut t = pool.begin().await.unwrap();
test_stock(&mut t, None, None, None).await;
t.rollback().await.unwrap();
}
#[actix::test]
async fn products_stock() {
let config = config::default_load(&mut NoOpts);
config
.lock()
.database_mut()
.set_url("postgres://postgres@localhost/bazzar_test");
let db = Database::build(config).await;
let pool = db.pool();
let mut t = pool.begin().await.unwrap();
let first = test_stock(&mut t, None, None, None).await;
let second = test_stock(&mut t, None, None, None).await;
let stocks: Vec<Stock> = super::product_stock(
ProductsStock {
product_ids: vec![first.product_id, second.product_id],
},
&mut t,
)
.await
.unwrap();
t.rollback().await.unwrap();
assert_eq!(stocks, vec![first, second]);
}
}

View File

@ -172,7 +172,7 @@ impl Role {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[derive(Copy, Clone, Debug, Hash, Display, Deserialize, Serialize)] #[derive(Copy, Clone, Debug, PartialEq, Hash, Display, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum QuantityUnit { pub enum QuantityUnit {
#[cfg_attr(feature = "db", sqlx(rename = "g"))] #[cfg_attr(feature = "db", sqlx(rename = "g"))]
@ -283,6 +283,12 @@ impl Default for Audience {
#[serde(transparent)] #[serde(transparent)]
pub struct Price(NonNegative); pub struct Price(NonNegative);
impl Price {
pub fn from_u32(price: u32) -> Self {
Self(NonNegative(price as i32))
}
}
impl ops::Mul<Quantity> for Price { impl ops::Mul<Quantity> for Price {
type Output = Self; type Output = Self;
@ -294,7 +300,7 @@ impl ops::Mul<Quantity> for Price {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Default, Debug, Copy, Clone, Hash, Deref, From)] #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Copy, Clone, Hash, Deref, From)]
#[serde(transparent)] #[serde(transparent)]
pub struct Quantity(NonNegative); pub struct Quantity(NonNegative);
@ -429,7 +435,7 @@ impl<'de> serde::Deserialize<'de> for Email {
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Default, Debug, Copy, Clone, Hash, Deref, Display)] #[derive(Serialize, Default, Debug, PartialEq, Copy, Clone, Hash, Deref, Display)]
#[serde(transparent)] #[serde(transparent)]
pub struct NonNegative(i32); pub struct NonNegative(i32);
@ -591,7 +597,7 @@ impl TryFrom<i32> for Day {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[derive(Serialize, Deserialize, Hash, Debug)] #[derive(Serialize, Deserialize, Hash, Debug)]
#[serde(transparent)] #[serde(transparent)]
pub struct Days(Vec<Day>); pub struct Days(pub Vec<Day>);
impl ops::Deref for Days { impl ops::Deref for Days {
type Target = Vec<Day>; type Target = Vec<Day>;
@ -856,13 +862,13 @@ pub struct Product {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(transparent)] #[serde(transparent)]
pub struct StockId(pub RecordId); pub struct StockId(pub RecordId);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Stock { pub struct Stock {
pub id: StockId, pub id: StockId,
pub product_id: ProductId, pub product_id: ProductId,