diff --git a/quotesdb/src/bin/api/db/native.rs b/quotesdb/src/bin/api/db/native.rs index 95f6e00..f4b8401 100644 --- a/quotesdb/src/bin/api/db/native.rs +++ b/quotesdb/src/bin/api/db/native.rs @@ -191,20 +191,7 @@ impl QuoteRepository for NativeRepository { let partial_quotes: Vec = stmt .query_map( rusqlite::params_from_iter(param_refs2.iter().copied()), - |row| { - let hidden_int: i64 = row.get(5)?; - Ok(Quote { - id: row.get(0)?, - text: row.get(1)?, - author: row.get(2)?, - source: row.get(3)?, - date: row.get(4)?, - hidden: hidden_int != 0, - created_at: row.get(6)?, - updated_at: row.get(7)?, - tags: vec![], - }) - }, + |row| row_to_quote(row, vec![]), )? .collect::, _>>()?; @@ -984,4 +971,135 @@ mod tests { let stored = repo.get_admin_auth_code().await.unwrap(); assert_eq!(stored.as_deref(), Some("correct-code")); } + + // ── hidden flag filter tests ─────────────────────────────────────────────── + + /// `list_quotes` must exclude hidden quotes and include only visible ones. + #[tokio::test] + async fn test_list_quotes_excludes_hidden() { + let repo = in_memory_repo().await; + + // Create a visible quote and a hidden quote. + let (visible, _) = repo + .create_quote(CreateQuoteInput { + text: "Visible quote".to_owned(), + author: "Author A".to_owned(), + source: None, + date: None, + tags: vec![], + auth_code: Some("auth-visible-xxxxx".to_owned()), + cf_turnstile_token: None, + }) + .await + .unwrap(); + + let (hidden, hidden_auth) = repo + .create_quote(CreateQuoteInput { + text: "Hidden quote".to_owned(), + author: "Author B".to_owned(), + source: None, + date: None, + tags: vec![], + auth_code: Some("auth-hidden-xxxxxx".to_owned()), + cf_turnstile_token: None, + }) + .await + .unwrap(); + + // Mark the second quote as hidden. + repo.update_quote( + &hidden.id, + UpdateQuoteInput { + hidden: Some(true), + ..UpdateQuoteInput::default() + }, + &hidden_auth, + ) + .await + .unwrap(); + + let result = repo.list_quotes(1, None, None, None, None).await.unwrap(); + assert_eq!(result.total_count, 1, "only the visible quote should count"); + assert_eq!(result.quotes.len(), 1); + assert_eq!( + result.quotes[0].id, visible.id, + "the returned quote must be the visible one" + ); + } + + /// `get_random_quote` must return `None` when the only quote is hidden. + #[tokio::test] + async fn test_get_random_quote_excludes_hidden() { + let repo = in_memory_repo().await; + + // Create a single quote and immediately hide it. + let (quote, auth) = repo + .create_quote(CreateQuoteInput { + text: "Only quote, hidden".to_owned(), + author: "Ghost".to_owned(), + source: None, + date: None, + tags: vec![], + auth_code: Some("auth-ghost-xxxxxxx".to_owned()), + cf_turnstile_token: None, + }) + .await + .unwrap(); + + repo.update_quote( + "e.id, + UpdateQuoteInput { + hidden: Some(true), + ..UpdateQuoteInput::default() + }, + &auth, + ) + .await + .unwrap(); + + let result = repo.get_random_quote().await.unwrap(); + assert!( + result.is_none(), + "get_random_quote should return None when only hidden quotes exist" + ); + } + + /// `get_quote` (direct ID lookup) must return the quote even when it is hidden. + #[tokio::test] + async fn test_get_quote_returns_hidden_quote() { + let repo = in_memory_repo().await; + + let (quote, auth) = repo + .create_quote(CreateQuoteInput { + text: "Accessible but hidden".to_owned(), + author: "Secret Author".to_owned(), + source: None, + date: None, + tags: vec![], + auth_code: Some("auth-secret-xxxxxx".to_owned()), + cf_turnstile_token: None, + }) + .await + .unwrap(); + + repo.update_quote( + "e.id, + UpdateQuoteInput { + hidden: Some(true), + ..UpdateQuoteInput::default() + }, + &auth, + ) + .await + .unwrap(); + + let fetched = repo.get_quote("e.id).await.unwrap(); + assert!( + fetched.is_some(), + "get_quote must return the quote even when it is hidden" + ); + let fetched = fetched.unwrap(); + assert_eq!(fetched.id, quote.id); + assert!(fetched.hidden, "the returned quote must have hidden=true"); + } }