@ -420,20 +420,41 @@ mod tests {
// ── Mock repository for handler tests ─────────────────────────────────────
// ── Mock repository for handler tests ─────────────────────────────────────
/// A simple mock [`QuoteRepository`] for unit-testing handlers.
/// A simple mock [`QuoteRepository`] for unit-testing handlers.
///
/// Tracks in-memory state for quotes, the admin auth code, and the
/// submissions-locked flag so all trait methods can be exercised without a
/// real database.
struct MockRepo {
struct MockRepo {
quotes : std ::sync ::Mutex < Vec < ( Quote , String ) > > ,
quotes : std ::sync ::Mutex < Vec < ( Quote , String ) > > ,
/// Stored admin super auth code (`None` until seeded).
admin_auth_code : std ::sync ::Mutex < Option < String > > ,
/// Whether new quote submissions are currently locked.
submissions_locked : std ::sync ::Mutex < bool > ,
}
}
impl MockRepo {
impl MockRepo {
fn empty ( ) -> Repo {
fn empty ( ) -> Repo {
Arc ::new ( Self {
Arc ::new ( Self {
quotes : std ::sync ::Mutex ::new ( vec! [ ] ) ,
quotes : std ::sync ::Mutex ::new ( vec! [ ] ) ,
admin_auth_code : std ::sync ::Mutex ::new ( None ) ,
submissions_locked : std ::sync ::Mutex ::new ( false ) ,
} )
} )
}
}
fn with_quote ( quote : Quote , auth : & str ) -> Repo {
fn with_quote ( quote : Quote , auth : & str ) -> Repo {
Arc ::new ( Self {
Arc ::new ( Self {
quotes : std ::sync ::Mutex ::new ( vec! [ ( quote , auth . to_owned ( ) ) ] ) ,
quotes : std ::sync ::Mutex ::new ( vec! [ ( quote , auth . to_owned ( ) ) ] ) ,
admin_auth_code : std ::sync ::Mutex ::new ( None ) ,
submissions_locked : std ::sync ::Mutex ::new ( false ) ,
} )
}
/// Build a [`Repo`] pre-seeded with the given admin auth code.
fn with_admin_code ( code : & str ) -> Arc < Self > {
Arc ::new ( Self {
quotes : std ::sync ::Mutex ::new ( vec! [ ] ) ,
admin_auth_code : std ::sync ::Mutex ::new ( Some ( code . to_owned ( ) ) ) ,
submissions_locked : std ::sync ::Mutex ::new ( false ) ,
} )
} )
}
}
}
}
@ -544,26 +565,41 @@ mod tests {
}
}
async fn get_admin_auth_code ( & self ) -> Result < Option < String > , DbError > {
async fn get_admin_auth_code ( & self ) -> Result < Option < String > , DbError > {
Ok ( None )
Ok ( self . admin_auth_code . lock ( ) . unwrap ( ) . clone ( ) )
}
}
async fn seed_admin_auth_code ( & self , _code : & str ) -> Result < ( ) , DbError > {
async fn seed_admin_auth_code ( & self , code : & str ) -> Result < ( ) , DbError > {
let mut guard = self . admin_auth_code . lock ( ) . unwrap ( ) ;
if guard . is_none ( ) {
* guard = Some ( code . to_owned ( ) ) ;
}
Ok ( ( ) )
Ok ( ( ) )
}
}
async fn update_admin_auth_code (
async fn update_admin_auth_code (
& self ,
& self ,
_ current: & str ,
current: & str ,
_ new_code: Option < & str > ,
new_code: Option < & str > ,
) -> Result < String , DbError > {
) -> Result < String , DbError > {
Err ( DbError ::Forbidden )
let mut guard = self . admin_auth_code . lock ( ) . unwrap ( ) ;
match guard . as_deref ( ) {
Some ( stored ) if stored = = current = > {
let replacement = new_code
. map ( | s | s . to_owned ( ) )
. unwrap_or_else ( | | "new-mock-code" . to_owned ( ) ) ;
* guard = Some ( replacement . clone ( ) ) ;
Ok ( replacement )
}
_ = > Err ( DbError ::Forbidden ) ,
}
}
}
async fn get_submissions_locked ( & self ) -> Result < bool , DbError > {
async fn get_submissions_locked ( & self ) -> Result < bool , DbError > {
Ok ( false )
Ok ( * self . submissions_locked . lock ( ) . unwrap ( ) )
}
}
async fn set_submissions_locked ( & self , _locked : bool ) -> Result < ( ) , DbError > {
async fn set_submissions_locked ( & self , locked : bool ) -> Result < ( ) , DbError > {
* self . submissions_locked . lock ( ) . unwrap ( ) = locked ;
Ok ( ( ) )
Ok ( ( ) )
}
}
@ -778,6 +814,70 @@ mod tests {
let ( status , _ ) = send ( app , req ) . await ;
let ( status , _ ) = send ( app , req ) . await ;
assert_eq! ( status , StatusCode ::NOT_FOUND ) ;
assert_eq! ( status , StatusCode ::NOT_FOUND ) ;
}
}
// ── Admin DB method tests ──────────────────────────────────────────────────
/// `get_submissions_locked` returns `false` when the repo has just been
/// created (no `set_submissions_locked` call has been made yet).
#[ tokio::test ]
async fn test_get_submissions_locked_default_false ( ) {
let repo = MockRepo ::empty ( ) ;
let locked = repo
. get_submissions_locked ( )
. await
. expect ( "get_submissions_locked should not fail" ) ;
assert! ( ! locked , "submissions should be unlocked by default" ) ;
}
/// After calling `set_submissions_locked(true)`, `get_submissions_locked`
/// must return `true`.
#[ tokio::test ]
async fn test_set_and_get_submissions_locked ( ) {
let repo = MockRepo ::empty ( ) ;
repo . set_submissions_locked ( true )
. await
. expect ( "set_submissions_locked should not fail" ) ;
let locked = repo
. get_submissions_locked ( )
. await
. expect ( "get_submissions_locked should not fail" ) ;
assert! (
locked ,
"submissions should be locked after set_submissions_locked(true)"
) ;
}
/// `update_admin_auth_code` with the correct current code succeeds and
/// returns the new code.
#[ tokio::test ]
async fn test_update_admin_auth_code_correct_current_succeeds ( ) {
let repo = MockRepo ::with_admin_code ( "old-code" ) ;
let new_code = repo
. update_admin_auth_code ( "old-code" , Some ( "brand-new-code" ) )
. await
. expect ( "update_admin_auth_code should succeed when current matches" ) ;
assert_eq! ( new_code , "brand-new-code" ) ;
// The stored code should now be the new one.
let stored = repo
. get_admin_auth_code ( )
. await
. expect ( "get_admin_auth_code should not fail" ) ;
assert_eq! ( stored . as_deref ( ) , Some ( "brand-new-code" ) ) ;
}
/// `update_admin_auth_code` with the wrong current code returns
/// `Err(DbError::Forbidden)`.
#[ tokio::test ]
async fn test_update_admin_auth_code_wrong_current_forbidden ( ) {
let repo = MockRepo ::with_admin_code ( "real-code" ) ;
let result = repo
. update_admin_auth_code ( "wrong-code" , Some ( "irrelevant" ) )
. await ;
assert! (
matches! ( result , Err ( DbError ::Forbidden ) ) ,
"expected Forbidden, got {result:?}" ,
) ;
}
}
}
// ── Integration tests (real NativeRepository + real SQLite) ─────────────────
// ── Integration tests (real NativeRepository + real SQLite) ─────────────────