Skip to main content

max / makenotwork

17.0 KB · 486 lines History Blame Raw
1 //! Tests for [`super`].
2 use super::*;
3
4 #[test]
5 fn percentage_discount_50() {
6 assert_eq!(apply_discount(1000, DiscountType::Percentage, 50), 500);
7 }
8
9 #[test]
10 fn percentage_discount_100() {
11 assert_eq!(apply_discount(1000, DiscountType::Percentage, 100), 0);
12 }
13
14 #[test]
15 fn percentage_discount_10() {
16 // 999 * 10 / 100 = 99 (integer), 999 - 99 = 900
17 assert_eq!(apply_discount(999, DiscountType::Percentage, 10), 900);
18 }
19
20 #[test]
21 fn fixed_discount() {
22 assert_eq!(apply_discount(1000, DiscountType::Fixed, 300), 700);
23 }
24
25 #[test]
26 fn fixed_discount_exceeds_price() {
27 assert_eq!(apply_discount(100, DiscountType::Fixed, 500), 0);
28 }
29
30 // Percentage discount edge cases
31
32 #[test]
33 fn percentage_discount_0() {
34 assert_eq!(apply_discount(1000, DiscountType::Percentage, 0), 1000);
35 }
36
37 #[test]
38 fn percentage_discount_over_100() {
39 // 150% discount should clamp to 0
40 assert_eq!(apply_discount(1000, DiscountType::Percentage, 150), 0);
41 }
42
43 #[test]
44 fn percentage_discount_1_percent() {
45 // 1000 * 1 / 100 = 10, result = 990
46 assert_eq!(apply_discount(1000, DiscountType::Percentage, 1), 990);
47 }
48
49 #[test]
50 fn percentage_discount_99_percent() {
51 // 1000 * 99 / 100 = 990, result = 10
52 assert_eq!(apply_discount(1000, DiscountType::Percentage, 99), 10);
53 }
54
55 #[test]
56 fn percentage_discount_rounding() {
57 // 1 cent * 50 / 100 = 0 (integer division), result = 1
58 assert_eq!(apply_discount(1, DiscountType::Percentage, 50), 1);
59 // 3 * 33 / 100 = 0 (integer), result = 3
60 assert_eq!(apply_discount(3, DiscountType::Percentage, 33), 3);
61 // 199 * 50 / 100 = 99, result = 100
62 assert_eq!(apply_discount(199, DiscountType::Percentage, 50), 100);
63 }
64
65 // Fixed discount edge cases
66
67 #[test]
68 fn fixed_discount_exact_price() {
69 assert_eq!(apply_discount(500, DiscountType::Fixed, 500), 0);
70 }
71
72 #[test]
73 fn fixed_discount_zero_value() {
74 assert_eq!(apply_discount(1000, DiscountType::Fixed, 0), 1000);
75 }
76
77 #[test]
78 fn fixed_discount_one_cent() {
79 assert_eq!(apply_discount(1000, DiscountType::Fixed, 1), 999);
80 }
81
82 // Zero price
83
84 #[test]
85 fn zero_price_percentage() {
86 assert_eq!(apply_discount(0, DiscountType::Percentage, 50), 0);
87 }
88
89 #[test]
90 fn zero_price_fixed() {
91 assert_eq!(apply_discount(0, DiscountType::Fixed, 100), 0);
92 }
93
94 // Negative values (defensive)
95
96 #[test]
97 fn negative_discount_value_percentage() {
98 // Negative discount values are clamped to 0, so price is unchanged
99 assert_eq!(apply_discount(1000, DiscountType::Percentage, -50), 1000);
100 }
101
102 #[test]
103 fn negative_discount_value_fixed() {
104 // Negative discount values are clamped to 0, so price is unchanged
105 assert_eq!(apply_discount(1000, DiscountType::Fixed, -500), 1000);
106 }
107
108 #[test]
109 fn negative_price_percentage() {
110 // Negative price with percentage discount, documents current behavior
111 // -1000 * 50 / 100 = -500, -1000 - (-500) = -500, max(0) = 0
112 assert_eq!(apply_discount(-1000, DiscountType::Percentage, 50), 0);
113 }
114
115 #[test]
116 fn negative_price_fixed() {
117 // -1000 - 500 = -1500, max(0) = 0
118 assert_eq!(apply_discount(-1000, DiscountType::Fixed, 500), 0);
119 }
120
121 // Large values (overflow safety)
122
123 #[test]
124 fn large_price_percentage_no_overflow() {
125 // The function uses i64 intermediate to avoid overflow
126 // i32::MAX = 2_147_483_647; 50% of that
127 let price = i32::MAX;
128 let result = apply_discount(price, DiscountType::Percentage, 50);
129 assert_eq!(result, 1_073_741_824); // (MAX - MAX*50/100)
130 }
131
132 // ── Adversarial (test-fuzz) ──
133
134 #[test]
135 fn adversarial_percentage_max_price_max_percentage() {
136 // i32::MAX price with 100% discount
137 let result = apply_discount(i32::MAX, DiscountType::Percentage, 100);
138 assert_eq!(result, 0, "100% discount on any price should be 0");
139 }
140
141 #[test]
142 fn adversarial_percentage_max_price_99_percent() {
143 let result = apply_discount(i32::MAX, DiscountType::Percentage, 99);
144 // i32::MAX * 99 / 100 via i64 = 2_125_999_810, remainder = 21_483_837
145 // Exact: 2_147_483_647 * 99 = 212_600_881_053 / 100 = 2_126_008_810
146 // 2_147_483_647 - 2_126_008_810 = 21_474_837
147 assert_eq!(result, 21_474_837);
148 assert!(result > 0, "99% discount should leave some remaining");
149 }
150
151 #[test]
152 fn adversarial_fixed_max_price_max_discount() {
153 let result = apply_discount(i32::MAX, DiscountType::Fixed, i32::MAX);
154 assert_eq!(result, 0);
155 }
156
157 #[test]
158 fn adversarial_both_negative() {
159 // Both negative price and negative discount
160 let result = apply_discount(-100, DiscountType::Fixed, -100);
161 // -100 - (-100) = 0
162 assert_eq!(result, 0);
163 }
164
165 #[test]
166 fn adversarial_percentage_discount_exactly_50_odd_price() {
167 // Rounding: 1 cent * 50% = 0 (integer division), so result = 1
168 assert_eq!(apply_discount(1, DiscountType::Percentage, 50), 1);
169 // 3 cents * 50% = 1 (via i64: 3*50/100=1), result = 2
170 assert_eq!(apply_discount(3, DiscountType::Percentage, 50), 2);
171 }
172
173 #[test]
174 fn adversarial_apply_discount_invariant() {
175 // For any valid (positive) price and percentage 0-100,
176 // result should be in [0, price]
177 for price in [1, 50, 100, 999, 10000, 1_000_000] {
178 for pct in [0, 1, 10, 25, 33, 50, 75, 99, 100] {
179 let result = apply_discount(price, DiscountType::Percentage, pct);
180 assert!(
181 result >= 0 && result <= price,
182 "Invariant violated: price={price}, pct={pct}, result={result}"
183 );
184 }
185 }
186 }
187
188 #[test]
189 fn adversarial_fixed_discount_invariant() {
190 // For any positive price and positive discount, result should be in [0, price]
191 for price in [1, 50, 100, 999, 10000] {
192 for discount in [0, 1, 50, 100, 999, 10000, 999_999] {
193 let result = apply_discount(price, DiscountType::Fixed, discount);
194 assert!(
195 result >= 0 && result <= price,
196 "Invariant violated: price={price}, discount={discount}, result={result}"
197 );
198 }
199 }
200 }
201
202 // ── Property-based tests (proptest) ──
203
204 proptest::proptest! {
205 #[test]
206 fn prop_percentage_discount_in_range(price in 0..=1_000_000i32, pct in 0..=100i32) {
207 let result = apply_discount(price, DiscountType::Percentage, pct);
208 proptest::prop_assert!(result >= 0, "Result {} should be >= 0", result);
209 proptest::prop_assert!(result <= price, "Result {} should be <= price {}", result, price);
210 }
211
212 #[test]
213 fn prop_fixed_discount_in_range(price in 0..=1_000_000i32, discount in 0..=1_000_000i32) {
214 let result = apply_discount(price, DiscountType::Fixed, discount);
215 proptest::prop_assert!(result >= 0, "Result {} should be >= 0", result);
216 proptest::prop_assert!(result <= price, "Result {} should be <= price {}", result, price);
217 }
218
219 #[test]
220 fn prop_100_percent_discount_is_zero(price in 0..=1_000_000i32) {
221 proptest::prop_assert_eq!(apply_discount(price, DiscountType::Percentage, 100), 0);
222 }
223
224 #[test]
225 fn prop_0_percent_discount_is_identity(price in 0..=1_000_000i32) {
226 proptest::prop_assert_eq!(apply_discount(price, DiscountType::Percentage, 0), price);
227 }
228 }
229
230 // ── Metamorphic: applying a discount and removing it again ───────────────
231 //
232 // Wiki `testing-posture`, phase 2. A metamorphic relation states how two
233 // runs relate rather than what either returns, so it needs no table of
234 // expected values and nothing has to be recomputed by hand when a price
235 // changes. See Chen et al. 1998.
236 //
237 // The relation asked for was "apply then remove is the identity". It is,
238 // for Fixed, and it is not for Percentage, because integer cents rounding
239 // is not invertible. What replaces it is not a tolerance: the loss is an
240 // exact quantity and the tests below pin it as one.
241
242 proptest::proptest! {
243 /// Fixed is invertible wherever it does not clamp: the discount is a
244 /// subtraction, and adding it back is the inverse of subtracting it.
245 #[test]
246 fn prop_removing_a_fixed_discount_restores_the_price_exactly(
247 price in 0..=1_000_000i32,
248 discount in 0..=1_000_000i32,
249 ) {
250 proptest::prop_assume!(discount <= price);
251 let discounted = apply_discount(price, DiscountType::Fixed, discount);
252 proptest::prop_assert_eq!(discounted + discount, price);
253 }
254
255 /// Above the price it is not invertible, and that is the intended
256 /// behaviour rather than a gap: the clamp to zero is what stops a
257 /// generous coupon paying the buyer. Every price at or below the
258 /// discount collapses to the same 0, so no inverse can tell them apart.
259 #[test]
260 fn prop_a_fixed_discount_over_the_price_destroys_it(
261 price in 0..=1_000_000i32,
262 excess in 0..=1_000_000i32,
263 ) {
264 let discount = price.saturating_add(excess);
265 proptest::prop_assert_eq!(apply_discount(price, DiscountType::Fixed, discount), 0);
266 }
267
268 /// What a percentage discount loses, stated exactly.
269 ///
270 /// `apply_discount` computes `price - (price * pct) / 100` with integer
271 /// division, so writing `price * pct = 100q + r` with `0 <= r < 100`
272 /// gives `discounted * 100 = price * (100 - pct) + r`. The remainder `r`
273 /// is the whole of the round-trip loss and it is bounded by 100
274 /// regardless of how large the price is.
275 ///
276 /// That identity is the tolerance the task asked to have pinned, and it
277 /// is worth having as an equation rather than an epsilon: the error does
278 /// not grow with the price, so a $10,000 sale is no less recoverable
279 /// than a $1 one.
280 #[test]
281 fn prop_a_percentage_discount_loses_exactly_the_rounding_remainder(
282 price in 0..=1_000_000i32,
283 pct in 0..=100i32,
284 ) {
285 let discounted = apply_discount(price, DiscountType::Percentage, pct);
286 let remainder = i64::from(discounted) * 100 - i64::from(price) * i64::from(100 - pct);
287 proptest::prop_assert!(
288 (0..100).contains(&remainder),
289 "price={} pct={} discounted={} left remainder {}, outside [0, 100)",
290 price, pct, discounted, remainder,
291 );
292 proptest::prop_assert_eq!(
293 remainder,
294 (i64::from(price) * i64::from(pct)) % 100,
295 "the remainder is not the one integer division dropped",
296 );
297 }
298
299 /// So removal is exact exactly when nothing was dropped, which is when
300 /// 100 divides `price * pct`. Constructing such a price is the point:
301 /// this is the half of the original relation that does survive.
302 #[test]
303 fn prop_removing_a_percentage_discount_is_exact_when_it_divides_evenly(
304 hundreds in 0..=10_000i32,
305 pct in 0..=99i32,
306 ) {
307 let price = hundreds * 100;
308 let discounted = apply_discount(price, DiscountType::Percentage, pct);
309 // No remainder, so the inverse is the plain rational one.
310 proptest::prop_assert_eq!(
311 i64::from(discounted) * 100 / i64::from(100 - pct),
312 i64::from(price),
313 );
314 }
315 }
316
317 /// The one case where the relation cannot hold however the price is chosen.
318 /// A full discount maps every price to 0, so removal has nothing to work
319 /// from. Worth a named test rather than an `prop_assume!` that quietly skips
320 /// it, since "free" is a real configuration and not an edge.
321 #[test]
322 fn removing_a_full_discount_is_impossible_by_construction() {
323 for price in [0, 1, 99, 100, 101, 999, 1_000_000] {
324 assert_eq!(apply_discount(price, DiscountType::Percentage, 100), 0);
325 }
326 }
327
328 // Cart promo semantics: one redemption = one use (ultra-fuzz Run 10 Pay S1)
329
330 /// Build a percentage-discount promo with no scope/min-price gating.
331 fn unscoped_discount_promo(max_uses: Option<i32>) -> ValidatedPromo {
332 ValidatedPromo {
333 code: DbPromoCode {
334 id: PromoCodeId::new(),
335 creator_id: UserId::new(),
336 code: "SAVE10".to_string(),
337 code_purpose: CodePurpose::Discount,
338 discount_type: Some(DiscountType::Percentage),
339 discount_value: Some(10),
340 min_price_cents: 0,
341 trial_days: None,
342 item_id: None,
343 project_id: None,
344 tier_id: None,
345 max_uses,
346 use_count: 0,
347 expires_at: None,
348 starts_at: None,
349 created_at: chrono::Utc::now(),
350 is_platform_wide: false,
351 },
352 is_platform_wide: false,
353 }
354 }
355
356 #[test]
357 fn single_use_code_discounts_every_eligible_cart_line() {
358 // A max_uses=1 code applied across a multi-item cart discounts EVERY
359 // eligible line. This is intentional: the handler reserves exactly one
360 // use per cart checkout (one redemption = one use), so the per-line
361 // discounting below is not a use-count leak. Pin it so a future change
362 // can't silently turn cart promos into per-line reservation.
363 let promo = unscoped_discount_promo(Some(1));
364 for base in [1000, 2000, 4999] {
365 let result = apply_promo_to_item(&promo, ItemId::new(), ProjectId::new(), base).unwrap();
366 let PromoApplication::Apply(applied) = result else {
367 panic!("expected Apply for an eligible cart line at base {base}");
368 };
369 assert_eq!(applied.price_cents, base - base / 10);
370 // A seller-scoped code is creator-funded, no platform reimbursement.
371 assert_eq!(applied.funding, DiscountFunding::CreatorFunded);
372 }
373 // apply_promo_to_item never touches use_count; reservation is the
374 // handler's once-per-checkout concern.
375 assert_eq!(promo.code.use_count, 0);
376 }
377
378 // Platform credit is a spend-once balance (ultra-fuzz Run 13 Payments)
379
380 /// Build a platform-wide fixed credit (the $5 Fan+ renewal credit shape).
381 fn platform_fixed_credit(cents: i32) -> ValidatedPromo {
382 ValidatedPromo {
383 code: DbPromoCode {
384 id: PromoCodeId::new(),
385 creator_id: UserId::new(),
386 code: "FANPLUS".to_string(),
387 code_purpose: CodePurpose::Discount,
388 discount_type: Some(DiscountType::Fixed),
389 discount_value: Some(cents),
390 min_price_cents: 0,
391 trial_days: None,
392 item_id: None,
393 project_id: None,
394 tier_id: None,
395 max_uses: None,
396 use_count: 0,
397 expires_at: None,
398 starts_at: None,
399 created_at: chrono::Utc::now(),
400 is_platform_wide: true,
401 },
402 is_platform_wide: true,
403 }
404 }
405
406 #[test]
407 fn platform_fixed_credit_budget_is_face_value() {
408 assert_eq!(
409 platform_fixed_credit(500).platform_credit_budget_cents(),
410 Some(500)
411 );
412 }
413
414 #[test]
415 fn seller_and_percentage_codes_have_no_credit_budget() {
416 // Seller-funded code: credit is always 0, no balance to cap.
417 assert_eq!(
418 unscoped_discount_promo(None).platform_credit_budget_cents(),
419 None
420 );
421 // Platform-wide *percentage*: an intentional platform-funded sale that
422 // legitimately applies to every line, not a spend-once balance.
423 let mut pct = platform_fixed_credit(500);
424 pct.code.discount_type = Some(DiscountType::Percentage);
425 pct.code.discount_value = Some(20);
426 assert_eq!(pct.platform_credit_budget_cents(), None);
427 }
428
429 #[test]
430 fn platform_fixed_credit_spent_once_across_cart() {
431 // The $5 (500¢) Fan+ credit across three $10 (1000¢) lines must discount
432 // the buyer and reimburse the seller a total of exactly 500¢, once, not
433 // 500¢ per line (Run 13 SERIOUS: cart platform-credit multiplication).
434 let promo = platform_fixed_credit(500);
435 let mut budget = promo.platform_credit_budget_cents();
436 assert_eq!(budget, Some(500));
437
438 let mut total_credit = 0i64;
439 let mut total_buyer_paid = 0i64;
440 for _ in 0..3 {
441 let PromoApplication::Apply(applied) =
442 apply_promo_to_item(&promo, ItemId::new(), ProjectId::new(), 1000).unwrap()
443 else {
444 panic!("expected Apply for an eligible platform-credit line");
445 };
446 let (final_price, credit) = cap_line_to_credit_budget(applied, &mut budget);
447 total_credit += credit;
448 total_buyer_paid += i64::from(final_price);
449 }
450 assert_eq!(
451 total_credit, 500,
452 "MNW reimburses the seller exactly the face value, once"
453 );
454 assert_eq!(
455 total_buyer_paid,
456 3000 - 500,
457 "buyer gets the $5 credit exactly once"
458 );
459 assert_eq!(budget, Some(0), "balance fully spent");
460 }
461
462 #[test]
463 fn platform_fixed_credit_carries_balance_across_cheap_lines() {
464 // A $5 credit on two $1 (100¢) items spends 100 then 100 (the balance
465 // carries instead of burning the whole $5 on the first line); 300¢ remain.
466 let promo = platform_fixed_credit(500);
467 let mut budget = promo.platform_credit_budget_cents();
468 let mut total_credit = 0i64;
469 for _ in 0..2 {
470 let PromoApplication::Apply(applied) =
471 apply_promo_to_item(&promo, ItemId::new(), ProjectId::new(), 100).unwrap()
472 else {
473 panic!("expected Apply");
474 };
475 let (final_price, credit) = cap_line_to_credit_budget(applied, &mut budget);
476 assert_eq!(final_price, 0, "a $1 item is fully covered by the credit");
477 total_credit += credit;
478 }
479 assert_eq!(total_credit, 200);
480 assert_eq!(
481 budget,
482 Some(300),
483 "unspent balance carries to the rest of the cart"
484 );
485 }
486