Add unit tests for list_ranked_articles' feed_id filtering
The sidebar's per-feed nav depends on this query correctly scoping to one feed (or joining all of them when feed_id is None), and it's real branching logic rather than a passthrough — exactly what the updated Definition of Done's testing rule calls for. Uses a throwaway SQLite file per test (migrated fresh, cleaned up via Drop) rather than a shared fixture, since diesel-async's bb8 pool would otherwise hand out per-connection ":memory:" databases that don't share state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PqTJmazHBQK878vjQ4JnnF
This commit is contained in:
parent
6a2bf8c9a8
commit
8edffe8973
1 changed files with 115 additions and 0 deletions
|
|
@ -152,3 +152,118 @@ impl Db {
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// A `Db` backed by a throwaway SQLite file under the OS temp dir,
|
||||||
|
/// migrated fresh per test and deleted (including its `-wal`/`-shm`
|
||||||
|
/// siblings) when the test finishes, so tests can't see each other's
|
||||||
|
/// data or leak files across runs.
|
||||||
|
struct TestDb {
|
||||||
|
db: Db,
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for TestDb {
|
||||||
|
type Target = Db;
|
||||||
|
fn deref(&self) -> &Db {
|
||||||
|
&self.db
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TestDb {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
for suffix in ["", "-wal", "-shm"] {
|
||||||
|
let _ = std::fs::remove_file(format!("{}{suffix}", self.path.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_db() -> TestDb {
|
||||||
|
let path = std::env::temp_dir().join(format!("feedsignal-test-{}.db", Uuid::new_v4()));
|
||||||
|
let db = Db::connect(path.to_str().unwrap())
|
||||||
|
.await
|
||||||
|
.expect("connect test db");
|
||||||
|
TestDb { db, path }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn article(feed_id: Uuid, final_score: f32) -> Article {
|
||||||
|
Article {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
feed_id,
|
||||||
|
url: format!("https://example.com/{}", Uuid::new_v4()),
|
||||||
|
title: "Title".to_string(),
|
||||||
|
summary: "Summary".to_string(),
|
||||||
|
published_at: None,
|
||||||
|
topics: vec![],
|
||||||
|
embedding_score: None,
|
||||||
|
llm_score: None,
|
||||||
|
final_score: Some(final_score),
|
||||||
|
estimated_read_seconds: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `feed_id: None` joins every subscribed feed's articles into one
|
||||||
|
/// ranked list — what the sidebar's "All" view depends on.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_ranked_articles_with_no_feed_filter_returns_every_feed() {
|
||||||
|
let db = test_db().await;
|
||||||
|
let feed_a = db
|
||||||
|
.upsert_feed("https://a.example.com/feed", "Feed A")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let feed_b = db
|
||||||
|
.upsert_feed("https://b.example.com/feed", "Feed B")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
db.insert_article(&article(feed_a, 0.9)).await.unwrap();
|
||||||
|
db.insert_article(&article(feed_b, 0.5)).await.unwrap();
|
||||||
|
|
||||||
|
let rows = db.list_ranked_articles(None, 10).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(rows.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `feed_id: Some(id)` scopes the list to that one feed and excludes
|
||||||
|
/// every other subscribed feed's articles — what the sidebar's
|
||||||
|
/// per-feed nav depends on.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_ranked_articles_with_feed_filter_excludes_other_feeds() {
|
||||||
|
let db = test_db().await;
|
||||||
|
let feed_a = db
|
||||||
|
.upsert_feed("https://a.example.com/feed", "Feed A")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let feed_b = db
|
||||||
|
.upsert_feed("https://b.example.com/feed", "Feed B")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
db.insert_article(&article(feed_a, 0.9)).await.unwrap();
|
||||||
|
db.insert_article(&article(feed_b, 0.5)).await.unwrap();
|
||||||
|
|
||||||
|
let rows = db.list_ranked_articles(Some(feed_a), 10).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(rows.len(), 1);
|
||||||
|
assert_eq!(rows[0].1, feed_a.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Results stay ordered by `final_score` descending regardless of
|
||||||
|
/// insertion order, matching how the UI ranks the article list.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_ranked_articles_orders_by_final_score_descending() {
|
||||||
|
let db = test_db().await;
|
||||||
|
let feed = db
|
||||||
|
.upsert_feed("https://a.example.com/feed", "Feed A")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
db.insert_article(&article(feed, 0.2)).await.unwrap();
|
||||||
|
db.insert_article(&article(feed, 0.8)).await.unwrap();
|
||||||
|
|
||||||
|
let rows = db.list_ranked_articles(None, 10).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(rows[0].6, Some(0.8));
|
||||||
|
assert_eq!(rows[1].6, Some(0.2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue