|
- package user_migration
-
- import (
- "fmt"
- "testing"
-
- "github.com/QuantumNous/new-api/common"
- "github.com/QuantumNous/new-api/model"
- "github.com/glebarez/sqlite"
- "github.com/stretchr/testify/require"
- "gorm.io/gorm"
- )
-
- type fakeClient struct {
- listPages map[int][]RemoteUserSnapshot
- getUserResp *RemoteUserSnapshot
- getUserByID map[int]*RemoteUserSnapshot
- hasCopyResult bool
- hasCopyByID map[int]bool
- convertCalls []int
- convertReqs map[int]*ConvertRemoteUserRequest
- }
-
- func (f *fakeClient) ListUsers(page, pageSize int) (*ListRemoteUsersResponse, error) {
- users := f.listPages[page]
- return &ListRemoteUsersResponse{
- Success: true,
- Data: users,
- Total: len(users),
- }, nil
- }
-
- func (f *fakeClient) GetUser(userId int) (*RemoteUserSnapshot, error) {
- if snapshot, ok := f.getUserByID[userId]; ok {
- return snapshot, nil
- }
- if f.getUserResp != nil {
- return f.getUserResp, nil
- }
- return nil, fmt.Errorf("user %d not found", userId)
- }
-
- func (f *fakeClient) ConvertToSynced(userId int, req *ConvertRemoteUserRequest) error {
- f.convertCalls = append(f.convertCalls, userId)
- if f.convertReqs == nil {
- f.convertReqs = make(map[int]*ConvertRemoteUserRequest)
- }
- if req != nil {
- copyReq := *req
- f.convertReqs[userId] = ©Req
- if snapshot, ok := f.getUserByID[userId]; ok && snapshot != nil {
- snapshot.Source = common.UserSourceSynced
- snapshot.RemoteUserId = req.RemoteUserId
- snapshot.SyncedQuota = req.SyncedQuota
- }
- if f.getUserResp != nil && f.getUserResp.Id == userId {
- f.getUserResp.Source = common.UserSourceSynced
- f.getUserResp.RemoteUserId = req.RemoteUserId
- f.getUserResp.SyncedQuota = req.SyncedQuota
- }
- }
- return nil
- }
-
- func (f *fakeClient) HasSyncedCopy(cnUserId int) (bool, error) {
- if value, ok := f.hasCopyByID[cnUserId]; ok {
- return value, nil
- }
- return f.hasCopyResult, nil
- }
-
- func setupMigrationServiceDB(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 TestScanBatch_NoConflict_MarksReady(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- batch := &model.UserMigrationBatch{Name: "wave-1", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- client := &fakeClient{
- listPages: map[int][]RemoteUserSnapshot{
- 0: {{
- Id: 10000001,
- Username: "ov-bob",
- Email: "bob@example.com",
- Quota: 456,
- AffCode: "A002",
- }},
- },
- }
- svc := &Service{client: client}
- 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.UserMigrationStrategyCreateNew, items[0].ResolutionStrategy)
- require.Equal(t, model.UserMigrationItemStatusReady, items[0].Status)
-
- var savedBatch model.UserMigrationBatch
- require.NoError(t, db.First(&savedBatch, batch.Id).Error)
- require.Equal(t, model.UserMigrationBatchStatusReady, savedBatch.Status)
- }
-
- func TestScanBatch_EmailConflict_NoCNSyncedCopy_RecommendsMerge(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 1,
- Username: "cn-alice",
- Password: "hash",
- Email: "alice@example.com",
- AffCode: "C001",
- }).Error)
-
- batch := &model.UserMigrationBatch{Name: "wave-2", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- client := &fakeClient{
- listPages: map[int][]RemoteUserSnapshot{
- 0: {{
- Id: 10000001,
- Username: "ov-alice",
- Email: "alice@example.com",
- Quota: 123,
- AffCode: "A001",
- }},
- },
- hasCopyResult: false,
- }
- svc := &Service{client: client}
- 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.UserMigrationStrategyMergeExisting, items[0].ResolutionStrategy)
- require.Equal(t, model.UserMigrationItemStatusReady, items[0].Status)
- require.Equal(t, 1, items[0].TargetUserId)
- }
-
- func TestScanBatch_EmailConflict_CNHasSyncedCopy_MarksHardConflict(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 456,
- Username: "cn-user456",
- Password: "hash",
- Email: "shared@example.com",
- AffCode: "C456",
- }).Error)
-
- batch := &model.UserMigrationBatch{Name: "wave-3", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- client := &fakeClient{
- listPages: map[int][]RemoteUserSnapshot{
- 0: {{
- Id: 10000003,
- Username: "ov-x",
- Email: "shared@example.com",
- Quota: 50,
- AffCode: "OVX",
- }},
- },
- hasCopyResult: true,
- }
- svc := &Service{client: client}
- 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.UserMigrationItemStatusConflict, items[0].Status)
- require.Contains(t, items[0].ConflictFlags, "cn_already_synced_to_ov")
- }
-
- func TestScanBatch_GitHubId_SameUser_RecommendsMerge(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 11,
- Username: "cn-github",
- Password: "hash",
- AffCode: "CGH1",
- GitHubId: "gh-123",
- }).Error)
-
- batch := &model.UserMigrationBatch{Name: "wave-github-merge", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- client := &fakeClient{
- listPages: map[int][]RemoteUserSnapshot{
- 0: {{
- Id: 10000011,
- Username: "ov-github",
- GitHubId: "gh-123",
- Quota: 88,
- AffCode: "OVGH1",
- }},
- },
- hasCopyByID: map[int]bool{11: false},
- }
- svc := &Service{client: client}
- 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.UserMigrationStrategyMergeExisting, items[0].ResolutionStrategy)
- require.Equal(t, model.UserMigrationItemStatusReady, items[0].Status)
- require.Equal(t, 11, items[0].TargetUserId)
- require.Contains(t, items[0].ConflictFlags, "github_id")
- }
-
- func TestScanBatch_OAuthId_DifferentUsers_MultipleMergeTargets(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 21,
- Username: "cn-github-owner",
- Password: "hash",
- AffCode: "COA1",
- GitHubId: "gh-shared",
- }).Error)
- require.NoError(t, db.Create(&model.User{
- Id: 22,
- Username: "cn-discord-owner",
- Password: "hash",
- AffCode: "COA2",
- DiscordId: "dc-shared",
- }).Error)
-
- batch := &model.UserMigrationBatch{Name: "wave-oauth-conflict", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- client := &fakeClient{
- listPages: map[int][]RemoteUserSnapshot{
- 0: {{
- Id: 10000012,
- Username: "ov-oauth-conflict",
- GitHubId: "gh-shared",
- DiscordId: "dc-shared",
- Quota: 66,
- AffCode: "OVOA1",
- }},
- },
- }
- svc := &Service{client: client}
- 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.UserMigrationItemStatusConflict, items[0].Status)
- require.Contains(t, items[0].ConflictFlags, "github_id")
- require.Contains(t, items[0].ConflictFlags, "discord_id")
- require.Contains(t, items[0].ConflictFlags, "multiple_merge_targets")
- }
-
- func TestScanBatch_CustomOAuthBinding_MatchesExistingUser_RecommendsMerge(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.CustomOAuthProvider{
- Id: 31,
- Name: "GitHub Enterprise",
- Slug: "gh-enterprise",
- Enabled: true,
- ClientId: "cid",
- AuthorizationEndpoint: "https://auth.example.com",
- TokenEndpoint: "https://token.example.com",
- UserInfoEndpoint: "https://userinfo.example.com",
- }).Error)
- require.NoError(t, db.Create(&model.User{
- Id: 32,
- Username: "cn-custom-oauth",
- Password: "hash",
- AffCode: "CCO1",
- }).Error)
- require.NoError(t, db.Create(&model.UserOAuthBinding{
- UserId: 32,
- ProviderId: 31,
- ProviderUserId: "provider-user-1",
- }).Error)
-
- batch := &model.UserMigrationBatch{Name: "wave-custom-oauth-merge", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- client := &fakeClient{
- listPages: map[int][]RemoteUserSnapshot{
- 0: {{
- Id: 10000013,
- Username: "ov-custom-oauth",
- Quota: 55,
- AffCode: "OVCO1",
- OAuthBindings: []RemoteOAuthBinding{{
- ProviderSlug: "gh-enterprise",
- ProviderUserId: "provider-user-1",
- }},
- }},
- },
- hasCopyByID: map[int]bool{32: false},
- }
- svc := &Service{client: client}
- 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.UserMigrationStrategyMergeExisting, items[0].ResolutionStrategy)
- require.Equal(t, model.UserMigrationItemStatusReady, items[0].Status)
- require.Equal(t, 32, items[0].TargetUserId)
- require.Contains(t, items[0].ConflictFlags, "oauth:gh-enterprise")
- }
-
- func TestScanBatch_CustomOAuthBinding_UnknownSlug_Ignored(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- batch := &model.UserMigrationBatch{Name: "wave-custom-oauth-unknown", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- client := &fakeClient{
- listPages: map[int][]RemoteUserSnapshot{
- 0: {{
- Id: 10000014,
- Username: "ov-unknown-oauth",
- Quota: 77,
- AffCode: "OVUK1",
- OAuthBindings: []RemoteOAuthBinding{{
- ProviderSlug: "unknown-provider",
- ProviderUserId: "u-1",
- }},
- }},
- },
- }
- svc := &Service{client: client}
- 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.UserMigrationStrategyCreateNew, items[0].ResolutionStrategy)
- require.Equal(t, model.UserMigrationItemStatusReady, items[0].Status)
- require.NotContains(t, items[0].ConflictFlags, "oauth:")
- }
-
- func TestScanBatch_EmailAndOAuth_SameUser_RecommendsMerge(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 41,
- Username: "cn-same-user",
- Password: "hash",
- Email: "same@example.com",
- AffCode: "CSU1",
- GitHubId: "gh-same",
- }).Error)
-
- batch := &model.UserMigrationBatch{Name: "wave-email-oauth-same", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- client := &fakeClient{
- listPages: map[int][]RemoteUserSnapshot{
- 0: {{
- Id: 10000015,
- Username: "ov-same-user",
- Email: "same@example.com",
- GitHubId: "gh-same",
- Quota: 91,
- AffCode: "OVSU1",
- }},
- },
- hasCopyByID: map[int]bool{41: false},
- }
- svc := &Service{client: client}
- 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.UserMigrationStrategyMergeExisting, items[0].ResolutionStrategy)
- require.Equal(t, model.UserMigrationItemStatusReady, items[0].Status)
- require.Equal(t, 41, items[0].TargetUserId)
- require.Contains(t, items[0].ConflictFlags, "email")
- require.Contains(t, items[0].ConflictFlags, "github_id")
- require.NotContains(t, items[0].ConflictFlags, "multiple_merge_targets")
- }
-
- func TestScanBatch_EmailAndOAuth_DifferentUsers_Conflicts(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 51,
- Username: "cn-email-owner",
- Password: "hash",
- Email: "combo@example.com",
- AffCode: "CEO1",
- }).Error)
- require.NoError(t, db.Create(&model.User{
- Id: 52,
- Username: "cn-oauth-owner",
- Password: "hash",
- AffCode: "COO1",
- GitHubId: "gh-combo",
- }).Error)
-
- batch := &model.UserMigrationBatch{Name: "wave-email-oauth-diff", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- client := &fakeClient{
- listPages: map[int][]RemoteUserSnapshot{
- 0: {{
- Id: 10000016,
- Username: "ov-combo-user",
- Email: "combo@example.com",
- GitHubId: "gh-combo",
- Quota: 92,
- AffCode: "OVCO2",
- }},
- },
- }
- svc := &Service{client: client}
- 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.UserMigrationItemStatusConflict, items[0].Status)
- require.Contains(t, items[0].ConflictFlags, "email")
- require.Contains(t, items[0].ConflictFlags, "github_id")
- require.Contains(t, items[0].ConflictFlags, "multiple_merge_targets")
- }
-
- func TestScanBatch_UsernameConflict_OverridesMergeCandidate(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 61,
- Username: "same-name",
- Password: "hash",
- AffCode: "CSN1",
- }).Error)
- require.NoError(t, db.Create(&model.User{
- Id: 62,
- Username: "other-user",
- Password: "hash",
- Email: "mergeable@example.com",
- AffCode: "CME1",
- }).Error)
-
- batch := &model.UserMigrationBatch{Name: "wave-username-overrides", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- client := &fakeClient{
- listPages: map[int][]RemoteUserSnapshot{
- 0: {{
- Id: 10000017,
- Username: "same-name",
- Email: "mergeable@example.com",
- Quota: 93,
- AffCode: "OVUN1",
- }},
- },
- hasCopyByID: map[int]bool{62: false},
- }
- svc := &Service{client: client}
- 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.UserMigrationItemStatusConflict, items[0].Status)
- require.Contains(t, items[0].ConflictFlags, "username")
- }
-
- func TestScanBatch_Paginated(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- batch := &model.UserMigrationBatch{Name: "wave-big", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- page0 := make([]RemoteUserSnapshot, scanPageSize)
- for i := range page0 {
- page0[i] = RemoteUserSnapshot{
- Id: 10000001 + i,
- Username: fmt.Sprintf("u%d", i),
- AffCode: fmt.Sprintf("A%d", i),
- }
- }
- page1 := []RemoteUserSnapshot{{
- Id: 10000001 + scanPageSize,
- Username: "last",
- AffCode: "LAST",
- }}
-
- client := &fakeClient{
- listPages: map[int][]RemoteUserSnapshot{
- 0: page0,
- 1: page1,
- },
- }
- svc := &Service{client: client}
- require.NoError(t, svc.ScanBatch(batch.Id))
-
- items, err := model.ListUserMigrationItemsByBatch(batch.Id)
- require.NoError(t, err)
- 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 TestMigrationFlow_GrayScenarios_EndToEnd(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 1,
- Username: "cn-merge",
- Password: "hash",
- Email: "merge@example.com",
- AffCode: "CNM1",
- Quota: 500,
- }).Error)
- require.NoError(t, db.Create(&model.User{
- Id: 2,
- Username: "taken-name",
- Password: "hash",
- Email: "taken@example.com",
- AffCode: "CNT2",
- Quota: 50,
- }).Error)
- require.NoError(t, db.Create(&model.User{
- Id: 3,
- Username: "cn-hard",
- Password: "hash",
- Email: "synced@example.com",
- AffCode: "CNH3",
- Quota: 300,
- }).Error)
- require.NoError(t, db.Create(&model.User{
- Id: 4,
- Username: "cn-soft",
- Password: "hash",
- Email: "soft-owner@example.com",
- AffCode: "SOFT1",
- Quota: 80,
- }).Error)
-
- batch := &model.UserMigrationBatch{
- Name: "gray-scenarios",
- SourceRegion: "ov",
- TargetRegion: "cn",
- OperatorId: 1,
- }
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- remoteUsers := []RemoteUserSnapshot{
- {
- Id: 10000001,
- Username: "ov-create",
- Email: "create@example.com",
- Quota: 100,
- AffCode: "OVC1",
- },
- {
- Id: 10000002,
- Username: "ov-merge",
- Email: "merge@example.com",
- Quota: 200,
- AffCode: "OVM2",
- },
- {
- Id: 10000003,
- Username: "ov-hard",
- Email: "synced@example.com",
- Quota: 300,
- AffCode: "OVH3",
- },
- {
- Id: 10000004,
- Username: "taken-name",
- Email: "username-conflict@example.com",
- Quota: 120,
- AffCode: "OVU4",
- },
- {
- Id: 10000005,
- Username: "ov-soft",
- Email: "soft@example.com",
- Quota: 150,
- AffCode: "SOFT1",
- },
- }
-
- getUserByID := make(map[int]*RemoteUserSnapshot, len(remoteUsers))
- for i := range remoteUsers {
- snapshot := remoteUsers[i]
- getUserByID[snapshot.Id] = &snapshot
- }
-
- client := &fakeClient{
- listPages: map[int][]RemoteUserSnapshot{
- 0: remoteUsers,
- },
- getUserByID: getUserByID,
- hasCopyByID: map[int]bool{
- 1: false,
- 3: true,
- },
- }
- svc := &Service{client: client}
-
- require.NoError(t, svc.ScanBatch(batch.Id))
-
- items, err := model.ListUserMigrationItemsByBatch(batch.Id)
- require.NoError(t, err)
- require.Len(t, items, 5)
-
- bySourceUserID := make(map[int]*model.UserMigrationItem, len(items))
- for _, item := range items {
- bySourceUserID[item.SourceUserId] = item
- }
-
- require.Equal(t, model.UserMigrationStrategyCreateNew, bySourceUserID[10000001].ResolutionStrategy)
- require.Equal(t, model.UserMigrationItemStatusReady, bySourceUserID[10000001].Status)
-
- require.Equal(t, model.UserMigrationStrategyMergeExisting, bySourceUserID[10000002].ResolutionStrategy)
- require.Equal(t, 1, bySourceUserID[10000002].TargetUserId)
- require.Equal(t, model.UserMigrationItemStatusReady, bySourceUserID[10000002].Status)
-
- require.Equal(t, model.UserMigrationItemStatusConflict, bySourceUserID[10000003].Status)
- require.Contains(t, bySourceUserID[10000003].ConflictFlags, "cn_already_synced_to_ov")
-
- require.Equal(t, model.UserMigrationItemStatusConflict, bySourceUserID[10000004].Status)
- require.Contains(t, bySourceUserID[10000004].ConflictFlags, "username")
-
- require.Equal(t, model.UserMigrationStrategyCreateNew, bySourceUserID[10000005].ResolutionStrategy)
- require.Equal(t, model.UserMigrationItemStatusReady, bySourceUserID[10000005].Status)
- require.Contains(t, bySourceUserID[10000005].ConflictFlags, "aff_code")
-
- require.NoError(t, db.Model(&model.UserMigrationItem{}).
- Where("id IN ?", []int{bySourceUserID[10000003].Id, bySourceUserID[10000004].Id}).
- Updates(map[string]any{
- "resolution_strategy": model.UserMigrationStrategySkip,
- "status": model.UserMigrationItemStatusReady,
- "error_message": "",
- }).Error)
- require.NoError(t, model.RefreshUserMigrationBatchStats(batch.Id))
-
- var readyBatch model.UserMigrationBatch
- require.NoError(t, db.First(&readyBatch, batch.Id).Error)
- require.Equal(t, model.UserMigrationBatchStatusReady, readyBatch.Status)
-
- locked, err := model.TryLockBatchForExecution(batch.Id)
- require.NoError(t, err)
- require.True(t, locked)
- require.NoError(t, svc.ExecuteBatch(batch.Id))
-
- items, err = model.ListUserMigrationItemsByBatch(batch.Id)
- require.NoError(t, err)
- bySourceUserID = make(map[int]*model.UserMigrationItem, len(items))
- for _, item := range items {
- bySourceUserID[item.SourceUserId] = item
- }
-
- require.Equal(t, model.UserMigrationItemStatusMigrated, bySourceUserID[10000001].Status)
- require.Equal(t, model.UserMigrationItemStatusMigrated, bySourceUserID[10000002].Status)
- require.Equal(t, model.UserMigrationItemStatusSkipped, bySourceUserID[10000003].Status)
- require.Equal(t, model.UserMigrationItemStatusSkipped, bySourceUserID[10000004].Status)
- require.Equal(t, model.UserMigrationItemStatusMigrated, bySourceUserID[10000005].Status)
-
- var createdUser model.User
- require.NoError(t, db.Where("username = ?", "ov-create").First(&createdUser).Error)
- require.Equal(t, 100, createdUser.Quota)
-
- var mergedUser model.User
- require.NoError(t, db.First(&mergedUser, 1).Error)
- require.Equal(t, 700, mergedUser.Quota)
-
- var softUser model.User
- require.NoError(t, db.Where("username = ?", "ov-soft").First(&softUser).Error)
- require.Equal(t, 150, softUser.Quota)
- require.NotEqual(t, "SOFT1", softUser.AffCode)
-
- require.Equal(t, common.UserSourceSynced, client.getUserByID[10000001].Source)
- require.Equal(t, createdUser.Id, client.getUserByID[10000001].RemoteUserId)
- require.Equal(t, 100, client.getUserByID[10000001].SyncedQuota)
-
- require.Equal(t, common.UserSourceSynced, client.getUserByID[10000002].Source)
- require.Equal(t, 1, client.getUserByID[10000002].RemoteUserId)
- require.Equal(t, 700, client.getUserByID[10000002].SyncedQuota)
-
- require.NotEqual(t, common.UserSourceSynced, client.getUserByID[10000003].Source)
- require.NotEqual(t, common.UserSourceSynced, client.getUserByID[10000004].Source)
-
- require.Equal(t, common.UserSourceSynced, client.getUserByID[10000005].Source)
- require.Equal(t, softUser.Id, client.getUserByID[10000005].RemoteUserId)
- require.Equal(t, 150, client.getUserByID[10000005].SyncedQuota)
-
- for _, sourceUserID := range []int{10000001, 10000002, 10000005} {
- result, verifyErr := svc.VerifyItem(bySourceUserID[sourceUserID])
- require.NoError(t, verifyErr)
- require.True(t, result.TargetUserExists)
- require.True(t, result.RemoteConverted)
- require.True(t, result.QuotaMatched)
- }
-
- require.NoError(t, db.Model(&model.UserMigrationBatch{}).Where("id = ?", batch.Id).
- Update("status", model.UserMigrationBatchStatusReady).Error)
- require.NoError(t, db.Model(&model.UserMigrationItem{}).
- Where("id IN ?", []int{
- bySourceUserID[10000001].Id,
- bySourceUserID[10000002].Id,
- bySourceUserID[10000005].Id,
- }).
- Update("status", model.UserMigrationItemStatusReady).Error)
-
- 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, 3)
-
- require.NoError(t, db.First(&mergedUser, 1).Error)
- require.Equal(t, 700, mergedUser.Quota)
- }
-
- func TestExecuteBatch_CreateNew_CreatesUserAndConvertsRemote(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- batch := &model.UserMigrationBatch{
- Name: "wave-exec",
- Status: model.UserMigrationBatchStatusReady,
- }
- batch.SourceRegion = "ov"
- batch.TargetRegion = "cn"
- batch.OperatorId = 1
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- item := &model.UserMigrationItem{
- BatchId: batch.Id,
- SourceUserId: 10000001,
- SourceUsername: "ov-alice",
- SourceEmail: "alice@example.com",
- SourceQuota: 300,
- ResolutionStrategy: model.UserMigrationStrategyCreateNew,
- Status: model.UserMigrationItemStatusReady,
- MigrationToken: "batch-1-user-10000001",
- }
- require.NoError(t, model.UpsertUserMigrationItem(item))
-
- client := &fakeClient{
- getUserResp: &RemoteUserSnapshot{
- Id: 10000001,
- Username: "ov-alice",
- Password: "hashed-password",
- Email: "alice@example.com",
- DisplayName: "Alice",
- Status: 1,
- Role: 1,
- Group: "default",
- Quota: 300,
- AffCode: "OVA1",
- },
- }
- 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))
-
- var cnUser model.User
- require.NoError(t, db.Where("username = ?", "ov-alice").First(&cnUser).Error)
- require.Equal(t, 300, cnUser.Quota)
- require.Less(t, cnUser.Id, 10000000)
-
- require.Len(t, client.convertCalls, 1)
- require.Equal(t, 10000001, client.convertCalls[0])
- }
-
- func TestExecuteBatch_CreateNew_IdempotentOnRetry(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- batch := &model.UserMigrationBatch{
- Name: "wave-retry",
- Status: model.UserMigrationBatchStatusReady,
- }
- batch.SourceRegion = "ov"
- batch.TargetRegion = "cn"
- batch.OperatorId = 1
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- item := &model.UserMigrationItem{
- BatchId: batch.Id,
- SourceUserId: 10000002,
- SourceUsername: "ov-carol",
- SourceEmail: "carol@example.com",
- SourceQuota: 150,
- ResolutionStrategy: model.UserMigrationStrategyCreateNew,
- Status: model.UserMigrationItemStatusReady,
- MigrationToken: "batch-1-user-10000002",
- }
- require.NoError(t, model.UpsertUserMigrationItem(item))
-
- snapshot := &RemoteUserSnapshot{
- Id: 10000002,
- Username: "ov-carol",
- Password: "hash",
- Email: "carol@example.com",
- Quota: 150,
- AffCode: "OVC1",
- }
- client := &fakeClient{getUserResp: snapshot}
- 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.NoError(t, db.Model(&model.UserMigrationBatch{}).Where("id = ?", batch.Id).
- Update("status", model.UserMigrationBatchStatusFailed).Error)
- require.NoError(t, db.Model(&model.UserMigrationItem{}).Where("batch_id = ?", batch.Id).
- Update("status", model.UserMigrationItemStatusReady).Error)
-
- locked, err = model.TryLockBatchForExecution(batch.Id)
- require.NoError(t, err)
- require.True(t, locked)
- require.NoError(t, svc.ExecuteBatch(batch.Id))
-
- var count int64
- require.NoError(t, db.Model(&model.User{}).Where("username = ?", "ov-carol").Count(&count).Error)
- require.EqualValues(t, 1, count)
- require.Len(t, client.convertCalls, 1)
- }
-
- func TestExecuteBatch_CreateNew_ExecutionTimeDriftFails(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 7,
- Username: "cn-alice",
- Password: "hash",
- Email: "alice@example.com",
- AffCode: "C007",
- }).Error)
-
- batch := &model.UserMigrationBatch{
- Name: "wave-create-drift",
- SourceRegion: "ov",
- TargetRegion: "cn",
- OperatorId: 1,
- Status: model.UserMigrationBatchStatusReady,
- }
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- item := &model.UserMigrationItem{
- BatchId: batch.Id,
- SourceUserId: 10000020,
- SourceUsername: "ov-drift",
- SourceEmail: "old@example.com",
- SourceQuota: 120,
- ResolutionStrategy: model.UserMigrationStrategyCreateNew,
- Status: model.UserMigrationItemStatusReady,
- MigrationToken: "batch-1-user-10000020",
- }
- require.NoError(t, model.UpsertUserMigrationItem(item))
-
- client := &fakeClient{
- getUserResp: &RemoteUserSnapshot{
- Id: 10000020,
- Username: "ov-drift",
- Password: "hash",
- Email: "alice@example.com",
- Quota: 120,
- AffCode: "OV20",
- },
- hasCopyByID: map[int]bool{7: false},
- }
- 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)
-
- var count int64
- require.NoError(t, db.Model(&model.User{}).Where("username = ?", "ov-drift").Count(&count).Error)
- require.EqualValues(t, 0, count)
-
- items, err := model.ListUserMigrationItemsByBatch(batch.Id)
- require.NoError(t, err)
- require.Len(t, items, 1)
- require.Equal(t, model.UserMigrationItemStatusFailed, items[0].Status)
- require.Contains(t, items[0].ErrorMessage, "identity drift")
- }
-
- func TestExecuteBatch_MergeExisting_IdempotentQuota(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 1,
- Username: "cn-bob",
- Password: "hash",
- Email: "bob@example.com",
- AffCode: "CNB1",
- Quota: 500,
- }).Error)
-
- batch := &model.UserMigrationBatch{
- Name: "wave-merge",
- Status: model.UserMigrationBatchStatusReady,
- }
- batch.SourceRegion = "ov"
- batch.TargetRegion = "cn"
- batch.OperatorId = 1
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- item := &model.UserMigrationItem{
- BatchId: batch.Id,
- SourceUserId: 10000002,
- TargetUserId: 1,
- SourceUsername: "ov-bob",
- SourceEmail: "bob@example.com",
- SourceQuota: 200,
- ResolutionStrategy: model.UserMigrationStrategyMergeExisting,
- Status: model.UserMigrationItemStatusReady,
- MigrationToken: "batch-1-user-10000002",
- }
- require.NoError(t, model.UpsertUserMigrationItem(item))
-
- client := &fakeClient{
- getUserResp: &RemoteUserSnapshot{
- Id: 10000002,
- Username: "ov-bob",
- Password: "hashed",
- Email: "bob@example.com",
- Quota: 200,
- },
- }
- svc := &Service{client: client}
-
- for i := 0; i < 2; i++ {
- require.NoError(t, db.Model(&model.UserMigrationBatch{}).Where("id = ?", batch.Id).
- Update("status", model.UserMigrationBatchStatusReady).Error)
- require.NoError(t, db.Model(&model.UserMigrationItem{}).Where("batch_id = ?", batch.Id).
- Update("status", model.UserMigrationItemStatusReady).Error)
-
- locked, err := model.TryLockBatchForExecution(batch.Id)
- require.NoError(t, err)
- require.True(t, locked)
- require.NoError(t, svc.ExecuteBatch(batch.Id))
- }
-
- var saved model.User
- require.NoError(t, db.First(&saved, 1).Error)
- require.Equal(t, 700, saved.Quota)
- require.Len(t, client.convertCalls, 1)
- }
-
- func TestExecuteBatch_MergeExisting_ExecutionTimeDriftFails(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 1,
- Username: "cn-bob",
- Password: "hash",
- Email: "bob@example.com",
- AffCode: "CNB1",
- Quota: 500,
- }).Error)
-
- batch := &model.UserMigrationBatch{
- Name: "wave-merge-drift",
- SourceRegion: "ov",
- TargetRegion: "cn",
- OperatorId: 1,
- Status: model.UserMigrationBatchStatusReady,
- }
- require.NoError(t, model.CreateUserMigrationBatch(batch))
-
- item := &model.UserMigrationItem{
- BatchId: batch.Id,
- SourceUserId: 10000021,
- TargetUserId: 1,
- SourceUsername: "ov-bob",
- SourceEmail: "bob@example.com",
- SourceQuota: 200,
- ResolutionStrategy: model.UserMigrationStrategyMergeExisting,
- Status: model.UserMigrationItemStatusReady,
- MigrationToken: "batch-1-user-10000021",
- }
- require.NoError(t, model.UpsertUserMigrationItem(item))
-
- client := &fakeClient{
- getUserResp: &RemoteUserSnapshot{
- Id: 10000021,
- Username: "ov-bob",
- Password: "hash",
- Email: "changed@example.com",
- Quota: 200,
- AffCode: "OV21",
- },
- }
- 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)
-
- var cnUser model.User
- require.NoError(t, db.First(&cnUser, 1).Error)
- require.Equal(t, 500, cnUser.Quota)
-
- items, err := model.ListUserMigrationItemsByBatch(batch.Id)
- require.NoError(t, err)
- require.Len(t, items, 1)
- require.Equal(t, model.UserMigrationItemStatusFailed, items[0].Status)
- require.Contains(t, items[0].ErrorMessage, "merge target drift")
- }
-
- 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
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 789,
- Username: "cn-migrated",
- Password: "hash",
- AffCode: "CM1",
- Quota: 300,
- }).Error)
-
- item := &model.UserMigrationItem{
- BatchId: 1,
- SourceUserId: 10000001,
- TargetUserId: 789,
- SourceQuota: 300,
- Status: model.UserMigrationItemStatusMigrated,
- }
-
- client := &fakeClient{
- getUserResp: &RemoteUserSnapshot{
- Id: 10000001,
- Source: common.UserSourceSynced,
- RemoteUserId: 789,
- SyncedQuota: 300,
- },
- }
- svc := &Service{client: client}
- result, err := svc.VerifyItem(item)
- require.NoError(t, err)
- require.True(t, result.TargetUserExists)
- require.True(t, result.RemoteConverted)
- require.True(t, result.QuotaMatched)
- }
-
- func TestVerifyItem_QuotaMismatchDetected(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 790,
- Username: "cn-mismatch",
- Password: "hash",
- AffCode: "CM2",
- Quota: 300,
- }).Error)
-
- item := &model.UserMigrationItem{
- BatchId: 1,
- SourceUserId: 10000002,
- TargetUserId: 790,
- SourceQuota: 300,
- Status: model.UserMigrationItemStatusMigrated,
- }
-
- client := &fakeClient{
- getUserResp: &RemoteUserSnapshot{
- Id: 10000002,
- Source: common.UserSourceSynced,
- RemoteUserId: 790,
- SyncedQuota: 250,
- },
- }
- svc := &Service{client: client}
- result, err := svc.VerifyItem(item)
- require.NoError(t, err)
- require.True(t, result.TargetUserExists)
- require.True(t, result.RemoteConverted)
- require.False(t, result.QuotaMatched)
- }
-
- func TestVerifyItem_AllowsPostMigrationQuotaConsumption(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 791,
- Username: "cn-consumed",
- Password: "hash",
- AffCode: "CM3",
- Quota: 250,
- }).Error)
-
- item := &model.UserMigrationItem{
- BatchId: 1,
- SourceUserId: 10000003,
- TargetUserId: 791,
- SourceQuota: 200,
- Status: model.UserMigrationItemStatusMigrated,
- }
-
- client := &fakeClient{
- getUserResp: &RemoteUserSnapshot{
- Id: 10000003,
- Source: common.UserSourceSynced,
- RemoteUserId: 791,
- SyncedQuota: 200,
- },
- }
- svc := &Service{client: client}
- result, err := svc.VerifyItem(item)
- require.NoError(t, err)
- require.True(t, result.TargetUserExists)
- require.True(t, result.RemoteConverted)
- require.True(t, result.QuotaMatched)
- }
-
- func TestExecuteBatch_CreateNewFastPathRevalidatesAndMarksMigrated(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 900,
- Username: "cn-existing",
- Password: "hash",
- AffCode: "CN900",
- Quota: 300,
- }).Error)
- require.NoError(t, db.Create(&model.UserMigrationItem{
- Id: 1,
- BatchId: 10,
- SourceUserId: 10000010,
- TargetUserId: 900,
- SourceUsername: "ov-existing",
- SourceQuota: 300,
- ResolutionStrategy: model.UserMigrationStrategyCreateNew,
- Status: model.UserMigrationItemStatusFailed,
- MigrationToken: "token-fast-path",
- }).Error)
- require.NoError(t, db.Create(&model.UserMigrationBatch{
- Id: 10,
- Name: "batch-fast-path",
- SourceRegion: "ov",
- TargetRegion: "cn",
- Status: model.UserMigrationBatchStatusFailed,
- OperatorId: 1,
- }).Error)
-
- client := &fakeClient{
- getUserByID: map[int]*RemoteUserSnapshot{
- 10000010: {
- Id: 10000010,
- Username: "ov-existing",
- Quota: 300,
- Source: common.UserSourceLocal,
- OAuthBindings: nil,
- },
- },
- }
-
- svc := &Service{client: client}
- require.NoError(t, svc.ExecuteBatch(10))
- require.Len(t, client.convertCalls, 1)
- require.Equal(t, 10000010, client.convertCalls[0])
-
- items, err := model.ListUserMigrationItemsByBatch(10)
- require.NoError(t, err)
- require.Len(t, items, 1)
- require.Equal(t, model.UserMigrationItemStatusMigrated, items[0].Status)
- }
-
- func TestExecuteBatch_CreateNewFastPath_DriftFails(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 901,
- Username: "cn-existing-drift",
- Password: "hash",
- AffCode: "CND901",
- Quota: 320,
- }).Error)
- require.NoError(t, db.Create(&model.UserMigrationItem{
- Id: 2,
- BatchId: 11,
- SourceUserId: 10000018,
- TargetUserId: 901,
- SourceUsername: "ov-fast-drift",
- SourceQuota: 320,
- ResolutionStrategy: model.UserMigrationStrategyCreateNew,
- Status: model.UserMigrationItemStatusFailed,
- MigrationToken: "token-fast-path-drift",
- }).Error)
- require.NoError(t, db.Create(&model.UserMigrationBatch{
- Id: 11,
- Name: "batch-fast-path-drift",
- SourceRegion: "ov",
- TargetRegion: "cn",
- Status: model.UserMigrationBatchStatusFailed,
- OperatorId: 1,
- }).Error)
-
- client := &fakeClient{
- getUserByID: map[int]*RemoteUserSnapshot{
- 10000018: {
- Id: 10000018,
- Username: "ov-fast-drift",
- Quota: 320,
- Source: common.UserSourceSynced,
- RemoteUserId: 999,
- SyncedQuota: 320,
- },
- },
- }
-
- svc := &Service{client: client}
- require.NoError(t, svc.ExecuteBatch(11))
- require.Empty(t, client.convertCalls)
-
- items, err := model.ListUserMigrationItemsByBatch(11)
- require.NoError(t, err)
- require.Len(t, items, 1)
- require.Equal(t, model.UserMigrationItemStatusFailed, items[0].Status)
- require.Contains(t, items[0].ErrorMessage, "already synced")
- }
-
- func TestExecuteBatch_CreateNewFastPath_AlreadySyncedToSameTargetSucceeds(t *testing.T) {
- db := setupMigrationServiceDB(t)
- orig := model.DB
- model.DB = db
- defer func() {
- model.DB = orig
- }()
-
- require.NoError(t, db.Create(&model.User{
- Id: 902,
- Username: "cn-existing-same",
- Password: "hash",
- AffCode: "CND902",
- Quota: 321,
- }).Error)
- require.NoError(t, db.Create(&model.UserMigrationItem{
- Id: 3,
- BatchId: 12,
- SourceUserId: 10000019,
- TargetUserId: 902,
- SourceUsername: "ov-fast-same",
- SourceQuota: 321,
- ResolutionStrategy: model.UserMigrationStrategyCreateNew,
- Status: model.UserMigrationItemStatusFailed,
- MigrationToken: "token-fast-path-same",
- }).Error)
- require.NoError(t, db.Create(&model.UserMigrationBatch{
- Id: 12,
- Name: "batch-fast-path-same",
- SourceRegion: "ov",
- TargetRegion: "cn",
- Status: model.UserMigrationBatchStatusFailed,
- OperatorId: 1,
- }).Error)
-
- client := &fakeClient{
- getUserByID: map[int]*RemoteUserSnapshot{
- 10000019: {
- Id: 10000019,
- Username: "ov-fast-same",
- Quota: 321,
- Source: common.UserSourceSynced,
- RemoteUserId: 902,
- SyncedQuota: 321,
- },
- },
- }
-
- svc := &Service{client: client}
- require.NoError(t, svc.ExecuteBatch(12))
- require.Empty(t, client.convertCalls)
-
- items, err := model.ListUserMigrationItemsByBatch(12)
- require.NoError(t, err)
- require.Len(t, items, 1)
- require.Equal(t, model.UserMigrationItemStatusMigrated, items[0].Status)
- }
|