//! Cart workflow tests, toggle, remove, count, PWYW amount update. use crate::harness::TestHarness; use serde_json::Value; /// Helper: create a seller with a published paid item. Returns (seller_user_id, project_id, item_id). /// Logs out the seller afterward. async fn setup_seller_with_item(h: &mut TestHarness, price_cents: i64) -> (String, String, String) { let setup = h .create_creator_with_item("cartseller", "digital", price_cents) .await; h.publish_project_and_item(&setup.project_id, &setup.item_id) .await; let seller_id = setup.user_id.to_string(); h.client.post_form("/logout", "").await; (seller_id, setup.project_id, setup.item_id) } // Toggle #[tokio::test] async fn toggle_cart_adds_and_removes() { let mut h = TestHarness::new().await; let (_, _, item_id) = setup_seller_with_item(&mut h, 500).await; h.signup("cartbuyer", "cartbuyer@test.com", "password123") .await; h.login("cartbuyer", "password123").await; // Add to cart let resp = h .client .post_form(&format!("/api/cart/{item_id}"), "") .await; assert!( resp.status.is_success(), "Toggle add failed: {} {}", resp.status, resp.text ); let data: Value = resp.json(); assert_eq!(data["in_cart"], true, "First toggle should add to cart"); // Toggle again to remove let resp = h .client .post_form(&format!("/api/cart/{item_id}"), "") .await; assert!( resp.status.is_success(), "Toggle remove failed: {} {}", resp.status, resp.text ); let data: Value = resp.json(); assert_eq!( data["in_cart"], false, "Second toggle should remove from cart" ); } #[tokio::test] async fn toggle_cart_own_item_rejected() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("selfcart", "digital", 500).await; h.publish_project_and_item(&setup.project_id, &setup.item_id) .await; // Creator tries to add own item to cart let resp = h .client .post_form(&format!("/api/cart/{}", setup.item_id), "") .await; assert!( resp.status.is_client_error(), "Own item should not be added to cart: {} {}", resp.status, resp.text ); } #[tokio::test] async fn toggle_cart_unpublished_item_rejected() { let mut h = TestHarness::new().await; let setup = h .create_creator_with_item("draftcart", "digital", 500) .await; // Items default to is_public=true; explicitly unpublish h.client .put_form(&format!("/api/items/{}", setup.item_id), "is_public=false") .await; h.client.post_form("/logout", "").await; h.signup("draftbuyer", "draftbuyer@test.com", "password123") .await; h.login("draftbuyer", "password123").await; let resp = h .client .post_form(&format!("/api/cart/{}", setup.item_id), "") .await; assert!( resp.status.is_client_error() || resp.status == 404, "Unpublished item should not be added to cart: {} {}", resp.status, resp.text ); } // Count #[tokio::test] async fn cart_count_empty() { let mut h = TestHarness::new().await; h.signup("emptycount", "emptycount@test.com", "password123") .await; h.login("emptycount", "password123").await; let resp = h.client.get("/api/cart/count").await; assert!( resp.status.is_success(), "Cart count failed: {} {}", resp.status, resp.text ); let data: Value = resp.json(); assert_eq!(data["count"], 0); } #[tokio::test] async fn cart_count_after_add() { let mut h = TestHarness::new().await; let (_, _, item_id) = setup_seller_with_item(&mut h, 500).await; h.signup("countbuyer", "countbuyer@test.com", "password123") .await; h.login("countbuyer", "password123").await; // Add item h.client .post_form(&format!("/api/cart/{item_id}"), "") .await; let resp = h.client.get("/api/cart/count").await; assert!(resp.status.is_success()); let data: Value = resp.json(); assert_eq!(data["count"], 1); } // Remove #[tokio::test] async fn remove_from_cart() { let mut h = TestHarness::new().await; let (_, _, item_id) = setup_seller_with_item(&mut h, 500).await; h.signup("rmbuyer", "rmbuyer@test.com", "password123").await; h.login("rmbuyer", "password123").await; // Add then explicitly remove h.client .post_form(&format!("/api/cart/{item_id}"), "") .await; let resp = h.client.delete(&format!("/api/cart/{item_id}")).await; assert!( resp.status.is_success() || resp.status == 204, "Remove from cart failed: {} {}", resp.status, resp.text ); // Verify count is 0 let resp = h.client.get("/api/cart/count").await; let data: Value = resp.json(); assert_eq!(data["count"], 0); } // PWYW amount #[tokio::test] async fn update_cart_pwyw_amount() { let mut h = TestHarness::new().await; // Create a PWYW item let setup = h.create_creator_with_item("pwywseller", "digital", 0).await; h.client .put_form( &format!("/api/items/{}", setup.item_id), "pwyw_enabled=on&pwyw_min_cents=500", ) .await; h.publish_project_and_item(&setup.project_id, &setup.item_id) .await; h.client.post_form("/logout", "").await; h.signup("pwywbuyer", "pwywbuyer@test.com", "password123") .await; h.login("pwywbuyer", "password123").await; // Add to cart h.client .post_form(&format!("/api/cart/{}", setup.item_id), "") .await; // Update PWYW amount let resp = h .client .put_json( &format!("/api/cart/{}", setup.item_id), r#"{"amount_cents": 1000}"#, ) .await; assert!( resp.status.is_success(), "PWYW amount update failed: {} {}", resp.status, resp.text ); } #[tokio::test] async fn update_cart_pwyw_below_minimum_rejected() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("pwywmin", "digital", 0).await; h.client .put_form( &format!("/api/items/{}", setup.item_id), "pwyw_enabled=on&pwyw_min_cents=500", ) .await; h.publish_project_and_item(&setup.project_id, &setup.item_id) .await; h.client.post_form("/logout", "").await; h.signup("lowbuyer", "lowbuyer@test.com", "password123") .await; h.login("lowbuyer", "password123").await; h.client .post_form(&format!("/api/cart/{}", setup.item_id), "") .await; // Try amount below minimum let resp = h .client .put_json( &format!("/api/cart/{}", setup.item_id), r#"{"amount_cents": 100}"#, ) .await; assert!( resp.status.is_client_error(), "Below-minimum PWYW should be rejected: {} {}", resp.status, resp.text ); } #[tokio::test] async fn update_cart_pwyw_above_cap_rejected() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("pwywcap", "digital", 0).await; h.client .put_form( &format!("/api/items/{}", setup.item_id), "pwyw_enabled=on&pwyw_min_cents=0", ) .await; h.publish_project_and_item(&setup.project_id, &setup.item_id) .await; h.client.post_form("/logout", "").await; h.signup("capbuyer", "capbuyer@test.com", "password123") .await; h.login("capbuyer", "password123").await; h.client .post_form(&format!("/api/cart/{}", setup.item_id), "") .await; // Try amount above $10,000 cap let resp = h .client .put_json( &format!("/api/cart/{}", setup.item_id), r#"{"amount_cents": 1000001}"#, ) .await; assert!( resp.status.is_client_error(), "Above-cap PWYW should be rejected: {} {}", resp.status, resp.text ); } // PERF-2: the item page collapses wishlist/cart/collection-count into one query // (db::items::get_viewer_item_flags). Pin that the combined query reflects each // piece of state and stays scoped to the viewer. #[tokio::test] async fn viewer_item_flags_reflects_wishlist_cart_and_collections() { use makenotwork::db::ItemId; use makenotwork::db::items::get_viewer_item_flags; let mut h = TestHarness::new().await; let setup = h .create_creator_with_item("flagviewer", "digital", 500) .await; let user_id = setup.user_id; let item = ItemId::from(uuid::Uuid::parse_str(&setup.item_id).unwrap()); // Nothing seeded yet. let f = get_viewer_item_flags(&h.db, user_id, item).await.unwrap(); assert!( !f.is_wishlisted && !f.in_cart && f.collection_count == 0, "empty state" ); sqlx::query("INSERT INTO wishlists (user_id, item_id) VALUES ($1, $2::uuid)") .bind(user_id) .bind(&setup.item_id) .execute(&h.db) .await .unwrap(); sqlx::query("INSERT INTO cart_items (user_id, item_id) VALUES ($1, $2::uuid)") .bind(user_id) .bind(&setup.item_id) .execute(&h.db) .await .unwrap(); let collection_id: uuid::Uuid = sqlx::query_scalar( "INSERT INTO collections (user_id, slug, title) VALUES ($1, 'fav', 'Favorites') RETURNING id", ).bind(user_id).fetch_one(&h.db).await.unwrap(); sqlx::query("INSERT INTO collection_items (collection_id, item_id) VALUES ($1, $2::uuid)") .bind(collection_id) .bind(&setup.item_id) .execute(&h.db) .await .unwrap(); let f = get_viewer_item_flags(&h.db, user_id, item).await.unwrap(); assert!(f.is_wishlisted, "wishlist flag"); assert!(f.in_cart, "cart flag"); assert_eq!(f.collection_count, 1, "collection count"); // A different viewer sees none of it (per-viewer scoping). let other = h .signup("flagother", "flagother@test.com", "password123") .await; let f2 = get_viewer_item_flags(&h.db, other, item).await.unwrap(); assert!( !f2.is_wishlisted && !f2.in_cart && f2.collection_count == 0, "other viewer isolated" ); }