feat: area manager can edit vehicle

refs #21
This commit is contained in:
Max Hohlfeld 2025-04-28 22:22:27 +02:00
parent b2fccdfa29
commit e1b5d593f5
6 changed files with 237 additions and 9 deletions

View File

@ -0,0 +1,62 @@
---
source: web/src/endpoints/vehicle/get_edit.rs
expression: body
snapshot_kind: text
---
<section class="section">
<div class="container"><form method="post" action="/vehicles/1">
<h1 class="title">Fahrzeug '11.49.1' bearbeiten</h1>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Funkkenner</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<input class="input" type="text" name="radio_call_name" required placeholder="11.49.1" value="11.49.1" />
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Wache</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<input class="input" type="text" name="station" required placeholder="FF Leipzig Ost" value="FF Leipzig Ost" />
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label"></div>
<div class="field-body">
<div class="field is-grouped">
<div class="control">
<button class="button is-success">
<svg class="icon">
<use href="/static/feather-sprite.svg#check-circle" />
</svg>
<span>Speichern</span>
</button>
</div>
<div class="control">
<a class="button is-link is-light" hx-boost="true" href="/vehicles">
<svg class="icon">
<use href="/static/feather-sprite.svg#arrow-left" />
</svg>
<span>Zurück</span>
</a>
</div>
</div>
</div>
</div>
</form>
</div>
</section>

View File

@ -13,7 +13,7 @@ pub async fn get(
pool: web::Data<PgPool>,
path: web::Path<IdPath>,
) -> Result<impl Responder, ApplicationError> {
if user.role != Role::Admin {
if user.role != Role::Admin && user.role != Role::AreaManager {
return Err(ApplicationError::Unauthorized);
}
@ -28,3 +28,68 @@ pub async fn get(
Ok(template.to_response()?)
}
#[cfg(test)]
mod tests {
use crate::{
models::{Function, Role, Vehicle},
utils::test_helper::{
assert_snapshot, read_body, test_get, DbTestContext, RequestConfig, StatusCode,
},
};
use brass_macros::db_test;
#[db_test]
async fn returns_not_found_for_user(context: &DbTestContext) {
Vehicle::create(&context.db_pool, "11.49.1", "FF Leipzig Ost")
.await
.unwrap();
let app = context.app().await;
let config = RequestConfig::new("/vehicles/1");
let response = test_get(&context.db_pool, &app, &config).await;
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
}
#[db_test]
async fn area_manager_can_edit(context: &DbTestContext) {
Vehicle::create(&context.db_pool, "11.49.1", "FF Leipzig Ost")
.await
.unwrap();
let app = context.app().await;
let config = RequestConfig {
uri: "/vehicles/1".to_string(),
role: Role::AreaManager,
function: vec![Function::Posten],
user_area: 1,
};
let response = test_get(&context.db_pool, &app, &config).await;
assert_eq!(StatusCode::OK, response.status());
}
#[db_test]
async fn produces_template_fine_when_user_is_admin(context: &DbTestContext) {
let app = context.app().await;
Vehicle::create(&context.db_pool, "11.49.1", "FF Leipzig Ost")
.await
.unwrap();
let config = RequestConfig {
uri: "/vehicles/1".to_string(),
role: Role::Admin,
function: vec![Function::Posten],
user_area: 1,
};
let response = test_get(&context.db_pool, &app, &config).await;
assert_eq!(StatusCode::OK, response.status());
let body = read_body(response).await;
assert_snapshot!(body);
}
}

View File

@ -11,13 +11,19 @@ pub mod post_edit;
pub mod post_new;
#[derive(Template)]
#[template(path = "vehicles/new_or_edit.html")]
#[cfg_attr(not(test), template(path = "vehicles/new_or_edit.html"))]
#[cfg_attr(
test,
template(path = "vehicles/new_or_edit.html", block = "content"),
allow(dead_code)
)]
pub struct VehicleNewOrEditTemplate {
user: User,
vehicle: Option<Vehicle>,
}
#[derive(Deserialize)]
#[cfg_attr(test, derive(serde::Serialize))]
pub struct VehicleForm {
radio_call_name: String,
station: String,

View File

@ -14,7 +14,7 @@ pub async fn post(
path: web::Path<IdPath>,
form: web::Form<VehicleForm>,
) -> Result<impl Responder, ApplicationError> {
if user.role != Role::Admin {
if user.role != Role::Admin && user.role != Role::AreaManager {
return Err(ApplicationError::Unauthorized);
}
@ -37,3 +37,80 @@ pub async fn post(
.insert_header(("HX-LOCATION", "/vehicles"))
.finish())
}
#[cfg(test)]
mod tests {
use actix_http::StatusCode;
use brass_macros::db_test;
use crate::{
endpoints::vehicle::VehicleForm,
models::{Role, Vehicle},
utils::test_helper::{test_post, DbTestContext, RequestConfig},
};
#[db_test]
async fn updates_vehicle_when_user_is_admin_and_vehicle_exists(context: &DbTestContext) {
works_for_role(context, Role::Admin).await;
}
#[db_test]
async fn updates_vehicle_when_user_is_area_manager_and_vehicle_exists(context: &DbTestContext) {
works_for_role(context, Role::AreaManager).await;
}
async fn works_for_role(context: &DbTestContext, role: Role) {
Vehicle::create(&context.db_pool, "11.49.1", "FF Leipzig Ost")
.await
.unwrap();
let app = context.app().await;
let config = RequestConfig::new("/vehicles/1").with_role(role);
let request = VehicleForm {
station: "FF Leipzig Ost".to_string(),
radio_call_name: "11.49.2".to_string(),
};
let response = test_post(&context.db_pool, app, &config, request).await;
assert_eq!(StatusCode::FOUND, response.status());
let updated_vehicle = Vehicle::read(&context.db_pool, 1).await.unwrap().unwrap();
assert_eq!("11.49.2".to_string(), updated_vehicle.radio_call_name);
}
#[db_test]
async fn returns_unauthorized_when_user_is_staff(context: &DbTestContext) {
let app = context.app().await;
let config = RequestConfig::new("/vehicles/1");
let request = VehicleForm {
station: "FF Leipzig Ost".to_string(),
radio_call_name: "11.49.2".to_string(),
};
let response = test_post(&context.db_pool, app, &config, request).await;
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
}
#[db_test]
async fn returns_not_found_when_vehicle_does_not_exist(context: &DbTestContext) {
let app = context.app().await;
let config = RequestConfig::new("/vehicles/1").with_role(Role::Admin);
let request = VehicleForm {
station: "FF Leipzig Ost".to_string(),
radio_call_name: "11.49.2".to_string(),
};
let response = test_post(&context.db_pool, app, &config, request).await;
assert_eq!(StatusCode::NOT_FOUND, response.status());
}
}

View File

@ -29,6 +29,24 @@ impl RequestConfig {
user_area: 1,
}
}
pub fn with_role(self, role: Role) -> Self {
let mut obj = self;
obj.role = role;
obj
}
pub fn with_functions(self, functions: &[Function]) -> Self {
let mut obj = self;
obj.function = functions.to_vec();
obj
}
pub fn with_user_area(self, user_area: i32) -> Self {
let mut obj = self;
obj.user_area = user_area;
obj
}
}
async fn create_user_and_get_login_cookie<'a, T, R>(

View File

@ -3,13 +3,13 @@
{% block content %}
<section class="section">
<div class="container">
{% if vehicle.is_some() %}
{%- if vehicle.is_some() -%}
<form method="post" action="/vehicles/{{ vehicle.as_ref().unwrap().id }}">
<h1 class="title">Fahrzeug '{{ vehicle.as_ref().unwrap().radio_call_name }}' bearbeiten</h1>
{% else %}
{%- else -%}
<form method="post" action="/vehicles/new">
<h1 class="title">Neues Fahrzeug anlegen</h1>
{% endif %}
{%- endif %}
<div class="field is-horizontal">
<div class="field-label">
@ -49,11 +49,11 @@
<use href="/static/feather-sprite.svg#check-circle" />
</svg>
<span>
{% if vehicle.is_some() %}
{%- if vehicle.is_some() -%}
Speichern
{% else %}
{%- else -%}
Erstellen
{% endif %}
{%- endif -%}
</span>
</button>
</div>