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