fix(attendance): add empty layout/label validation and put_json test helper
This commit is contained in:
@@ -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<SqlitePool>,
|
||||
) -> Result<Json<Vec<Value>>, 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<Value> = 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<SqlitePool> {
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user