feat: add tournoment

This commit is contained in:
2026-07-07 15:17:33 +03:30
parent bcfaf35d73
commit e58c50d6e5
10 changed files with 744 additions and 1 deletions
+140
View File
@@ -0,0 +1,140 @@
package economy
import (
"context"
"testing"
"time"
)
// makeUser یک کاربرِ آزمایشی با شماره‌ی دلخواه می‌سازد.
func makeUser(t *testing.T, s *Service, mobile string) int64 {
t.Helper()
res, err := s.db.Exec(`INSERT INTO users (mobile) VALUES (?)`, mobile)
if err != nil {
t.Fatal(err)
}
id, _ := res.LastInsertId()
return id
}
func TestTournamentJoinPointsSettle(t *testing.T) {
ctx := context.Background()
svc, u1 := setup(t)
u2 := makeUser(t, svc, "09120000002")
setCoins(t, svc, u1, 1000)
setCoins(t, svc, u2, 1000)
// تورنومنتی که همین حالا فعال است.
id, err := svc.CreateTournament(ctx, Tournament{
Title: "شبانه",
EntryFee: 200,
Prizes: []int64{500, 200}, // رتبه۱=۵۰۰، رتبه۲=۲۰۰
StartsAt: nowUTC().Add(-time.Minute),
EndsAt: nowUTC().Add(time.Hour),
})
if err != nil {
t.Fatalf("create: %v", err)
}
// هر دو ثبت‌نام می‌کنند ⇒ ۲۰۰ سکه کسر می‌شود.
if err := svc.JoinTournament(ctx, u1, id); err != nil {
t.Fatalf("join u1: %v", err)
}
if err := svc.JoinTournament(ctx, u2, id); err != nil {
t.Fatalf("join u2: %v", err)
}
if c := coinsOf(t, svc, u1); c != 800 {
t.Errorf("entry fee not charged: coins=%d", c)
}
// ثبت‌نامِ دوباره باید خطا دهد.
if err := svc.JoinTournament(ctx, u1, id); err != ErrAlreadyJoined {
t.Errorf("expected ErrAlreadyJoined, got %v", err)
}
// u1 دو برد، u2 یک برد ⇒ u1 رتبه‌ی اول.
svc.AddTournamentPoints(ctx, u1, true)
svc.AddTournamentPoints(ctx, u1, true)
svc.AddTournamentPoints(ctx, u2, true)
svc.AddTournamentPoints(ctx, u2, false)
st, err := svc.TournamentStandings(ctx, id, 10)
if err != nil {
t.Fatalf("standings: %v", err)
}
if len(st) != 2 || st[0].UserID != u1 || st[0].Points != 200 {
t.Fatalf("bad standings: %+v", st)
}
if st[1].UserID != u2 || st[1].Points != 125 {
t.Errorf("u2 points wrong: %+v", st[1])
}
// پایانِ تورنومنت را به گذشته می‌بریم؛ تسویه جوایز را پرداخت می‌کند.
if _, err := svc.db.Exec(`UPDATE tournaments SET ends_at = ? WHERE id = ?`,
nowUTC().Add(-time.Minute).Format(tsLayout), id); err != nil {
t.Fatal(err)
}
svc.SettleTournaments(ctx)
if c := coinsOf(t, svc, u1); c != 800+500 {
t.Errorf("u1 prize not paid: coins=%d (want %d)", c, 800+500)
}
if c := coinsOf(t, svc, u2); c != 800+200 {
t.Errorf("u2 prize not paid: coins=%d (want %d)", c, 800+200)
}
// تسویه‌ی دوباره نباید دوباره پرداخت کند.
svc.SettleTournaments(ctx)
if c := coinsOf(t, svc, u1); c != 1300 {
t.Errorf("double settle paid again: coins=%d", c)
}
}
func TestListTournamentsWithRows(t *testing.T) {
// رگرسیون: با MaxOpenConns(1)، ListTournaments نباید حین باز بودنِ rows
// زیرکوئری بزند (self-deadlock). این تست با تورنومنتِ واقعی آن مسیر را می‌پیماید.
ctx := context.Background()
svc, u1 := setup(t)
setCoins(t, svc, u1, 1000)
id, err := svc.CreateTournament(ctx, Tournament{
Title: "لیست", EntryFee: 100, Prizes: []int64{500},
StartsAt: nowUTC().Add(-time.Minute), EndsAt: nowUTC().Add(time.Hour),
})
if err != nil {
t.Fatal(err)
}
if err := svc.JoinTournament(ctx, u1, id); err != nil {
t.Fatal(err)
}
svc.AddTournamentPoints(ctx, u1, true)
done := make(chan []TournamentView, 1)
go func() {
v, e := svc.ListTournaments(ctx, u1)
if e != nil {
t.Errorf("list: %v", e)
}
done <- v
}()
select {
case v := <-done:
if len(v) != 1 || !v[0].Joined || v[0].MyPoints != 100 ||
v[0].Players != 1 || v[0].MyRank != 1 {
t.Errorf("bad view: %+v", v)
}
case <-time.After(5 * time.Second):
t.Fatal("ListTournaments deadlocked (nested query on single conn)")
}
}
func TestTournamentJoinInsufficient(t *testing.T) {
ctx := context.Background()
svc, u1 := setup(t)
setCoins(t, svc, u1, 50)
id, _ := svc.CreateTournament(ctx, Tournament{
Title: "گران", EntryFee: 500, Prizes: []int64{100},
StartsAt: nowUTC().Add(-time.Minute), EndsAt: nowUTC().Add(time.Hour),
})
if err := svc.JoinTournament(ctx, u1, id); err != ErrInsufficient {
t.Errorf("expected ErrInsufficient, got %v", err)
}
}