Files

241 lines
7.1 KiB
Go

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)
}
}