package economy import ( "context" "path/filepath" "testing" "time" "hakemsho/internal/store" ) func setup(t *testing.T) (*Service, int64) { t.Helper() dbPath := filepath.Join(t.TempDir(), "test.db") st, err := store.Open(dbPath) if err != nil { t.Fatalf("open db: %v", err) } t.Cleanup(func() { st.Close() }) res, err := st.DB.Exec(`INSERT INTO users (mobile) VALUES ('09120000000')`) if err != nil { t.Fatalf("insert user: %v", err) } id, _ := res.LastInsertId() svc := New(st.DB, DevAdVerifier{}, DevIAPVerifier{}) if err := svc.LoadCatalog(context.Background()); err != nil { t.Fatalf("load catalog: %v", err) } return svc, id } func setCoins(t *testing.T, s *Service, userID, coins int64) { t.Helper() if _, err := s.db.Exec(`UPDATE users SET coins = ? WHERE id = ?`, coins, userID); err != nil { t.Fatal(err) } } func coinsOf(t *testing.T, s *Service, userID int64) int64 { t.Helper() p, err := s.GetProfile(context.Background(), userID) if err != nil { t.Fatal(err) } return p.Coins } func TestLevelInfo(t *testing.T) { cases := []struct { xp int64 level int into int64 need int64 }{ {0, 1, 0, 50}, {49, 1, 49, 50}, {50, 2, 0, 100}, {150, 3, 0, 150}, } for _, c := range cases { lvl, into, need := LevelInfo(c.xp) if lvl != c.level || into != c.into || need != c.need { t.Errorf("LevelInfo(%d) = (%d,%d,%d), want (%d,%d,%d)", c.xp, lvl, into, need, c.level, c.into, c.need) } } } func TestWalletInsufficient(t *testing.T) { svc, id := setup(t) ctx := context.Background() setCoins(t, svc, id, 100) if err := svc.Adjust(ctx, id, CurrencyCoin, -200, "test", ""); err != ErrInsufficient { t.Fatalf("want ErrInsufficient, got %v", err) } if c := coinsOf(t, svc, id); c != 100 { t.Fatalf("balance changed on failed debit: %d", c) } if err := svc.Adjust(ctx, id, CurrencyCoin, -60, "test", ""); err != nil { t.Fatal(err) } if c := coinsOf(t, svc, id); c != 40 { t.Fatalf("want 40, got %d", c) } } func TestDailyCooldown(t *testing.T) { svc, id := setup(t) ctx := context.Background() // اولین دریافت: استریک ۱، جایزه = DailyReward. if amt, streak, _, err := svc.ClaimDaily(ctx, id); err != nil || amt != DailyReward || streak != 1 { t.Fatalf("first claim: amt=%d streak=%d err=%v", amt, streak, err) } if _, _, _, err := svc.ClaimDaily(ctx, id); err != ErrTooSoon { t.Fatalf("want ErrTooSoon, got %v", err) } if c := coinsOf(t, svc, id); c != DailyReward { t.Fatalf("want %d coins, got %d", DailyReward, c) } } func TestDailyStreakContinuesAndResets(t *testing.T) { svc, id := setup(t) ctx := context.Background() // دیروز گرفته ⇒ امروز استریک به ۲ می‌رسد و جایزه بیشتر می‌شود. ySet := func(daysAgo int) { ts := nowUTC().Add(time.Duration(-daysAgo) * 24 * time.Hour).Format(tsLayout) if _, err := svc.db.Exec(`UPDATE users SET last_daily_at=?, daily_streak=1 WHERE id=?`, ts, id); err != nil { t.Fatal(err) } } ySet(1) amt, streak, _, err := svc.ClaimDaily(ctx, id) if err != nil || streak != 2 || amt != streakReward(2) { t.Fatalf("continue: amt=%d streak=%d err=%v", amt, streak, err) } // دو روز فاصله ⇒ استریک ریست به ۱. if _, err := svc.db.Exec( `UPDATE users SET last_daily_at=?, daily_streak=5 WHERE id=?`, nowUTC().Add(-48*time.Hour).Format(tsLayout), id); err != nil { t.Fatal(err) } _, streak, _, err = svc.ClaimDaily(ctx, id) if err != nil || streak != 1 { t.Fatalf("reset: streak=%d err=%v", streak, err) } } func TestAdRewardDedupeAndCredit(t *testing.T) { svc, id := setup(t) ctx := context.Background() if amt, err := svc.ClaimAdReward(ctx, id, "tok-1"); err != nil || amt != AdReward { t.Fatalf("claim: amt=%d err=%v", amt, err) } // همان توکن دوباره ⇒ تکراری if _, err := svc.ClaimAdReward(ctx, id, "tok-1"); err != ErrAdDuplicate { t.Fatalf("want ErrAdDuplicate, got %v", err) } // توکن خالی ⇒ تأیید نشده if _, err := svc.ClaimAdReward(ctx, id, ""); err != ErrAdNotVerified { t.Fatalf("want ErrAdNotVerified, got %v", err) } if c := coinsOf(t, svc, id); c != AdReward { t.Fatalf("want %d, got %d", AdReward, c) } } func TestBuyAndSelectCard(t *testing.T) { svc, id := setup(t) ctx := context.Background() setCoins(t, svc, id, 10000) // انتخاب کارتی که مالکش نیست ⇒ خطا if err := svc.SelectCard(ctx, id, "wizard"); err != ErrNotOwned { t.Fatalf("want ErrNotOwned, got %v", err) } // خرید کارت جادوگر (۱۰۰۰۰) if err := svc.BuyCard(ctx, id, "wizard"); err != nil { t.Fatalf("buy: %v", err) } if c := coinsOf(t, svc, id); c != 0 { t.Fatalf("want 0 after buy, got %d", c) } // خرید دوباره ⇒ قبلاً مالک است if err := svc.BuyCard(ctx, id, "wizard"); err != ErrAlreadyOwned { t.Fatalf("want ErrAlreadyOwned, got %v", err) } // حالا قابل انتخاب است if err := svc.SelectCard(ctx, id, "wizard"); err != nil { t.Fatalf("select: %v", err) } p, _ := svc.GetProfile(ctx, id) if p.SelectedCard != "wizard" { t.Fatalf("selected = %s", p.SelectedCard) } // خرید بدون سکه کافی ⇒ insufficient if err := svc.BuyCard(ctx, id, "epic"); err != ErrInsufficient { t.Fatalf("want ErrInsufficient, got %v", err) } } func TestPurchaseCreditsAndIdempotent(t *testing.T) { svc, id := setup(t) ctx := context.Background() req := PurchaseRequest{Store: "bazaar", Kind: "coin", ProductID: "small", Token: "pt-1"} if err := svc.VerifyPurchase(ctx, id, req); err != nil { t.Fatalf("purchase: %v", err) } pkg := svc.findCoinPackage("small") if c := coinsOf(t, svc, id); c != pkg.Coins { t.Fatalf("want %d coins, got %d", pkg.Coins, c) } // نوع و قیمتِ خرید باید ثبت شده باشند (پنلِ تراکنش‌ها). var kind string var priceToman int if err := svc.db.QueryRow(`SELECT kind, price_toman FROM purchases WHERE user_id=?`, id). Scan(&kind, &priceToman); err != nil { t.Fatalf("read purchase: %v", err) } if kind != "coin" || priceToman != pkg.PriceToman { t.Fatalf("want kind=coin price=%d, got kind=%q price=%d", pkg.PriceToman, kind, priceToman) } // همان توکن دوباره ⇒ قبلاً پردازش شده، بدون شارژ مجدد if err := svc.VerifyPurchase(ctx, id, req); err != ErrAlreadyOwned { t.Fatalf("want ErrAlreadyOwned, got %v", err) } if c := coinsOf(t, svc, id); c != pkg.Coins { t.Fatalf("double credit! got %d", c) } } func TestGameSettlement(t *testing.T) { svc, id := setup(t) ctx := context.Background() setCoins(t, svc, id, 1000) // کسر ورودی if err := svc.ChargeEntry(ctx, id, 100, "r1"); err != nil { t.Fatalf("charge: %v", err) } if c := coinsOf(t, svc, id); c != 900 { t.Fatalf("after entry want 900, got %d", c) } // جایزه برنده if err := svc.AwardWinner(ctx, id, 250, 15, 50, "r1"); err != nil { t.Fatalf("award: %v", err) } p, _ := svc.GetProfile(ctx, id) if p.Coins != 1150 { t.Fatalf("after prize want 1150, got %d", p.Coins) } if p.XP != 15 { t.Fatalf("want 15 xp, got %d", p.XP) } if p.Trophies != 50 { t.Fatalf("want 50 trophies, got %d", p.Trophies) } }