Skip to main content

max / balanced_breakfast

10.1 KB · 261 lines History Blame Raw
1 //! Search filter, body/bite text matching, combined search filters,
2 //! and pagination with filters integration tests.
3
4 mod common;
5
6 use bb_feed::{FeedFilter, FeedGenerator};
7
8 // ── Search Filter ────────────────────────────────────────────────────
9
10 #[tokio::test]
11 async fn list_items_search_matches_title() {
12 let db = common::test_db().await;
13 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
14 common::insert_item(&db, &feed, "rss:1", "Rust Programming", 1).await;
15 common::insert_item(&db, &feed, "rss:2", "Go Language", 2).await;
16 common::insert_item(&db, &feed, "rss:3", "Rust Async Runtime", 3).await;
17
18 let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().search("rust"));
19 let result = fg.get_items(0).await.unwrap();
20 assert_eq!(result.items.len(), 2);
21 for item in &result.items {
22 let title = item.content.title.as_deref().unwrap_or("");
23 assert!(title.to_lowercase().contains("rust"));
24 }
25 }
26
27 #[tokio::test]
28 async fn list_items_search_no_match() {
29 let db = common::test_db().await;
30 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
31 common::insert_item(&db, &feed, "rss:1", "Rust Programming", 1).await;
32
33 let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().search("python"));
34 let result = fg.get_items(0).await.unwrap();
35 assert!(result.items.is_empty());
36 }
37
38 #[tokio::test]
39 async fn list_items_search_case_insensitive() {
40 let db = common::test_db().await;
41 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
42 common::insert_item(&db, &feed, "rss:1", "RUST PROGRAMMING", 1).await;
43
44 let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().search("rust"));
45 let result = fg.get_items(0).await.unwrap();
46 assert_eq!(result.items.len(), 1);
47 }
48
49 #[tokio::test]
50 async fn list_items_search_with_source_filter() {
51 let db = common::test_db().await;
52 let feed_rss = common::create_rss_feed(&db, "RSS", "https://example.com/rss").await;
53 let feed_hn = common::create_other_feed(&db, "hn", "HN").await;
54 common::insert_item(&db, &feed_rss, "rss:1", "Rust News", 1).await;
55 common::insert_item(&db, &feed_hn, "hn:1", "Rust on HN", 1).await;
56
57 let fg = FeedGenerator::new(db)
58 .with_filter(FeedFilter::new().search("rust").source("rss"));
59 let result = fg.get_items(0).await.unwrap();
60 assert_eq!(result.items.len(), 1);
61 assert_eq!(result.items[0].id.source, "rss");
62 }
63
64 #[tokio::test]
65 async fn list_items_search_with_unread_filter() {
66 let db = common::test_db().await;
67 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
68 let item1 = common::insert_item(&db, &feed, "rss:1", "Rust Alpha", 1).await;
69 common::insert_item(&db, &feed, "rss:2", "Rust Beta", 2).await;
70
71 db.items().mark_read(item1.id, true).await.unwrap();
72
73 let fg = FeedGenerator::new(db)
74 .with_filter(FeedFilter::new().search("rust").unread_only());
75 let result = fg.get_items(0).await.unwrap();
76 assert_eq!(result.items.len(), 1);
77 assert!(!result.items[0].is_read);
78 }
79
80 // ── Search: Body and Bite Text Matching ──────────────────────────────
81
82 #[tokio::test]
83 async fn search_matches_body_content() {
84 let db = common::test_db().await;
85 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
86
87 // insert_item helper puts "Body of {title}" in the body field
88 common::insert_item(&db, &feed, "rss:1", "Generic Title", 1).await;
89
90 // Search for text that appears in the title (and body via "Body of Generic Title")
91 let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().search("Generic"));
92 let result = fg.get_items(0).await.unwrap();
93 assert_eq!(result.items.len(), 1);
94 }
95
96 #[tokio::test]
97 async fn search_matches_bite_text() {
98 let db = common::test_db().await;
99 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
100
101 // insert_item puts "Item {external_id}" in bite_text
102 common::insert_item(&db, &feed, "rss:searchbite", "Unrelated Title", 1).await;
103
104 // Search for text in bite_text field: "Item rss:searchbite"
105 let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().search("searchbite"));
106 let result = fg.get_items(0).await.unwrap();
107 assert_eq!(result.items.len(), 1);
108 }
109
110 // ── Search with Combined Filters: Starred + Source ───────────────────
111
112 #[tokio::test]
113 async fn search_with_starred_and_source_filter() {
114 let db = common::test_db().await;
115 let feed_rss = common::create_rss_feed(&db, "RSS", "https://example.com/rss").await;
116 let feed_hn = common::create_other_feed(&db, "hn", "HN").await;
117
118 let rss1 = common::insert_item(&db, &feed_rss, "rss:1", "Rust News", 1).await;
119 common::insert_item(&db, &feed_rss, "rss:2", "Rust Guide", 2).await;
120 let hn1 = common::insert_item(&db, &feed_hn, "hn:1", "Rust on HN", 1).await;
121
122 // Star rss:1 and hn:1, leave rss:2 unstarred
123 db.items().mark_starred(rss1.id, true).await.unwrap();
124 db.items().mark_starred(hn1.id, true).await.unwrap();
125
126 // Search "Rust" + starred + source=rss -> only rss:1
127 let fg = FeedGenerator::new(db)
128 .with_filter(
129 FeedFilter::new()
130 .search("rust")
131 .starred_only()
132 .source("rss"),
133 );
134 let result = fg.get_items(0).await.unwrap();
135 assert_eq!(result.items.len(), 1);
136 assert_eq!(result.items[0].id.item_id, "rss:1");
137 assert!(result.items[0].is_starred);
138 assert_eq!(result.items[0].id.source, "rss");
139 }
140
141 // ── Pagination with Filters ──────────────────────────────────────────
142
143 #[tokio::test]
144 async fn pagination_with_unread_filter() {
145 let db = common::test_db().await;
146 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
147
148 // Insert 5 items, mark 2 as read
149 let item0 = common::insert_item(&db, &feed, "rss:p0", "Item 0", 0).await;
150 let item1 = common::insert_item(&db, &feed, "rss:p1", "Item 1", 1).await;
151 common::insert_item(&db, &feed, "rss:p2", "Item 2", 2).await;
152 common::insert_item(&db, &feed, "rss:p3", "Item 3", 3).await;
153 common::insert_item(&db, &feed, "rss:p4", "Item 4", 4).await;
154
155 db.items().mark_read(item0.id, true).await.unwrap();
156 db.items().mark_read(item1.id, true).await.unwrap();
157
158 // 3 unread items, paginate with page_size=2
159 let fg = FeedGenerator::new(db)
160 .with_filter(FeedFilter::new().unread_only())
161 .with_page_size(2);
162
163 let page0 = fg.get_items(0).await.unwrap();
164 assert_eq!(page0.items.len(), 2);
165 assert!(page0.has_more);
166 assert!(page0.items.iter().all(|i| !i.is_read));
167
168 let page1 = fg.get_items(1).await.unwrap();
169 assert_eq!(page1.items.len(), 1);
170 assert!(!page1.has_more);
171 assert!(page1.items.iter().all(|i| !i.is_read));
172 }
173
174 #[tokio::test]
175 async fn pagination_with_starred_filter() {
176 let db = common::test_db().await;
177 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
178
179 // Insert 4 items, star 3 of them
180 common::insert_item(&db, &feed, "rss:s0", "Item 0", 0).await;
181 let item1 = common::insert_item(&db, &feed, "rss:s1", "Item 1", 1).await;
182 let item2 = common::insert_item(&db, &feed, "rss:s2", "Item 2", 2).await;
183 let item3 = common::insert_item(&db, &feed, "rss:s3", "Item 3", 3).await;
184
185 db.items().mark_starred(item1.id, true).await.unwrap();
186 db.items().mark_starred(item2.id, true).await.unwrap();
187 db.items().mark_starred(item3.id, true).await.unwrap();
188
189 let fg = FeedGenerator::new(db)
190 .with_filter(FeedFilter::new().starred_only())
191 .with_page_size(2);
192
193 let page0 = fg.get_items(0).await.unwrap();
194 assert_eq!(page0.items.len(), 2);
195 assert!(page0.has_more);
196 assert!(page0.items.iter().all(|i| i.is_starred));
197
198 let page1 = fg.get_items(1).await.unwrap();
199 assert_eq!(page1.items.len(), 1);
200 assert!(!page1.has_more);
201 }
202
203 #[tokio::test]
204 async fn pagination_with_source_filter() {
205 let db = common::test_db().await;
206 let feed_rss = common::create_rss_feed(&db, "RSS", "https://example.com/rss").await;
207 let feed_hn = common::create_other_feed(&db, "hn", "HN").await;
208
209 // 4 RSS items, 2 HN items
210 for i in 0..4 {
211 common::insert_item(&db, &feed_rss, &format!("rss:p{i}"), &format!("RSS {i}"), i).await;
212 }
213 for i in 0..2 {
214 common::insert_item(&db, &feed_hn, &format!("hn:p{i}"), &format!("HN {i}"), i).await;
215 }
216
217 let fg = FeedGenerator::new(db)
218 .with_filter(FeedFilter::new().source("rss"))
219 .with_page_size(3);
220
221 let page0 = fg.get_items(0).await.unwrap();
222 assert_eq!(page0.items.len(), 3);
223 assert!(page0.has_more);
224 assert!(page0.items.iter().all(|i| i.id.source == "rss"));
225
226 let page1 = fg.get_items(1).await.unwrap();
227 assert_eq!(page1.items.len(), 1);
228 assert!(!page1.has_more);
229 assert!(page1.items.iter().all(|i| i.id.source == "rss"));
230 }
231
232 #[tokio::test]
233 async fn pagination_with_search_filter() {
234 let db = common::test_db().await;
235 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
236
237 // Insert items: 4 with "Rust" in title, 2 without
238 for i in 0..4 {
239 common::insert_item(&db, &feed, &format!("rss:rust{i}"), &format!("Rust Article {i}"), i).await;
240 }
241 common::insert_item(&db, &feed, "rss:go1", "Go Article", 5).await;
242 common::insert_item(&db, &feed, "rss:go2", "Go Article 2", 6).await;
243
244 let fg = FeedGenerator::new(db)
245 .with_filter(FeedFilter::new().search("rust"))
246 .with_page_size(2);
247
248 let page0 = fg.get_items(0).await.unwrap();
249 assert_eq!(page0.items.len(), 2);
250 assert!(page0.has_more);
251
252 let page1 = fg.get_items(1).await.unwrap();
253 assert_eq!(page1.items.len(), 2);
254 assert!(!page1.has_more);
255
256 // Page past end should be empty
257 let page2 = fg.get_items(2).await.unwrap();
258 assert!(page2.items.is_empty());
259 assert!(!page2.has_more);
260 }
261