test(quotesdb): add handler-level tests for DB admin methods

Make MockRepo stateful for admin_auth_code and submissions_locked so
the new QuoteRepository methods can be exercised without a real DB.

Add four tests to src/bin/api/handlers/mod.rs:
- get_submissions_locked returns false by default
- set_submissions_locked(true) then get_submissions_locked returns true
- update_admin_auth_code with correct current succeeds and returns new code
- update_admin_auth_code with wrong current returns Forbidden

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
quotesdb
Elijah Voigt 3 months ago
parent c9142edbbf
commit 585f4b2f02

@ -420,20 +420,41 @@ mod tests {
// ── Mock repository for handler tests ─────────────────────────────────────
/// 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 {
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 {
fn empty() -> Repo {
Arc::new(Self {
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 {
Arc::new(Self {
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> {
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(())
}
async fn update_admin_auth_code(
&self,
_current: &str,
_new_code: Option<&str>,
current: &str,
new_code: Option<&str>,
) -> 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> {
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(())
}
@ -778,6 +814,70 @@ mod tests {
let (status, _) = send(app, req).await;
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) ─────────────────

Loading…
Cancel
Save