| 1 |
1 |
|
//! Tests for community-level bans and mutes.
|
| 2 |
2 |
|
|
|
3 |
+ |
use axum::http::StatusCode;
|
|
4 |
+ |
|
| 3 |
5 |
|
use crate::harness::TestHarness;
|
| 4 |
6 |
|
|
| 5 |
7 |
|
#[sqlx::test]
|
| 15 |
17 |
|
h.ban_user(community_id, member, owner, "ban").await;
|
| 16 |
18 |
|
|
| 17 |
19 |
|
let resp = h.client.get("/p/test").await;
|
| 18 |
|
- |
assert_eq!(resp.status, axum::http::StatusCode::FORBIDDEN);
|
|
20 |
+ |
assert_eq!(resp.status, StatusCode::FORBIDDEN);
|
| 19 |
21 |
|
}
|
| 20 |
22 |
|
|
| 21 |
23 |
|
#[sqlx::test]
|
| 36 |
38 |
|
"/p/test/general/new",
|
| 37 |
39 |
|
"title=Hello&body=World",
|
| 38 |
40 |
|
).await;
|
| 39 |
|
- |
assert_eq!(resp.status, axum::http::StatusCode::FORBIDDEN);
|
|
41 |
+ |
assert_eq!(resp.status, StatusCode::FORBIDDEN);
|
| 40 |
42 |
|
}
|
| 41 |
43 |
|
|
| 42 |
44 |
|
#[sqlx::test]
|
| 57 |
59 |
|
&format!("/p/test/general/{thread_id}/reply"),
|
| 58 |
60 |
|
"body=My+reply",
|
| 59 |
61 |
|
).await;
|
| 60 |
|
- |
assert_eq!(resp.status, axum::http::StatusCode::FORBIDDEN);
|
|
62 |
+ |
assert_eq!(resp.status, StatusCode::FORBIDDEN);
|
| 61 |
63 |
|
}
|
| 62 |
64 |
|
|
| 63 |
65 |
|
#[sqlx::test]
|
| 73 |
75 |
|
h.ban_user(community_id, member, owner, "mute").await;
|
| 74 |
76 |
|
|
| 75 |
77 |
|
let resp = h.client.get("/p/test").await;
|
| 76 |
|
- |
assert_eq!(resp.status, axum::http::StatusCode::OK);
|
|
78 |
+ |
assert_eq!(resp.status, StatusCode::OK);
|
| 77 |
79 |
|
}
|
| 78 |
80 |
|
|
| 79 |
81 |
|
#[sqlx::test]
|
| 93 |
95 |
|
"/p/test/general/new",
|
| 94 |
96 |
|
"title=Hello&body=World",
|
| 95 |
97 |
|
).await;
|
| 96 |
|
- |
assert_eq!(resp.status, axum::http::StatusCode::FORBIDDEN);
|
|
98 |
+ |
assert_eq!(resp.status, StatusCode::FORBIDDEN);
|
| 97 |
99 |
|
}
|
| 98 |
100 |
|
|
| 99 |
101 |
|
#[sqlx::test]
|
| 114 |
116 |
|
&format!("/p/test/general/{thread_id}/reply"),
|
| 115 |
117 |
|
"body=My+reply",
|
| 116 |
118 |
|
).await;
|
| 117 |
|
- |
assert_eq!(resp.status, axum::http::StatusCode::FORBIDDEN);
|
|
119 |
+ |
assert_eq!(resp.status, StatusCode::FORBIDDEN);
|
| 118 |
120 |
|
}
|
| 119 |
121 |
|
|
| 120 |
122 |
|
#[sqlx::test]
|
| 146 |
148 |
|
"/p/test/moderation/ban",
|
| 147 |
149 |
|
"username=member&duration=permanent&reason=spam",
|
| 148 |
150 |
|
).await;
|
| 149 |
|
- |
assert!(resp.status.is_redirection() || resp.status == axum::http::StatusCode::OK);
|
|
151 |
+ |
assert!(resp.status.is_redirection() || resp.status == StatusCode::OK);
|
| 150 |
152 |
|
}
|
| 151 |
153 |
|
|
| 152 |
154 |
|
#[sqlx::test]
|
| 177 |
179 |
|
"/p/test/moderation/ban",
|
| 178 |
180 |
|
"username=mod2&duration=permanent",
|
| 179 |
181 |
|
).await;
|
| 180 |
|
- |
assert_eq!(resp.status, axum::http::StatusCode::FORBIDDEN);
|
|
182 |
+ |
assert_eq!(resp.status, StatusCode::FORBIDDEN);
|
| 181 |
183 |
|
}
|
| 182 |
184 |
|
|
| 183 |
185 |
|
#[sqlx::test]
|
| 205 |
207 |
|
"/p/test/moderation/ban",
|
| 206 |
208 |
|
"username=moduser&duration=permanent&reason=abuse",
|
| 207 |
209 |
|
).await;
|
| 208 |
|
- |
assert!(resp.status.is_redirection() || resp.status == axum::http::StatusCode::OK);
|
|
210 |
+ |
assert!(resp.status.is_redirection() || resp.status == StatusCode::OK);
|
| 209 |
211 |
|
}
|
| 210 |
212 |
|
|
| 211 |
213 |
|
#[sqlx::test]
|
| 233 |
235 |
|
"/p/test/moderation/ban",
|
| 234 |
236 |
|
"username=theowner&duration=permanent",
|
| 235 |
237 |
|
).await;
|
| 236 |
|
- |
assert_eq!(resp.status, axum::http::StatusCode::FORBIDDEN);
|
|
238 |
+ |
assert_eq!(resp.status, StatusCode::FORBIDDEN);
|
| 237 |
239 |
|
}
|
| 238 |
240 |
|
|
| 239 |
241 |
|
#[sqlx::test]
|
| 250 |
252 |
|
|
| 251 |
253 |
|
// Verify banned
|
| 252 |
254 |
|
let resp = h.client.get("/p/test").await;
|
| 253 |
|
- |
assert_eq!(resp.status, axum::http::StatusCode::FORBIDDEN);
|
|
255 |
+ |
assert_eq!(resp.status, StatusCode::FORBIDDEN);
|
| 254 |
256 |
|
|
| 255 |
257 |
|
// Unban via direct SQL
|
| 256 |
258 |
|
sqlx::query("DELETE FROM community_bans WHERE community_id = $1 AND user_id = $2 AND ban_type = 'ban'")
|
| 261 |
263 |
|
.unwrap();
|
| 262 |
264 |
|
|
| 263 |
265 |
|
let resp = h.client.get("/p/test").await;
|
| 264 |
|
- |
assert_eq!(resp.status, axum::http::StatusCode::OK);
|
|
266 |
+ |
assert_eq!(resp.status, StatusCode::OK);
|
| 265 |
267 |
|
}
|
| 266 |
268 |
|
|
| 267 |
269 |
|
#[sqlx::test]
|
| 278 |
280 |
|
|
| 279 |
281 |
|
// Verify muted (can read)
|
| 280 |
282 |
|
let resp = h.client.get("/p/test").await;
|
| 281 |
|
- |
assert_eq!(resp.status, axum::http::StatusCode::OK);
|
|
283 |
+ |
assert_eq!(resp.status, StatusCode::OK);
|
| 282 |
284 |
|
|
| 283 |
285 |
|
// Verify muted (cannot write)
|
| 284 |
286 |
|
h.client.get("/").await;
|
| 286 |
288 |
|
"/p/test/general/new",
|
| 287 |
289 |
|
"title=Hello&body=World",
|
| 288 |
290 |
|
).await;
|
| 289 |
|
- |
assert_eq!(resp.status, axum::http::StatusCode::FORBIDDEN);
|
|
291 |
+ |
assert_eq!(resp.status, StatusCode::FORBIDDEN);
|
| 290 |
292 |
|
|
| 291 |
293 |
|
// Unmute via direct SQL
|
| 292 |
294 |
|
sqlx::query("DELETE FROM community_bans WHERE community_id = $1 AND user_id = $2 AND ban_type = 'mute'")
|
| 302 |
304 |
|
"/p/test/general/new",
|
| 303 |
305 |
|
"title=Hello&body=World",
|
| 304 |
306 |
|
).await;
|
| 305 |
|
- |
assert!(resp.status.is_redirection() || resp.status == axum::http::StatusCode::OK);
|
|
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");
|
| 306 |
530 |
|
}
|