Skip to main content

max / makenotwork

13.3 KB · 425 lines History Blame Raw
1 //! Report workflow tests: submit, self-report prevention, auth, admin resolve/dismiss, validation.
2
3 use crate::harness::TestHarness;
4
5 // ── Submit report (logged in) ──
6
7 #[tokio::test]
8 async fn submit_report_logged_in() {
9 let mut h = TestHarness::new().await;
10
11 // Creator creates a project+item
12 let setup = h.create_creator_with_item("rptcreator", "text", 0).await;
13 h.publish_project_and_item(&setup.project_id, &setup.item_id).await;
14
15 // Different user logs in and reports the project
16 h.client.post_form("/logout", "").await;
17 let _reporter = h.signup("rptreporter", "rptreporter@test.com", "password123").await;
18 h.login("rptreporter", "password123").await;
19
20 let resp = h.client
21 .post_form(
22 "/api/reports",
23 &format!(
24 "target_type=project&target_id={}&report_type=spam&reason=looks+like+spam",
25 setup.project_id
26 ),
27 )
28 .await;
29 assert!(
30 resp.status.is_success(),
31 "Submit report failed: {} {}",
32 resp.status, resp.text
33 );
34 assert!(
35 resp.text.contains("Report submitted"),
36 "Should show success message, got: {}",
37 resp.text
38 );
39 }
40
41 // ── Submit report for item ──
42
43 #[tokio::test]
44 async fn submit_report_for_item() {
45 let mut h = TestHarness::new().await;
46 let setup = h.create_creator_with_item("rptitem", "text", 0).await;
47 h.publish_project_and_item(&setup.project_id, &setup.item_id).await;
48
49 h.client.post_form("/logout", "").await;
50 let _reporter = h.signup("rptitemrpt", "rptitemrpt@test.com", "password123").await;
51 h.login("rptitemrpt", "password123").await;
52
53 let resp = h.client
54 .post_form(
55 "/api/reports",
56 &format!(
57 "target_type=item&target_id={}&report_type=abuse&reason=offensive+content",
58 setup.item_id
59 ),
60 )
61 .await;
62 assert!(
63 resp.status.is_success(),
64 "Submit item report failed: {} {}",
65 resp.status, resp.text
66 );
67 }
68
69 // ── Submit report (not logged in → rejected) ──
70
71 #[tokio::test]
72 async fn submit_report_not_logged_in() {
73 let mut h = TestHarness::new().await;
74 let setup = h.create_creator_with_item("rptnoauth", "text", 0).await;
75
76 h.client.post_form("/logout", "").await;
77 h.client.fetch_csrf_token().await;
78
79 let resp = h.client
80 .post_form(
81 "/api/reports",
82 &format!(
83 "target_type=project&target_id={}&report_type=spam&reason=test",
84 setup.project_id
85 ),
86 )
87 .await;
88 assert!(
89 resp.status.as_u16() == 401 || resp.status.as_u16() == 303,
90 "Unauthenticated report should be rejected: {} {}",
91 resp.status, resp.text
92 );
93 }
94
95 // ── Self-report prevention ──
96
97 #[tokio::test]
98 async fn cannot_report_own_project() {
99 let mut h = TestHarness::new().await;
100 let setup = h.create_creator_with_item("rptself", "text", 0).await;
101
102 let resp = h.client
103 .post_form(
104 "/api/reports",
105 &format!(
106 "target_type=project&target_id={}&report_type=spam&reason=testing",
107 setup.project_id
108 ),
109 )
110 .await;
111 assert!(
112 resp.status.as_u16() == 422 || resp.text.contains("cannot report your own"),
113 "Self-report should be rejected: {} {}",
114 resp.status, resp.text
115 );
116 }
117
118 #[tokio::test]
119 async fn cannot_report_own_item() {
120 let mut h = TestHarness::new().await;
121 let setup = h.create_creator_with_item("rptselfitm", "text", 0).await;
122
123 let resp = h.client
124 .post_form(
125 "/api/reports",
126 &format!(
127 "target_type=item&target_id={}&report_type=spam&reason=testing",
128 setup.item_id
129 ),
130 )
131 .await;
132 assert!(
133 resp.status.as_u16() == 422 || resp.text.contains("cannot report your own"),
134 "Self-report should be rejected: {} {}",
135 resp.status, resp.text
136 );
137 }
138
139 // ── Report type validation ──
140
141 #[tokio::test]
142 async fn invalid_report_type_rejected() {
143 let mut h = TestHarness::new().await;
144 let setup = h.create_creator_with_item("rptbadtype", "text", 0).await;
145 h.publish_project_and_item(&setup.project_id, &setup.item_id).await;
146
147 h.client.post_form("/logout", "").await;
148 let _reporter = h.signup("rptbadrptr", "rptbadrptr@test.com", "password123").await;
149 h.login("rptbadrptr", "password123").await;
150
151 let resp = h.client
152 .post_form(
153 "/api/reports",
154 &format!(
155 "target_type=project&target_id={}&report_type=invalid_type&reason=test",
156 setup.project_id
157 ),
158 )
159 .await;
160 assert!(
161 resp.status.as_u16() == 422 || resp.text.contains("Invalid report type"),
162 "Invalid report type should be rejected: {} {}",
163 resp.status, resp.text
164 );
165 }
166
167 #[tokio::test]
168 async fn other_report_type_requires_reason() {
169 let mut h = TestHarness::new().await;
170 let setup = h.create_creator_with_item("rptnoreas", "text", 0).await;
171 h.publish_project_and_item(&setup.project_id, &setup.item_id).await;
172
173 h.client.post_form("/logout", "").await;
174 let _reporter = h.signup("rptnorsrpt", "rptnorsrpt@test.com", "password123").await;
175 h.login("rptnorsrpt", "password123").await;
176
177 let resp = h.client
178 .post_form(
179 "/api/reports",
180 &format!(
181 "target_type=project&target_id={}&report_type=other&reason=",
182 setup.project_id
183 ),
184 )
185 .await;
186 assert!(
187 resp.status.as_u16() == 422 || resp.text.contains("provide details"),
188 "Other report without reason should be rejected: {} {}",
189 resp.status, resp.text
190 );
191 }
192
193 // ── Admin resolve/dismiss ──
194
195 #[tokio::test]
196 async fn admin_resolve_report() {
197 let (mut h, _admin_id) = TestHarness::with_admin().await;
198
199 // Creator makes content
200 let setup = h.create_creator_with_item("rptadmres", "text", 0).await;
201 h.publish_project_and_item(&setup.project_id, &setup.item_id).await;
202
203 // Different user reports it
204 h.client.post_form("/logout", "").await;
205 let _reporter = h.signup("rptadmrptr", "rptadmrptr@test.com", "password123").await;
206 h.login("rptadmrptr", "password123").await;
207
208 h.client
209 .post_form(
210 "/api/reports",
211 &format!(
212 "target_type=project&target_id={}&report_type=spam&reason=looks+spammy",
213 setup.project_id
214 ),
215 )
216 .await;
217
218 // Get report ID from DB
219 let row: (uuid::Uuid,) = sqlx::query_as("SELECT id FROM reports ORDER BY created_at DESC LIMIT 1")
220 .fetch_one(&h.db)
221 .await
222 .unwrap();
223 let report_id = row.0;
224
225 // Admin resolves it
226 h.client.post_form("/logout", "").await;
227 h.login("admin", "password123").await;
228
229 let resp = h.client
230 .post_form(
231 &format!("/api/admin/reports/{}/resolve", report_id),
232 "decision=resolve&admin_notes=Investigated+and+confirmed+spam",
233 )
234 .await;
235 assert!(
236 resp.status.is_success(),
237 "Admin resolve failed: {} {}",
238 resp.status, resp.text
239 );
240
241 // Verify status in DB
242 let (status,): (String,) =
243 sqlx::query_as("SELECT status FROM reports WHERE id = $1")
244 .bind(report_id)
245 .fetch_one(&h.db)
246 .await
247 .unwrap();
248 assert_eq!(status, "resolved");
249 }
250
251 #[tokio::test]
252 async fn admin_dismiss_report() {
253 let (mut h, _admin_id) = TestHarness::with_admin().await;
254
255 let setup = h.create_creator_with_item("rptadmdis", "text", 0).await;
256 h.publish_project_and_item(&setup.project_id, &setup.item_id).await;
257
258 h.client.post_form("/logout", "").await;
259 let _reporter = h.signup("rptdisrptr", "rptdisrptr@test.com", "password123").await;
260 h.login("rptdisrptr", "password123").await;
261
262 h.client
263 .post_form(
264 "/api/reports",
265 &format!(
266 "target_type=item&target_id={}&report_type=abuse&reason=test+report",
267 setup.item_id
268 ),
269 )
270 .await;
271
272 let row: (uuid::Uuid,) = sqlx::query_as("SELECT id FROM reports ORDER BY created_at DESC LIMIT 1")
273 .fetch_one(&h.db)
274 .await
275 .unwrap();
276 let report_id = row.0;
277
278 h.client.post_form("/logout", "").await;
279 h.login("admin", "password123").await;
280
281 let resp = h.client
282 .post_form(
283 &format!("/api/admin/reports/{}/resolve", report_id),
284 "decision=dismiss&admin_notes=Not+a+real+issue",
285 )
286 .await;
287 assert!(
288 resp.status.is_success(),
289 "Admin dismiss failed: {} {}",
290 resp.status, resp.text
291 );
292
293 let (status,): (String,) =
294 sqlx::query_as("SELECT status FROM reports WHERE id = $1")
295 .bind(report_id)
296 .fetch_one(&h.db)
297 .await
298 .unwrap();
299 assert_eq!(status, "dismissed");
300 }
301
302 // ── Admin page access ──
303
304 #[tokio::test]
305 async fn admin_reports_page_loads() {
306 let (mut h, _admin_id) = TestHarness::with_admin().await;
307 h.client.post_form("/logout", "").await;
308 h.login("admin", "password123").await;
309
310 let resp = h.client.get("/admin/reports").await;
311 assert!(
312 resp.status.is_success(),
313 "Admin reports page failed: {} {}",
314 resp.status, resp.text
315 );
316 assert!(
317 resp.text.contains("Reports Queue"),
318 "Page should have title"
319 );
320 }
321
322 #[tokio::test]
323 async fn non_admin_cannot_access_reports_page() {
324 let mut h = TestHarness::new().await;
325 let _user_id = h.create_creator("rptnonadm").await;
326
327 let resp = h.client.get("/admin/reports").await;
328 assert!(
329 resp.status.as_u16() == 404 || resp.status.as_u16() == 403 || resp.status.as_u16() == 401,
330 "Non-admin should be rejected: {} {}",
331 resp.status, resp.text
332 );
333 }
334
335 #[tokio::test]
336 async fn non_admin_cannot_resolve_report() {
337 let (mut h, _admin_id) = TestHarness::with_admin().await;
338
339 // Create a report via admin flow
340 let setup = h.create_creator_with_item("rptnoadmr", "text", 0).await;
341 h.publish_project_and_item(&setup.project_id, &setup.item_id).await;
342
343 h.client.post_form("/logout", "").await;
344 let _reporter = h.signup("rptnarptr", "rptnarptr@test.com", "password123").await;
345 h.login("rptnarptr", "password123").await;
346
347 h.client
348 .post_form(
349 "/api/reports",
350 &format!(
351 "target_type=project&target_id={}&report_type=spam&reason=test",
352 setup.project_id
353 ),
354 )
355 .await;
356
357 let row: (uuid::Uuid,) = sqlx::query_as("SELECT id FROM reports ORDER BY created_at DESC LIMIT 1")
358 .fetch_one(&h.db)
359 .await
360 .unwrap();
361 let report_id = row.0;
362
363 // Non-admin tries to resolve
364 let resp = h.client
365 .post_form(
366 &format!("/api/admin/reports/{}/resolve", report_id),
367 "decision=resolve&admin_notes=hacked",
368 )
369 .await;
370 assert!(
371 resp.status.as_u16() == 404 || resp.status.as_u16() == 403 || resp.status.as_u16() == 401,
372 "Non-admin should be rejected: {} {}",
373 resp.status, resp.text
374 );
375 }
376
377 // ---------------------------------------------------------------------------
378 // Report-spam rate limit (test-fuzz Phase 2.3)
379 //
380 // submit_report enforces max 10 reports per reporter per 24h via
381 // count_recent_reports_by_user. The cap itself was untested — a regression
382 // (e.g. an off-by-one or a dropped check) would let one user flood the
383 // moderation queue. This pins the boundary: the 10th is accepted, the 11th is
384 // rejected, and exactly 10 rows land.
385 // ---------------------------------------------------------------------------
386
387 #[tokio::test]
388 async fn report_spam_rate_limited_at_ten_per_day() {
389 let mut h = TestHarness::new().await;
390
391 // A creator with a public item to report. (create_report has no per-target
392 // dedup, so repeatedly reporting the same item is what a spammer would do.)
393 let setup = h.create_creator_with_item("spamtarget", "text", 0).await;
394 h.publish_project_and_item(&setup.project_id, &setup.item_id).await;
395
396 h.client.post_form("/logout", "").await;
397 let reporter_id = h.signup("spamreporter", "spamreporter@test.com", "password123").await;
398 h.login("spamreporter", "password123").await;
399
400 let body = format!(
401 "target_type=item&target_id={}&report_type=spam&reason=spam",
402 setup.item_id
403 );
404
405 // First 10 are accepted (the cap is `>= 10` checked BEFORE insert, so the
406 // 10th sees a count of 9 and goes through).
407 for n in 1..=10 {
408 let resp = h.client.post_form("/api/reports", &body).await;
409 assert!(resp.status.is_success(), "report #{} should be accepted: {} {}", n, resp.status, resp.text);
410 }
411
412 // The 11th sees a count of 10 and is rejected.
413 let resp = h.client.post_form("/api/reports", &body).await;
414 assert!(!resp.status.is_success() || resp.text.contains("limit reached"),
415 "the 11th report must be rate-limited, got: {} {}", resp.status, resp.text);
416
417 // Exactly 10 rows persisted — the rejected one wrote nothing.
418 let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM reports WHERE reporter_user_id = $1")
419 .bind(reporter_id)
420 .fetch_one(&h.db)
421 .await
422 .unwrap();
423 assert_eq!(count, 10, "rate limit must cap stored reports at 10, found {count}");
424 }
425