fix(model): centralize row locking in transactional flows
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
@@ -98,3 +99,83 @@ func TestSearchRedemptionsFiltersAndPaginates(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupRedeemFixture(t *testing.T, quota int) (userId int, key string) {
|
||||
t.Helper()
|
||||
require.NoError(t, DB.AutoMigrate(&Redemption{}))
|
||||
require.NoError(t, DB.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Delete(&Redemption{}).Error)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, DB.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Delete(&Redemption{}).Error)
|
||||
DB.Exec("DELETE FROM users")
|
||||
DB.Exec("DELETE FROM logs")
|
||||
})
|
||||
|
||||
user := &User{Username: "redeem-user", Password: "password", Status: common.UserStatusEnabled, Quota: 0}
|
||||
require.NoError(t, DB.Create(user).Error)
|
||||
|
||||
key = "10000000000000000000000000000001"
|
||||
redemption := &Redemption{
|
||||
Name: "redeem-test",
|
||||
Key: key,
|
||||
Status: common.RedemptionCodeStatusEnabled,
|
||||
Quota: quota,
|
||||
CreatedTime: common.GetTimestamp(),
|
||||
}
|
||||
require.NoError(t, DB.Create(redemption).Error)
|
||||
return user.Id, key
|
||||
}
|
||||
|
||||
func TestRedeemCreditsQuotaExactlyOnce(t *testing.T) {
|
||||
userId, key := setupRedeemFixture(t, 500)
|
||||
|
||||
quota, err := Redeem(key, userId)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 500, quota)
|
||||
|
||||
var user User
|
||||
require.NoError(t, DB.First(&user, "id = ?", userId).Error)
|
||||
assert.Equal(t, 500, user.Quota)
|
||||
|
||||
var redemption Redemption
|
||||
require.NoError(t, DB.First(&redemption, "name = ?", "redeem-test").Error)
|
||||
assert.Equal(t, common.RedemptionCodeStatusUsed, redemption.Status)
|
||||
assert.Equal(t, userId, redemption.UsedUserId)
|
||||
|
||||
// Redeeming the same code again must fail and must not credit quota.
|
||||
_, err = Redeem(key, userId)
|
||||
require.Error(t, err)
|
||||
require.NoError(t, DB.First(&user, "id = ?", userId).Error)
|
||||
assert.Equal(t, 500, user.Quota)
|
||||
}
|
||||
|
||||
// Exactly one of several concurrent redeems of the same code may win, and
|
||||
// quota must be credited exactly once.
|
||||
func TestRedeemConcurrentSingleSuccess(t *testing.T) {
|
||||
userId, key := setupRedeemFixture(t, 300)
|
||||
|
||||
const goroutines = 5
|
||||
successes := make([]bool, goroutines)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(goroutines)
|
||||
for i := 0; i < goroutines; i++ {
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
if _, err := Redeem(key, userId); err == nil {
|
||||
successes[idx] = true
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
successCount := 0
|
||||
for _, ok := range successes {
|
||||
if ok {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 1, successCount, "exactly one concurrent redeem should succeed")
|
||||
|
||||
var user User
|
||||
require.NoError(t, DB.First(&user, "id = ?", userId).Error)
|
||||
assert.Equal(t, 300, user.Quota, "quota must be credited exactly once")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user