//! Security A+ (ultra-fuzz Run 7): owner-scoping (Sec-M2 defense in depth) on //! the guard-dependent mutating helpers, plus coverage for the previously //! untested admin moderation handlers. use crate::harness::TestHarness; use makenotwork::db; // ── Owner-scoped mutations (Sec-M2): the SQL itself rejects cross-owner writes ── async fn creator_with_project(h: &TestHarness, tag: &str) -> (db::UserId, db::ProjectId) { let user: db::UserId = sqlx::query_scalar( "INSERT INTO users (username, email, password_hash, email_verified) \ VALUES ($1, $2, 'x', true) RETURNING id", ) .bind(format!("idor_{tag}")) .bind(format!("idor_{tag}@test.com")) .fetch_one(&h.db) .await .unwrap(); let project: db::ProjectId = sqlx::query_scalar( "INSERT INTO projects (user_id, slug, title) VALUES ($1, $2, 'P') RETURNING id", ) .bind(user) .bind(format!("idorproj_{tag}")) .fetch_one(&h.db) .await .unwrap(); (user, project) } #[tokio::test] async fn delete_blog_post_is_owner_scoped() { let h = TestHarness::new().await; let (owner_a, _proj_a) = creator_with_project(&h, "blog_a").await; let (owner_b, proj_b) = creator_with_project(&h, "blog_b").await; let post_b: db::BlogPostId = sqlx::query_scalar( "INSERT INTO blog_posts (project_id, author_id, title, slug) \ VALUES ($1, $2, 'B post', 'b-post') RETURNING id", ) .bind(proj_b) .bind(owner_b) .fetch_one(&h.db) .await .unwrap(); // Owner A cannot delete owner B's post, the SQL scope matches no row. let cross = db::blog_posts::delete_blog_post(&h.db, post_b, owner_a) .await .unwrap(); assert!(!cross, "a cross-owner delete must report no row deleted"); let still_there: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM blog_posts WHERE id = $1") .bind(post_b) .fetch_one(&h.db) .await .unwrap(); assert_eq!( still_there, 1, "the post must survive a cross-owner delete attempt" ); // The real owner deletes it. let owned = db::blog_posts::delete_blog_post(&h.db, post_b, owner_b) .await .unwrap(); assert!(owned, "the owner's delete must succeed"); } #[tokio::test] async fn set_item_listed_is_owner_scoped() { let h = TestHarness::new().await; let (owner_a, _proj_a) = creator_with_project(&h, "item_a").await; let (owner_b, proj_b) = creator_with_project(&h, "item_b").await; let item_b: db::ItemId = sqlx::query_scalar( "INSERT INTO items (project_id, title, item_type, price_cents, slug, listed) \ VALUES ($1, 'B item', 'digital', 0, 'b-item', true) RETURNING id", ) .bind(proj_b) .fetch_one(&h.db) .await .unwrap(); // Owner A cannot toggle owner B's item. let cross = db::bundles::set_item_listed(&h.db, item_b, false, owner_a) .await .unwrap(); assert!( !cross, "a cross-owner listed-toggle must report no row updated" ); let listed: bool = sqlx::query_scalar("SELECT listed FROM items WHERE id = $1") .bind(item_b) .fetch_one(&h.db) .await .unwrap(); assert!( listed, "the item's listed flag must be unchanged by a cross-owner attempt" ); // The real owner can. let owned = db::bundles::set_item_listed(&h.db, item_b, false, owner_b) .await .unwrap(); assert!(owned, "the owner's toggle must succeed"); } // ── Admin moderation handlers (previously uncovered) ── #[tokio::test] async fn admin_remove_and_restore_item() { let (mut h, _admin) = TestHarness::with_admin().await; let setup = h.create_creator_with_item("modtarget", "text", 0).await; h.publish_project_and_item(&setup.project_id, &setup.item_id) .await; h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form( &format!("/api/admin/items/{}/remove", setup.item_id), "reason=Infringing+content", ) .await; assert!( resp.status.is_success(), "remove failed: {} {}", resp.status, resp.text ); let (removed, public): (bool, bool) = sqlx::query_as("SELECT removed_by_admin, is_public FROM items WHERE id = $1::uuid") .bind(&setup.item_id) .fetch_one(&h.db) .await .unwrap(); assert!(removed, "item must be flagged removed_by_admin"); assert!(!public, "removed item must be hidden"); let resp = h .client .post_form(&format!("/api/admin/items/{}/restore", setup.item_id), "") .await; assert!( resp.status.is_success(), "restore failed: {} {}", resp.status, resp.text ); let removed_after: bool = sqlx::query_scalar("SELECT removed_by_admin FROM items WHERE id = $1::uuid") .bind(&setup.item_id) .fetch_one(&h.db) .await .unwrap(); assert!(!removed_after, "restore must clear removed_by_admin"); } #[tokio::test] async fn admin_decide_appeal_denied_records_decision() { let (mut h, _admin) = TestHarness::with_admin().await; let target = h .signup("appellant", "appellant@test.com", "password123") .await; // Suspend the user and have them submit an appeal. sqlx::query("UPDATE users SET suspended_at = NOW(), suspension_reason = 'spam' WHERE id = $1") .bind(target) .execute(&h.db) .await .unwrap(); db::users::submit_appeal(&h.db, target, "Please reconsider") .await .unwrap(); let pending = db::users::get_pending_appeals(&h.db).await.unwrap(); assert!( pending.iter().any(|u| u.id == target), "appeal must be pending before decision" ); h.client.post_form("/logout", "").await; h.login("admin", "password123").await; let resp = h .client .post_form( &format!("/api/admin/appeals/{target}/decide"), "decision=denied&response=Upheld+after+review", ) .await; assert!( resp.status.is_success(), "decide failed: {} {}", resp.status, resp.text ); // Decided appeals drop out of the pending queue; denial keeps the suspension. let pending_after = db::users::get_pending_appeals(&h.db).await.unwrap(); assert!( !pending_after.iter().any(|u| u.id == target), "decided appeal must leave the queue" ); let still_suspended: bool = sqlx::query_scalar("SELECT suspended_at IS NOT NULL FROM users WHERE id = $1") .bind(target) .fetch_one(&h.db) .await .unwrap(); assert!(still_suspended, "a denied appeal must keep the suspension"); }