241 lines
7.1 KiB
Go
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)
|
|
}
|
|
}
|