//! Adversarial business-logic tests. //! //! Focus: Checkout, purchase, library, promo code, and PWYW boundary abuse. //! Each test attempts to exploit a business-logic flaw. Tests that PASS prove //! the app correctly rejects the exploit. use crate::harness::TestHarness; use makenotwork::db; use serde_json::Value; /// Helper: create a creator with a published paid item ($10) and a published free item. /// Returns (creator_id, project_id, paid_item_id, free_item_id). /// Stays logged in as the creator. async fn setup_creator_with_items(h: &mut TestHarness) -> (db::UserId, String, String, String) { let setup = h.create_creator_with_item("bizseller", "digital", 1000).await; let paid_item_id = setup.item_id; // Create second (free) item in same project let resp = h .client .post_form( &format!("/api/projects/{}/items", setup.project_id), "title=Free+Item&item_type=digital&price_cents=0", ) .await; assert!(resp.status.is_success(), "Create free item failed: {}", resp.text); let free: Value = resp.json(); let free_item_id = free["id"].as_str().unwrap().to_string(); // Publish all h.publish_project_and_item(&setup.project_id, &paid_item_id).await; h.client .put_form(&format!("/api/items/{}", free_item_id), "is_public=true") .await; (setup.user_id, setup.project_id, paid_item_id, free_item_id) } // ============================================================================= // Self-purchase prevention // ============================================================================= /// Vulnerability tested: Creator buys their own item to inflate sales/launder funds. #[tokio::test] async fn self_purchase_blocked() { let mut h = TestHarness::new().await; let (_creator_id, _project_id, paid_item_id, _free_item_id) = setup_creator_with_items(&mut h).await; // Creator tries to checkout their own paid item let resp = h .client .post_form( &format!("/stripe/checkout/{}", paid_item_id), "", ) .await; assert_eq!( resp.status, 400, "Creator should not be able to purchase their own item: {} {}", resp.status, resp.text ); } /// Vulnerability tested: Creator adds their own free item to library to inflate sales count. #[tokio::test] async fn self_claim_free_item_allowed_but_idempotent() { let mut h = TestHarness::new().await; let (_creator_id, _project_id, _paid_item_id, free_item_id) = setup_creator_with_items(&mut h).await; // Creator adds their own free item — this is allowed (they own it anyway) let resp = h .client .post_form(&format!("/api/library/add/{}", free_item_id), "") .await; assert!( resp.status.is_success(), "Adding own free item to library should work: {} {}", resp.status, resp.text ); // Adding again should be idempotent (no error, not double-counted) let resp = h .client .post_form(&format!("/api/library/add/{}", free_item_id), "") .await; assert!( resp.status.is_success(), "Duplicate add should not error: {} {}", resp.status, resp.text ); } // ============================================================================= // Draft/unpublished item abuse // ============================================================================= /// Vulnerability tested: Buyer checks out a draft item that shouldn't be purchasable. #[tokio::test] async fn draft_item_checkout_rejected() { let mut h = TestHarness::new().await; let creator_id = h.signup("draftseller", "draftseller@test.com", "password123").await; h.grant_creator(creator_id).await; h.client.post_form("/logout", "").await; h.login("draftseller", "password123").await; let resp = h .client .post_form("/api/projects", "slug=draft-shop&title=Draft+Shop") .await; let project: Value = resp.json(); let project_id = project["id"].as_str().unwrap(); // Create item but DO NOT publish it let resp = h .client .post_form( &format!("/api/projects/{}/items", project_id), "title=Draft+Item&item_type=digital&price_cents=500", ) .await; let item: Value = resp.json(); let item_id = item["id"].as_str().unwrap(); // Publish the project but NOT the item h.client .put_json( &format!("/api/projects/{}", project_id), r#"{"is_public": true}"#, ) .await; // Switch to buyer h.client.post_form("/logout", "").await; let _buyer_id = h.signup("draftbuyer", "draftbuyer@test.com", "password456").await; // Buyer tries to checkout the draft item let resp = h .client .post_form(&format!("/stripe/checkout/{}", item_id), "") .await; assert!( resp.status.is_client_error(), "Draft item checkout should be rejected: {} {}", resp.status, resp.text ); } /// Vulnerability tested: Buyer claims a draft free item via library-add. #[tokio::test] async fn draft_free_item_library_add_rejected() { let mut h = TestHarness::new().await; let creator_id = h.signup("draftfree", "draftfree@test.com", "password123").await; h.grant_creator(creator_id).await; h.client.post_form("/logout", "").await; h.login("draftfree", "password123").await; let resp = h .client .post_form("/api/projects", "slug=draftfree-shop&title=DraftFree") .await; let project: Value = resp.json(); let project_id = project["id"].as_str().unwrap(); // Create free item, then explicitly unpublish it (items default to public) let resp = h .client .post_form( &format!("/api/projects/{}/items", project_id), "title=Hidden+Free&item_type=digital&price_cents=0", ) .await; let item: Value = resp.json(); let item_id = item["id"].as_str().unwrap(); h.client .put_form(&format!("/api/items/{}", item_id), "is_public=false") .await; // Switch to buyer h.client.post_form("/logout", "").await; let _buyer_id = h.signup("draftfreebuyer", "draftfreebuyer@test.com", "password456").await; // Try to claim the draft free item let resp = h .client .post_form(&format!("/api/library/add/{}", item_id), "") .await; assert!( resp.status.is_client_error(), "Draft free item should not be claimable: {} {}", resp.status, resp.text ); } // ============================================================================= // Free vs paid boundary // ============================================================================= /// Vulnerability tested: Buyer uses checkout endpoint for a free (non-PWYW) item, /// trying to bypass the library-add flow. #[tokio::test] async fn free_item_checkout_rejected() { let mut h = TestHarness::new().await; let (_creator_id, _project_id, _paid_item_id, free_item_id) = setup_creator_with_items(&mut h).await; // Switch to buyer h.client.post_form("/logout", "").await; let _buyer_id = h.signup("freechk", "freechk@test.com", "password456").await; // Try to checkout a free item let resp = h .client .post_form(&format!("/stripe/checkout/{}", free_item_id), "") .await; assert_eq!( resp.status, 400, "Free item checkout should be rejected: {} {}", resp.status, resp.text ); } /// Vulnerability tested: Buyer uses library-add for a paid item, trying to get it free. #[tokio::test] async fn paid_item_library_add_rejected() { let mut h = TestHarness::new().await; let (_creator_id, _project_id, paid_item_id, _free_item_id) = setup_creator_with_items(&mut h).await; // Switch to buyer h.client.post_form("/logout", "").await; let _buyer_id = h.signup("paidlib", "paidlib@test.com", "password456").await; // Try to add paid item to library (free-claim endpoint) let resp = h .client .post_form(&format!("/api/library/add/{}", paid_item_id), "") .await; assert!( resp.status.is_client_error(), "Paid item should not be claimable via library-add: {} {}", resp.status, resp.text ); } // ============================================================================= // Double-purchase prevention // ============================================================================= /// Vulnerability tested: Buyer tries to purchase the same item twice. /// Uses a 100% discount code to complete a free-claim first purchase. #[tokio::test] async fn double_purchase_redirects() { let mut h = TestHarness::new().await; let (_creator_id, _project_id, paid_item_id, _free_item_id) = setup_creator_with_items(&mut h).await; // Create 100% discount code let resp = h .client .post_form( "/api/promo-codes", "code=FREE100&code_purpose=discount&discount_type=percentage&discount_value=100", ) .await; assert!(resp.status.is_success(), "Create promo code failed: {}", resp.text); // Switch to buyer h.client.post_form("/logout", "").await; let _buyer_id = h.signup("doublebuyer", "doublebuyer@test.com", "password456").await; // First purchase with 100% discount → free claim path let resp = h .client .post_form( &format!("/stripe/checkout/{}", paid_item_id), "promo_code=FREE100", ) .await; assert!( resp.status.is_redirection() || resp.status.is_success(), "First purchase should succeed: {} {}", resp.status, resp.text ); // Second purchase attempt → should redirect to item page (already owned) let resp = h .client .post_form( &format!("/stripe/checkout/{}", paid_item_id), "", ) .await; assert!( resp.status.is_redirection(), "Double purchase should redirect: {} {}", resp.status, resp.text ); } // ============================================================================= // Promo code cross-creator abuse // ============================================================================= /// Vulnerability tested: Buyer uses seller A's discount code on seller B's item. /// The code lookup is scoped by seller_id, so it should be "Invalid". #[tokio::test] async fn promo_code_cross_creator_rejected() { let mut h = TestHarness::new().await; // Seller A creates a 100% discount code let seller_a = h.signup("sellera", "sellera@test.com", "password123").await; h.grant_creator(seller_a).await; h.client.post_form("/logout", "").await; h.login("sellera", "password123").await; let resp = h .client .post_form( "/api/promo-codes", "code=STEALME&code_purpose=discount&discount_type=percentage&discount_value=100", ) .await; assert!(resp.status.is_success(), "Create promo code failed: {}", resp.text); // Seller B creates a published paid item h.client.post_form("/logout", "").await; let seller_b = h.signup("sellerb", "sellerb@test.com", "password123").await; h.grant_creator(seller_b).await; h.client.post_form("/logout", "").await; h.login("sellerb", "password123").await; let resp = h .client .post_form("/api/projects", "slug=b-shop&title=B+Shop") .await; let project: Value = resp.json(); let project_id = project["id"].as_str().unwrap(); let resp = h .client .post_form( &format!("/api/projects/{}/items", project_id), "title=B+Item&item_type=digital&price_cents=2000", ) .await; let item: Value = resp.json(); let item_id = item["id"].as_str().unwrap(); h.client .put_json( &format!("/api/projects/{}", project_id), r#"{"is_public": true}"#, ) .await; h.client .put_form(&format!("/api/items/{}", item_id), "is_public=true") .await; // Buyer tries seller A's code on seller B's item h.client.post_form("/logout", "").await; let _buyer_id = h.signup("crossbuyer", "crossbuyer@test.com", "password456").await; let resp = h .client .post_form( &format!("/stripe/checkout/{}", item_id), "promo_code=STEALME", ) .await; assert_eq!( resp.status, 400, "Cross-creator promo code should be rejected: {} {}", resp.status, resp.text ); } // ============================================================================= // Promo code scope abuse // ============================================================================= /// Vulnerability tested: Promo code scoped to item A used on item B (same creator). #[tokio::test] async fn promo_code_wrong_item_scope_rejected() { let mut h = TestHarness::new().await; let (_creator_id, project_id, paid_item_id, _free_item_id) = setup_creator_with_items(&mut h).await; // Create a second paid item let resp = h .client .post_form( &format!("/api/projects/{}/items", project_id), "title=Other+Item&item_type=digital&price_cents=500", ) .await; assert!(resp.status.is_success()); let other: Value = resp.json(); let other_item_id = other["id"].as_str().unwrap(); h.client .put_form(&format!("/api/items/{}", other_item_id), "is_public=true") .await; // Create 100% discount code scoped to the FIRST item let resp = h .client .post_form( "/api/promo-codes", &format!( "code=ITEM1ONLY&code_purpose=discount&discount_type=percentage&discount_value=100&item_id={}", paid_item_id ), ) .await; assert!(resp.status.is_success(), "Create scoped code failed: {}", resp.text); // Buyer uses code on the SECOND item h.client.post_form("/logout", "").await; let _buyer_id = h.signup("scopebuyer", "scopebuyer@test.com", "password456").await; let resp = h .client .post_form( &format!("/stripe/checkout/{}", other_item_id), "promo_code=ITEM1ONLY", ) .await; assert_eq!( resp.status, 400, "Item-scoped code on wrong item should be rejected: {} {}", resp.status, resp.text ); } /// Vulnerability tested: Promo code scoped to project A used on item from project B. #[tokio::test] async fn promo_code_wrong_project_scope_rejected() { let mut h = TestHarness::new().await; // Creator with two projects let creator_id = h.signup("projscope", "projscope@test.com", "password123").await; h.grant_creator(creator_id).await; h.client.post_form("/logout", "").await; h.login("projscope", "password123").await; // Project 1 let resp = h .client .post_form("/api/projects", "slug=proj1-shop&title=Proj1") .await; assert!(resp.status.is_success()); let p1: Value = resp.json(); let project1_id = p1["id"].as_str().unwrap().to_string(); // Project 2 with a paid item let resp = h .client .post_form("/api/projects", "slug=proj2-shop&title=Proj2") .await; assert!(resp.status.is_success()); let p2: Value = resp.json(); let project2_id = p2["id"].as_str().unwrap(); let resp = h .client .post_form( &format!("/api/projects/{}/items", project2_id), "title=P2+Item&item_type=digital&price_cents=800", ) .await; assert!(resp.status.is_success()); let item2: Value = resp.json(); let item2_id = item2["id"].as_str().unwrap(); h.client .put_json( &format!("/api/projects/{}", project2_id), r#"{"is_public": true}"#, ) .await; // Create 100% discount code scoped to project 1 let resp = h .client .post_form( "/api/promo-codes", &format!( "code=PROJ1ONLY&code_purpose=discount&discount_type=percentage&discount_value=100&project_id={}", project1_id ), ) .await; assert!(resp.status.is_success(), "Create project-scoped code failed: {}", resp.text); // Buyer uses code on item from project 2 h.client.post_form("/logout", "").await; let _buyer_id = h.signup("projbuyer", "projbuyer@test.com", "password456").await; let resp = h .client .post_form( &format!("/stripe/checkout/{}", item2_id), "promo_code=PROJ1ONLY", ) .await; assert_eq!( resp.status, 400, "Project-scoped code on wrong project should be rejected: {} {}", resp.status, resp.text ); } // ============================================================================= // Promo code exhaustion // ============================================================================= /// Vulnerability tested: Exhausted promo code (max_uses reached) still accepted. /// Uses the claim endpoint with a free_access code (max_uses=1). #[tokio::test] async fn exhausted_promo_code_rejected() { let mut h = TestHarness::new().await; let creator_id = h.signup("exhseller", "exhseller@test.com", "password123").await; h.grant_creator(creator_id).await; h.client.post_form("/logout", "").await; h.login("exhseller", "password123").await; let resp = h .client .post_form("/api/projects", "slug=exh-shop&title=Exh+Shop") .await; let project: Value = resp.json(); let project_id = project["id"].as_str().unwrap(); let resp = h .client .post_form( &format!("/api/projects/{}/items", project_id), "title=Exh+Item&item_type=digital&price_cents=0", ) .await; let item: Value = resp.json(); let item_id = item["id"].as_str().unwrap(); h.client .put_json( &format!("/api/projects/{}", project_id), r#"{"is_public": true}"#, ) .await; h.client .put_form(&format!("/api/items/{}", item_id), "is_public=true") .await; // Create free_access code with max_uses=1 let resp = h .client .post_form( "/api/promo-codes", &format!("code_purpose=free_access&item_id={}&max_uses=1", item_id), ) .await; assert!(resp.status.is_success(), "Create code failed: {}", resp.text); let code: Value = resp.json(); let key_code = code["code"].as_str().unwrap().to_string(); // Buyer 1 claims successfully h.client.post_form("/logout", "").await; let _buyer1 = h.signup("exhbuyer1", "exhbuyer1@test.com", "password456").await; let resp = h .client .post_form("/api/promo-codes/claim", &format!("code={}", key_code)) .await; assert!( resp.status.is_success(), "First claim should succeed: {} {}", resp.status, resp.text ); // Buyer 2 tries to claim — should be rejected (max_uses exhausted) h.client.post_form("/logout", "").await; let _buyer2 = h.signup("exhbuyer2", "exhbuyer2@test.com", "password456").await; let resp = h .client .post_form("/api/promo-codes/claim", &format!("code={}", key_code)) .await; assert_eq!( resp.status, 400, "Exhausted code should be rejected: {} {}", resp.status, resp.text ); assert!( resp.text.contains("usage limit"), "Error should mention usage limit: {}", resp.text ); } // ============================================================================= // PWYW abuse // ============================================================================= /// Vulnerability tested: PWYW amount below minimum. #[tokio::test] async fn pwyw_below_minimum_rejected() { let mut h = TestHarness::new().await; let creator_id = h.signup("pwyws", "pwyws@test.com", "password123").await; h.grant_creator(creator_id).await; h.client.post_form("/logout", "").await; h.login("pwyws", "password123").await; let resp = h .client .post_form("/api/projects", "slug=pwyw-shop&title=PWYW+Shop") .await; let project: Value = resp.json(); let project_id = project["id"].as_str().unwrap(); // Create PWYW item with min $5 let resp = h .client .post_form( &format!("/api/projects/{}/items", project_id), "title=PWYW+Item&item_type=digital&price_cents=1000&pwyw_enabled=true&pwyw_min_cents=500", ) .await; assert!(resp.status.is_success(), "Create PWYW item failed: {}", resp.text); let item: Value = resp.json(); let item_id = item["id"].as_str().unwrap(); h.client .put_json( &format!("/api/projects/{}", project_id), r#"{"is_public": true}"#, ) .await; h.client .put_form(&format!("/api/items/{}", item_id), "is_public=true") .await; // Switch to buyer h.client.post_form("/logout", "").await; let _buyer_id = h.signup("pwywbuyer", "pwywbuyer@test.com", "password456").await; // Try to pay $1 (below $5 minimum) let resp = h .client .post_form( &format!("/stripe/checkout/{}", item_id), "amount_cents=100", ) .await; assert_eq!( resp.status, 400, "PWYW below minimum should be rejected: {} {}", resp.status, resp.text ); } /// Vulnerability tested: PWYW item submitted without amount_cents. #[tokio::test] async fn pwyw_missing_amount_rejected() { let mut h = TestHarness::new().await; let creator_id = h.signup("pwywm", "pwywm@test.com", "password123").await; h.grant_creator(creator_id).await; h.client.post_form("/logout", "").await; h.login("pwywm", "password123").await; let resp = h .client .post_form("/api/projects", "slug=pwywm-shop&title=PWYWM+Shop") .await; let project: Value = resp.json(); let project_id = project["id"].as_str().unwrap(); let resp = h .client .post_form( &format!("/api/projects/{}/items", project_id), "title=PWYW+Item2&item_type=digital&price_cents=1000&pwyw_enabled=true&pwyw_min_cents=500", ) .await; assert!(resp.status.is_success(), "Create PWYW item failed: {}", resp.text); let item: Value = resp.json(); let item_id = item["id"].as_str().unwrap(); h.client .put_json( &format!("/api/projects/{}", project_id), r#"{"is_public": true}"#, ) .await; h.client .put_form(&format!("/api/items/{}", item_id), "is_public=true") .await; // Switch to buyer h.client.post_form("/logout", "").await; let _buyer_id = h.signup("pwywmbuyer", "pwywmbuyer@test.com", "password456").await; // Submit checkout without amount_cents let resp = h .client .post_form( &format!("/stripe/checkout/{}", item_id), "", ) .await; assert_eq!( resp.status, 400, "PWYW without amount should be rejected: {} {}", resp.status, resp.text ); } // ============================================================================= // Discount applies to list price, not PWYW amount // ============================================================================= /// Verification: Discount code applies to the item's list price, not the buyer's /// chosen PWYW amount. A 100% discount on a PWYW item should make it free /// (the buyer can't inflate the "discounted" amount by choosing a high PWYW price). #[tokio::test] async fn discount_applies_to_list_price_not_pwyw() { let mut h = TestHarness::new().await; let creator_id = h.signup("pwywd", "pwywd@test.com", "password123").await; h.grant_creator(creator_id).await; h.client.post_form("/logout", "").await; h.login("pwywd", "password123").await; let resp = h .client .post_form("/api/projects", "slug=pwywd-shop&title=PWYWD+Shop") .await; let project: Value = resp.json(); let project_id = project["id"].as_str().unwrap(); // PWYW item, list price $10, min $0 let resp = h .client .post_form( &format!("/api/projects/{}/items", project_id), "title=PWYW+Disc&item_type=digital&price_cents=1000&pwyw_enabled=true&pwyw_min_cents=0", ) .await; assert!(resp.status.is_success()); let item: Value = resp.json(); let item_id = item["id"].as_str().unwrap(); h.client .put_json( &format!("/api/projects/{}", project_id), r#"{"is_public": true}"#, ) .await; h.client .put_form(&format!("/api/items/{}", item_id), "is_public=true") .await; // 100% discount code let resp = h .client .post_form( "/api/promo-codes", "code=FULL100&code_purpose=discount&discount_type=percentage&discount_value=100", ) .await; assert!(resp.status.is_success()); // Buyer chooses $50 PWYW, but 100% discount on $10 list price → $0 → free claim h.client.post_form("/logout", "").await; let _buyer_id = h.signup("pwywdbuyer", "pwywdbuyer@test.com", "password456").await; let resp = h .client .post_form( &format!("/stripe/checkout/{}", item_id), "amount_cents=5000&promo_code=FULL100", ) .await; // Should succeed via free-claim path (redirect to /library) assert!( resp.status.is_redirection() || resp.status.is_success(), "100% discount should trigger free claim: {} {}", resp.status, resp.text ); }