Skip to main content

max / makenotwork

15.0 KB · 475 lines History Blame Raw
1 use crate::harness::TestHarness;
2
3 #[tokio::test]
4 async fn pin_toggle() {
5 let mut h = TestHarness::new().await;
6 let mod_id = h.login_as("pintoggler").await;
7 let comm_id = h.create_community("Test", "test").await;
8 let cat_id = h.create_category(comm_id, "General", "general").await;
9 h.add_membership(mod_id, comm_id, "moderator").await;
10
11 let thread_id = h
12 .create_thread_with_post(cat_id, mod_id, "Toggle Pin", "Body")
13 .await;
14
15 let thread_url = format!("/p/test/general/{}", thread_id);
16
17 // Pin the thread
18 h.client.get(&thread_url).await;
19 let pin_url = format!("/p/test/general/{}/pin", thread_id);
20 h.client.post_form(&pin_url, "").await;
21
22 // Verify pinned badge
23 let resp = h.client.get(&thread_url).await;
24 assert!(resp.text.contains("[pinned]"), "Expected [pinned] badge");
25
26 // Unpin
27 h.client.post_form(&pin_url, "").await;
28 let resp = h.client.get(&thread_url).await;
29 assert!(
30 !resp.text.contains("[pinned]"),
31 "Expected [pinned] badge to be gone after unpin"
32 );
33 }
34
35 #[tokio::test]
36 async fn lock_prevents_replies() {
37 let mut h = TestHarness::new().await;
38 let mod_id = h.login_as("locker").await;
39 let comm_id = h.create_community("Test", "test").await;
40 let cat_id = h.create_category(comm_id, "General", "general").await;
41 h.add_membership(mod_id, comm_id, "moderator").await;
42
43 let thread_id = h
44 .create_thread_with_post(cat_id, mod_id, "Lock Test", "Body")
45 .await;
46
47 let thread_url = format!("/p/test/general/{}", thread_id);
48
49 // Lock
50 h.client.get(&thread_url).await;
51 let lock_url = format!("/p/test/general/{}/lock", thread_id);
52 h.client.post_form(&lock_url, "").await;
53
54 // Try to reply
55 h.client.get(&thread_url).await;
56 let reply_url = format!("/p/test/general/{}/reply", thread_id);
57 let resp = h.client.post_form(&reply_url, "body=Nope").await;
58
59 assert_eq!(resp.status.as_u16(), 403);
60 }
61
62 #[tokio::test]
63 async fn pinned_threads_first_in_listing() {
64 let mut h = TestHarness::new().await;
65 let mod_id = h.login_as("orderer").await;
66 let comm_id = h.create_community("Test", "test").await;
67 let cat_id = h.create_category(comm_id, "General", "general").await;
68 h.add_membership(mod_id, comm_id, "moderator").await;
69
70 // Create two threads
71 let _thread1 = h
72 .create_thread_with_post(cat_id, mod_id, "First Thread", "First body")
73 .await;
74
75 // Small delay so last_activity differs
76 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
77
78 let thread2 = h
79 .create_thread_with_post(cat_id, mod_id, "Second Thread", "Second body")
80 .await;
81
82 // Pin the second thread
83 mt_db::mutations::set_thread_pinned(&h.db, thread2, true)
84 .await
85 .unwrap();
86
87 let resp = h.client.get("/p/test/general").await;
88
89 // The pinned "Second Thread" should appear before "First Thread"
90 let pos_second = resp.text.find("Second Thread").expect("Second Thread not found");
91 let pos_first = resp.text.find("First Thread").expect("First Thread not found");
92 assert!(
93 pos_second < pos_first,
94 "Pinned thread should appear before unpinned thread"
95 );
96 }
97
98 #[tokio::test]
99 async fn update_community_settings() {
100 let mut h = TestHarness::new().await;
101 let owner_id = h.login_as("settingswriter").await;
102 let comm_id = h.create_community("Old Name", "test").await;
103 h.add_membership(owner_id, comm_id, "owner").await;
104 let _cat_id = h.create_category(comm_id, "General", "general").await;
105
106 // GET settings to get CSRF
107 h.client.get("/p/test/settings").await;
108
109 let resp = h
110 .client
111 .post_form(
112 "/p/test/settings",
113 "name=New+Name&description=Updated+desc",
114 )
115 .await;
116
117 assert!(
118 resp.status.is_redirection(),
119 "Expected redirect, got {}",
120 resp.status
121 );
122
123 // Verify the name was saved
124 let community = mt_db::queries::get_community_by_slug(&h.db, "test")
125 .await
126 .unwrap()
127 .unwrap();
128 assert_eq!(community.name, "New Name");
129 }
130
131 #[tokio::test]
132 async fn create_category_via_settings() {
133 let mut h = TestHarness::new().await;
134 let owner_id = h.login_as("catcreator").await;
135 let comm_id = h.create_community("Test", "test").await;
136 h.add_membership(owner_id, comm_id, "owner").await;
137 let _cat_id = h.create_category(comm_id, "General", "general").await;
138
139 // GET settings for CSRF
140 h.client.get("/p/test/settings").await;
141
142 let resp = h
143 .client
144 .post_form(
145 "/p/test/settings/categories/new",
146 "name=New+Category&slug=new-cat&description=A+new+category",
147 )
148 .await;
149
150 assert!(
151 resp.status.is_redirection(),
152 "Expected redirect, got {}",
153 resp.status
154 );
155
156 // Verify category appears in settings
157 let resp = h.client.get("/p/test/settings").await;
158 assert!(
159 resp.text.contains("New Category"),
160 "New category should appear in settings page"
161 );
162 }
163
164 #[tokio::test]
165 async fn edit_category_via_settings() {
166 let mut h = TestHarness::new().await;
167 let owner_id = h.login_as("cateditor").await;
168 let comm_id = h.create_community("Test", "test").await;
169 h.add_membership(owner_id, comm_id, "owner").await;
170 let cat_id = h.create_category(comm_id, "Old Name", "general").await;
171
172 // GET edit form for CSRF
173 h.client
174 .get(&format!("/p/test/settings/categories/{cat_id}/edit"))
175 .await;
176
177 let resp = h
178 .client
179 .post_form(
180 &format!("/p/test/settings/categories/{cat_id}/edit"),
181 "name=New+Name&description=Updated",
182 )
183 .await;
184
185 assert!(
186 resp.status.is_redirection(),
187 "Expected redirect, got {}",
188 resp.status
189 );
190
191 // Verify the name was saved
192 let cat = mt_db::queries::get_category_by_id(&h.db, cat_id)
193 .await
194 .unwrap()
195 .unwrap();
196 assert_eq!(cat.name, "New Name");
197 }
198
199 #[tokio::test]
200 async fn reorder_categories_via_settings() {
201 let mut h = TestHarness::new().await;
202 let owner_id = h.login_as("catmover").await;
203 let comm_id = h.create_community("Test", "test").await;
204 h.add_membership(owner_id, comm_id, "owner").await;
205
206 // Create two categories with explicit sort order
207 let cat_a = h.create_category(comm_id, "Alpha", "alpha").await;
208 // Update sort_order so cat_a=0 (default from create_category)
209 sqlx::query("UPDATE categories SET sort_order = 1 WHERE id = $1")
210 .bind(cat_a)
211 .execute(&h.db)
212 .await
213 .unwrap();
214 let cat_b = h.create_category(comm_id, "Beta", "beta").await;
215 sqlx::query("UPDATE categories SET sort_order = 2 WHERE id = $1")
216 .bind(cat_b)
217 .execute(&h.db)
218 .await
219 .unwrap();
220
221 // Verify initial order: Alpha(1), Beta(2)
222 let cats = mt_db::queries::list_categories_for_settings(&h.db, comm_id)
223 .await
224 .unwrap();
225 assert_eq!(cats[0].name, "Alpha");
226 assert_eq!(cats[1].name, "Beta");
227
228 // Move Alpha down (swap with Beta)
229 h.client.get("/p/test/settings").await;
230 let resp = h
231 .client
232 .post_form(
233 &format!("/p/test/settings/categories/{cat_a}/move"),
234 "direction=down",
235 )
236 .await;
237 assert!(
238 resp.status.is_redirection(),
239 "Expected redirect, got {}",
240 resp.status
241 );
242
243 // Verify new order: Beta(1), Alpha(2)
244 let cats = mt_db::queries::list_categories_for_settings(&h.db, comm_id)
245 .await
246 .unwrap();
247 assert_eq!(cats[0].name, "Beta", "Beta should be first after move");
248 assert_eq!(cats[1].name, "Alpha", "Alpha should be second after move");
249 }
250
251 // ============================================================================
252 // Post removal via direct handler (not via flag)
253 // ============================================================================
254
255 #[tokio::test]
256 async fn mod_remove_post_directly() {
257 let mut h = TestHarness::new().await;
258 let author_id = h.login_as("postauthor").await;
259 let comm_id = h.create_community("Test", "test").await;
260 let cat_id = h.create_category(comm_id, "General", "general").await;
261 h.add_membership(author_id, comm_id, "member").await;
262
263 let thread_id = h
264 .create_thread_with_post(cat_id, author_id, "Remove Direct", "Content to remove")
265 .await;
266
267 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
268 .await
269 .unwrap();
270 let post_id = posts[0].id;
271
272 // Log in as moderator
273 let mod_id = h.login_as("directmod").await;
274 h.add_membership(mod_id, comm_id, "moderator").await;
275
276 let thread_url = format!("/p/test/general/{}", thread_id);
277 h.client.get(&thread_url).await;
278
279 let remove_url = format!("/p/test/general/{}/posts/{}/remove", thread_id, post_id);
280 let resp = h.client.post_form(&remove_url, "").await;
281 assert!(
282 resp.status.is_redirection(),
283 "Expected redirect, got {}",
284 resp.status
285 );
286
287 // Verify post is removed in DB
288 let removed: bool = sqlx::query_scalar(
289 "SELECT removed_at IS NOT NULL FROM posts WHERE id = $1",
290 )
291 .bind(post_id)
292 .fetch_one(&h.db)
293 .await
294 .unwrap();
295 assert!(removed, "Post should be marked as removed");
296
297 // Verify removed_by is the mod
298 let removed_by: uuid::Uuid = sqlx::query_scalar(
299 "SELECT removed_by FROM posts WHERE id = $1",
300 )
301 .bind(post_id)
302 .fetch_one(&h.db)
303 .await
304 .unwrap();
305 assert_eq!(removed_by, mod_id, "removed_by should be the moderator");
306 }
307
308 #[tokio::test]
309 async fn member_cannot_remove_post() {
310 let mut h = TestHarness::new().await;
311 let author_id = h.login_as("noremoveauthor").await;
312 let comm_id = h.create_community("Test", "test").await;
313 let cat_id = h.create_category(comm_id, "General", "general").await;
314 h.add_membership(author_id, comm_id, "member").await;
315
316 let thread_id = h
317 .create_thread_with_post(cat_id, author_id, "No Remove", "Content")
318 .await;
319
320 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
321 .await
322 .unwrap();
323 let post_id = posts[0].id;
324
325 // Log in as a different member (not mod)
326 let member_id = h.login_as("regularmember").await;
327 h.add_membership(member_id, comm_id, "member").await;
328
329 let thread_url = format!("/p/test/general/{}", thread_id);
330 h.client.get(&thread_url).await;
331
332 let remove_url = format!("/p/test/general/{}/posts/{}/remove", thread_id, post_id);
333 let resp = h.client.post_form(&remove_url, "").await;
334 assert_eq!(
335 resp.status.as_u16(),
336 403,
337 "Non-mod should get 403 when trying to remove a post"
338 );
339 }
340
341 #[tokio::test]
342 async fn removed_post_shows_removed_in_thread() {
343 let mut h = TestHarness::new().await;
344 let author_id = h.login_as("removedviewauthor").await;
345 let comm_id = h.create_community("Test", "test").await;
346 let cat_id = h.create_category(comm_id, "General", "general").await;
347 h.add_membership(author_id, comm_id, "member").await;
348
349 let thread_id = h
350 .create_thread_with_post(cat_id, author_id, "View Removed", "Visible content")
351 .await;
352
353 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
354 .await
355 .unwrap();
356 let post_id = posts[0].id;
357
358 // Mod removes the post
359 mt_db::mutations::mod_remove_post(&h.db, post_id, author_id)
360 .await
361 .unwrap();
362
363 // View the thread — should contain the "removed" CSS class
364 let thread_url = format!("/p/test/general/{}", thread_id);
365 let resp = h.client.get(&thread_url).await;
366 assert!(
367 resp.text.contains("post-removed"),
368 "Removed post should have 'post-removed' class in thread view"
369 );
370 }
371
372 // ============================================================================
373 // Mod log page
374 // ============================================================================
375
376 #[tokio::test]
377 async fn mod_log_shows_actions() {
378 let mut h = TestHarness::new().await;
379 let mod_id = h.login_as("logmod").await;
380 let comm_id = h.create_community("Test", "test").await;
381 let cat_id = h.create_category(comm_id, "General", "general").await;
382 h.add_membership(mod_id, comm_id, "moderator").await;
383
384 // Perform a mod action (pin a thread) to generate a log entry
385 let thread_id = h
386 .create_thread_with_post(cat_id, mod_id, "Log Test Thread", "Body")
387 .await;
388
389 let thread_url = format!("/p/test/general/{}", thread_id);
390 h.client.get(&thread_url).await;
391 let pin_url = format!("/p/test/general/{}/pin", thread_id);
392 h.client.post_form(&pin_url, "").await;
393
394 // View mod log
395 let resp = h.client.get("/p/test/moderation/log").await;
396 assert_eq!(resp.status.as_u16(), 200);
397 assert!(
398 resp.text.contains("pin_thread"),
399 "Mod log should contain the pin_thread action"
400 );
401 assert!(
402 resp.text.contains("logmod"),
403 "Mod log should show the acting moderator's username"
404 );
405 }
406
407 #[tokio::test]
408 async fn mod_log_forbidden_for_members() {
409 let mut h = TestHarness::new().await;
410 let member_id = h.login_as("logmember").await;
411 let comm_id = h.create_community("Test", "test").await;
412 h.add_membership(member_id, comm_id, "member").await;
413
414 let resp = h.client.get("/p/test/moderation/log").await;
415 assert_eq!(
416 resp.status.as_u16(),
417 403,
418 "Non-mod should get 403 for mod log"
419 );
420 }
421
422 #[tokio::test]
423 async fn moderation_page_shows_bans_and_flags() {
424 let mut h = TestHarness::new().await;
425 let owner_id = h.login_as("modpageowner").await;
426 let comm_id = h.create_community("Test", "test").await;
427 let cat_id = h.create_category(comm_id, "General", "general").await;
428 h.add_membership(owner_id, comm_id, "owner").await;
429
430 // Create a member and ban them
431 let member_id = uuid::Uuid::new_v4();
432 sqlx::query(
433 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'banneduser', 'Banned User')",
434 )
435 .bind(member_id)
436 .execute(&h.db)
437 .await
438 .unwrap();
439 h.add_membership(member_id, comm_id, "member").await;
440 h.ban_user(comm_id, member_id, owner_id, "ban").await;
441
442 // Create a post and flag it
443 let thread_id = h
444 .create_thread_with_post(cat_id, owner_id, "Flagged Thread", "Some content")
445 .await;
446 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
447 .await
448 .unwrap();
449 let post_id = posts[0].id;
450
451 let flagger_id = uuid::Uuid::new_v4();
452 sqlx::query(
453 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'flaggeruser', 'Flagger')",
454 )
455 .bind(flagger_id)
456 .execute(&h.db)
457 .await
458 .unwrap();
459 mt_db::mutations::insert_flag(&h.db, post_id, flagger_id, "spam", None)
460 .await
461 .unwrap();
462
463 // View moderation page
464 let resp = h.client.get("/p/test/moderation").await;
465 assert_eq!(resp.status.as_u16(), 200);
466 assert!(
467 resp.text.contains("banneduser"),
468 "Moderation page should show banned user"
469 );
470 assert!(
471 resp.text.contains("spam"),
472 "Moderation page should show pending flag reason"
473 );
474 }
475