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