Skip to main content

max / makenotwork

12.8 KB · 379 lines History Blame Raw
1 use crate::harness::TestHarness;
2
3 /// Regression (ultra-fuzz NOTE-1): footnoting your own post by routing the
4 /// request through a *different* community's slug must 404, matching the
5 /// flag/endorse/remove paths. Closes the lone remaining ban-evasion-via-slug gap.
6 #[tokio::test]
7 async fn footnote_via_mismatched_community_slug_is_404() {
8 let mut h = TestHarness::new().await;
9 let author_id = h.login_as("author").await;
10 let comm_a = h.create_community("Alpha", "alpha").await;
11 let cat_a = h.create_category(comm_a, "General", "general").await;
12 h.add_membership(author_id, comm_a, "member").await;
13 let comm_b = h.create_community("Beta", "beta").await;
14 h.create_category(comm_b, "General", "general").await;
15 h.add_membership(author_id, comm_b, "member").await;
16
17 let thread_id = h
18 .create_thread_with_post(cat_a, author_id, "Post in A", "Content")
19 .await;
20 let post_id = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
21 .await
22 .unwrap()[0]
23 .id;
24
25 h.client.get(&format!("/p/alpha/general/{thread_id}")).await; // CSRF
26
27 // Author footnotes their own post but via community B's slug.
28 let url = format!("/p/beta/general/{thread_id}/posts/{post_id}/footnote");
29 let resp = h.client.post_form(&url, "body=sneaky+footnote").await;
30 assert_eq!(
31 resp.status.as_u16(),
32 404,
33 "mismatched-slug footnote must 404"
34 );
35
36 let count = mt_db::queries::count_footnotes_for_post(&h.db, post_id)
37 .await
38 .unwrap();
39 assert_eq!(count, 0, "no footnote should be recorded");
40 }
41
42 #[tokio::test]
43 async fn endorse_post_happy_path() {
44 let mut h = TestHarness::new().await;
45 let author_id = h.login_as("author").await;
46 let comm_id = h.create_community("Test", "test").await;
47 let cat_id = h.create_category(comm_id, "General", "general").await;
48 h.add_membership(author_id, comm_id, "member").await;
49
50 let thread_id = h
51 .create_thread_with_post(cat_id, author_id, "Good Post", "Content")
52 .await;
53
54 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
55 .await
56 .unwrap();
57 let post_id = posts[0].id;
58
59 let endorser_id = h.login_as("endorser").await;
60 h.add_membership(endorser_id, comm_id, "member").await;
61
62 // GET thread page for CSRF
63 let thread_url = format!("/p/test/general/{thread_id}");
64 h.client.get(&thread_url).await;
65
66 let endorse_url = format!("/p/test/general/{thread_id}/posts/{post_id}/endorse");
67 let resp = h.client.post_form(&endorse_url, "").await;
68
69 assert!(
70 resp.status.is_redirection(),
71 "Expected redirect after endorse, got {}",
72 resp.status
73 );
74
75 let endorsements = mt_db::queries::list_endorsements_for_posts(&h.db, &[post_id])
76 .await
77 .unwrap();
78 assert_eq!(endorsements.len(), 1);
79 assert_eq!(endorsements[0].endorser_id, endorser_id);
80 }
81
82 #[tokio::test]
83 async fn endorse_via_mismatched_community_slug_is_404() {
84 // A post lives in community A; routing the endorse through community B's
85 // slug must 404 (post.community_id != slug-derived community), so a user
86 // banned in A can't evade the ban check via another slug (audit S-2).
87 let mut h = TestHarness::new().await;
88 let author_id = h.login_as("author").await;
89 let comm_a = h.create_community("Alpha", "alpha").await;
90 let cat_a = h.create_category(comm_a, "General", "general").await;
91 h.add_membership(author_id, comm_a, "member").await;
92 // A second, unrelated community the attacker is a member of.
93 let comm_b = h.create_community("Beta", "beta").await;
94 h.create_category(comm_b, "General", "general").await;
95
96 let thread_id = h
97 .create_thread_with_post(cat_a, author_id, "Post in A", "Content")
98 .await;
99 let post_id = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
100 .await
101 .unwrap()[0]
102 .id;
103
104 let endorser_id = h.login_as("endorser").await;
105 h.add_membership(endorser_id, comm_b, "member").await;
106 h.client.get(&format!("/p/alpha/general/{thread_id}")).await; // CSRF
107
108 // Use community B's slug with A's thread/post ids.
109 let url = format!("/p/beta/general/{thread_id}/posts/{post_id}/endorse");
110 let resp = h.client.post_form(&url, "").await;
111 assert_eq!(
112 resp.status.as_u16(),
113 404,
114 "mismatched-slug endorse must 404"
115 );
116
117 let endorsements = mt_db::queries::list_endorsements_for_posts(&h.db, &[post_id])
118 .await
119 .unwrap();
120 assert!(endorsements.is_empty(), "no endorsement should be recorded");
121 }
122
123 #[tokio::test]
124 async fn toggle_endorsement_removes() {
125 let mut h = TestHarness::new().await;
126 let author_id = h.login_as("author2").await;
127 let comm_id = h.create_community("Test", "test").await;
128 let cat_id = h.create_category(comm_id, "General", "general").await;
129 h.add_membership(author_id, comm_id, "member").await;
130
131 let thread_id = h
132 .create_thread_with_post(cat_id, author_id, "Toggle Post", "Content")
133 .await;
134
135 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
136 .await
137 .unwrap();
138 let post_id = posts[0].id;
139
140 let toggler_id = h.login_as("toggler").await;
141 h.add_membership(toggler_id, comm_id, "member").await;
142
143 let thread_url = format!("/p/test/general/{thread_id}");
144 h.client.get(&thread_url).await;
145
146 let endorse_url = format!("/p/test/general/{thread_id}/posts/{post_id}/endorse");
147
148 // First endorse
149 h.client.post_form(&endorse_url, "").await;
150 let endorsements = mt_db::queries::list_endorsements_for_posts(&h.db, &[post_id])
151 .await
152 .unwrap();
153 assert_eq!(
154 endorsements.len(),
155 1,
156 "Should have 1 endorsement after first toggle"
157 );
158
159 // GET again for fresh CSRF
160 h.client.get(&thread_url).await;
161
162 // Second endorse (un-endorse)
163 h.client.post_form(&endorse_url, "").await;
164 let endorsements = mt_db::queries::list_endorsements_for_posts(&h.db, &[post_id])
165 .await
166 .unwrap();
167 assert_eq!(
168 endorsements.len(),
169 0,
170 "Should have 0 endorsements after second toggle"
171 );
172 }
173
174 #[tokio::test]
175 async fn self_endorse_rejected() {
176 let mut h = TestHarness::new().await;
177 let author_id = h.login_as("selfendorser").await;
178 let comm_id = h.create_community("Test", "test").await;
179 let cat_id = h.create_category(comm_id, "General", "general").await;
180 h.add_membership(author_id, comm_id, "member").await;
181
182 let thread_id = h
183 .create_thread_with_post(cat_id, author_id, "My Post", "Content")
184 .await;
185
186 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
187 .await
188 .unwrap();
189 let post_id = posts[0].id;
190
191 // GET thread page for CSRF
192 let thread_url = format!("/p/test/general/{thread_id}");
193 h.client.get(&thread_url).await;
194
195 let endorse_url = format!("/p/test/general/{thread_id}/posts/{post_id}/endorse");
196 let resp = h.client.post_form(&endorse_url, "").await;
197
198 assert_eq!(resp.status.as_u16(), 403, "Self-endorsement should be 403");
199 }
200
201 #[tokio::test]
202 async fn endorse_requires_login() {
203 let mut h = TestHarness::new().await;
204 let author_id = h.login_as("loginauthor").await;
205 let comm_id = h.create_community("Test", "test").await;
206 let cat_id = h.create_category(comm_id, "General", "general").await;
207 h.add_membership(author_id, comm_id, "member").await;
208
209 let thread_id = h
210 .create_thread_with_post(cat_id, author_id, "Login Test", "Content")
211 .await;
212
213 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
214 .await
215 .unwrap();
216 let post_id = posts[0].id;
217
218 // Don't login; create a fresh harness without login
219 let mut h2 = TestHarness::new().await;
220 // GET some page for CSRF
221 h2.client.get("/").await;
222
223 let endorse_url = format!("/p/test/general/{thread_id}/posts/{post_id}/endorse");
224 let resp = h2.client.post_form(&endorse_url, "").await;
225
226 assert!(
227 resp.status.is_redirection(),
228 "Expected redirect to login, got {}",
229 resp.status
230 );
231 }
232
233 #[tokio::test]
234 async fn endorse_removed_post_rejected() {
235 let mut h = TestHarness::new().await;
236 let author_id = h.login_as("removedauthor").await;
237 let comm_id = h.create_community("Test", "test").await;
238 let cat_id = h.create_category(comm_id, "General", "general").await;
239 h.add_membership(author_id, comm_id, "member").await;
240
241 let thread_id = h
242 .create_thread_with_post(cat_id, author_id, "Removed Post", "Content")
243 .await;
244
245 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
246 .await
247 .unwrap();
248 let post_id = posts[0].id;
249
250 mt_db::mutations::mod_remove_post(&h.db, post_id, author_id)
251 .await
252 .unwrap();
253
254 let endorser_id = h.login_as("removedendorser").await;
255 h.add_membership(endorser_id, comm_id, "member").await;
256
257 let thread_url = format!("/p/test/general/{thread_id}");
258 h.client.get(&thread_url).await;
259
260 let endorse_url = format!("/p/test/general/{thread_id}/posts/{post_id}/endorse");
261 let resp = h.client.post_form(&endorse_url, "").await;
262
263 assert_eq!(
264 resp.status.as_u16(),
265 403,
266 "Endorsing removed post should be 403"
267 );
268 }
269
270 #[tokio::test]
271 async fn endorsement_count_visible_to_author() {
272 let mut h = TestHarness::new().await;
273 let author_id = h.login_as("countauthor").await;
274 let comm_id = h.create_community("Test", "test").await;
275 let cat_id = h.create_category(comm_id, "General", "general").await;
276 h.add_membership(author_id, comm_id, "member").await;
277
278 let thread_id = h
279 .create_thread_with_post(cat_id, author_id, "Count Test", "Content")
280 .await;
281
282 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
283 .await
284 .unwrap();
285 let post_id = posts[0].id;
286
287 // Insert endorsement directly via DB (avoids needing to switch users)
288 let endorser_id = uuid::Uuid::new_v4();
289 sqlx::query("INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, $2, $2)")
290 .bind(endorser_id)
291 .bind("countendorser")
292 .execute(&h.db)
293 .await
294 .unwrap();
295 mt_db::mutations::toggle_endorsement(&h.db, post_id, endorser_id)
296 .await
297 .unwrap();
298
299 // Author is still logged in, view thread
300 let thread_url = format!("/p/test/general/{thread_id}");
301 let resp = h.client.get(&thread_url).await;
302
303 assert!(
304 resp.text.contains("endorsement-count"),
305 "Author should see endorsement count"
306 );
307 }
308
309 #[tokio::test]
310 async fn endorsement_count_visible_to_mod() {
311 let mut h = TestHarness::new().await;
312 let author_id = h.login_as("modcountauthor").await;
313 let comm_id = h.create_community("Test", "test").await;
314 let cat_id = h.create_category(comm_id, "General", "general").await;
315 h.add_membership(author_id, comm_id, "member").await;
316
317 let thread_id = h
318 .create_thread_with_post(cat_id, author_id, "Mod Count Test", "Content")
319 .await;
320
321 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
322 .await
323 .unwrap();
324 let post_id = posts[0].id;
325
326 let endorser_id = h.login_as("modcountendorser").await;
327 h.add_membership(endorser_id, comm_id, "member").await;
328 mt_db::mutations::toggle_endorsement(&h.db, post_id, endorser_id)
329 .await
330 .unwrap();
331
332 let mod_id = h.login_as("modviewer").await;
333 h.add_membership(mod_id, comm_id, "moderator").await;
334
335 let thread_url = format!("/p/test/general/{thread_id}");
336 let resp = h.client.get(&thread_url).await;
337
338 assert!(
339 resp.text.contains("endorsement-count"),
340 "Mod should see endorsement count"
341 );
342 }
343
344 #[tokio::test]
345 async fn endorsement_count_hidden_from_others() {
346 let mut h = TestHarness::new().await;
347 let author_id = h.login_as("hiddenauthor").await;
348 let comm_id = h.create_community("Test", "test").await;
349 let cat_id = h.create_category(comm_id, "General", "general").await;
350 h.add_membership(author_id, comm_id, "member").await;
351
352 let thread_id = h
353 .create_thread_with_post(cat_id, author_id, "Hidden Count", "Content")
354 .await;
355
356 let posts = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
357 .await
358 .unwrap();
359 let post_id = posts[0].id;
360
361 let endorser_id = h.login_as("hiddenendorser").await;
362 h.add_membership(endorser_id, comm_id, "member").await;
363 mt_db::mutations::toggle_endorsement(&h.db, post_id, endorser_id)
364 .await
365 .unwrap();
366
367 // Login as a third user (not author, not endorser, not mod)
368 let bystander_id = h.login_as("bystander").await;
369 h.add_membership(bystander_id, comm_id, "member").await;
370
371 let thread_url = format!("/p/test/general/{thread_id}");
372 let resp = h.client.get(&thread_url).await;
373
374 assert!(
375 !resp.text.contains("endorsement-count"),
376 "Bystander should NOT see endorsement count"
377 );
378 }
379