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

64
Cargo.lock generated
View file

@ -51,6 +51,8 @@ dependencies = [
"rest", "rest",
"service_impl", "service_impl",
"sqlx", "sqlx",
"time",
"time-macros",
"tokio", "tokio",
] ]
@ -272,6 +274,7 @@ dependencies = [
"async-trait", "async-trait",
"mockall", "mockall",
"thiserror", "thiserror",
"time",
"uuid", "uuid",
] ]
@ -282,6 +285,8 @@ dependencies = [
"async-trait", "async-trait",
"dao", "dao",
"sqlx", "sqlx",
"time",
"uuid",
] ]
[[package]] [[package]]
@ -295,6 +300,16 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
"serde",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -849,6 +864,12 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.46" version = "0.1.46"
@ -1007,6 +1028,12 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -1115,6 +1142,8 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"service", "service",
"thiserror",
"time",
"tokio", "tokio",
"uuid", "uuid",
] ]
@ -1237,7 +1266,9 @@ dependencies = [
"dao", "dao",
"mockall", "mockall",
"thiserror", "thiserror",
"time",
"tokio", "tokio",
"uuid",
] ]
[[package]] [[package]]
@ -1248,7 +1279,9 @@ dependencies = [
"dao", "dao",
"mockall", "mockall",
"service", "service",
"time",
"tokio", "tokio",
"uuid",
] ]
[[package]] [[package]]
@ -1635,6 +1668,37 @@ dependencies = [
"syn 2.0.60", "syn 2.0.60",
] ]
[[package]]
name = "time"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.6.0" version = "1.6.0"

View file

@ -1,2 +1,3 @@
[workspace] [workspace]
members = ["rest", "service", "service_impl", "app", "dao", "dao_impl"] members = ["rest", "service", "service_impl", "app", "dao", "dao_impl"]
resolver = "2"

View file

@ -24,3 +24,9 @@ features = ["full"]
[dependencies.sqlx] [dependencies.sqlx]
version = "0.7.4" version = "0.7.4"
features = ["runtime-tokio", "sqlite"] features = ["runtime-tokio", "sqlite"]
[dependencies.time]
version = "0.3.36"
[dependencies.time-macros]
version = "0.2.18"

View file

@ -5,15 +5,25 @@ use sqlx::SqlitePool;
type PermissionService = type PermissionService =
service_impl::PermissionServiceImpl<dao_impl::PermissionDaoImpl, service_impl::UserServiceDev>; service_impl::PermissionServiceImpl<dao_impl::PermissionDaoImpl, service_impl::UserServiceDev>;
type HelloService = service_impl::HelloServiceImpl<dao_impl::HelloDaoImpl, PermissionService>; type HelloService = service_impl::HelloServiceImpl<dao_impl::HelloDaoImpl, PermissionService>;
type ClockService = service_impl::clock::ClockServiceImpl;
type UuidService = service_impl::uuid_service::UuidServiceImpl;
type SlotService = service_impl::slot::SlotServiceImpl<
dao_impl::slot::SlotDaoImpl,
PermissionService,
ClockService,
UuidService,
>;
#[derive(Clone)] #[derive(Clone)]
pub struct RestStateImpl { pub struct RestStateImpl {
hello_service: Arc<HelloService>, hello_service: Arc<HelloService>,
permission_service: Arc<PermissionService>, permission_service: Arc<PermissionService>,
slot_service: Arc<SlotService>,
} }
impl rest::RestStateDef for RestStateImpl { impl rest::RestStateDef for RestStateImpl {
type HelloService = HelloService; type HelloService = HelloService;
type PermissionService = PermissionService; type PermissionService = PermissionService;
type SlotService = SlotService;
fn hello_service(&self) -> Arc<Self::HelloService> { fn hello_service(&self) -> Arc<Self::HelloService> {
self.hello_service.clone() self.hello_service.clone()
@ -21,11 +31,15 @@ impl rest::RestStateDef for RestStateImpl {
fn permission_service(&self) -> Arc<Self::PermissionService> { fn permission_service(&self) -> Arc<Self::PermissionService> {
self.permission_service.clone() self.permission_service.clone()
} }
fn slot_service(&self) -> Arc<Self::SlotService> {
self.slot_service.clone()
}
} }
impl RestStateImpl { impl RestStateImpl {
pub fn new(pool: Arc<sqlx::Pool<sqlx::Sqlite>>) -> Self { pub fn new(pool: Arc<sqlx::Pool<sqlx::Sqlite>>) -> Self {
let hello_dao = dao_impl::HelloDaoImpl::new(pool.clone()); let hello_dao = dao_impl::HelloDaoImpl::new(pool.clone());
let permission_dao = dao_impl::PermissionDaoImpl::new(pool); let permission_dao = dao_impl::PermissionDaoImpl::new(pool.clone());
let slot_dao = dao_impl::slot::SlotDaoImpl::new(pool);
// Always authenticate with DEVUSER during development. // Always authenticate with DEVUSER during development.
// This is used to test the permission service locally without a login service. // This is used to test the permission service locally without a login service.
@ -42,9 +56,18 @@ impl RestStateImpl {
hello_dao.into(), hello_dao.into(),
permission_service.clone(), permission_service.clone(),
)); ));
let clock_service = Arc::new(service_impl::clock::ClockServiceImpl);
let uuid_service = Arc::new(service_impl::uuid_service::UuidServiceImpl);
let slot_service = Arc::new(service_impl::slot::SlotServiceImpl::new(
slot_dao.into(),
permission_service.clone(),
clock_service,
uuid_service,
));
Self { Self {
hello_service, hello_service,
permission_service, permission_service,
slot_service,
} }
} }
} }

View file

@ -10,3 +10,7 @@ async-trait = "0.1.80"
mockall = "0.12.1" mockall = "0.12.1"
thiserror = "1.0.59" thiserror = "1.0.59"
uuid = "1.8" uuid = "1.8"
[dependencies.time]
version = "0.3.36"
features = ["parsing"]

View file

@ -4,7 +4,8 @@ use async_trait::async_trait;
use mockall::automock; use mockall::automock;
use thiserror::Error; use thiserror::Error;
mod permission; pub mod permission;
pub mod slot;
pub use permission::MockPermissionDao; pub use permission::MockPermissionDao;
pub use permission::PermissionDao; pub use permission::PermissionDao;
@ -15,7 +16,16 @@ pub use permission::UserEntity;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum DaoError { pub enum DaoError {
#[error("Database query error: {0}")] #[error("Database query error: {0}")]
DatabaseQueryError(#[from] Box<dyn std::error::Error>), DatabaseQueryError(#[from] Box<dyn std::error::Error + Send + Sync>),
#[error("Uuid error: {0}")]
UuidError(#[from] uuid::Error),
#[error("Invalid day of week number: {0}")]
InvalidDayOfWeek(u8),
#[error("Date/Time parse error: {0}")]
DateTimeParseError(#[from] time::error::Parse),
} }
#[automock] #[automock]

65
dao/src/slot.rs Normal file
View file

@ -0,0 +1,65 @@
use std::sync::Arc;
use async_trait::async_trait;
use mockall::automock;
use uuid::Uuid;
use crate::DaoError;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum DayOfWeek {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
impl DayOfWeek {
pub fn from_number(number: u8) -> Option<Self> {
match number {
1 => Some(DayOfWeek::Monday),
2 => Some(DayOfWeek::Tuesday),
3 => Some(DayOfWeek::Wednesday),
4 => Some(DayOfWeek::Thursday),
5 => Some(DayOfWeek::Friday),
6 => Some(DayOfWeek::Saturday),
7 => Some(DayOfWeek::Sunday),
_ => None,
}
}
pub fn to_number(&self) -> u8 {
match self {
DayOfWeek::Monday => 1,
DayOfWeek::Tuesday => 2,
DayOfWeek::Wednesday => 3,
DayOfWeek::Thursday => 4,
DayOfWeek::Friday => 5,
DayOfWeek::Saturday => 6,
DayOfWeek::Sunday => 7,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct SlotEntity {
pub id: Uuid,
pub day_of_week: DayOfWeek,
pub from: time::Time,
pub to: time::Time,
pub valid_from: time::Date,
pub valid_to: Option<time::Date>,
pub deleted: Option<time::PrimitiveDateTime>,
pub version: Uuid,
}
#[automock]
#[async_trait]
pub trait SlotDao {
async fn get_slots(&self) -> Result<Arc<[SlotEntity]>, DaoError>;
async fn get_slot(&self, id: &Uuid) -> Result<Option<SlotEntity>, DaoError>;
async fn create_slot(&self, slot: &SlotEntity, process: &str) -> Result<(), DaoError>;
//async fn delete_slot(&self, id: &Uuid, process: &str) -> Result<(), DaoError>;
async fn update_slot(&self, slot: &SlotEntity, process: &str) -> Result<(), DaoError>;
}

View file

@ -5,6 +5,7 @@ edition = "2021"
[dependencies] [dependencies]
async-trait = "0.1.80" async-trait = "0.1.80"
uuid = "1.8.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -14,3 +15,7 @@ features = ["runtime-tokio", "sqlite"]
[dependencies.dao] [dependencies.dao]
path = "../dao" path = "../dao"
[dependencies.time]
version = "0.3.36"
features = ["parsing"]

View file

@ -4,10 +4,12 @@ use async_trait::async_trait;
use dao::DaoError; use dao::DaoError;
use sqlx::{query, query_as, SqlitePool}; use sqlx::{query, query_as, SqlitePool};
pub mod slot;
pub trait ResultDbErrorExt<T, E> { pub trait ResultDbErrorExt<T, E> {
fn map_db_error(self) -> Result<T, DaoError>; fn map_db_error(self) -> Result<T, DaoError>;
} }
impl<T, E: std::error::Error + 'static> ResultDbErrorExt<T, E> for Result<T, E> { impl<T, E: std::error::Error + Send + Sync + 'static> ResultDbErrorExt<T, E> for Result<T, E> {
fn map_db_error(self) -> Result<T, DaoError> { fn map_db_error(self) -> Result<T, DaoError> {
self.map_err(|err| DaoError::DatabaseQueryError(Box::new(err))) self.map_err(|err| DaoError::DatabaseQueryError(Box::new(err)))
} }

128
dao_impl/src/slot.rs Normal file
View file

@ -0,0 +1,128 @@
use std::sync::Arc;
use async_trait::async_trait;
use dao::{
slot::{DayOfWeek, SlotEntity},
DaoError,
};
use sqlx::{query, SqlitePool};
use time::{format_description::well_known::Iso8601, Date, PrimitiveDateTime, Time};
use uuid::Uuid;
use crate::ResultDbErrorExt;
pub struct SlotDaoImpl {
pool: Arc<SqlitePool>,
}
impl SlotDaoImpl {
pub fn new(pool: Arc<SqlitePool>) -> Self {
Self { pool }
}
}
#[async_trait]
impl dao::slot::SlotDao for SlotDaoImpl {
async fn get_slots(&self) -> Result<Arc<[SlotEntity]>, DaoError> {
let result = query!(r"SELECT id, day_of_week, time_from, time_to, valid_from, valid_to, deleted, update_version FROM slot WHERE deleted IS NULL")
.fetch_all(self.pool.as_ref())
.await
.map_err(|err| DaoError::DatabaseQueryError(Box::new(err)))?;
result
.iter()
.map(|row| {
Ok(SlotEntity {
id: Uuid::from_slice(row.id.as_ref())?,
day_of_week: DayOfWeek::from_number(row.day_of_week as u8)
.ok_or(DaoError::InvalidDayOfWeek(row.day_of_week as u8))?,
from: Time::parse(&row.time_from, &Iso8601::TIME)?,
to: Time::parse(&row.time_to, &Iso8601::TIME)?,
valid_from: Date::parse(&row.valid_from, &Iso8601::DATE)?,
valid_to: row
.valid_to
.as_ref()
.map(|valid_to| Date::parse(valid_to, &Iso8601::DATE))
.transpose()?,
deleted: row
.deleted
.as_ref()
.map(|deleted| PrimitiveDateTime::parse(deleted, &Iso8601::DATE))
.transpose()?,
version: Uuid::from_slice(&row.update_version)?,
})
})
.collect()
}
async fn get_slot(&self, id: &Uuid) -> Result<Option<SlotEntity>, DaoError> {
let id_vec = id.as_bytes().to_vec();
let result = query!(r"SELECT id, day_of_week, time_from, time_to, valid_from, valid_to, deleted, update_version FROM slot WHERE id = ?", id_vec)
.fetch_optional(self.pool.as_ref())
.await
.map_err(|err| DaoError::DatabaseQueryError(Box::new(err)))?;
result
.map(|row| {
Ok(SlotEntity {
id: Uuid::from_slice(row.id.as_ref())?,
day_of_week: DayOfWeek::from_number(row.day_of_week as u8)
.ok_or(DaoError::InvalidDayOfWeek(row.day_of_week as u8))?,
from: Time::parse(&row.time_from, &Iso8601::TIME)?,
to: Time::parse(&row.time_to, &Iso8601::TIME)?,
valid_from: Date::parse(&row.valid_from, &Iso8601::DATE)?,
valid_to: row
.valid_to
.as_ref()
.map(|valid_to| Date::parse(valid_to, &Iso8601::DATE))
.transpose()?,
deleted: row
.deleted
.as_ref()
.map(|deleted| PrimitiveDateTime::parse(deleted, &Iso8601::DATE))
.transpose()?,
version: Uuid::from_slice(&row.update_version)?,
})
})
.transpose()
}
async fn create_slot(&self, slot: &SlotEntity, process: &str) -> Result<(), DaoError> {
let id_vec = slot.id.as_bytes().to_vec();
let version_vec = slot.version.as_bytes().to_vec();
let day_of_week = slot.day_of_week.to_number();
let from = slot.from.to_string();
let to = slot.to.to_string();
let valid_from = slot.valid_from.to_string();
let valid_to = slot.valid_to.map(|valid_to| valid_to.to_string());
let deleted = slot.deleted.map(|deleted| deleted.to_string());
query!("INSERT INTO slot (id, day_of_week, time_from, time_to, valid_from, valid_to, deleted, update_version, update_process) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
id_vec,
day_of_week,
from,
to,
valid_from,
valid_to,
deleted,
version_vec,
process,
)
.execute(self.pool.as_ref())
.await
.map_db_error()?;
Ok(())
}
async fn update_slot(&self, slot: &SlotEntity, process: &str) -> Result<(), DaoError> {
let id_vec = slot.id.as_bytes().to_vec();
let version_vec = slot.version.as_bytes().to_vec();
let valid_to = slot.valid_to.map(|valid_to| valid_to.to_string());
let deleted = slot.deleted.map(|deleted| deleted.to_string());
query!("UPDATE slot SET valid_to = ?, deleted = ?, update_version = ?, update_process = ? WHERE id = ?",
valid_to,
deleted,
version_vec,
process,
id_vec,
)
.execute(self.pool.as_ref())
.await
.map_db_error()?;
Ok(())
}
}

View file

@ -0,0 +1,15 @@
-- Add migration script here
CREATE TABLE slot (
id blob(16) NOT NULL PRIMARY KEY,
day_of_week INTEGER NOT NULL,
time_from TEXT NOT NULL,
time_to TEXT NOT NULL,
valid_from TEXT NOT NULL,
valid_to TEXT,
deleted TEXT,
update_timestamp TEXT,
update_process TEXT NOT NULL,
update_version blob(16) NOT NULL
);

View file

@ -9,8 +9,8 @@ edition = "2021"
axum = "0.7.5" axum = "0.7.5"
bytes = "1.6.0" bytes = "1.6.0"
http-body = "1.0.0" http-body = "1.0.0"
serde = "1.0.198"
serde_json = "1.0.116" serde_json = "1.0.116"
time = { version = "0.3.36", features = ["serde-human-readable"] }
[dependencies.tokio] [dependencies.tokio]
version = "1.37.0" version = "1.37.0"
@ -22,3 +22,10 @@ path = "../service"
[dependencies.uuid] [dependencies.uuid]
version = "1.8.0" version = "1.8.0"
features = ["v4", "serde"] features = ["v4", "serde"]
[dependencies.serde]
version = "1.0.198"
features = ["derive", "std", "alloc", "rc"]
[dependencies.thiserror]
version = "1.0"

View file

@ -2,8 +2,11 @@ use std::{convert::Infallible, sync::Arc};
mod hello; mod hello;
mod permission; mod permission;
mod slot;
use axum::{body::Body, response::Response, routing::get, Router}; use axum::{body::Body, response::Response, routing::get, Router};
use thiserror::Error;
use uuid::Uuid;
pub struct RoString(Arc<str>, bool); pub struct RoString(Arc<str>, bool);
impl http_body::Body for RoString { impl http_body::Body for RoString {
@ -39,31 +42,103 @@ impl From<RoString> for Response {
} }
} }
fn error_handler(result: Result<Response, service::ServiceError>) -> Response { #[derive(Debug, Error)]
pub enum RestError {
#[error("Service error")]
ServiceError(#[from] service::ServiceError),
#[error("Inconsistent id. Got {0} in path but {1} in body")]
InconsistentId(Uuid, Uuid),
}
fn error_handler(result: Result<Response, RestError>) -> Response {
match result { match result {
Ok(response) => response, Ok(response) => response,
Err(service::ServiceError::Forbidden) => { Err(err @ RestError::InconsistentId(_, _)) => Response::builder()
.status(400)
.body(Body::new(err.to_string()))
.unwrap(),
Err(RestError::ServiceError(service::ServiceError::Forbidden)) => {
Response::builder().status(403).body(Body::empty()).unwrap() Response::builder().status(403).body(Body::empty()).unwrap()
} }
Err(service::ServiceError::DatabaseQueryError(e)) => Response::builder() Err(RestError::ServiceError(service::ServiceError::DatabaseQueryError(e))) => {
.status(500) Response::builder()
.body(Body::new(e.to_string())) .status(500)
.unwrap(), .body(Body::new(e.to_string()))
.unwrap()
}
Err(RestError::ServiceError(service::ServiceError::EntityAlreadyExists(id))) => {
Response::builder()
.status(409)
.body(Body::new(id.to_string()))
.unwrap()
}
Err(RestError::ServiceError(service::ServiceError::EntityNotFound(id))) => {
Response::builder()
.status(404)
.body(Body::new(id.to_string()))
.unwrap()
}
Err(RestError::ServiceError(err @ service::ServiceError::EntityConflicts(_, _, _))) => {
Response::builder()
.status(409)
.body(Body::new(err.to_string()))
.unwrap()
}
Err(RestError::ServiceError(err @ service::ServiceError::ValidationError(_))) => {
Response::builder()
.status(422)
.body(Body::new(err.to_string()))
.unwrap()
}
Err(RestError::ServiceError(err @ service::ServiceError::IdSetOnCreate)) => {
Response::builder()
.status(422)
.body(Body::new(err.to_string()))
.unwrap()
}
Err(RestError::ServiceError(err @ service::ServiceError::VersionSetOnCreate)) => {
Response::builder()
.status(422)
.body(Body::new(err.to_string()))
.unwrap()
}
Err(RestError::ServiceError(err @ service::ServiceError::OverlappingTimeRange)) => {
Response::builder()
.status(409)
.body(Body::new(err.to_string()))
.unwrap()
}
Err(RestError::ServiceError(err @ service::ServiceError::TimeOrderWrong(_, _))) => {
Response::builder()
.status(422)
.body(Body::new(err.to_string()))
.unwrap()
}
Err(RestError::ServiceError(err @ service::ServiceError::DateOrderWrong(_, _))) => {
Response::builder()
.status(422)
.body(Body::new(err.to_string()))
.unwrap()
}
} }
} }
pub trait RestStateDef: Clone + Send + Sync + 'static { pub trait RestStateDef: Clone + Send + Sync + 'static {
type HelloService: service::HelloService + Send + Sync + 'static; type HelloService: service::HelloService + Send + Sync + 'static;
type PermissionService: service::PermissionService + Send + Sync + 'static; type PermissionService: service::PermissionService + Send + Sync + 'static;
type SlotService: service::slot::SlotService + Send + Sync + 'static;
fn hello_service(&self) -> Arc<Self::HelloService>; fn hello_service(&self) -> Arc<Self::HelloService>;
fn permission_service(&self) -> Arc<Self::PermissionService>; fn permission_service(&self) -> Arc<Self::PermissionService>;
fn slot_service(&self) -> Arc<Self::SlotService>;
} }
pub async fn start_server<RestState: RestStateDef>(rest_state: RestState) { pub async fn start_server<RestState: RestStateDef>(rest_state: RestState) {
let app = Router::new() let app = Router::new()
.route("/", get(hello::hello::<RestState>)) .route("/", get(hello::hello::<RestState>))
.nest("/permission", permission::generate_route()) .nest("/permission", permission::generate_route())
.nest("/slot", slot::generate_route())
.with_state(rest_state); .with_state(rest_state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await .await

View file

@ -13,10 +13,10 @@ use crate::{error_handler, RestStateDef};
use service::PermissionService; use service::PermissionService;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct User { pub struct UserTO {
pub name: String, pub name: String,
} }
impl From<&service::User> for User { impl From<&service::User> for UserTO {
fn from(user: &service::User) -> Self { fn from(user: &service::User) -> Self {
Self { Self {
name: user.name.to_string(), name: user.name.to_string(),
@ -25,10 +25,10 @@ impl From<&service::User> for User {
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Role { pub struct RoleTO {
pub name: String, pub name: String,
} }
impl From<&service::Role> for Role { impl From<&service::Role> for RoleTO {
fn from(role: &service::Role) -> Self { fn from(role: &service::Role) -> Self {
Self { Self {
name: role.name.to_string(), name: role.name.to_string(),
@ -37,10 +37,10 @@ impl From<&service::Role> for Role {
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Privilege { pub struct PrivilegeTO {
pub name: String, pub name: String,
} }
impl From<&service::Privilege> for Privilege { impl From<&service::Privilege> for PrivilegeTO {
fn from(privilege: &service::Privilege) -> Self { fn from(privilege: &service::Privilege) -> Self {
Self { Self {
name: privilege.name.to_string(), name: privilege.name.to_string(),
@ -80,7 +80,7 @@ pub fn generate_route<RestState: RestStateDef>() -> Router<RestState> {
pub async fn add_user<RestState: RestStateDef>( pub async fn add_user<RestState: RestStateDef>(
rest_state: State<RestState>, rest_state: State<RestState>,
Json(user): Json<User>, Json(user): Json<UserTO>,
) -> Response { ) -> Response {
println!("Adding user: {:?}", user); println!("Adding user: {:?}", user);
error_handler( error_handler(
@ -117,7 +117,7 @@ pub async fn remove_user<RestState: RestStateDef>(
pub async fn add_role<RestState: RestStateDef>( pub async fn add_role<RestState: RestStateDef>(
rest_state: State<RestState>, rest_state: State<RestState>,
Json(role): Json<Role>, Json(role): Json<RoleTO>,
) -> Response { ) -> Response {
error_handler( error_handler(
(async { (async {
@ -238,12 +238,12 @@ pub async fn remove_role_privilege<RestState: RestStateDef>(
pub async fn get_all_users<RestState: RestStateDef>(rest_state: State<RestState>) -> Response { pub async fn get_all_users<RestState: RestStateDef>(rest_state: State<RestState>) -> Response {
error_handler( error_handler(
(async { (async {
let users: Arc<[User]> = rest_state let users: Arc<[UserTO]> = rest_state
.permission_service() .permission_service()
.get_all_users() .get_all_users()
.await? .await?
.iter() .iter()
.map(User::from) .map(UserTO::from)
.collect(); .collect();
Ok(Response::builder() Ok(Response::builder()
.status(200) .status(200)
@ -257,12 +257,12 @@ pub async fn get_all_users<RestState: RestStateDef>(rest_state: State<RestState>
pub async fn get_all_roles<RestState: RestStateDef>(rest_state: State<RestState>) -> Response { pub async fn get_all_roles<RestState: RestStateDef>(rest_state: State<RestState>) -> Response {
error_handler( error_handler(
(async { (async {
let roles: Arc<[Role]> = rest_state let roles: Arc<[RoleTO]> = rest_state
.permission_service() .permission_service()
.get_all_roles() .get_all_roles()
.await? .await?
.iter() .iter()
.map(Role::from) .map(RoleTO::from)
.collect(); .collect();
Ok(Response::builder() Ok(Response::builder()
.status(200) .status(200)
@ -276,12 +276,12 @@ pub async fn get_all_roles<RestState: RestStateDef>(rest_state: State<RestState>
pub async fn get_all_privileges<RestState: RestStateDef>(rest_state: State<RestState>) -> Response { pub async fn get_all_privileges<RestState: RestStateDef>(rest_state: State<RestState>) -> Response {
error_handler( error_handler(
(async { (async {
let privileges: Arc<[Privilege]> = rest_state let privileges: Arc<[PrivilegeTO]> = rest_state
.permission_service() .permission_service()
.get_all_privileges() .get_all_privileges()
.await? .await?
.iter() .iter()
.map(Privilege::from) .map(PrivilegeTO::from)
.collect(); .collect();
Ok(Response::builder() Ok(Response::builder()
.status(200) .status(200)

182
rest/src/slot.rs Normal file
View file

@ -0,0 +1,182 @@
use std::sync::Arc;
use axum::{
body::Body,
extract::{Path, State},
response::Response,
routing::{get, post, put},
Json, Router,
};
use serde::{Deserialize, Serialize};
use service::slot::SlotService;
use uuid::Uuid;
use crate::{error_handler, RestError, RestStateDef};
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub enum DayOfWeek {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
impl From<service::slot::DayOfWeek> for DayOfWeek {
fn from(day_of_week: service::slot::DayOfWeek) -> Self {
match day_of_week {
service::slot::DayOfWeek::Monday => Self::Monday,
service::slot::DayOfWeek::Tuesday => Self::Tuesday,
service::slot::DayOfWeek::Wednesday => Self::Wednesday,
service::slot::DayOfWeek::Thursday => Self::Thursday,
service::slot::DayOfWeek::Friday => Self::Friday,
service::slot::DayOfWeek::Saturday => Self::Saturday,
service::slot::DayOfWeek::Sunday => Self::Sunday,
}
}
}
impl From<DayOfWeek> for service::slot::DayOfWeek {
fn from(day_of_week: DayOfWeek) -> Self {
match day_of_week {
DayOfWeek::Monday => Self::Monday,
DayOfWeek::Tuesday => Self::Tuesday,
DayOfWeek::Wednesday => Self::Wednesday,
DayOfWeek::Thursday => Self::Thursday,
DayOfWeek::Friday => Self::Friday,
DayOfWeek::Saturday => Self::Saturday,
DayOfWeek::Sunday => Self::Sunday,
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SlotTO {
#[serde(default)]
pub id: Uuid,
pub day_of_week: DayOfWeek,
pub from: time::Time,
pub to: time::Time,
pub valid_from: time::Date,
pub valid_to: Option<time::Date>,
#[serde(default)]
pub deleted: Option<time::PrimitiveDateTime>,
#[serde(rename = "$version")]
#[serde(default)]
pub version: Uuid,
}
impl From<&service::slot::Slot> for SlotTO {
fn from(slot: &service::slot::Slot) -> Self {
Self {
id: slot.id,
day_of_week: slot.day_of_week.into(),
from: slot.from,
to: slot.to,
valid_from: slot.valid_from,
valid_to: slot.valid_to,
deleted: slot.deleted,
version: slot.version,
}
}
}
impl From<&SlotTO> for service::slot::Slot {
fn from(slot: &SlotTO) -> Self {
Self {
id: slot.id,
day_of_week: slot.day_of_week.into(),
from: slot.from,
to: slot.to,
valid_from: slot.valid_from,
valid_to: slot.valid_to,
deleted: slot.deleted,
version: slot.version,
}
}
}
pub fn generate_route<RestState: RestStateDef>() -> Router<RestState> {
Router::new()
.route("/", get(get_all_slots::<RestState>))
.route("/:id", get(get_slot::<RestState>))
.route("/", post(create_slot::<RestState>))
.route("/:id", put(update_slot::<RestState>))
}
pub async fn get_all_slots<RestState: RestStateDef>(rest_state: State<RestState>) -> Response {
error_handler(
(async {
let slots: Arc<[SlotTO]> = rest_state
.slot_service()
.get_slots()
.await?
.iter()
.map(SlotTO::from)
.collect();
Ok(Response::builder()
.status(200)
.body(Body::new(serde_json::to_string(&slots).unwrap()))
.unwrap())
})
.await,
)
}
pub async fn get_slot<RestState: RestStateDef>(
rest_state: State<RestState>,
Path(slot_id): Path<Uuid>,
) -> Response {
error_handler(
(async {
let slot = SlotTO::from(&rest_state.slot_service().get_slot(&slot_id).await?.into());
Ok(Response::builder()
.status(200)
.body(Body::new(serde_json::to_string(&slot).unwrap()))
.unwrap())
})
.await,
)
}
pub async fn create_slot<RestState: RestStateDef>(
rest_state: State<RestState>,
Json(slot): Json<SlotTO>,
) -> Response {
error_handler(
(async {
let slot = SlotTO::from(
&rest_state
.slot_service()
.create_slot(&(&slot).into())
.await?,
);
Ok(Response::builder()
.status(200)
.body(Body::new(serde_json::to_string(&slot).unwrap()))
.unwrap())
})
.await,
)
}
pub async fn update_slot<RestState: RestStateDef>(
rest_state: State<RestState>,
Path(slot_id): Path<Uuid>,
Json(slot): Json<SlotTO>,
) -> Response {
error_handler(
(async {
if slot_id != slot.id {
return Err(RestError::InconsistentId(slot_id, slot.id));
}
rest_state
.slot_service()
.update_slot(&(&slot).into())
.await?;
Ok(Response::builder()
.status(200)
.body(Body::new(serde_json::to_string(&slot).unwrap()))
.unwrap())
})
.await,
)
}

View file

@ -6,6 +6,8 @@ edition = "2021"
[dependencies] [dependencies]
async-trait = "0.1.80" async-trait = "0.1.80"
mockall = "0.12.1" mockall = "0.12.1"
time = "0.3.36"
uuid = "1.8.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

8
service/src/clock.rs Normal file
View file

@ -0,0 +1,8 @@
use mockall::automock;
#[automock]
pub trait ClockService {
fn time_now(&self) -> time::Time;
fn date_now(&self) -> time::Date;
fn date_time_now(&self) -> time::PrimitiveDateTime;
}

View file

@ -2,8 +2,14 @@ use async_trait::async_trait;
use mockall::automock; use mockall::automock;
use std::{future::Future, sync::Arc}; use std::{future::Future, sync::Arc};
use thiserror::Error; use thiserror::Error;
use time::Date;
use time::Time;
use uuid::Uuid;
pub mod clock;
pub mod permission; pub mod permission;
pub mod slot;
pub mod uuid_service;
pub use permission::MockPermissionService; pub use permission::MockPermissionService;
pub use permission::PermissionService; pub use permission::PermissionService;
@ -11,6 +17,11 @@ pub use permission::Privilege;
pub use permission::Role; pub use permission::Role;
pub use permission::User; pub use permission::User;
#[derive(Debug, PartialEq, Eq)]
pub enum ValidationFailureItem {
ModificationNotAllowed(Arc<str>),
}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ServiceError { pub enum ServiceError {
#[error("Database query error: {0}")] #[error("Database query error: {0}")]
@ -18,6 +29,33 @@ pub enum ServiceError {
#[error("Forbidden")] #[error("Forbidden")]
Forbidden, Forbidden,
#[error("Entity {0} aready exists")]
EntityAlreadyExists(Uuid),
#[error("Entity {0} not found")]
EntityNotFound(Uuid),
#[error("Entity {0} conflicts, expected version {1} but got {2}")]
EntityConflicts(Uuid, Uuid, Uuid),
#[error("Validation error: {0:?}")]
ValidationError(Arc<[ValidationFailureItem]>),
#[error("ID cannot be set on create")]
IdSetOnCreate,
#[error("Version cannot be set on create")]
VersionSetOnCreate,
#[error("Overlapping time range")]
OverlappingTimeRange,
#[error("Time order wrong. {0} must is not smaller or equal to {1}")]
TimeOrderWrong(Time, Time),
#[error("Date order wrong. {0} must is not smaller or equal to {1}")]
DateOrderWrong(Date, Date),
} }
#[automock] #[automock]

93
service/src/slot.rs Normal file
View file

@ -0,0 +1,93 @@
use async_trait::async_trait;
use mockall::automock;
use std::sync::Arc;
use uuid::Uuid;
use crate::ServiceError;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum DayOfWeek {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
impl From<dao::slot::DayOfWeek> for DayOfWeek {
fn from(day_of_week: dao::slot::DayOfWeek) -> Self {
match day_of_week {
dao::slot::DayOfWeek::Monday => Self::Monday,
dao::slot::DayOfWeek::Tuesday => Self::Tuesday,
dao::slot::DayOfWeek::Wednesday => Self::Wednesday,
dao::slot::DayOfWeek::Thursday => Self::Thursday,
dao::slot::DayOfWeek::Friday => Self::Friday,
dao::slot::DayOfWeek::Saturday => Self::Saturday,
dao::slot::DayOfWeek::Sunday => Self::Sunday,
}
}
}
impl From<DayOfWeek> for dao::slot::DayOfWeek {
fn from(day_of_week: DayOfWeek) -> Self {
match day_of_week {
DayOfWeek::Monday => Self::Monday,
DayOfWeek::Tuesday => Self::Tuesday,
DayOfWeek::Wednesday => Self::Wednesday,
DayOfWeek::Thursday => Self::Thursday,
DayOfWeek::Friday => Self::Friday,
DayOfWeek::Saturday => Self::Saturday,
DayOfWeek::Sunday => Self::Sunday,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Slot {
pub id: Uuid,
pub day_of_week: DayOfWeek,
pub from: time::Time,
pub to: time::Time,
pub valid_from: time::Date,
pub valid_to: Option<time::Date>,
pub deleted: Option<time::PrimitiveDateTime>,
pub version: Uuid,
}
impl From<&dao::slot::SlotEntity> for Slot {
fn from(slot: &dao::slot::SlotEntity) -> Self {
Self {
id: slot.id,
day_of_week: slot.day_of_week.into(),
from: slot.from,
to: slot.to,
valid_from: slot.valid_from,
valid_to: slot.valid_to,
deleted: slot.deleted,
version: slot.version,
}
}
}
impl From<&Slot> for dao::slot::SlotEntity {
fn from(slot: &Slot) -> Self {
Self {
id: slot.id,
day_of_week: slot.day_of_week.into(),
from: slot.from,
to: slot.to,
valid_from: slot.valid_from,
valid_to: slot.valid_to,
deleted: slot.deleted,
version: slot.version,
}
}
}
#[automock]
#[async_trait]
pub trait SlotService {
async fn get_slots(&self) -> Result<Arc<[Slot]>, ServiceError>;
async fn get_slot(&self, id: &Uuid) -> Result<Slot, ServiceError>;
async fn create_slot(&self, slot: &Slot) -> Result<Slot, ServiceError>;
async fn delete_slot(&self, id: &Uuid) -> Result<(), ServiceError>;
async fn update_slot(&self, slot: &Slot) -> Result<(), ServiceError>;
}

View file

@ -0,0 +1,7 @@
use mockall::automock;
use uuid::Uuid;
#[automock]
pub trait UuidService {
fn new_uuid(&self, usage: &str) -> Uuid;
}

View file

@ -6,6 +6,7 @@ edition = "2021"
[dependencies] [dependencies]
async-trait = "0.1.80" async-trait = "0.1.80"
mockall = "0.12.1" mockall = "0.12.1"
tokio = "1.37.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -15,6 +16,14 @@ path = "../service"
[dependencies.dao] [dependencies.dao]
path = "../dao" path = "../dao"
[dependencies.time]
version = "0.3.36"
features = ["std"]
[dependencies.uuid]
version = "1.8.0"
features = ["v4"]
[dev-dependencies.tokio] [dev-dependencies.tokio]
version = "1.37.0" version = "1.37.0"
features = ["full"] 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; use async_trait::async_trait;
mod permission; pub mod clock;
#[cfg(test)] pub mod permission;
pub mod slot;
mod test; mod test;
pub mod uuid_service;
pub use permission::PermissionServiceImpl; 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; mod permission_test;
#[cfg(test)]
pub mod slot;

View file

@ -20,7 +20,7 @@ fn generate_dependencies_mocks_permission(
(permission_dao, user_service) (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 { if let Err(service::ServiceError::Forbidden) = result {
// All good // All good
} else { } 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()
}
}