301 lines
11 KiB
Go
301 lines
11 KiB
Go
package gift
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"regexp"
|
|
"testing"
|
|
|
|
bizgift "kra/internal/biz/gift"
|
|
"kra/internal/data/ledger"
|
|
|
|
"github.com/glebarez/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type testProvider struct{ db *gorm.DB }
|
|
|
|
func (p testProvider) DB() *gorm.DB { return p.db }
|
|
|
|
func openGiftTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.AutoMigrate(&giftPO{}, &ledger.WalletPO{}, &ledger.GiftSendPO{}, &ledger.SettingPO{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, statement := range []string{
|
|
`CREATE TABLE sys_users (id integer primary key, username text, nick_name text)`,
|
|
`CREATE TABLE live_rooms (id integer primary key, name text, anchor_user_id integer, enabled numeric, deleted_at datetime)`,
|
|
} {
|
|
if err = db.Exec(statement).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return db
|
|
}
|
|
|
|
func TestGiftRepoProtectsLastEnabledGift(t *testing.T) {
|
|
db := openGiftTestDB(t)
|
|
repo := NewRepo(testProvider{db: db})
|
|
item, err := repo.Save(context.Background(), &bizgift.Gift{Name: "Rose", Icon: "rose", Price: 1, Tiers: []int{1}, Enabled: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = repo.SetEnabled(context.Background(), item.ID, false); !errors.Is(err, bizgift.ErrLastEnabled) {
|
|
t.Fatalf("disable last gift error = %v", err)
|
|
}
|
|
if err = repo.Delete(context.Background(), item.ID); !errors.Is(err, bizgift.ErrLastEnabled) {
|
|
t.Fatalf("delete last gift error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGiftRepoSendUsesTierAndDebitsAtomically(t *testing.T) {
|
|
db := openGiftTestDB(t)
|
|
repo := NewRepo(testProvider{db: db})
|
|
if err := db.Exec(`INSERT INTO sys_users(id,username,nick_name) VALUES (1,'viewer','Viewer'),(2,'anchor','Anchor')`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Exec(`INSERT INTO live_rooms(id,name,anchor_user_id,enabled) VALUES (7,'Room',2,1)`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(&ledger.WalletPO{UserID: 1, DiamondBalance: 100}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
item, err := repo.Save(context.Background(), &bizgift.Gift{Name: "Star", Icon: "star", Price: 5, Tiers: []int{1, 10}, Enabled: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = repo.Send(context.Background(), 1, 7, item.ID, 2); err != nil {
|
|
t.Fatalf("custom quantity should be accepted: %v", err)
|
|
}
|
|
var before ledger.WalletPO
|
|
if err = db.Where("user_id = ?", 1).First(&before).Error; err != nil || before.DiamondBalance != 90 {
|
|
t.Fatalf("wallet did not debit custom quantity: wallet=%+v err=%v", before, err)
|
|
}
|
|
if _, err = repo.Send(context.Background(), 1, 7, item.ID, 10); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var after ledger.WalletPO
|
|
var sends int64
|
|
db.Where("user_id = ?", 1).First(&after)
|
|
db.Model(&ledger.GiftSendPO{}).Count(&sends)
|
|
if after.DiamondBalance != 40 || after.TotalSpentDiamonds != 60 || sends != 2 {
|
|
t.Fatalf("atomic send result wallet=%+v sends=%d", after, sends)
|
|
}
|
|
}
|
|
|
|
func TestSeedDefaultCatalogAddsMissingGiftsWithoutOverwriting(t *testing.T) {
|
|
db := openGiftTestDB(t)
|
|
custom := giftPO{Name: "玫瑰", Icon: "custom", Price: 99, Description: "admin configured", Tiers: "[1]", Sort: 88, Enabled: false}
|
|
if err := db.Create(&custom).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Model(&giftPO{}).Where("id = ?", custom.ID).Update("enabled", false).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := seedDefaultCatalog(db); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := seedDefaultCatalog(db); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var count int64
|
|
if err := db.Model(&giftPO{}).Count(&count).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 24 {
|
|
t.Fatalf("gift count = %d, want 24", count)
|
|
}
|
|
var rose giftPO
|
|
if err := db.Where("name = ?", "玫瑰").First(&rose).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rose.Icon != "custom" || rose.Price != 99 || rose.Sort != 88 || rose.Enabled {
|
|
t.Fatalf("existing gift was overwritten: %+v", rose)
|
|
}
|
|
}
|
|
|
|
func TestGiftRepoInsufficientBalanceDoesNotWriteLedger(t *testing.T) {
|
|
db := openGiftTestDB(t)
|
|
repo := NewRepo(testProvider{db: db})
|
|
if err := db.Exec(`INSERT INTO sys_users(id,username,nick_name) VALUES (1,'viewer','Viewer'),(2,'anchor','Anchor')`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Exec(`INSERT INTO live_rooms(id,name,anchor_user_id,enabled) VALUES (7,'Room',2,1)`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(&ledger.WalletPO{UserID: 1, DiamondBalance: 11}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
item, err := repo.Save(context.Background(), &bizgift.Gift{Name: "Star", Icon: "star", Price: 7, Tiers: []int{1, 10}, Enabled: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = repo.Send(context.Background(), 1, 7, item.ID, 3)
|
|
var insufficient *bizgift.InsufficientBalanceError
|
|
if !errors.Is(err, bizgift.ErrInsufficientBalance) || !errors.As(err, &insufficient) {
|
|
t.Fatalf("insufficient balance error = %v", err)
|
|
}
|
|
if insufficient.Required != 21 || insufficient.Balance != 11 || insufficient.Missing != 10 {
|
|
t.Fatalf("incorrect recharge shortfall: %+v", insufficient)
|
|
}
|
|
var wallet ledger.WalletPO
|
|
if err := db.Where("user_id = ?", 1).First(&wallet).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var sends int64
|
|
if err := db.Model(&ledger.GiftSendPO{}).Count(&sends).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if wallet.DiamondBalance != 11 || wallet.TotalSpentDiamonds != 0 || sends != 0 {
|
|
t.Fatalf("failed gift changed ledger: wallet=%+v sends=%d", wallet, sends)
|
|
}
|
|
}
|
|
|
|
func TestGiftRepoWalletFailureRollsBackGiftReceipt(t *testing.T) {
|
|
db := openGiftTestDB(t)
|
|
repo := NewRepo(testProvider{db: db})
|
|
if err := db.Exec(`INSERT INTO sys_users(id,username,nick_name) VALUES (1,'viewer','Viewer'),(2,'anchor','Anchor')`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Exec(`INSERT INTO live_rooms(id,name,anchor_user_id,enabled) VALUES (7,'Room',2,1)`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(&ledger.WalletPO{UserID: 1, DiamondBalance: 100}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
item, err := repo.Save(context.Background(), &bizgift.Gift{Name: "Star", Icon: "star", Price: 7, Tiers: []int{1, 10}, Enabled: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Exec(`CREATE TRIGGER reject_wallet_update BEFORE UPDATE ON live_wallets BEGIN SELECT RAISE(ABORT, 'forced wallet failure'); END`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := repo.Send(context.Background(), 1, 7, item.ID, 3); err == nil {
|
|
t.Fatal("forced wallet update failure was ignored")
|
|
}
|
|
var wallet ledger.WalletPO
|
|
if err := db.Where("user_id = ?", 1).First(&wallet).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var sends int64
|
|
if err := db.Model(&ledger.GiftSendPO{}).Count(&sends).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if wallet.DiamondBalance != 100 || wallet.TotalSpentDiamonds != 0 || sends != 0 {
|
|
t.Fatalf("wallet failure was not rolled back: wallet=%+v sends=%d", wallet, sends)
|
|
}
|
|
}
|
|
|
|
func TestGiftRepoCreateDisabledGiftCannotExposeOrDebitIt(t *testing.T) {
|
|
db := openGiftTestDB(t)
|
|
if err := db.Table("live_gifts").AutoMigrate(&struct {
|
|
Enabled bool `gorm:"default:true"`
|
|
}{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repo := NewRepo(testProvider{db: db})
|
|
ctx := context.Background()
|
|
active, err := repo.Save(ctx, &bizgift.Gift{Name: "Active", Icon: "active.png", Price: 1, Tiers: []int{1}, Enabled: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
disabled, err := repo.Save(ctx, &bizgift.Gift{Name: "Disabled", Icon: "disabled.png", Price: 5, Tiers: []int{1}, Enabled: false})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if disabled.Enabled {
|
|
t.Fatal("a newly created disabled gift was enabled")
|
|
}
|
|
reloaded, err := repo.Get(ctx, disabled.ID)
|
|
if err != nil || reloaded.Enabled {
|
|
t.Fatalf("reloaded disabled gift=%+v error=%v", reloaded, err)
|
|
}
|
|
enabled := true
|
|
items, total, err := repo.List(ctx, bizgift.Filter{Enabled: &enabled}, bizgift.ListOptions{Limit: 10})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if total != 1 || len(items) != 1 || items[0].ID != active.ID {
|
|
t.Fatalf("public gifts include a disabled gift: total=%d items=%+v", total, items)
|
|
}
|
|
if err = repo.SetEnabled(ctx, active.ID, false); !errors.Is(err, bizgift.ErrLastEnabled) {
|
|
t.Fatalf("disabled gift counted as an enabled alternative: error=%v", err)
|
|
}
|
|
if err = db.Exec(`INSERT INTO sys_users(id,username,nick_name) VALUES (1,'viewer','Viewer'),(2,'anchor','Anchor')`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.Exec(`INSERT INTO live_rooms(id,name,anchor_user_id,enabled) VALUES (7,'Room',2,1)`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.Create(&ledger.WalletPO{UserID: 1, DiamondBalance: 100}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = repo.Send(ctx, 1, 7, disabled.ID, 1); !errors.Is(err, bizgift.ErrSendNotAllowed) {
|
|
t.Fatalf("disabled gift could be sent: error=%v", err)
|
|
}
|
|
var wallet ledger.WalletPO
|
|
if err = db.Where("user_id = ?", 1).First(&wallet).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var sends int64
|
|
if err = db.Model(&ledger.GiftSendPO{}).Count(&sends).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if wallet.DiamondBalance != 100 || wallet.TotalSpentDiamonds != 0 || sends != 0 {
|
|
t.Fatalf("disabled gift changed the ledger: wallet=%+v sends=%d", wallet, sends)
|
|
}
|
|
}
|
|
|
|
func TestGiftRepoConsecutiveSendsHaveUniqueShortSerialNumbers(t *testing.T) {
|
|
db := openGiftTestDB(t)
|
|
ctx := context.Background()
|
|
repo := NewRepo(testProvider{db: db})
|
|
if err := db.Exec(`INSERT INTO sys_users(id,username,nick_name) VALUES (1,'viewer','Viewer'),(2,'anchor','Anchor')`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Exec(`INSERT INTO live_rooms(id,name,anchor_user_id,enabled) VALUES (7,'Room',2,1)`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const count = 100
|
|
if err := db.Create(&ledger.WalletPO{UserID: 1, DiamondBalance: count}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(&ledger.SettingPO{Key: "diamond_rate_cents", Value: "10"}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gift, err := repo.Save(ctx, &bizgift.Gift{Name: "Gift", Icon: "gift.png", Price: 1, Tiers: []int{1}, Enabled: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pattern := regexp.MustCompile(`^DS[A-Z2-7]{26}$`)
|
|
seen := make(map[string]struct{}, count)
|
|
for index := 0; index < count; index++ {
|
|
sent, err := repo.Send(ctx, 1, 7, gift.ID, 1)
|
|
if err != nil {
|
|
t.Fatalf("gift %d failed: %v", index, err)
|
|
}
|
|
if !pattern.MatchString(sent.SerialNo) || len(sent.SerialNo) > 32 {
|
|
t.Fatalf("invalid gift serial: %q", sent.SerialNo)
|
|
}
|
|
if _, exists := seen[sent.SerialNo]; exists {
|
|
t.Fatalf("duplicate gift serial: %q", sent.SerialNo)
|
|
}
|
|
seen[sent.SerialNo] = struct{}{}
|
|
}
|
|
var receipts int64
|
|
if err := db.Model(&ledger.GiftSendPO{}).Count(&receipts).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var wallet ledger.WalletPO
|
|
if err := db.Where("user_id = ?", 1).First(&wallet).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if receipts != count || wallet.DiamondBalance != 0 || wallet.TotalSpentDiamonds != count {
|
|
t.Fatalf("consecutive gifts did not reconcile: receipts=%d wallet=%+v", receipts, wallet)
|
|
}
|
|
}
|