Add REST endpoint for slot

This commit is contained in:
Simon Goller 2024-05-02 23:25:04 +02:00
parent 82e89baeeb
commit 8f378472ea
28 changed files with 1925 additions and 28 deletions

View file

@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
async-trait = "0.1.80"
mockall = "0.12.1"
tokio = "1.37.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -15,6 +16,14 @@ path = "../service"
[dependencies.dao]
path = "../dao"
[dependencies.time]
version = "0.3.36"
features = ["std"]
[dependencies.uuid]
version = "1.8.0"
features = ["v4"]
[dev-dependencies.tokio]
version = "1.37.0"
features = ["full"]

16
service_impl/src/clock.rs Normal file
View file

@ -0,0 +1,16 @@
use service::clock::ClockService;
use time::OffsetDateTime;
pub struct ClockServiceImpl;
impl ClockService for ClockServiceImpl {
fn time_now(&self) -> time::Time {
OffsetDateTime::now_utc().time()
}
fn date_now(&self) -> time::Date {
OffsetDateTime::now_utc().date()
}
fn date_time_now(&self) -> time::PrimitiveDateTime {
let now = OffsetDateTime::now_utc();
time::PrimitiveDateTime::new(now.date(), now.time())
}
}

View file

@ -2,9 +2,11 @@ use std::sync::Arc;
use async_trait::async_trait;
mod permission;
#[cfg(test)]
pub mod clock;
pub mod permission;
pub mod slot;
mod test;
pub mod uuid_service;
pub use permission::PermissionServiceImpl;

199
service_impl/src/slot.rs Normal file
View file

@ -0,0 +1,199 @@
use std::sync::Arc;
use async_trait::async_trait;
use service::{slot::Slot, ServiceError, ValidationFailureItem};
use tokio::join;
use uuid::Uuid;
const SLOT_SERVICE_PROCESS: &str = "slot-service";
pub struct SlotServiceImpl<SlotDao, PermissionService, ClockService, UuidService>
where
SlotDao: dao::slot::SlotDao + Send + Sync,
PermissionService: service::permission::PermissionService + Send + Sync,
ClockService: service::clock::ClockService + Send + Sync,
UuidService: service::uuid_service::UuidService + Send + Sync,
{
pub slot_dao: Arc<SlotDao>,
pub permission_service: Arc<PermissionService>,
pub clock_service: Arc<ClockService>,
pub uuid_service: Arc<UuidService>,
}
impl<SlotDao, PermissionService, ClockService, UuidService>
SlotServiceImpl<SlotDao, PermissionService, ClockService, UuidService>
where
SlotDao: dao::slot::SlotDao + Send + Sync,
PermissionService: service::permission::PermissionService + Send + Sync,
ClockService: service::clock::ClockService + Send + Sync,
UuidService: service::uuid_service::UuidService + Send + Sync,
{
pub fn new(
slot_dao: Arc<SlotDao>,
permission_service: Arc<PermissionService>,
clock_service: Arc<ClockService>,
uuid_service: Arc<UuidService>,
) -> Self {
Self {
slot_dao,
permission_service,
clock_service,
uuid_service,
}
}
}
pub fn test_overlapping_slots(slot_1: &Slot, slot_2: &Slot) -> bool {
slot_1.day_of_week == slot_2.day_of_week
&& (slot_2.from < slot_1.from && slot_1.from < slot_2.to
|| slot_1.from < slot_2.from && slot_2.from < slot_1.to
|| slot_1.from == slot_2.from && slot_1.to == slot_2.to)
}
#[async_trait]
impl<SlotDao, PermissionService, ClockService, UuidService> service::slot::SlotService
for SlotServiceImpl<SlotDao, PermissionService, ClockService, UuidService>
where
SlotDao: dao::slot::SlotDao + Send + Sync,
PermissionService: service::permission::PermissionService + Send + Sync,
ClockService: service::clock::ClockService + Send + Sync,
UuidService: service::uuid_service::UuidService + Send + Sync,
{
async fn get_slots(&self) -> Result<Arc<[Slot]>, ServiceError> {
let (hr_permission, sales_permission) = join!(
self.permission_service.check_permission("hr"),
self.permission_service.check_permission("sales"),
);
hr_permission.or(sales_permission)?;
Ok(self
.slot_dao
.get_slots()
.await?
.iter()
.map(Slot::from)
.collect())
}
async fn get_slot(&self, id: &Uuid) -> Result<Slot, ServiceError> {
let (hr_permission, sales_permission) = join!(
self.permission_service.check_permission("hr"),
self.permission_service.check_permission("sales"),
);
hr_permission.or(sales_permission)?;
let slot_entity = self.slot_dao.get_slot(id).await?;
let slot = slot_entity
.as_ref()
.map(Slot::from)
.ok_or_else(move || ServiceError::EntityNotFound(*id))?;
Ok(slot)
}
async fn create_slot(&self, slot: &Slot) -> Result<Slot, ServiceError> {
self.permission_service.check_permission("hr").await?;
if slot.id != Uuid::nil() {
return Err(ServiceError::IdSetOnCreate);
}
if slot.version != Uuid::nil() {
return Err(ServiceError::VersionSetOnCreate);
}
if slot.from > slot.to {
return Err(ServiceError::TimeOrderWrong(slot.from, slot.to));
}
if slot.valid_to.is_some() && slot.valid_to.unwrap() < slot.valid_from {
return Err(ServiceError::DateOrderWrong(
slot.valid_from,
slot.valid_to.unwrap(),
));
}
if self
.get_slots()
.await?
.iter()
.any(|s| test_overlapping_slots(slot, s))
{
return Err(ServiceError::OverlappingTimeRange);
}
let slot = Slot {
id: self.uuid_service.new_uuid("slot-id"),
version: self.uuid_service.new_uuid("slot-version"),
..slot.clone()
};
self.slot_dao
.create_slot(&(&slot).into(), SLOT_SERVICE_PROCESS)
.await?;
Ok(slot)
}
async fn delete_slot(&self, id: &Uuid) -> Result<(), ServiceError> {
self.permission_service.check_permission("hr").await?;
let mut slot = self
.slot_dao
.get_slot(id)
.await?
.ok_or(ServiceError::EntityNotFound(*id))?;
slot.deleted = Some(self.clock_service.date_time_now());
self.slot_dao
.update_slot(&slot, SLOT_SERVICE_PROCESS)
.await?;
Ok(())
}
async fn update_slot(&self, slot: &Slot) -> Result<(), ServiceError> {
self.permission_service.check_permission("hr").await?;
let persisted_slot = self
.slot_dao
.get_slot(&slot.id)
.await?
.ok_or(ServiceError::EntityNotFound(slot.id))?;
if persisted_slot.version != slot.version {
return Err(ServiceError::EntityConflicts(
slot.id,
persisted_slot.version,
slot.version,
));
}
if slot.valid_to.is_some() && slot.valid_to.unwrap() < slot.valid_from {
return Err(ServiceError::DateOrderWrong(
slot.valid_from,
slot.valid_to.unwrap(),
));
}
let mut validation = Vec::new();
if persisted_slot.day_of_week != slot.day_of_week.into() {
validation.push(ValidationFailureItem::ModificationNotAllowed(
"day_of_week".into(),
));
}
if persisted_slot.from != slot.from {
validation.push(ValidationFailureItem::ModificationNotAllowed("from".into()));
}
if persisted_slot.to != slot.to {
validation.push(ValidationFailureItem::ModificationNotAllowed("to".into()));
}
if persisted_slot.valid_from != slot.valid_from {
validation.push(ValidationFailureItem::ModificationNotAllowed(
"valid_from".into(),
));
}
if persisted_slot.valid_to.is_some() && persisted_slot.valid_to != slot.valid_to {
validation.push(ValidationFailureItem::ModificationNotAllowed(
"valid_to".into(),
));
}
if !validation.is_empty() {
return Err(ServiceError::ValidationError(validation.into()));
}
let slot = Slot {
version: self.uuid_service.new_uuid("slot-version"),
..slot.clone()
};
self.slot_dao
.update_slot(&(&slot).into(), SLOT_SERVICE_PROCESS)
.await?;
Ok(())
}
}

View file

@ -1 +1,4 @@
#[cfg(test)]
mod permission_test;
#[cfg(test)]
pub mod slot;

View file

@ -20,7 +20,7 @@ fn generate_dependencies_mocks_permission(
(permission_dao, user_service)
}
fn test_forbidden<T>(result: &Result<T, service::ServiceError>) {
pub fn test_forbidden<T>(result: &Result<T, service::ServiceError>) {
if let Err(service::ServiceError::Forbidden) = result {
// All good
} else {

View file

@ -0,0 +1,924 @@
use std::sync::Arc;
use crate::slot::*;
use crate::test::permission_test::test_forbidden;
use dao::slot::{MockSlotDao, SlotEntity};
use mockall::predicate::eq;
use service::{
clock::MockClockService, slot::*, uuid_service::MockUuidService, MockPermissionService,
ValidationFailureItem,
};
use time::{Date, Month, PrimitiveDateTime, Time};
use tokio;
use uuid::{uuid, Uuid};
pub fn default_id() -> Uuid {
uuid!("682DA62E-20CB-49D9-A2A7-3F53C6842405")
}
pub fn default_version() -> Uuid {
uuid!("86DE856C-D176-4F1F-A4FE-0D9844C02C03")
}
pub fn default_changed_version() -> Uuid {
uuid!("4A818852-45D2-400F-A02A-755D34FFE815")
}
pub fn generate_default_slot() -> Slot {
Slot {
id: default_id(),
day_of_week: DayOfWeek::Monday,
from: time::Time::from_hms(10, 0, 0).unwrap(),
to: time::Time::from_hms(11, 0, 0).unwrap(),
valid_from: time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 1).unwrap(),
valid_to: None,
deleted: None,
version: default_version(),
}
}
pub fn generate_default_slot_entity() -> SlotEntity {
SlotEntity {
id: uuid!("682DA62E-20CB-49D9-A2A7-3F53C6842405"),
day_of_week: dao::slot::DayOfWeek::Monday,
from: time::Time::from_hms(10, 0, 0).unwrap(),
to: time::Time::from_hms(11, 0, 0).unwrap(),
valid_from: time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 1).unwrap(),
valid_to: None,
deleted: None,
version: uuid!("86DE856C-D176-4F1F-A4FE-0D9844C02C03"),
}
}
pub fn test_not_found<T>(result: &Result<T, service::ServiceError>, target_id: &Uuid) {
if let Err(service::ServiceError::EntityNotFound(id)) = result {
assert_eq!(
id, target_id,
"Expected entity {} not found but got {}",
target_id, id
);
} else {
panic!("Expected entity {} not found error", target_id);
}
}
pub fn test_zero_id_error<T>(result: &Result<T, service::ServiceError>) {
if let Err(service::ServiceError::IdSetOnCreate) = result {
} else {
panic!("Expected id set on create error");
}
}
pub fn test_zero_version_error<T>(result: &Result<T, service::ServiceError>) {
if let Err(service::ServiceError::VersionSetOnCreate) = result {
} else {
panic!("Expected version set on create error");
}
}
pub fn test_overlapping_time_range_error<T>(result: &Result<T, service::ServiceError>) {
if let Err(service::ServiceError::OverlappingTimeRange) = result {
} else {
panic!("Expected overlapping time range error");
}
}
pub fn test_time_order_wrong<T>(result: &Result<T, service::ServiceError>) {
if let Err(service::ServiceError::TimeOrderWrong(_from, _to)) = result {
} else {
panic!("Expected time order failure");
}
}
pub fn test_date_order_wrong<T>(result: &Result<T, service::ServiceError>) {
if let Err(service::ServiceError::DateOrderWrong(_from, _to)) = result {
} else {
panic!("Expected date order failure");
}
}
pub fn test_conflicts<T>(
result: &Result<T, service::ServiceError>,
target_id: &Uuid,
expected_version: &Uuid,
actual_version: &Uuid,
) {
if let Err(service::ServiceError::EntityConflicts(
err_id,
err_expected_version,
err_actual_version,
)) = result
{
assert_eq!(
err_id, target_id,
"Expected entity {} conflicts but got {}",
target_id, err_id
);
assert_eq!(
expected_version, err_expected_version,
"Expected expected version {} but got {}",
expected_version, err_expected_version
);
assert_eq!(
actual_version, err_actual_version,
"Expected actual version {} but got {}",
actual_version, err_actual_version
);
} else {
panic!("Expected entity {} conflicts error", target_id);
}
}
pub fn test_validation_error(
result: &Result<(), service::ServiceError>,
validation_failure: &ValidationFailureItem,
fail_count: usize,
) {
if let Err(service::ServiceError::ValidationError(validation_failure_items)) = result {
if !validation_failure_items.contains(validation_failure) {
panic!(
"Validation failure not found: {:?} in {:?}",
validation_failure, validation_failure_items
);
}
assert_eq!(fail_count, validation_failure_items.len());
} else {
panic!("Expected validation error");
}
}
pub struct SlotServiceDependencies {
pub slot_dao: MockSlotDao,
pub permission_service: MockPermissionService,
pub clock_service: MockClockService,
pub uuid_service: MockUuidService,
}
impl SlotServiceDependencies {
pub fn build_service(
self,
) -> SlotServiceImpl<MockSlotDao, MockPermissionService, MockClockService, MockUuidService>
{
SlotServiceImpl::new(
self.slot_dao.into(),
self.permission_service.into(),
self.clock_service.into(),
self.uuid_service.into(),
)
}
}
pub fn build_dependencies(permission: bool, role: &'static str) -> SlotServiceDependencies {
let slot_dao = MockSlotDao::new();
let mut permission_service = MockPermissionService::new();
permission_service
.expect_check_permission()
.with(eq(role))
.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();
SlotServiceDependencies {
slot_dao,
permission_service,
clock_service,
uuid_service,
}
}
#[tokio::test]
async fn test_get_slots() {
let mut dependencies = build_dependencies(true, "hr");
dependencies.slot_dao.expect_get_slots().returning(|| {
Ok(Arc::new([
SlotEntity {
id: uuid!("DA703BC1-F488-4E4F-BA10-0972196639F7"),
version: uuid!("FAC4FAD9-89AE-4E56-9608-03C56558B192"),
..generate_default_slot_entity()
},
generate_default_slot_entity(),
]))
});
let slot_service = dependencies.build_service();
let result = slot_service.get_slots().await;
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result.len(), 2);
assert_eq!(
result[0],
Slot {
id: uuid!("DA703BC1-F488-4E4F-BA10-0972196639F7"),
version: uuid!("FAC4FAD9-89AE-4E56-9608-03C56558B192"),
..generate_default_slot()
},
);
assert_eq!(result[1], generate_default_slot(),);
}
#[tokio::test]
async fn test_get_slots_sales_role() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slots()
.returning(|| Ok(Arc::new([])));
let slot_service = dependencies.build_service();
let result = slot_service.get_slots().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_get_slots_no_permission() {
let mut dependencies = build_dependencies(false, "hr");
dependencies
.slot_dao
.expect_get_slots()
.returning(|| Ok(Arc::new([])));
let slot_service = dependencies.build_service();
let result = slot_service.get_slots().await;
test_forbidden(&result);
}
#[tokio::test]
async fn test_get_slot() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.times(1)
.returning(|_| Ok(Some(generate_default_slot_entity())));
let slot_service = dependencies.build_service();
let result = slot_service.get_slot(&default_id()).await;
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result, generate_default_slot());
}
#[tokio::test]
async fn test_get_slot_sales_role() {
let mut dependencies = build_dependencies(true, "sales");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.times(1)
.returning(|_| Ok(Some(generate_default_slot_entity())));
let slot_service = dependencies.build_service();
let result = slot_service.get_slot(&default_id()).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_get_slot_not_found() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.times(1)
.returning(|_| Ok(None));
let slot_service = dependencies.build_service();
let result = slot_service.get_slot(&default_id()).await;
test_not_found(&result, &default_id());
}
#[tokio::test]
async fn test_get_slot_no_permission() {
let dependencies = build_dependencies(false, "hr");
let slot_service = dependencies.build_service();
let result = slot_service.get_slot(&default_id()).await;
test_forbidden(&result);
}
#[tokio::test]
async fn test_create_slot() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_create_slot()
.with(eq(generate_default_slot_entity()), eq("slot-service"))
.times(1)
.returning(|_, _| Ok(()));
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("slot-id"))
.returning(|_| default_id());
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("slot-version"))
.returning(|_| default_version());
dependencies
.slot_dao
.expect_get_slots()
.returning(|| Ok(Arc::new([])));
let slot_service = dependencies.build_service();
let result = slot_service
.create_slot(&Slot {
id: Uuid::nil(),
version: Uuid::nil(),
..generate_default_slot()
})
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), generate_default_slot());
}
#[tokio::test]
async fn test_create_slot_no_permission() {
let dependencies = build_dependencies(false, "hr");
let slot_service = dependencies.build_service();
let result = slot_service.create_slot(&generate_default_slot()).await;
test_forbidden(&result);
}
#[tokio::test]
async fn test_create_slot_non_zero_id() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("slot-id"))
.returning(|_| default_id());
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("slot-version"))
.returning(|_| default_version());
let slot_service = dependencies.build_service();
let result = slot_service
.create_slot(&Slot {
version: Uuid::nil(),
..generate_default_slot()
})
.await;
test_zero_id_error(&result);
}
#[tokio::test]
async fn test_create_slot_non_zero_version() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("slot-id"))
.returning(|_| default_id());
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("slot-version"))
.returning(|_| default_version());
let slot_service = dependencies.build_service();
let result = slot_service
.create_slot(&Slot {
id: Uuid::nil(),
..generate_default_slot()
})
.await;
test_zero_version_error(&result);
}
#[tokio::test]
async fn test_create_slot_intersects() {
let mut dependencies = build_dependencies(true, "hr");
dependencies.slot_dao.expect_get_slots().returning(|| {
Ok(Arc::new([
generate_default_slot_entity(),
SlotEntity {
id: Uuid::new_v4(),
from: Time::from_hms(12, 0, 0).unwrap(),
to: Time::from_hms(13, 0, 0).unwrap(),
..generate_default_slot_entity()
},
SlotEntity {
id: Uuid::new_v4(),
day_of_week: DayOfWeek::Wednesday.into(),
from: Time::from_hms(11, 0, 0).unwrap(),
to: Time::from_hms(12, 0, 0).unwrap(),
..generate_default_slot_entity()
},
]))
});
dependencies
.slot_dao
.expect_create_slot()
.returning(|_, _| Ok(()));
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("slot-id"))
.returning(|_| default_id());
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("slot-version"))
.returning(|_| default_version());
let slot_service = dependencies.build_service();
// Test successful case, directly between two existing slots.
let result = slot_service
.create_slot(&Slot {
id: Uuid::nil(),
version: Uuid::nil(),
from: Time::from_hms(11, 0, 0).unwrap(),
to: Time::from_hms(12, 0, 0).unwrap(),
..generate_default_slot()
})
.await;
assert!(result.is_ok());
// Test case where it is exactly on an existing slot.
let result = slot_service
.create_slot(&Slot {
id: Uuid::nil(),
version: Uuid::nil(),
from: Time::from_hms(10, 0, 0).unwrap(),
to: Time::from_hms(11, 0, 0).unwrap(),
..generate_default_slot()
})
.await;
test_overlapping_time_range_error(&result);
// Test case where from is inside an existing slot.
let result = slot_service
.create_slot(&Slot {
id: Uuid::nil(),
version: Uuid::nil(),
from: Time::from_hms(10, 30, 0).unwrap(),
to: Time::from_hms(11, 30, 0).unwrap(),
..generate_default_slot()
})
.await;
test_overlapping_time_range_error(&result);
// Test case where to is inside an existing slot.
let result = slot_service
.create_slot(&Slot {
id: Uuid::nil(),
version: Uuid::nil(),
from: Time::from_hms(11, 30, 0).unwrap(),
to: Time::from_hms(12, 30, 0).unwrap(),
..generate_default_slot()
})
.await;
test_overlapping_time_range_error(&result);
// Test case where is completely inside an existing slot.
let result = slot_service
.create_slot(&Slot {
id: Uuid::nil(),
version: Uuid::nil(),
from: Time::from_hms(10, 15, 0).unwrap(),
to: Time::from_hms(10, 45, 0).unwrap(),
..generate_default_slot()
})
.await;
test_overlapping_time_range_error(&result);
// Test case where is completely outside of an existing slot.
let result = slot_service
.create_slot(&Slot {
id: Uuid::nil(),
version: Uuid::nil(),
from: Time::from_hms(9, 0, 0).unwrap(),
to: Time::from_hms(11, 0, 0).unwrap(),
..generate_default_slot()
})
.await;
test_overlapping_time_range_error(&result);
// Test case where is would intersect on monday but not on tuesday.
// Test case where is completely outside of an existing slot.
let result = slot_service
.create_slot(&Slot {
id: Uuid::nil(),
version: Uuid::nil(),
day_of_week: DayOfWeek::Tuesday.into(),
..generate_default_slot()
})
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_create_slot_time_order() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_create_slot()
.returning(|_, _| Ok(()));
dependencies
.slot_dao
.expect_get_slots()
.returning(|| Ok(Arc::new([])));
let slot_service = dependencies.build_service();
let result = slot_service
.create_slot(&Slot {
id: Uuid::nil(),
version: Uuid::nil(),
from: Time::from_hms(12, 00, 0).unwrap(),
to: Time::from_hms(11, 00, 00).unwrap(),
..generate_default_slot()
})
.await;
test_time_order_wrong(&result);
}
#[tokio::test]
async fn test_create_slot_date_order() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_create_slot()
.returning(|_, _| Ok(()));
dependencies
.slot_dao
.expect_get_slots()
.returning(|| Ok(Arc::new([])));
let slot_service = dependencies.build_service();
let result = slot_service
.create_slot(&Slot {
id: Uuid::nil(),
version: Uuid::nil(),
valid_from: Date::from_calendar_date(2022, Month::January, 2).unwrap(),
valid_to: Some(Date::from_calendar_date(2022, Month::January, 1).unwrap()),
..generate_default_slot()
})
.await;
test_date_order_wrong(&result);
}
#[tokio::test]
async fn test_delete_slot() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.times(1)
.returning(|_| Ok(Some(generate_default_slot_entity())));
dependencies
.slot_dao
.expect_update_slot()
.with(
eq(SlotEntity {
deleted: Some(PrimitiveDateTime::new(
Date::from_calendar_date(2063, time::Month::April, 5).unwrap(),
Time::from_hms(23, 42, 0).unwrap(),
)),
..generate_default_slot_entity()
}),
eq("slot-service"),
)
.times(1)
.returning(|_, _| Ok(()));
let slot_service = dependencies.build_service();
let result = slot_service.delete_slot(&default_id()).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_delete_slot_no_permission() {
let dependencies = build_dependencies(false, "hr");
let slot_service = dependencies.build_service();
let result = slot_service.delete_slot(&default_id()).await;
test_forbidden(&result);
}
#[tokio::test]
async fn test_delete_slot_not_found() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.times(1)
.returning(|_| Ok(None));
let slot_service = dependencies.build_service();
let result = slot_service.delete_slot(&default_id()).await;
test_not_found(&result, &default_id());
}
#[tokio::test]
async fn test_update_slot_no_permission() {
let dependencies = build_dependencies(false, "hr");
let slot_service = dependencies.build_service();
let result = slot_service.update_slot(&generate_default_slot()).await;
test_forbidden(&result);
}
#[tokio::test]
async fn test_update_slot_not_found() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.times(1)
.returning(|_| Ok(None));
let slot_service = dependencies.build_service();
let result = slot_service.update_slot(&generate_default_slot()).await;
test_not_found(&result, &default_id());
}
#[tokio::test]
async fn test_update_slot_version_mismatch() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.returning(|_| Ok(Some(generate_default_slot_entity())));
let slot_service = dependencies.build_service();
let result = slot_service
.update_slot(&service::slot::Slot {
version: uuid!("86DE856C-D176-4F1F-A4FE-0D9844C02C04"),
..generate_default_slot()
})
.await;
test_conflicts(
&result,
&default_id(),
&default_version(),
&uuid!("86DE856C-D176-4F1F-A4FE-0D9844C02C04"),
);
}
#[tokio::test]
async fn test_update_slot_valid_to() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_update_slot()
.once()
.with(
eq(dao::slot::SlotEntity {
valid_to: Some(
time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 10).unwrap(),
),
version: default_changed_version(),
..generate_default_slot_entity()
}),
eq("slot-service"),
)
.times(1)
.returning(|_, _| Ok(()));
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.returning(|_| Ok(Some(generate_default_slot_entity())));
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("slot-version"))
.returning(|_| default_changed_version());
let slot_service = dependencies.build_service();
let result = slot_service
.update_slot(&service::slot::Slot {
valid_to: Some(
time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 10).unwrap(),
),
..generate_default_slot()
})
.await;
dbg!(&result);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_update_slot_valid_to_before_valid_from() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.returning(|_| Ok(Some(generate_default_slot_entity())));
let slot_service = dependencies.build_service();
let result = slot_service
.update_slot(&service::slot::Slot {
valid_to: Some(
time::Date::from_calendar_date(2021, 1.try_into().unwrap(), 10).unwrap(),
),
..generate_default_slot()
})
.await;
test_date_order_wrong(&result);
}
#[tokio::test]
async fn test_update_slot_deleted() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.returning(|_| Ok(Some(generate_default_slot_entity())));
dependencies
.slot_dao
.expect_update_slot()
.once()
.with(
eq(dao::slot::SlotEntity {
deleted: Some(time::PrimitiveDateTime::new(
Date::from_calendar_date(2022, 1.try_into().unwrap(), 10).unwrap(),
Time::from_hms(0, 0, 0).unwrap(),
)),
version: default_changed_version(),
..generate_default_slot_entity()
}),
eq("slot-service"),
)
.returning(|_, _| Ok(()));
dependencies
.uuid_service
.expect_new_uuid()
.with(eq("slot-version"))
.returning(|_| default_changed_version());
let slot_service = dependencies.build_service();
let result = slot_service
.update_slot(&Slot {
deleted: Some(time::PrimitiveDateTime::new(
Date::from_calendar_date(2022, 1.try_into().unwrap(), 10).unwrap(),
Time::from_hms(0, 0, 0).unwrap(),
)),
..generate_default_slot()
})
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_update_slot_day_of_week_forbidden() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.returning(|_| Ok(Some(generate_default_slot_entity())));
let slot_service = dependencies.build_service();
let result = slot_service
.update_slot(&service::slot::Slot {
day_of_week: service::slot::DayOfWeek::Friday,
..generate_default_slot()
})
.await;
test_validation_error(
&result,
&ValidationFailureItem::ModificationNotAllowed("day_of_week".into()),
1,
);
}
#[tokio::test]
async fn test_update_to_forbidden_when_not_none() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.returning(|_| {
Ok(Some(SlotEntity {
valid_to: Some(
time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 3).unwrap(),
),
..generate_default_slot_entity()
}))
});
let slot_service = dependencies.build_service();
let result = slot_service
.update_slot(&service::slot::Slot {
valid_to: Some(time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 4).unwrap()),
..generate_default_slot()
})
.await;
test_validation_error(
&result,
&ValidationFailureItem::ModificationNotAllowed("valid_to".into()),
1,
);
}
#[tokio::test]
async fn test_update_from_forbidden() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.returning(|_| Ok(Some(generate_default_slot_entity())));
let slot_service = dependencies.build_service();
let result = slot_service
.update_slot(&service::slot::Slot {
from: time::Time::from_hms(14, 0, 0).unwrap(),
..generate_default_slot()
})
.await;
test_validation_error(
&result,
&ValidationFailureItem::ModificationNotAllowed("from".into()),
1,
);
}
#[tokio::test]
async fn test_update_to_forbidden() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.returning(|_| Ok(Some(generate_default_slot_entity())));
let slot_service = dependencies.build_service();
let result = slot_service
.update_slot(&service::slot::Slot {
to: time::Time::from_hms(14, 0, 0).unwrap(),
..generate_default_slot()
})
.await;
test_validation_error(
&result,
&ValidationFailureItem::ModificationNotAllowed("to".into()),
1,
);
}
#[tokio::test]
async fn test_update_valid_from_forbidden() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.returning(|_| Ok(Some(generate_default_slot_entity())));
let slot_service = dependencies.build_service();
let result = slot_service
.update_slot(&service::slot::Slot {
valid_from: time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 10).unwrap(),
..generate_default_slot()
})
.await;
test_validation_error(
&result,
&ValidationFailureItem::ModificationNotAllowed("valid_from".into()),
1,
);
}
#[tokio::test]
async fn test_update_valid_multiple_forbidden_changes() {
let mut dependencies = build_dependencies(true, "hr");
dependencies
.slot_dao
.expect_get_slot()
.with(eq(default_id()))
.returning(|_| Ok(Some(generate_default_slot_entity())));
let slot_service = dependencies.build_service();
let result = slot_service
.update_slot(&service::slot::Slot {
valid_from: time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 10).unwrap(),
from: time::Time::from_hms(14, 0, 0).unwrap(),
..generate_default_slot()
})
.await;
test_validation_error(
&result,
&ValidationFailureItem::ModificationNotAllowed("valid_from".into()),
2,
);
test_validation_error(
&result,
&ValidationFailureItem::ModificationNotAllowed("from".into()),
2,
);
}

View file

@ -0,0 +1,9 @@
use uuid::Uuid;
pub struct UuidServiceImpl;
impl service::uuid_service::UuidService for UuidServiceImpl {
fn new_uuid(&self, _usage: &str) -> Uuid {
Uuid::new_v4()
}
}