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 <noreply@anthropic.com>
main
Elijah Voigt 3 months ago
parent 177a892d94
commit feac146403

@ -159,6 +159,25 @@ async fn openapi_handler() -> Response {
.into_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<Repo>) -> 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. /// `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 /// 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<dyn QuoteRepository + Send + Sync>) -> Router {
Router::new() Router::new()
// Meta // Meta
.route("/api/", get(openapi_handler)) .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 // IMPORTANT: /random must be registered before /{id} so the static
// segment wins over the dynamic capture. // segment wins over the dynamic capture.
.route("/api/quotes/random", get(random_handler)) .route("/api/quotes/random", get(random_handler))
@ -878,6 +899,56 @@ mod tests {
"expected Forbidden, got {result:?}", "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) ───────────────── // ── Integration tests (real NativeRepository + real SQLite) ─────────────────

Loading…
Cancel
Save