//! Tests for [`super`]. use super::*; #[test] fn percentage_discount_50() { assert_eq!(apply_discount(1000, DiscountType::Percentage, 50), 500); } #[test] fn percentage_discount_100() { assert_eq!(apply_discount(1000, DiscountType::Percentage, 100), 0); } #[test] fn percentage_discount_10() { // 999 * 10 / 100 = 99 (integer), 999 - 99 = 900 assert_eq!(apply_discount(999, DiscountType::Percentage, 10), 900); } #[test] fn fixed_discount() { assert_eq!(apply_discount(1000, DiscountType::Fixed, 300), 700); } #[test] fn fixed_discount_exceeds_price() { assert_eq!(apply_discount(100, DiscountType::Fixed, 500), 0); } // Percentage discount edge cases #[test] fn percentage_discount_0() { assert_eq!(apply_discount(1000, DiscountType::Percentage, 0), 1000); } #[test] fn percentage_discount_over_100() { // 150% discount should clamp to 0 assert_eq!(apply_discount(1000, DiscountType::Percentage, 150), 0); } #[test] fn percentage_discount_1_percent() { // 1000 * 1 / 100 = 10, result = 990 assert_eq!(apply_discount(1000, DiscountType::Percentage, 1), 990); } #[test] fn percentage_discount_99_percent() { // 1000 * 99 / 100 = 990, result = 10 assert_eq!(apply_discount(1000, DiscountType::Percentage, 99), 10); } #[test] fn percentage_discount_rounding() { // 1 cent * 50 / 100 = 0 (integer division), result = 1 assert_eq!(apply_discount(1, DiscountType::Percentage, 50), 1); // 3 * 33 / 100 = 0 (integer), result = 3 assert_eq!(apply_discount(3, DiscountType::Percentage, 33), 3); // 199 * 50 / 100 = 99, result = 100 assert_eq!(apply_discount(199, DiscountType::Percentage, 50), 100); } // Fixed discount edge cases #[test] fn fixed_discount_exact_price() { assert_eq!(apply_discount(500, DiscountType::Fixed, 500), 0); } #[test] fn fixed_discount_zero_value() { assert_eq!(apply_discount(1000, DiscountType::Fixed, 0), 1000); } #[test] fn fixed_discount_one_cent() { assert_eq!(apply_discount(1000, DiscountType::Fixed, 1), 999); } // Zero price #[test] fn zero_price_percentage() { assert_eq!(apply_discount(0, DiscountType::Percentage, 50), 0); } #[test] fn zero_price_fixed() { assert_eq!(apply_discount(0, DiscountType::Fixed, 100), 0); } // Negative values (defensive) #[test] fn negative_discount_value_percentage() { // Negative discount values are clamped to 0, so price is unchanged assert_eq!(apply_discount(1000, DiscountType::Percentage, -50), 1000); } #[test] fn negative_discount_value_fixed() { // Negative discount values are clamped to 0, so price is unchanged assert_eq!(apply_discount(1000, DiscountType::Fixed, -500), 1000); } #[test] fn negative_price_percentage() { // Negative price with percentage discount, documents current behavior // -1000 * 50 / 100 = -500, -1000 - (-500) = -500, max(0) = 0 assert_eq!(apply_discount(-1000, DiscountType::Percentage, 50), 0); } #[test] fn negative_price_fixed() { // -1000 - 500 = -1500, max(0) = 0 assert_eq!(apply_discount(-1000, DiscountType::Fixed, 500), 0); } // Large values (overflow safety) #[test] fn large_price_percentage_no_overflow() { // The function uses i64 intermediate to avoid overflow // i32::MAX = 2_147_483_647; 50% of that let price = i32::MAX; let result = apply_discount(price, DiscountType::Percentage, 50); assert_eq!(result, 1_073_741_824); // (MAX - MAX*50/100) } // ── Adversarial (test-fuzz) ── #[test] fn adversarial_percentage_max_price_max_percentage() { // i32::MAX price with 100% discount let result = apply_discount(i32::MAX, DiscountType::Percentage, 100); assert_eq!(result, 0, "100% discount on any price should be 0"); } #[test] fn adversarial_percentage_max_price_99_percent() { let result = apply_discount(i32::MAX, DiscountType::Percentage, 99); // i32::MAX * 99 / 100 via i64 = 2_125_999_810, remainder = 21_483_837 // Exact: 2_147_483_647 * 99 = 212_600_881_053 / 100 = 2_126_008_810 // 2_147_483_647 - 2_126_008_810 = 21_474_837 assert_eq!(result, 21_474_837); assert!(result > 0, "99% discount should leave some remaining"); } #[test] fn adversarial_fixed_max_price_max_discount() { let result = apply_discount(i32::MAX, DiscountType::Fixed, i32::MAX); assert_eq!(result, 0); } #[test] fn adversarial_both_negative() { // Both negative price and negative discount let result = apply_discount(-100, DiscountType::Fixed, -100); // -100 - (-100) = 0 assert_eq!(result, 0); } #[test] fn adversarial_percentage_discount_exactly_50_odd_price() { // Rounding: 1 cent * 50% = 0 (integer division), so result = 1 assert_eq!(apply_discount(1, DiscountType::Percentage, 50), 1); // 3 cents * 50% = 1 (via i64: 3*50/100=1), result = 2 assert_eq!(apply_discount(3, DiscountType::Percentage, 50), 2); } #[test] fn adversarial_apply_discount_invariant() { // For any valid (positive) price and percentage 0-100, // result should be in [0, price] for price in [1, 50, 100, 999, 10000, 1_000_000] { for pct in [0, 1, 10, 25, 33, 50, 75, 99, 100] { let result = apply_discount(price, DiscountType::Percentage, pct); assert!( result >= 0 && result <= price, "Invariant violated: price={price}, pct={pct}, result={result}" ); } } } #[test] fn adversarial_fixed_discount_invariant() { // For any positive price and positive discount, result should be in [0, price] for price in [1, 50, 100, 999, 10000] { for discount in [0, 1, 50, 100, 999, 10000, 999_999] { let result = apply_discount(price, DiscountType::Fixed, discount); assert!( result >= 0 && result <= price, "Invariant violated: price={price}, discount={discount}, result={result}" ); } } } // ── Property-based tests (proptest) ── proptest::proptest! { #[test] fn prop_percentage_discount_in_range(price in 0..=1_000_000i32, pct in 0..=100i32) { let result = apply_discount(price, DiscountType::Percentage, pct); proptest::prop_assert!(result >= 0, "Result {} should be >= 0", result); proptest::prop_assert!(result <= price, "Result {} should be <= price {}", result, price); } #[test] fn prop_fixed_discount_in_range(price in 0..=1_000_000i32, discount in 0..=1_000_000i32) { let result = apply_discount(price, DiscountType::Fixed, discount); proptest::prop_assert!(result >= 0, "Result {} should be >= 0", result); proptest::prop_assert!(result <= price, "Result {} should be <= price {}", result, price); } #[test] fn prop_100_percent_discount_is_zero(price in 0..=1_000_000i32) { proptest::prop_assert_eq!(apply_discount(price, DiscountType::Percentage, 100), 0); } #[test] fn prop_0_percent_discount_is_identity(price in 0..=1_000_000i32) { proptest::prop_assert_eq!(apply_discount(price, DiscountType::Percentage, 0), price); } } // ── Metamorphic: applying a discount and removing it again ─────────────── // // Wiki `testing-posture`, phase 2. A metamorphic relation states how two // runs relate rather than what either returns, so it needs no table of // expected values and nothing has to be recomputed by hand when a price // changes. See Chen et al. 1998. // // The relation asked for was "apply then remove is the identity". It is, // for Fixed, and it is not for Percentage, because integer cents rounding // is not invertible. What replaces it is not a tolerance: the loss is an // exact quantity and the tests below pin it as one. proptest::proptest! { /// Fixed is invertible wherever it does not clamp: the discount is a /// subtraction, and adding it back is the inverse of subtracting it. #[test] fn prop_removing_a_fixed_discount_restores_the_price_exactly( price in 0..=1_000_000i32, discount in 0..=1_000_000i32, ) { proptest::prop_assume!(discount <= price); let discounted = apply_discount(price, DiscountType::Fixed, discount); proptest::prop_assert_eq!(discounted + discount, price); } /// Above the price it is not invertible, and that is the intended /// behaviour rather than a gap: the clamp to zero is what stops a /// generous coupon paying the buyer. Every price at or below the /// discount collapses to the same 0, so no inverse can tell them apart. #[test] fn prop_a_fixed_discount_over_the_price_destroys_it( price in 0..=1_000_000i32, excess in 0..=1_000_000i32, ) { let discount = price.saturating_add(excess); proptest::prop_assert_eq!(apply_discount(price, DiscountType::Fixed, discount), 0); } /// What a percentage discount loses, stated exactly. /// /// `apply_discount` computes `price - (price * pct) / 100` with integer /// division, so writing `price * pct = 100q + r` with `0 <= r < 100` /// gives `discounted * 100 = price * (100 - pct) + r`. The remainder `r` /// is the whole of the round-trip loss and it is bounded by 100 /// regardless of how large the price is. /// /// That identity is the tolerance the task asked to have pinned, and it /// is worth having as an equation rather than an epsilon: the error does /// not grow with the price, so a $10,000 sale is no less recoverable /// than a $1 one. #[test] fn prop_a_percentage_discount_loses_exactly_the_rounding_remainder( price in 0..=1_000_000i32, pct in 0..=100i32, ) { let discounted = apply_discount(price, DiscountType::Percentage, pct); let remainder = i64::from(discounted) * 100 - i64::from(price) * i64::from(100 - pct); proptest::prop_assert!( (0..100).contains(&remainder), "price={} pct={} discounted={} left remainder {}, outside [0, 100)", price, pct, discounted, remainder, ); proptest::prop_assert_eq!( remainder, (i64::from(price) * i64::from(pct)) % 100, "the remainder is not the one integer division dropped", ); } /// So removal is exact exactly when nothing was dropped, which is when /// 100 divides `price * pct`. Constructing such a price is the point: /// this is the half of the original relation that does survive. #[test] fn prop_removing_a_percentage_discount_is_exact_when_it_divides_evenly( hundreds in 0..=10_000i32, pct in 0..=99i32, ) { let price = hundreds * 100; let discounted = apply_discount(price, DiscountType::Percentage, pct); // No remainder, so the inverse is the plain rational one. proptest::prop_assert_eq!( i64::from(discounted) * 100 / i64::from(100 - pct), i64::from(price), ); } } /// The one case where the relation cannot hold however the price is chosen. /// A full discount maps every price to 0, so removal has nothing to work /// from. Worth a named test rather than an `prop_assume!` that quietly skips /// it, since "free" is a real configuration and not an edge. #[test] fn removing_a_full_discount_is_impossible_by_construction() { for price in [0, 1, 99, 100, 101, 999, 1_000_000] { assert_eq!(apply_discount(price, DiscountType::Percentage, 100), 0); } } // Cart promo semantics: one redemption = one use (ultra-fuzz Run 10 Pay S1) /// Build a percentage-discount promo with no scope/min-price gating. fn unscoped_discount_promo(max_uses: Option) -> ValidatedPromo { ValidatedPromo { code: DbPromoCode { id: PromoCodeId::new(), creator_id: UserId::new(), code: "SAVE10".to_string(), code_purpose: CodePurpose::Discount, discount_type: Some(DiscountType::Percentage), discount_value: Some(10), min_price_cents: 0, trial_days: None, item_id: None, project_id: None, tier_id: None, max_uses, use_count: 0, expires_at: None, starts_at: None, created_at: chrono::Utc::now(), is_platform_wide: false, }, is_platform_wide: false, } } #[test] fn single_use_code_discounts_every_eligible_cart_line() { // A max_uses=1 code applied across a multi-item cart discounts EVERY // eligible line. This is intentional: the handler reserves exactly one // use per cart checkout (one redemption = one use), so the per-line // discounting below is not a use-count leak. Pin it so a future change // can't silently turn cart promos into per-line reservation. let promo = unscoped_discount_promo(Some(1)); for base in [1000, 2000, 4999] { let result = apply_promo_to_item(&promo, ItemId::new(), ProjectId::new(), base).unwrap(); let PromoApplication::Apply(applied) = result else { panic!("expected Apply for an eligible cart line at base {base}"); }; assert_eq!(applied.price_cents, base - base / 10); // A seller-scoped code is creator-funded, no platform reimbursement. assert_eq!(applied.funding, DiscountFunding::CreatorFunded); } // apply_promo_to_item never touches use_count; reservation is the // handler's once-per-checkout concern. assert_eq!(promo.code.use_count, 0); } // Platform credit is a spend-once balance (ultra-fuzz Run 13 Payments) /// Build a platform-wide fixed credit (the $5 Fan+ renewal credit shape). fn platform_fixed_credit(cents: i32) -> ValidatedPromo { ValidatedPromo { code: DbPromoCode { id: PromoCodeId::new(), creator_id: UserId::new(), code: "FANPLUS".to_string(), code_purpose: CodePurpose::Discount, discount_type: Some(DiscountType::Fixed), discount_value: Some(cents), min_price_cents: 0, trial_days: None, item_id: None, project_id: None, tier_id: None, max_uses: None, use_count: 0, expires_at: None, starts_at: None, created_at: chrono::Utc::now(), is_platform_wide: true, }, is_platform_wide: true, } } #[test] fn platform_fixed_credit_budget_is_face_value() { assert_eq!( platform_fixed_credit(500).platform_credit_budget_cents(), Some(500) ); } #[test] fn seller_and_percentage_codes_have_no_credit_budget() { // Seller-funded code: credit is always 0, no balance to cap. assert_eq!( unscoped_discount_promo(None).platform_credit_budget_cents(), None ); // Platform-wide *percentage*: an intentional platform-funded sale that // legitimately applies to every line, not a spend-once balance. let mut pct = platform_fixed_credit(500); pct.code.discount_type = Some(DiscountType::Percentage); pct.code.discount_value = Some(20); assert_eq!(pct.platform_credit_budget_cents(), None); } #[test] fn platform_fixed_credit_spent_once_across_cart() { // The $5 (500¢) Fan+ credit across three $10 (1000¢) lines must discount // the buyer and reimburse the seller a total of exactly 500¢, once, not // 500¢ per line (Run 13 SERIOUS: cart platform-credit multiplication). let promo = platform_fixed_credit(500); let mut budget = promo.platform_credit_budget_cents(); assert_eq!(budget, Some(500)); let mut total_credit = 0i64; let mut total_buyer_paid = 0i64; for _ in 0..3 { let PromoApplication::Apply(applied) = apply_promo_to_item(&promo, ItemId::new(), ProjectId::new(), 1000).unwrap() else { panic!("expected Apply for an eligible platform-credit line"); }; let (final_price, credit) = cap_line_to_credit_budget(applied, &mut budget); total_credit += credit; total_buyer_paid += i64::from(final_price); } assert_eq!( total_credit, 500, "MNW reimburses the seller exactly the face value, once" ); assert_eq!( total_buyer_paid, 3000 - 500, "buyer gets the $5 credit exactly once" ); assert_eq!(budget, Some(0), "balance fully spent"); } #[test] fn platform_fixed_credit_carries_balance_across_cheap_lines() { // A $5 credit on two $1 (100¢) items spends 100 then 100 (the balance // carries instead of burning the whole $5 on the first line); 300¢ remain. let promo = platform_fixed_credit(500); let mut budget = promo.platform_credit_budget_cents(); let mut total_credit = 0i64; for _ in 0..2 { let PromoApplication::Apply(applied) = apply_promo_to_item(&promo, ItemId::new(), ProjectId::new(), 100).unwrap() else { panic!("expected Apply"); }; let (final_price, credit) = cap_line_to_credit_budget(applied, &mut budget); assert_eq!(final_price, 0, "a $1 item is fully covered by the credit"); total_credit += credit; } assert_eq!(total_credit, 200); assert_eq!( budget, Some(300), "unspent balance carries to the rest of the cart" ); }