diff --git a/dao/src/lib.rs b/dao/src/lib.rs index c7fd129..6a77f47 100644 --- a/dao/src/lib.rs +++ b/dao/src/lib.rs @@ -5,6 +5,7 @@ use mockall::automock; use thiserror::Error; pub mod permission; +pub mod sales_person; pub mod slot; pub use permission::MockPermissionDao; diff --git a/dao/src/sales_person.rs b/dao/src/sales_person.rs new file mode 100644 index 0000000..1c2a505 --- /dev/null +++ b/dao/src/sales_person.rs @@ -0,0 +1,25 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use mockall::automock; +use uuid::Uuid; + +use crate::DaoError; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SalesPersonEntity { + pub id: Uuid, + pub name: Arc, + pub deleted: Option, + pub inactive: bool, + pub version: Uuid, +} + +#[automock] +#[async_trait] +pub trait SalesPersonDao { + async fn all(&self) -> Result, DaoError>; + async fn find_by_id(&self, id: Uuid) -> Result, DaoError>; + async fn create(&self, entity: &SalesPersonEntity, process: &str) -> Result<(), DaoError>; + async fn update(&self, entity: &SalesPersonEntity, process: &str) -> Result<(), DaoError>; +} diff --git a/service/src/lib.rs b/service/src/lib.rs index 7da8309..896369d 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -6,6 +6,7 @@ use uuid::Uuid; pub mod clock; pub mod permission; +pub mod sales_person; pub mod slot; pub mod user_service; pub mod uuid_service; diff --git a/service/src/sales_person.rs b/service/src/sales_person.rs new file mode 100644 index 0000000..daedaea --- /dev/null +++ b/service/src/sales_person.rs @@ -0,0 +1,58 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use mockall::automock; +use uuid::Uuid; + +use crate::ServiceError; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SalesPerson { + pub id: Uuid, + pub name: Arc, + pub inactive: bool, + pub deleted: Option, + pub version: Uuid, +} +impl From<&dao::sales_person::SalesPersonEntity> for SalesPerson { + fn from(sales_person: &dao::sales_person::SalesPersonEntity) -> Self { + Self { + id: sales_person.id, + name: sales_person.name.clone(), + inactive: sales_person.inactive, + deleted: sales_person.deleted, + version: sales_person.version, + } + } +} +impl From<&SalesPerson> for dao::sales_person::SalesPersonEntity { + fn from(sales_person: &SalesPerson) -> Self { + Self { + id: sales_person.id, + name: sales_person.name.clone(), + inactive: sales_person.inactive, + deleted: sales_person.deleted, + version: sales_person.version, + } + } +} + +#[automock(type Context=();)] +#[async_trait] +pub trait SalesPersonService { + type Context: Clone + Send + Sync + 'static; + + async fn get_all(&self, context: Self::Context) -> Result, ServiceError>; + async fn get(&self, id: Uuid, context: Self::Context) -> Result; + async fn create( + &self, + item: &SalesPerson, + context: Self::Context, + ) -> Result; + async fn update( + &self, + item: &SalesPerson, + context: Self::Context, + ) -> Result; + async fn delete(&self, id: Uuid, context: Self::Context) -> Result<(), ServiceError>; +} diff --git a/service_impl/src/lib.rs b/service_impl/src/lib.rs index e9fe606..77eac20 100644 --- a/service_impl/src/lib.rs +++ b/service_impl/src/lib.rs @@ -4,6 +4,7 @@ use async_trait::async_trait; pub mod clock; pub mod permission; +pub mod sales_person; pub mod slot; mod test; pub mod uuid_service; diff --git a/service_impl/src/sales_person.rs b/service_impl/src/sales_person.rs new file mode 100644 index 0000000..c225665 --- /dev/null +++ b/service_impl/src/sales_person.rs @@ -0,0 +1,183 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use dao::sales_person::SalesPersonEntity; +use service::{sales_person::SalesPerson, ServiceError, ValidationFailureItem}; +use uuid::Uuid; + +pub struct SalesPersonServiceImpl +where + SalesPersonDao: dao::sales_person::SalesPersonDao + Send + Sync, + PermissionService: service::permission::PermissionService + Send + Sync, + ClockService: service::clock::ClockService + Send + Sync, + UuidService: service::uuid_service::UuidService + Send + Sync, +{ + pub sales_person_dao: Arc, + pub permission_service: Arc, + pub clock_service: Arc, + pub uuid_service: Arc, +} +impl + SalesPersonServiceImpl +where + SalesPersonDao: dao::sales_person::SalesPersonDao + Send + Sync, + PermissionService: service::permission::PermissionService + Send + Sync, + ClockService: service::clock::ClockService + Send + Sync, + UuidService: service::uuid_service::UuidService + Send + Sync, +{ + pub fn new( + sales_person_dao: Arc, + permission_service: Arc, + clock_service: Arc, + uuid_service: Arc, + ) -> Self { + Self { + sales_person_dao, + permission_service, + clock_service, + uuid_service, + } + } +} + +const SALES_PERSON_SERVICE_PROCESS: &str = "sales-person-service"; + +#[async_trait] +impl + service::sales_person::SalesPersonService + for SalesPersonServiceImpl +where + SalesPersonDao: dao::sales_person::SalesPersonDao + Send + Sync, + PermissionService: service::permission::PermissionService + Send + Sync, + ClockService: service::clock::ClockService + Send + Sync, + UuidService: service::uuid_service::UuidService + Send + Sync, +{ + type Context = PermissionService::Context; + + async fn get_all( + &self, + context: Self::Context, + ) -> Result, service::ServiceError> { + self.permission_service + .check_permission("hr", context) + .await?; + Ok(self + .sales_person_dao + .all() + .await? + .iter() + .map(SalesPerson::from) + .collect()) + } + + async fn get( + &self, + id: Uuid, + context: Self::Context, + ) -> Result { + self.permission_service + .check_permission("hr", context) + .await?; + self.sales_person_dao + .find_by_id(id) + .await? + .as_ref() + .map(SalesPerson::from) + .ok_or(ServiceError::EntityNotFound(id)) + } + + async fn create( + &self, + sales_person: &SalesPerson, + context: Self::Context, + ) -> Result { + self.permission_service + .check_permission("hr", context) + .await?; + + if sales_person.id != Uuid::nil() { + return Err(ServiceError::IdSetOnCreate); + } + if sales_person.version != Uuid::nil() { + return Err(ServiceError::VersionSetOnCreate); + } + + let sales_person = SalesPerson { + id: self.uuid_service.new_uuid("sales-person-id"), + version: self.uuid_service.new_uuid("sales-person-version"), + ..sales_person.clone() + }; + self.sales_person_dao + .create( + &SalesPersonEntity::from(&sales_person), + SALES_PERSON_SERVICE_PROCESS, + ) + .await?; + Ok(sales_person) + } + + async fn update( + &self, + sales_person: &SalesPerson, + context: Self::Context, + ) -> Result { + self.permission_service + .check_permission("hr", context) + .await?; + + let sales_person_entity = self + .sales_person_dao + .find_by_id(sales_person.id) + .await? + .as_ref() + .map(SalesPerson::from) + .ok_or_else(move || ServiceError::EntityNotFound(sales_person.id))?; + + if sales_person.version != sales_person_entity.version { + return Err(ServiceError::EntityConflicts( + sales_person.id, + sales_person_entity.version, + sales_person.version, + )); + } + + if sales_person.deleted != sales_person_entity.deleted { + return Err(ServiceError::ValidationError( + [ValidationFailureItem::ModificationNotAllowed( + "deleted".into(), + )] + .into(), + )); + } + + let sales_person = SalesPerson { + version: self.uuid_service.new_uuid("sales-person-version"), + ..sales_person.clone() + }; + + self.sales_person_dao + .update( + &SalesPersonEntity::from(&sales_person), + SALES_PERSON_SERVICE_PROCESS, + ) + .await?; + Ok(sales_person) + } + + async fn delete(&self, id: Uuid, context: Self::Context) -> Result<(), ServiceError> { + self.permission_service + .check_permission("hr", context) + .await?; + let mut sales_person_entity = self + .sales_person_dao + .find_by_id(id) + .await? + .ok_or(ServiceError::EntityNotFound(id))?; + sales_person_entity.deleted = Some(self.clock_service.date_time_now()); + sales_person_entity.version = self.uuid_service.new_uuid("sales-person-version"); + self.sales_person_dao + .update(&sales_person_entity, SALES_PERSON_SERVICE_PROCESS) + .await?; + Ok(()) + } +} diff --git a/service_impl/src/test/error_test.rs b/service_impl/src/test/error_test.rs index 5eb18fb..d48ca4c 100644 --- a/service_impl/src/test/error_test.rs +++ b/service_impl/src/test/error_test.rs @@ -89,8 +89,8 @@ pub fn test_conflicts( } } -pub fn test_validation_error( - result: &Result<(), service::ServiceError>, +pub fn test_validation_error( + result: &Result, validation_failure: &ValidationFailureItem, fail_count: usize, ) { diff --git a/service_impl/src/test/mod.rs b/service_impl/src/test/mod.rs index 47715f1..abd9b9c 100644 --- a/service_impl/src/test/mod.rs +++ b/service_impl/src/test/mod.rs @@ -3,4 +3,6 @@ pub mod error_test; #[cfg(test)] mod permission_test; #[cfg(test)] +pub mod sales_person; +#[cfg(test)] pub mod slot; diff --git a/service_impl/src/test/sales_person.rs b/service_impl/src/test/sales_person.rs new file mode 100644 index 0000000..1ceb096 --- /dev/null +++ b/service_impl/src/test/sales_person.rs @@ -0,0 +1,519 @@ +use super::error_test::*; +use dao::sales_person::{MockSalesPersonDao, SalesPersonEntity}; +use mockall::predicate::eq; +use service::{ + clock::MockClockService, + sales_person::{SalesPerson, SalesPersonService}, + uuid_service::MockUuidService, + MockPermissionService, +}; +use time::{Date, Month, PrimitiveDateTime, Time}; +use tokio; +use uuid::{uuid, Uuid}; + +use crate::sales_person::SalesPersonServiceImpl; + +pub struct SalesPersonServiceDependencies { + pub sales_person_dao: MockSalesPersonDao, + pub permission_service: MockPermissionService, + pub clock_service: MockClockService, + pub uuid_service: MockUuidService, +} +impl SalesPersonServiceDependencies { + pub fn build_service( + self, + ) -> SalesPersonServiceImpl< + MockSalesPersonDao, + MockPermissionService, + MockClockService, + MockUuidService, + > { + SalesPersonServiceImpl::new( + self.sales_person_dao.into(), + self.permission_service.into(), + self.clock_service.into(), + self.uuid_service.into(), + ) + } +} + +pub fn build_dependencies(permission: bool, role: &'static str) -> SalesPersonServiceDependencies { + let sales_person_dao = MockSalesPersonDao::new(); + let mut permission_service = MockPermissionService::new(); + permission_service + .expect_check_permission() + .with(eq(role), eq(())) + .returning(move |_, _| { + if permission { + Ok(()) + } else { + Err(service::ServiceError::Forbidden) + } + }); + permission_service + .expect_check_permission() + .returning(move |_, _| Err(service::ServiceError::Forbidden)); + let mut clock_service = MockClockService::new(); + clock_service + .expect_time_now() + .returning(|| time::Time::from_hms(23, 42, 0).unwrap()); + clock_service + .expect_date_now() + .returning(|| time::Date::from_calendar_date(2063, 4.try_into().unwrap(), 5).unwrap()); + clock_service.expect_date_time_now().returning(|| { + time::PrimitiveDateTime::new( + time::Date::from_calendar_date(2063, 4.try_into().unwrap(), 5).unwrap(), + time::Time::from_hms(23, 42, 0).unwrap(), + ) + }); + let uuid_service = MockUuidService::new(); + + SalesPersonServiceDependencies { + sales_person_dao, + permission_service, + clock_service, + uuid_service, + } +} + +pub fn default_id() -> uuid::Uuid { + uuid!("67D91F86-2EC7-4FA6-8EB4-9C76A2D4C6E0") +} +pub fn alternate_id() -> uuid::Uuid { + uuid!("67D91F86-2EC7-4FA6-8EB4-9C76A2D4C6E1") +} + +pub fn default_version() -> uuid::Uuid { + uuid!("CCB5F4E2-8C7D-4388-AC4E-641D43ADF580") +} +pub fn alternate_version() -> uuid::Uuid { + uuid!("CCB5F4E2-8C7D-4388-AC4E-641D43ADF581") +} + +pub fn default_sales_person_entity() -> dao::sales_person::SalesPersonEntity { + dao::sales_person::SalesPersonEntity { + id: default_id(), + name: "John Doe".into(), + deleted: None, + inactive: false, + version: default_version(), + } +} + +pub fn default_sales_person() -> service::sales_person::SalesPerson { + service::sales_person::SalesPerson { + id: default_id(), + name: "John Doe".into(), + inactive: false, + deleted: None, + version: default_version(), + } +} + +#[tokio::test] +async fn test_get_all() { + let mut dependencies = build_dependencies(true, "hr"); + dependencies.sales_person_dao.expect_all().returning(|| { + Ok([ + default_sales_person_entity(), + SalesPersonEntity { + id: alternate_id(), + name: "Jane Doe".into(), + ..default_sales_person_entity() + }, + ] + .into()) + }); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service.get_all(()).await.unwrap(); + assert_eq!(2, result.len()); + assert_eq!(default_sales_person(), result[0]); + assert_eq!( + service::sales_person::SalesPerson { + id: alternate_id(), + name: "Jane Doe".into(), + ..default_sales_person() + }, + result[1] + ); +} + +#[tokio::test] +async fn test_get_all_no_permission() { + let dependencies = build_dependencies(false, "hr"); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service.get_all(()).await; + test_forbidden(&result); +} + +#[tokio::test] +async fn test_get() { + let mut dependencies = build_dependencies(true, "hr"); + dependencies + .sales_person_dao + .expect_find_by_id() + .with(eq(default_id())) + .times(1) + .returning(|_| Ok(Some(default_sales_person_entity()))); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service.get(default_id(), ()).await; + assert_eq!(default_sales_person(), result.unwrap()); +} + +#[tokio::test] +async fn test_get_no_permission() { + let dependencies = build_dependencies(false, "hr"); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service.get(default_id(), ()).await; + test_forbidden(&result); +} + +#[tokio::test] +async fn test_get_not_found() { + let mut dependencies = build_dependencies(true, "hr"); + dependencies + .sales_person_dao + .expect_find_by_id() + .with(eq(default_id())) + .times(1) + .returning(|_| Ok(None)); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service.get(default_id(), ()).await; + test_not_found(&result, &default_id()); +} + +#[tokio::test] +async fn test_create() { + let mut dependencies = build_dependencies(true, "hr"); + dependencies + .sales_person_dao + .expect_create() + .with( + eq(default_sales_person_entity()), + eq("sales-person-service"), + ) + .times(1) + .returning(|_, _| Ok(())); + dependencies + .uuid_service + .expect_new_uuid() + .with(eq("sales-person-id")) + .times(1) + .returning(|_| default_id()); + dependencies + .uuid_service + .expect_new_uuid() + .with(eq("sales-person-version")) + .times(1) + .returning(|_| default_version()); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service + .create( + &SalesPerson { + id: Uuid::nil(), + version: Uuid::nil(), + ..default_sales_person() + }, + (), + ) + .await + .unwrap(); + assert_eq!(result, default_sales_person()); +} + +#[tokio::test] +async fn test_create_no_permission() { + let dependencies = build_dependencies(false, "hr"); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service + .create( + &SalesPerson { + id: Uuid::nil(), + version: Uuid::nil(), + ..default_sales_person() + }, + (), + ) + .await; + test_forbidden(&result); +} + +#[tokio::test] +async fn test_create_validation() { + let mut dependencies = build_dependencies(true, "hr"); + dependencies + .uuid_service + .expect_new_uuid() + .with(eq("sales-person-id")) + .returning(|_| default_id()); + dependencies + .uuid_service + .expect_new_uuid() + .with(eq("sales-person-version")) + .returning(|_| default_version()); + + let sales_person_service = dependencies.build_service(); + let result = sales_person_service + .create( + &SalesPerson { + version: Uuid::nil(), + ..default_sales_person() + }, + (), + ) + .await; + test_zero_id_error(&result); + + let result = sales_person_service + .create( + &SalesPerson { + id: Uuid::nil(), + ..default_sales_person() + }, + (), + ) + .await; + test_zero_version_error(&result); +} + +#[tokio::test] +async fn test_update_no_permission() { + let dependencies = build_dependencies(false, "hr"); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service + .update( + &SalesPerson { + name: "Jane Doe".into(), + ..default_sales_person() + }, + (), + ) + .await; + test_forbidden(&result); +} + +#[tokio::test] +async fn test_update_not_found() { + let mut dependencies = build_dependencies(true, "hr"); + dependencies + .sales_person_dao + .expect_find_by_id() + .with(eq(default_id())) + .returning(|_| Ok(None)); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service + .update( + &SalesPerson { + name: "Jane Doe".into(), + ..default_sales_person() + }, + (), + ) + .await; + test_not_found(&result, &default_id()); +} + +#[tokio::test] +async fn test_update_conflicts() { + let mut dependencies = build_dependencies(true, "hr"); + dependencies + .sales_person_dao + .expect_find_by_id() + .with(eq(default_id())) + .returning(|_| Ok(Some(default_sales_person_entity()))); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service + .update( + &SalesPerson { + version: alternate_version(), + ..default_sales_person() + }, + (), + ) + .await; + test_conflicts( + &result, + &default_id(), + &default_version(), + &alternate_version(), + ); +} + +#[tokio::test] +async fn test_update_deleted_no_allowed() { + let mut dependencies = build_dependencies(true, "hr"); + dependencies + .sales_person_dao + .expect_find_by_id() + .with(eq(default_id())) + .returning(|_| Ok(Some(default_sales_person_entity()))); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service + .update( + &SalesPerson { + deleted: Some(PrimitiveDateTime::new( + Date::from_calendar_date(2000, Month::January, 1).unwrap(), + Time::from_hms(1, 0, 0).unwrap(), + )), + ..default_sales_person() + }, + (), + ) + .await; + test_validation_error( + &result, + &service::ValidationFailureItem::ModificationNotAllowed("deleted".into()), + 1, + ); +} + +#[tokio::test] +async fn test_update_inactive() { + let mut dependencies = build_dependencies(true, "hr"); + dependencies + .sales_person_dao + .expect_find_by_id() + .with(eq(default_id())) + .returning(|_| Ok(Some(default_sales_person_entity()))); + dependencies + .sales_person_dao + .expect_update() + .with( + eq(SalesPersonEntity { + inactive: true, + version: alternate_version(), + ..default_sales_person_entity() + }), + eq("sales-person-service"), + ) + .returning(|_, _| Ok(())); + dependencies + .uuid_service + .expect_new_uuid() + .with(eq("sales-person-version")) + .returning(|_| alternate_version()); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service + .update( + &SalesPerson { + inactive: true, + ..default_sales_person() + }, + (), + ) + .await + .unwrap(); + assert_eq!( + result, + SalesPerson { + inactive: true, + version: alternate_version(), + ..default_sales_person() + } + ); +} + +#[tokio::test] +async fn test_update_name() { + let mut dependencies = build_dependencies(true, "hr"); + dependencies + .sales_person_dao + .expect_find_by_id() + .with(eq(default_id())) + .returning(|_| Ok(Some(default_sales_person_entity()))); + dependencies + .sales_person_dao + .expect_update() + .with( + eq(SalesPersonEntity { + name: "Jane Doe".into(), + version: alternate_version(), + ..default_sales_person_entity() + }), + eq("sales-person-service"), + ) + .returning(|_, _| Ok(())); + dependencies + .uuid_service + .expect_new_uuid() + .with(eq("sales-person-version")) + .returning(|_| alternate_version()); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service + .update( + &SalesPerson { + name: "Jane Doe".into(), + ..default_sales_person() + }, + (), + ) + .await + .unwrap(); + assert_eq!( + result, + SalesPerson { + name: "Jane Doe".into(), + version: alternate_version(), + ..default_sales_person() + } + ); +} + +#[tokio::test] +async fn test_delete() { + let mut dependencies = build_dependencies(true, "hr"); + dependencies + .sales_person_dao + .expect_find_by_id() + .with(eq(default_id())) + .returning(|_| Ok(Some(default_sales_person_entity()))); + dependencies + .sales_person_dao + .expect_update() + .with( + eq(SalesPersonEntity { + deleted: Some(PrimitiveDateTime::new( + Date::from_calendar_date(2063, Month::April, 5).unwrap(), + Time::from_hms(23, 42, 0).unwrap(), + )), + version: alternate_version(), + ..default_sales_person_entity() + }), + eq("sales-person-service"), + ) + .returning(|_, _| Ok(())); + dependencies + .uuid_service + .expect_new_uuid() + .with(eq("sales-person-version")) + .returning(|_| alternate_version()); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service.delete(default_id(), ()).await; + assert!(result.is_ok()); +} + +#[tokio::test] +async fn test_delete_no_permission() { + let mut dependencies = build_dependencies(false, "hr"); + dependencies + .sales_person_dao + .expect_find_by_id() + .with(eq(default_id())) + .returning(|_| Ok(Some(default_sales_person_entity()))); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service.delete(default_id(), ()).await; + test_forbidden(&result); +} + +#[tokio::test] +async fn test_delete_not_found() { + let mut dependencies = build_dependencies(true, "hr"); + dependencies + .sales_person_dao + .expect_find_by_id() + .with(eq(default_id())) + .returning(|_| Ok(None)); + let sales_person_service = dependencies.build_service(); + let result = sales_person_service.delete(default_id(), ()).await; + test_not_found(&result, &default_id()); +}