Skip to main content

max / makenotwork

17.0 KB · 602 lines History Blame Raw
1 //! Admin workflow tests: suspend/unsuspend, trust/untrust, upload review, appeal decisions.
2
3 use crate::harness::TestHarness;
4 use makenotwork::db::UserId;
5
6 // ── Structural admin gate (Run 20) ──
7
8 /// The blanket `require_admin_layer` must hide every `/admin*` route from
9 /// anonymous and non-admin callers, regardless of whether the individual
10 /// handler remembers `AdminUser`, except the explicit public health endpoint.
11 #[tokio::test]
12 async fn admin_gate_rejects_anonymous_and_non_admin() {
13 let (mut h, _admin_id) = TestHarness::with_admin().await;
14 h.client.post_form("/logout", "").await; // ensure anonymous
15
16 // Gated routes: an anonymous caller is rejected before the handler by the
17 // layer's AuthUser check (401 Unauthorized). The invariant that matters is
18 // that no gated /admin route is ever reachable (2xx) without auth.
19 for path in [
20 "/admin/users",
21 "/admin/uploads",
22 "/admin/metrics",
23 "/admin/reports",
24 "/admin/signups",
25 "/admin/uploads/queue-summary",
26 ] {
27 let resp = h.client.get(path).await;
28 assert_eq!(resp.status.as_u16(), 401, "anonymous must not reach {path}");
29 }
30
31 // The one explicit exemption stays public for PoM.
32 let resp = h.client.get("/admin/uploads/health.json").await;
33 assert_eq!(
34 resp.status.as_u16(),
35 200,
36 "scan-health JSON must stay public"
37 );
38
39 // A logged-in non-admin is equally hidden.
40 h.signup("plainuser", "plainuser@test.com", "password123")
41 .await;
42 let resp = h.client.get("/admin/users").await;
43 assert_eq!(
44 resp.status.as_u16(),
45 404,
46 "non-admin must not reach /admin/users"
47 );
48 // ...but the public health endpoint is still reachable for them too.
49 let resp = h.client.get("/admin/uploads/health.json").await;
50 assert_eq!(
51 resp.status.as_u16(),
52 200,
53 "health JSON stays public for non-admins"
54 );
55 }
56
57 /// `/health` stays publicly reachable (200) but the live subsystem fan-out is
58 /// admin-only, gated BEFORE the probes run. An anonymous caller gets a minimal
59 /// cached-status page, never the admin dashboard, so an unauthenticated
60 /// request can't amplify into the ~10-op DB/S3/PoM sweep.
61 #[tokio::test]
62 async fn health_gates_full_dashboard_to_admins() {
63 let (mut h, _admin_id) = TestHarness::with_admin().await;
64
65 // Anonymous: minimal reachable status page, no admin fan-out content.
66 h.client.post_form("/logout", "").await;
67 let anon = h.client.get("/health").await;
68 assert_eq!(
69 anon.status.as_u16(),
70 200,
71 "health must stay publicly reachable"
72 );
73 assert!(
74 anon.text.contains("Makenotwork"),
75 "anon gets the minimal status page"
76 );
77 assert!(
78 !anon.text.contains("System Health"),
79 "anon must NOT receive the admin fan-out dashboard"
80 );
81
82 // Admin: full live dashboard.
83 h.login("admin", "password123").await;
84 let admin = h.client.get("/health").await;
85 assert_eq!(admin.status.as_u16(), 200);
86 assert!(
87 admin.text.contains("System Health"),
88 "admin gets the full health dashboard"
89 );
90 }
91
92 // ── User Suspension ──
93
94 #[tokio::test]
95 async fn admin_suspend_user() {
96 let (mut h, _admin_id) = TestHarness::with_admin().await;
97
98 let user_id = h
99 .signup("susptarget", "susptarget@test.com", "password123")
100 .await;
101
102 // Log in as admin
103 h.client.post_form("/logout", "").await;
104 h.login("admin", "password123").await;
105
106 let resp = h
107 .client
108 .post_form(
109 &format!("/api/admin/users/{}/suspend", *user_id),
110 "reason=Violated+terms+of+service",
111 )
112 .await;
113 assert_eq!(
114 resp.status, 200,
115 "Admin suspend failed: {} {}",
116 resp.status, resp.text
117 );
118
119 // Verify in DB
120 let (suspended, reason): (bool, Option<String>) = sqlx::query_as(
121 "SELECT (suspended_at IS NOT NULL), suspension_reason FROM users WHERE id = $1",
122 )
123 .bind(*user_id)
124 .fetch_one(&h.db)
125 .await
126 .unwrap();
127 assert!(suspended, "User should be suspended");
128 assert_eq!(reason.as_deref(), Some("Violated terms of service"));
129 }
130
131 #[tokio::test]
132 async fn admin_suspend_empty_reason_rejected() {
133 let (mut h, _admin_id) = TestHarness::with_admin().await;
134
135 let user_id = h
136 .signup("suspempty", "suspempty@test.com", "password123")
137 .await;
138
139 h.client.post_form("/logout", "").await;
140 h.login("admin", "password123").await;
141
142 let resp = h
143 .client
144 .post_form(&format!("/api/admin/users/{}/suspend", *user_id), "reason=")
145 .await;
146 assert_eq!(
147 resp.status, 422,
148 "Empty reason should be rejected: {} {}",
149 resp.status, resp.text
150 );
151 }
152
153 #[tokio::test]
154 async fn admin_unsuspend_user() {
155 let (mut h, _admin_id) = TestHarness::with_admin().await;
156
157 let user_id = h
158 .signup("unsusptarget", "unsusptarget@test.com", "password123")
159 .await;
160 h.suspend_user(user_id).await;
161
162 h.client.post_form("/logout", "").await;
163 h.login("admin", "password123").await;
164
165 let resp = h
166 .client
167 .post_form(&format!("/api/admin/users/{}/unsuspend", *user_id), "")
168 .await;
169 assert_eq!(
170 resp.status, 200,
171 "Admin unsuspend failed: {} {}",
172 resp.status, resp.text
173 );
174
175 // Verify cleared
176 let suspended: bool =
177 sqlx::query_scalar("SELECT (suspended_at IS NOT NULL) FROM users WHERE id = $1")
178 .bind(*user_id)
179 .fetch_one(&h.db)
180 .await
181 .unwrap();
182 assert!(!suspended, "User should no longer be suspended");
183 }
184
185 // ── Trust Management ──
186
187 #[tokio::test]
188 async fn admin_trust_user() {
189 let (mut h, _admin_id) = TestHarness::with_admin().await;
190
191 let user_id = h.signup("trustme", "trustme@test.com", "password123").await;
192
193 h.client.post_form("/logout", "").await;
194 h.login("admin", "password123").await;
195
196 let resp = h
197 .client
198 .post_form(&format!("/api/admin/users/{}/trust", *user_id), "")
199 .await;
200 assert_eq!(
201 resp.status, 200,
202 "Admin trust failed: {} {}",
203 resp.status, resp.text
204 );
205
206 let trusted: bool = sqlx::query_scalar("SELECT upload_trusted FROM users WHERE id = $1")
207 .bind(*user_id)
208 .fetch_one(&h.db)
209 .await
210 .unwrap();
211 assert!(trusted, "User should be trusted for uploads");
212 }
213
214 #[tokio::test]
215 async fn admin_untrust_user() {
216 let (mut h, _admin_id) = TestHarness::with_admin().await;
217
218 let user_id = h
219 .signup("untrustme", "untrustme@test.com", "password123")
220 .await;
221 h.trust_user(user_id).await;
222
223 h.client.post_form("/logout", "").await;
224 h.login("admin", "password123").await;
225
226 let resp = h
227 .client
228 .post_form(&format!("/api/admin/users/{}/untrust", *user_id), "")
229 .await;
230 assert_eq!(
231 resp.status, 200,
232 "Admin untrust failed: {} {}",
233 resp.status, resp.text
234 );
235
236 let trusted: bool = sqlx::query_scalar("SELECT upload_trusted FROM users WHERE id = $1")
237 .bind(*user_id)
238 .fetch_one(&h.db)
239 .await
240 .unwrap();
241 assert!(!trusted, "User should no longer be trusted for uploads");
242 }
243
244 // ── Upload Review ──
245
246 /// Helper: create a project and item via SQL, set item to held_for_review.
247 async fn create_held_item(db: &sqlx::PgPool, user_id: UserId) -> uuid::Uuid {
248 let project_id: uuid::Uuid = sqlx::query_scalar(
249 "INSERT INTO projects (user_id, slug, title) VALUES ($1, 'held-proj', 'Held Project') RETURNING id",
250 )
251 .bind(*user_id)
252 .fetch_one(db)
253 .await
254 .unwrap();
255
256 let item_id: uuid::Uuid = sqlx::query_scalar(
257 "INSERT INTO items (project_id, title, price_cents, item_type, scan_status, slug) \
258 VALUES ($1, 'Held Item', 0, 'audio', 'held_for_review', 'held-item-' || $1::text) RETURNING id",
259 )
260 .bind(project_id)
261 .fetch_one(db)
262 .await
263 .unwrap();
264
265 item_id
266 }
267
268 /// Helper: create a version for an item, set to held_for_review.
269 async fn create_held_version(db: &sqlx::PgPool, item_id: uuid::Uuid) -> uuid::Uuid {
270 let version_id: uuid::Uuid = sqlx::query_scalar(
271 "INSERT INTO versions (item_id, version_number, scan_status) \
272 VALUES ($1, '1.0', 'held_for_review') RETURNING id",
273 )
274 .bind(item_id)
275 .fetch_one(db)
276 .await
277 .unwrap();
278
279 version_id
280 }
281
282 #[tokio::test]
283 async fn admin_approve_item_upload() {
284 let (mut h, _admin_id) = TestHarness::with_admin().await;
285
286 let user_id = h
287 .signup("itemapprove", "itemapprove@test.com", "password123")
288 .await;
289 h.grant_creator(user_id).await;
290 let item_id = create_held_item(&h.db, user_id).await;
291
292 h.client.post_form("/logout", "").await;
293 h.login("admin", "password123").await;
294
295 let resp = h
296 .client
297 .post_form(&format!("/api/admin/uploads/items/{item_id}/promote"), "")
298 .await;
299 assert_eq!(
300 resp.status, 200,
301 "Approve item failed: {} {}",
302 resp.status, resp.text
303 );
304
305 let status: String = sqlx::query_scalar("SELECT scan_status FROM items WHERE id = $1")
306 .bind(item_id)
307 .fetch_one(&h.db)
308 .await
309 .unwrap();
310 assert_eq!(status, "clean");
311 }
312
313 #[tokio::test]
314 async fn admin_reject_item_upload() {
315 let (mut h, _admin_id) = TestHarness::with_admin().await;
316
317 let user_id = h
318 .signup("itemreject", "itemreject@test.com", "password123")
319 .await;
320 h.grant_creator(user_id).await;
321 let item_id = create_held_item(&h.db, user_id).await;
322
323 h.client.post_form("/logout", "").await;
324 h.login("admin", "password123").await;
325
326 let resp = h
327 .client
328 .post_form(
329 &format!("/api/admin/uploads/items/{item_id}/quarantine"),
330 "",
331 )
332 .await;
333 assert_eq!(
334 resp.status, 200,
335 "Reject item failed: {} {}",
336 resp.status, resp.text
337 );
338
339 let status: String = sqlx::query_scalar("SELECT scan_status FROM items WHERE id = $1")
340 .bind(item_id)
341 .fetch_one(&h.db)
342 .await
343 .unwrap();
344 assert_eq!(status, "quarantined");
345 }
346
347 #[tokio::test]
348 async fn admin_approve_version_upload() {
349 let (mut h, _admin_id) = TestHarness::with_admin().await;
350
351 let user_id = h
352 .signup("verapprove", "verapprove@test.com", "password123")
353 .await;
354 h.grant_creator(user_id).await;
355 let item_id = create_held_item(&h.db, user_id).await;
356 let version_id = create_held_version(&h.db, item_id).await;
357
358 h.client.post_form("/logout", "").await;
359 h.login("admin", "password123").await;
360
361 let resp = h
362 .client
363 .post_form(
364 &format!("/api/admin/uploads/versions/{version_id}/promote"),
365 "",
366 )
367 .await;
368 assert_eq!(
369 resp.status, 200,
370 "Approve version failed: {} {}",
371 resp.status, resp.text
372 );
373
374 let status: String = sqlx::query_scalar("SELECT scan_status FROM versions WHERE id = $1")
375 .bind(version_id)
376 .fetch_one(&h.db)
377 .await
378 .unwrap();
379 assert_eq!(status, "clean");
380 }
381
382 #[tokio::test]
383 async fn admin_reject_version_upload() {
384 let (mut h, _admin_id) = TestHarness::with_admin().await;
385
386 let user_id = h
387 .signup("verreject", "verreject@test.com", "password123")
388 .await;
389 h.grant_creator(user_id).await;
390 let item_id = create_held_item(&h.db, user_id).await;
391 let version_id = create_held_version(&h.db, item_id).await;
392
393 h.client.post_form("/logout", "").await;
394 h.login("admin", "password123").await;
395
396 let resp = h
397 .client
398 .post_form(
399 &format!("/api/admin/uploads/versions/{version_id}/quarantine"),
400 "",
401 )
402 .await;
403 assert_eq!(
404 resp.status, 200,
405 "Reject version failed: {} {}",
406 resp.status, resp.text
407 );
408
409 let status: String = sqlx::query_scalar("SELECT scan_status FROM versions WHERE id = $1")
410 .bind(version_id)
411 .fetch_one(&h.db)
412 .await
413 .unwrap();
414 assert_eq!(status, "quarantined");
415 }
416
417 // ── Appeal Decisions ──
418
419 #[tokio::test]
420 async fn admin_approve_appeal() {
421 let (mut h, _admin_id) = TestHarness::with_admin().await;
422
423 let user_id = h
424 .signup("appealapprove", "appealapprove@test.com", "password123")
425 .await;
426 h.suspend_user(user_id).await;
427
428 // User submits appeal
429 h.client.post_form("/logout", "").await;
430 h.login("appealapprove", "password123").await;
431 let resp = h
432 .client
433 .post_form(
434 "/api/users/me/appeal",
435 "appeal_text=I+believe+this+was+a+mistake",
436 )
437 .await;
438 assert_eq!(
439 resp.status, 204,
440 "Appeal submission failed: {} {}",
441 resp.status, resp.text
442 );
443
444 // Admin decides
445 h.client.post_form("/logout", "").await;
446 h.login("admin", "password123").await;
447 let resp = h
448 .client
449 .post_form(
450 &format!("/api/admin/appeals/{}/decide", *user_id),
451 "decision=approved&response=Suspension+was+a+mistake",
452 )
453 .await;
454 assert_eq!(
455 resp.status, 200,
456 "Admin approve appeal failed: {} {}",
457 resp.status, resp.text
458 );
459
460 // Verify: user is unsuspended and appeal decision recorded
461 let (suspended, decision): (bool, Option<String>) = sqlx::query_as(
462 "SELECT (suspended_at IS NOT NULL), appeal_decision FROM users WHERE id = $1",
463 )
464 .bind(*user_id)
465 .fetch_one(&h.db)
466 .await
467 .unwrap();
468 assert!(
469 !suspended,
470 "User should be unsuspended after approved appeal"
471 );
472 assert_eq!(decision.as_deref(), Some("approved"));
473 }
474
475 #[tokio::test]
476 async fn admin_deny_appeal() {
477 let (mut h, _admin_id) = TestHarness::with_admin().await;
478
479 let user_id = h
480 .signup("appealdeny", "appealdeny@test.com", "password123")
481 .await;
482 h.suspend_user(user_id).await;
483
484 // User submits appeal
485 h.client.post_form("/logout", "").await;
486 h.login("appealdeny", "password123").await;
487 h.client
488 .post_form(
489 "/api/users/me/appeal",
490 "appeal_text=Please+reconsider+my+case",
491 )
492 .await;
493
494 // Admin denies
495 h.client.post_form("/logout", "").await;
496 h.login("admin", "password123").await;
497 let resp = h
498 .client
499 .post_form(
500 &format!("/api/admin/appeals/{}/decide", *user_id),
501 "decision=denied&response=Violation+confirmed",
502 )
503 .await;
504 assert_eq!(
505 resp.status, 200,
506 "Admin deny appeal failed: {} {}",
507 resp.status, resp.text
508 );
509
510 // Verify: user stays suspended, decision recorded
511 let (suspended, decision): (bool, Option<String>) = sqlx::query_as(
512 "SELECT (suspended_at IS NOT NULL), appeal_decision FROM users WHERE id = $1",
513 )
514 .bind(*user_id)
515 .fetch_one(&h.db)
516 .await
517 .unwrap();
518 assert!(
519 suspended,
520 "User should remain suspended after denied appeal"
521 );
522 assert_eq!(decision.as_deref(), Some("denied"));
523 }
524
525 #[tokio::test]
526 async fn admin_appeal_empty_response_rejected() {
527 let (mut h, _admin_id) = TestHarness::with_admin().await;
528
529 let user_id = h
530 .signup("appealemptyresp", "appealemptyresp@test.com", "password123")
531 .await;
532 h.suspend_user(user_id).await;
533
534 // User submits appeal
535 h.client.post_form("/logout", "").await;
536 h.login("appealemptyresp", "password123").await;
537 h.client
538 .post_form("/api/users/me/appeal", "appeal_text=Please+reconsider")
539 .await;
540
541 // Admin tries to decide with empty response
542 h.client.post_form("/logout", "").await;
543 h.login("admin", "password123").await;
544 let resp = h
545 .client
546 .post_form(
547 &format!("/api/admin/appeals/{}/decide", *user_id),
548 "decision=approved&response=",
549 )
550 .await;
551 assert_eq!(
552 resp.status, 422,
553 "Empty response should be rejected: {} {}",
554 resp.status, resp.text
555 );
556 }
557
558 // ── Non-Admin Access ──
559
560 #[tokio::test]
561 async fn non_admin_suspend_gets_404() {
562 let (mut h, _admin_id) = TestHarness::with_admin().await;
563
564 let user_id = h
565 .signup("nonadminsus", "nonadminsus@test.com", "password123")
566 .await;
567
568 // Regular user tries admin suspend route
569 let resp = h
570 .client
571 .post_form(
572 &format!("/api/admin/users/{}/suspend", *user_id),
573 "reason=hacking",
574 )
575 .await;
576 assert_eq!(
577 resp.status, 404,
578 "Non-admin suspend should be 404, got {} {}",
579 resp.status, resp.text
580 );
581 }
582
583 #[tokio::test]
584 async fn non_admin_upload_review_gets_404() {
585 let (mut h, _admin_id) = TestHarness::with_admin().await;
586
587 let _user_id = h
588 .signup("nonadminupload", "nonadminupload@test.com", "password123")
589 .await;
590
591 let fake_id = uuid::Uuid::new_v4();
592 let resp = h
593 .client
594 .post_form(&format!("/api/admin/uploads/items/{fake_id}/promote"), "")
595 .await;
596 assert_eq!(
597 resp.status, 404,
598 "Non-admin item approve should be 404, got {} {}",
599 resp.status, resp.text
600 );
601 }
602