test: new assignment

This commit is contained in:
Max Hohlfeld 2025-06-30 13:17:27 +02:00
parent 10e6ba80a2
commit 2abeeb20df
11 changed files with 310 additions and 21 deletions

View File

@ -166,8 +166,8 @@ fn user_is_admin_or_area_manager_of_event_area(
let user_is_area_manager_event_area =
user.role == Role::AreaManager && user.area_id == event.location.as_ref().unwrap().area_id;
if !user_is_admin || !user_is_area_manager_event_area {
return Err(AsyncValidateError::new(""));
if !user_is_admin && !user_is_area_manager_event_area {
return Err(AsyncValidateError::new("TODO: admin or areamanager"));
}
Ok(())

View File

@ -0,0 +1,67 @@
---
source: web/src/endpoints/assignment/post_new.rs
expression: body
snapshot_kind: text
---
<table class="table is-fullwidth">
<thead>
<tr>
<th>Name</th>
<th>Funktion</th>
<th>Zeitraum</th>
<th>Kommentar</th>
<th>Planung</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>Max Mustermann</td>
<td>
<div class="tags"><span class="tag is-primary is-light">Posten</span></div>
</td>
<td>
10:00 bis 10.01.2025 20:00
</td>
<td>
</td>
<td>
<div class="dropdown">
<div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span>als Posten geplant</span>
<svg class="icon">
<use href="/static/feather-sprite.svg#edit-2" />
</svg>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content" hx-target="closest table" hx-swap="outerHTML">
<a class="dropdown-item"
hx-post="/assignments/new?event=1&availability=1&function=1" disabled>
als Posten planen</a>
<hr class="dropdown-divider" />
<a class="dropdown-item"
hx-delete="/assignments/delete?event=1&availability=1"
class="button is-small">entplanen</a>
</div>
</div>
</div>
</td>
<td>
</td>
</tr>
</tbody>
</table>

View File

@ -55,7 +55,7 @@ mod tests {
name: "Neuer Name".to_string(),
};
let response = test_post(&context.db_pool, app, &config, request).await;
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
assert_eq!(StatusCode::FOUND, response.status());
@ -82,7 +82,7 @@ mod tests {
name: "Neuer Name".to_string(),
};
let response = test_post(&context.db_pool, app, &config, request).await;
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
}
@ -102,7 +102,7 @@ mod tests {
name: "Neuer Name".to_string(),
};
let response = test_post(&context.db_pool, app, &config, request).await;
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
assert_eq!(StatusCode::NOT_FOUND, response.status());
}

View File

@ -48,7 +48,7 @@ mod tests {
name: "Neuer Name".to_string(),
};
let response = test_post(&context.db_pool, app, &config, request).await;
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
assert_eq!(StatusCode::FOUND, response.status());
@ -75,7 +75,7 @@ mod tests {
name: "Neuer Name".to_string(),
};
let response = test_post(&context.db_pool, app, &config, request).await;
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
}

View File

@ -50,7 +50,7 @@ pub async fn post(
};
if let Err(e) = changeset.validate_with_context(&context).await {
return Ok(HttpResponse::BadRequest().body(e.to_string()));
return Ok(HttpResponse::UnprocessableEntity().body(e.to_string()));
};
Assignment::create(pool.get_ref(), event.id, query.availability, changeset).await?;
@ -73,3 +73,181 @@ pub async fn post(
Ok(template.to_response()?)
}
#[cfg(test)]
mod tests {
use actix_http::StatusCode;
use brass_db::models::{
Area, Availability, AvailabilityChangeset, Event, EventChangeset, Location, Role, User,
UserChangeset,
};
use brass_macros::db_test;
use chrono::NaiveDateTime;
use fake::{Fake, Faker};
use sqlx::PgPool;
use crate::utils::test_helper::{
assert_snapshot, test_post, DbTestContext, NaiveDateTimeExt, RequestConfig,
ServiceResponseExt,
};
async fn arrange(pool: &PgPool) {
Location::create(pool, &Faker.fake::<String>(), 1)
.await
.unwrap();
let mut user_changeset: UserChangeset = Faker.fake();
user_changeset.name = String::from("Max Mustermann");
User::create(pool, user_changeset).await.unwrap();
}
async fn arrange_event(pool: &PgPool, start: NaiveDateTime, end: NaiveDateTime, location: i32) {
let mut changeset: EventChangeset = EventChangeset::create_for_test(start, end);
changeset.location_id = location;
Event::create(pool, changeset).await.unwrap();
}
async fn arrange_availability(pool: &PgPool, start: NaiveDateTime, end: NaiveDateTime) {
Availability::create(
pool,
1,
AvailabilityChangeset {
time: (start, end),
comment: None,
},
)
.await
.unwrap();
}
#[db_test]
fn response_produces_updated_template(context: &DbTestContext) {
let app = context.app().await;
let start = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 10, 0, 0).unwrap();
let end = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 20, 0, 0).unwrap();
arrange(&context.db_pool).await;
arrange_event(&context.db_pool, start, end, 1).await;
arrange_availability(&context.db_pool, start, end).await;
let config = RequestConfig::new("/assignments/new?availability=1&function=1&event=1")
.with_role(Role::Admin);
let response = test_post::<_, _, String>(&context.db_pool, app, &config, None).await;
let (status, body) = response.into_status_and_body().await;
assert_eq!(StatusCode::OK, status, "{body}");
assert_snapshot!(body);
}
#[db_test]
fn fails_when_availability_does_not_exist(context: &DbTestContext) {
let app = context.app().await;
let start = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 10, 0, 0).unwrap();
let end = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 20, 0, 0).unwrap();
arrange(&context.db_pool).await;
arrange_event(&context.db_pool, start, end, 1).await;
let config = RequestConfig::new("/assignments/new?availability=1&function=1&event=1")
.with_role(Role::Admin);
let response = test_post::<_, _, String>(&context.db_pool, app, &config, None).await;
assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status());
}
#[db_test]
fn fails_when_event_does_not_exist(context: &DbTestContext) {
let app = context.app().await;
let start = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 10, 0, 0).unwrap();
let end = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 20, 0, 0).unwrap();
arrange(&context.db_pool).await;
arrange_availability(&context.db_pool, start, end).await;
let config = RequestConfig::new("/assignments/new?availability=1&function=1&event=1")
.with_role(Role::Admin);
let response = test_post::<_, _, String>(&context.db_pool, app, &config, None).await;
assert_eq!(StatusCode::NOT_FOUND, response.status());
}
#[db_test]
fn fails_when_area_manager_is_different_area_from_event(context: &DbTestContext) {
let app = context.app().await;
let start = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 10, 0, 0).unwrap();
let end = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 20, 0, 0).unwrap();
arrange(&context.db_pool).await;
arrange_event(&context.db_pool, start, end, 1).await;
arrange_availability(&context.db_pool, start, end).await;
Area::create(&context.db_pool, &Faker.fake::<String>())
.await
.unwrap();
let config = RequestConfig::new("/assignments/new?availability=1&function=1&event=1")
.with_role(Role::AreaManager)
.with_user_area(2);
let response = test_post::<_, _, String>(&context.db_pool, app, &config, None).await;
assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status());
}
#[db_test]
fn fails_when_availability_user_not_in_event_area(context: &DbTestContext) {
let app = context.app().await;
let start = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 10, 0, 0).unwrap();
let end = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 20, 0, 0).unwrap();
arrange(&context.db_pool).await;
Area::create(&context.db_pool, &Faker.fake::<String>())
.await
.unwrap();
Location::create(&context.db_pool, &Faker.fake::<String>(), 2)
.await
.unwrap();
arrange_event(&context.db_pool, start, end, 2).await;
arrange_availability(&context.db_pool, start, end).await;
let config = RequestConfig::new("/assignments/new?availability=1&function=1&event=1")
.with_role(Role::Admin);
let response = test_post::<_, _, String>(&context.db_pool, app, &config, None).await;
assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status());
}
#[db_test]
fn fails_assignment_time_doesnt_fit_into_availability_time(context: &DbTestContext) {
assert!(false)
}
#[db_test]
fn fails_when_end_time_lies_before_start_time(context: &DbTestContext) {
assert!(false)
}
#[db_test]
fn fails_when_availability_time_already_assigned(context: &DbTestContext) {
assert!(false)
}
#[db_test]
fn fails_when_availability_user_does_not_have_function(context: &DbTestContext) {
assert!(false)
}
#[db_test]
fn fails_when_event_already_has_enough_assignments_for_function(context: &DbTestContext) {
assert!(false)
}
}

View File

@ -52,7 +52,7 @@ mod tests {
area: Some(1),
};
let response = test_post(&context.db_pool, app, &config, form).await;
let response = test_post(&context.db_pool, app, &config, Some(form)).await;
assert_eq!(StatusCode::FOUND, response.status());
assert_eq!(
@ -80,7 +80,7 @@ mod tests {
area: None,
};
let response = test_post(&context.db_pool, app, &config, form).await;
let response = test_post(&context.db_pool, app, &config, Some(form)).await;
assert_eq!(StatusCode::FOUND, response.status());
assert_eq!(

View File

@ -1,5 +1,8 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use brass_db::{models::{Function, Role, User, UserChangeset}, validation::{AsyncValidate, DbContext}};
use brass_db::{
models::{Function, Role, User, UserChangeset},
validation::{AsyncValidate, DbContext},
};
use sqlx::PgPool;
use crate::{
@ -113,7 +116,7 @@ mod tests {
area: Some(2),
};
let response = test_post(&context.db_pool, app, &config, form).await;
let response = test_post(&context.db_pool, app, &config, Some(form)).await;
assert_eq!(StatusCode::FOUND, response.status());
let updated_user = User::read_by_id(&context.db_pool, 1)
@ -148,7 +151,7 @@ mod tests {
area: Some(1),
};
let response = test_post(&context.db_pool, app, &config, form).await;
let response = test_post(&context.db_pool, app, &config, Some(form)).await;
assert_eq!(StatusCode::BAD_REQUEST, response.status());
}
@ -173,7 +176,7 @@ mod tests {
area: Some(2),
};
let response = test_post(&context.db_pool, app, &config, form).await;
let response = test_post(&context.db_pool, app, &config, Some(form)).await;
assert_eq!(StatusCode::FOUND, response.status());
let updated_user = User::read_by_id(&context.db_pool, 1)
@ -211,7 +214,7 @@ mod tests {
area: Some(2),
};
let response = test_post(&context.db_pool, app, &config, form).await;
let response = test_post(&context.db_pool, app, &config, Some(form)).await;
assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status());
}
}

View File

@ -73,7 +73,7 @@ mod tests {
radio_call_name: "11.49.2".to_string(),
};
let response = test_post(&context.db_pool, app, &config, request).await;
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
assert_eq!(StatusCode::FOUND, response.status());
@ -93,7 +93,7 @@ mod tests {
radio_call_name: "11.49.2".to_string(),
};
let response = test_post(&context.db_pool, app, &config, request).await;
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
}
@ -109,7 +109,7 @@ mod tests {
radio_call_name: "11.49.2".to_string(),
};
let response = test_post(&context.db_pool, app, &config, request).await;
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
assert_eq!(StatusCode::NOT_FOUND, response.status());
}

View File

@ -53,7 +53,7 @@ mod tests {
radio_call_name: "11.49.1".to_string(),
};
let response = test_post(&context.db_pool, app, &config, request).await;
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
assert_eq!(StatusCode::FOUND, response.status());
@ -73,7 +73,7 @@ mod tests {
radio_call_name: "11.49.2".to_string(),
};
let response = test_post(&context.db_pool, app, &config, request).await;
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
}

View File

@ -1,5 +1,9 @@
mod test_context;
mod test_requests;
use actix_web::body::MessageBody;
use actix_web::dev::ServiceResponse;
use actix_web::test;
use chrono::{NaiveDate, NaiveDateTime};
pub use test_context::{setup, teardown, DbTestContext};
pub use test_requests::RequestConfig;
pub use test_requests::{read_body, test_delete, test_get, test_post, test_put};
@ -27,3 +31,40 @@ macro_rules! assert_mail_snapshot {
pub(crate) use assert_mail_snapshot;
pub(crate) use assert_snapshot;
pub trait NaiveDateTimeExt {
fn from_ymd_and_hms(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
) -> Option<NaiveDateTime>;
}
impl NaiveDateTimeExt for NaiveDateTime {
fn from_ymd_and_hms(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
) -> Option<NaiveDateTime> {
NaiveDate::from_ymd_opt(year, month, day)?.and_hms_opt(hour, minute, second)
}
}
pub trait ServiceResponseExt {
async fn into_status_and_body(self) -> (StatusCode, String);
}
impl<B> ServiceResponseExt for ServiceResponse<B> where B: MessageBody {
async fn into_status_and_body(self) -> (StatusCode, String) {
let status = self.status();
let response = String::from_utf8(test::read_body(self).await.to_vec()).unwrap();
(status, response)
}
}

View File

@ -118,7 +118,7 @@ pub async fn test_post<T, R, F>(
pool: &Pool<Postgres>,
app: T,
config: &RequestConfig,
form: F,
form: Option<F>,
) -> ServiceResponse<R>
where
T: Service<Request, Response = ServiceResponse<R>, Error = Error>,