Merge pull request 'fix: check for duplicate store names in owner scope before creating store' (#30) from fix-check-duplicate-store-name-for-owner into master
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Reviewed-on: #30
This commit is contained in:
commit
9675b0138e
7 changed files with 171 additions and 1 deletions
|
@ -17,6 +17,8 @@ impl From<SqlxError> for InventoryDBError {
|
||||||
let msg = err.message();
|
let msg = err.message();
|
||||||
if msg.contains("cqrs_inventory_store_query_store_id_key") {
|
if msg.contains("cqrs_inventory_store_query_store_id_key") {
|
||||||
return Self::DuplicateStoreID;
|
return Self::DuplicateStoreID;
|
||||||
|
} else if msg.contains("cqrs_inventory_store_query_name_key") {
|
||||||
|
return Self::DuplicateStoreName;
|
||||||
} else {
|
} else {
|
||||||
println!("{msg}");
|
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)]
|
#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum InventoryDBError {
|
pub enum InventoryDBError {
|
||||||
DuplicateCategoryName,
|
DuplicateCategoryName,
|
||||||
|
DuplicateStoreName,
|
||||||
DuplicateStoreID,
|
DuplicateStoreID,
|
||||||
InternalError,
|
InternalError,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,3 +6,4 @@ pub mod category_id_exists;
|
||||||
pub mod category_name_exists_for_store;
|
pub mod category_name_exists_for_store;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod store_id_exists;
|
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-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
|
@ -9,7 +10,9 @@ use mockall::*;
|
||||||
|
|
||||||
use super::errors::*;
|
use super::errors::*;
|
||||||
use crate::inventory::{
|
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::{
|
domain::{
|
||||||
add_store_command::AddStoreCommand,
|
add_store_command::AddStoreCommand,
|
||||||
store_added_event::{StoreAddedEvent, StoreAddedEventBuilder},
|
store_added_event::{StoreAddedEvent, StoreAddedEventBuilder},
|
||||||
|
@ -29,6 +32,7 @@ pub type AddStoreServiceObj = Arc<dyn AddStoreUseCase>;
|
||||||
#[derive(Clone, Builder)]
|
#[derive(Clone, Builder)]
|
||||||
pub struct AddStoreService {
|
pub struct AddStoreService {
|
||||||
db_store_id_exists: StoreIDExistsDBPortObj,
|
db_store_id_exists: StoreIDExistsDBPortObj,
|
||||||
|
db_store_name_exists: StoreNameExistsDBPortObj,
|
||||||
get_uuid: GetUUIDInterfaceObj,
|
get_uuid: GetUUIDInterfaceObj,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +47,11 @@ impl AddStoreUseCase for AddStoreService {
|
||||||
.store_id(store_id.clone())
|
.store_id(store_id.clone())
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
if self.db_store_name_exists.store_name_exists(&store).await? {
|
||||||
|
return Err(InventoryError::DuplicateStoreName);
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if self.db_store_id_exists.store_id_exists(&store).await? {
|
if self.db_store_id_exists.store_id_exists(&store).await? {
|
||||||
store_id = self.get_uuid.get_uuid();
|
store_id = self.get_uuid.get_uuid();
|
||||||
|
@ -111,6 +120,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = AddStoreServiceBuilder::default()
|
let s = AddStoreServiceBuilder::default()
|
||||||
.db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
|
.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))
|
.get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE))
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -121,4 +131,26 @@ pub mod tests {
|
||||||
assert_eq!(res.owner(), cmd.owner());
|
assert_eq!(res.owner(), cmd.owner());
|
||||||
assert_eq!(res.store_id(), &UUID);
|
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)]
|
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum InventoryError {
|
pub enum InventoryError {
|
||||||
DuplicateCategoryName,
|
DuplicateCategoryName,
|
||||||
|
DuplicateStoreName,
|
||||||
InternalError,
|
InternalError,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ impl From<InventoryDBError> for InventoryError {
|
||||||
fn from(value: InventoryDBError) -> Self {
|
fn from(value: InventoryDBError) -> Self {
|
||||||
match value {
|
match value {
|
||||||
InventoryDBError::DuplicateCategoryName => Self::DuplicateCategoryName,
|
InventoryDBError::DuplicateCategoryName => Self::DuplicateCategoryName,
|
||||||
|
InventoryDBError::DuplicateStoreName => Self::DuplicateStoreName,
|
||||||
InventoryDBError::DuplicateStoreID => {
|
InventoryDBError::DuplicateStoreID => {
|
||||||
error!("DuplicateStoreID");
|
error!("DuplicateStoreID");
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
|
|
Loading…
Reference in a new issue