Skip to main content

max / makenotwork

9.9 KB · 318 lines History Blame Raw
1 //! Creator-tier comp codes: the admin mint route and the row contract that the
2 //! creator-tier checkout's `get_platform_trial_code_by_code` lookup depends on
3 //! (platform-wide + free_trial + a trial length).
4
5 use crate::harness::TestHarness;
6
7 #[tokio::test]
8 async fn admin_mints_creator_tier_comp_code() {
9 let (mut h, _admin_id) = TestHarness::with_admin().await;
10 h.login("admin", "password123").await;
11
12 // Lowercase input to also prove the handler uppercases the stored code.
13 let resp = h
14 .client
15 .post_form(
16 "/api/admin/comp-codes/create",
17 "code=alpha6mo&trial_days=180&max_uses=10&expires_in_days=60",
18 )
19 .await;
20 assert!(
21 resp.status.is_success(),
22 "mint failed: {} {}",
23 resp.status,
24 resp.text
25 );
26
27 // The creator-tier checkout lookup filters on exactly these columns, so
28 // assert the minted row matches: uppercased code, platform-wide, free_trial,
29 // and the requested trial length / use cap.
30 let (purpose, platform, trial_days, max_uses): (String, bool, Option<i32>, Option<i32>) =
31 sqlx::query_as(
32 "SELECT code_purpose::text, is_platform_wide, trial_days, max_uses \
33 FROM promo_codes WHERE code = $1",
34 )
35 .bind("ALPHA6MO")
36 .fetch_one(&h.db)
37 .await
38 .expect("comp code row should exist");
39
40 assert_eq!(purpose, "free_trial");
41 assert!(
42 platform,
43 "comp code must be platform-wide so it resolves at creator-tier checkout"
44 );
45 assert_eq!(trial_days, Some(180));
46 assert_eq!(max_uses, Some(10));
47
48 // The mint response is the refreshed list partial, so the new code shows up.
49 assert!(
50 resp.text.contains("ALPHA6MO"),
51 "mint response should re-render the list with the new code: {}",
52 resp.text
53 );
54 }
55
56 #[tokio::test]
57 async fn comp_codes_dashboard_lists_codes() {
58 let (mut h, admin_id) = TestHarness::with_admin().await;
59 h.login("admin", "password123").await;
60
61 sqlx::query(
62 "INSERT INTO promo_codes \
63 (creator_id, code, code_purpose, min_price_cents, trial_days, max_uses, is_platform_wide) \
64 VALUES ($1, 'DASH-JAMIE', 'free_trial', 0, 180, 1, true)",
65 )
66 .bind(*admin_id)
67 .execute(&h.db)
68 .await
69 .expect("seed comp code");
70
71 let resp = h.client.get("/admin/comp-codes").await;
72 assert!(
73 resp.status.is_success(),
74 "page should render: {} {}",
75 resp.status,
76 resp.text
77 );
78 assert!(
79 resp.text.contains("Comp codes"),
80 "page should have the heading"
81 );
82 assert!(
83 resp.text.contains("DASH-JAMIE"),
84 "page should list the seeded code"
85 );
86 // One-use code that hasn't been redeemed reads as Unused.
87 assert!(
88 resp.text.contains("Unused"),
89 "an unredeemed code should show status Unused"
90 );
91 }
92
93 #[tokio::test]
94 async fn admin_comp_code_rejects_zero_trial_days() {
95 let (mut h, _admin_id) = TestHarness::with_admin().await;
96 h.login("admin", "password123").await;
97
98 let resp = h
99 .client
100 .post_form("/api/admin/comp-codes/create", "code=BADCOMP&trial_days=0")
101 .await;
102 assert_eq!(
103 resp.status, 400,
104 "zero trial days should be rejected: {}",
105 resp.text
106 );
107 }
108
109 /// End-to-end redemption: a regular user redeems a platform-wide free-trial
110 /// comp code at creator-tier checkout. Proves the new lookup + reserve wiring
111 /// runs, the trial length is threaded to Stripe, and the code's use is counted.
112 #[tokio::test]
113 async fn comp_code_redeemed_at_creator_tier_checkout() {
114 let mut h = TestHarness::with_creator_tier_checkout().await;
115 let user_id = h
116 .signup("comptester", "comptester@test.com", "password123")
117 .await;
118
119 // Seed a 180-day platform-wide free-trial code (the shape the admin mint
120 // route produces); creator_id just records ownership.
121 sqlx::query(
122 "INSERT INTO promo_codes \
123 (creator_id, code, code_purpose, min_price_cents, trial_days, max_uses, is_platform_wide) \
124 VALUES ($1, 'ALPHA6MO', 'free_trial', 0, 180, 5, true)",
125 )
126 .bind(*user_id)
127 .execute(&h.db)
128 .await
129 .expect("seed comp code");
130
131 // Redeem at creator-tier checkout (lowercase code proves the handler upper-cases).
132 let resp = h
133 .client
134 .post_form(
135 "/stripe/creator-tier",
136 "tier=everything&promo_code=alpha6mo",
137 )
138 .await;
139 assert!(
140 resp.status.is_redirection() || resp.status.is_success(),
141 "comp redemption should reach Stripe checkout, got: {} {}",
142 resp.status,
143 resp.text
144 );
145
146 // The trial length was threaded through to the Stripe call...
147 let trials = h.mock_stripe.as_ref().unwrap().creator_tier_trial_days();
148 assert_eq!(
149 trials,
150 vec![Some(180)],
151 "the 180-day trial should reach Stripe"
152 );
153
154 // ...and the code's use was reserved exactly once.
155 let use_count: i32 =
156 sqlx::query_scalar("SELECT use_count FROM promo_codes WHERE code = 'ALPHA6MO'")
157 .fetch_one(&h.db)
158 .await
159 .unwrap();
160 assert_eq!(use_count, 1, "redemption should reserve one use");
161 }
162
163 /// A reusable comp code (no global cap) grants each distinct individual one
164 /// trial: the same user redeeming twice is rejected, a different user succeeds.
165 #[tokio::test]
166 async fn reusable_comp_code_enforces_once_per_individual() {
167 let mut h = TestHarness::with_creator_tier_checkout().await;
168
169 // Owner for the FK, then a reusable 1-month code with no global cap.
170 let owner = h
171 .signup("codeowner", "codeowner@test.com", "password123")
172 .await;
173 sqlx::query(
174 "INSERT INTO promo_codes \
175 (creator_id, code, code_purpose, min_price_cents, trial_days, is_platform_wide) \
176 VALUES ($1, 'SHARE1MO', 'free_trial', 0, 30, true)",
177 )
178 .bind(*owner)
179 .execute(&h.db)
180 .await
181 .expect("seed reusable code");
182
183 // User A redeems once: succeeds.
184 h.signup("share_a", "share_a@test.com", "password123").await;
185 let r1 = h
186 .client
187 .post_form(
188 "/stripe/creator-tier",
189 "tier=everything&promo_code=share1mo",
190 )
191 .await;
192 assert!(
193 r1.status.is_redirection() || r1.status.is_success(),
194 "A first redeem: {} {}",
195 r1.status,
196 r1.text
197 );
198
199 // User A redeems the SAME code again: rejected (once per individual).
200 let r2 = h
201 .client
202 .post_form(
203 "/stripe/creator-tier",
204 "tier=everything&promo_code=share1mo",
205 )
206 .await;
207 assert_eq!(
208 r2.status, 400,
209 "A's repeat redeem should be rejected: {}",
210 r2.text
211 );
212 assert!(
213 r2.text.to_lowercase().contains("already used"),
214 "rejection should explain the repeat: {}",
215 r2.text
216 );
217
218 // A different individual redeems the same code: succeeds.
219 h.signup("share_b", "share_b@test.com", "password123").await;
220 let r3 = h
221 .client
222 .post_form(
223 "/stripe/creator-tier",
224 "tier=everything&promo_code=share1mo",
225 )
226 .await;
227 assert!(
228 r3.status.is_redirection() || r3.status.is_success(),
229 "B redeem: {} {}",
230 r3.status,
231 r3.text
232 );
233
234 // Two distinct individuals redeemed; the repeat did not count.
235 let use_count: i32 =
236 sqlx::query_scalar("SELECT use_count FROM promo_codes WHERE code = 'SHARE1MO'")
237 .fetch_one(&h.db)
238 .await
239 .unwrap();
240 assert_eq!(use_count, 2, "exactly two distinct redeemers");
241 let redemptions: i64 = sqlx::query_scalar(
242 "SELECT COUNT(*) FROM promo_code_redemptions r \
243 JOIN promo_codes p ON p.id = r.promo_code_id WHERE p.code = 'SHARE1MO'",
244 )
245 .fetch_one(&h.db)
246 .await
247 .unwrap();
248 assert_eq!(redemptions, 2, "one redemption row per distinct user");
249
250 // The 30-day trial reached Stripe for both successful redemptions only.
251 let trials = h.mock_stripe.as_ref().unwrap().creator_tier_trial_days();
252 assert_eq!(
253 trials,
254 vec![Some(30), Some(30)],
255 "both grants pass a 30-day trial; the rejected repeat does not"
256 );
257 }
258
259 /// An expired comp code is rejected: no Stripe session, no use burned.
260 #[tokio::test]
261 async fn expired_comp_code_rejected_at_creator_tier_checkout() {
262 let mut h = TestHarness::with_creator_tier_checkout().await;
263 let user_id = h
264 .signup("exptester", "exptester@test.com", "password123")
265 .await;
266
267 sqlx::query(
268 "INSERT INTO promo_codes \
269 (creator_id, code, code_purpose, min_price_cents, trial_days, is_platform_wide, expires_at) \
270 VALUES ($1, 'EXPIRED6MO', 'free_trial', 0, 180, true, NOW() - INTERVAL '1 day')",
271 )
272 .bind(*user_id)
273 .execute(&h.db)
274 .await
275 .expect("seed expired comp code");
276
277 let resp = h
278 .client
279 .post_form(
280 "/stripe/creator-tier",
281 "tier=everything&promo_code=expired6mo",
282 )
283 .await;
284 assert_eq!(
285 resp.status, 400,
286 "expired code should be rejected: {}",
287 resp.text
288 );
289
290 assert!(
291 h.mock_stripe.as_ref().unwrap().checkouts().is_empty(),
292 "no Stripe session should be created for an expired code"
293 );
294 let use_count: i32 =
295 sqlx::query_scalar("SELECT use_count FROM promo_codes WHERE code = 'EXPIRED6MO'")
296 .fetch_one(&h.db)
297 .await
298 .unwrap();
299 assert_eq!(use_count, 0, "a rejected code must not burn a use");
300 }
301
302 #[tokio::test]
303 async fn comp_code_mint_requires_admin() {
304 let mut h = TestHarness::new().await;
305 h.signup("notadmin", "notadmin@test.com", "password123")
306 .await;
307
308 let resp = h
309 .client
310 .post_form("/api/admin/comp-codes/create", "code=SNEAKY&trial_days=180")
311 .await;
312 assert!(
313 !resp.status.is_success(),
314 "a non-admin must not be able to mint comp codes (got {})",
315 resp.status
316 );
317 }
318