package user_migration import ( "fmt" "testing" "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 } 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_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) }