Files
new-api/controller/topup_quota_limit_test.go
T
Seefs a073f74b38 refactor: deprecate int32 (#7025)
* refactor: deprecate int32

* fix(db): reject legacy user quota schemas at startup

* fix(quota): enforce wallet bounds and saturating billing conversions

* fix(rate-limit): keep count*duration from wrapping int64

* fix: error message
2026-08-26 20:57:54 +08:00

204 lines
6.4 KiB
Go

package controller
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func TestTopUpQuotaValidation(t *testing.T) {
oldQuotaPerUnit := common.QuotaPerUnit
oldDisplayType := operation_setting.GetGeneralSetting().QuotaDisplayType
common.QuotaPerUnit = 500000
t.Cleanup(func() {
common.QuotaPerUnit = oldQuotaPerUnit
operation_setting.GetGeneralSetting().QuotaDisplayType = oldDisplayType
})
testCases := []struct {
name string
displayType string
amount int64
wantQuota int
wantErr bool
}{
{
name: "currency amount below limit",
displayType: operation_setting.QuotaDisplayTypeUSD,
amount: 4294,
wantQuota: 2_147_000_000,
},
{
name: "currency amount above limit",
displayType: operation_setting.QuotaDisplayTypeUSD,
amount: 4295,
wantQuota: 2_147_500_000,
},
{
name: "token amount preserves settlement truncation",
displayType: operation_setting.QuotaDisplayTypeTokens,
amount: 2_147_500_000,
wantQuota: 2_147_500_000,
},
{
name: "token amount above legacy int32 range",
displayType: operation_setting.QuotaDisplayTypeTokens,
amount: 4_294_500_000,
wantQuota: 4_294_500_000,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
operation_setting.GetGeneralSetting().QuotaDisplayType = tc.displayType
quota, err := getTopUpQuota(tc.amount)
if tc.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tc.wantQuota, quota)
})
}
}
func TestValidateTopUpQuotaReturnsMaximumAmount(t *testing.T) {
oldQuotaPerUnit := common.QuotaPerUnit
oldDisplayType := operation_setting.GetGeneralSetting().QuotaDisplayType
common.QuotaPerUnit = 500000
operation_setting.GetGeneralSetting().QuotaDisplayType = operation_setting.QuotaDisplayTypeUSD
t.Cleanup(func() {
common.QuotaPerUnit = oldQuotaPerUnit
operation_setting.GetGeneralSetting().QuotaDisplayType = oldDisplayType
})
maxAmount := decimal.NewFromInt(common.MaxWalletQuota).
Div(decimal.NewFromFloat(common.QuotaPerUnit)).
Floor().IntPart()
_, err := validateTopUpQuota(maxAmount)
require.NoError(t, err)
_, err = validateTopUpQuota(maxAmount + 1)
require.EqualError(t, err, fmt.Sprintf("单笔充值数量不能大于 %d", maxAmount))
}
func TestRequestAmountRejectsTopUpThatCannotBeSettled(t *testing.T) {
oldQuotaPerUnit := common.QuotaPerUnit
oldDisplayType := operation_setting.GetGeneralSetting().QuotaDisplayType
common.QuotaPerUnit = 500000
operation_setting.GetGeneralSetting().QuotaDisplayType = operation_setting.QuotaDisplayTypeUSD
t.Cleanup(func() {
common.QuotaPerUnit = oldQuotaPerUnit
operation_setting.GetGeneralSetting().QuotaDisplayType = oldDisplayType
})
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
maxAmount := decimal.NewFromInt(common.MaxWalletQuota).
Div(decimal.NewFromFloat(common.QuotaPerUnit)).
Floor().IntPart()
ctx.Request = httptest.NewRequest(
http.MethodPost,
"/api/user/amount",
strings.NewReader(fmt.Sprintf(`{"amount":%d}`, maxAmount+1)),
)
ctx.Request.Header.Set("Content-Type", "application/json")
RequestAmount(ctx)
assert.Equal(t, http.StatusOK, recorder.Code)
assert.JSONEq(t, fmt.Sprintf(`{"message":"error","data":"单笔充值数量不能大于 %d"}`, maxAmount), recorder.Body.String())
}
func TestRequestAmountRejectsTopUpThatWouldOverflowWallet(t *testing.T) {
oldQuotaPerUnit := common.QuotaPerUnit
oldDisplayType := operation_setting.GetGeneralSetting().QuotaDisplayType
oldDB := model.DB
common.QuotaPerUnit = 500000
operation_setting.GetGeneralSetting().QuotaDisplayType = operation_setting.QuotaDisplayTypeUSD
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&model.User{}))
model.DB = db
t.Cleanup(func() {
common.QuotaPerUnit = oldQuotaPerUnit
operation_setting.GetGeneralSetting().QuotaDisplayType = oldDisplayType
model.DB = oldDB
sqlDB, dbErr := db.DB()
if dbErr == nil {
require.NoError(t, sqlDB.Close())
}
})
require.NoError(t, model.DB.Create(&model.User{
Id: 42,
Username: "topup_capacity_user",
Quota: common.MaxWalletQuota - 100_000,
Status: common.UserStatusEnabled,
}).Error)
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Set("id", 42)
ctx.Request = httptest.NewRequest(
http.MethodPost,
"/api/user/amount",
strings.NewReader(`{"amount":1}`),
)
ctx.Request.Header.Set("Content-Type", "application/json")
RequestAmount(ctx)
assert.Equal(t, http.StatusOK, recorder.Code)
assert.JSONEq(t, `{"message":"error","data":"top-up quota limit exceeded"}`, recorder.Body.String())
}
func TestValidateCreditedQuotaRejectsOverflow(t *testing.T) {
_, err := validateCreditedQuota(decimal.NewFromInt(int64(common.MaxWalletQuota / 2)))
require.NoError(t, err)
_, err = validateCreditedQuota(decimal.Zero)
require.EqualError(t, err, "充值额度必须大于 0")
_, err = validateCreditedQuota(decimal.NewFromInt(common.MaxWalletQuota + 1))
require.EqualError(
t,
err,
"充值额度超出系统可表示范围",
)
}
func TestStripeCreditedQuotaIncludesGroupRatio(t *testing.T) {
oldQuotaPerUnit := common.QuotaPerUnit
oldTopupGroupRatio := common.TopupGroupRatio2JSONString()
common.QuotaPerUnit = 500000
require.NoError(t, common.UpdateTopupGroupRatioByJSONString(`{"vip":2}`))
t.Cleanup(func() {
common.QuotaPerUnit = oldQuotaPerUnit
require.NoError(t, common.UpdateTopupGroupRatioByJSONString(oldTopupGroupRatio))
})
_, err := validateCreditedQuota(getStripeCreditedQuota(2147, "vip"))
require.NoError(t, err)
_, err = validateCreditedQuota(getStripeCreditedQuota(2148, "vip"))
require.NoError(t, err)
_, err = validateCreditedQuota(getStripeCreditedQuota(int64(common.MaxWalletQuota), "vip"))
require.Error(t, err)
require.NoError(t, common.UpdateTopupGroupRatioByJSONString(`{"free":0}`))
assert.True(t, decimal.NewFromInt(500000).Equal(getStripeCreditedQuota(1, "free")))
}