Skip to main content

max / balanced_breakfast

7.5 KB · 191 lines History Blame Raw
1 //! FeedGenerator mark_read/starred, count method, and ordering
2 //! integration tests.
3
4 mod common;
5
6 use bb_db::{BusserId, CreateFeedItem};
7 use bb_feed::{FeedFilter, FeedGenerator};
8 use chrono::{Duration, Utc};
9
10 // ── FeedGenerator mark_read / mark_starred + Unread Count ────────────
11
12 #[tokio::test]
13 async fn generator_mark_read_updates_unread_count() {
14 let db = common::test_db().await;
15 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
16 common::insert_item(&db, &feed, "rss:mr1", "Article 1", 1).await;
17 common::insert_item(&db, &feed, "rss:mr2", "Article 2", 2).await;
18 common::insert_item(&db, &feed, "rss:mr3", "Article 3", 3).await;
19
20 let fg = FeedGenerator::new(db.clone());
21
22 // All 3 unread initially
23 assert_eq!(fg.unread_count().await.unwrap(), 3);
24
25 // Mark one read via generator (uses external_id lookup)
26 fg.mark_read("rss:mr1", true).await.unwrap();
27 assert_eq!(fg.unread_count().await.unwrap(), 2);
28
29 // Mark another read
30 fg.mark_read("rss:mr2", true).await.unwrap();
31 assert_eq!(fg.unread_count().await.unwrap(), 1);
32
33 // Mark one back to unread
34 fg.mark_read("rss:mr1", false).await.unwrap();
35 assert_eq!(fg.unread_count().await.unwrap(), 2);
36 }
37
38 #[tokio::test]
39 async fn generator_mark_starred_does_not_affect_unread_count() {
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:ms1", "Article 1", 1).await;
43 common::insert_item(&db, &feed, "rss:ms2", "Article 2", 2).await;
44
45 let fg = FeedGenerator::new(db.clone());
46 assert_eq!(fg.unread_count().await.unwrap(), 2);
47
48 // Starring should not change unread count
49 fg.mark_starred("rss:ms1", true).await.unwrap();
50 assert_eq!(fg.unread_count().await.unwrap(), 2);
51
52 fg.mark_starred("rss:ms1", false).await.unwrap();
53 assert_eq!(fg.unread_count().await.unwrap(), 2);
54 }
55
56 #[tokio::test]
57 async fn generator_mark_read_nonexistent_preserves_count() {
58 let db = common::test_db().await;
59 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
60 common::insert_item(&db, &feed, "rss:1", "Article", 1).await;
61
62 let fg = FeedGenerator::new(db.clone());
63 // Should not panic or error
64 fg.mark_read("nonexistent:999", true).await.unwrap();
65 // Original item should remain unread
66 assert_eq!(fg.unread_count().await.unwrap(), 1);
67 }
68
69 // ── Ordering ─────────────────────────────────────────────────────────
70
71 #[tokio::test]
72 async fn list_items_order_by_score() {
73 let db = common::test_db().await;
74 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
75
76 // Insert items with scores via upsert (need to use CreateFeedItem directly)
77 for (ext_id, title, score, hours_ago) in [
78 ("rss:lo", "Low Score", Some(10i64), 1i64),
79 ("rss:hi", "High Score", Some(100), 2),
80 ("rss:no", "No Score", None, 3),
81 ] {
82 db.items()
83 .upsert(CreateFeedItem {
84 external_id: ext_id.to_string(),
85 feed_id: feed.id,
86 busser_id: BusserId::new("rss"),
87 bite_author: "author".to_string(),
88 bite_text: format!("Item {ext_id}"),
89 bite_secondary: None,
90 bite_indicator: None,
91 title: Some(title.to_string()),
92 body: None,
93 url: None,
94 media: vec![],
95 published_at: Utc::now() - Duration::hours(hours_ago),
96 source_name: "test".to_string(),
97 score,
98 tags: vec![],
99 actions: vec![],
100 })
101 .await
102 .unwrap();
103 }
104
105 let fg = FeedGenerator::new(db).with_order(bb_feed::OrderBy::Score);
106 let result = fg.get_items(0).await.unwrap();
107 assert_eq!(result.items.len(), 3);
108 assert_eq!(result.items[0].meta.score, Some(100));
109 assert_eq!(result.items[1].meta.score, Some(10));
110 assert_eq!(result.items[2].meta.score, None);
111 }
112
113 #[tokio::test]
114 async fn list_items_order_starred_first() {
115 let db = common::test_db().await;
116 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
117 let item1 = common::insert_item(&db, &feed, "rss:1", "Normal", 1).await;
118 let item2 = common::insert_item(&db, &feed, "rss:2", "Starred", 2).await;
119
120 db.items().mark_starred(item2.id, true).await.unwrap();
121
122 let fg = FeedGenerator::new(db).with_order(bb_feed::OrderBy::StarredFirst);
123 let result = fg.get_items(0).await.unwrap();
124 assert!(result.items[0].is_starred, "starred item should sort first");
125 assert!(!result.items[1].is_starred);
126 // item1 should not have id leakage
127 assert_eq!(result.items[1].id.item_id, item1.external_id);
128 }
129
130 #[tokio::test]
131 async fn list_items_order_unread_first() {
132 let db = common::test_db().await;
133 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
134 let item1 = common::insert_item(&db, &feed, "rss:1", "Read Item", 1).await;
135 common::insert_item(&db, &feed, "rss:2", "Unread Item", 2).await;
136
137 db.items().mark_read(item1.id, true).await.unwrap();
138
139 let fg = FeedGenerator::new(db).with_order(bb_feed::OrderBy::UnreadFirst);
140 let result = fg.get_items(0).await.unwrap();
141 // UnreadFirst groups items by read status — verify they're grouped
142 let first_read = result.items[0].is_read;
143 let last_read = result.items[1].is_read;
144 assert_ne!(first_read, last_read, "items should be grouped by read status");
145 }
146
147 // ── FeedGenerator Count Method ───────────────────────────────────────
148
149 #[tokio::test]
150 async fn generator_count_all() {
151 let db = common::test_db().await;
152 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
153 common::insert_item(&db, &feed, "rss:1", "A", 1).await;
154 common::insert_item(&db, &feed, "rss:2", "B", 2).await;
155 common::insert_item(&db, &feed, "rss:3", "C", 3).await;
156
157 let fg = FeedGenerator::new(db);
158 assert_eq!(fg.count().await.unwrap(), 3);
159 }
160
161 #[tokio::test]
162 async fn generator_count_with_source_filter() {
163 let db = common::test_db().await;
164 let feed_rss = common::create_rss_feed(&db, "RSS", "https://example.com/rss").await;
165 let feed_hn = common::create_other_feed(&db, "hn", "HN").await;
166 common::insert_item(&db, &feed_rss, "rss:1", "A", 1).await;
167 common::insert_item(&db, &feed_hn, "hn:1", "B", 1).await;
168 common::insert_item(&db, &feed_hn, "hn:2", "C", 2).await;
169
170 let fg = FeedGenerator::new(db.clone()).with_filter(FeedFilter::new().source("hn"));
171 assert_eq!(fg.count().await.unwrap(), 2);
172
173 // Source that doesn't exist
174 let gen2 = FeedGenerator::new(db).with_filter(FeedFilter::new().source("nonexistent"));
175 assert_eq!(gen2.count().await.unwrap(), 0);
176 }
177
178 #[tokio::test]
179 async fn generator_count_with_unread_filter() {
180 let db = common::test_db().await;
181 let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await;
182 let item1 = common::insert_item(&db, &feed, "rss:1", "A", 1).await;
183 common::insert_item(&db, &feed, "rss:2", "B", 2).await;
184 common::insert_item(&db, &feed, "rss:3", "C", 3).await;
185
186 db.items().mark_read(item1.id, true).await.unwrap();
187
188 let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().unread_only());
189 assert_eq!(fg.count().await.unwrap(), 2);
190 }
191