Skip to main content

max / makenotwork

13.3 KB · 400 lines History Blame Raw
1 use crate::harness::TestHarness;
2
3 // ============================================================================
4 // Auto-hide threshold tests
5 // ============================================================================
6
7 #[tokio::test]
8 async fn auto_hide_threshold_removes_post_at_threshold() {
9 let mut h = TestHarness::new().await;
10 let author_id = h.login_as("hideauthor").await;
11 let comm_id = h.create_community("Test", "test").await;
12 let cat_id = h.create_category(comm_id, "General", "general").await;
13 h.add_membership(author_id, comm_id, "member").await;
14
15 // Set auto_hide_threshold to 2
16 sqlx::query("UPDATE communities SET auto_hide_threshold = 2 WHERE id = $1")
17 .bind(comm_id)
18 .execute(&h.db)
19 .await
20 .unwrap();
21
22 let thread_id = h
23 .create_thread_with_post(cat_id, author_id, "Auto Hide Test", "Content to hide")
24 .await;
25
26 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
27 .await
28 .unwrap();
29 let post_id = posts[0].id;
30
31 // First flag — should NOT auto-hide (threshold=2, only 1 flag)
32 let flagger1 = h.login_as("flagger1").await;
33 h.add_membership(flagger1, comm_id, "member").await;
34 let thread_url = format!("/p/test/general/{}", thread_id);
35 h.client.get(&thread_url).await;
36 let flag_url = format!("/p/test/general/{}/posts/{}/flag", thread_id, post_id);
37 h.client.post_form(&flag_url, "reason=spam").await;
38
39 // Verify NOT removed yet
40 let removed: bool = sqlx::query_scalar(
41 "SELECT removed_at IS NOT NULL FROM posts WHERE id = $1",
42 )
43 .bind(post_id)
44 .fetch_one(&h.db)
45 .await
46 .unwrap();
47 assert!(!removed, "Post should NOT be removed after 1 flag (threshold=2)");
48
49 // Second flag — should auto-hide (2 flags = threshold)
50 let flagger2 = h.login_as("flagger2").await;
51 h.add_membership(flagger2, comm_id, "member").await;
52 h.client.get(&thread_url).await;
53 h.client.post_form(&flag_url, "reason=off_topic").await;
54
55 let removed: bool = sqlx::query_scalar(
56 "SELECT removed_at IS NOT NULL FROM posts WHERE id = $1",
57 )
58 .bind(post_id)
59 .fetch_one(&h.db)
60 .await
61 .unwrap();
62 assert!(removed, "Post should be auto-hidden after 2 flags (threshold=2)");
63 }
64
65 #[tokio::test]
66 async fn auto_hide_disabled_when_threshold_null() {
67 let mut h = TestHarness::new().await;
68 let author_id = h.login_as("nohideauthor").await;
69 let comm_id = h.create_community("Test", "test").await;
70 let cat_id = h.create_category(comm_id, "General", "general").await;
71 h.add_membership(author_id, comm_id, "member").await;
72
73 // auto_hide_threshold is NULL by default — no auto-hide
74
75 let thread_id = h
76 .create_thread_with_post(cat_id, author_id, "No Hide Test", "Content stays")
77 .await;
78
79 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
80 .await
81 .unwrap();
82 let post_id = posts[0].id;
83
84 // Flag 5 times from different users
85 for i in 0..5 {
86 let flagger = h.login_as(&format!("nohideflagger{i}")).await;
87 h.add_membership(flagger, comm_id, "member").await;
88 let thread_url = format!("/p/test/general/{}", thread_id);
89 h.client.get(&thread_url).await;
90 let flag_url = format!("/p/test/general/{}/posts/{}/flag", thread_id, post_id);
91 h.client.post_form(&flag_url, "reason=spam").await;
92 }
93
94 // Verify NOT removed (threshold is NULL = disabled)
95 let removed: bool = sqlx::query_scalar(
96 "SELECT removed_at IS NOT NULL FROM posts WHERE id = $1",
97 )
98 .bind(post_id)
99 .fetch_one(&h.db)
100 .await
101 .unwrap();
102 assert!(!removed, "Post should NOT be removed when auto_hide_threshold is NULL");
103 }
104
105 #[tokio::test]
106 async fn settings_saves_auto_hide_threshold() {
107 let mut h = TestHarness::new().await;
108 let owner_id = h.login_as("thresholdowner").await;
109 let comm_id = h.create_community("Test", "test").await;
110 h.add_membership(owner_id, comm_id, "owner").await;
111 let _cat_id = h.create_category(comm_id, "General", "general").await;
112
113 // GET settings for CSRF
114 h.client.get("/p/test/settings").await;
115
116 // Save with threshold=3
117 let resp = h
118 .client
119 .post_form(
120 "/p/test/settings",
121 "name=Test&description=desc&auto_hide_threshold=3",
122 )
123 .await;
124 assert!(resp.status.is_redirection(), "Expected redirect, got {}", resp.status);
125
126 // Verify in DB
127 let threshold: Option<i32> = sqlx::query_scalar(
128 "SELECT auto_hide_threshold FROM communities WHERE id = $1",
129 )
130 .bind(comm_id)
131 .fetch_one(&h.db)
132 .await
133 .unwrap();
134 assert_eq!(threshold, Some(3));
135
136 // Save with threshold=0 (disabled)
137 h.client.get("/p/test/settings").await;
138 h.client
139 .post_form(
140 "/p/test/settings",
141 "name=Test&description=desc&auto_hide_threshold=0",
142 )
143 .await;
144
145 let threshold: Option<i32> = sqlx::query_scalar(
146 "SELECT auto_hide_threshold FROM communities WHERE id = $1",
147 )
148 .bind(comm_id)
149 .fetch_one(&h.db)
150 .await
151 .unwrap();
152 assert_eq!(threshold, None, "Threshold 0 should be stored as NULL");
153 }
154
155 #[tokio::test]
156 async fn flag_post_happy_path() {
157 let mut h = TestHarness::new().await;
158 let author_id = h.login_as("flagauthor").await;
159 let comm_id = h.create_community("Test", "test").await;
160 let cat_id = h.create_category(comm_id, "General", "general").await;
161 h.add_membership(author_id, comm_id, "member").await;
162
163 let thread_id = h
164 .create_thread_with_post(cat_id, author_id, "Flag Test", "Content")
165 .await;
166
167 let flagger_id = h.login_as("flagger").await;
168 h.add_membership(flagger_id, comm_id, "member").await;
169
170 let thread_url = format!("/p/test/general/{}", thread_id);
171 h.client.get(&thread_url).await;
172
173 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
174 .await
175 .unwrap();
176 let post_id = posts[0].id;
177
178 let flag_url = format!(
179 "/p/test/general/{}/posts/{}/flag",
180 thread_id, post_id
181 );
182 let resp = h.client.post_form(&flag_url, "reason=spam").await;
183 assert!(resp.status.is_redirection(), "Expected redirect, got {}", resp.status);
184
185 // Verify flag in DB
186 let has_flag = mt_db::queries::has_user_flagged_post(&h.db, post_id, flagger_id)
187 .await
188 .unwrap();
189 assert!(has_flag, "Flag should exist in DB");
190 }
191
192 #[tokio::test]
193 async fn duplicate_flag_silently_ignored() {
194 let mut h = TestHarness::new().await;
195 let author_id = h.login_as("dupflagauthor").await;
196 let comm_id = h.create_community("Test", "test").await;
197 let cat_id = h.create_category(comm_id, "General", "general").await;
198 h.add_membership(author_id, comm_id, "member").await;
199
200 let thread_id = h
201 .create_thread_with_post(cat_id, author_id, "Dup Flag", "Content")
202 .await;
203
204 let flagger_id = h.login_as("dupflagger").await;
205 h.add_membership(flagger_id, comm_id, "member").await;
206
207 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
208 .await
209 .unwrap();
210 let post_id = posts[0].id;
211
212 let thread_url = format!("/p/test/general/{}", thread_id);
213 h.client.get(&thread_url).await;
214
215 let flag_url = format!(
216 "/p/test/general/{}/posts/{}/flag",
217 thread_id, post_id
218 );
219
220 // Flag once
221 h.client.post_form(&flag_url, "reason=spam").await;
222
223 // GET for CSRF refresh
224 h.client.get(&thread_url).await;
225
226 // Flag again — should not error
227 let resp = h.client.post_form(&flag_url, "reason=off_topic").await;
228 assert!(resp.status.is_redirection(), "Duplicate flag should redirect, got {}", resp.status);
229 }
230
231 #[tokio::test]
232 async fn flag_requires_login() {
233 let mut h = TestHarness::new().await;
234 let author_id = h.login_as("flagloginauthor").await;
235 let comm_id = h.create_community("Test", "test").await;
236 let cat_id = h.create_category(comm_id, "General", "general").await;
237 h.add_membership(author_id, comm_id, "member").await;
238
239 let thread_id = h
240 .create_thread_with_post(cat_id, author_id, "Login Flag", "Content")
241 .await;
242
243 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
244 .await
245 .unwrap();
246 let post_id = posts[0].id;
247
248 // New harness (no login)
249 let mut h2 = TestHarness::new().await;
250 h2.client.get("/").await;
251
252 let flag_url = format!(
253 "/p/test/general/{}/posts/{}/flag",
254 thread_id, post_id
255 );
256 let resp = h2.client.post_form(&flag_url, "reason=spam").await;
257 assert!(
258 resp.status.is_redirection(),
259 "Expected redirect to login, got {}",
260 resp.status
261 );
262 }
263
264 #[tokio::test]
265 async fn flag_own_post_rejected() {
266 let mut h = TestHarness::new().await;
267 let author_id = h.login_as("selfflagauthor").await;
268 let comm_id = h.create_community("Test", "test").await;
269 let cat_id = h.create_category(comm_id, "General", "general").await;
270 h.add_membership(author_id, comm_id, "member").await;
271
272 let thread_id = h
273 .create_thread_with_post(cat_id, author_id, "Self Flag", "Content")
274 .await;
275
276 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
277 .await
278 .unwrap();
279 let post_id = posts[0].id;
280
281 let thread_url = format!("/p/test/general/{}", thread_id);
282 h.client.get(&thread_url).await;
283
284 let flag_url = format!(
285 "/p/test/general/{}/posts/{}/flag",
286 thread_id, post_id
287 );
288 let resp = h.client.post_form(&flag_url, "reason=spam").await;
289 assert_eq!(resp.status.as_u16(), 403, "Flagging own post should be 403");
290 }
291
292 #[tokio::test]
293 async fn mod_dismiss_flag() {
294 let mut h = TestHarness::new().await;
295 let author_id = h.login_as("dismissauthor").await;
296 let comm_id = h.create_community("Test", "test").await;
297 let cat_id = h.create_category(comm_id, "General", "general").await;
298 h.add_membership(author_id, comm_id, "member").await;
299
300 let thread_id = h
301 .create_thread_with_post(cat_id, author_id, "Dismiss Test", "Content")
302 .await;
303
304 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
305 .await
306 .unwrap();
307 let post_id = posts[0].id;
308
309 // Insert a flag directly
310 let flagger_id = h.login_as("dismissflagger").await;
311 h.add_membership(flagger_id, comm_id, "member").await;
312 mt_db::mutations::insert_flag(&h.db, post_id, flagger_id, "spam", None)
313 .await
314 .unwrap();
315
316 // Login as mod
317 let mod_id = h.login_as("dismissmod").await;
318 h.add_membership(mod_id, comm_id, "moderator").await;
319
320 // Get flag ID
321 let flags = mt_db::queries::list_pending_flags(&h.db, comm_id).await.unwrap();
322 assert_eq!(flags.len(), 1);
323 let flag_id = flags[0].flag_id;
324
325 // GET moderation page for CSRF
326 h.client.get("/p/test/moderation").await;
327
328 let dismiss_url = format!("/p/test/moderation/flags/{}/dismiss", flag_id);
329 let resp = h.client.post_form(&dismiss_url, "").await;
330 assert!(resp.status.is_redirection(), "Expected redirect, got {}", resp.status);
331
332 // Verify flag resolved
333 let flags = mt_db::queries::list_pending_flags(&h.db, comm_id).await.unwrap();
334 assert_eq!(flags.len(), 0, "Flag should be resolved after dismiss");
335 }
336
337 #[tokio::test]
338 async fn mod_remove_via_flag() {
339 let mut h = TestHarness::new().await;
340 let author_id = h.login_as("removeauthor").await;
341 let comm_id = h.create_community("Test", "test").await;
342 let cat_id = h.create_category(comm_id, "General", "general").await;
343 h.add_membership(author_id, comm_id, "member").await;
344
345 let thread_id = h
346 .create_thread_with_post(cat_id, author_id, "Remove Test", "Content")
347 .await;
348
349 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
350 .await
351 .unwrap();
352 let post_id = posts[0].id;
353
354 // Insert flags from two different users
355 let flagger1 = h.login_as("removeflag1").await;
356 h.add_membership(flagger1, comm_id, "member").await;
357 mt_db::mutations::insert_flag(&h.db, post_id, flagger1, "spam", None)
358 .await
359 .unwrap();
360
361 let flagger2_id = uuid::Uuid::new_v4();
362 sqlx::query("INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, $2, $2)")
363 .bind(flagger2_id)
364 .bind("removeflag2")
365 .execute(&h.db)
366 .await
367 .unwrap();
368 mt_db::mutations::insert_flag(&h.db, post_id, flagger2_id, "rule_breaking", Some("bad content"))
369 .await
370 .unwrap();
371
372 // Login as mod
373 let mod_id = h.login_as("removemod").await;
374 h.add_membership(mod_id, comm_id, "moderator").await;
375
376 let flags = mt_db::queries::list_pending_flags(&h.db, comm_id).await.unwrap();
377 assert_eq!(flags.len(), 2, "Should have 2 pending flags");
378 let flag_id = flags[0].flag_id;
379
380 h.client.get("/p/test/moderation").await;
381
382 let remove_url = format!("/p/test/moderation/flags/{}/remove", flag_id);
383 let resp = h.client.post_form(&remove_url, "").await;
384 assert!(resp.status.is_redirection(), "Expected redirect, got {}", resp.status);
385
386 // All flags should be resolved
387 let flags = mt_db::queries::list_pending_flags(&h.db, comm_id).await.unwrap();
388 assert_eq!(flags.len(), 0, "All flags should be resolved after remove");
389
390 // Post should be removed
391 let post: Option<(bool,)> = sqlx::query_as(
392 "SELECT removed_at IS NOT NULL FROM posts WHERE id = $1",
393 )
394 .bind(post_id)
395 .fetch_optional(&h.db)
396 .await
397 .unwrap();
398 assert!(post.unwrap().0, "Post should be mod-removed");
399 }
400