Skip to main content

max / makenotwork

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