//! Search filter, body/bite text matching, combined search filters, //! and pagination with filters integration tests. mod common; use bb_feed::{FeedFilter, FeedGenerator}; // ── Search Filter ──────────────────────────────────────────────────── #[tokio::test] async fn list_items_search_matches_title() { let db = common::test_db().await; let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await; common::insert_item(&db, &feed, "rss:1", "Rust Programming", 1).await; common::insert_item(&db, &feed, "rss:2", "Go Language", 2).await; common::insert_item(&db, &feed, "rss:3", "Rust Async Runtime", 3).await; let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().search("rust")); let result = fg.get_items(0).await.unwrap(); assert_eq!(result.items.len(), 2); for item in &result.items { let title = item.content.title.as_deref().unwrap_or(""); assert!(title.to_lowercase().contains("rust")); } } #[tokio::test] async fn list_items_search_no_match() { let db = common::test_db().await; let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await; common::insert_item(&db, &feed, "rss:1", "Rust Programming", 1).await; let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().search("python")); let result = fg.get_items(0).await.unwrap(); assert!(result.items.is_empty()); } #[tokio::test] async fn list_items_search_case_insensitive() { let db = common::test_db().await; let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await; common::insert_item(&db, &feed, "rss:1", "RUST PROGRAMMING", 1).await; let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().search("rust")); let result = fg.get_items(0).await.unwrap(); assert_eq!(result.items.len(), 1); } #[tokio::test] async fn list_items_search_with_source_filter() { let db = common::test_db().await; let feed_rss = common::create_rss_feed(&db, "RSS", "https://example.com/rss").await; let feed_hn = common::create_other_feed(&db, "hn", "HN").await; common::insert_item(&db, &feed_rss, "rss:1", "Rust News", 1).await; common::insert_item(&db, &feed_hn, "hn:1", "Rust on HN", 1).await; let fg = FeedGenerator::new(db) .with_filter(FeedFilter::new().search("rust").source("rss")); let result = fg.get_items(0).await.unwrap(); assert_eq!(result.items.len(), 1); assert_eq!(result.items[0].id.source, "rss"); } #[tokio::test] async fn list_items_search_with_unread_filter() { let db = common::test_db().await; let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await; let item1 = common::insert_item(&db, &feed, "rss:1", "Rust Alpha", 1).await; common::insert_item(&db, &feed, "rss:2", "Rust Beta", 2).await; db.items().mark_read(item1.id, true).await.unwrap(); let fg = FeedGenerator::new(db) .with_filter(FeedFilter::new().search("rust").unread_only()); let result = fg.get_items(0).await.unwrap(); assert_eq!(result.items.len(), 1); assert!(!result.items[0].is_read); } // ── Search: Body and Bite Text Matching ────────────────────────────── #[tokio::test] async fn search_matches_body_content() { let db = common::test_db().await; let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await; // insert_item helper puts "Body of {title}" in the body field common::insert_item(&db, &feed, "rss:1", "Generic Title", 1).await; // Search for text that appears in the title (and body via "Body of Generic Title") let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().search("Generic")); let result = fg.get_items(0).await.unwrap(); assert_eq!(result.items.len(), 1); } #[tokio::test] async fn search_matches_bite_text() { let db = common::test_db().await; let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await; // insert_item puts "Item {external_id}" in bite_text common::insert_item(&db, &feed, "rss:searchbite", "Unrelated Title", 1).await; // Search for text in bite_text field: "Item rss:searchbite" let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().search("searchbite")); let result = fg.get_items(0).await.unwrap(); assert_eq!(result.items.len(), 1); } // ── Search with Combined Filters: Starred + Source ─────────────────── #[tokio::test] async fn search_with_starred_and_source_filter() { let db = common::test_db().await; let feed_rss = common::create_rss_feed(&db, "RSS", "https://example.com/rss").await; let feed_hn = common::create_other_feed(&db, "hn", "HN").await; let rss1 = common::insert_item(&db, &feed_rss, "rss:1", "Rust News", 1).await; common::insert_item(&db, &feed_rss, "rss:2", "Rust Guide", 2).await; let hn1 = common::insert_item(&db, &feed_hn, "hn:1", "Rust on HN", 1).await; // Star rss:1 and hn:1, leave rss:2 unstarred db.items().mark_starred(rss1.id, true).await.unwrap(); db.items().mark_starred(hn1.id, true).await.unwrap(); // Search "Rust" + starred + source=rss -> only rss:1 let fg = FeedGenerator::new(db) .with_filter( FeedFilter::new() .search("rust") .starred_only() .source("rss"), ); let result = fg.get_items(0).await.unwrap(); assert_eq!(result.items.len(), 1); assert_eq!(result.items[0].id.item_id, "rss:1"); assert!(result.items[0].is_starred); assert_eq!(result.items[0].id.source, "rss"); } // ── Pagination with Filters ────────────────────────────────────────── #[tokio::test] async fn pagination_with_unread_filter() { let db = common::test_db().await; let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await; // Insert 5 items, mark 2 as read let item0 = common::insert_item(&db, &feed, "rss:p0", "Item 0", 0).await; let item1 = common::insert_item(&db, &feed, "rss:p1", "Item 1", 1).await; common::insert_item(&db, &feed, "rss:p2", "Item 2", 2).await; common::insert_item(&db, &feed, "rss:p3", "Item 3", 3).await; common::insert_item(&db, &feed, "rss:p4", "Item 4", 4).await; db.items().mark_read(item0.id, true).await.unwrap(); db.items().mark_read(item1.id, true).await.unwrap(); // 3 unread items, paginate with page_size=2 let fg = FeedGenerator::new(db) .with_filter(FeedFilter::new().unread_only()) .with_page_size(2); let page0 = fg.get_items(0).await.unwrap(); assert_eq!(page0.items.len(), 2); assert!(page0.has_more); assert!(page0.items.iter().all(|i| !i.is_read)); let page1 = fg.get_items(1).await.unwrap(); assert_eq!(page1.items.len(), 1); assert!(!page1.has_more); assert!(page1.items.iter().all(|i| !i.is_read)); } #[tokio::test] async fn pagination_with_starred_filter() { let db = common::test_db().await; let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await; // Insert 4 items, star 3 of them common::insert_item(&db, &feed, "rss:s0", "Item 0", 0).await; let item1 = common::insert_item(&db, &feed, "rss:s1", "Item 1", 1).await; let item2 = common::insert_item(&db, &feed, "rss:s2", "Item 2", 2).await; let item3 = common::insert_item(&db, &feed, "rss:s3", "Item 3", 3).await; db.items().mark_starred(item1.id, true).await.unwrap(); db.items().mark_starred(item2.id, true).await.unwrap(); db.items().mark_starred(item3.id, true).await.unwrap(); let fg = FeedGenerator::new(db) .with_filter(FeedFilter::new().starred_only()) .with_page_size(2); let page0 = fg.get_items(0).await.unwrap(); assert_eq!(page0.items.len(), 2); assert!(page0.has_more); assert!(page0.items.iter().all(|i| i.is_starred)); let page1 = fg.get_items(1).await.unwrap(); assert_eq!(page1.items.len(), 1); assert!(!page1.has_more); } #[tokio::test] async fn pagination_with_source_filter() { let db = common::test_db().await; let feed_rss = common::create_rss_feed(&db, "RSS", "https://example.com/rss").await; let feed_hn = common::create_other_feed(&db, "hn", "HN").await; // 4 RSS items, 2 HN items for i in 0..4 { common::insert_item(&db, &feed_rss, &format!("rss:p{i}"), &format!("RSS {i}"), i).await; } for i in 0..2 { common::insert_item(&db, &feed_hn, &format!("hn:p{i}"), &format!("HN {i}"), i).await; } let fg = FeedGenerator::new(db) .with_filter(FeedFilter::new().source("rss")) .with_page_size(3); let page0 = fg.get_items(0).await.unwrap(); assert_eq!(page0.items.len(), 3); assert!(page0.has_more); assert!(page0.items.iter().all(|i| i.id.source == "rss")); let page1 = fg.get_items(1).await.unwrap(); assert_eq!(page1.items.len(), 1); assert!(!page1.has_more); assert!(page1.items.iter().all(|i| i.id.source == "rss")); } #[tokio::test] async fn pagination_with_search_filter() { let db = common::test_db().await; let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await; // Insert items: 4 with "Rust" in title, 2 without for i in 0..4 { common::insert_item(&db, &feed, &format!("rss:rust{i}"), &format!("Rust Article {i}"), i).await; } common::insert_item(&db, &feed, "rss:go1", "Go Article", 5).await; common::insert_item(&db, &feed, "rss:go2", "Go Article 2", 6).await; let fg = FeedGenerator::new(db) .with_filter(FeedFilter::new().search("rust")) .with_page_size(2); let page0 = fg.get_items(0).await.unwrap(); assert_eq!(page0.items.len(), 2); assert!(page0.has_more); let page1 = fg.get_items(1).await.unwrap(); assert_eq!(page1.items.len(), 2); assert!(!page1.has_more); // Page past end should be empty let page2 = fg.get_items(2).await.unwrap(); assert!(page2.items.is_empty()); assert!(!page2.has_more); }