Skip to main content

max / makenotwork

17.2 KB · 615 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 (audit Run 22).
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!(
114 resp.status.is_success(),
115 "Admin suspend failed: {} {}",
116 resp.status,
117 resp.text
118 );
119
120 // Verify in DB
121 let (suspended, reason): (bool, Option<String>) = sqlx::query_as(
122 "SELECT (suspended_at IS NOT NULL), suspension_reason FROM users WHERE id = $1",
123 )
124 .bind(*user_id)
125 .fetch_one(&h.db)
126 .await
127 .unwrap();
128 assert!(suspended, "User should be suspended");
129 assert_eq!(reason.as_deref(), Some("Violated terms of service"));
130 }
131
132 #[tokio::test]
133 async fn admin_suspend_empty_reason_rejected() {
134 let (mut h, _admin_id) = TestHarness::with_admin().await;
135
136 let user_id = h
137 .signup("suspempty", "suspempty@test.com", "password123")
138 .await;
139
140 h.client.post_form("/logout", "").await;
141 h.login("admin", "password123").await;
142
143 let resp = h
144 .client
145 .post_form(&format!("/api/admin/users/{}/suspend", *user_id), "reason=")
146 .await;
147 assert!(
148 resp.status.is_client_error(),
149 "Empty reason should be rejected: {} {}",
150 resp.status,
151 resp.text
152 );
153 }
154
155 #[tokio::test]
156 async fn admin_unsuspend_user() {
157 let (mut h, _admin_id) = TestHarness::with_admin().await;
158
159 let user_id = h
160 .signup("unsusptarget", "unsusptarget@test.com", "password123")
161 .await;
162 h.suspend_user(user_id).await;
163
164 h.client.post_form("/logout", "").await;
165 h.login("admin", "password123").await;
166
167 let resp = h
168 .client
169 .post_form(&format!("/api/admin/users/{}/unsuspend", *user_id), "")
170 .await;
171 assert!(
172 resp.status.is_success(),
173 "Admin unsuspend failed: {} {}",
174 resp.status,
175 resp.text
176 );
177
178 // Verify cleared
179 let suspended: bool =
180 sqlx::query_scalar("SELECT (suspended_at IS NOT NULL) FROM users WHERE id = $1")
181 .bind(*user_id)
182 .fetch_one(&h.db)
183 .await
184 .unwrap();
185 assert!(!suspended, "User should no longer be suspended");
186 }
187
188 // ── Trust Management ──
189
190 #[tokio::test]
191 async fn admin_trust_user() {
192 let (mut h, _admin_id) = TestHarness::with_admin().await;
193
194 let user_id = h.signup("trustme", "trustme@test.com", "password123").await;
195
196 h.client.post_form("/logout", "").await;
197 h.login("admin", "password123").await;
198
199 let resp = h
200 .client
201 .post_form(&format!("/api/admin/users/{}/trust", *user_id), "")
202 .await;
203 assert!(
204 resp.status.is_success(),
205 "Admin trust failed: {} {}",
206 resp.status,
207 resp.text
208 );
209
210 let trusted: bool = sqlx::query_scalar("SELECT upload_trusted FROM users WHERE id = $1")
211 .bind(*user_id)
212 .fetch_one(&h.db)
213 .await
214 .unwrap();
215 assert!(trusted, "User should be trusted for uploads");
216 }
217
218 #[tokio::test]
219 async fn admin_untrust_user() {
220 let (mut h, _admin_id) = TestHarness::with_admin().await;
221
222 let user_id = h
223 .signup("untrustme", "untrustme@test.com", "password123")
224 .await;
225 h.trust_user(user_id).await;
226
227 h.client.post_form("/logout", "").await;
228 h.login("admin", "password123").await;
229
230 let resp = h
231 .client
232 .post_form(&format!("/api/admin/users/{}/untrust", *user_id), "")
233 .await;
234 assert!(
235 resp.status.is_success(),
236 "Admin untrust failed: {} {}",
237 resp.status,
238 resp.text
239 );
240
241 let trusted: bool = sqlx::query_scalar("SELECT upload_trusted FROM users WHERE id = $1")
242 .bind(*user_id)
243 .fetch_one(&h.db)
244 .await
245 .unwrap();
246 assert!(!trusted, "User should no longer be trusted for uploads");
247 }
248
249 // ── Upload Review ──
250
251 /// Helper: create a project and item via SQL, set item to held_for_review.
252 async fn create_held_item(db: &sqlx::PgPool, user_id: UserId) -> uuid::Uuid {
253 let project_id: uuid::Uuid = sqlx::query_scalar(
254 "INSERT INTO projects (user_id, slug, title) VALUES ($1, 'held-proj', 'Held Project') RETURNING id",
255 )
256 .bind(*user_id)
257 .fetch_one(db)
258 .await
259 .unwrap();
260
261 let item_id: uuid::Uuid = sqlx::query_scalar(
262 "INSERT INTO items (project_id, title, price_cents, item_type, scan_status, slug) \
263 VALUES ($1, 'Held Item', 0, 'audio', 'held_for_review', 'held-item-' || $1::text) RETURNING id",
264 )
265 .bind(project_id)
266 .fetch_one(db)
267 .await
268 .unwrap();
269
270 item_id
271 }
272
273 /// Helper: create a version for an item, set to held_for_review.
274 async fn create_held_version(db: &sqlx::PgPool, item_id: uuid::Uuid) -> uuid::Uuid {
275 let version_id: uuid::Uuid = sqlx::query_scalar(
276 "INSERT INTO versions (item_id, version_number, scan_status) \
277 VALUES ($1, '1.0', 'held_for_review') RETURNING id",
278 )
279 .bind(item_id)
280 .fetch_one(db)
281 .await
282 .unwrap();
283
284 version_id
285 }
286
287 #[tokio::test]
288 async fn admin_approve_item_upload() {
289 let (mut h, _admin_id) = TestHarness::with_admin().await;
290
291 let user_id = h
292 .signup("itemapprove", "itemapprove@test.com", "password123")
293 .await;
294 h.grant_creator(user_id).await;
295 let item_id = create_held_item(&h.db, user_id).await;
296
297 h.client.post_form("/logout", "").await;
298 h.login("admin", "password123").await;
299
300 let resp = h
301 .client
302 .post_form(&format!("/api/admin/uploads/items/{item_id}/promote"), "")
303 .await;
304 assert!(
305 resp.status.is_success(),
306 "Approve item failed: {} {}",
307 resp.status,
308 resp.text
309 );
310
311 let status: String = sqlx::query_scalar("SELECT scan_status FROM items WHERE id = $1")
312 .bind(item_id)
313 .fetch_one(&h.db)
314 .await
315 .unwrap();
316 assert_eq!(status, "clean");
317 }
318
319 #[tokio::test]
320 async fn admin_reject_item_upload() {
321 let (mut h, _admin_id) = TestHarness::with_admin().await;
322
323 let user_id = h
324 .signup("itemreject", "itemreject@test.com", "password123")
325 .await;
326 h.grant_creator(user_id).await;
327 let item_id = create_held_item(&h.db, user_id).await;
328
329 h.client.post_form("/logout", "").await;
330 h.login("admin", "password123").await;
331
332 let resp = h
333 .client
334 .post_form(
335 &format!("/api/admin/uploads/items/{item_id}/quarantine"),
336 "",
337 )
338 .await;
339 assert!(
340 resp.status.is_success(),
341 "Reject item failed: {} {}",
342 resp.status,
343 resp.text
344 );
345
346 let status: String = sqlx::query_scalar("SELECT scan_status FROM items WHERE id = $1")
347 .bind(item_id)
348 .fetch_one(&h.db)
349 .await
350 .unwrap();
351 assert_eq!(status, "quarantined");
352 }
353
354 #[tokio::test]
355 async fn admin_approve_version_upload() {
356 let (mut h, _admin_id) = TestHarness::with_admin().await;
357
358 let user_id = h
359 .signup("verapprove", "verapprove@test.com", "password123")
360 .await;
361 h.grant_creator(user_id).await;
362 let item_id = create_held_item(&h.db, user_id).await;
363 let version_id = create_held_version(&h.db, item_id).await;
364
365 h.client.post_form("/logout", "").await;
366 h.login("admin", "password123").await;
367
368 let resp = h
369 .client
370 .post_form(
371 &format!("/api/admin/uploads/versions/{version_id}/promote"),
372 "",
373 )
374 .await;
375 assert!(
376 resp.status.is_success(),
377 "Approve version failed: {} {}",
378 resp.status,
379 resp.text
380 );
381
382 let status: String = sqlx::query_scalar("SELECT scan_status FROM versions WHERE id = $1")
383 .bind(version_id)
384 .fetch_one(&h.db)
385 .await
386 .unwrap();
387 assert_eq!(status, "clean");
388 }
389
390 #[tokio::test]
391 async fn admin_reject_version_upload() {
392 let (mut h, _admin_id) = TestHarness::with_admin().await;
393
394 let user_id = h
395 .signup("verreject", "verreject@test.com", "password123")
396 .await;
397 h.grant_creator(user_id).await;
398 let item_id = create_held_item(&h.db, user_id).await;
399 let version_id = create_held_version(&h.db, item_id).await;
400
401 h.client.post_form("/logout", "").await;
402 h.login("admin", "password123").await;
403
404 let resp = h
405 .client
406 .post_form(
407 &format!("/api/admin/uploads/versions/{version_id}/quarantine"),
408 "",
409 )
410 .await;
411 assert!(
412 resp.status.is_success(),
413 "Reject version failed: {} {}",
414 resp.status,
415 resp.text
416 );
417
418 let status: String = sqlx::query_scalar("SELECT scan_status FROM versions WHERE id = $1")
419 .bind(version_id)
420 .fetch_one(&h.db)
421 .await
422 .unwrap();
423 assert_eq!(status, "quarantined");
424 }
425
426 // ── Appeal Decisions ──
427
428 #[tokio::test]
429 async fn admin_approve_appeal() {
430 let (mut h, _admin_id) = TestHarness::with_admin().await;
431
432 let user_id = h
433 .signup("appealapprove", "appealapprove@test.com", "password123")
434 .await;
435 h.suspend_user(user_id).await;
436
437 // User submits appeal
438 h.client.post_form("/logout", "").await;
439 h.login("appealapprove", "password123").await;
440 let resp = h
441 .client
442 .post_form(
443 "/api/users/me/appeal",
444 "appeal_text=I+believe+this+was+a+mistake",
445 )
446 .await;
447 assert!(
448 resp.status.is_success() || resp.status == 204,
449 "Appeal submission failed: {} {}",
450 resp.status,
451 resp.text
452 );
453
454 // Admin decides
455 h.client.post_form("/logout", "").await;
456 h.login("admin", "password123").await;
457 let resp = h
458 .client
459 .post_form(
460 &format!("/api/admin/appeals/{}/decide", *user_id),
461 "decision=approved&response=Suspension+was+a+mistake",
462 )
463 .await;
464 assert!(
465 resp.status.is_success(),
466 "Admin approve appeal failed: {} {}",
467 resp.status,
468 resp.text
469 );
470
471 // Verify: user is unsuspended and appeal decision recorded
472 let (suspended, decision): (bool, Option<String>) = sqlx::query_as(
473 "SELECT (suspended_at IS NOT NULL), appeal_decision FROM users WHERE id = $1",
474 )
475 .bind(*user_id)
476 .fetch_one(&h.db)
477 .await
478 .unwrap();
479 assert!(
480 !suspended,
481 "User should be unsuspended after approved appeal"
482 );
483 assert_eq!(decision.as_deref(), Some("approved"));
484 }
485
486 #[tokio::test]
487 async fn admin_deny_appeal() {
488 let (mut h, _admin_id) = TestHarness::with_admin().await;
489
490 let user_id = h
491 .signup("appealdeny", "appealdeny@test.com", "password123")
492 .await;
493 h.suspend_user(user_id).await;
494
495 // User submits appeal
496 h.client.post_form("/logout", "").await;
497 h.login("appealdeny", "password123").await;
498 h.client
499 .post_form(
500 "/api/users/me/appeal",
501 "appeal_text=Please+reconsider+my+case",
502 )
503 .await;
504
505 // Admin denies
506 h.client.post_form("/logout", "").await;
507 h.login("admin", "password123").await;
508 let resp = h
509 .client
510 .post_form(
511 &format!("/api/admin/appeals/{}/decide", *user_id),
512 "decision=denied&response=Violation+confirmed",
513 )
514 .await;
515 assert!(
516 resp.status.is_success(),
517 "Admin deny appeal failed: {} {}",
518 resp.status,
519 resp.text
520 );
521
522 // Verify: user stays suspended, decision recorded
523 let (suspended, decision): (bool, Option<String>) = sqlx::query_as(
524 "SELECT (suspended_at IS NOT NULL), appeal_decision FROM users WHERE id = $1",
525 )
526 .bind(*user_id)
527 .fetch_one(&h.db)
528 .await
529 .unwrap();
530 assert!(
531 suspended,
532 "User should remain suspended after denied appeal"
533 );
534 assert_eq!(decision.as_deref(), Some("denied"));
535 }
536
537 #[tokio::test]
538 async fn admin_appeal_empty_response_rejected() {
539 let (mut h, _admin_id) = TestHarness::with_admin().await;
540
541 let user_id = h
542 .signup("appealemptyresp", "appealemptyresp@test.com", "password123")
543 .await;
544 h.suspend_user(user_id).await;
545
546 // User submits appeal
547 h.client.post_form("/logout", "").await;
548 h.login("appealemptyresp", "password123").await;
549 h.client
550 .post_form("/api/users/me/appeal", "appeal_text=Please+reconsider")
551 .await;
552
553 // Admin tries to decide with empty response
554 h.client.post_form("/logout", "").await;
555 h.login("admin", "password123").await;
556 let resp = h
557 .client
558 .post_form(
559 &format!("/api/admin/appeals/{}/decide", *user_id),
560 "decision=approved&response=",
561 )
562 .await;
563 assert!(
564 resp.status.is_client_error(),
565 "Empty response should be rejected: {} {}",
566 resp.status,
567 resp.text
568 );
569 }
570
571 // ── Non-Admin Access ──
572
573 #[tokio::test]
574 async fn non_admin_suspend_gets_404() {
575 let (mut h, _admin_id) = TestHarness::with_admin().await;
576
577 let user_id = h
578 .signup("nonadminsus", "nonadminsus@test.com", "password123")
579 .await;
580
581 // Regular user tries admin suspend route
582 let resp = h
583 .client
584 .post_form(
585 &format!("/api/admin/users/{}/suspend", *user_id),
586 "reason=hacking",
587 )
588 .await;
589 assert_eq!(
590 resp.status, 404,
591 "Non-admin suspend should be 404, got {} {}",
592 resp.status, resp.text
593 );
594 }
595
596 #[tokio::test]
597 async fn non_admin_upload_review_gets_404() {
598 let (mut h, _admin_id) = TestHarness::with_admin().await;
599
600 let _user_id = h
601 .signup("nonadminupload", "nonadminupload@test.com", "password123")
602 .await;
603
604 let fake_id = uuid::Uuid::new_v4();
605 let resp = h
606 .client
607 .post_form(&format!("/api/admin/uploads/items/{fake_id}/promote"), "")
608 .await;
609 assert_eq!(
610 resp.status, 404,
611 "Non-admin item approve should be 404, got {} {}",
612 resp.status, resp.text
613 );
614 }
615