//! Integration tests for admin queries and membership counting. //! //! Covers: list_all_communities, search_users (prefix, limit, LIKE escaping), //! get_user_membership_summary (post counts, suspended exclusion). use crate::harness::TestHarness; use uuid::Uuid; // list_all_communities #[tokio::test] async fn test_list_all_communities() { let h = TestHarness::new().await; let _c1 = h.create_community("Alpha Forum", "alpha").await; let _c2 = h.create_community("Beta Forum", "beta").await; let _c3 = h.create_community("Gamma Forum", "gamma").await; let rows = mt_db::queries::list_all_communities(&h.db, 500) .await .unwrap(); assert_eq!(rows.len(), 3, "Should return all 3 communities"); let names: Vec<&str> = rows.iter().map(|r| r.name.as_str()).collect(); assert_eq!( names, vec!["Alpha Forum", "Beta Forum", "Gamma Forum"], "Communities should be ordered by name" ); // Verify slugs are present assert_eq!(rows[0].slug, "alpha"); assert_eq!(rows[1].slug, "beta"); assert_eq!(rows[2].slug, "gamma"); // Verify suspended fields default to None assert!(rows[0].suspended_at.is_none()); assert!(rows[0].suspension_reason.is_none()); } // search_users #[tokio::test] async fn test_search_users_exact_prefix() { let h = TestHarness::new().await; // Insert users directly (no login needed for DB-level tests) for (name, display) in [("alice", "Alice"), ("alvin", "Alvin"), ("bob", "Bob")] { let id = Uuid::new_v4(); sqlx::query( "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, $2, $3)", ) .bind(id) .bind(name) .bind(display) .execute(&h.db) .await .unwrap(); } let results = mt_db::queries::search_users(&h.db, "al").await.unwrap(); let usernames: Vec<&str> = results.iter().map(|r| r.username.as_str()).collect(); assert_eq!( usernames, vec!["alice", "alvin"], "Search for 'al' should match alice and alvin, ordered alphabetically" ); // Verify bob is excluded assert!( !usernames.contains(&"bob"), "bob should not match prefix 'al'" ); } #[tokio::test] async fn test_search_users_limit() { let h = TestHarness::new().await; // Insert a handful of users to verify the query executes with LIMIT 50 for i in 0..5 { let id = Uuid::new_v4(); let name = format!("limituser{i}"); sqlx::query( "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, $2, $2)", ) .bind(id) .bind(&name) .execute(&h.db) .await .unwrap(); } let results = mt_db::queries::search_users(&h.db, "limituser") .await .unwrap(); assert_eq!(results.len(), 5, "Should return all 5 matching users"); // Verify they are ordered alphabetically let usernames: Vec<&str> = results.iter().map(|r| r.username.as_str()).collect(); let mut sorted = usernames.clone(); sorted.sort_unstable(); assert_eq!( usernames, sorted, "Results should be alphabetically ordered" ); } #[tokio::test] async fn test_search_users_special_chars() { let h = TestHarness::new().await; // Insert users: one that looks like a LIKE wildcard match, one normal for (name, display) in [("alice", "Alice"), ("al%pha", "Al%pha")] { let id = Uuid::new_v4(); sqlx::query( "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, $2, $3)", ) .bind(id) .bind(name) .bind(display) .execute(&h.db) .await .unwrap(); } // Search for literal "al%", should only match usernames starting with "al%" let results = mt_db::queries::search_users(&h.db, "al%").await.unwrap(); let usernames: Vec<&str> = results.iter().map(|r| r.username.as_str()).collect(); assert_eq!( usernames, vec!["al%pha"], "Searching 'al%' should match literal percent, not act as wildcard" ); // alice should NOT be matched, the % is escaped, so the query is 'al\%%' assert!( !usernames.contains(&"alice"), "alice should not match when searching for literal 'al%'" ); } // get_user_membership_summary #[tokio::test] async fn test_membership_summary_post_count() { let mut h = TestHarness::new().await; let user_id = h.login_as("postwriter").await; let comm_id = h.create_community("Writers Guild", "writers").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(user_id, comm_id, "member").await; // Create a thread with an initial post, then add two more posts let thread_id = h .create_thread_with_post(cat_id, user_id, "First Thread", "First post body") .await; mt_db::mutations::create_post( &h.db, thread_id, user_id, "Second post", "

Second post

", ) .await .unwrap(); mt_db::mutations::create_post(&h.db, thread_id, user_id, "Third post", "

Third post

") .await .unwrap(); let summaries = mt_db::queries::get_user_membership_summary(&h.db, user_id) .await .unwrap(); assert_eq!(summaries.len(), 1, "Should have one membership"); assert_eq!(summaries[0].community_name, "Writers Guild"); assert_eq!(summaries[0].community_slug, "writers"); assert_eq!(summaries[0].role, mt_core::types::CommunityRole::Member); assert_eq!( summaries[0].post_count, 3, "Should count all 3 posts (initial + 2 replies)" ); } #[tokio::test] async fn profile_counts_exclude_mod_removed_posts() { // Mod-removed posts (removed_at set) must not inflate profile/membership // post counts or the activity feed, the live hide column is removed_at, // not the (unused-for-posts) deleted_at (audit B1/B2). let mut h = TestHarness::new().await; let user_id = h.login_as("removee").await; let mod_id = h.login_as("themod").await; let comm_id = h.create_community("Mod Test", "modtest").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(user_id, comm_id, "member").await; // OP + two replies = 3 posts; then mod-remove one reply. let thread_id = h .create_thread_with_post(cat_id, user_id, "Thread", "OP body") .await; mt_db::mutations::create_post(&h.db, thread_id, user_id, "kept reply", "

kept

") .await .unwrap(); let removed_post = mt_db::mutations::create_post(&h.db, thread_id, user_id, "bad reply", "

bad

") .await .unwrap(); assert!( mt_db::mutations::mod_remove_post(&h.db, removed_post, mod_id) .await .unwrap() ); // Membership summary: 3 created, 1 removed → 2. let summaries = mt_db::queries::get_user_membership_summary(&h.db, user_id) .await .unwrap(); assert_eq!( summaries[0].post_count, 2, "removed post excluded from summary" ); // Profile counts: same exclusion on the per-community post_count. let profile = mt_db::queries::get_user_profile_in_community(&h.db, "modtest", "removee") .await .unwrap() .expect("profile exists"); assert_eq!( profile.post_count, 2, "removed post excluded from profile count" ); // Activity feed must not surface the removed post. let activity = mt_db::queries::get_user_activity_in_community(&h.db, comm_id, user_id, 50) .await .unwrap(); assert_eq!( activity.len(), 2, "removed post excluded from activity feed" ); } #[tokio::test] async fn test_membership_summary_excludes_suspended() { let mut h = TestHarness::new().await; let user_id = h.login_as("suspendcheck").await; let active_id = h.create_community("Active Community", "active").await; h.add_membership(user_id, active_id, "member").await; let suspended_id = h.create_community("Suspended Community", "suspended").await; h.add_membership(user_id, suspended_id, "member").await; // Suspend the second community mt_db::mutations::suspend_community(&h.db, suspended_id, Some("policy violation")) .await .unwrap(); let summaries = mt_db::queries::get_user_membership_summary(&h.db, user_id) .await .unwrap(); assert_eq!(summaries.len(), 1, "Should exclude the suspended community"); assert_eq!(summaries[0].community_name, "Active Community"); }