|
- package controller
-
- import (
- "bytes"
- "encoding/json"
- "errors"
- "net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/QuantumNous/new-api/model"
- user_migration "github.com/QuantumNous/new-api/service/user_migration"
- "github.com/gin-gonic/gin"
- "github.com/glebarez/sqlite"
- "github.com/stretchr/testify/require"
- "gorm.io/gorm"
- )
-
- func setupUserMigrationControllerDB(t *testing.T) *gorm.DB {
- t.Helper()
-
- db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
- require.NoError(t, err)
-
- sqlDB, err := db.DB()
- require.NoError(t, err)
- sqlDB.SetMaxOpenConns(1)
-
- require.NoError(t, db.AutoMigrate(
- &model.User{},
- &model.CustomOAuthProvider{},
- &model.UserOAuthBinding{},
- &model.UserMigrationBatch{},
- &model.UserMigrationItem{},
- &model.MigrationQuotaGrant{},
- ))
-
- return db
- }
-
- func testRootAuthMiddleware() gin.HandlerFunc {
- return func(c *gin.Context) {
- c.Set("id", 1)
- c.Set("role", 100)
- c.Next()
- }
- }
-
- type fakeMigrationCandidateClient struct {
- resp *user_migration.ListRemoteUsersResponse
- err error
- gotKeyword string
- gotPage int
- gotPageSize int
- }
-
- func (f *fakeMigrationCandidateClient) ListUsers(page, pageSize int, keyword string) (*user_migration.ListRemoteUsersResponse, error) {
- f.gotKeyword = keyword
- f.gotPage = page
- f.gotPageSize = pageSize
- if f.err != nil {
- return nil, f.err
- }
- return f.resp, nil
- }
-
- type fakeMigrationVerifier struct {
- results map[int]*user_migration.VerifyResult
- errs map[int]error
- }
-
- func (f *fakeMigrationVerifier) VerifyItem(item *model.UserMigrationItem) (*user_migration.VerifyResult, error) {
- if err, ok := f.errs[item.SourceUserId]; ok {
- return nil, err
- }
- if result, ok := f.results[item.SourceUserId]; ok {
- return result, nil
- }
- return nil, errors.New("unexpected source user")
- }
-
- func TestCreateUserMigrationBatch_Success(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.POST("/api/user-migrations/batches", CreateUserMigrationBatch)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- body := map[string]any{
- "name": "wave-root-1",
- "source_region": "overseas",
- "target_region": "cn",
- }
- bodyBytes, err := json.Marshal(body)
- require.NoError(t, err)
-
- req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches", bytes.NewBuffer(bodyBytes))
- req.Header.Set("Content-Type", "application/json")
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
-
- require.Equal(t, http.StatusOK, w.Code)
- var resp map[string]any
- require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
- require.True(t, resp["success"].(bool))
- require.NotNil(t, resp["data"])
- }
-
- func TestCreateUserMigrationBatch_ExplicitIDsSuccess(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.POST("/api/user-migrations/batches", CreateUserMigrationBatch)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches", bytes.NewBufferString(`{
- "name":"explicit-batch",
- "source_region":"ov",
- "target_region":"cn",
- "selection_mode":"explicit_ids",
- "source_user_ids":[1001,1002,1002]
- }`))
- req.Header.Set("Content-Type", "application/json")
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
-
- require.Equal(t, http.StatusOK, w.Code)
- var batch model.UserMigrationBatch
- require.NoError(t, db.First(&batch, 1).Error)
- require.Equal(t, "explicit_ids", batch.SelectionMode)
- require.Equal(t, 2, batch.RequestedUserCount)
- }
-
- func TestCreateUserMigrationBatch_ExplicitIDsValidation(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.POST("/api/user-migrations/batches", CreateUserMigrationBatch)
-
- cases := []string{
- `{"name":"b1","source_region":"ov","target_region":"cn","selection_mode":"explicit_ids","source_user_ids":[]}`,
- `{"name":"b2","source_region":"ov","target_region":"cn","selection_mode":"explicit_ids","source_user_ids":[0]}`,
- `{"name":"b3","source_region":"ov","target_region":"cn","selection_mode":"invalid","source_user_ids":[1]}`,
- }
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- for _, body := range cases {
- req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches", bytes.NewBufferString(body))
- req.Header.Set("Content-Type", "application/json")
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
- require.Equal(t, http.StatusBadRequest, w.Code)
- }
- }
-
- func TestCreateUserMigrationBatch_ExplicitIDs_RejectsActiveBatchOverlap(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.POST("/api/user-migrations/batches", CreateUserMigrationBatch)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- existingBatch := &model.UserMigrationBatch{
- Name: "existing-draft-batch",
- SourceRegion: "ov",
- TargetRegion: "cn",
- SelectionMode: "explicit_ids",
- SelectionPayload: `{"source_user_ids":[1002]}`,
- Status: model.UserMigrationBatchStatusDraft,
- OperatorId: 1,
- }
- require.NoError(t, db.Create(existingBatch).Error)
-
- req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches", bytes.NewBufferString(`{
- "name":"explicit-batch",
- "source_region":"ov",
- "target_region":"cn",
- "selection_mode":"explicit_ids",
- "source_user_ids":[1001,1002]
- }`))
- req.Header.Set("Content-Type", "application/json")
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
-
- require.Equal(t, http.StatusConflict, w.Code)
- var resp struct {
- Success bool `json:"success"`
- Error string `json:"error"`
- Conflicts []struct {
- SourceUserID int `json:"source_user_id"`
- BatchID int `json:"batch_id"`
- BatchStatus string `json:"batch_status"`
- } `json:"conflicts"`
- }
- require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
- require.False(t, resp.Success)
- require.Equal(t, "some selected users already exist in active migration batches", resp.Error)
- require.Len(t, resp.Conflicts, 1)
- require.Equal(t, 1002, resp.Conflicts[0].SourceUserID)
- require.Equal(t, existingBatch.Id, resp.Conflicts[0].BatchID)
- require.Equal(t, model.UserMigrationBatchStatusDraft, resp.Conflicts[0].BatchStatus)
- }
-
- func TestListMigrationCandidateUsers_Success(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
-
- origFactory := newMigrationCandidateClient
- defer func() {
- newMigrationCandidateClient = origFactory
- }()
- newMigrationCandidateClient = func() migrationCandidateClient {
- return &fakeMigrationCandidateClient{
- resp: &user_migration.ListRemoteUsersResponse{
- Success: true,
- Data: []user_migration.RemoteUserSnapshot{
- {Id: 10000001, Username: "ov-a", Email: "a@example.com", Quota: 100},
- },
- Total: 1,
- },
- }
- }
-
- router.GET("/api/user-migrations/candidate-users", ListMigrationCandidateUsers)
- req := httptest.NewRequest(http.MethodGet, "/api/user-migrations/candidate-users?page=0&page_size=50", nil)
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
-
- require.Equal(t, http.StatusOK, w.Code)
- var resp map[string]any
- require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
- require.True(t, resp["success"].(bool))
- require.EqualValues(t, 1, resp["total"])
- }
-
- func TestListMigrationCandidateUsers_UpstreamFailure(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
-
- origFactory := newMigrationCandidateClient
- defer func() {
- newMigrationCandidateClient = origFactory
- }()
- newMigrationCandidateClient = func() migrationCandidateClient {
- return &fakeMigrationCandidateClient{err: errors.New("upstream unavailable")}
- }
-
- router.GET("/api/user-migrations/candidate-users", ListMigrationCandidateUsers)
- req := httptest.NewRequest(http.MethodGet, "/api/user-migrations/candidate-users?page=0&page_size=50", nil)
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
-
- require.Equal(t, http.StatusInternalServerError, w.Code)
- }
-
- func TestListMigrationCandidateUsers_ForwardsKeyword(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
-
- client := &fakeMigrationCandidateClient{
- resp: &user_migration.ListRemoteUsersResponse{
- Success: true,
- Data: []user_migration.RemoteUserSnapshot{},
- Total: 0,
- },
- }
-
- origFactory := newMigrationCandidateClient
- defer func() {
- newMigrationCandidateClient = origFactory
- }()
- newMigrationCandidateClient = func() migrationCandidateClient {
- return client
- }
-
- router.GET("/api/user-migrations/candidate-users", ListMigrationCandidateUsers)
- req := httptest.NewRequest(http.MethodGet, "/api/user-migrations/candidate-users?page=2&page_size=30&keyword=alice", nil)
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
-
- require.Equal(t, http.StatusOK, w.Code)
- require.Equal(t, 2, client.gotPage)
- require.Equal(t, 30, client.gotPageSize)
- require.Equal(t, "alice", client.gotKeyword)
- }
-
- func TestListUserMigrationBatches_Success(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.GET("/api/user-migrations/batches", ListUserMigrationBatches)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.UserMigrationBatch{
- Id: 1,
- Name: "batch-1",
- SourceRegion: "ov",
- TargetRegion: "cn",
- OperatorId: 1,
- }).Error)
- require.NoError(t, db.Create(&model.UserMigrationBatch{
- Id: 2,
- Name: "batch-2",
- SourceRegion: "ov",
- TargetRegion: "cn",
- OperatorId: 1,
- }).Error)
-
- req := httptest.NewRequest(http.MethodGet, "/api/user-migrations/batches", nil)
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
-
- require.Equal(t, http.StatusOK, w.Code)
- var resp map[string]any
- require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
- require.True(t, resp["success"].(bool))
- require.Len(t, resp["data"], 2)
- }
-
- func TestGetUserMigrationBatch_Success(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.GET("/api/user-migrations/batches/:id", GetUserMigrationBatch)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.UserMigrationBatch{
- Id: 1,
- Name: "detail-batch",
- SourceRegion: "ov",
- TargetRegion: "cn",
- OperatorId: 1,
- }).Error)
- require.NoError(t, db.Create(&model.UserMigrationItem{
- BatchId: 1,
- SourceUserId: 1001,
- SourceUsername: "ov-u1",
- Status: model.UserMigrationItemStatusReady,
- }).Error)
-
- req := httptest.NewRequest(http.MethodGet, "/api/user-migrations/batches/1?page=0&page_size=50", nil)
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
-
- require.Equal(t, http.StatusOK, w.Code)
- var resp map[string]any
- require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
- require.True(t, resp["success"].(bool))
- require.NotNil(t, resp["data"])
- require.Len(t, resp["items"], 1)
- require.EqualValues(t, 1, resp["total"])
- }
-
- func TestGetUserMigrationBatch_NotFound(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.GET("/api/user-migrations/batches/:id", GetUserMigrationBatch)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- req := httptest.NewRequest(http.MethodGet, "/api/user-migrations/batches/999", nil)
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
-
- require.Equal(t, http.StatusNotFound, w.Code)
- }
-
- func TestExecuteUserMigrationBatch_AlreadyRunning(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.POST("/api/user-migrations/batches/:id/execute", ExecuteUserMigrationBatch)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- batch := &model.UserMigrationBatch{
- Name: "b",
- SourceRegion: "ov",
- TargetRegion: "cn",
- OperatorId: 1,
- Status: model.UserMigrationBatchStatusRunning,
- }
- require.NoError(t, db.Create(batch).Error)
-
- req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches/1/execute", nil)
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
-
- require.Equal(t, http.StatusConflict, w.Code)
- }
-
- func TestExecuteUserMigrationBatch_InvalidStateRejected(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.POST("/api/user-migrations/batches/:id/execute", ExecuteUserMigrationBatch)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- for _, status := range []string{model.UserMigrationBatchStatusDraft, model.UserMigrationBatchStatusScanned} {
- require.NoError(t, db.Exec("DELETE FROM user_migration_batches").Error)
- require.NoError(t, db.Create(&model.UserMigrationBatch{
- Id: 1,
- Name: "invalid-exec",
- SourceRegion: "ov",
- TargetRegion: "cn",
- OperatorId: 1,
- Status: status,
- }).Error)
-
- req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches/1/execute", nil)
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
- require.Equal(t, http.StatusConflict, w.Code)
- }
- }
-
- func TestResolveUserMigrationItem_InvalidStrategy(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.POST("/api/user-migrations/items/:id/resolve", ResolveUserMigrationItem)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.UserMigrationItem{
- BatchId: 1,
- SourceUserId: 10000001,
- SourceUsername: "u",
- Status: model.UserMigrationItemStatusConflict,
- }).Error)
-
- req := httptest.NewRequest(
- http.MethodPost,
- "/api/user-migrations/items/1/resolve",
- bytes.NewBufferString(`{"resolution_strategy":"invalid_strategy"}`),
- )
- req.Header.Set("Content-Type", "application/json")
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
-
- require.Equal(t, http.StatusBadRequest, w.Code)
- }
-
- func TestResolveUserMigrationItem_RefreshesBatchStatusToReady(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.POST("/api/user-migrations/items/:id/resolve", ResolveUserMigrationItem)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- batch := &model.UserMigrationBatch{
- Name: "resolve-ready",
- SourceRegion: "ov",
- TargetRegion: "cn",
- OperatorId: 1,
- Status: model.UserMigrationBatchStatusScanned,
- }
- require.NoError(t, model.CreateUserMigrationBatch(batch))
- require.NoError(t, db.Create(&model.UserMigrationItem{
- BatchId: batch.Id,
- SourceUserId: 1001,
- SourceUsername: "ov-u1",
- Status: model.UserMigrationItemStatusConflict,
- }).Error)
-
- req := httptest.NewRequest(
- http.MethodPost,
- "/api/user-migrations/items/1/resolve",
- bytes.NewBufferString(`{"resolution_strategy":"create_new"}`),
- )
- req.Header.Set("Content-Type", "application/json")
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
-
- require.Equal(t, http.StatusOK, w.Code)
-
- var refreshed model.UserMigrationBatch
- require.NoError(t, db.First(&refreshed, batch.Id).Error)
- require.Equal(t, model.UserMigrationBatchStatusReady, refreshed.Status)
- }
-
- func TestResolveUserMigrationItem_RejectsMergeWhenCNAlreadyHasSyncedCopy(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.POST("/api/user-migrations/items/:id/resolve", ResolveUserMigrationItem)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.UserMigrationItem{
- Id: 1,
- BatchId: 1,
- SourceUserId: 1001,
- SourceUsername: "ov-u1",
- Status: model.UserMigrationItemStatusConflict,
- ConflictFlags: `["email","cn_already_synced_to_ov"]`,
- TargetUserId: 24,
- }).Error)
-
- req := httptest.NewRequest(
- http.MethodPost,
- "/api/user-migrations/items/1/resolve",
- bytes.NewBufferString(`{"resolution_strategy":"merge_into_existing","target_user_id":24}`),
- )
- req.Header.Set("Content-Type", "application/json")
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
-
- require.Equal(t, http.StatusConflict, w.Code)
-
- var item model.UserMigrationItem
- require.NoError(t, db.First(&item, 1).Error)
- require.Equal(t, model.UserMigrationItemStatusConflict, item.Status)
- require.Equal(t, 24, item.TargetUserId)
- }
-
- func TestRetryUserMigrationBatch_OnlyFailedItemsReset(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.POST("/api/user-migrations/batches/:id/retry", RetryUserMigrationBatch)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.UserMigrationBatch{
- Id: 1,
- Name: "retry-batch",
- SourceRegion: "ov",
- TargetRegion: "cn",
- OperatorId: 1,
- Status: model.UserMigrationBatchStatusRunning,
- }).Error)
- require.NoError(t, db.Create(&model.UserMigrationItem{
- Id: 1,
- BatchId: 1,
- SourceUserId: 1001,
- SourceUsername: "failed-user",
- Status: model.UserMigrationItemStatusFailed,
- ErrorMessage: "boom",
- }).Error)
- require.NoError(t, db.Create(&model.UserMigrationItem{
- Id: 2,
- BatchId: 1,
- SourceUserId: 1002,
- SourceUsername: "migrated-user",
- Status: model.UserMigrationItemStatusMigrated,
- }).Error)
- require.NoError(t, db.Create(&model.UserMigrationItem{
- Id: 3,
- BatchId: 1,
- SourceUserId: 1003,
- SourceUsername: "skipped-user",
- Status: model.UserMigrationItemStatusSkipped,
- }).Error)
-
- req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches/1/retry", nil)
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
- require.Equal(t, http.StatusConflict, w.Code)
-
- var failedItem, migratedItem, skippedItem model.UserMigrationItem
- require.NoError(t, db.First(&failedItem, 1).Error)
- require.NoError(t, db.First(&migratedItem, 2).Error)
- require.NoError(t, db.First(&skippedItem, 3).Error)
-
- require.Equal(t, model.UserMigrationItemStatusReady, failedItem.Status)
- require.Empty(t, failedItem.ErrorMessage)
- require.Equal(t, model.UserMigrationItemStatusMigrated, migratedItem.Status)
- require.Equal(t, model.UserMigrationItemStatusSkipped, skippedItem.Status)
- }
-
- func TestCancelUserMigrationBatch_SoftDeleteSuccess(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.POST("/api/user-migrations/batches/:id/cancel", CancelUserMigrationBatch)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.UserMigrationBatch{
- Id: 1,
- Name: "cancel-me",
- SourceRegion: "ov",
- TargetRegion: "cn",
- SelectionMode: "explicit_ids",
- SelectionPayload: `{"source_user_ids":[11,12]}`,
- OperatorId: 1,
- Status: model.UserMigrationBatchStatusScanned,
- }).Error)
-
- req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches/1/cancel", nil)
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
- require.Equal(t, http.StatusOK, w.Code)
-
- var batch model.UserMigrationBatch
- require.NoError(t, db.First(&batch, 1).Error)
- require.Equal(t, model.UserMigrationBatchStatusCancelled, batch.Status)
- }
-
- func TestCancelUserMigrationBatch_InvalidStateRejected(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.POST("/api/user-migrations/batches/:id/cancel", CancelUserMigrationBatch)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- for _, status := range []string{
- model.UserMigrationBatchStatusScanning,
- model.UserMigrationBatchStatusRunning,
- model.UserMigrationBatchStatusCompleted,
- } {
- require.NoError(t, db.Exec("DELETE FROM user_migration_batches").Error)
- require.NoError(t, db.Create(&model.UserMigrationBatch{
- Id: 1,
- Name: "cannot-cancel",
- SourceRegion: "ov",
- TargetRegion: "cn",
- OperatorId: 1,
- Status: status,
- }).Error)
-
- req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches/1/cancel", nil)
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
- require.Equal(t, http.StatusConflict, w.Code)
- }
- }
-
- func TestCreateUserMigrationBatch_ExplicitIDs_AllowsReuseAfterCancel(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.POST("/api/user-migrations/batches", CreateUserMigrationBatch)
- router.POST("/api/user-migrations/batches/:id/cancel", CancelUserMigrationBatch)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- existingBatch := &model.UserMigrationBatch{
- Name: "existing-scanned-batch",
- SourceRegion: "ov",
- TargetRegion: "cn",
- SelectionMode: "explicit_ids",
- SelectionPayload: `{"source_user_ids":[11,12]}`,
- Status: model.UserMigrationBatchStatusScanned,
- OperatorId: 1,
- }
- require.NoError(t, db.Create(existingBatch).Error)
-
- cancelReq := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches/1/cancel", nil)
- cancelW := httptest.NewRecorder()
- router.ServeHTTP(cancelW, cancelReq)
- require.Equal(t, http.StatusOK, cancelW.Code)
-
- createReq := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches", bytes.NewBufferString(`{
- "name":"recreated-batch",
- "source_region":"ov",
- "target_region":"cn",
- "selection_mode":"explicit_ids",
- "source_user_ids":[11,12]
- }`))
- createReq.Header.Set("Content-Type", "application/json")
- createW := httptest.NewRecorder()
- router.ServeHTTP(createW, createReq)
- require.Equal(t, http.StatusOK, createW.Code)
- }
-
- func TestVerifyUserMigrationBatch_AggregatesResult(t *testing.T) {
- gin.SetMode(gin.TestMode)
- router := gin.New()
- router.Use(testRootAuthMiddleware())
- router.POST("/api/user-migrations/batches/:id/verify", VerifyUserMigrationBatch)
-
- db := setupUserMigrationControllerDB(t)
- orig := model.DB
- origFactory := newMigrationVerifier
- model.DB = db
- defer func() {
- model.DB = orig
- newMigrationVerifier = origFactory
- }()
-
- require.NoError(t, db.Create(&model.UserMigrationItem{
- Id: 1,
- BatchId: 1,
- SourceUserId: 1001,
- TargetUserId: 1,
- Status: model.UserMigrationItemStatusMigrated,
- }).Error)
- require.NoError(t, db.Create(&model.UserMigrationItem{
- Id: 2,
- BatchId: 1,
- SourceUserId: 1002,
- TargetUserId: 2,
- Status: model.UserMigrationItemStatusMigrated,
- }).Error)
- require.NoError(t, db.Create(&model.UserMigrationItem{
- Id: 3,
- BatchId: 1,
- SourceUserId: 1003,
- TargetUserId: 3,
- Status: model.UserMigrationItemStatusMigrated,
- }).Error)
-
- newMigrationVerifier = func() migrationVerifier {
- return &fakeMigrationVerifier{
- results: map[int]*user_migration.VerifyResult{
- 1001: {
- TargetUserExists: true,
- RemoteConverted: true,
- QuotaMatched: true,
- },
- 1002: {
- TargetUserExists: false,
- RemoteConverted: true,
- QuotaMatched: false,
- },
- },
- errs: map[int]error{
- 1003: errors.New("remote user not found"),
- },
- }
- }
-
- req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches/1/verify", nil)
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
- require.Equal(t, http.StatusOK, w.Code)
-
- var resp struct {
- Success bool `json:"success"`
- Data []map[string]any `json:"data"`
- }
- require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
- require.True(t, resp.Success)
- require.Len(t, resp.Data, 3)
-
- bySource := make(map[int]map[string]any, len(resp.Data))
- for _, entry := range resp.Data {
- bySource[int(entry["source_user_id"].(float64))] = entry
- }
-
- require.NotNil(t, bySource[1001]["result"])
- require.NotNil(t, bySource[1002]["result"])
- require.Equal(t, "remote user not found", bySource[1003]["error"])
- }
|