From fcf2787bcc9f77c3d69bb2e160b9555cb48d64f9 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Tue, 28 Apr 2026 03:18:56 +0200 Subject: [PATCH] fix(attendance): add empty layout/label validation and put_json test helper --- backend/src/routes/rooms.rs | 69 +++++++++++++++++++++++++++++-------- backend/src/test_helpers.rs | 20 +++++++++++ 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/backend/src/routes/rooms.rs b/backend/src/routes/rooms.rs index 5f2d76a..6d7659c 100644 --- a/backend/src/routes/rooms.rs +++ b/backend/src/routes/rooms.rs @@ -15,6 +15,10 @@ use crate::{ }; fn validate_layout(layout: &[LayoutElement]) -> Result<(), AppError> { + if layout.is_empty() { + return Err(AppError::BadRequest("layout must contain at least one element".into())); + } + let valid_types = ["seat", "table", "gap", "door"]; let mut ids: HashSet<&str> = HashSet::new(); let mut seat_labels: HashSet<&str> = HashSet::new(); @@ -46,6 +50,9 @@ fn validate_layout(layout: &[LayoutElement]) -> Result<(), AppError> { } // seat labels must be unique among seats if elem.kind == "seat" { + if elem.label.is_empty() { + return Err(AppError::BadRequest("seat label must be non-empty".into())); + } if !seat_labels.insert(elem.label.as_str()) { return Err(AppError::BadRequest(format!( "duplicate seat label: {}", @@ -62,12 +69,12 @@ async fn list_rooms( _claims: TutorClaims, State(pool): State, ) -> Result>, AppError> { - let rows = sqlx::query_as::<_, Room>("SELECT id, name, layout_json FROM rooms ORDER BY id") + let rows = sqlx::query_as::<_, (i64, String)>("SELECT id, name FROM rooms ORDER BY id") .fetch_all(&pool) .await?; let result: Vec = rows .into_iter() - .map(|r| json!({"id": r.id, "name": r.name})) + .map(|(id, name)| json!({"id": id, "name": name})) .collect(); Ok(Json(result)) } @@ -139,7 +146,7 @@ pub fn router() -> Router { #[cfg(test)] mod tests { use super::*; - use crate::test_helpers::{build_test_app, get, post_json}; + use crate::test_helpers::{build_test_app, get, post_json, put_json}; use axum::http::StatusCode; use serde_json::{json, Value}; @@ -252,22 +259,18 @@ mod tests { .unwrap(); // update layout via PUT - use axum::http::Request; - use http_body_util::BodyExt; - use tower::ServiceExt; let new_layout = json!([ {"id":"s1","label":"A1","x":0.0,"y":0.0,"width":1.0,"height":1.0,"type":"seat"}, {"id":"s2","label":"A2","x":1.0,"y":0.0,"width":1.0,"height":1.0,"type":"seat"} ]); - let req = Request::builder() - .method("PUT") - .uri(format!("/api/admin/rooms/{id}/layout")) - .header("Content-Type", "application/json") - .header("Authorization", &auth) - .body(axum::body::Body::from(new_layout.to_string())) - .unwrap(); - let res = app.clone().oneshot(req).await.unwrap(); - assert_eq!(res.status(), StatusCode::OK); + let (status, _) = put_json( + app.clone(), + &format!("/api/admin/rooms/{id}/layout"), + &auth, + new_layout, + ) + .await; + assert_eq!(status, StatusCode::OK); // verify new layout has 2 seats let (status, body) = get(app, &format!("/api/admin/rooms/{id}"), &auth).await; @@ -282,4 +285,40 @@ mod tests { let (status, _) = get(app, "/api/admin/rooms", "").await; assert_eq!(status, StatusCode::UNAUTHORIZED); } + + #[sqlx::test(migrations = "./migrations")] + async fn layout_validation_rejects_empty_layout(pool: sqlx::SqlitePool) { + let (app, auth) = build_test_app(pool).await; + let (status, _) = post_json( + app, + "/api/admin/rooms", + &auth, + json!({"name":"Bad","layout":[]}), + ) + .await; + assert_eq!(status, StatusCode::BAD_REQUEST); + } + + #[sqlx::test(migrations = "./migrations")] + async fn layout_validation_rejects_empty_seat_label(pool: sqlx::SqlitePool) { + let (app, auth) = build_test_app(pool).await; + let layout = json!([ + {"id":"s1","label":"","x":0.0,"y":0.0,"width":1.0,"height":1.0,"type":"seat"} + ]); + let (status, _) = post_json( + app, + "/api/admin/rooms", + &auth, + json!({"name":"Bad","layout":layout}), + ) + .await; + assert_eq!(status, StatusCode::BAD_REQUEST); + } + + #[sqlx::test(migrations = "./migrations")] + async fn get_room_not_found(pool: sqlx::SqlitePool) { + let (app, auth) = build_test_app(pool).await; + let (status, _) = get(app, "/api/admin/rooms/999", &auth).await; + assert_eq!(status, StatusCode::NOT_FOUND); + } } diff --git a/backend/src/test_helpers.rs b/backend/src/test_helpers.rs index 320db74..83088d6 100644 --- a/backend/src/test_helpers.rs +++ b/backend/src/test_helpers.rs @@ -44,6 +44,26 @@ pub async fn post_json(app: Router, path: &str, auth: &str, body: serde_json::Va (status, body) } +/// PUT JSON body to the app (one-shot), returns (StatusCode, response body bytes). +pub async fn put_json(app: Router, uri: &str, auth: &str, body: serde_json::Value) + -> (StatusCode, bytes::Bytes) +{ + let mut req = Request::builder() + .method("PUT") + .uri(uri) + .header("Content-Type", "application/json"); + if !auth.is_empty() { + req = req.header("Authorization", auth); + } + let req = req + .body(axum::body::Body::from(body.to_string())) + .unwrap(); + let res = app.oneshot(req).await.unwrap(); + let status = res.status(); + let body = res.into_body().collect().await.unwrap().to_bytes(); + (status, body) +} + /// GET from the app (one-shot), returns (StatusCode, response body bytes). pub async fn get(app: Router, path: &str, auth: &str) -> (StatusCode, bytes::Bytes) { let mut builder = Request::builder()