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"]) }