new-api/controller/affiliate_test.go
zizi 886d32f255 feat: add affiliate admin API
Add admin endpoints for affiliate overview, invites, rebates, transfers, user overrides, batch rate updates, and global rebate config.
2026-05-20 14:18:06 +08:00

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, "邀请额度转入余额")
}