//! Tests for community-level bans and mutes. use axum::http::StatusCode; use crate::harness::TestHarness; #[sqlx::test] async fn banned_user_cannot_view_community(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; h.create_category(community_id, "General", "general").await; let member = h.login_as("member").await; h.add_membership(member, community_id, "member").await; h.ban_user(community_id, member, owner, "ban").await; let resp = h.client.get("/p/test").await; assert_eq!(resp.status, StatusCode::FORBIDDEN); } #[sqlx::test] async fn banned_user_cannot_create_thread(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; h.create_category(community_id, "General", "general").await; let member = h.login_as("member").await; h.add_membership(member, community_id, "member").await; h.ban_user(community_id, member, owner, "ban").await; // GET to acquire CSRF token h.client.get("/").await; let resp = h.client.post_form( "/p/test/general/new", "title=Hello&body=World", ).await; assert_eq!(resp.status, StatusCode::FORBIDDEN); } #[sqlx::test] async fn banned_user_cannot_reply(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; let cat_id = h.create_category(community_id, "General", "general").await; let thread_id = h.create_thread_with_post(cat_id, owner, "Thread", "OP body").await; let member = h.login_as("member").await; h.add_membership(member, community_id, "member").await; h.ban_user(community_id, member, owner, "ban").await; h.client.get("/").await; let resp = h.client.post_form( &format!("/p/test/general/{thread_id}/reply"), "body=My+reply", ).await; assert_eq!(resp.status, StatusCode::FORBIDDEN); } #[sqlx::test] async fn muted_user_can_view_pages(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; h.create_category(community_id, "General", "general").await; let member = h.login_as("member").await; h.add_membership(member, community_id, "member").await; h.ban_user(community_id, member, owner, "mute").await; let resp = h.client.get("/p/test").await; assert_eq!(resp.status, StatusCode::OK); } #[sqlx::test] async fn muted_user_cannot_create_thread(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; h.create_category(community_id, "General", "general").await; let member = h.login_as("member").await; h.add_membership(member, community_id, "member").await; h.ban_user(community_id, member, owner, "mute").await; h.client.get("/").await; let resp = h.client.post_form( "/p/test/general/new", "title=Hello&body=World", ).await; assert_eq!(resp.status, StatusCode::FORBIDDEN); } #[sqlx::test] async fn muted_user_cannot_reply(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; let cat_id = h.create_category(community_id, "General", "general").await; let thread_id = h.create_thread_with_post(cat_id, owner, "Thread", "OP body").await; let member = h.login_as("member").await; h.add_membership(member, community_id, "member").await; h.ban_user(community_id, member, owner, "mute").await; h.client.get("/").await; let resp = h.client.post_form( &format!("/p/test/general/{thread_id}/reply"), "body=My+reply", ).await; assert_eq!(resp.status, StatusCode::FORBIDDEN); } #[sqlx::test] async fn mod_can_ban_member(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; // Create owner and community let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; // Create the target member user in the same DB let member_id = uuid::Uuid::new_v4(); sqlx::query( "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'member', 'Member')", ) .bind(member_id) .execute(&h.db) .await .unwrap(); h.add_membership(member_id, community_id, "member").await; // Log in as mod and ban member let moduser = h.login_as("moduser").await; h.add_membership(moduser, community_id, "moderator").await; h.client.get("/p/test/moderation").await; let resp = h.client.post_form( "/p/test/moderation/ban", "username=member&duration=permanent&reason=spam", ).await; assert!(resp.status.is_redirection() || resp.status == StatusCode::OK); } #[sqlx::test] async fn mod_cannot_ban_other_mod(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; // Create mod2 user in same DB let mod2_id = uuid::Uuid::new_v4(); sqlx::query( "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'mod2', 'Mod2')", ) .bind(mod2_id) .execute(&h.db) .await .unwrap(); h.add_membership(mod2_id, community_id, "moderator").await; // Log in as mod and try to ban mod2 let moduser = h.login_as("moduser").await; h.add_membership(moduser, community_id, "moderator").await; h.client.get("/p/test/moderation").await; let resp = h.client.post_form( "/p/test/moderation/ban", "username=mod2&duration=permanent", ).await; assert_eq!(resp.status, StatusCode::FORBIDDEN); } #[sqlx::test] async fn owner_can_ban_mod(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; // Create moduser in same DB let mod_id = uuid::Uuid::new_v4(); sqlx::query( "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'moduser', 'ModUser')", ) .bind(mod_id) .execute(&h.db) .await .unwrap(); h.add_membership(mod_id, community_id, "moderator").await; // Owner bans mod h.client.get("/p/test/moderation").await; let resp = h.client.post_form( "/p/test/moderation/ban", "username=moduser&duration=permanent&reason=abuse", ).await; assert!(resp.status.is_redirection() || resp.status == StatusCode::OK); } #[sqlx::test] async fn nobody_can_ban_owner(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; // Create owner user directly so we know the username let owner_id = uuid::Uuid::new_v4(); sqlx::query( "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'theowner', 'TheOwner')", ) .bind(owner_id) .execute(&h.db) .await .unwrap(); let community_id = h.create_community("Test", "test").await; h.add_membership(owner_id, community_id, "owner").await; // Log in as mod let moduser = h.login_as("moduser").await; h.add_membership(moduser, community_id, "moderator").await; h.client.get("/p/test/moderation").await; let resp = h.client.post_form( "/p/test/moderation/ban", "username=theowner&duration=permanent", ).await; assert_eq!(resp.status, StatusCode::FORBIDDEN); } #[sqlx::test] async fn unban_restores_access(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; h.create_category(community_id, "General", "general").await; let member = h.login_as("member").await; h.add_membership(member, community_id, "member").await; h.ban_user(community_id, member, owner, "ban").await; // Verify banned let resp = h.client.get("/p/test").await; assert_eq!(resp.status, StatusCode::FORBIDDEN); // Unban via direct SQL sqlx::query("DELETE FROM community_bans WHERE community_id = $1 AND user_id = $2 AND ban_type = 'ban'") .bind(community_id) .bind(member) .execute(&h.db) .await .unwrap(); let resp = h.client.get("/p/test").await; assert_eq!(resp.status, StatusCode::OK); } #[sqlx::test] async fn unmute_restores_write_access(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; h.create_category(community_id, "General", "general").await; let member = h.login_as("member").await; h.add_membership(member, community_id, "member").await; h.ban_user(community_id, member, owner, "mute").await; // Verify muted (can read) let resp = h.client.get("/p/test").await; assert_eq!(resp.status, StatusCode::OK); // Verify muted (cannot write) h.client.get("/").await; let resp = h.client.post_form( "/p/test/general/new", "title=Hello&body=World", ).await; assert_eq!(resp.status, StatusCode::FORBIDDEN); // Unmute via direct SQL sqlx::query("DELETE FROM community_bans WHERE community_id = $1 AND user_id = $2 AND ban_type = 'mute'") .bind(community_id) .bind(member) .execute(&h.db) .await .unwrap(); // Verify can write again h.client.get("/p/test/general/new").await; let resp = h.client.post_form( "/p/test/general/new", "title=Hello&body=World", ).await; assert!(resp.status.is_redirection() || resp.status == StatusCode::OK); } // ============================================================================ // Handler-based moderation tests (mute, unban, unmute via HTTP POST) // ============================================================================ #[sqlx::test] async fn mod_can_mute_member_via_handler(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; h.create_category(community_id, "General", "general").await; // Create target member via direct SQL let member_id = uuid::Uuid::new_v4(); sqlx::query( "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'target', 'Target')", ) .bind(member_id) .execute(&h.db) .await .unwrap(); h.add_membership(member_id, community_id, "member").await; // Log in as mod, mute target let moduser = h.login_as("moduser").await; h.add_membership(moduser, community_id, "moderator").await; h.client.get("/p/test/moderation").await; let resp = h.client.post_form( "/p/test/moderation/mute", "username=target&duration=1d&reason=spam", ).await; assert!(resp.status.is_redirection(), "Expected redirect, got {}", resp.status); // Verify mute row was created in DB let is_muted = mt_db::queries::is_user_muted(&h.db, community_id, member_id) .await .unwrap(); assert!(is_muted, "Target should be muted after handler call"); } #[sqlx::test] async fn mod_can_unban_member_via_handler(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; // Create and ban target via direct SQL let member_id = uuid::Uuid::new_v4(); sqlx::query( "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'banned', 'Banned')", ) .bind(member_id) .execute(&h.db) .await .unwrap(); h.add_membership(member_id, community_id, "member").await; h.ban_user(community_id, member_id, owner, "ban").await; // Verify banned via DB let is_banned = mt_db::queries::is_user_banned(&h.db, community_id, member_id) .await .unwrap(); assert!(is_banned, "User should be banned before unban"); // Log in as mod, unban via handler let moduser = h.login_as("moduser").await; h.add_membership(moduser, community_id, "moderator").await; h.client.get("/p/test/moderation").await; let resp = h.client.post_form( "/p/test/moderation/unban", "username=banned", ).await; assert!(resp.status.is_redirection(), "Expected redirect, got {}", resp.status); // Verify unbanned via DB let is_banned = mt_db::queries::is_user_banned(&h.db, community_id, member_id) .await .unwrap(); assert!(!is_banned, "User should not be banned after unban handler"); } #[sqlx::test] async fn mod_can_unmute_member_via_handler(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; // Create and mute target via direct SQL let member_id = uuid::Uuid::new_v4(); sqlx::query( "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'muted', 'Muted')", ) .bind(member_id) .execute(&h.db) .await .unwrap(); h.add_membership(member_id, community_id, "member").await; h.ban_user(community_id, member_id, owner, "mute").await; // Verify muted via DB let is_muted = mt_db::queries::is_user_muted(&h.db, community_id, member_id) .await .unwrap(); assert!(is_muted, "User should be muted before unmute"); // Log in as mod, unmute via handler let moduser = h.login_as("moduser").await; h.add_membership(moduser, community_id, "moderator").await; h.client.get("/p/test/moderation").await; let resp = h.client.post_form( "/p/test/moderation/unmute", "username=muted", ).await; assert!(resp.status.is_redirection(), "Expected redirect, got {}", resp.status); // Verify unmuted via DB let is_muted = mt_db::queries::is_user_muted(&h.db, community_id, member_id) .await .unwrap(); assert!(!is_muted, "User should not be muted after unmute handler"); } // ============================================================================ // Expired ban tests // ============================================================================ #[sqlx::test] async fn expired_ban_does_not_block_access(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; h.create_category(community_id, "General", "general").await; let member = h.login_as("member").await; h.add_membership(member, community_id, "member").await; // Insert an already-expired ban directly sqlx::query( "INSERT INTO community_bans (community_id, user_id, banned_by, ban_type, expires_at) VALUES ($1, $2, $3, 'ban', now() - interval '1 hour')", ) .bind(community_id) .bind(member) .bind(owner) .execute(&h.db) .await .unwrap(); // User should still be able to access the community let resp = h.client.get("/p/test").await; assert_eq!(resp.status, StatusCode::OK); // User should be able to create a thread h.client.get("/p/test/general/new").await; let resp = h.client.post_form( "/p/test/general/new", "title=Hello&body=Not+banned", ).await; assert!(resp.status.is_redirection() || resp.status == StatusCode::OK); } #[sqlx::test] async fn expired_bans_cleaned_on_moderation_view(_pool: sqlx::PgPool) { let mut h = TestHarness::new().await; let owner = h.login_as("owner").await; let community_id = h.create_community("Test", "test").await; h.add_membership(owner, community_id, "owner").await; // Create target user let member_id = uuid::Uuid::new_v4(); sqlx::query( "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, 'expired', 'Expired')", ) .bind(member_id) .execute(&h.db) .await .unwrap(); h.add_membership(member_id, community_id, "member").await; // Insert an already-expired ban sqlx::query( "INSERT INTO community_bans (community_id, user_id, banned_by, ban_type, expires_at) VALUES ($1, $2, $3, 'ban', now() - interval '1 hour')", ) .bind(community_id) .bind(member_id) .bind(owner) .execute(&h.db) .await .unwrap(); // Verify the row exists let count: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM community_bans WHERE community_id = $1", ) .bind(community_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(count, 1, "Expired ban row should exist before cleanup"); // Visit moderation page — triggers cleanup h.client.get("/p/test/moderation").await; // Verify row was cleaned up let count: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM community_bans WHERE community_id = $1", ) .bind(community_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(count, 0, "Expired ban row should be deleted after moderation page view"); }