Skip to main content

max / makenotwork

12.7 KB · 426 lines History Blame Raw
1 //! Suspension enforcement tests.
2 //!
3 //! Verifies that suspended users are blocked from checkout, promo code,
4 //! and license key operations. Also tests that suspension applied after
5 //! login takes effect immediately via the session refresh mechanism.
6
7 use crate::harness::TestHarness;
8 use makenotwork::db;
9 use serde_json::Value;
10
11 // CRITICAL: Suspended user checkout blocked
12
13 /// Suspended user tries to initiate a purchase checkout.
14 #[tokio::test]
15 async fn suspended_user_checkout_blocked() {
16 let mut h = TestHarness::new().await;
17
18 // Create a creator with a published paid item
19 let setup = h
20 .create_creator_with_item("chkseller", "digital", 1000)
21 .await;
22 h.publish_project_and_item(&setup.project_id, &setup.item_id)
23 .await;
24
25 // Create a buyer, then suspend them
26 h.client.post_form("/logout", "").await;
27 let buyer_id = h
28 .signup("chkbuyer", "chkbuyer@test.com", "password123")
29 .await;
30
31 db::users::suspend_user(&h.db, buyer_id, "test suspension")
32 .await
33 .unwrap();
34
35 // Re-login to pick up suspended state
36 h.client.post_form("/logout", "").await;
37 h.login("chkbuyer", "password123").await;
38
39 // Try to checkout, should be blocked
40 let resp = h
41 .client
42 .post_form(&format!("/stripe/checkout/{}", setup.item_id), "")
43 .await;
44 assert_eq!(
45 resp.status, 403,
46 "Suspended user should not be able to checkout: {} {}",
47 resp.status, resp.text
48 );
49 }
50
51 /// Active (non-suspended) user can initiate checkout normally.
52 #[tokio::test]
53 async fn active_user_checkout_proceeds() {
54 let mut h = TestHarness::new().await;
55
56 // Create a creator with a published paid item
57 let setup = h
58 .create_creator_with_item("chkseller2", "digital", 1000)
59 .await;
60 h.publish_project_and_item(&setup.project_id, &setup.item_id)
61 .await;
62
63 // Create a normal buyer (not suspended)
64 h.client.post_form("/logout", "").await;
65 let _buyer_id = h
66 .signup("chkbuyer2", "chkbuyer2@test.com", "password123")
67 .await;
68
69 // Try to checkout, should proceed past the suspension check.
70 // It will fail later (no Stripe configured) but NOT with 403.
71 let resp = h
72 .client
73 .post_form(&format!("/stripe/checkout/{}", setup.item_id), "")
74 .await;
75 assert_ne!(
76 resp.status, 403,
77 "Active user should not get 403 on checkout: {} {}",
78 resp.status, resp.text
79 );
80 }
81
82 // HIGH: Suspended user promo code operations blocked
83
84 /// Suspended user tries to create a promo code.
85 #[tokio::test]
86 async fn suspended_user_create_promo_code_blocked() {
87 let mut h = TestHarness::new().await;
88
89 let creator_id = h.create_creator("promoseller").await;
90
91 db::users::suspend_user(&h.db, creator_id, "test suspension")
92 .await
93 .unwrap();
94
95 // Re-login to pick up suspended state
96 h.client.post_form("/logout", "").await;
97 h.login("promoseller", "password123").await;
98
99 // Try to create a promo code
100 let resp = h
101 .client
102 .post_form(
103 "/api/promo-codes",
104 "code=TESTCODE&code_purpose=discount&discount_type=percentage&discount_value=50",
105 )
106 .await;
107 assert_eq!(
108 resp.status, 403,
109 "Suspended user should not create promo codes: {} {}",
110 resp.status, resp.text
111 );
112 }
113
114 /// Suspended user tries to claim a promo code.
115 #[tokio::test]
116 async fn suspended_user_claim_promo_code_blocked() {
117 let mut h = TestHarness::new().await;
118
119 // Create a creator with a published item and a free_access code
120 let _creator_id = h.create_creator("claimseller").await;
121 let resp = h
122 .client
123 .post_form("/api/projects", "slug=claim-shop&title=Claim+Shop")
124 .await;
125 assert_eq!(resp.status, 200, "{}", resp.text);
126 let project: Value = resp.json();
127 let project_id = project["id"].as_str().unwrap();
128
129 let resp = h
130 .client
131 .post_form(
132 &format!("/api/projects/{project_id}/items"),
133 "title=Claim+Item&item_type=digital&price_cents=0",
134 )
135 .await;
136 assert_eq!(resp.status, 200, "{}", resp.text);
137 let item: Value = resp.json();
138 let item_id = item["id"].as_str().unwrap();
139
140 h.client
141 .put_json(
142 &format!("/api/projects/{project_id}"),
143 r#"{"is_public": true}"#,
144 )
145 .await;
146 h.client
147 .put_form(&format!("/api/items/{item_id}"), "is_public=true")
148 .await;
149
150 let resp = h
151 .client
152 .post_form(
153 "/api/promo-codes",
154 &format!("code_purpose=free_access&item_id={item_id}"),
155 )
156 .await;
157 assert_eq!(resp.status, 200, "{}", resp.text);
158 let code: Value = resp.json();
159 let key_code = code["code"].as_str().unwrap().to_string();
160
161 // Create a buyer and suspend them
162 h.client.post_form("/logout", "").await;
163 let buyer_id = h
164 .signup("claimbuyer", "claimbuyer@test.com", "password123")
165 .await;
166 db::users::suspend_user(&h.db, buyer_id, "test suspension")
167 .await
168 .unwrap();
169 h.client.post_form("/logout", "").await;
170 h.login("claimbuyer", "password123").await;
171
172 // Try to claim the promo code
173 let resp = h
174 .client
175 .post_form("/api/promo-codes/claim", &format!("code={key_code}"))
176 .await;
177 assert_eq!(
178 resp.status, 403,
179 "Suspended user should not claim promo codes: {} {}",
180 resp.status, resp.text
181 );
182 }
183
184 /// Suspended user tries to delete a promo code they created before suspension.
185 #[tokio::test]
186 async fn suspended_user_delete_promo_code_blocked() {
187 let mut h = TestHarness::new().await;
188
189 // Create a creator with a promo code
190 let creator_id = h.create_creator("delseller").await;
191
192 let resp = h
193 .client
194 .post_form(
195 "/api/promo-codes",
196 "code=DELCODE&code_purpose=discount&discount_type=percentage&discount_value=50",
197 )
198 .await;
199 assert_eq!(resp.status, 200, "{}", resp.text);
200 let code: Value = resp.json();
201 let code_id = code["id"].as_str().unwrap().to_string();
202
203 db::users::suspend_user(&h.db, creator_id, "test suspension")
204 .await
205 .unwrap();
206 h.client.post_form("/logout", "").await;
207 h.login("delseller", "password123").await;
208
209 // Try to delete the promo code
210 let resp = h
211 .client
212 .delete(&format!("/api/promo-codes/{code_id}"))
213 .await;
214 assert_eq!(
215 resp.status, 403,
216 "Suspended user should not delete promo codes: {} {}",
217 resp.status, resp.text
218 );
219 }
220
221 // HIGH: Suspended user license key operations blocked
222
223 /// Suspended user tries to generate a license key.
224 #[tokio::test]
225 async fn suspended_user_generate_license_key_blocked() {
226 let mut h = TestHarness::new().await;
227
228 // Create a creator with a published item that has license keys enabled
229 let setup = h
230 .create_creator_with_item("lkseller", "digital", 1000)
231 .await;
232 h.publish_project_and_item(&setup.project_id, &setup.item_id)
233 .await;
234
235 // Enable license keys on the item
236 let resp = h
237 .client
238 .put_form(
239 &format!("/api/items/{}/license-settings", setup.item_id),
240 "enable_license_keys=true",
241 )
242 .await;
243 assert_eq!(
244 resp.status, 204,
245 "Enable license keys failed: {} {}",
246 resp.status, resp.text
247 );
248
249 // Suspend the creator
250 db::users::suspend_user(&h.db, setup.user_id, "test suspension")
251 .await
252 .unwrap();
253 h.client.post_form("/logout", "").await;
254 h.login("lkseller", "password123").await;
255
256 // Try to generate a license key
257 let resp = h
258 .client
259 .post_form(&format!("/api/items/{}/keys", setup.item_id), "")
260 .await;
261 assert_eq!(
262 resp.status, 403,
263 "Suspended user should not generate license keys: {} {}",
264 resp.status, resp.text
265 );
266 }
267
268 // HIGH: Stale session suspension, suspension applied after login
269
270 /// User logs in while active, admin suspends them, subsequent request reflects suspension.
271 #[tokio::test]
272 async fn suspension_after_login_takes_effect() {
273 let mut h = TestHarness::new().await;
274
275 let creator_id = h.create_creator("stalesuspend").await;
276
277 // Create a project while active, should succeed
278 let resp = h
279 .client
280 .post_form("/api/projects", "slug=stale-shop&title=Stale+Shop")
281 .await;
282 assert_eq!(
283 resp.status, 200,
284 "Active user should create project: {} {}",
285 resp.status, resp.text
286 );
287 let project: Value = resp.json();
288 let project_id = project["id"].as_str().unwrap();
289
290 // Admin suspends the user via direct DB (simulating admin action)
291 db::users::suspend_user(&h.db, creator_id, "admin suspension")
292 .await
293 .unwrap();
294
295 // The session touch cache is not reachable from the test, so nothing evicts
296 // it here. Re-logging in is what makes the suspension visible.
297
298 // Log out and back in, the login itself will set suspended=true
299 // because the login handler reads it from the DB.
300 h.client.post_form("/logout", "").await;
301 h.login("stalesuspend", "password123").await;
302
303 // Now write operations should fail with 403
304 let resp = h
305 .client
306 .put_json(
307 &format!("/api/projects/{project_id}"),
308 r#"{"title": "Should Fail"}"#,
309 )
310 .await;
311 assert_eq!(
312 resp.status, 403,
313 "Suspended user (post-login) should be blocked: {} {}",
314 resp.status, resp.text
315 );
316 }
317
318 // HIGH: Webhook signature timestamp freshness
319
320 /// Webhook with stale timestamp (10 minutes old) is rejected.
321 #[tokio::test]
322 async fn webhook_stale_timestamp_rejected() {
323 use crate::harness::stripe::{TEST_WEBHOOK_SECRET_V2, sign_webhook_payload_with_timestamp};
324 use std::time::{SystemTime, UNIX_EPOCH};
325
326 let mut h = TestHarness::with_stripe().await;
327
328 let payload = r#"{"id":"evt_stale","type":"v2.core.event_destination.ping"}"#;
329 let stale_ts = SystemTime::now()
330 .duration_since(UNIX_EPOCH)
331 .unwrap()
332 .as_secs()
333 - 600; // 10 minutes ago
334 let signature = sign_webhook_payload_with_timestamp(payload, TEST_WEBHOOK_SECRET_V2, stale_ts);
335
336 let resp = h
337 .client
338 .request_with_headers(
339 "POST",
340 "/stripe/webhook/v2",
341 Some(payload),
342 &[
343 ("stripe-signature", &signature),
344 ("content-type", "application/json"),
345 ],
346 )
347 .await;
348 assert_eq!(
349 resp.status.as_u16(),
350 400,
351 "Stale webhook timestamp should be rejected: {} {}",
352 resp.status,
353 resp.text
354 );
355 }
356
357 /// Webhook with current timestamp is accepted (signature is valid).
358 #[tokio::test]
359 async fn webhook_current_timestamp_accepted() {
360 use crate::harness::stripe::{TEST_WEBHOOK_SECRET_V2, sign_webhook_payload};
361
362 let mut h = TestHarness::with_stripe().await;
363
364 let payload = r#"{"id":"evt_fresh","type":"v2.core.event_destination.ping"}"#;
365 let signature = sign_webhook_payload(payload, TEST_WEBHOOK_SECRET_V2);
366
367 let resp = h
368 .client
369 .request_with_headers(
370 "POST",
371 "/stripe/webhook/v2",
372 Some(payload),
373 &[
374 ("stripe-signature", &signature),
375 ("content-type", "application/json"),
376 ],
377 )
378 .await;
379 // Should not be 400 (signature valid). May be 200 or other status depending
380 // on event handling, but critically not a signature rejection.
381 assert_ne!(
382 resp.status.as_u16(),
383 400,
384 "Valid webhook signature should not be rejected: {} {}",
385 resp.status,
386 resp.text
387 );
388 }
389
390 /// Webhook with timestamp 1 second ago is accepted.
391 #[tokio::test]
392 async fn webhook_recent_timestamp_accepted() {
393 use crate::harness::stripe::{TEST_WEBHOOK_SECRET_V2, sign_webhook_payload_with_timestamp};
394 use std::time::{SystemTime, UNIX_EPOCH};
395
396 let mut h = TestHarness::with_stripe().await;
397
398 let payload = r#"{"id":"evt_recent","type":"v2.core.event_destination.ping"}"#;
399 let recent_ts = SystemTime::now()
400 .duration_since(UNIX_EPOCH)
401 .unwrap()
402 .as_secs()
403 - 1;
404 let signature = sign_webhook_payload_with_timestamp(payload, TEST_WEBHOOK_SECRET_V2, recent_ts);
405
406 let resp = h
407 .client
408 .request_with_headers(
409 "POST",
410 "/stripe/webhook/v2",
411 Some(payload),
412 &[
413 ("stripe-signature", &signature),
414 ("content-type", "application/json"),
415 ],
416 )
417 .await;
418 assert_ne!(
419 resp.status.as_u16(),
420 400,
421 "Recent webhook timestamp should not be rejected: {} {}",
422 resp.status,
423 resp.text
424 );
425 }
426