use crate::harness::TestHarness; // ============================================================================ // Auto-hide threshold tests // ============================================================================ #[tokio::test] async fn auto_hide_threshold_removes_post_at_threshold() { let mut h = TestHarness::new().await; let author_id = h.login_as("hideauthor").await; let comm_id = h.create_community("Test", "test").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(author_id, comm_id, "member").await; // Set auto_hide_threshold to 2 sqlx::query("UPDATE communities SET auto_hide_threshold = 2 WHERE id = $1") .bind(comm_id) .execute(&h.db) .await .unwrap(); let thread_id = h .create_thread_with_post(cat_id, author_id, "Auto Hide Test", "Content to hide") .await; let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id) .await .unwrap(); let post_id = posts[0].id; // First flag — should NOT auto-hide (threshold=2, only 1 flag) let flagger1 = h.login_as("flagger1").await; h.add_membership(flagger1, comm_id, "member").await; let thread_url = format!("/p/test/general/{}", thread_id); h.client.get(&thread_url).await; let flag_url = format!("/p/test/general/{}/posts/{}/flag", thread_id, post_id); h.client.post_form(&flag_url, "reason=spam").await; // Verify NOT removed yet let removed: bool = sqlx::query_scalar( "SELECT removed_at IS NOT NULL FROM posts WHERE id = $1", ) .bind(post_id) .fetch_one(&h.db) .await .unwrap(); assert!(!removed, "Post should NOT be removed after 1 flag (threshold=2)"); // Second flag — should auto-hide (2 flags = threshold) let flagger2 = h.login_as("flagger2").await; h.add_membership(flagger2, comm_id, "member").await; h.client.get(&thread_url).await; h.client.post_form(&flag_url, "reason=off_topic").await; let removed: bool = sqlx::query_scalar( "SELECT removed_at IS NOT NULL FROM posts WHERE id = $1", ) .bind(post_id) .fetch_one(&h.db) .await .unwrap(); assert!(removed, "Post should be auto-hidden after 2 flags (threshold=2)"); } #[tokio::test] async fn auto_hide_disabled_when_threshold_null() { let mut h = TestHarness::new().await; let author_id = h.login_as("nohideauthor").await; let comm_id = h.create_community("Test", "test").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(author_id, comm_id, "member").await; // auto_hide_threshold is NULL by default — no auto-hide let thread_id = h .create_thread_with_post(cat_id, author_id, "No Hide Test", "Content stays") .await; let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id) .await .unwrap(); let post_id = posts[0].id; // Flag 5 times from different users for i in 0..5 { let flagger = h.login_as(&format!("nohideflagger{i}")).await; h.add_membership(flagger, comm_id, "member").await; let thread_url = format!("/p/test/general/{}", thread_id); h.client.get(&thread_url).await; let flag_url = format!("/p/test/general/{}/posts/{}/flag", thread_id, post_id); h.client.post_form(&flag_url, "reason=spam").await; } // Verify NOT removed (threshold is NULL = disabled) let removed: bool = sqlx::query_scalar( "SELECT removed_at IS NOT NULL FROM posts WHERE id = $1", ) .bind(post_id) .fetch_one(&h.db) .await .unwrap(); assert!(!removed, "Post should NOT be removed when auto_hide_threshold is NULL"); } #[tokio::test] async fn settings_saves_auto_hide_threshold() { let mut h = TestHarness::new().await; let owner_id = h.login_as("thresholdowner").await; let comm_id = h.create_community("Test", "test").await; h.add_membership(owner_id, comm_id, "owner").await; let _cat_id = h.create_category(comm_id, "General", "general").await; // GET settings for CSRF h.client.get("/p/test/settings").await; // Save with threshold=3 let resp = h .client .post_form( "/p/test/settings", "name=Test&description=desc&auto_hide_threshold=3", ) .await; assert!(resp.status.is_redirection(), "Expected redirect, got {}", resp.status); // Verify in DB let threshold: Option = sqlx::query_scalar( "SELECT auto_hide_threshold FROM communities WHERE id = $1", ) .bind(comm_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(threshold, Some(3)); // Save with threshold=0 (disabled) h.client.get("/p/test/settings").await; h.client .post_form( "/p/test/settings", "name=Test&description=desc&auto_hide_threshold=0", ) .await; let threshold: Option = sqlx::query_scalar( "SELECT auto_hide_threshold FROM communities WHERE id = $1", ) .bind(comm_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(threshold, None, "Threshold 0 should be stored as NULL"); } #[tokio::test] async fn flag_post_happy_path() { let mut h = TestHarness::new().await; let author_id = h.login_as("flagauthor").await; let comm_id = h.create_community("Test", "test").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(author_id, comm_id, "member").await; let thread_id = h .create_thread_with_post(cat_id, author_id, "Flag Test", "Content") .await; let flagger_id = h.login_as("flagger").await; h.add_membership(flagger_id, comm_id, "member").await; let thread_url = format!("/p/test/general/{}", thread_id); h.client.get(&thread_url).await; let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id) .await .unwrap(); let post_id = posts[0].id; let flag_url = format!( "/p/test/general/{}/posts/{}/flag", thread_id, post_id ); let resp = h.client.post_form(&flag_url, "reason=spam").await; assert!(resp.status.is_redirection(), "Expected redirect, got {}", resp.status); // Verify flag in DB let has_flag = mt_db::queries::has_user_flagged_post(&h.db, post_id, flagger_id) .await .unwrap(); assert!(has_flag, "Flag should exist in DB"); } #[tokio::test] async fn duplicate_flag_silently_ignored() { let mut h = TestHarness::new().await; let author_id = h.login_as("dupflagauthor").await; let comm_id = h.create_community("Test", "test").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(author_id, comm_id, "member").await; let thread_id = h .create_thread_with_post(cat_id, author_id, "Dup Flag", "Content") .await; let flagger_id = h.login_as("dupflagger").await; h.add_membership(flagger_id, comm_id, "member").await; let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id) .await .unwrap(); let post_id = posts[0].id; let thread_url = format!("/p/test/general/{}", thread_id); h.client.get(&thread_url).await; let flag_url = format!( "/p/test/general/{}/posts/{}/flag", thread_id, post_id ); // Flag once h.client.post_form(&flag_url, "reason=spam").await; // GET for CSRF refresh h.client.get(&thread_url).await; // Flag again — should not error let resp = h.client.post_form(&flag_url, "reason=off_topic").await; assert!(resp.status.is_redirection(), "Duplicate flag should redirect, got {}", resp.status); } #[tokio::test] async fn flag_requires_login() { let mut h = TestHarness::new().await; let author_id = h.login_as("flagloginauthor").await; let comm_id = h.create_community("Test", "test").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(author_id, comm_id, "member").await; let thread_id = h .create_thread_with_post(cat_id, author_id, "Login Flag", "Content") .await; let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id) .await .unwrap(); let post_id = posts[0].id; // New harness (no login) let mut h2 = TestHarness::new().await; h2.client.get("/").await; let flag_url = format!( "/p/test/general/{}/posts/{}/flag", thread_id, post_id ); let resp = h2.client.post_form(&flag_url, "reason=spam").await; assert!( resp.status.is_redirection(), "Expected redirect to login, got {}", resp.status ); } #[tokio::test] async fn flag_own_post_rejected() { let mut h = TestHarness::new().await; let author_id = h.login_as("selfflagauthor").await; let comm_id = h.create_community("Test", "test").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(author_id, comm_id, "member").await; let thread_id = h .create_thread_with_post(cat_id, author_id, "Self Flag", "Content") .await; let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id) .await .unwrap(); let post_id = posts[0].id; let thread_url = format!("/p/test/general/{}", thread_id); h.client.get(&thread_url).await; let flag_url = format!( "/p/test/general/{}/posts/{}/flag", thread_id, post_id ); let resp = h.client.post_form(&flag_url, "reason=spam").await; assert_eq!(resp.status.as_u16(), 403, "Flagging own post should be 403"); } #[tokio::test] async fn mod_dismiss_flag() { let mut h = TestHarness::new().await; let author_id = h.login_as("dismissauthor").await; let comm_id = h.create_community("Test", "test").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(author_id, comm_id, "member").await; let thread_id = h .create_thread_with_post(cat_id, author_id, "Dismiss Test", "Content") .await; let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id) .await .unwrap(); let post_id = posts[0].id; // Insert a flag directly let flagger_id = h.login_as("dismissflagger").await; h.add_membership(flagger_id, comm_id, "member").await; mt_db::mutations::insert_flag(&h.db, post_id, flagger_id, "spam", None) .await .unwrap(); // Login as mod let mod_id = h.login_as("dismissmod").await; h.add_membership(mod_id, comm_id, "moderator").await; // Get flag ID let flags = mt_db::queries::list_pending_flags(&h.db, comm_id).await.unwrap(); assert_eq!(flags.len(), 1); let flag_id = flags[0].flag_id; // GET moderation page for CSRF h.client.get("/p/test/moderation").await; let dismiss_url = format!("/p/test/moderation/flags/{}/dismiss", flag_id); let resp = h.client.post_form(&dismiss_url, "").await; assert!(resp.status.is_redirection(), "Expected redirect, got {}", resp.status); // Verify flag resolved let flags = mt_db::queries::list_pending_flags(&h.db, comm_id).await.unwrap(); assert_eq!(flags.len(), 0, "Flag should be resolved after dismiss"); } #[tokio::test] async fn mod_remove_via_flag() { let mut h = TestHarness::new().await; let author_id = h.login_as("removeauthor").await; let comm_id = h.create_community("Test", "test").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(author_id, comm_id, "member").await; let thread_id = h .create_thread_with_post(cat_id, author_id, "Remove Test", "Content") .await; let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id) .await .unwrap(); let post_id = posts[0].id; // Insert flags from two different users let flagger1 = h.login_as("removeflag1").await; h.add_membership(flagger1, comm_id, "member").await; mt_db::mutations::insert_flag(&h.db, post_id, flagger1, "spam", None) .await .unwrap(); let flagger2_id = uuid::Uuid::new_v4(); sqlx::query("INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, $2, $2)") .bind(flagger2_id) .bind("removeflag2") .execute(&h.db) .await .unwrap(); mt_db::mutations::insert_flag(&h.db, post_id, flagger2_id, "rule_breaking", Some("bad content")) .await .unwrap(); // Login as mod let mod_id = h.login_as("removemod").await; h.add_membership(mod_id, comm_id, "moderator").await; let flags = mt_db::queries::list_pending_flags(&h.db, comm_id).await.unwrap(); assert_eq!(flags.len(), 2, "Should have 2 pending flags"); let flag_id = flags[0].flag_id; h.client.get("/p/test/moderation").await; let remove_url = format!("/p/test/moderation/flags/{}/remove", flag_id); let resp = h.client.post_form(&remove_url, "").await; assert!(resp.status.is_redirection(), "Expected redirect, got {}", resp.status); // All flags should be resolved let flags = mt_db::queries::list_pending_flags(&h.db, comm_id).await.unwrap(); assert_eq!(flags.len(), 0, "All flags should be resolved after remove"); // Post should be removed let post: Option<(bool,)> = sqlx::query_as( "SELECT removed_at IS NOT NULL FROM posts WHERE id = $1", ) .bind(post_id) .fetch_optional(&h.db) .await .unwrap(); assert!(post.unwrap().0, "Post should be mod-removed"); }