Skip to main content

max / makenotwork

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