fix: check for duplicate store names in owner scope before creating store
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful

This commit is contained in:
Aravinth Manivannan 2024-07-14 18:29:48 +05:30
parent e4181c7cb4
commit 69dfb926f2
Signed by: realaravinth
GPG key ID: F8F50389936984FF
7 changed files with 171 additions and 1 deletions

View file

@ -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}");
} }

View file

@ -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;
}
}

View file

@ -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,
} }

View file

@ -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;

View file

@ -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)
}
}

View file

@ -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)
);
}
} }

View file

@ -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