Skip to main content

max / makenotwork

17.9 KB · 531 lines History Blame Raw
1 //! Tests for community-level bans and mutes.
2
3 use axum::http::StatusCode;
4
5 use crate::harness::TestHarness;
6
7 #[sqlx::test]
8 async fn banned_user_cannot_view_community(_pool: sqlx::PgPool) {
9 let mut h = TestHarness::new().await;
10 let owner = h.login_as("owner").await;
11 let community_id = h.create_community("Test", "test").await;
12 h.add_membership(owner, community_id, "owner").await;
13 h.create_category(community_id, "General", "general").await;
14
15 let member = h.login_as("member").await;
16 h.add_membership(member, community_id, "member").await;
17 h.ban_user(community_id, member, owner, "ban").await;
18
19 let resp = h.client.get("/p/test").await;
20 assert_eq!(resp.status, StatusCode::FORBIDDEN);
21 }
22
23 #[sqlx::test]
24 async fn banned_user_cannot_create_thread(_pool: sqlx::PgPool) {
25 let mut h = TestHarness::new().await;
26 let owner = h.login_as("owner").await;
27 let community_id = h.create_community("Test", "test").await;
28 h.add_membership(owner, community_id, "owner").await;
29 h.create_category(community_id, "General", "general").await;
30
31 let member = h.login_as("member").await;
32 h.add_membership(member, community_id, "member").await;
33 h.ban_user(community_id, member, owner, "ban").await;
34
35 // GET to acquire CSRF token
36 h.client.get("/").await;
37 let resp = h.client.post_form(
38 "/p/test/general/new",
39 "title=Hello&body=World",
40 ).await;
41 assert_eq!(resp.status, StatusCode::FORBIDDEN);
42 }
43
44 #[sqlx::test]
45 async fn banned_user_cannot_reply(_pool: sqlx::PgPool) {
46 let mut h = TestHarness::new().await;
47 let owner = h.login_as("owner").await;
48 let community_id = h.create_community("Test", "test").await;
49 h.add_membership(owner, community_id, "owner").await;
50 let cat_id = h.create_category(community_id, "General", "general").await;
51 let thread_id = h.create_thread_with_post(cat_id, owner, "Thread", "OP body").await;
52
53 let member = h.login_as("member").await;
54 h.add_membership(member, community_id, "member").await;
55 h.ban_user(community_id, member, owner, "ban").await;
56
57 h.client.get("/").await;
58 let resp = h.client.post_form(
59 &format!("/p/test/general/{thread_id}/reply"),
60 "body=My+reply",
61 ).await;
62 assert_eq!(resp.status, StatusCode::FORBIDDEN);
63 }
64
65 #[sqlx::test]
66 async fn muted_user_can_view_pages(_pool: sqlx::PgPool) {
67 let mut h = TestHarness::new().await;
68 let owner = h.login_as("owner").await;
69 let community_id = h.create_community("Test", "test").await;
70 h.add_membership(owner, community_id, "owner").await;
71 h.create_category(community_id, "General", "general").await;
72
73 let member = h.login_as("member").await;
74 h.add_membership(member, community_id, "member").await;
75 h.ban_user(community_id, member, owner, "mute").await;
76
77 let resp = h.client.get("/p/test").await;
78 assert_eq!(resp.status, StatusCode::OK);
79 }
80
81 #[sqlx::test]
82 async fn muted_user_cannot_create_thread(_pool: sqlx::PgPool) {
83 let mut h = TestHarness::new().await;
84 let owner = h.login_as("owner").await;
85 let community_id = h.create_community("Test", "test").await;
86 h.add_membership(owner, community_id, "owner").await;
87 h.create_category(community_id, "General", "general").await;
88
89 let member = h.login_as("member").await;
90 h.add_membership(member, community_id, "member").await;
91 h.ban_user(community_id, member, owner, "mute").await;
92
93 h.client.get("/").await;
94 let resp = h.client.post_form(
95 "/p/test/general/new",
96 "title=Hello&body=World",
97 ).await;
98 assert_eq!(resp.status, StatusCode::FORBIDDEN);
99 }
100
101 #[sqlx::test]
102 async fn muted_user_cannot_reply(_pool: sqlx::PgPool) {
103 let mut h = TestHarness::new().await;
104 let owner = h.login_as("owner").await;
105 let community_id = h.create_community("Test", "test").await;
106 h.add_membership(owner, community_id, "owner").await;
107 let cat_id = h.create_category(community_id, "General", "general").await;
108 let thread_id = h.create_thread_with_post(cat_id, owner, "Thread", "OP body").await;
109
110 let member = h.login_as("member").await;
111 h.add_membership(member, community_id, "member").await;
112 h.ban_user(community_id, member, owner, "mute").await;
113
114 h.client.get("/").await;
115 let resp = h.client.post_form(
116 &format!("/p/test/general/{thread_id}/reply"),
117 "body=My+reply",
118 ).await;
119 assert_eq!(resp.status, StatusCode::FORBIDDEN);
120 }
121
122 #[sqlx::test]
123 async fn mod_can_ban_member(_pool: sqlx::PgPool) {
124 let mut h = TestHarness::new().await;
125
126 // Create owner and community
127 let owner = h.login_as("owner").await;
128 let community_id = h.create_community("Test", "test").await;
129 h.add_membership(owner, community_id, "owner").await;
130
131 // Create the target member user in the same DB
132 let member_id = uuid::Uuid::new_v4();
133 sqlx::query(
134 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'member', 'Member')",
135 )
136 .bind(member_id)
137 .execute(&h.db)
138 .await
139 .unwrap();
140 h.add_membership(member_id, community_id, "member").await;
141
142 // Log in as mod and ban member
143 let moduser = h.login_as("moduser").await;
144 h.add_membership(moduser, community_id, "moderator").await;
145
146 h.client.get("/p/test/moderation").await;
147 let resp = h.client.post_form(
148 "/p/test/moderation/ban",
149 "username=member&duration=permanent&reason=spam",
150 ).await;
151 assert!(resp.status.is_redirection() || resp.status == StatusCode::OK);
152 }
153
154 #[sqlx::test]
155 async fn mod_cannot_ban_other_mod(_pool: sqlx::PgPool) {
156 let mut h = TestHarness::new().await;
157
158 let owner = h.login_as("owner").await;
159 let community_id = h.create_community("Test", "test").await;
160 h.add_membership(owner, community_id, "owner").await;
161
162 // Create mod2 user in same DB
163 let mod2_id = uuid::Uuid::new_v4();
164 sqlx::query(
165 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'mod2', 'Mod2')",
166 )
167 .bind(mod2_id)
168 .execute(&h.db)
169 .await
170 .unwrap();
171 h.add_membership(mod2_id, community_id, "moderator").await;
172
173 // Log in as mod and try to ban mod2
174 let moduser = h.login_as("moduser").await;
175 h.add_membership(moduser, community_id, "moderator").await;
176
177 h.client.get("/p/test/moderation").await;
178 let resp = h.client.post_form(
179 "/p/test/moderation/ban",
180 "username=mod2&duration=permanent",
181 ).await;
182 assert_eq!(resp.status, StatusCode::FORBIDDEN);
183 }
184
185 #[sqlx::test]
186 async fn owner_can_ban_mod(_pool: sqlx::PgPool) {
187 let mut h = TestHarness::new().await;
188
189 let owner = h.login_as("owner").await;
190 let community_id = h.create_community("Test", "test").await;
191 h.add_membership(owner, community_id, "owner").await;
192
193 // Create moduser in same DB
194 let mod_id = uuid::Uuid::new_v4();
195 sqlx::query(
196 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'moduser', 'ModUser')",
197 )
198 .bind(mod_id)
199 .execute(&h.db)
200 .await
201 .unwrap();
202 h.add_membership(mod_id, community_id, "moderator").await;
203
204 // Owner bans mod
205 h.client.get("/p/test/moderation").await;
206 let resp = h.client.post_form(
207 "/p/test/moderation/ban",
208 "username=moduser&duration=permanent&reason=abuse",
209 ).await;
210 assert!(resp.status.is_redirection() || resp.status == StatusCode::OK);
211 }
212
213 #[sqlx::test]
214 async fn nobody_can_ban_owner(_pool: sqlx::PgPool) {
215 let mut h = TestHarness::new().await;
216
217 // Create owner user directly so we know the username
218 let owner_id = uuid::Uuid::new_v4();
219 sqlx::query(
220 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'theowner', 'TheOwner')",
221 )
222 .bind(owner_id)
223 .execute(&h.db)
224 .await
225 .unwrap();
226 let community_id = h.create_community("Test", "test").await;
227 h.add_membership(owner_id, community_id, "owner").await;
228
229 // Log in as mod
230 let moduser = h.login_as("moduser").await;
231 h.add_membership(moduser, community_id, "moderator").await;
232
233 h.client.get("/p/test/moderation").await;
234 let resp = h.client.post_form(
235 "/p/test/moderation/ban",
236 "username=theowner&duration=permanent",
237 ).await;
238 assert_eq!(resp.status, StatusCode::FORBIDDEN);
239 }
240
241 #[sqlx::test]
242 async fn unban_restores_access(_pool: sqlx::PgPool) {
243 let mut h = TestHarness::new().await;
244 let owner = h.login_as("owner").await;
245 let community_id = h.create_community("Test", "test").await;
246 h.add_membership(owner, community_id, "owner").await;
247 h.create_category(community_id, "General", "general").await;
248
249 let member = h.login_as("member").await;
250 h.add_membership(member, community_id, "member").await;
251 h.ban_user(community_id, member, owner, "ban").await;
252
253 // Verify banned
254 let resp = h.client.get("/p/test").await;
255 assert_eq!(resp.status, StatusCode::FORBIDDEN);
256
257 // Unban via direct SQL
258 sqlx::query("DELETE FROM community_bans WHERE community_id = $1 AND user_id = $2 AND ban_type = 'ban'")
259 .bind(community_id)
260 .bind(member)
261 .execute(&h.db)
262 .await
263 .unwrap();
264
265 let resp = h.client.get("/p/test").await;
266 assert_eq!(resp.status, StatusCode::OK);
267 }
268
269 #[sqlx::test]
270 async fn unmute_restores_write_access(_pool: sqlx::PgPool) {
271 let mut h = TestHarness::new().await;
272 let owner = h.login_as("owner").await;
273 let community_id = h.create_community("Test", "test").await;
274 h.add_membership(owner, community_id, "owner").await;
275 h.create_category(community_id, "General", "general").await;
276
277 let member = h.login_as("member").await;
278 h.add_membership(member, community_id, "member").await;
279 h.ban_user(community_id, member, owner, "mute").await;
280
281 // Verify muted (can read)
282 let resp = h.client.get("/p/test").await;
283 assert_eq!(resp.status, StatusCode::OK);
284
285 // Verify muted (cannot write)
286 h.client.get("/").await;
287 let resp = h.client.post_form(
288 "/p/test/general/new",
289 "title=Hello&body=World",
290 ).await;
291 assert_eq!(resp.status, StatusCode::FORBIDDEN);
292
293 // Unmute via direct SQL
294 sqlx::query("DELETE FROM community_bans WHERE community_id = $1 AND user_id = $2 AND ban_type = 'mute'")
295 .bind(community_id)
296 .bind(member)
297 .execute(&h.db)
298 .await
299 .unwrap();
300
301 // Verify can write again
302 h.client.get("/p/test/general/new").await;
303 let resp = h.client.post_form(
304 "/p/test/general/new",
305 "title=Hello&body=World",
306 ).await;
307 assert!(resp.status.is_redirection() || resp.status == StatusCode::OK);
308 }
309
310 // ============================================================================
311 // Handler-based moderation tests (mute, unban, unmute via HTTP POST)
312 // ============================================================================
313
314 #[sqlx::test]
315 async fn mod_can_mute_member_via_handler(_pool: sqlx::PgPool) {
316 let mut h = TestHarness::new().await;
317
318 let owner = h.login_as("owner").await;
319 let community_id = h.create_community("Test", "test").await;
320 h.add_membership(owner, community_id, "owner").await;
321 h.create_category(community_id, "General", "general").await;
322
323 // Create target member via direct SQL
324 let member_id = uuid::Uuid::new_v4();
325 sqlx::query(
326 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'target', 'Target')",
327 )
328 .bind(member_id)
329 .execute(&h.db)
330 .await
331 .unwrap();
332 h.add_membership(member_id, community_id, "member").await;
333
334 // Log in as mod, mute target
335 let moduser = h.login_as("moduser").await;
336 h.add_membership(moduser, community_id, "moderator").await;
337
338 h.client.get("/p/test/moderation").await;
339 let resp = h.client.post_form(
340 "/p/test/moderation/mute",
341 "username=target&duration=1d&reason=spam",
342 ).await;
343 assert!(resp.status.is_redirection(), "Expected redirect, got {}", resp.status);
344
345 // Verify mute row was created in DB
346 let is_muted = mt_db::queries::is_user_muted(&h.db, community_id, member_id)
347 .await
348 .unwrap();
349 assert!(is_muted, "Target should be muted after handler call");
350 }
351
352 #[sqlx::test]
353 async fn mod_can_unban_member_via_handler(_pool: sqlx::PgPool) {
354 let mut h = TestHarness::new().await;
355
356 let owner = h.login_as("owner").await;
357 let community_id = h.create_community("Test", "test").await;
358 h.add_membership(owner, community_id, "owner").await;
359
360 // Create and ban target via direct SQL
361 let member_id = uuid::Uuid::new_v4();
362 sqlx::query(
363 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'banned', 'Banned')",
364 )
365 .bind(member_id)
366 .execute(&h.db)
367 .await
368 .unwrap();
369 h.add_membership(member_id, community_id, "member").await;
370 h.ban_user(community_id, member_id, owner, "ban").await;
371
372 // Verify banned via DB
373 let is_banned = mt_db::queries::is_user_banned(&h.db, community_id, member_id)
374 .await
375 .unwrap();
376 assert!(is_banned, "User should be banned before unban");
377
378 // Log in as mod, unban via handler
379 let moduser = h.login_as("moduser").await;
380 h.add_membership(moduser, community_id, "moderator").await;
381 h.client.get("/p/test/moderation").await;
382 let resp = h.client.post_form(
383 "/p/test/moderation/unban",
384 "username=banned",
385 ).await;
386 assert!(resp.status.is_redirection(), "Expected redirect, got {}", resp.status);
387
388 // Verify unbanned via DB
389 let is_banned = mt_db::queries::is_user_banned(&h.db, community_id, member_id)
390 .await
391 .unwrap();
392 assert!(!is_banned, "User should not be banned after unban handler");
393 }
394
395 #[sqlx::test]
396 async fn mod_can_unmute_member_via_handler(_pool: sqlx::PgPool) {
397 let mut h = TestHarness::new().await;
398
399 let owner = h.login_as("owner").await;
400 let community_id = h.create_community("Test", "test").await;
401 h.add_membership(owner, community_id, "owner").await;
402
403 // Create and mute target via direct SQL
404 let member_id = uuid::Uuid::new_v4();
405 sqlx::query(
406 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'muted', 'Muted')",
407 )
408 .bind(member_id)
409 .execute(&h.db)
410 .await
411 .unwrap();
412 h.add_membership(member_id, community_id, "member").await;
413 h.ban_user(community_id, member_id, owner, "mute").await;
414
415 // Verify muted via DB
416 let is_muted = mt_db::queries::is_user_muted(&h.db, community_id, member_id)
417 .await
418 .unwrap();
419 assert!(is_muted, "User should be muted before unmute");
420
421 // Log in as mod, unmute via handler
422 let moduser = h.login_as("moduser").await;
423 h.add_membership(moduser, community_id, "moderator").await;
424 h.client.get("/p/test/moderation").await;
425 let resp = h.client.post_form(
426 "/p/test/moderation/unmute",
427 "username=muted",
428 ).await;
429 assert!(resp.status.is_redirection(), "Expected redirect, got {}", resp.status);
430
431 // Verify unmuted via DB
432 let is_muted = mt_db::queries::is_user_muted(&h.db, community_id, member_id)
433 .await
434 .unwrap();
435 assert!(!is_muted, "User should not be muted after unmute handler");
436 }
437
438 // ============================================================================
439 // Expired ban tests
440 // ============================================================================
441
442 #[sqlx::test]
443 async fn expired_ban_does_not_block_access(_pool: sqlx::PgPool) {
444 let mut h = TestHarness::new().await;
445 let owner = h.login_as("owner").await;
446 let community_id = h.create_community("Test", "test").await;
447 h.add_membership(owner, community_id, "owner").await;
448 h.create_category(community_id, "General", "general").await;
449
450 let member = h.login_as("member").await;
451 h.add_membership(member, community_id, "member").await;
452
453 // Insert an already-expired ban directly
454 sqlx::query(
455 "INSERT INTO community_bans (community_id, user_id, banned_by, ban_type, expires_at)
456 VALUES ($1, $2, $3, 'ban', now() - interval '1 hour')",
457 )
458 .bind(community_id)
459 .bind(member)
460 .bind(owner)
461 .execute(&h.db)
462 .await
463 .unwrap();
464
465 // User should still be able to access the community
466 let resp = h.client.get("/p/test").await;
467 assert_eq!(resp.status, StatusCode::OK);
468
469 // User should be able to create a thread
470 h.client.get("/p/test/general/new").await;
471 let resp = h.client.post_form(
472 "/p/test/general/new",
473 "title=Hello&body=Not+banned",
474 ).await;
475 assert!(resp.status.is_redirection() || resp.status == StatusCode::OK);
476 }
477
478 #[sqlx::test]
479 async fn expired_bans_cleaned_on_moderation_view(_pool: sqlx::PgPool) {
480 let mut h = TestHarness::new().await;
481 let owner = h.login_as("owner").await;
482 let community_id = h.create_community("Test", "test").await;
483 h.add_membership(owner, community_id, "owner").await;
484
485 // Create target user
486 let member_id = uuid::Uuid::new_v4();
487 sqlx::query(
488 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'expired', 'Expired')",
489 )
490 .bind(member_id)
491 .execute(&h.db)
492 .await
493 .unwrap();
494 h.add_membership(member_id, community_id, "member").await;
495
496 // Insert an already-expired ban
497 sqlx::query(
498 "INSERT INTO community_bans (community_id, user_id, banned_by, ban_type, expires_at)
499 VALUES ($1, $2, $3, 'ban', now() - interval '1 hour')",
500 )
501 .bind(community_id)
502 .bind(member_id)
503 .bind(owner)
504 .execute(&h.db)
505 .await
506 .unwrap();
507
508 // Verify the row exists
509 let count: i64 = sqlx::query_scalar(
510 "SELECT COUNT(*) FROM community_bans WHERE community_id = $1",
511 )
512 .bind(community_id)
513 .fetch_one(&h.db)
514 .await
515 .unwrap();
516 assert_eq!(count, 1, "Expired ban row should exist before cleanup");
517
518 // Visit moderation page — triggers cleanup
519 h.client.get("/p/test/moderation").await;
520
521 // Verify row was cleaned up
522 let count: i64 = sqlx::query_scalar(
523 "SELECT COUNT(*) FROM community_bans WHERE community_id = $1",
524 )
525 .bind(community_id)
526 .fetch_one(&h.db)
527 .await
528 .unwrap();
529 assert_eq!(count, 0, "Expired ban row should be deleted after moderation page view");
530 }
531