Implement booking service without validity checks

This commit is contained in:
Simon Goller 2024-05-07 08:31:50 +02:00
parent 8efc3843ad
commit 4bca60a23c
8 changed files with 536 additions and 0 deletions

28
dao/src/booking.rs Normal file
View file

@ -0,0 +1,28 @@
use std::sync::Arc;
use async_trait::async_trait;
use mockall::automock;
use time::PrimitiveDateTime;
use uuid::Uuid;
use crate::DaoError;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BookingEntity {
pub id: Uuid,
pub sales_person_id: Uuid,
pub slot_id: Uuid,
pub calendar_week: i32,
pub year: u32,
pub deleted: Option<PrimitiveDateTime>,
pub version: Uuid,
}
#[automock]
#[async_trait]
pub trait BookingDao {
async fn all(&self) -> Result<Arc<[BookingEntity]>, DaoError>;
async fn find_by_id(&self, id: Uuid) -> Result<Option<BookingEntity>, DaoError>;
async fn create(&self, entity: &BookingEntity, process: &str) -> Result<(), DaoError>;
async fn update(&self, entity: &BookingEntity, process: &str) -> Result<(), DaoError>;
}

View file

@ -4,6 +4,7 @@ use async_trait::async_trait;
use mockall::automock; use mockall::automock;
use thiserror::Error; use thiserror::Error;
pub mod booking;
pub mod permission; pub mod permission;
pub mod sales_person; pub mod sales_person;
pub mod slot; pub mod slot;

60
service/src/booking.rs Normal file
View file

@ -0,0 +1,60 @@
use std::sync::Arc;
use async_trait::async_trait;
use time::PrimitiveDateTime;
use uuid::Uuid;
use crate::ServiceError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Booking {
pub id: Uuid,
pub sales_person_id: Uuid,
pub slot_id: Uuid,
pub calendar_week: i32,
pub year: u32,
pub deleted: Option<PrimitiveDateTime>,
pub version: Uuid,
}
impl From<&dao::booking::BookingEntity> for Booking {
fn from(booking: &dao::booking::BookingEntity) -> Self {
Self {
id: booking.id,
sales_person_id: booking.sales_person_id,
slot_id: booking.slot_id,
calendar_week: booking.calendar_week,
year: booking.year,
deleted: booking.deleted,
version: booking.version,
}
}
}
impl From<&Booking> for dao::booking::BookingEntity {
fn from(booking: &Booking) -> Self {
Self {
id: booking.id,
sales_person_id: booking.sales_person_id,
slot_id: booking.slot_id,
calendar_week: booking.calendar_week,
year: booking.year,
deleted: booking.deleted,
version: booking.version,
}
}
}
#[async_trait]
pub trait BookingService {
type Context: Clone + Send + Sync;
async fn get_all(&self, context: Self::Context) -> Result<Arc<[Booking]>, ServiceError>;
async fn get(&self, id: Uuid, context: Self::Context) -> Result<Booking, ServiceError>;
async fn create(
&self,
booking: &Booking,
context: Self::Context,
) -> Result<Booking, ServiceError>;
async fn delete(&self, id: Uuid, context: Self::Context) -> Result<(), ServiceError>;
}

View file

@ -4,6 +4,7 @@ use time::Date;
use time::Time; use time::Time;
use uuid::Uuid; use uuid::Uuid;
pub mod booking;
pub mod clock; pub mod clock;
pub mod permission; pub mod permission;
pub mod sales_person; pub mod sales_person;

131
service_impl/src/booking.rs Normal file
View file

@ -0,0 +1,131 @@
use async_trait::async_trait;
use service::{
booking::{Booking, BookingService},
ServiceError,
};
use std::sync::Arc;
use uuid::Uuid;
const BOOKING_SERVICE_PROCESS: &str = "booking-service";
pub struct BookingServiceImpl<BookingDao, PermissionService, ClockService, UuidService>
where
BookingDao: dao::booking::BookingDao + Send + Sync,
PermissionService: service::permission::PermissionService + Send + Sync,
UuidService: service::uuid_service::UuidService + Send + Sync,
ClockService: service::clock::ClockService + Send + Sync,
{
pub booking_dao: Arc<BookingDao>,
pub permission_service: Arc<PermissionService>,
pub clock_service: Arc<ClockService>,
pub uuid_service: Arc<UuidService>,
}
impl<BookingDao, PermissionService, ClockService, UuidService>
BookingServiceImpl<BookingDao, PermissionService, ClockService, UuidService>
where
BookingDao: dao::booking::BookingDao + Send + Sync,
PermissionService: service::permission::PermissionService + Send + Sync,
UuidService: service::uuid_service::UuidService + Send + Sync,
ClockService: service::clock::ClockService + Send + Sync,
{
pub fn new(
booking_dao: Arc<BookingDao>,
permission_service: Arc<PermissionService>,
clock_service: Arc<ClockService>,
uuid_service: Arc<UuidService>,
) -> Self {
Self {
booking_dao,
permission_service,
clock_service,
uuid_service,
}
}
}
#[async_trait]
impl<BookingDao, PermissionService, ClockService, UuidService> BookingService
for BookingServiceImpl<BookingDao, PermissionService, ClockService, UuidService>
where
BookingDao: dao::booking::BookingDao + Send + Sync,
PermissionService: service::permission::PermissionService + Send + Sync,
UuidService: service::uuid_service::UuidService + Send + Sync,
ClockService: service::clock::ClockService + Send + Sync,
{
type Context = PermissionService::Context;
async fn get_all(&self, context: Self::Context) -> Result<Arc<[Booking]>, ServiceError> {
self.permission_service
.check_permission("hr", context)
.await?;
Ok(self
.booking_dao
.all()
.await?
.iter()
.map(Booking::from)
.collect())
}
async fn get(&self, id: Uuid, context: Self::Context) -> Result<Booking, ServiceError> {
self.permission_service
.check_permission("hr", context)
.await?;
let booking_entity = self.booking_dao.find_by_id(id).await?;
let booking = booking_entity
.as_ref()
.map(Booking::from)
.ok_or_else(move || ServiceError::EntityNotFound(id))?;
Ok(booking)
}
async fn create(
&self,
booking: &Booking,
context: Self::Context,
) -> Result<Booking, ServiceError> {
self.permission_service
.check_permission("hr", context)
.await?;
if booking.id != Uuid::nil() {
return Err(ServiceError::IdSetOnCreate);
}
if booking.version != Uuid::nil() {
return Err(ServiceError::VersionSetOnCreate);
}
let new_id = self.uuid_service.new_uuid("booking-id");
let new_version = self.uuid_service.new_uuid("booking-version");
let new_booking = Booking {
id: new_id,
version: new_version,
..booking.clone()
};
self.booking_dao
.create(&(&new_booking).into(), BOOKING_SERVICE_PROCESS)
.await?;
Ok(new_booking)
}
async fn delete(&self, id: Uuid, context: Self::Context) -> Result<(), ServiceError> {
self.permission_service
.check_permission("hr", context)
.await?;
let mut booking_entity = self
.booking_dao
.find_by_id(id)
.await?
.ok_or_else(move || ServiceError::EntityNotFound(id))?;
booking_entity.deleted = Some(self.clock_service.date_time_now());
booking_entity.version = self.uuid_service.new_uuid("booking-version");
self.booking_dao
.update(&booking_entity, BOOKING_SERVICE_PROCESS)
.await?;
Ok(())
}
}

View file

@ -2,6 +2,7 @@ use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
pub mod booking;
pub mod clock; pub mod clock;
pub mod permission; pub mod permission;
pub mod sales_person; pub mod sales_person;

View file

@ -0,0 +1,312 @@
use crate::test::error_test::*;
use dao::booking::{BookingEntity, MockBookingDao};
use mockall::predicate::eq;
use service::{
booking::Booking, clock::MockClockService, uuid_service::MockUuidService, MockPermissionService,
};
use time::{Date, Month, PrimitiveDateTime, Time};
use uuid::{uuid, Uuid};
use crate::booking::BookingServiceImpl;
use service::booking::BookingService;
pub fn default_id() -> Uuid {
uuid!("CEA260A0-112B-4970-936C-F7E529955BD0")
}
pub fn alternate_id() -> Uuid {
uuid!("CEA260A0-112B-4970-936C-F7E529955BD1")
}
pub fn default_version() -> Uuid {
uuid!("F79C462A-8D4E-42E1-8171-DB4DBD019E50")
}
pub fn alternate_version() -> Uuid {
uuid!("F79C462A-8D4E-42E1-8171-DB4DBD019E51")
}
pub fn default_sales_person_id() -> Uuid {
uuid!("04215DFE-13C4-413C-8C66-77AC741BB5F0")
}
pub fn default_slot_id() -> Uuid {
uuid!("7A7FF57A-782B-4C2E-A68B-4E2D81D79380")
}
pub fn default_booking() -> Booking {
Booking {
id: default_id(),
sales_person_id: default_sales_person_id(),
slot_id: default_slot_id(),
calendar_week: 3,
year: 2024,
deleted: None,
version: default_version(),
}
}
pub fn default_booking_entity() -> BookingEntity {
BookingEntity {
id: default_id(),
sales_person_id: default_sales_person_id(),
slot_id: default_slot_id(),
calendar_week: 3,
year: 2024,
deleted: None,
version: default_version(),
}
}
pub struct BookingServiceDependencies {
pub booking_dao: MockBookingDao,
pub permission_service: MockPermissionService,
pub clock_service: MockClockService,
pub uuid_service: MockUuidService,
}
impl BookingServiceDependencies {
pub fn build_service(
self,
) -> BookingServiceImpl<MockBookingDao, MockPermissionService, MockClockService, MockUuidService>
{
BookingServiceImpl::new(
self.booking_dao.into(),
self.permission_service.into(),
self.clock_service.into(),
self.uuid_service.into(),
)
}
}
pub fn build_dependencies(permission: bool, role: &'static str) -> BookingServiceDependencies {
let booking_dao = MockBookingDao::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();
BookingServiceDependencies {
booking_dao,
permission_service,
clock_service,
uuid_service,
}
}
#[tokio::test]
async fn test_get_all() {
let mut deps = build_dependencies(true, "hr");
deps.booking_dao.expect_all().returning(|| {
Ok([
default_booking_entity(),
BookingEntity {
id: alternate_id(),
..default_booking_entity()
},
]
.into())
});
let service = deps.build_service();
let result = service.get_all(()).await;
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0], default_booking());
assert_eq!(
result[1],
Booking {
id: alternate_id(),
..default_booking()
}
);
}
#[tokio::test]
async fn test_get_all_no_permission() {
let deps = build_dependencies(false, "hr");
let service = deps.build_service();
let result = service.get_all(()).await;
test_forbidden(&result);
}
#[tokio::test]
async fn test_get() {
let mut deps = build_dependencies(true, "hr");
deps.booking_dao
.expect_find_by_id()
.with(eq(default_id()))
.returning(|_| Ok(Some(default_booking_entity())));
let service = deps.build_service();
let result = service.get(default_id(), ()).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), default_booking());
}
#[tokio::test]
async fn test_get_not_found() {
let mut deps = build_dependencies(true, "hr");
deps.booking_dao
.expect_find_by_id()
.with(eq(default_id()))
.returning(|_| Ok(None));
let service = deps.build_service();
let result = service.get(default_id(), ()).await;
test_not_found(&result, &default_id());
}
#[tokio::test]
async fn test_get_no_permission() {
let deps = build_dependencies(false, "hr");
let service = deps.build_service();
let result = service.get(default_id(), ()).await;
test_forbidden(&result);
}
#[tokio::test]
async fn test_create() {
let mut deps = build_dependencies(true, "hr");
deps.booking_dao
.expect_create()
.with(eq(default_booking_entity()), eq("booking-service"))
.returning(|_, _| Ok(()));
deps.uuid_service
.expect_new_uuid()
.with(eq("booking-id"))
.returning(|_| default_id());
deps.uuid_service
.expect_new_uuid()
.with(eq("booking-version"))
.returning(|_| default_version());
let service = deps.build_service();
let result = service
.create(
&Booking {
id: Uuid::nil(),
version: Uuid::nil(),
..default_booking()
},
(),
)
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), default_booking());
}
#[tokio::test]
async fn test_create_no_permission() {
let deps = build_dependencies(false, "hr");
let service = deps.build_service();
let result = service
.create(
&Booking {
id: Uuid::nil(),
version: Uuid::nil(),
..default_booking()
},
(),
)
.await;
test_forbidden(&result);
}
#[tokio::test]
async fn test_create_with_id() {
let deps = build_dependencies(true, "hr");
let service = deps.build_service();
let result = service
.create(
&Booking {
version: Uuid::nil(),
..default_booking()
},
(),
)
.await;
test_zero_id_error(&result);
}
#[tokio::test]
async fn test_create_with_version() {
let deps = build_dependencies(true, "hr");
let service = deps.build_service();
let result = service
.create(
&Booking {
id: Uuid::nil(),
..default_booking()
},
(),
)
.await;
test_zero_version_error(&result);
}
#[tokio::test]
async fn test_delete_no_permission() {
let deps = build_dependencies(false, "hr");
let service = deps.build_service();
let result = service.delete(default_id(), ()).await;
test_forbidden(&result);
}
#[tokio::test]
async fn test_delete_not_found() {
let mut deps = build_dependencies(true, "hr");
deps.booking_dao
.expect_find_by_id()
.with(eq(default_id()))
.returning(|_| Ok(None));
let service = deps.build_service();
let result = service.delete(default_id(), ()).await;
test_not_found(&result, &default_id());
}
#[tokio::test]
async fn test_delete() {
let mut deps = build_dependencies(true, "hr");
deps.booking_dao
.expect_find_by_id()
.with(eq(default_id()))
.returning(|_| Ok(Some(default_booking_entity())));
deps.booking_dao
.expect_update()
.with(
eq(BookingEntity {
deleted: Some(PrimitiveDateTime::new(
Date::from_calendar_date(2063, Month::April, 5).unwrap(),
Time::from_hms(23, 42, 0).unwrap(),
)),
version: alternate_version(),
..default_booking_entity()
}),
eq("booking-service"),
)
.returning(|_, _| Ok(()));
deps.uuid_service
.expect_new_uuid()
.with(eq("booking-version"))
.returning(|_| alternate_version());
let service = deps.build_service();
let result = service.delete(default_id(), ()).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), ());
}

View file

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