diff --git a/db/src/models/availability_changeset.rs b/db/src/models/availability_changeset.rs index 90d63aca..0e8b89ac 100644 --- a/db/src/models/availability_changeset.rs +++ b/db/src/models/availability_changeset.rs @@ -1,4 +1,4 @@ -use chrono::{Days, NaiveDateTime}; +use chrono::{Days, NaiveDate, NaiveDateTime}; use sqlx::PgPool; use super::Availability; @@ -150,3 +150,20 @@ pub fn find_free_date_time_slots( available_slots } + +#[cfg(feature = "test-helpers")] +impl AvailabilityChangeset { + pub fn create_for_test( + date: &NaiveDate, + start_hour: u32, + end_hour: u32, + ) -> AvailabilityChangeset { + AvailabilityChangeset { + time: ( + date.and_hms_opt(start_hour, 0, 0).unwrap(), + date.and_hms_opt(end_hour, 0, 0).unwrap(), + ), + comment: None, + } + } +} diff --git a/web/src/endpoints/availability/mod.rs b/web/src/endpoints/availability/mod.rs index b5dec38b..b75ea133 100644 --- a/web/src/endpoints/availability/mod.rs +++ b/web/src/endpoints/availability/mod.rs @@ -35,6 +35,7 @@ struct NewOrEditAvailabilityTemplate<'a> { } #[derive(Deserialize)] +#[cfg_attr(test, derive(serde::Serialize))] pub struct AvailabilityForm { pub startdate: NaiveDate, pub enddate: NaiveDate, diff --git a/web/src/endpoints/availability/post_new.rs b/web/src/endpoints/availability/post_new.rs index 03d5c2aa..86577460 100644 --- a/web/src/endpoints/availability/post_new.rs +++ b/web/src/endpoints/availability/post_new.rs @@ -7,7 +7,7 @@ use crate::{ utils::{self, ApplicationError}, }; use brass_db::{ - models::{Availability, AvailabilityChangeset, AvailabilityContext, User}, + models::{Availability, AvailabilityChangeset, AvailabilityContext, Role, User}, validation::AsyncValidate, }; @@ -20,6 +20,13 @@ pub async fn post( let start = form.startdate.and_time(form.starttime); let end = form.enddate.and_time(form.endtime); let user_for_availability = form.user.unwrap_or(user.id); + let Some(other_user) = User::read_by_id(pool.get_ref(), user_for_availability).await? else { + return Ok(HttpResponse::UnprocessableEntity().body("Nutzer existiert nicht.")); + }; + + if other_user.area_id != user.area_id && user.role != Role::Admin { + return Err(ApplicationError::Unauthorized); + } let context = AvailabilityContext { pool: pool.get_ref(), @@ -75,3 +82,192 @@ pub async fn post( .insert_header(("HX-LOCATION", url)) .finish()) } + +#[cfg(test)] +mod tests { + use actix_http::StatusCode; + use brass_db::models::{Area, Availability, AvailabilityChangeset, Role, User, UserChangeset}; + use brass_macros::db_test; + use chrono::{NaiveDate, NaiveTime}; + use fake::{Fake, Faker}; + + use crate::{ + endpoints::availability::AvailabilityForm, + utils::test_helper::{create_test_login_user, test_post, DbTestContext, RequestConfig}, + }; + + fn create_form( + date: &NaiveDate, + start_hour: u32, + end_hour: u32, + user: Option, + ) -> AvailabilityForm { + AvailabilityForm { + startdate: *date, + enddate: *date, + starttime: NaiveTime::from_hms_opt(start_hour, 0, 0).unwrap(), + endtime: NaiveTime::from_hms_opt(end_hour, 0, 0).unwrap(), + comment: None, + user, + } + } + + #[db_test] + async fn can_create_availability_for_myself_fine(context: &DbTestContext) { + let app = context.app().await; + + let config = RequestConfig::new("/availability/new"); + create_test_login_user(&context.db_pool, &config).await; + + let date = Faker.fake(); + let form = create_form(&date, 10, 20, None); + let response = test_post(app, &config, Some(form)).await; + + assert_eq!(StatusCode::FOUND, response.status()); + assert!(Availability::read(&context.db_pool, 1) + .await + .unwrap() + .is_some()) + } + + #[db_test] + async fn cant_create_availability_for_myself_when_time_slot_already_present( + context: &DbTestContext, + ) { + let app = context.app().await; + + let config = RequestConfig::new("/availability/new"); + create_test_login_user(&context.db_pool, &config).await; + + let date = Faker.fake(); + let changeset = AvailabilityChangeset::create_for_test(&date, 10, 20); + Availability::create(&context.db_pool, 1, &changeset) + .await + .unwrap(); + + let form = create_form(&date, 10, 20, None); + let response = test_post(app, &config, Some(form)).await; + + assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status()); + } + + #[db_test] + async fn create_new_updates_existing_when_time_slots_connect(context: &DbTestContext) { + let app = context.app().await; + + let config = RequestConfig::new("/availability/new"); + create_test_login_user(&context.db_pool, &config).await; + + let date = Faker.fake(); + let changeset = AvailabilityChangeset::create_for_test(&date, 4, 10); + Availability::create(&context.db_pool, 1, &changeset) + .await + .unwrap(); + + let form = create_form(&date, 10, 20, None); + let response = test_post(app, &config, Some(form)).await; + + assert_eq!(StatusCode::FOUND, response.status()); + assert_eq!( + 1, + Availability::read_all_by_user_and_date(&context.db_pool, 1, &date) + .await + .unwrap() + .len() + ) + } + + #[db_test] + async fn cant_create_availability_for_myself_when_start_time_before_end_time( + context: &DbTestContext, + ) { + let app = context.app().await; + + let config = RequestConfig::new("/availability/new"); + create_test_login_user(&context.db_pool, &config).await; + + let date = Faker.fake(); + let form = create_form(&date, 20, 10, None); + let response = test_post(app, &config, Some(form)).await; + + assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status()); + } + + #[db_test] + async fn can_create_availability_for_other_user_in_same_area_as_area_manager( + context: &DbTestContext, + ) { + let app = context.app().await; + + let config = RequestConfig::new("/availability/new").with_role(Role::AreaManager); + create_test_login_user(&context.db_pool, &config).await; + + User::create(&context.db_pool, &Faker.fake()).await.unwrap(); + + let date = Faker.fake(); + let form = create_form(&date, 10, 20, Some(2)); + let response = test_post(app, &config, Some(form)).await; + + assert_eq!(StatusCode::FOUND, response.status()); + assert_eq!( + 1, + Availability::read_all_by_user_and_date(&context.db_pool, 2, &date) + .await + .unwrap() + .len() + ) + } + + #[db_test] + async fn cant_create_availability_for_other_user_in_other_area_as_area_manager( + context: &DbTestContext, + ) { + let app = context.app().await; + + let config = RequestConfig::new("/availability/new").with_role(Role::AreaManager); + create_test_login_user(&context.db_pool, &config).await; + + Area::create(&context.db_pool, "Süd").await.unwrap(); + let mut user_changeset: UserChangeset = Faker.fake(); + user_changeset.area_id = 2; + User::create(&context.db_pool, &user_changeset) + .await + .unwrap(); + + let date = Faker.fake(); + let form = create_form(&date, 10, 20, Some(2)); + let response = test_post(app, &config, Some(form)).await; + + assert_eq!(StatusCode::UNAUTHORIZED, response.status()); + } + + #[db_test] + async fn can_create_availability_for_other_user_in_other_area_as_admin( + context: &DbTestContext, + ) { + let app = context.app().await; + + let config = RequestConfig::new("/availability/new").with_role(Role::Admin); + create_test_login_user(&context.db_pool, &config).await; + + Area::create(&context.db_pool, "Süd").await.unwrap(); + let mut user_changeset: UserChangeset = Faker.fake(); + user_changeset.area_id = 2; + User::create(&context.db_pool, &user_changeset) + .await + .unwrap(); + + let date = Faker.fake(); + let form = create_form(&date, 10, 20, Some(2)); + let response = test_post(app, &config, Some(form)).await; + + assert_eq!(StatusCode::FOUND, response.status()); + assert_eq!( + 1, + Availability::read_all_by_user_and_date(&context.db_pool, 2, &date) + .await + .unwrap() + .len() + ) + } +}