diff --git a/Cargo.lock b/Cargo.lock index 5a1d5994..fc1b581a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -691,6 +691,7 @@ dependencies = [ "change-detection", "chrono", "dotenv", + "fake", "futures-util", "idna 1.0.2", "insta", @@ -1079,6 +1080,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "deunicode" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" + [[package]] name = "digest" version = "0.10.7" @@ -1208,6 +1215,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fake" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "661cb0601b5f4050d1e65452c5b0ea555c0b3e88fb5ed7855906adc6c42523ef" +dependencies = [ + "deunicode", + "rand", +] + [[package]] name = "fancy-regex" version = "0.13.0" diff --git a/web/Cargo.toml b/web/Cargo.toml index f3812fc3..1d5ae0e4 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -43,3 +43,4 @@ change-detection = "1.2.0" [dev-dependencies] insta = "1.41.1" +fake = "3.0.1" diff --git a/web/src/endpoints/location/mod.rs b/web/src/endpoints/location/mod.rs index 2c831ffd..9d88506e 100644 --- a/web/src/endpoints/location/mod.rs +++ b/web/src/endpoints/location/mod.rs @@ -1,5 +1,5 @@ use rinja::Template; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::models::{Area, Location, Role, User}; use crate::filters; @@ -19,7 +19,7 @@ pub struct LocationTemplate { location: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, Serialize)] pub struct LocationForm { name: String, area: Option, diff --git a/web/src/endpoints/location/post_new.rs b/web/src/endpoints/location/post_new.rs index 37e70b6e..65b035bc 100644 --- a/web/src/endpoints/location/post_new.rs +++ b/web/src/endpoints/location/post_new.rs @@ -1,11 +1,16 @@ use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; +use brass_macros::db_test; use sqlx::PgPool; use crate::{ endpoints::location::LocationForm, - models::{Location, Role, User}, utils::ApplicationError, + models::{Location, Role, User}, + utils::ApplicationError, }; +#[cfg(test)] +use crate::utils::test_helper::{test_post, DbTestContext, RequestConfig, StatusCode}; + #[actix_web::post("/locations/new")] pub async fn post( user: web::ReqData, @@ -22,17 +27,66 @@ pub async fn post( area_id = form.area.unwrap(); } - // TODO: rework - match Location::create(pool.get_ref(), &form.name, area_id).await { - Ok(_) => { - return Ok(HttpResponse::Found() - .insert_header((LOCATION, "/locations")) - .insert_header(("HX-LOCATION", "/locations")) - .finish()) - } - Err(err) => { - println!("{}", err); - return Ok(HttpResponse::InternalServerError().finish()); - } - } + Location::create(pool.get_ref(), &form.name, area_id).await?; + + return Ok(HttpResponse::Found() + .insert_header((LOCATION, "/locations")) + .insert_header(("HX-LOCATION", "/locations")) + .finish()); +} + +#[db_test] +async fn works_when_user_is_admin(context: &DbTestContext) { + let app = context.app().await; + let config = RequestConfig { + uri: "/locations/new".to_string(), + role: Role::Admin, + function: crate::models::Function::Posten, + user_area: 1, + }; + + let form = LocationForm { + name: "Hauptbahnhof".to_string(), + area: Some(1), + }; + + let response = test_post(&context.db_pool, app, &config, form).await; + assert_eq!(StatusCode::FOUND, response.status()); + + assert_eq!( + "Hauptbahnhof".to_string(), + Location::read_by_id(&context.db_pool, 1) + .await + .unwrap() + .unwrap() + .name + ); +} + +#[db_test] +async fn uses_area_id_of_area_manager(context: &DbTestContext) { + let app = context.app().await; + let config = RequestConfig { + uri: "/locations/new".to_string(), + role: Role::AreaManager, + function: crate::models::Function::Posten, + user_area: 1, + }; + + let form = LocationForm { + name: "Hauptbahnhof".to_string(), + area: None, + }; + + let response = test_post(&context.db_pool, app, &config, form).await; + assert_eq!(StatusCode::FOUND, response.status()); + + assert_eq!( + "Hauptbahnhof".to_string(), + Location::read_by_id(&context.db_pool, 1) + .await + .unwrap() + .unwrap() + .name + ); } diff --git a/web/src/endpoints/user/post_edit.rs b/web/src/endpoints/user/post_edit.rs index 617c90e0..532ea344 100644 --- a/web/src/endpoints/user/post_edit.rs +++ b/web/src/endpoints/user/post_edit.rs @@ -1,14 +1,24 @@ -use actix_identity::Identity; use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; -use serde::Deserialize; +use brass_macros::db_test; +use serde::{Deserialize, Serialize}; use sqlx::PgPool; use crate::{ endpoints::IdPath, models::{Function, Role, User}, + utils::ApplicationError, }; -#[derive(Deserialize)] +#[cfg(test)] +use crate::utils::test_helper::{test_post, DbTestContext, RequestConfig, StatusCode}; +#[cfg(test)] +use fake::{ + faker::{internet::raw::SafeEmail, name::raw::Name}, + locales::EN, + Fake, +}; + +#[derive(Deserialize, Serialize)] pub struct EditUserForm { email: String, name: String, @@ -19,77 +29,167 @@ pub struct EditUserForm { #[actix_web::post("/users/edit/{id}")] pub async fn post_edit( - user: Identity, + user: web::ReqData, pool: web::Data, path: web::Path, form: web::Form, -) -> impl Responder { - // TODO: rewrite - let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap()) - .await - .unwrap().unwrap(); - - if current_user.role != Role::AreaManager && current_user.role != Role::Admin { - return HttpResponse::Unauthorized().finish(); +) -> Result { + if user.role != Role::AreaManager && user.role != Role::Admin { + return Err(ApplicationError::Unauthorized); } - if let Some(user_in_db) = User::read_by_id(pool.get_ref(), path.id).await.unwrap() { - if current_user.role == Role::AreaManager && current_user.area_id != user_in_db.area_id { - return HttpResponse::Unauthorized().finish(); - } - - let mut changed = false; - - let email = if user_in_db.email != form.email { - changed = true; - Some(form.email.as_str()) - } else { - None - }; - - let name = if user_in_db.name != form.name { - changed = true; - Some(form.name.as_str()) - } else { - None - }; - - let role = if user_in_db.role as u8 != form.role { - if let Ok(r) = Role::try_from(form.role) { - changed = true; - Some(r) - } else { - None - } - } else { - None - }; - - let function = if user_in_db.function as u8 != form.function { - if let Ok(f) = Function::try_from(form.function) { - changed = true; - Some(f) - } else { - None - } - } else { - None - }; - - let area = if current_user.role == Role::Admin && form.area.is_some() && user_in_db.area_id != form.area.unwrap() { - changed = true; - Some(form.area.unwrap()) - } else { - None - }; - - if changed { - match User::update(pool.get_ref(), path.id, email, name, None, None, role, function, area, None, None).await { - Ok(_) => return HttpResponse::Found().insert_header((LOCATION, "/users")).finish(), - Err(err) => println!("{}", err) - } - } + if user.id == path.id { + return Ok(HttpResponse::BadRequest().finish()); } - return HttpResponse::BadRequest().body("Fehler beim Bearbeiten des Nutzers"); + let Some(user_in_db) = User::read_by_id(pool.get_ref(), path.id).await? else { + return Ok(HttpResponse::NotFound().finish()); + }; + + if user.role == Role::AreaManager && user.area_id != user_in_db.area_id { + return Err(ApplicationError::Unauthorized); + } + + let mut changed = false; + + let email = if user_in_db.email != form.email { + changed = true; + Some(form.email.as_str()) + } else { + None + }; + + let name = if user_in_db.name != form.name { + changed = true; + Some(form.name.as_str()) + } else { + None + }; + + let role = if user_in_db.role as u8 != form.role { + if let Ok(r) = Role::try_from(form.role) { + changed = true; + Some(r) + } else { + None + } + } else { + None + }; + + let function = if user_in_db.function as u8 != form.function { + if let Ok(f) = Function::try_from(form.function) { + changed = true; + Some(f) + } else { + None + } + } else { + None + }; + + let area = if user.role == Role::Admin + && form.area.is_some() + && user_in_db.area_id != form.area.unwrap() + { + changed = true; + Some(form.area.unwrap()) + } else { + None + }; + + if changed { + User::update( + pool.get_ref(), + path.id, + email, + name, + None, + None, + role, + function, + area, + None, + None, + ) + .await?; + } + + Ok(HttpResponse::Found() + .insert_header((LOCATION, "/users")) + .insert_header(("HX-LOCATION", "/users")) + .finish()) +} + +#[db_test] +async fn works_when_user_is_admin(context: &DbTestContext) { + User::create( + &context.db_pool, + &Name(EN).fake::(), + &SafeEmail(EN).fake::(), + Role::Staff, + Function::Posten, + 1, + ) + .await + .unwrap(); + + crate::models::Area::create(&context.db_pool, "Süd") + .await + .unwrap(); + + let app = context.app().await; + let config = RequestConfig { + uri: "/users/edit/1".to_string(), + role: Role::Admin, + function: crate::models::Function::Posten, + user_area: 1, + }; + + let new_name: String = Name(EN).fake(); + let new_mail: String = SafeEmail(EN).fake(); + + let form = EditUserForm { + name: new_name.clone(), + email: new_mail.clone(), + role: Role::AreaManager as u8, + function: Function::Fuehrungsassistent as u8, + area: Some(2), + }; + + let response = test_post(&context.db_pool, app, &config, form).await; + assert_eq!(StatusCode::FOUND, response.status()); + + let updated_user = User::read_by_id(&context.db_pool, 1) + .await + .unwrap() + .unwrap(); + + assert_eq!(new_name, updated_user.name); + assert_eq!(new_mail, updated_user.email); + assert_eq!(Role::AreaManager, updated_user.role); + assert_eq!(Function::Fuehrungsassistent, updated_user.function); + assert_eq!(2, updated_user.area_id); +} + +#[db_test] +async fn cant_edit_oneself(context: &DbTestContext) { + let app = context.app().await; + let config = RequestConfig { + uri: "/users/edit/1".to_string(), + role: Role::Admin, + function: crate::models::Function::Posten, + user_area: 1, + }; + + let form = EditUserForm { + name: "".to_string(), + email: "".to_string(), + role: Role::AreaManager as u8, + function: Function::Fuehrungsassistent as u8, + area: Some(1), + }; + + let response = test_post(&context.db_pool, app, &config, form).await; + assert_eq!(StatusCode::BAD_REQUEST, response.status()); } diff --git a/web/src/models/function.rs b/web/src/models/function.rs index 322b9f6e..8d3c9e7d 100644 --- a/web/src/models/function.rs +++ b/web/src/models/function.rs @@ -28,6 +28,7 @@ impl TryFrom for Function { fn try_from(value: u8) -> Result { match value { 1 => Ok(Function::Posten), + 5 => Ok(Function::Fuehrungsassistent), 10 => Ok(Function::Wachhabender), _ => Err(ApplicationError::UnsupportedEnumValue { value: value.to_string(),