Skip to main content

max / makenotwork

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