From 36b47379f70cc4400f8e8a6b76000f164ea8201b Mon Sep 17 00:00:00 2001 From: fengsilin Date: Wed, 3 Jun 2026 13:04:53 +0800 Subject: [PATCH] test(user-migration): cover rescan and batch status regressions --- controller/user_migration_test.go | 44 +++++++++++++++ model/user_migration_test.go | 31 ++++++++++ service/user_migration/service_test.go | 78 ++++++++++++++++++++++++++ 3 files changed, 153 insertions(+) diff --git a/controller/user_migration_test.go b/controller/user_migration_test.go index 94ea99c..fef6dec 100644 --- a/controller/user_migration_test.go +++ b/controller/user_migration_test.go @@ -135,3 +135,47 @@ func TestResolveUserMigrationItem_InvalidStrategy(t *testing.T) { 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) +} diff --git a/model/user_migration_test.go b/model/user_migration_test.go index 65c3a2d..a1d5aa6 100644 --- a/model/user_migration_test.go +++ b/model/user_migration_test.go @@ -148,3 +148,34 @@ func TestAtomicGrantQuotaIfNotExists_Idempotent(t *testing.T) { require.NoError(t, db.First(&user, 1).Error) require.Equal(t, 700, user.Quota) } + +func TestRefreshBatchStats_ReadyOnlyWhenNoPendingScan(t *testing.T) { + db := setupUserMigrationTestDB(t) + orig := DB + DB = db + defer func() { + DB = orig + }() + + batch := &UserMigrationBatch{Name: "stats-test", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1} + require.NoError(t, CreateUserMigrationBatch(batch)) + + require.NoError(t, DB.Create(&UserMigrationItem{ + BatchId: batch.Id, + SourceUserId: 1, + SourceUsername: "u1", + Status: UserMigrationItemStatusReady, + }).Error) + require.NoError(t, DB.Create(&UserMigrationItem{ + BatchId: batch.Id, + SourceUserId: 2, + SourceUsername: "u2", + Status: UserMigrationItemStatusPendingScan, + }).Error) + + require.NoError(t, RefreshUserMigrationBatchStats(batch.Id)) + + var refreshed UserMigrationBatch + require.NoError(t, DB.First(&refreshed, batch.Id).Error) + require.Equal(t, UserMigrationBatchStatusScanned, refreshed.Status) +} diff --git a/service/user_migration/service_test.go b/service/user_migration/service_test.go index 8f6146e..fffe51c 100644 --- a/service/user_migration/service_test.go +++ b/service/user_migration/service_test.go @@ -247,6 +247,43 @@ func TestScanBatch_Paginated(t *testing.T) { require.Len(t, items, scanPageSize+1) } +func TestScanBatch_Rescan_PreservesHumanStrategy(t *testing.T) { + db := setupMigrationServiceDB(t) + orig := model.DB + model.DB = db + defer func() { + model.DB = orig + }() + + batch := &model.UserMigrationBatch{Name: "wave-rescan", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1} + require.NoError(t, model.CreateUserMigrationBatch(batch)) + + client := &fakeClient{ + listPages: map[int][]RemoteUserSnapshot{ + 0: {{ + Id: 10000001, + Username: "ov-dave", + Email: "dave@example.com", + Quota: 100, + AffCode: "OVD1", + }}, + }, + hasCopyResult: false, + } + svc := &Service{client: client} + require.NoError(t, svc.ScanBatch(batch.Id)) + + require.NoError(t, db.Model(&model.UserMigrationItem{}).Where("batch_id = ?", batch.Id). + Update("resolution_strategy", model.UserMigrationStrategySkip).Error) + + require.NoError(t, svc.ScanBatch(batch.Id)) + + items, err := model.ListUserMigrationItemsByBatch(batch.Id) + require.NoError(t, err) + require.Len(t, items, 1) + require.Equal(t, model.UserMigrationStrategySkip, items[0].ResolutionStrategy) +} + func TestExecuteBatch_CreateNew_CreatesUserAndConvertsRemote(t *testing.T) { db := setupMigrationServiceDB(t) orig := model.DB @@ -435,6 +472,47 @@ func TestExecuteBatch_MergeExisting_IdempotentQuota(t *testing.T) { require.Len(t, client.convertCalls, 1) } +func TestExecuteBatch_SkipItem_DoesNotConvert(t *testing.T) { + db := setupMigrationServiceDB(t) + orig := model.DB + model.DB = db + defer func() { + model.DB = orig + }() + + batch := &model.UserMigrationBatch{ + Name: "skip-test", + SourceRegion: "ov", + TargetRegion: "cn", + OperatorId: 1, + Status: model.UserMigrationBatchStatusReady, + } + require.NoError(t, model.CreateUserMigrationBatch(batch)) + require.NoError(t, model.UpsertUserMigrationItem(&model.UserMigrationItem{ + BatchId: batch.Id, + SourceUserId: 10000099, + SourceUsername: "skip-user", + SourceQuota: 50, + ResolutionStrategy: model.UserMigrationStrategySkip, + Status: model.UserMigrationItemStatusReady, + MigrationToken: "batch-1-user-10000099", + })) + + client := &fakeClient{} + svc := &Service{client: client} + + locked, err := model.TryLockBatchForExecution(batch.Id) + require.NoError(t, err) + require.True(t, locked) + require.NoError(t, svc.ExecuteBatch(batch.Id)) + + require.Len(t, client.convertCalls, 0) + items, err := model.ListUserMigrationItemsByBatch(batch.Id) + require.NoError(t, err) + require.Len(t, items, 1) + require.Equal(t, model.UserMigrationItemStatusSkipped, items[0].Status) +} + func TestVerifyItem_CorrectlyChecksRemoteConverted(t *testing.T) { db := setupMigrationServiceDB(t) orig := model.DB