Skip to main content

max / makenotwork

22.9 KB · 716 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
38 .client
39 .post_form("/p/test/general/new", "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
52 .create_thread_with_post(cat_id, owner, "Thread", "OP body")
53 .await;
54
55 let member = h.login_as("member").await;
56 h.add_membership(member, community_id, "member").await;
57 h.ban_user(community_id, member, owner, "ban").await;
58
59 h.client.get("/").await;
60 let resp = h
61 .client
62 .post_form(
63 &format!("/p/test/general/{thread_id}/reply"),
64 "body=My+reply",
65 )
66 .await;
67 assert_eq!(resp.status, StatusCode::FORBIDDEN);
68 }
69
70 #[sqlx::test]
71 async fn muted_user_can_view_pages(_pool: sqlx::PgPool) {
72 let mut h = TestHarness::new().await;
73 let owner = h.login_as("owner").await;
74 let community_id = h.create_community("Test", "test").await;
75 h.add_membership(owner, community_id, "owner").await;
76 h.create_category(community_id, "General", "general").await;
77
78 let member = h.login_as("member").await;
79 h.add_membership(member, community_id, "member").await;
80 h.ban_user(community_id, member, owner, "mute").await;
81
82 let resp = h.client.get("/p/test").await;
83 assert_eq!(resp.status, StatusCode::OK);
84 }
85
86 #[sqlx::test]
87 async fn muted_user_cannot_create_thread(_pool: sqlx::PgPool) {
88 let mut h = TestHarness::new().await;
89 let owner = h.login_as("owner").await;
90 let community_id = h.create_community("Test", "test").await;
91 h.add_membership(owner, community_id, "owner").await;
92 h.create_category(community_id, "General", "general").await;
93
94 let member = h.login_as("member").await;
95 h.add_membership(member, community_id, "member").await;
96 h.ban_user(community_id, member, owner, "mute").await;
97
98 h.client.get("/").await;
99 let resp = h
100 .client
101 .post_form("/p/test/general/new", "title=Hello&body=World")
102 .await;
103 assert_eq!(resp.status, StatusCode::FORBIDDEN);
104 }
105
106 #[sqlx::test]
107 async fn muted_user_cannot_reply(_pool: sqlx::PgPool) {
108 let mut h = TestHarness::new().await;
109 let owner = h.login_as("owner").await;
110 let community_id = h.create_community("Test", "test").await;
111 h.add_membership(owner, community_id, "owner").await;
112 let cat_id = h.create_category(community_id, "General", "general").await;
113 let thread_id = h
114 .create_thread_with_post(cat_id, owner, "Thread", "OP body")
115 .await;
116
117 let member = h.login_as("member").await;
118 h.add_membership(member, community_id, "member").await;
119 h.ban_user(community_id, member, owner, "mute").await;
120
121 h.client.get("/").await;
122 let resp = h
123 .client
124 .post_form(
125 &format!("/p/test/general/{thread_id}/reply"),
126 "body=My+reply",
127 )
128 .await;
129 assert_eq!(resp.status, StatusCode::FORBIDDEN);
130 }
131
132 #[sqlx::test]
133 async fn mod_can_ban_member(_pool: sqlx::PgPool) {
134 let mut h = TestHarness::new().await;
135
136 // Create owner and community
137 let owner = h.login_as("owner").await;
138 let community_id = h.create_community("Test", "test").await;
139 h.add_membership(owner, community_id, "owner").await;
140
141 // Create the target member user in the same DB
142 let member_id = uuid::Uuid::new_v4();
143 sqlx::query(
144 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'member', 'Member')",
145 )
146 .bind(member_id)
147 .execute(&h.db)
148 .await
149 .unwrap();
150 h.add_membership(member_id, community_id, "member").await;
151
152 // Log in as mod and ban member
153 let moduser = h.login_as("moduser").await;
154 h.add_membership(moduser, community_id, "moderator").await;
155
156 h.client.get("/p/test/moderation").await;
157 let resp = h
158 .client
159 .post_form(
160 "/p/test/moderation/ban",
161 "username=member&duration=permanent&reason=spam",
162 )
163 .await;
164 assert!(resp.status.is_redirection() || resp.status == StatusCode::OK);
165 }
166
167 /// Regression (fuzz-2026-07-06 SERIOUS #2, suspended-community mutation gap):
168 /// a platform-suspended community is frozen to its own owner/mods. The 403
169 /// previously lived only on the moderation *page* GET while every mutation
170 /// (ban/mute/pin/lock/mod-remove) gated on role alone, so a mod could keep
171 /// moderating a suspended community. Every mutation must now 403.
172 #[sqlx::test]
173 async fn suspended_community_blocks_mod_mutations(_pool: sqlx::PgPool) {
174 let mut h = TestHarness::new().await;
175
176 let owner = h.login_as("suspmodowner").await;
177 let community_id = h.create_community("Susp", "susp").await;
178 h.add_membership(owner, community_id, "owner").await;
179 let cat_id = h.create_category(community_id, "General", "general").await;
180 let thread_id = h
181 .create_thread_with_post(cat_id, owner, "Thread", "OP body")
182 .await;
183 let post_id = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
184 .await
185 .unwrap()[0]
186 .id;
187
188 // A victim member the mod would act on.
189 let victim = h.login_as("suspvictim").await;
190 h.add_membership(victim, community_id, "member").await;
191
192 // Log in as the mod and seed a CSRF token from the still-live moderation page.
193 let moduser = h.login_as("suspmoduser").await;
194 h.add_membership(moduser, community_id, "moderator").await;
195 let live = h.client.get("/p/susp/moderation").await;
196 assert!(
197 live.status.is_success(),
198 "moderation page should render pre-suspension"
199 );
200
201 // Platform admin suspends the community.
202 sqlx::query("UPDATE communities SET suspended_at = now() WHERE id = $1")
203 .bind(community_id)
204 .execute(&h.db)
205 .await
206 .unwrap();
207
208 // The moderation page GET now 403s...
209 assert_eq!(
210 h.client.get("/p/susp/moderation").await.status,
211 StatusCode::FORBIDDEN
212 );
213
214 // ...and so must every mutation the mod attempts.
215 let mutations: &[(&str, &str)] = &[
216 (
217 "/p/susp/moderation/ban",
218 "username=suspvictim&duration=permanent&reason=x",
219 ),
220 (
221 "/p/susp/moderation/mute",
222 "username=suspvictim&duration=permanent",
223 ),
224 (&format!("/p/susp/general/{thread_id}/pin"), ""),
225 (&format!("/p/susp/general/{thread_id}/lock"), ""),
226 (
227 &format!("/p/susp/general/{thread_id}/posts/{post_id}/remove"),
228 "",
229 ),
230 ];
231 for (path, body) in mutations {
232 let resp = h.client.post_form(path, body).await;
233 assert_eq!(
234 resp.status,
235 StatusCode::FORBIDDEN,
236 "suspended community must 403 the mutation at {path}, got {}",
237 resp.status
238 );
239 }
240
241 // Nothing leaked through: the victim is neither banned nor muted.
242 assert!(
243 !mt_db::queries::is_user_banned(&h.db, community_id, victim)
244 .await
245 .unwrap()
246 );
247 assert!(
248 !mt_db::queries::is_user_muted(&h.db, community_id, victim)
249 .await
250 .unwrap()
251 );
252 }
253
254 /// Regression (fuzz-2026-07-06 MED, mod can ban the platform admin): the
255 /// platform admin is a config identity with no community role, so the owner/mod
256 /// target-protection guards (which key on role) don't cover them. A mod must not
257 /// be able to ban or mute them out of a community.
258 #[sqlx::test]
259 async fn mod_cannot_ban_or_mute_platform_admin(_pool: sqlx::PgPool) {
260 let admin_id = uuid::Uuid::new_v4();
261 let mut h = TestHarness::new_with_admin(admin_id).await;
262
263 // The platform admin needs a users row so a mod can name them by username.
264 sqlx::query(
265 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'theadmin', 'Admin')",
266 )
267 .bind(admin_id)
268 .execute(&h.db)
269 .await
270 .unwrap();
271
272 let owner = h.login_as("banadminowner").await;
273 let community_id = h.create_community("Test", "test").await;
274 h.add_membership(owner, community_id, "owner").await;
275
276 let moduser = h.login_as("banadminmod").await;
277 h.add_membership(moduser, community_id, "moderator").await;
278
279 h.client.get("/p/test/moderation").await;
280 let ban = h
281 .client
282 .post_form(
283 "/p/test/moderation/ban",
284 "username=theadmin&duration=permanent",
285 )
286 .await;
287 assert_eq!(
288 ban.status,
289 StatusCode::FORBIDDEN,
290 "a mod must not ban the platform admin"
291 );
292
293 let mute = h
294 .client
295 .post_form(
296 "/p/test/moderation/mute",
297 "username=theadmin&duration=permanent",
298 )
299 .await;
300 assert_eq!(
301 mute.status,
302 StatusCode::FORBIDDEN,
303 "a mod must not mute the platform admin"
304 );
305
306 assert!(
307 !mt_db::queries::is_user_banned(&h.db, community_id, admin_id)
308 .await
309 .unwrap()
310 );
311 assert!(
312 !mt_db::queries::is_user_muted(&h.db, community_id, admin_id)
313 .await
314 .unwrap()
315 );
316 }
317
318 #[sqlx::test]
319 async fn mod_cannot_ban_other_mod(_pool: sqlx::PgPool) {
320 let mut h = TestHarness::new().await;
321
322 let owner = h.login_as("owner").await;
323 let community_id = h.create_community("Test", "test").await;
324 h.add_membership(owner, community_id, "owner").await;
325
326 // Create mod2 user in same DB
327 let mod2_id = uuid::Uuid::new_v4();
328 sqlx::query(
329 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'mod2', 'Mod2')",
330 )
331 .bind(mod2_id)
332 .execute(&h.db)
333 .await
334 .unwrap();
335 h.add_membership(mod2_id, community_id, "moderator").await;
336
337 // Log in as mod and try to ban mod2
338 let moduser = h.login_as("moduser").await;
339 h.add_membership(moduser, community_id, "moderator").await;
340
341 h.client.get("/p/test/moderation").await;
342 let resp = h
343 .client
344 .post_form("/p/test/moderation/ban", "username=mod2&duration=permanent")
345 .await;
346 assert_eq!(resp.status, StatusCode::FORBIDDEN);
347 }
348
349 #[sqlx::test]
350 async fn owner_can_ban_mod(_pool: sqlx::PgPool) {
351 let mut h = TestHarness::new().await;
352
353 let owner = h.login_as("owner").await;
354 let community_id = h.create_community("Test", "test").await;
355 h.add_membership(owner, community_id, "owner").await;
356
357 // Create moduser in same DB
358 let mod_id = uuid::Uuid::new_v4();
359 sqlx::query(
360 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'moduser', 'ModUser')",
361 )
362 .bind(mod_id)
363 .execute(&h.db)
364 .await
365 .unwrap();
366 h.add_membership(mod_id, community_id, "moderator").await;
367
368 // Owner bans mod
369 h.client.get("/p/test/moderation").await;
370 let resp = h
371 .client
372 .post_form(
373 "/p/test/moderation/ban",
374 "username=moduser&duration=permanent&reason=abuse",
375 )
376 .await;
377 assert!(resp.status.is_redirection() || resp.status == StatusCode::OK);
378 }
379
380 #[sqlx::test]
381 async fn nobody_can_ban_owner(_pool: sqlx::PgPool) {
382 let mut h = TestHarness::new().await;
383
384 // Create owner user directly so we know the username
385 let owner_id = uuid::Uuid::new_v4();
386 sqlx::query(
387 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'theowner', 'TheOwner')",
388 )
389 .bind(owner_id)
390 .execute(&h.db)
391 .await
392 .unwrap();
393 let community_id = h.create_community("Test", "test").await;
394 h.add_membership(owner_id, community_id, "owner").await;
395
396 let moduser = h.login_as("moduser").await;
397 h.add_membership(moduser, community_id, "moderator").await;
398
399 h.client.get("/p/test/moderation").await;
400 let resp = h
401 .client
402 .post_form(
403 "/p/test/moderation/ban",
404 "username=theowner&duration=permanent",
405 )
406 .await;
407 assert_eq!(resp.status, StatusCode::FORBIDDEN);
408 }
409
410 #[sqlx::test]
411 async fn unban_restores_access(_pool: sqlx::PgPool) {
412 let mut h = TestHarness::new().await;
413 let owner = h.login_as("owner").await;
414 let community_id = h.create_community("Test", "test").await;
415 h.add_membership(owner, community_id, "owner").await;
416 h.create_category(community_id, "General", "general").await;
417
418 let member = h.login_as("member").await;
419 h.add_membership(member, community_id, "member").await;
420 h.ban_user(community_id, member, owner, "ban").await;
421
422 // Verify banned
423 let resp = h.client.get("/p/test").await;
424 assert_eq!(resp.status, StatusCode::FORBIDDEN);
425
426 // Unban via direct SQL
427 sqlx::query(
428 "DELETE FROM community_bans WHERE community_id = $1 AND user_id = $2 AND ban_type = 'ban'",
429 )
430 .bind(community_id)
431 .bind(member)
432 .execute(&h.db)
433 .await
434 .unwrap();
435
436 let resp = h.client.get("/p/test").await;
437 assert_eq!(resp.status, StatusCode::OK);
438 }
439
440 #[sqlx::test]
441 async fn unmute_restores_write_access(_pool: sqlx::PgPool) {
442 let mut h = TestHarness::new().await;
443 let owner = h.login_as("owner").await;
444 let community_id = h.create_community("Test", "test").await;
445 h.add_membership(owner, community_id, "owner").await;
446 h.create_category(community_id, "General", "general").await;
447
448 let member = h.login_as("member").await;
449 h.add_membership(member, community_id, "member").await;
450 h.ban_user(community_id, member, owner, "mute").await;
451
452 // Verify muted (can read)
453 let resp = h.client.get("/p/test").await;
454 assert_eq!(resp.status, StatusCode::OK);
455
456 // Verify muted (cannot write)
457 h.client.get("/").await;
458 let resp = h
459 .client
460 .post_form("/p/test/general/new", "title=Hello&body=World")
461 .await;
462 assert_eq!(resp.status, StatusCode::FORBIDDEN);
463
464 // Unmute via direct SQL
465 sqlx::query(
466 "DELETE FROM community_bans WHERE community_id = $1 AND user_id = $2 AND ban_type = 'mute'",
467 )
468 .bind(community_id)
469 .bind(member)
470 .execute(&h.db)
471 .await
472 .unwrap();
473
474 // Verify can write again
475 h.client.get("/p/test/general/new").await;
476 let resp = h
477 .client
478 .post_form("/p/test/general/new", "title=Hello&body=World")
479 .await;
480 assert!(resp.status.is_redirection() || resp.status == StatusCode::OK);
481 }
482
483 // Handler-based moderation tests (mute, unban, unmute via HTTP POST)
484
485 #[sqlx::test]
486 async fn mod_can_mute_member_via_handler(_pool: sqlx::PgPool) {
487 let mut h = TestHarness::new().await;
488
489 let owner = h.login_as("owner").await;
490 let community_id = h.create_community("Test", "test").await;
491 h.add_membership(owner, community_id, "owner").await;
492 h.create_category(community_id, "General", "general").await;
493
494 // Create target member via direct SQL
495 let member_id = uuid::Uuid::new_v4();
496 sqlx::query(
497 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'target', 'Target')",
498 )
499 .bind(member_id)
500 .execute(&h.db)
501 .await
502 .unwrap();
503 h.add_membership(member_id, community_id, "member").await;
504
505 // Log in as mod, mute target
506 let moduser = h.login_as("moduser").await;
507 h.add_membership(moduser, community_id, "moderator").await;
508
509 h.client.get("/p/test/moderation").await;
510 let resp = h
511 .client
512 .post_form(
513 "/p/test/moderation/mute",
514 "username=target&duration=1d&reason=spam",
515 )
516 .await;
517 assert!(
518 resp.status.is_redirection(),
519 "Expected redirect, got {}",
520 resp.status
521 );
522
523 // Verify mute row was created in DB
524 let is_muted = mt_db::queries::is_user_muted(&h.db, community_id, member_id)
525 .await
526 .unwrap();
527 assert!(is_muted, "Target should be muted after handler call");
528 }
529
530 #[sqlx::test]
531 async fn mod_can_unban_member_via_handler(_pool: sqlx::PgPool) {
532 let mut h = TestHarness::new().await;
533
534 let owner = h.login_as("owner").await;
535 let community_id = h.create_community("Test", "test").await;
536 h.add_membership(owner, community_id, "owner").await;
537
538 // Create and ban target via direct SQL
539 let member_id = uuid::Uuid::new_v4();
540 sqlx::query(
541 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'banned', 'Banned')",
542 )
543 .bind(member_id)
544 .execute(&h.db)
545 .await
546 .unwrap();
547 h.add_membership(member_id, community_id, "member").await;
548 h.ban_user(community_id, member_id, owner, "ban").await;
549
550 // Verify banned via DB
551 let is_banned = mt_db::queries::is_user_banned(&h.db, community_id, member_id)
552 .await
553 .unwrap();
554 assert!(is_banned, "User should be banned before unban");
555
556 // Log in as mod, unban via handler
557 let moduser = h.login_as("moduser").await;
558 h.add_membership(moduser, community_id, "moderator").await;
559 h.client.get("/p/test/moderation").await;
560 let resp = h
561 .client
562 .post_form("/p/test/moderation/unban", "username=banned")
563 .await;
564 assert!(
565 resp.status.is_redirection(),
566 "Expected redirect, got {}",
567 resp.status
568 );
569
570 // Verify unbanned via DB
571 let is_banned = mt_db::queries::is_user_banned(&h.db, community_id, member_id)
572 .await
573 .unwrap();
574 assert!(!is_banned, "User should not be banned after unban handler");
575 }
576
577 #[sqlx::test]
578 async fn mod_can_unmute_member_via_handler(_pool: sqlx::PgPool) {
579 let mut h = TestHarness::new().await;
580
581 let owner = h.login_as("owner").await;
582 let community_id = h.create_community("Test", "test").await;
583 h.add_membership(owner, community_id, "owner").await;
584
585 // Create and mute target via direct SQL
586 let member_id = uuid::Uuid::new_v4();
587 sqlx::query(
588 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'muted', 'Muted')",
589 )
590 .bind(member_id)
591 .execute(&h.db)
592 .await
593 .unwrap();
594 h.add_membership(member_id, community_id, "member").await;
595 h.ban_user(community_id, member_id, owner, "mute").await;
596
597 // Verify muted via DB
598 let is_muted = mt_db::queries::is_user_muted(&h.db, community_id, member_id)
599 .await
600 .unwrap();
601 assert!(is_muted, "User should be muted before unmute");
602
603 // Log in as mod, unmute via handler
604 let moduser = h.login_as("moduser").await;
605 h.add_membership(moduser, community_id, "moderator").await;
606 h.client.get("/p/test/moderation").await;
607 let resp = h
608 .client
609 .post_form("/p/test/moderation/unmute", "username=muted")
610 .await;
611 assert!(
612 resp.status.is_redirection(),
613 "Expected redirect, got {}",
614 resp.status
615 );
616
617 // Verify unmuted via DB
618 let is_muted = mt_db::queries::is_user_muted(&h.db, community_id, member_id)
619 .await
620 .unwrap();
621 assert!(!is_muted, "User should not be muted after unmute handler");
622 }
623
624 // Expired ban tests
625
626 #[sqlx::test]
627 async fn expired_ban_does_not_block_access(_pool: sqlx::PgPool) {
628 let mut h = TestHarness::new().await;
629 let owner = h.login_as("owner").await;
630 let community_id = h.create_community("Test", "test").await;
631 h.add_membership(owner, community_id, "owner").await;
632 h.create_category(community_id, "General", "general").await;
633
634 let member = h.login_as("member").await;
635 h.add_membership(member, community_id, "member").await;
636
637 // Insert an already-expired ban directly
638 sqlx::query(
639 "INSERT INTO community_bans (community_id, user_id, banned_by, ban_type, expires_at)
640 VALUES ($1, $2, $3, 'ban', now() - interval '1 hour')",
641 )
642 .bind(community_id)
643 .bind(member)
644 .bind(owner)
645 .execute(&h.db)
646 .await
647 .unwrap();
648
649 // User should still be able to access the community
650 let resp = h.client.get("/p/test").await;
651 assert_eq!(resp.status, StatusCode::OK);
652
653 // User should be able to create a thread
654 h.client.get("/p/test/general/new").await;
655 let resp = h
656 .client
657 .post_form("/p/test/general/new", "title=Hello&body=Not+banned")
658 .await;
659 assert!(resp.status.is_redirection() || resp.status == StatusCode::OK);
660 }
661
662 #[sqlx::test]
663 async fn expired_bans_cleaned_on_moderation_view(_pool: sqlx::PgPool) {
664 let mut h = TestHarness::new().await;
665 let owner = h.login_as("owner").await;
666 let community_id = h.create_community("Test", "test").await;
667 h.add_membership(owner, community_id, "owner").await;
668
669 // Create target user
670 let member_id = uuid::Uuid::new_v4();
671 sqlx::query(
672 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'expired', 'Expired')",
673 )
674 .bind(member_id)
675 .execute(&h.db)
676 .await
677 .unwrap();
678 h.add_membership(member_id, community_id, "member").await;
679
680 // Insert an already-expired ban
681 sqlx::query(
682 "INSERT INTO community_bans (community_id, user_id, banned_by, ban_type, expires_at)
683 VALUES ($1, $2, $3, 'ban', now() - interval '1 hour')",
684 )
685 .bind(community_id)
686 .bind(member_id)
687 .bind(owner)
688 .execute(&h.db)
689 .await
690 .unwrap();
691
692 // Verify the row exists
693 let count: i64 =
694 sqlx::query_scalar("SELECT COUNT(*) FROM community_bans WHERE community_id = $1")
695 .bind(community_id)
696 .fetch_one(&h.db)
697 .await
698 .unwrap();
699 assert_eq!(count, 1, "Expired ban row should exist before cleanup");
700
701 // Visit moderation page, triggers cleanup
702 h.client.get("/p/test/moderation").await;
703
704 // Verify row was cleaned up
705 let count: i64 =
706 sqlx::query_scalar("SELECT COUNT(*) FROM community_bans WHERE community_id = $1")
707 .bind(community_id)
708 .fetch_one(&h.db)
709 .await
710 .unwrap();
711 assert_eq!(
712 count, 0,
713 "Expired ban row should be deleted after moderation page view"
714 );
715 }
716