From feac14640301f9a53d5f03ac57aa6a6f002e195f Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Wed, 4 Mar 2026 13:05:45 -0800 Subject: [PATCH] feat(quotesdb): GET /api/status public endpoint Adds the GET /api/status handler that returns {"submissions_locked": bool}. Registers the route in the router before the quotes routes. Adds three unit tests covering unlocked state, locked state, and default false. Co-Authored-By: Claude Sonnet 4.6 --- quotesdb/src/bin/api/handlers/mod.rs | 71 ++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/quotesdb/src/bin/api/handlers/mod.rs b/quotesdb/src/bin/api/handlers/mod.rs index 69cde28..c42c1bb 100644 --- a/quotesdb/src/bin/api/handlers/mod.rs +++ b/quotesdb/src/bin/api/handlers/mod.rs @@ -159,6 +159,25 @@ async fn openapi_handler() -> Response { .into_response() } +/// `GET /api/status` — return whether quote submissions are currently locked. +/// +/// This endpoint requires no authentication and is intended to be called by +/// the UI on mount for both the `/submit` and `/admin` pages. Returns a JSON +/// object with a single boolean field: +/// +/// ```json +/// { "submissions_locked": false } +/// ``` +/// +/// Returns `500 Internal Server Error` if the database query fails. +#[cfg_attr(target_arch = "wasm32", worker::send)] +pub async fn get_status(State(repo): State) -> Response { + match repo.get_submissions_locked().await { + Ok(locked) => Json(serde_json::json!({ "submissions_locked": locked })).into_response(), + Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } +} + /// `GET /api/quotes` — list quotes with optional filtering and pagination. /// /// Accepts `?page=N&author=X&tag=Y&date_after_year=Y&date_before_year=Y` (and @@ -394,6 +413,8 @@ pub fn router(repo: Arc) -> Router { Router::new() // Meta .route("/api/", get(openapi_handler)) + // Public status — exposes whether submissions are currently locked. + .route("/api/status", get(get_status)) // IMPORTANT: /random must be registered before /{id} so the static // segment wins over the dynamic capture. .route("/api/quotes/random", get(random_handler)) @@ -878,6 +899,56 @@ mod tests { "expected Forbidden, got {result:?}", ); } + + // ── GET /api/status handler tests ───────────────────────────────────────── + + /// `GET /api/status` returns `200` with `{"submissions_locked": false}` when + /// the repo's submissions lock is unset (the default `false` state). + #[tokio::test] + async fn test_get_status_unlocked() { + let app = router(MockRepo::empty()); + let req = Request::builder() + .method(Method::GET) + .uri("/api/status") + .body(Body::empty()) + .unwrap(); + let (status, body) = send(app, req).await; + assert_eq!(status, StatusCode::OK); + let v: serde_json::Value = serde_json::from_str(&body).unwrap(); + assert_eq!(v["submissions_locked"], false); + } + + /// `GET /api/status` returns `200` with `{"submissions_locked": true}` after + /// the lock has been enabled via `set_submissions_locked(true)`. + #[tokio::test] + async fn test_get_status_locked() { + let repo = MockRepo::empty(); + repo.set_submissions_locked(true) + .await + .expect("set_submissions_locked should not fail"); + let app = router(repo); + let req = Request::builder() + .method(Method::GET) + .uri("/api/status") + .body(Body::empty()) + .unwrap(); + let (status, body) = send(app, req).await; + assert_eq!(status, StatusCode::OK); + let v: serde_json::Value = serde_json::from_str(&body).unwrap(); + assert_eq!(v["submissions_locked"], true); + } + + /// `get_submissions_locked` returns `false` for a freshly created repo + /// (graceful default — no DB row or explicit seed needed). + #[tokio::test] + async fn test_get_submissions_locked_default_is_false() { + let repo = MockRepo::empty(); + let locked = repo + .get_submissions_locked() + .await + .expect("get_submissions_locked should not fail"); + assert!(!locked, "submissions should default to unlocked"); + } } // ── Integration tests (real NativeRepository + real SQLite) ─────────────────