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