Skip to main content

max / balanced_breakfast

5.8 KB · 199 lines History Blame Raw
1 //! Query Feeds repository CRUD and FeedFilter integration tests.
2
3 mod common;
4
5 use bb_db::{CreateQueryFeed, QueryCondition, QueryFeedId};
6 use bb_feed::{FeedFilter, FeedGenerator};
7
8 // ── Query Feeds Repository CRUD ──────────────────────────────────────
9
10 #[tokio::test]
11 async fn query_feed_create_and_list() {
12 let db = common::test_db().await;
13
14 let qf = db
15 .query_feeds()
16 .create(CreateQueryFeed {
17 name: "Rust stuff".to_string(),
18 rules: vec![QueryCondition {
19 field: "title".to_string(),
20 operator: "contains".to_string(),
21 value: "rust".to_string(),
22 }],
23 })
24 .await
25 .unwrap();
26
27 assert_eq!(qf.name, "Rust stuff");
28 assert!(!qf.id.to_string().is_empty());
29
30 let all = db.query_feeds().list_all().await.unwrap();
31 assert_eq!(all.len(), 1);
32 assert_eq!(all[0].name, "Rust stuff");
33
34 let rules = all[0].rules_vec();
35 assert_eq!(rules.len(), 1);
36 assert_eq!(rules[0].field, "title");
37 assert_eq!(rules[0].operator, "contains");
38 assert_eq!(rules[0].value, "rust");
39 }
40
41 #[tokio::test]
42 async fn query_feed_get_by_id() {
43 let db = common::test_db().await;
44
45 let qf = db
46 .query_feeds()
47 .create(CreateQueryFeed {
48 name: "Stars".to_string(),
49 rules: vec![QueryCondition {
50 field: "starred".to_string(),
51 operator: "is".to_string(),
52 value: "true".to_string(),
53 }],
54 })
55 .await
56 .unwrap();
57
58 let fetched = db.query_feeds().get(qf.id).await.unwrap().unwrap();
59 assert_eq!(fetched.name, "Stars");
60 assert_eq!(fetched.id, qf.id);
61 }
62
63 #[tokio::test]
64 async fn query_feed_get_nonexistent_returns_none() {
65 let db = common::test_db().await;
66 let fake_id = QueryFeedId::new();
67 let result = db.query_feeds().get(fake_id).await.unwrap();
68 assert!(result.is_none());
69 }
70
71 #[tokio::test]
72 async fn query_feed_update() {
73 let db = common::test_db().await;
74
75 let qf = db
76 .query_feeds()
77 .create(CreateQueryFeed {
78 name: "Original".to_string(),
79 rules: vec![QueryCondition {
80 field: "title".to_string(),
81 operator: "contains".to_string(),
82 value: "old".to_string(),
83 }],
84 })
85 .await
86 .unwrap();
87
88 let new_rules = vec![QueryCondition {
89 field: "author".to_string(),
90 operator: "equals".to_string(),
91 value: "alice".to_string(),
92 }];
93
94 db.query_feeds()
95 .update(qf.id, "Renamed", &new_rules)
96 .await
97 .unwrap();
98
99 let fetched = db.query_feeds().get(qf.id).await.unwrap().unwrap();
100 assert_eq!(fetched.name, "Renamed");
101 let rules = fetched.rules_vec();
102 assert_eq!(rules.len(), 1);
103 assert_eq!(rules[0].field, "author");
104 }
105
106 #[tokio::test]
107 async fn query_feed_delete() {
108 let db = common::test_db().await;
109
110 let qf = db
111 .query_feeds()
112 .create(CreateQueryFeed {
113 name: "Temporary".to_string(),
114 rules: vec![],
115 })
116 .await
117 .unwrap();
118
119 assert_eq!(db.query_feeds().list_all().await.unwrap().len(), 1);
120
121 db.query_feeds().delete(qf.id).await.unwrap();
122
123 assert!(db.query_feeds().list_all().await.unwrap().is_empty());
124 assert!(db.query_feeds().get(qf.id).await.unwrap().is_none());
125 }
126
127 #[tokio::test]
128 async fn query_feed_rules_json_roundtrip() {
129 let db = common::test_db().await;
130
131 let rules = vec![
132 QueryCondition {
133 field: "title".to_string(),
134 operator: "contains".to_string(),
135 value: "rust".to_string(),
136 },
137 QueryCondition {
138 field: "source".to_string(),
139 operator: "equals".to_string(),
140 value: "rss".to_string(),
141 },
142 QueryCondition {
143 field: "starred".to_string(),
144 operator: "is".to_string(),
145 value: "true".to_string(),
146 },
147 ];
148
149 let qf = db
150 .query_feeds()
151 .create(CreateQueryFeed {
152 name: "Complex".to_string(),
153 rules: rules.clone(),
154 })
155 .await
156 .unwrap();
157
158 let fetched = db.query_feeds().get(qf.id).await.unwrap().unwrap();
159 let roundtripped = fetched.rules_vec();
160 assert_eq!(roundtripped.len(), 3);
161 assert_eq!(roundtripped[0].field, "title");
162 assert_eq!(roundtripped[1].field, "source");
163 assert_eq!(roundtripped[2].field, "starred");
164 }
165
166 // ── Query Feed + FeedFilter Integration ──────────────────────────────
167
168 #[tokio::test]
169 async fn query_feed_filter_items_by_conditions() {
170 let db = common::test_db().await;
171 let feed_rss = common::create_rss_feed(&db, "RSS", "https://example.com/rss").await;
172 let feed_hn = common::create_other_feed(&db, "hn", "HN").await;
173
174 common::insert_item(&db, &feed_rss, "rss:1", "Rust Programming Guide", 1).await;
175 common::insert_item(&db, &feed_rss, "rss:2", "Python Tutorial", 2).await;
176 common::insert_item(&db, &feed_hn, "hn:1", "Rust on HN", 1).await;
177
178 // Create a query feed that filters for "rust" in title + source=rss
179 let conditions = vec![
180 QueryCondition {
181 field: "title".to_string(),
182 operator: "contains".to_string(),
183 value: "rust".to_string(),
184 },
185 QueryCondition {
186 field: "source".to_string(),
187 operator: "equals".to_string(),
188 value: "rss".to_string(),
189 },
190 ];
191
192 let filter = FeedFilter::from_conditions(conditions);
193 let fg = FeedGenerator::new(db).with_filter(filter);
194 let result = fg.get_items(0).await.unwrap();
195
196 assert_eq!(result.items.len(), 1);
197 assert_eq!(result.items[0].id.item_id, "rss:1");
198 }
199