@ -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) ─────────────────