package controller import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/service/region_sync" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" ) func TestReceiveSyncedUserCreate_Success(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() // 使用测试专用的认证中间件,绕过系统设置检查 router.Use(testSyncAuthMiddleware()) router.POST("/api/internal/sync/user/create", ReceiveSyncedUserCreate) origDB := model.DB defer func() { model.DB = origDB }() // 设置测试数据库 db := setupSyncTestDB(t) model.DB = db req := region_sync.SyncUserRequest{ Username: "testuser", Email: "test@example.com", PasswordHash: "hashedpassword", DisplayName: "Test User", Quota: 10000, RemoteUserId: 10000001, Group: "default", } reqBody, _ := json.Marshal(req) reqHTTP, _ := http.NewRequest("POST", "/api/internal/sync/user/create", bytes.NewBuffer(reqBody)) reqHTTP.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, reqHTTP) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) assert.True(t, resp["success"].(bool)) // 验证用户已创建 var user model.User err := db.Where("username = ?", "testuser").First(&user).Error require.NoError(t, err) assert.Equal(t, "testuser", user.Username) assert.Equal(t, 10000001, user.RemoteUserId) assert.Equal(t, "synced", user.Source) } func TestReceiveQuotaUpdate_Success(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(testSyncAuthMiddleware()) router.POST("/api/internal/sync/quota/update", ReceiveQuotaUpdate) origDB := model.DB defer func() { model.DB = origDB }() // 设置测试数据库 db := setupSyncTestDB(t) model.DB = db // 创建测试用户 user := model.User{ Username: "testuser", RemoteUserId: 10000001, Source: "synced", Quota: 5000, SyncedQuota: 5000, AffCode: "aff-qu-1", } db.Create(&user) req := region_sync.UpdateQuotaRequest{ RemoteUserId: 10000001, Quota: 10000, } reqBody, _ := json.Marshal(req) reqHTTP, _ := http.NewRequest("POST", "/api/internal/sync/quota/update", bytes.NewBuffer(reqBody)) reqHTTP.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, reqHTTP) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) assert.True(t, resp["success"].(bool)) // 验证余额已更新 var updatedUser model.User err := db.Where("remote_user_id = ?", 10000001).First(&updatedUser).Error require.NoError(t, err) assert.Equal(t, 10000, updatedUser.SyncedQuota) } func TestQueryUserQuota_Success(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(testSyncAuthMiddleware()) router.POST("/api/internal/sync/quota/query", QueryUserQuota) origDB := model.DB defer func() { model.DB = origDB }() // 设置测试数据库 db := setupSyncTestDB(t) model.DB = db // 创建测试用户 user := model.User{ Username: "testuser", Quota: 15000, AffCode: "aff-qq-1", } db.Create(&user) req := region_sync.QueryQuotaRequest{ UserId: user.Id, } reqBody, _ := json.Marshal(req) reqHTTP, _ := http.NewRequest("POST", "/api/internal/sync/quota/query", bytes.NewBuffer(reqBody)) reqHTTP.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, reqHTTP) assert.Equal(t, http.StatusOK, w.Code) var resp region_sync.QueryQuotaResponse json.Unmarshal(w.Body.Bytes(), &resp) assert.True(t, resp.Success) assert.Equal(t, 15000, resp.Quota) } func TestBatchDeductQuota_Success(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(testSyncAuthMiddleware()) router.POST("/api/internal/sync/quota/batch-deduct", BatchDeductQuota) origDB := model.DB defer func() { model.DB = origDB }() // 设置测试数据库 db := setupSyncTestDB(t) model.DB = db // 创建测试用户 user1 := model.User{Username: "user1", Quota: 1000, AffCode: "aff-bd-1"} user2 := model.User{Username: "user2", Quota: 500, AffCode: "aff-bd-2"} db.Create(&user1) db.Create(&user2) req := region_sync.BatchDeductRequest{ Records: []region_sync.BatchDeductRecord{ {UserId: user1.Id, RequestId: "req-1", Quota: 100}, {UserId: user2.Id, RequestId: "req-2", Quota: 50}, }, } reqBody, _ := json.Marshal(req) reqHTTP, _ := http.NewRequest("POST", "/api/internal/sync/quota/batch-deduct", bytes.NewBuffer(reqBody)) reqHTTP.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, reqHTTP) assert.Equal(t, http.StatusOK, w.Code) var resp region_sync.BatchDeductResponse json.Unmarshal(w.Body.Bytes(), &resp) assert.True(t, resp.Success) assert.Len(t, resp.Results, 2) // 验证用户余额已更新 var updatedUser1 model.User db.First(&updatedUser1, user1.Id) assert.Equal(t, 900, updatedUser1.Quota) var updatedUser2 model.User db.First(&updatedUser2, user2.Id) assert.Equal(t, 450, updatedUser2.Quota) } func TestBatchDeductQuota_InsufficientQuota(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(testSyncAuthMiddleware()) router.POST("/api/internal/sync/quota/batch-deduct", BatchDeductQuota) origDB := model.DB defer func() { model.DB = origDB }() // 设置测试数据库 db := setupSyncTestDB(t) model.DB = db // 创建余额不足的用户 user := model.User{Username: "pooruser", Quota: 10, AffCode: "aff-poor-1"} db.Create(&user) req := region_sync.BatchDeductRequest{ Records: []region_sync.BatchDeductRecord{ {UserId: user.Id, RequestId: "req-1", Quota: 100}, }, } reqBody, _ := json.Marshal(req) reqHTTP, _ := http.NewRequest("POST", "/api/internal/sync/quota/batch-deduct", bytes.NewBuffer(reqBody)) reqHTTP.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, reqHTTP) assert.Equal(t, http.StatusOK, w.Code) var resp region_sync.BatchDeductResponse json.Unmarshal(w.Body.Bytes(), &resp) assert.True(t, resp.Success) assert.Len(t, resp.Results, 1) assert.False(t, resp.Results[0].Success) assert.Contains(t, resp.Results[0].Error, "insufficient quota") } // testSyncAuthMiddleware 测试专用的认证中间件,绕过系统设置 func testSyncAuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Set("sync_node", "test-node") c.Next() } } // setupSyncTestDB 设置同步测试数据库 func setupSyncTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) sqlDB, _ := db.DB() sqlDB.SetMaxOpenConns(1) err = db.AutoMigrate(&model.User{}, &model.QuotaSyncLog{}) require.NoError(t, err) return db } // --------------------------------------------------------------------------- // 阶段 4:Controller 边界条件测试 // --------------------------------------------------------------------------- func TestReceiveSyncedUserCreate_DuplicateUser(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(testSyncAuthMiddleware()) router.POST("/api/internal/sync/user/create", ReceiveSyncedUserCreate) origDB := model.DB defer func() { model.DB = origDB }() db := setupSyncTestDB(t) model.DB = db // 先创建一个用户 db.Create(&model.User{ Username: "dupuser", RemoteUserId: 10000001, Source: common.UserSourceSynced, Password: "hash", AffCode: "aff-dup-1", }) req := region_sync.SyncUserRequest{ Username: "dupuser", RemoteUserId: 10000001, PasswordHash: "newhash", } reqBody, _ := json.Marshal(req) reqHTTP, _ := http.NewRequest("POST", "/api/internal/sync/user/create", bytes.NewBuffer(reqBody)) reqHTTP.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, reqHTTP) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) assert.True(t, resp["success"].(bool)) assert.Contains(t, resp["message"], "already exists") } func TestReceiveSyncedUserCreate_InvalidBody(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(testSyncAuthMiddleware()) router.POST("/api/internal/sync/user/create", ReceiveSyncedUserCreate) reqHTTP, _ := http.NewRequest("POST", "/api/internal/sync/user/create", strings.NewReader("not json")) reqHTTP.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, reqHTTP) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestReceiveQuotaUpdate_UserNotFound(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(testSyncAuthMiddleware()) router.POST("/api/internal/sync/quota/update", ReceiveQuotaUpdate) origDB := model.DB defer func() { model.DB = origDB }() db := setupSyncTestDB(t) model.DB = db req := region_sync.UpdateQuotaRequest{ RemoteUserId: 99999999, Quota: 5000, } reqBody, _ := json.Marshal(req) reqHTTP, _ := http.NewRequest("POST", "/api/internal/sync/quota/update", bytes.NewBuffer(reqBody)) reqHTTP.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, reqHTTP) assert.Equal(t, http.StatusNotFound, w.Code) } func TestReceiveQuotaUpdate_InvalidBody(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(testSyncAuthMiddleware()) router.POST("/api/internal/sync/quota/update", ReceiveQuotaUpdate) reqHTTP, _ := http.NewRequest("POST", "/api/internal/sync/quota/update", strings.NewReader("invalid")) reqHTTP.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, reqHTTP) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestQueryUserQuota_UserNotFound(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(testSyncAuthMiddleware()) router.POST("/api/internal/sync/quota/query", QueryUserQuota) origDB := model.DB defer func() { model.DB = origDB }() db := setupSyncTestDB(t) model.DB = db req := region_sync.QueryQuotaRequest{UserId: 99999} reqBody, _ := json.Marshal(req) reqHTTP, _ := http.NewRequest("POST", "/api/internal/sync/quota/query", bytes.NewBuffer(reqBody)) reqHTTP.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, reqHTTP) assert.Equal(t, http.StatusNotFound, w.Code) } func TestBatchDeductQuota_EmptyRecords(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(testSyncAuthMiddleware()) router.POST("/api/internal/sync/quota/batch-deduct", BatchDeductQuota) origDB := model.DB defer func() { model.DB = origDB }() db := setupSyncTestDB(t) model.DB = db req := region_sync.BatchDeductRequest{Records: []region_sync.BatchDeductRecord{}} reqBody, _ := json.Marshal(req) reqHTTP, _ := http.NewRequest("POST", "/api/internal/sync/quota/batch-deduct", bytes.NewBuffer(reqBody)) reqHTTP.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, reqHTTP) assert.Equal(t, http.StatusOK, w.Code) var resp region_sync.BatchDeductResponse json.Unmarshal(w.Body.Bytes(), &resp) assert.True(t, resp.Success) assert.Len(t, resp.Results, 0) } func TestBatchDeductQuota_UserNotFound(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(testSyncAuthMiddleware()) router.POST("/api/internal/sync/quota/batch-deduct", BatchDeductQuota) origDB := model.DB defer func() { model.DB = origDB }() db := setupSyncTestDB(t) model.DB = db req := region_sync.BatchDeductRequest{ Records: []region_sync.BatchDeductRecord{ {UserId: 99999, RequestId: "req-nf", Quota: 100}, }, } reqBody, _ := json.Marshal(req) reqHTTP, _ := http.NewRequest("POST", "/api/internal/sync/quota/batch-deduct", bytes.NewBuffer(reqBody)) reqHTTP.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, reqHTTP) assert.Equal(t, http.StatusOK, w.Code) var resp region_sync.BatchDeductResponse json.Unmarshal(w.Body.Bytes(), &resp) assert.True(t, resp.Success) assert.Len(t, resp.Results, 1) assert.False(t, resp.Results[0].Success) assert.Contains(t, resp.Results[0].Error, "user not found") } func TestBatchDeductQuota_InvalidBody(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(testSyncAuthMiddleware()) router.POST("/api/internal/sync/quota/batch-deduct", BatchDeductQuota) reqHTTP, _ := http.NewRequest("POST", "/api/internal/sync/quota/batch-deduct", strings.NewReader("bad")) reqHTTP.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, reqHTTP) assert.Equal(t, http.StatusBadRequest, w.Code) }