From 69dfb926f2e391ea970ed5cdab3d5d4638525d78 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Sun, 14 Jul 2024 18:29:48 +0530 Subject: [PATCH] fix: check for duplicate store names in owner scope before creating store --- .../adapters/output/db/postgres/errors.rs | 2 + .../output/db/postgres/store_name_exists.rs | 78 +++++++++++++++++++ .../application/port/output/db/errors.rs | 1 + .../application/port/output/db/mod.rs | 1 + .../port/output/db/store_name_exists.rs | 54 +++++++++++++ .../application/services/add_store_service.rs | 34 +++++++- src/inventory/application/services/errors.rs | 2 + 7 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 src/inventory/adapters/output/db/postgres/store_name_exists.rs create mode 100644 src/inventory/application/port/output/db/store_name_exists.rs diff --git a/src/inventory/adapters/output/db/postgres/errors.rs b/src/inventory/adapters/output/db/postgres/errors.rs index 2676699..e4e9378 100644 --- a/src/inventory/adapters/output/db/postgres/errors.rs +++ b/src/inventory/adapters/output/db/postgres/errors.rs @@ -17,6 +17,8 @@ impl From 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}"); } diff --git a/src/inventory/adapters/output/db/postgres/store_name_exists.rs b/src/inventory/adapters/output/db/postgres/store_name_exists.rs new file mode 100644 index 0000000..0df0a1e --- /dev/null +++ b/src/inventory/adapters/output/db/postgres/store_name_exists.rs @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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 { + 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; + } +} diff --git a/src/inventory/application/port/output/db/errors.rs b/src/inventory/application/port/output/db/errors.rs index 3d80dce..5066b15 100644 --- a/src/inventory/application/port/output/db/errors.rs +++ b/src/inventory/application/port/output/db/errors.rs @@ -10,6 +10,7 @@ pub type InventoryDBResult = Result; #[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum InventoryDBError { DuplicateCategoryName, + DuplicateStoreName, DuplicateStoreID, InternalError, } diff --git a/src/inventory/application/port/output/db/mod.rs b/src/inventory/application/port/output/db/mod.rs index 782819f..d5741ec 100644 --- a/src/inventory/application/port/output/db/mod.rs +++ b/src/inventory/application/port/output/db/mod.rs @@ -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; diff --git a/src/inventory/application/port/output/db/store_name_exists.rs b/src/inventory/application/port/output/db/store_name_exists.rs new file mode 100644 index 0000000..9b625df --- /dev/null +++ b/src/inventory/application/port/output/db/store_name_exists.rs @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// 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; +} + +pub type StoreNameExistsDBPortObj = std::sync::Arc; + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + + pub fn mock_store_name_exists_db_port_false(times: Option) -> 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) -> 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) + } +} diff --git a/src/inventory/application/services/add_store_service.rs b/src/inventory/application/services/add_store_service.rs index ad49d02..5181d2b 100644 --- a/src/inventory/application/services/add_store_service.rs +++ b/src/inventory/application/services/add_store_service.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2024 Aravinth Manivannan // // 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; #[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) + ); + } } diff --git a/src/inventory/application/services/errors.rs b/src/inventory/application/services/errors.rs index 3a091f3..7bb9fb2 100644 --- a/src/inventory/application/services/errors.rs +++ b/src/inventory/application/services/errors.rs @@ -13,6 +13,7 @@ pub type InventoryResult = Result; #[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum InventoryError { DuplicateCategoryName, + DuplicateStoreName, InternalError, } @@ -20,6 +21,7 @@ impl From for InventoryError { fn from(value: InventoryDBError) -> Self { match value { InventoryDBError::DuplicateCategoryName => Self::DuplicateCategoryName, + InventoryDBError::DuplicateStoreName => Self::DuplicateStoreName, InventoryDBError::DuplicateStoreID => { error!("DuplicateStoreID"); Self::InternalError