Skip to main content

max / makenotwork

7.4 KB · 216 lines History Blame Raw
1 use crate::harness::TestHarness;
2
3 /// Regression (ultra-fuzz S1): a search match on a *reply* must be attributed to
4 /// the reply's author, not the thread's OP. The query previously joined the
5 /// displayed author on `t.author_id` while the snippet came from `p.body_markdown`.
6 #[tokio::test]
7 async fn search_post_match_attributes_reply_author() {
8 let mut h = TestHarness::new().await;
9 let op = h.login_as("threadstarter").await;
10 let comm_id = h.create_community("Test", "test").await;
11 let cat_id = h.create_category(comm_id, "General", "general").await;
12 h.add_membership(op, comm_id, "member").await;
13
14 // OP body has no match term; the matching content lives in a reply by a
15 // different author.
16 let thread_id = h
17 .create_thread_with_post(cat_id, op, "Plain Title", "ordinary opening post")
18 .await;
19 let replier = h.login_as("thereplier").await;
20 h.add_membership(replier, comm_id, "member").await;
21 mt_db::mutations::create_post(
22 &h.db,
23 thread_id,
24 replier,
25 "quintessential zygomorphic detail",
26 "<p>x</p>",
27 )
28 .await
29 .expect("reply insert");
30
31 let results = mt_db::queries::search_threads(&h.db, "zygomorphic", None, 20)
32 .await
33 .expect("search");
34
35 let row = results
36 .iter()
37 .find(|r| r.thread_title == "Plain Title")
38 .expect("reply match should surface the thread");
39 assert_eq!(
40 row.author_username, "thereplier",
41 "reply match must be attributed to the reply author, not the OP"
42 );
43 }
44
45 #[tokio::test]
46 async fn search_by_thread_title() {
47 let mut h = TestHarness::new().await;
48 let user_id = h.login_as("searchtitle").await;
49 let comm_id = h.create_community("Test", "test").await;
50 let cat_id = h.create_category(comm_id, "General", "general").await;
51 h.add_membership(user_id, comm_id, "member").await;
52
53 h.create_thread_with_post(cat_id, user_id, "Unique Flamingo Discussion", "body text")
54 .await;
55 h.create_thread_with_post(cat_id, user_id, "Other Thread", "nothing here")
56 .await;
57
58 let resp = h.client.get("/search?q=flamingo").await;
59 assert_eq!(resp.status, 200);
60 assert!(
61 resp.text.contains("Unique Flamingo Discussion"),
62 "Expected thread title in search results. Body: {}",
63 resp.text
64 );
65 assert!(
66 !resp.text.contains("Other Thread"),
67 "Non-matching thread should not appear"
68 );
69 }
70
71 #[tokio::test]
72 async fn search_body_content_match() {
73 let mut h = TestHarness::new().await;
74 let user_id = h.login_as("searchbody").await;
75 let comm_id = h.create_community("Test", "test").await;
76 let cat_id = h.create_category(comm_id, "General", "general").await;
77 h.add_membership(user_id, comm_id, "member").await;
78
79 h.create_thread_with_post(
80 cat_id,
81 user_id,
82 "Generic Title",
83 "zygomorphic flower patterns",
84 )
85 .await;
86
87 let resp = h.client.get("/search?q=zygomorphic").await;
88 assert_eq!(resp.status, 200);
89 assert!(
90 resp.text.contains("Generic Title"),
91 "Thread with matching body should appear. Body: {}",
92 resp.text
93 );
94 }
95
96 #[tokio::test]
97 async fn search_scoped_to_community() {
98 let mut h = TestHarness::new().await;
99 let user_id = h.login_as("searchscoped").await;
100
101 let comm1 = h.create_community("Alpha", "alpha").await;
102 let cat1 = h.create_category(comm1, "General", "general").await;
103 h.add_membership(user_id, comm1, "member").await;
104
105 let comm2 = h.create_community("Beta", "beta").await;
106 let cat2 = h.create_category(comm2, "General", "general").await;
107 h.add_membership(user_id, comm2, "member").await;
108
109 h.create_thread_with_post(cat1, user_id, "Shared Keyword Xylophone", "content")
110 .await;
111 h.create_thread_with_post(cat2, user_id, "Shared Keyword Xylophone Two", "content")
112 .await;
113
114 // Scoped to alpha
115 let resp = h.client.get("/search?q=xylophone&scope=alpha").await;
116 assert_eq!(resp.status, 200);
117 assert!(
118 resp.text.contains("Shared Keyword Xylophone"),
119 "Alpha result should appear"
120 );
121 assert!(
122 !resp.text.contains("Xylophone Two"),
123 "Beta result should NOT appear in scoped search"
124 );
125
126 // Global
127 let resp = h.client.get("/search?q=xylophone").await;
128 assert!(
129 resp.text.contains("Xylophone Two"),
130 "Beta result should appear in global search"
131 );
132 }
133
134 #[tokio::test]
135 async fn search_empty_query_returns_nothing() {
136 let mut h = TestHarness::new().await;
137 let user_id = h.login_as("searchempty").await;
138 let comm_id = h.create_community("Test", "test").await;
139 let cat_id = h.create_category(comm_id, "General", "general").await;
140 h.add_membership(user_id, comm_id, "member").await;
141
142 h.create_thread_with_post(cat_id, user_id, "Should Not Appear", "content")
143 .await;
144
145 let resp = h.client.get("/search?q=").await;
146 assert_eq!(resp.status, 200);
147 assert!(
148 !resp.text.contains("Should Not Appear"),
149 "Empty query should return no results"
150 );
151 }
152
153 #[tokio::test]
154 async fn search_deleted_thread_excluded() {
155 let mut h = TestHarness::new().await;
156 let user_id = h.login_as("searchdeleted").await;
157 let comm_id = h.create_community("Test", "test").await;
158 let cat_id = h.create_category(comm_id, "General", "general").await;
159 h.add_membership(user_id, comm_id, "member").await;
160
161 let thread_id = h
162 .create_thread_with_post(cat_id, user_id, "Ephemeral Jellyfish", "content")
163 .await;
164
165 // Soft-delete the thread
166 mt_db::mutations::soft_delete_thread(&h.db, thread_id)
167 .await
168 .unwrap();
169
170 let resp = h.client.get("/search?q=jellyfish").await;
171 assert_eq!(resp.status, 200);
172 assert!(
173 !resp.text.contains("Ephemeral Jellyfish"),
174 "Deleted thread should not appear in search"
175 );
176 }
177
178 /// Regression (audit N1): the fuzzy title branch must still catch a misspelling.
179 /// The query switched from `similarity(title, $2) > 0.1` (a seq scan) to the
180 /// index-served `title % $2` with `pg_trgm.similarity_threshold` pinned to 0.1;
181 /// this guards that the looser trigram threshold survives that rewrite. The typo
182 /// term is not a lexeme of the title, so the tsvector branch cannot match it,
183 /// only the trigram branch can.
184 #[tokio::test]
185 async fn search_fuzzy_title_typo_matches_via_trigram() {
186 let mut h = TestHarness::new().await;
187 let user_id = h.login_as("searchfuzzy").await;
188 let comm_id = h.create_community("Test", "test").await;
189 let cat_id = h.create_category(comm_id, "General", "general").await;
190 h.add_membership(user_id, comm_id, "member").await;
191
192 h.create_thread_with_post(cat_id, user_id, "Florbnak Widget Notes", "body text")
193 .await;
194 h.create_thread_with_post(cat_id, user_id, "Totally Unrelated", "nothing here")
195 .await;
196
197 // "florbnk" (dropped 'a') is a typo, not a stemmable lexeme of the title, so
198 // it can only surface through the trigram similarity branch.
199 let results = mt_db::queries::search_threads(&h.db, "florbnk", None, 20)
200 .await
201 .expect("search");
202
203 assert!(
204 results
205 .iter()
206 .any(|r| r.thread_title == "Florbnak Widget Notes"),
207 "typo query should surface the near-match title via the trigram branch"
208 );
209 assert!(
210 !results
211 .iter()
212 .any(|r| r.thread_title == "Totally Unrelated"),
213 "dissimilar title must not match"
214 );
215 }
216