fix: check for duplicate store names in owner scope before creating store #30
7 changed files with 171 additions and 1 deletions
|
@ -17,6 +17,8 @@ impl From<SqlxError> for InventoryDBError {
|
|||
let msg = err.message();
|
||||
if msg.contains("cqrs_inventory_store_query_store_id_key") {
|
||||
return Self::DuplicateStoreID;
|
||||
} else if msg.contains("cqrs_inventory_store_query_name_key") {
|
||||
return Self::DuplicateStoreName;
|
||||
} else {
|
||||
println!("{msg}");
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use super::InventoryDBPostgresAdapter;
|
||||
use crate::inventory::application::port::output::db::{errors::*, store_name_exists::*};
|
||||
use crate::inventory::domain::store_aggregate::*;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl StoreNameExistsDBPort for InventoryDBPostgresAdapter {
|
||||
async fn store_name_exists(&self, s: &Store) -> InventoryDBResult<bool> {
|
||||
let res = sqlx::query!(
|
||||
"SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM cqrs_inventory_store_query
|
||||
WHERE
|
||||
name = $1
|
||||
);",
|
||||
s.name(),
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
if let Some(x) = res.exists {
|
||||
Ok(x)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_postgres_store_exists() {
|
||||
let store_id = Uuid::new_v4();
|
||||
let settings = crate::settings::tests::get_settings().await;
|
||||
settings.create_db().await;
|
||||
let db = super::InventoryDBPostgresAdapter::new(
|
||||
sqlx::postgres::PgPool::connect(&settings.database.url)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let store = StoreBuilder::default()
|
||||
.name("store_name".into())
|
||||
.owner("store_owner".into())
|
||||
.address(Some("store_address".into()))
|
||||
.store_id(store_id)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// state doesn't exist
|
||||
assert!(!db.store_name_exists(&store).await.unwrap());
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO cqrs_inventory_store_query
|
||||
(version, name, address, store_id, owner)
|
||||
VALUES ($1, $2, $3, $4, $5);",
|
||||
1,
|
||||
store.name(),
|
||||
store.address().as_ref().unwrap(),
|
||||
store.store_id(),
|
||||
store.owner(),
|
||||
)
|
||||
.execute(&db.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// state exists
|
||||
assert!(db.store_name_exists(&store).await.unwrap());
|
||||
|
||||
settings.drop_db().await;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ pub type InventoryDBResult<V> = Result<V, InventoryDBError>;
|
|||
#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum InventoryDBError {
|
||||
DuplicateCategoryName,
|
||||
DuplicateStoreName,
|
||||
DuplicateStoreID,
|
||||
InternalError,
|
||||
}
|
||||
|
|
|
@ -6,3 +6,4 @@ pub mod category_id_exists;
|
|||
pub mod category_name_exists_for_store;
|
||||
pub mod errors;
|
||||
pub mod store_id_exists;
|
||||
pub mod store_name_exists;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
|
||||
use crate::inventory::domain::store_aggregate::Store;
|
||||
|
||||
use super::errors::*;
|
||||
#[cfg(test)]
|
||||
#[allow(unused_imports)]
|
||||
pub use tests::*;
|
||||
|
||||
#[automock]
|
||||
#[async_trait::async_trait]
|
||||
pub trait StoreNameExistsDBPort: Send + Sync {
|
||||
async fn store_name_exists(&self, s: &Store) -> InventoryDBResult<bool>;
|
||||
}
|
||||
|
||||
pub type StoreNameExistsDBPortObj = std::sync::Arc<dyn StoreNameExistsDBPort>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_store_name_exists_db_port_false(times: Option<usize>) -> StoreNameExistsDBPortObj {
|
||||
let mut m = MockStoreNameExistsDBPort::new();
|
||||
if let Some(times) = times {
|
||||
m.expect_store_name_exists()
|
||||
.times(times)
|
||||
.returning(|_| Ok(false));
|
||||
} else {
|
||||
m.expect_store_name_exists().returning(|_| Ok(false));
|
||||
}
|
||||
|
||||
Arc::new(m)
|
||||
}
|
||||
|
||||
pub fn mock_store_name_exists_db_port_true(times: Option<usize>) -> StoreNameExistsDBPortObj {
|
||||
let mut m = MockStoreNameExistsDBPort::new();
|
||||
if let Some(times) = times {
|
||||
m.expect_store_name_exists()
|
||||
.times(times)
|
||||
.returning(|_| Ok(true));
|
||||
} else {
|
||||
m.expect_store_name_exists().returning(|_| Ok(true));
|
||||
}
|
||||
|
||||
Arc::new(m)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use derive_builder::Builder;
|
||||
|
@ -9,7 +10,9 @@ use mockall::*;
|
|||
|
||||
use super::errors::*;
|
||||
use crate::inventory::{
|
||||
application::port::output::db::{errors::InventoryDBError, store_id_exists::*},
|
||||
application::port::output::db::{
|
||||
errors::InventoryDBError, store_id_exists::*, store_name_exists::*,
|
||||
},
|
||||
domain::{
|
||||
add_store_command::AddStoreCommand,
|
||||
store_added_event::{StoreAddedEvent, StoreAddedEventBuilder},
|
||||
|
@ -29,6 +32,7 @@ pub type AddStoreServiceObj = Arc<dyn AddStoreUseCase>;
|
|||
#[derive(Clone, Builder)]
|
||||
pub struct AddStoreService {
|
||||
db_store_id_exists: StoreIDExistsDBPortObj,
|
||||
db_store_name_exists: StoreNameExistsDBPortObj,
|
||||
get_uuid: GetUUIDInterfaceObj,
|
||||
}
|
||||
|
||||
|
@ -43,6 +47,11 @@ impl AddStoreUseCase for AddStoreService {
|
|||
.store_id(store_id.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
if self.db_store_name_exists.store_name_exists(&store).await? {
|
||||
return Err(InventoryError::DuplicateStoreName);
|
||||
}
|
||||
|
||||
loop {
|
||||
if self.db_store_id_exists.store_id_exists(&store).await? {
|
||||
store_id = self.get_uuid.get_uuid();
|
||||
|
@ -111,6 +120,7 @@ pub mod tests {
|
|||
|
||||
let s = AddStoreServiceBuilder::default()
|
||||
.db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
|
||||
.db_store_name_exists(mock_store_name_exists_db_port_false(IS_CALLED_ONLY_ONCE))
|
||||
.get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
@ -121,4 +131,26 @@ pub mod tests {
|
|||
assert_eq!(res.owner(), cmd.owner());
|
||||
assert_eq!(res.store_id(), &UUID);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_service_store_name_exists() {
|
||||
let name = "foo";
|
||||
let address = "bar";
|
||||
let username = "baz";
|
||||
|
||||
// address = None
|
||||
let cmd = AddStoreCommand::new(name.into(), Some(address.into()), username.into()).unwrap();
|
||||
|
||||
let s = AddStoreServiceBuilder::default()
|
||||
.db_store_id_exists(mock_store_id_exists_db_port_false(IS_NEVER_CALLED))
|
||||
.db_store_name_exists(mock_store_name_exists_db_port_true(IS_CALLED_ONLY_ONCE))
|
||||
.get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
s.add_store(cmd.clone()).await,
|
||||
Err(InventoryError::DuplicateStoreName)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ pub type InventoryResult<V> = Result<V, InventoryError>;
|
|||
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum InventoryError {
|
||||
DuplicateCategoryName,
|
||||
DuplicateStoreName,
|
||||
InternalError,
|
||||
}
|
||||
|
||||
|
@ -20,6 +21,7 @@ impl From<InventoryDBError> for InventoryError {
|
|||
fn from(value: InventoryDBError) -> Self {
|
||||
match value {
|
||||
InventoryDBError::DuplicateCategoryName => Self::DuplicateCategoryName,
|
||||
InventoryDBError::DuplicateStoreName => Self::DuplicateStoreName,
|
||||
InventoryDBError::DuplicateStoreID => {
|
||||
error!("DuplicateStoreID");
|
||||
Self::InternalError
|
||||
|
|
Loading…
Reference in a new issue