//! Admin workflow tests: suspend/unsuspend, trust/untrust, upload review, appeal decisions. use crate::harness::TestHarness; use makenotwork::db::UserId; // ── Structural admin gate (Run 20) ── /// The blanket `require_admin_layer` must hide every `/admin*` route from /// anonymous and non-admin callers, regardless of whether the individual /// handler remembers `AdminUser`, except the explicit public health endpoint. #[tokio::test] async fn admin_gate_rejects_anonymous_and_non_admin() { let (mut h, _admin_id) = TestHarness::with_admin().await; h.client.post_form("/logout", "").await; // ensure anonymous // Gated routes: an anonymous caller is rejected before the handler by the // layer's AuthUser check (401 Unauthorized). The invariant that matters is // that no gated /admin route is ever reachable (2xx) without auth. for path in [ "/admin/users", "/admin/uploads", "/admin/metrics", "/admin/reports", "/admin/signups", "/admin/uploads/queue-summary", ] { let resp = h.client.get(path).await; assert_eq!(resp.status.as_u16(), 401, "anonymous must not reach {path}"); } // The one explicit exemption stays public for PoM. let resp = h.client.get("/admin/uploads/health.json").await; assert_eq!( resp.status.as_u16(), 200, "scan-health JSON must stay public" ); // A logged-in non-admin is equally hidden. h.signup("plainuser", "plainuser@test.com", "password123") .await; let resp = h.client.get("/admin/users").await; assert_eq!( resp.status.as_u16(), 404, "non-admin must not reach /admin/users" ); // ...but the public health endpoint is still reachable for them too. let resp = h.client.get("/admin/uploads/health.json").await; assert_eq!( resp.status.as_u16(), 200, "health JSON stays public for non-admins" ); } /// `/health` stays publicly reachable (200) but the live subsystem fan-out is /// admin-only, gated BEFORE the probes run. An anonymous caller gets a minimal /// cached-status page, never the admin dashboard, so an unauthenticated /// request can't amplify into the ~10-op DB/S3/PoM sweep (audit Run 22). #[tokio::test] async fn health_gates_full_dashboard_to_admins() { let (mut h, _admin_id) = TestHarness::with_admin().await; // Anonymous: minimal reachable status page, no admin fan-out content. h.client.post_form("/logout", "").await; let anon = h.client.get("/health").await; assert_eq!( anon.status.as_u16(), 200, "health must stay publicly reachable" ); assert!( anon.text.contains("Makenotwork"), "anon gets the minimal status page" ); assert!( !anon.text.contains("System Health"), "anon must NOT receive the admin fan-out dashboard" ); // Admin: full live dashboard. h.login("admin", "password123").await; let admin = h.client.get("/health").await; assert_eq!(admin.status.as_u16(), 200); assert!( admin.text.contains("System Health"), "admin gets the full health dashboard" ); } // ── User Suspension ── #[tokio::test] async fn admin_suspend_user() { let (mut h, _admin_id) = TestHarness::with_admin().await; let user_id = h .signup("susptarget", "susptarget@test.com", "password123") .await; // Log in as admin h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form( &format!("/api/admin/users/{}/suspend", *user_id), "reason=Violated+terms+of+service", ) .await; assert_eq!( resp.status, 200, "Admin suspend failed: {} {}", resp.status, resp.text ); // Verify in DB let (suspended, reason): (bool, Option) = sqlx::query_as( "SELECT (suspended_at IS NOT NULL), suspension_reason FROM users WHERE id = $1", ) .bind(*user_id) .fetch_one(&h.db) .await .unwrap(); assert!(suspended, "User should be suspended"); assert_eq!(reason.as_deref(), Some("Violated terms of service")); } #[tokio::test] async fn admin_suspend_empty_reason_rejected() { let (mut h, _admin_id) = TestHarness::with_admin().await; let user_id = h .signup("suspempty", "suspempty@test.com", "password123") .await; h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form(&format!("/api/admin/users/{}/suspend", *user_id), "reason=") .await; assert_eq!( resp.status, 422, "Empty reason should be rejected: {} {}", resp.status, resp.text ); } #[tokio::test] async fn admin_unsuspend_user() { let (mut h, _admin_id) = TestHarness::with_admin().await; let user_id = h .signup("unsusptarget", "unsusptarget@test.com", "password123") .await; h.suspend_user(user_id).await; h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form(&format!("/api/admin/users/{}/unsuspend", *user_id), "") .await; assert_eq!( resp.status, 200, "Admin unsuspend failed: {} {}", resp.status, resp.text ); // Verify cleared let suspended: bool = sqlx::query_scalar("SELECT (suspended_at IS NOT NULL) FROM users WHERE id = $1") .bind(*user_id) .fetch_one(&h.db) .await .unwrap(); assert!(!suspended, "User should no longer be suspended"); } // ── Trust Management ── #[tokio::test] async fn admin_trust_user() { let (mut h, _admin_id) = TestHarness::with_admin().await; let user_id = h.signup("trustme", "trustme@test.com", "password123").await; h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form(&format!("/api/admin/users/{}/trust", *user_id), "") .await; assert_eq!( resp.status, 200, "Admin trust failed: {} {}", resp.status, resp.text ); let trusted: bool = sqlx::query_scalar("SELECT upload_trusted FROM users WHERE id = $1") .bind(*user_id) .fetch_one(&h.db) .await .unwrap(); assert!(trusted, "User should be trusted for uploads"); } #[tokio::test] async fn admin_untrust_user() { let (mut h, _admin_id) = TestHarness::with_admin().await; let user_id = h .signup("untrustme", "untrustme@test.com", "password123") .await; h.trust_user(user_id).await; h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form(&format!("/api/admin/users/{}/untrust", *user_id), "") .await; assert_eq!( resp.status, 200, "Admin untrust failed: {} {}", resp.status, resp.text ); let trusted: bool = sqlx::query_scalar("SELECT upload_trusted FROM users WHERE id = $1") .bind(*user_id) .fetch_one(&h.db) .await .unwrap(); assert!(!trusted, "User should no longer be trusted for uploads"); } // ── Upload Review ── /// Helper: create a project and item via SQL, set item to held_for_review. async fn create_held_item(db: &sqlx::PgPool, user_id: UserId) -> uuid::Uuid { let project_id: uuid::Uuid = sqlx::query_scalar( "INSERT INTO projects (user_id, slug, title) VALUES ($1, 'held-proj', 'Held Project') RETURNING id", ) .bind(*user_id) .fetch_one(db) .await .unwrap(); let item_id: uuid::Uuid = sqlx::query_scalar( "INSERT INTO items (project_id, title, price_cents, item_type, scan_status, slug) \ VALUES ($1, 'Held Item', 0, 'audio', 'held_for_review', 'held-item-' || $1::text) RETURNING id", ) .bind(project_id) .fetch_one(db) .await .unwrap(); item_id } /// Helper: create a version for an item, set to held_for_review. async fn create_held_version(db: &sqlx::PgPool, item_id: uuid::Uuid) -> uuid::Uuid { let version_id: uuid::Uuid = sqlx::query_scalar( "INSERT INTO versions (item_id, version_number, scan_status) \ VALUES ($1, '1.0', 'held_for_review') RETURNING id", ) .bind(item_id) .fetch_one(db) .await .unwrap(); version_id } #[tokio::test] async fn admin_approve_item_upload() { let (mut h, _admin_id) = TestHarness::with_admin().await; let user_id = h .signup("itemapprove", "itemapprove@test.com", "password123") .await; h.grant_creator(user_id).await; let item_id = create_held_item(&h.db, user_id).await; h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form(&format!("/api/admin/uploads/items/{item_id}/promote"), "") .await; assert_eq!( resp.status, 200, "Approve item failed: {} {}", resp.status, resp.text ); let status: String = sqlx::query_scalar("SELECT scan_status FROM items WHERE id = $1") .bind(item_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(status, "clean"); } #[tokio::test] async fn admin_reject_item_upload() { let (mut h, _admin_id) = TestHarness::with_admin().await; let user_id = h .signup("itemreject", "itemreject@test.com", "password123") .await; h.grant_creator(user_id).await; let item_id = create_held_item(&h.db, user_id).await; h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form( &format!("/api/admin/uploads/items/{item_id}/quarantine"), "", ) .await; assert_eq!( resp.status, 200, "Reject item failed: {} {}", resp.status, resp.text ); let status: String = sqlx::query_scalar("SELECT scan_status FROM items WHERE id = $1") .bind(item_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(status, "quarantined"); } #[tokio::test] async fn admin_approve_version_upload() { let (mut h, _admin_id) = TestHarness::with_admin().await; let user_id = h .signup("verapprove", "verapprove@test.com", "password123") .await; h.grant_creator(user_id).await; let item_id = create_held_item(&h.db, user_id).await; let version_id = create_held_version(&h.db, item_id).await; h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form( &format!("/api/admin/uploads/versions/{version_id}/promote"), "", ) .await; assert_eq!( resp.status, 200, "Approve version failed: {} {}", resp.status, resp.text ); let status: String = sqlx::query_scalar("SELECT scan_status FROM versions WHERE id = $1") .bind(version_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(status, "clean"); } #[tokio::test] async fn admin_reject_version_upload() { let (mut h, _admin_id) = TestHarness::with_admin().await; let user_id = h .signup("verreject", "verreject@test.com", "password123") .await; h.grant_creator(user_id).await; let item_id = create_held_item(&h.db, user_id).await; let version_id = create_held_version(&h.db, item_id).await; h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form( &format!("/api/admin/uploads/versions/{version_id}/quarantine"), "", ) .await; assert_eq!( resp.status, 200, "Reject version failed: {} {}", resp.status, resp.text ); let status: String = sqlx::query_scalar("SELECT scan_status FROM versions WHERE id = $1") .bind(version_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(status, "quarantined"); } // ── Appeal Decisions ── #[tokio::test] async fn admin_approve_appeal() { let (mut h, _admin_id) = TestHarness::with_admin().await; let user_id = h .signup("appealapprove", "appealapprove@test.com", "password123") .await; h.suspend_user(user_id).await; // User submits appeal h.client.post_form("/logout", "").await; h.login("appealapprove", "password123").await; let resp = h .client .post_form( "/api/users/me/appeal", "appeal_text=I+believe+this+was+a+mistake", ) .await; assert_eq!( resp.status, 204, "Appeal submission failed: {} {}", resp.status, resp.text ); // Admin decides h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form( &format!("/api/admin/appeals/{}/decide", *user_id), "decision=approved&response=Suspension+was+a+mistake", ) .await; assert_eq!( resp.status, 200, "Admin approve appeal failed: {} {}", resp.status, resp.text ); // Verify: user is unsuspended and appeal decision recorded let (suspended, decision): (bool, Option) = sqlx::query_as( "SELECT (suspended_at IS NOT NULL), appeal_decision FROM users WHERE id = $1", ) .bind(*user_id) .fetch_one(&h.db) .await .unwrap(); assert!( !suspended, "User should be unsuspended after approved appeal" ); assert_eq!(decision.as_deref(), Some("approved")); } #[tokio::test] async fn admin_deny_appeal() { let (mut h, _admin_id) = TestHarness::with_admin().await; let user_id = h .signup("appealdeny", "appealdeny@test.com", "password123") .await; h.suspend_user(user_id).await; // User submits appeal h.client.post_form("/logout", "").await; h.login("appealdeny", "password123").await; h.client .post_form( "/api/users/me/appeal", "appeal_text=Please+reconsider+my+case", ) .await; // Admin denies h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form( &format!("/api/admin/appeals/{}/decide", *user_id), "decision=denied&response=Violation+confirmed", ) .await; assert_eq!( resp.status, 200, "Admin deny appeal failed: {} {}", resp.status, resp.text ); // Verify: user stays suspended, decision recorded let (suspended, decision): (bool, Option) = sqlx::query_as( "SELECT (suspended_at IS NOT NULL), appeal_decision FROM users WHERE id = $1", ) .bind(*user_id) .fetch_one(&h.db) .await .unwrap(); assert!( suspended, "User should remain suspended after denied appeal" ); assert_eq!(decision.as_deref(), Some("denied")); } #[tokio::test] async fn admin_appeal_empty_response_rejected() { let (mut h, _admin_id) = TestHarness::with_admin().await; let user_id = h .signup("appealemptyresp", "appealemptyresp@test.com", "password123") .await; h.suspend_user(user_id).await; // User submits appeal h.client.post_form("/logout", "").await; h.login("appealemptyresp", "password123").await; h.client .post_form("/api/users/me/appeal", "appeal_text=Please+reconsider") .await; // Admin tries to decide with empty response h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form( &format!("/api/admin/appeals/{}/decide", *user_id), "decision=approved&response=", ) .await; assert_eq!( resp.status, 422, "Empty response should be rejected: {} {}", resp.status, resp.text ); } // ── Non-Admin Access ── #[tokio::test] async fn non_admin_suspend_gets_404() { let (mut h, _admin_id) = TestHarness::with_admin().await; let user_id = h .signup("nonadminsus", "nonadminsus@test.com", "password123") .await; // Regular user tries admin suspend route let resp = h .client .post_form( &format!("/api/admin/users/{}/suspend", *user_id), "reason=hacking", ) .await; assert_eq!( resp.status, 404, "Non-admin suspend should be 404, got {} {}", resp.status, resp.text ); } #[tokio::test] async fn non_admin_upload_review_gets_404() { let (mut h, _admin_id) = TestHarness::with_admin().await; let _user_id = h .signup("nonadminupload", "nonadminupload@test.com", "password123") .await; let fake_id = uuid::Uuid::new_v4(); let resp = h .client .post_form(&format!("/api/admin/uploads/items/{fake_id}/promote"), "") .await; assert_eq!( resp.status, 404, "Non-admin item approve should be 404, got {} {}", resp.status, resp.text ); }