Add admin endpoints for affiliate overview, invites, rebates, transfers, user overrides, batch rate updates, and global rebate config.
239 lines
7.1 KiB
Go
239 lines
7.1 KiB
Go
package controller
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/model"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/glebarez/sqlite"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type affiliateAPIResponse struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message"`
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
|
|
type affiliatePageResponse struct {
|
|
Total int `json:"total"`
|
|
Items json.RawMessage `json:"items"`
|
|
}
|
|
|
|
func setupAffiliateControllerTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
|
|
previousDB := model.DB
|
|
previousLogDB := model.LOG_DB
|
|
common.OptionMapRWMutex.RLock()
|
|
previousOptionMap := common.OptionMap
|
|
common.OptionMapRWMutex.RUnlock()
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
common.UsingSQLite = true
|
|
common.UsingMySQL = false
|
|
common.UsingPostgreSQL = false
|
|
common.RedisEnabled = false
|
|
|
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", strings.ReplaceAll(t.Name(), "/", "_"))
|
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(&model.User{}, &model.RebateRecord{}, &model.Option{}, &model.Log{}))
|
|
|
|
model.DB = db
|
|
model.LOG_DB = db
|
|
common.OptionMapRWMutex.Lock()
|
|
common.OptionMap = map[string]string{
|
|
model.AffRebateRatePercentKey: "0",
|
|
model.AffRebateFrozenDaysKey: "0",
|
|
model.AffRebateEnabledKey: "false",
|
|
}
|
|
common.OptionMapRWMutex.Unlock()
|
|
|
|
t.Cleanup(func() {
|
|
model.DB = previousDB
|
|
model.LOG_DB = previousLogDB
|
|
common.OptionMapRWMutex.Lock()
|
|
common.OptionMap = previousOptionMap
|
|
common.OptionMapRWMutex.Unlock()
|
|
sqlDB, err := db.DB()
|
|
if err == nil {
|
|
_ = sqlDB.Close()
|
|
}
|
|
})
|
|
|
|
return db
|
|
}
|
|
|
|
func performAffiliateRequest(t *testing.T, handler gin.HandlerFunc, method string, target string, body any, params ...gin.Param) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
|
|
var requestBody *bytes.Reader
|
|
if body == nil {
|
|
requestBody = bytes.NewReader(nil)
|
|
} else {
|
|
bodyBytes, err := common.Marshal(body)
|
|
require.NoError(t, err)
|
|
requestBody = bytes.NewReader(bodyBytes)
|
|
}
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(method, target, requestBody)
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
ctx.Params = params
|
|
handler(ctx)
|
|
return recorder
|
|
}
|
|
|
|
func decodeAffiliateAPIResponse(t *testing.T, recorder *httptest.ResponseRecorder) affiliateAPIResponse {
|
|
t.Helper()
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
var payload affiliateAPIResponse
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &payload))
|
|
return payload
|
|
}
|
|
|
|
func TestAdminUpdateAffiliateConfigRejectsNegativeValues(t *testing.T) {
|
|
setupAffiliateControllerTestDB(t)
|
|
|
|
recorder := performAffiliateRequest(t, AdminUpdateAffiliateConfig, http.MethodPut, "/api/admin/affiliates/config", gin.H{
|
|
"aff_rebate_rate_percent": -1,
|
|
})
|
|
|
|
payload := decodeAffiliateAPIResponse(t, recorder)
|
|
require.False(t, payload.Success)
|
|
require.Contains(t, payload.Message, "比例")
|
|
}
|
|
|
|
func TestAdminUpdateAffiliateConfigPersistsOptionMap(t *testing.T) {
|
|
setupAffiliateControllerTestDB(t)
|
|
|
|
recorder := performAffiliateRequest(t, AdminUpdateAffiliateConfig, http.MethodPut, "/api/admin/affiliates/config", gin.H{
|
|
"aff_rebate_rate_percent": 12,
|
|
"aff_rebate_frozen_days": 30,
|
|
"aff_rebate_enabled": true,
|
|
})
|
|
|
|
payload := decodeAffiliateAPIResponse(t, recorder)
|
|
require.True(t, payload.Success)
|
|
common.OptionMapRWMutex.RLock()
|
|
defer common.OptionMapRWMutex.RUnlock()
|
|
require.Equal(t, "12", common.OptionMap[model.AffRebateRatePercentKey])
|
|
require.Equal(t, "30", common.OptionMap[model.AffRebateFrozenDaysKey])
|
|
require.Equal(t, "true", common.OptionMap[model.AffRebateEnabledKey])
|
|
}
|
|
|
|
func TestAdminBatchUpdateAffiliateRateRejectsTooManyUsers(t *testing.T) {
|
|
setupAffiliateControllerTestDB(t)
|
|
|
|
userIds := make([]int, 1001)
|
|
for i := range userIds {
|
|
userIds[i] = i + 1
|
|
}
|
|
recorder := performAffiliateRequest(t, AdminBatchUpdateAffiliateRate, http.MethodPost, "/api/admin/affiliates/users/batch-rate", gin.H{
|
|
"user_ids": userIds,
|
|
"aff_rebate_rate_percent": 20,
|
|
})
|
|
|
|
payload := decodeAffiliateAPIResponse(t, recorder)
|
|
require.False(t, payload.Success)
|
|
require.Contains(t, payload.Message, "1000")
|
|
}
|
|
|
|
func TestAdminUpdateAffiliateUserSettingsPersistsFields(t *testing.T) {
|
|
db := setupAffiliateControllerTestDB(t)
|
|
require.NoError(t, db.Create(&model.User{
|
|
Id: 11,
|
|
Username: "affiliate_admin_user",
|
|
Password: "password123",
|
|
Status: common.UserStatusEnabled,
|
|
}).Error)
|
|
|
|
recorder := performAffiliateRequest(t, AdminUpdateAffiliateUserSettings, http.MethodPut, "/api/admin/affiliates/users/11", gin.H{
|
|
"aff_rebate_rate_percent": 25,
|
|
"aff_rebate_frozen_days": 7,
|
|
}, gin.Param{Key: "user_id", Value: "11"})
|
|
|
|
payload := decodeAffiliateAPIResponse(t, recorder)
|
|
require.True(t, payload.Success)
|
|
var user model.User
|
|
require.NoError(t, db.First(&user, 11).Error)
|
|
require.Equal(t, 25, user.AffRebateRatePercent)
|
|
require.Equal(t, 7, user.AffRebateFrozenDays)
|
|
}
|
|
|
|
func TestAdminGetAffiliateInvitesReturnsInvitePairs(t *testing.T) {
|
|
db := setupAffiliateControllerTestDB(t)
|
|
require.NoError(t, db.Create(&model.User{
|
|
Id: 21,
|
|
Username: "inviter_user",
|
|
Password: "password123",
|
|
Status: common.UserStatusEnabled,
|
|
AffCode: "aff21",
|
|
CreatedAt: 100,
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.User{
|
|
Id: 22,
|
|
Username: "invitee_user",
|
|
Password: "password123",
|
|
Status: common.UserStatusEnabled,
|
|
AffCode: "aff22",
|
|
InviterId: 21,
|
|
CreatedAt: 200,
|
|
}).Error)
|
|
|
|
recorder := performAffiliateRequest(t, AdminGetAffiliateInvites, http.MethodGet, "/api/admin/affiliates/invites?user_id=21", nil)
|
|
|
|
payload := decodeAffiliateAPIResponse(t, recorder)
|
|
require.True(t, payload.Success)
|
|
var page affiliatePageResponse
|
|
require.NoError(t, common.Unmarshal(payload.Data, &page))
|
|
require.Equal(t, 1, page.Total)
|
|
var invites []model.AffiliateInvite
|
|
require.NoError(t, common.Unmarshal(page.Items, &invites))
|
|
require.Len(t, invites, 1)
|
|
require.Equal(t, 22, invites[0].Invitee.Id)
|
|
require.Equal(t, 21, invites[0].Inviter.Id)
|
|
}
|
|
|
|
func TestAdminGetAffiliateTransfersReturnsTransferLogs(t *testing.T) {
|
|
db := setupAffiliateControllerTestDB(t)
|
|
require.NoError(t, db.Create(&model.User{
|
|
Id: 31,
|
|
Username: "transfer_user",
|
|
Password: "password123",
|
|
Status: common.UserStatusEnabled,
|
|
AffCode: "aff31",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.Log{
|
|
UserId: 31,
|
|
Username: "transfer_user",
|
|
CreatedAt: 300,
|
|
Type: model.LogTypeSystem,
|
|
Content: "邀请额度转入余额 $1.00",
|
|
}).Error)
|
|
|
|
recorder := performAffiliateRequest(t, AdminGetAffiliateTransfers, http.MethodGet, "/api/admin/affiliates/transfers?user_id=31", nil)
|
|
|
|
payload := decodeAffiliateAPIResponse(t, recorder)
|
|
require.True(t, payload.Success)
|
|
var page affiliatePageResponse
|
|
require.NoError(t, common.Unmarshal(payload.Data, &page))
|
|
require.Equal(t, 1, page.Total)
|
|
var logs []model.Log
|
|
require.NoError(t, common.Unmarshal(page.Items, &logs))
|
|
require.Len(t, logs, 1)
|
|
require.Equal(t, 31, logs[0].UserId)
|
|
require.Contains(t, logs[0].Content, "邀请额度转入余额")
|
|
}
|