use crate::harness::TestHarness; /// Regression (ultra-fuzz S1): a search match on a *reply* must be attributed to /// the reply's author, not the thread's OP. The query previously joined the /// displayed author on `t.author_id` while the snippet came from `p.body_markdown`. #[tokio::test] async fn search_post_match_attributes_reply_author() { let mut h = TestHarness::new().await; let op = h.login_as("threadstarter").await; let comm_id = h.create_community("Test", "test").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(op, comm_id, "member").await; // OP body has no match term; the matching content lives in a reply by a // different author. let thread_id = h .create_thread_with_post(cat_id, op, "Plain Title", "ordinary opening post") .await; let replier = h.login_as("thereplier").await; h.add_membership(replier, comm_id, "member").await; mt_db::mutations::create_post( &h.db, thread_id, replier, "quintessential zygomorphic detail", "

x

", ) .await .expect("reply insert"); let results = mt_db::queries::search_threads(&h.db, "zygomorphic", None, 20) .await .expect("search"); let row = results .iter() .find(|r| r.thread_title == "Plain Title") .expect("reply match should surface the thread"); assert_eq!( row.author_username, "thereplier", "reply match must be attributed to the reply author, not the OP" ); } #[tokio::test] async fn search_by_thread_title() { let mut h = TestHarness::new().await; let user_id = h.login_as("searchtitle").await; let comm_id = h.create_community("Test", "test").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(user_id, comm_id, "member").await; h.create_thread_with_post(cat_id, user_id, "Unique Flamingo Discussion", "body text") .await; h.create_thread_with_post(cat_id, user_id, "Other Thread", "nothing here") .await; let resp = h.client.get("/search?q=flamingo").await; assert_eq!(resp.status, 200); assert!( resp.text.contains("Unique Flamingo Discussion"), "Expected thread title in search results. Body: {}", resp.text ); assert!( !resp.text.contains("Other Thread"), "Non-matching thread should not appear" ); } #[tokio::test] async fn search_body_content_match() { let mut h = TestHarness::new().await; let user_id = h.login_as("searchbody").await; let comm_id = h.create_community("Test", "test").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(user_id, comm_id, "member").await; h.create_thread_with_post( cat_id, user_id, "Generic Title", "zygomorphic flower patterns", ) .await; let resp = h.client.get("/search?q=zygomorphic").await; assert_eq!(resp.status, 200); assert!( resp.text.contains("Generic Title"), "Thread with matching body should appear. Body: {}", resp.text ); } #[tokio::test] async fn search_scoped_to_community() { let mut h = TestHarness::new().await; let user_id = h.login_as("searchscoped").await; let comm1 = h.create_community("Alpha", "alpha").await; let cat1 = h.create_category(comm1, "General", "general").await; h.add_membership(user_id, comm1, "member").await; let comm2 = h.create_community("Beta", "beta").await; let cat2 = h.create_category(comm2, "General", "general").await; h.add_membership(user_id, comm2, "member").await; h.create_thread_with_post(cat1, user_id, "Shared Keyword Xylophone", "content") .await; h.create_thread_with_post(cat2, user_id, "Shared Keyword Xylophone Two", "content") .await; // Scoped to alpha let resp = h.client.get("/search?q=xylophone&scope=alpha").await; assert_eq!(resp.status, 200); assert!( resp.text.contains("Shared Keyword Xylophone"), "Alpha result should appear" ); assert!( !resp.text.contains("Xylophone Two"), "Beta result should NOT appear in scoped search" ); // Global let resp = h.client.get("/search?q=xylophone").await; assert!( resp.text.contains("Xylophone Two"), "Beta result should appear in global search" ); } #[tokio::test] async fn search_empty_query_returns_nothing() { let mut h = TestHarness::new().await; let user_id = h.login_as("searchempty").await; let comm_id = h.create_community("Test", "test").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(user_id, comm_id, "member").await; h.create_thread_with_post(cat_id, user_id, "Should Not Appear", "content") .await; let resp = h.client.get("/search?q=").await; assert_eq!(resp.status, 200); assert!( !resp.text.contains("Should Not Appear"), "Empty query should return no results" ); } #[tokio::test] async fn search_deleted_thread_excluded() { let mut h = TestHarness::new().await; let user_id = h.login_as("searchdeleted").await; let comm_id = h.create_community("Test", "test").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(user_id, comm_id, "member").await; let thread_id = h .create_thread_with_post(cat_id, user_id, "Ephemeral Jellyfish", "content") .await; // Soft-delete the thread mt_db::mutations::soft_delete_thread(&h.db, thread_id) .await .unwrap(); let resp = h.client.get("/search?q=jellyfish").await; assert_eq!(resp.status, 200); assert!( !resp.text.contains("Ephemeral Jellyfish"), "Deleted thread should not appear in search" ); } /// Regression (audit N1): the fuzzy title branch must still catch a misspelling. /// The query switched from `similarity(title, $2) > 0.1` (a seq scan) to the /// index-served `title % $2` with `pg_trgm.similarity_threshold` pinned to 0.1; /// this guards that the looser trigram threshold survives that rewrite. The typo /// term is not a lexeme of the title, so the tsvector branch cannot match it, /// only the trigram branch can. #[tokio::test] async fn search_fuzzy_title_typo_matches_via_trigram() { let mut h = TestHarness::new().await; let user_id = h.login_as("searchfuzzy").await; let comm_id = h.create_community("Test", "test").await; let cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(user_id, comm_id, "member").await; h.create_thread_with_post(cat_id, user_id, "Florbnak Widget Notes", "body text") .await; h.create_thread_with_post(cat_id, user_id, "Totally Unrelated", "nothing here") .await; // "florbnk" (dropped 'a') is a typo, not a stemmable lexeme of the title, so // it can only surface through the trigram similarity branch. let results = mt_db::queries::search_threads(&h.db, "florbnk", None, 20) .await .expect("search"); assert!( results .iter() .any(|r| r.thread_title == "Florbnak Widget Notes"), "typo query should surface the near-match title via the trigram branch" ); assert!( !results .iter() .any(|r| r.thread_title == "Totally Unrelated"), "dissimilar title must not match" ); }