Skip to main content

max / makenotwork

10.0 KB · 346 lines History Blame Raw
1 //! Cart workflow tests, toggle, remove, count, PWYW amount update.
2
3 use crate::harness::TestHarness;
4 use serde_json::Value;
5
6 /// Helper: create a seller with a published paid item. Returns (seller_user_id, project_id, item_id).
7 /// Logs out the seller afterward.
8 async fn setup_seller_with_item(h: &mut TestHarness, price_cents: i64) -> (String, String, String) {
9 let setup = h
10 .create_creator_with_item("cartseller", "digital", price_cents)
11 .await;
12 h.publish_project_and_item(&setup.project_id, &setup.item_id)
13 .await;
14 let seller_id = setup.user_id.to_string();
15 h.client.post_form("/logout", "").await;
16 (seller_id, setup.project_id, setup.item_id)
17 }
18
19 // Toggle
20
21 #[tokio::test]
22 async fn toggle_cart_adds_and_removes() {
23 let mut h = TestHarness::new().await;
24 let (_, _, item_id) = setup_seller_with_item(&mut h, 500).await;
25
26 h.signup("cartbuyer", "cartbuyer@test.com", "password123")
27 .await;
28 h.login("cartbuyer", "password123").await;
29
30 // Add to cart
31 let resp = h
32 .client
33 .post_form(&format!("/api/cart/{item_id}"), "")
34 .await;
35 assert_eq!(
36 resp.status, 200,
37 "Toggle add failed: {} {}",
38 resp.status, resp.text
39 );
40 let data: Value = resp.json();
41 assert_eq!(data["in_cart"], true, "First toggle should add to cart");
42
43 // Toggle again to remove
44 let resp = h
45 .client
46 .post_form(&format!("/api/cart/{item_id}"), "")
47 .await;
48 assert_eq!(
49 resp.status, 200,
50 "Toggle remove failed: {} {}",
51 resp.status, resp.text
52 );
53 let data: Value = resp.json();
54 assert_eq!(
55 data["in_cart"], false,
56 "Second toggle should remove from cart"
57 );
58 }
59
60 #[tokio::test]
61 async fn toggle_cart_own_item_rejected() {
62 let mut h = TestHarness::new().await;
63 let setup = h.create_creator_with_item("selfcart", "digital", 500).await;
64 h.publish_project_and_item(&setup.project_id, &setup.item_id)
65 .await;
66
67 // Creator tries to add own item to cart
68 let resp = h
69 .client
70 .post_form(&format!("/api/cart/{}", setup.item_id), "")
71 .await;
72 assert_eq!(
73 resp.status, 400,
74 "Own item should not be added to cart: {} {}",
75 resp.status, resp.text
76 );
77 }
78
79 #[tokio::test]
80 async fn toggle_cart_unpublished_item_rejected() {
81 let mut h = TestHarness::new().await;
82 let setup = h
83 .create_creator_with_item("draftcart", "digital", 500)
84 .await;
85 // Items default to is_public=true; explicitly unpublish
86 h.client
87 .put_form(&format!("/api/items/{}", setup.item_id), "is_public=false")
88 .await;
89 h.client.post_form("/logout", "").await;
90
91 h.signup("draftbuyer", "draftbuyer@test.com", "password123")
92 .await;
93 h.login("draftbuyer", "password123").await;
94
95 let resp = h
96 .client
97 .post_form(&format!("/api/cart/{}", setup.item_id), "")
98 .await;
99 assert_eq!(
100 resp.status, 404,
101 "Unpublished item should not be added to cart: {} {}",
102 resp.status, resp.text
103 );
104 }
105
106 // Count
107
108 #[tokio::test]
109 async fn cart_count_empty() {
110 let mut h = TestHarness::new().await;
111 h.signup("emptycount", "emptycount@test.com", "password123")
112 .await;
113 h.login("emptycount", "password123").await;
114
115 let resp = h.client.get("/api/cart/count").await;
116 assert_eq!(
117 resp.status, 200,
118 "Cart count failed: {} {}",
119 resp.status, resp.text
120 );
121 let data: Value = resp.json();
122 assert_eq!(data["count"], 0);
123 }
124
125 #[tokio::test]
126 async fn cart_count_after_add() {
127 let mut h = TestHarness::new().await;
128 let (_, _, item_id) = setup_seller_with_item(&mut h, 500).await;
129
130 h.signup("countbuyer", "countbuyer@test.com", "password123")
131 .await;
132 h.login("countbuyer", "password123").await;
133
134 // Add item
135 h.client
136 .post_form(&format!("/api/cart/{item_id}"), "")
137 .await;
138
139 let resp = h.client.get("/api/cart/count").await;
140 assert_eq!(resp.status, 200, "{}", resp.text);
141 let data: Value = resp.json();
142 assert_eq!(data["count"], 1);
143 }
144
145 // Remove
146
147 #[tokio::test]
148 async fn remove_from_cart() {
149 let mut h = TestHarness::new().await;
150 let (_, _, item_id) = setup_seller_with_item(&mut h, 500).await;
151
152 h.signup("rmbuyer", "rmbuyer@test.com", "password123").await;
153 h.login("rmbuyer", "password123").await;
154
155 // Add then explicitly remove
156 h.client
157 .post_form(&format!("/api/cart/{item_id}"), "")
158 .await;
159 let resp = h.client.delete(&format!("/api/cart/{item_id}")).await;
160 assert_eq!(
161 resp.status, 204,
162 "Remove from cart failed: {} {}",
163 resp.status, resp.text
164 );
165
166 // Verify count is 0
167 let resp = h.client.get("/api/cart/count").await;
168 let data: Value = resp.json();
169 assert_eq!(data["count"], 0);
170 }
171
172 // PWYW amount
173
174 #[tokio::test]
175 async fn update_cart_pwyw_amount() {
176 let mut h = TestHarness::new().await;
177 // Create a PWYW item
178 let setup = h.create_creator_with_item("pwywseller", "digital", 0).await;
179 h.client
180 .put_form(
181 &format!("/api/items/{}", setup.item_id),
182 "pwyw_enabled=on&pwyw_min_cents=500",
183 )
184 .await;
185 h.publish_project_and_item(&setup.project_id, &setup.item_id)
186 .await;
187 h.client.post_form("/logout", "").await;
188
189 h.signup("pwywbuyer", "pwywbuyer@test.com", "password123")
190 .await;
191 h.login("pwywbuyer", "password123").await;
192
193 // Add to cart
194 h.client
195 .post_form(&format!("/api/cart/{}", setup.item_id), "")
196 .await;
197
198 // Update PWYW amount
199 let resp = h
200 .client
201 .put_json(
202 &format!("/api/cart/{}", setup.item_id),
203 r#"{"amount_cents": 1000}"#,
204 )
205 .await;
206 assert_eq!(
207 resp.status, 200,
208 "PWYW amount update failed: {} {}",
209 resp.status, resp.text
210 );
211 }
212
213 #[tokio::test]
214 async fn update_cart_pwyw_below_minimum_rejected() {
215 let mut h = TestHarness::new().await;
216 let setup = h.create_creator_with_item("pwywmin", "digital", 0).await;
217 h.client
218 .put_form(
219 &format!("/api/items/{}", setup.item_id),
220 "pwyw_enabled=on&pwyw_min_cents=500",
221 )
222 .await;
223 h.publish_project_and_item(&setup.project_id, &setup.item_id)
224 .await;
225 h.client.post_form("/logout", "").await;
226
227 h.signup("lowbuyer", "lowbuyer@test.com", "password123")
228 .await;
229 h.login("lowbuyer", "password123").await;
230
231 h.client
232 .post_form(&format!("/api/cart/{}", setup.item_id), "")
233 .await;
234
235 // Try amount below minimum
236 let resp = h
237 .client
238 .put_json(
239 &format!("/api/cart/{}", setup.item_id),
240 r#"{"amount_cents": 100}"#,
241 )
242 .await;
243 assert_eq!(
244 resp.status, 400,
245 "Below-minimum PWYW should be rejected: {} {}",
246 resp.status, resp.text
247 );
248 }
249
250 #[tokio::test]
251 async fn update_cart_pwyw_above_cap_rejected() {
252 let mut h = TestHarness::new().await;
253 let setup = h.create_creator_with_item("pwywcap", "digital", 0).await;
254 h.client
255 .put_form(
256 &format!("/api/items/{}", setup.item_id),
257 "pwyw_enabled=on&pwyw_min_cents=0",
258 )
259 .await;
260 h.publish_project_and_item(&setup.project_id, &setup.item_id)
261 .await;
262 h.client.post_form("/logout", "").await;
263
264 h.signup("capbuyer", "capbuyer@test.com", "password123")
265 .await;
266 h.login("capbuyer", "password123").await;
267
268 h.client
269 .post_form(&format!("/api/cart/{}", setup.item_id), "")
270 .await;
271
272 // Try amount above $10,000 cap
273 let resp = h
274 .client
275 .put_json(
276 &format!("/api/cart/{}", setup.item_id),
277 r#"{"amount_cents": 1000001}"#,
278 )
279 .await;
280 assert_eq!(
281 resp.status, 400,
282 "Above-cap PWYW should be rejected: {} {}",
283 resp.status, resp.text
284 );
285 }
286
287 // PERF-2: the item page collapses wishlist/cart/collection-count into one query
288 // (db::items::get_viewer_item_flags). Pin that the combined query reflects each
289 // piece of state and stays scoped to the viewer.
290 #[tokio::test]
291 async fn viewer_item_flags_reflects_wishlist_cart_and_collections() {
292 use makenotwork::db::ItemId;
293 use makenotwork::db::items::get_viewer_item_flags;
294
295 let mut h = TestHarness::new().await;
296 let setup = h
297 .create_creator_with_item("flagviewer", "digital", 500)
298 .await;
299 let user_id = setup.user_id;
300 let item = ItemId::from(uuid::Uuid::parse_str(&setup.item_id).unwrap());
301
302 // Nothing seeded yet.
303 let f = get_viewer_item_flags(&h.db, user_id, item).await.unwrap();
304 assert!(
305 !f.is_wishlisted && !f.in_cart && f.collection_count == 0,
306 "empty state"
307 );
308
309 sqlx::query("INSERT INTO wishlists (user_id, item_id) VALUES ($1, $2::uuid)")
310 .bind(user_id)
311 .bind(&setup.item_id)
312 .execute(&h.db)
313 .await
314 .unwrap();
315 sqlx::query("INSERT INTO cart_items (user_id, item_id) VALUES ($1, $2::uuid)")
316 .bind(user_id)
317 .bind(&setup.item_id)
318 .execute(&h.db)
319 .await
320 .unwrap();
321 let collection_id: uuid::Uuid = sqlx::query_scalar(
322 "INSERT INTO collections (user_id, slug, title) VALUES ($1, 'fav', 'Favorites') RETURNING id",
323 ).bind(user_id).fetch_one(&h.db).await.unwrap();
324 sqlx::query("INSERT INTO collection_items (collection_id, item_id) VALUES ($1, $2::uuid)")
325 .bind(collection_id)
326 .bind(&setup.item_id)
327 .execute(&h.db)
328 .await
329 .unwrap();
330
331 let f = get_viewer_item_flags(&h.db, user_id, item).await.unwrap();
332 assert!(f.is_wishlisted, "wishlist flag");
333 assert!(f.in_cart, "cart flag");
334 assert_eq!(f.collection_count, 1, "collection count");
335
336 // A different viewer sees none of it (per-viewer scoping).
337 let other = h
338 .signup("flagother", "flagother@test.com", "password123")
339 .await;
340 let f2 = get_viewer_item_flags(&h.db, other, item).await.unwrap();
341 assert!(
342 !f2.is_wishlisted && !f2.in_cart && f2.collection_count == 0,
343 "other viewer isolated"
344 );
345 }
346