You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

955 lines
27 KiB

  1. package user_migration
  2. import (
  3. "fmt"
  4. "testing"
  5. "github.com/QuantumNous/new-api/common"
  6. "github.com/QuantumNous/new-api/model"
  7. "github.com/glebarez/sqlite"
  8. "github.com/stretchr/testify/require"
  9. "gorm.io/gorm"
  10. )
  11. type fakeClient struct {
  12. listPages map[int][]RemoteUserSnapshot
  13. getUserResp *RemoteUserSnapshot
  14. getUserByID map[int]*RemoteUserSnapshot
  15. hasCopyResult bool
  16. hasCopyByID map[int]bool
  17. convertCalls []int
  18. convertReqs map[int]*ConvertRemoteUserRequest
  19. }
  20. func (f *fakeClient) ListUsers(page, pageSize int) (*ListRemoteUsersResponse, error) {
  21. users := f.listPages[page]
  22. return &ListRemoteUsersResponse{
  23. Success: true,
  24. Data: users,
  25. Total: len(users),
  26. }, nil
  27. }
  28. func (f *fakeClient) GetUser(userId int) (*RemoteUserSnapshot, error) {
  29. if snapshot, ok := f.getUserByID[userId]; ok {
  30. return snapshot, nil
  31. }
  32. if f.getUserResp != nil {
  33. return f.getUserResp, nil
  34. }
  35. return nil, fmt.Errorf("user %d not found", userId)
  36. }
  37. func (f *fakeClient) ConvertToSynced(userId int, req *ConvertRemoteUserRequest) error {
  38. f.convertCalls = append(f.convertCalls, userId)
  39. if f.convertReqs == nil {
  40. f.convertReqs = make(map[int]*ConvertRemoteUserRequest)
  41. }
  42. if req != nil {
  43. copyReq := *req
  44. f.convertReqs[userId] = &copyReq
  45. if snapshot, ok := f.getUserByID[userId]; ok && snapshot != nil {
  46. snapshot.Source = common.UserSourceSynced
  47. snapshot.RemoteUserId = req.RemoteUserId
  48. snapshot.SyncedQuota = req.SyncedQuota
  49. }
  50. if f.getUserResp != nil && f.getUserResp.Id == userId {
  51. f.getUserResp.Source = common.UserSourceSynced
  52. f.getUserResp.RemoteUserId = req.RemoteUserId
  53. f.getUserResp.SyncedQuota = req.SyncedQuota
  54. }
  55. }
  56. return nil
  57. }
  58. func (f *fakeClient) HasSyncedCopy(cnUserId int) (bool, error) {
  59. if value, ok := f.hasCopyByID[cnUserId]; ok {
  60. return value, nil
  61. }
  62. return f.hasCopyResult, nil
  63. }
  64. func setupMigrationServiceDB(t *testing.T) *gorm.DB {
  65. t.Helper()
  66. db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
  67. require.NoError(t, err)
  68. sqlDB, err := db.DB()
  69. require.NoError(t, err)
  70. sqlDB.SetMaxOpenConns(1)
  71. require.NoError(t, db.AutoMigrate(
  72. &model.User{},
  73. &model.CustomOAuthProvider{},
  74. &model.UserOAuthBinding{},
  75. &model.UserMigrationBatch{},
  76. &model.UserMigrationItem{},
  77. &model.MigrationQuotaGrant{},
  78. ))
  79. return db
  80. }
  81. func TestScanBatch_NoConflict_MarksReady(t *testing.T) {
  82. db := setupMigrationServiceDB(t)
  83. orig := model.DB
  84. model.DB = db
  85. defer func() {
  86. model.DB = orig
  87. }()
  88. batch := &model.UserMigrationBatch{Name: "wave-1", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
  89. require.NoError(t, model.CreateUserMigrationBatch(batch))
  90. client := &fakeClient{
  91. listPages: map[int][]RemoteUserSnapshot{
  92. 0: {{
  93. Id: 10000001,
  94. Username: "ov-bob",
  95. Email: "bob@example.com",
  96. Quota: 456,
  97. AffCode: "A002",
  98. }},
  99. },
  100. }
  101. svc := &Service{client: client}
  102. require.NoError(t, svc.ScanBatch(batch.Id))
  103. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  104. require.NoError(t, err)
  105. require.Len(t, items, 1)
  106. require.Equal(t, model.UserMigrationStrategyCreateNew, items[0].ResolutionStrategy)
  107. require.Equal(t, model.UserMigrationItemStatusReady, items[0].Status)
  108. var savedBatch model.UserMigrationBatch
  109. require.NoError(t, db.First(&savedBatch, batch.Id).Error)
  110. require.Equal(t, model.UserMigrationBatchStatusReady, savedBatch.Status)
  111. }
  112. func TestScanBatch_EmailConflict_NoCNSyncedCopy_RecommendsMerge(t *testing.T) {
  113. db := setupMigrationServiceDB(t)
  114. orig := model.DB
  115. model.DB = db
  116. defer func() {
  117. model.DB = orig
  118. }()
  119. require.NoError(t, db.Create(&model.User{
  120. Id: 1,
  121. Username: "cn-alice",
  122. Password: "hash",
  123. Email: "alice@example.com",
  124. AffCode: "C001",
  125. }).Error)
  126. batch := &model.UserMigrationBatch{Name: "wave-2", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
  127. require.NoError(t, model.CreateUserMigrationBatch(batch))
  128. client := &fakeClient{
  129. listPages: map[int][]RemoteUserSnapshot{
  130. 0: {{
  131. Id: 10000001,
  132. Username: "ov-alice",
  133. Email: "alice@example.com",
  134. Quota: 123,
  135. AffCode: "A001",
  136. }},
  137. },
  138. hasCopyResult: false,
  139. }
  140. svc := &Service{client: client}
  141. require.NoError(t, svc.ScanBatch(batch.Id))
  142. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  143. require.NoError(t, err)
  144. require.Len(t, items, 1)
  145. require.Equal(t, model.UserMigrationStrategyMergeExisting, items[0].ResolutionStrategy)
  146. require.Equal(t, model.UserMigrationItemStatusReady, items[0].Status)
  147. require.Equal(t, 1, items[0].TargetUserId)
  148. }
  149. func TestScanBatch_EmailConflict_CNHasSyncedCopy_MarksHardConflict(t *testing.T) {
  150. db := setupMigrationServiceDB(t)
  151. orig := model.DB
  152. model.DB = db
  153. defer func() {
  154. model.DB = orig
  155. }()
  156. require.NoError(t, db.Create(&model.User{
  157. Id: 456,
  158. Username: "cn-user456",
  159. Password: "hash",
  160. Email: "shared@example.com",
  161. AffCode: "C456",
  162. }).Error)
  163. batch := &model.UserMigrationBatch{Name: "wave-3", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
  164. require.NoError(t, model.CreateUserMigrationBatch(batch))
  165. client := &fakeClient{
  166. listPages: map[int][]RemoteUserSnapshot{
  167. 0: {{
  168. Id: 10000003,
  169. Username: "ov-x",
  170. Email: "shared@example.com",
  171. Quota: 50,
  172. AffCode: "OVX",
  173. }},
  174. },
  175. hasCopyResult: true,
  176. }
  177. svc := &Service{client: client}
  178. require.NoError(t, svc.ScanBatch(batch.Id))
  179. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  180. require.NoError(t, err)
  181. require.Len(t, items, 1)
  182. require.Equal(t, model.UserMigrationItemStatusConflict, items[0].Status)
  183. require.Contains(t, items[0].ConflictFlags, "cn_already_synced_to_ov")
  184. }
  185. func TestScanBatch_Paginated(t *testing.T) {
  186. db := setupMigrationServiceDB(t)
  187. orig := model.DB
  188. model.DB = db
  189. defer func() {
  190. model.DB = orig
  191. }()
  192. batch := &model.UserMigrationBatch{Name: "wave-big", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
  193. require.NoError(t, model.CreateUserMigrationBatch(batch))
  194. page0 := make([]RemoteUserSnapshot, scanPageSize)
  195. for i := range page0 {
  196. page0[i] = RemoteUserSnapshot{
  197. Id: 10000001 + i,
  198. Username: fmt.Sprintf("u%d", i),
  199. AffCode: fmt.Sprintf("A%d", i),
  200. }
  201. }
  202. page1 := []RemoteUserSnapshot{{
  203. Id: 10000001 + scanPageSize,
  204. Username: "last",
  205. AffCode: "LAST",
  206. }}
  207. client := &fakeClient{
  208. listPages: map[int][]RemoteUserSnapshot{
  209. 0: page0,
  210. 1: page1,
  211. },
  212. }
  213. svc := &Service{client: client}
  214. require.NoError(t, svc.ScanBatch(batch.Id))
  215. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  216. require.NoError(t, err)
  217. require.Len(t, items, scanPageSize+1)
  218. }
  219. func TestScanBatch_Rescan_PreservesHumanStrategy(t *testing.T) {
  220. db := setupMigrationServiceDB(t)
  221. orig := model.DB
  222. model.DB = db
  223. defer func() {
  224. model.DB = orig
  225. }()
  226. batch := &model.UserMigrationBatch{Name: "wave-rescan", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
  227. require.NoError(t, model.CreateUserMigrationBatch(batch))
  228. client := &fakeClient{
  229. listPages: map[int][]RemoteUserSnapshot{
  230. 0: {{
  231. Id: 10000001,
  232. Username: "ov-dave",
  233. Email: "dave@example.com",
  234. Quota: 100,
  235. AffCode: "OVD1",
  236. }},
  237. },
  238. hasCopyResult: false,
  239. }
  240. svc := &Service{client: client}
  241. require.NoError(t, svc.ScanBatch(batch.Id))
  242. require.NoError(t, db.Model(&model.UserMigrationItem{}).Where("batch_id = ?", batch.Id).
  243. Update("resolution_strategy", model.UserMigrationStrategySkip).Error)
  244. require.NoError(t, svc.ScanBatch(batch.Id))
  245. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  246. require.NoError(t, err)
  247. require.Len(t, items, 1)
  248. require.Equal(t, model.UserMigrationStrategySkip, items[0].ResolutionStrategy)
  249. }
  250. func TestMigrationFlow_GrayScenarios_EndToEnd(t *testing.T) {
  251. db := setupMigrationServiceDB(t)
  252. orig := model.DB
  253. model.DB = db
  254. defer func() {
  255. model.DB = orig
  256. }()
  257. require.NoError(t, db.Create(&model.User{
  258. Id: 1,
  259. Username: "cn-merge",
  260. Password: "hash",
  261. Email: "merge@example.com",
  262. AffCode: "CNM1",
  263. Quota: 500,
  264. }).Error)
  265. require.NoError(t, db.Create(&model.User{
  266. Id: 2,
  267. Username: "taken-name",
  268. Password: "hash",
  269. Email: "taken@example.com",
  270. AffCode: "CNT2",
  271. Quota: 50,
  272. }).Error)
  273. require.NoError(t, db.Create(&model.User{
  274. Id: 3,
  275. Username: "cn-hard",
  276. Password: "hash",
  277. Email: "synced@example.com",
  278. AffCode: "CNH3",
  279. Quota: 300,
  280. }).Error)
  281. require.NoError(t, db.Create(&model.User{
  282. Id: 4,
  283. Username: "cn-soft",
  284. Password: "hash",
  285. Email: "soft-owner@example.com",
  286. AffCode: "SOFT1",
  287. Quota: 80,
  288. }).Error)
  289. batch := &model.UserMigrationBatch{
  290. Name: "gray-scenarios",
  291. SourceRegion: "ov",
  292. TargetRegion: "cn",
  293. OperatorId: 1,
  294. }
  295. require.NoError(t, model.CreateUserMigrationBatch(batch))
  296. remoteUsers := []RemoteUserSnapshot{
  297. {
  298. Id: 10000001,
  299. Username: "ov-create",
  300. Email: "create@example.com",
  301. Quota: 100,
  302. AffCode: "OVC1",
  303. },
  304. {
  305. Id: 10000002,
  306. Username: "ov-merge",
  307. Email: "merge@example.com",
  308. Quota: 200,
  309. AffCode: "OVM2",
  310. },
  311. {
  312. Id: 10000003,
  313. Username: "ov-hard",
  314. Email: "synced@example.com",
  315. Quota: 300,
  316. AffCode: "OVH3",
  317. },
  318. {
  319. Id: 10000004,
  320. Username: "taken-name",
  321. Email: "username-conflict@example.com",
  322. Quota: 120,
  323. AffCode: "OVU4",
  324. },
  325. {
  326. Id: 10000005,
  327. Username: "ov-soft",
  328. Email: "soft@example.com",
  329. Quota: 150,
  330. AffCode: "SOFT1",
  331. },
  332. }
  333. getUserByID := make(map[int]*RemoteUserSnapshot, len(remoteUsers))
  334. for i := range remoteUsers {
  335. snapshot := remoteUsers[i]
  336. getUserByID[snapshot.Id] = &snapshot
  337. }
  338. client := &fakeClient{
  339. listPages: map[int][]RemoteUserSnapshot{
  340. 0: remoteUsers,
  341. },
  342. getUserByID: getUserByID,
  343. hasCopyByID: map[int]bool{
  344. 1: false,
  345. 3: true,
  346. },
  347. }
  348. svc := &Service{client: client}
  349. require.NoError(t, svc.ScanBatch(batch.Id))
  350. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  351. require.NoError(t, err)
  352. require.Len(t, items, 5)
  353. bySourceUserID := make(map[int]*model.UserMigrationItem, len(items))
  354. for _, item := range items {
  355. bySourceUserID[item.SourceUserId] = item
  356. }
  357. require.Equal(t, model.UserMigrationStrategyCreateNew, bySourceUserID[10000001].ResolutionStrategy)
  358. require.Equal(t, model.UserMigrationItemStatusReady, bySourceUserID[10000001].Status)
  359. require.Equal(t, model.UserMigrationStrategyMergeExisting, bySourceUserID[10000002].ResolutionStrategy)
  360. require.Equal(t, 1, bySourceUserID[10000002].TargetUserId)
  361. require.Equal(t, model.UserMigrationItemStatusReady, bySourceUserID[10000002].Status)
  362. require.Equal(t, model.UserMigrationItemStatusConflict, bySourceUserID[10000003].Status)
  363. require.Contains(t, bySourceUserID[10000003].ConflictFlags, "cn_already_synced_to_ov")
  364. require.Equal(t, model.UserMigrationItemStatusConflict, bySourceUserID[10000004].Status)
  365. require.Contains(t, bySourceUserID[10000004].ConflictFlags, "username")
  366. require.Equal(t, model.UserMigrationStrategyCreateNew, bySourceUserID[10000005].ResolutionStrategy)
  367. require.Equal(t, model.UserMigrationItemStatusReady, bySourceUserID[10000005].Status)
  368. require.Contains(t, bySourceUserID[10000005].ConflictFlags, "aff_code")
  369. require.NoError(t, db.Model(&model.UserMigrationItem{}).
  370. Where("id IN ?", []int{bySourceUserID[10000003].Id, bySourceUserID[10000004].Id}).
  371. Updates(map[string]any{
  372. "resolution_strategy": model.UserMigrationStrategySkip,
  373. "status": model.UserMigrationItemStatusReady,
  374. "error_message": "",
  375. }).Error)
  376. require.NoError(t, model.RefreshUserMigrationBatchStats(batch.Id))
  377. var readyBatch model.UserMigrationBatch
  378. require.NoError(t, db.First(&readyBatch, batch.Id).Error)
  379. require.Equal(t, model.UserMigrationBatchStatusReady, readyBatch.Status)
  380. locked, err := model.TryLockBatchForExecution(batch.Id)
  381. require.NoError(t, err)
  382. require.True(t, locked)
  383. require.NoError(t, svc.ExecuteBatch(batch.Id))
  384. items, err = model.ListUserMigrationItemsByBatch(batch.Id)
  385. require.NoError(t, err)
  386. bySourceUserID = make(map[int]*model.UserMigrationItem, len(items))
  387. for _, item := range items {
  388. bySourceUserID[item.SourceUserId] = item
  389. }
  390. require.Equal(t, model.UserMigrationItemStatusMigrated, bySourceUserID[10000001].Status)
  391. require.Equal(t, model.UserMigrationItemStatusMigrated, bySourceUserID[10000002].Status)
  392. require.Equal(t, model.UserMigrationItemStatusSkipped, bySourceUserID[10000003].Status)
  393. require.Equal(t, model.UserMigrationItemStatusSkipped, bySourceUserID[10000004].Status)
  394. require.Equal(t, model.UserMigrationItemStatusMigrated, bySourceUserID[10000005].Status)
  395. var createdUser model.User
  396. require.NoError(t, db.Where("username = ?", "ov-create").First(&createdUser).Error)
  397. require.Equal(t, 100, createdUser.Quota)
  398. var mergedUser model.User
  399. require.NoError(t, db.First(&mergedUser, 1).Error)
  400. require.Equal(t, 700, mergedUser.Quota)
  401. var softUser model.User
  402. require.NoError(t, db.Where("username = ?", "ov-soft").First(&softUser).Error)
  403. require.Equal(t, 150, softUser.Quota)
  404. require.NotEqual(t, "SOFT1", softUser.AffCode)
  405. require.Equal(t, common.UserSourceSynced, client.getUserByID[10000001].Source)
  406. require.Equal(t, createdUser.Id, client.getUserByID[10000001].RemoteUserId)
  407. require.Equal(t, 100, client.getUserByID[10000001].SyncedQuota)
  408. require.Equal(t, common.UserSourceSynced, client.getUserByID[10000002].Source)
  409. require.Equal(t, 1, client.getUserByID[10000002].RemoteUserId)
  410. require.Equal(t, 700, client.getUserByID[10000002].SyncedQuota)
  411. require.NotEqual(t, common.UserSourceSynced, client.getUserByID[10000003].Source)
  412. require.NotEqual(t, common.UserSourceSynced, client.getUserByID[10000004].Source)
  413. require.Equal(t, common.UserSourceSynced, client.getUserByID[10000005].Source)
  414. require.Equal(t, softUser.Id, client.getUserByID[10000005].RemoteUserId)
  415. require.Equal(t, 150, client.getUserByID[10000005].SyncedQuota)
  416. for _, sourceUserID := range []int{10000001, 10000002, 10000005} {
  417. result, verifyErr := svc.VerifyItem(bySourceUserID[sourceUserID])
  418. require.NoError(t, verifyErr)
  419. require.True(t, result.TargetUserExists)
  420. require.True(t, result.RemoteConverted)
  421. require.True(t, result.QuotaMatched)
  422. }
  423. require.NoError(t, db.Model(&model.UserMigrationBatch{}).Where("id = ?", batch.Id).
  424. Update("status", model.UserMigrationBatchStatusReady).Error)
  425. require.NoError(t, db.Model(&model.UserMigrationItem{}).
  426. Where("id IN ?", []int{
  427. bySourceUserID[10000001].Id,
  428. bySourceUserID[10000002].Id,
  429. bySourceUserID[10000005].Id,
  430. }).
  431. Update("status", model.UserMigrationItemStatusReady).Error)
  432. locked, err = model.TryLockBatchForExecution(batch.Id)
  433. require.NoError(t, err)
  434. require.True(t, locked)
  435. require.NoError(t, svc.ExecuteBatch(batch.Id))
  436. require.Len(t, client.convertCalls, 3)
  437. require.NoError(t, db.First(&mergedUser, 1).Error)
  438. require.Equal(t, 700, mergedUser.Quota)
  439. }
  440. func TestExecuteBatch_CreateNew_CreatesUserAndConvertsRemote(t *testing.T) {
  441. db := setupMigrationServiceDB(t)
  442. orig := model.DB
  443. model.DB = db
  444. defer func() {
  445. model.DB = orig
  446. }()
  447. batch := &model.UserMigrationBatch{
  448. Name: "wave-exec",
  449. Status: model.UserMigrationBatchStatusReady,
  450. }
  451. batch.SourceRegion = "ov"
  452. batch.TargetRegion = "cn"
  453. batch.OperatorId = 1
  454. require.NoError(t, model.CreateUserMigrationBatch(batch))
  455. item := &model.UserMigrationItem{
  456. BatchId: batch.Id,
  457. SourceUserId: 10000001,
  458. SourceUsername: "ov-alice",
  459. SourceEmail: "alice@example.com",
  460. SourceQuota: 300,
  461. ResolutionStrategy: model.UserMigrationStrategyCreateNew,
  462. Status: model.UserMigrationItemStatusReady,
  463. MigrationToken: "batch-1-user-10000001",
  464. }
  465. require.NoError(t, model.UpsertUserMigrationItem(item))
  466. client := &fakeClient{
  467. getUserResp: &RemoteUserSnapshot{
  468. Id: 10000001,
  469. Username: "ov-alice",
  470. Password: "hashed-password",
  471. Email: "alice@example.com",
  472. DisplayName: "Alice",
  473. Status: 1,
  474. Role: 1,
  475. Group: "default",
  476. Quota: 300,
  477. AffCode: "OVA1",
  478. },
  479. }
  480. svc := &Service{client: client}
  481. locked, err := model.TryLockBatchForExecution(batch.Id)
  482. require.NoError(t, err)
  483. require.True(t, locked)
  484. require.NoError(t, svc.ExecuteBatch(batch.Id))
  485. var cnUser model.User
  486. require.NoError(t, db.Where("username = ?", "ov-alice").First(&cnUser).Error)
  487. require.Equal(t, 300, cnUser.Quota)
  488. require.Less(t, cnUser.Id, 10000000)
  489. require.Len(t, client.convertCalls, 1)
  490. require.Equal(t, 10000001, client.convertCalls[0])
  491. }
  492. func TestExecuteBatch_CreateNew_IdempotentOnRetry(t *testing.T) {
  493. db := setupMigrationServiceDB(t)
  494. orig := model.DB
  495. model.DB = db
  496. defer func() {
  497. model.DB = orig
  498. }()
  499. batch := &model.UserMigrationBatch{
  500. Name: "wave-retry",
  501. Status: model.UserMigrationBatchStatusReady,
  502. }
  503. batch.SourceRegion = "ov"
  504. batch.TargetRegion = "cn"
  505. batch.OperatorId = 1
  506. require.NoError(t, model.CreateUserMigrationBatch(batch))
  507. item := &model.UserMigrationItem{
  508. BatchId: batch.Id,
  509. SourceUserId: 10000002,
  510. SourceUsername: "ov-carol",
  511. SourceEmail: "carol@example.com",
  512. SourceQuota: 150,
  513. ResolutionStrategy: model.UserMigrationStrategyCreateNew,
  514. Status: model.UserMigrationItemStatusReady,
  515. MigrationToken: "batch-1-user-10000002",
  516. }
  517. require.NoError(t, model.UpsertUserMigrationItem(item))
  518. snapshot := &RemoteUserSnapshot{
  519. Id: 10000002,
  520. Username: "ov-carol",
  521. Password: "hash",
  522. Email: "carol@example.com",
  523. Quota: 150,
  524. AffCode: "OVC1",
  525. }
  526. client := &fakeClient{getUserResp: snapshot}
  527. svc := &Service{client: client}
  528. locked, err := model.TryLockBatchForExecution(batch.Id)
  529. require.NoError(t, err)
  530. require.True(t, locked)
  531. require.NoError(t, svc.ExecuteBatch(batch.Id))
  532. require.NoError(t, db.Model(&model.UserMigrationBatch{}).Where("id = ?", batch.Id).
  533. Update("status", model.UserMigrationBatchStatusFailed).Error)
  534. require.NoError(t, db.Model(&model.UserMigrationItem{}).Where("batch_id = ?", batch.Id).
  535. Update("status", model.UserMigrationItemStatusReady).Error)
  536. locked, err = model.TryLockBatchForExecution(batch.Id)
  537. require.NoError(t, err)
  538. require.True(t, locked)
  539. require.NoError(t, svc.ExecuteBatch(batch.Id))
  540. var count int64
  541. require.NoError(t, db.Model(&model.User{}).Where("username = ?", "ov-carol").Count(&count).Error)
  542. require.EqualValues(t, 1, count)
  543. require.Len(t, client.convertCalls, 1)
  544. }
  545. func TestExecuteBatch_CreateNew_ExecutionTimeDriftFails(t *testing.T) {
  546. db := setupMigrationServiceDB(t)
  547. orig := model.DB
  548. model.DB = db
  549. defer func() {
  550. model.DB = orig
  551. }()
  552. require.NoError(t, db.Create(&model.User{
  553. Id: 7,
  554. Username: "cn-alice",
  555. Password: "hash",
  556. Email: "alice@example.com",
  557. AffCode: "C007",
  558. }).Error)
  559. batch := &model.UserMigrationBatch{
  560. Name: "wave-create-drift",
  561. SourceRegion: "ov",
  562. TargetRegion: "cn",
  563. OperatorId: 1,
  564. Status: model.UserMigrationBatchStatusReady,
  565. }
  566. require.NoError(t, model.CreateUserMigrationBatch(batch))
  567. item := &model.UserMigrationItem{
  568. BatchId: batch.Id,
  569. SourceUserId: 10000020,
  570. SourceUsername: "ov-drift",
  571. SourceEmail: "old@example.com",
  572. SourceQuota: 120,
  573. ResolutionStrategy: model.UserMigrationStrategyCreateNew,
  574. Status: model.UserMigrationItemStatusReady,
  575. MigrationToken: "batch-1-user-10000020",
  576. }
  577. require.NoError(t, model.UpsertUserMigrationItem(item))
  578. client := &fakeClient{
  579. getUserResp: &RemoteUserSnapshot{
  580. Id: 10000020,
  581. Username: "ov-drift",
  582. Password: "hash",
  583. Email: "alice@example.com",
  584. Quota: 120,
  585. AffCode: "OV20",
  586. },
  587. hasCopyByID: map[int]bool{7: false},
  588. }
  589. svc := &Service{client: client}
  590. locked, err := model.TryLockBatchForExecution(batch.Id)
  591. require.NoError(t, err)
  592. require.True(t, locked)
  593. require.NoError(t, svc.ExecuteBatch(batch.Id))
  594. require.Len(t, client.convertCalls, 0)
  595. var count int64
  596. require.NoError(t, db.Model(&model.User{}).Where("username = ?", "ov-drift").Count(&count).Error)
  597. require.EqualValues(t, 0, count)
  598. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  599. require.NoError(t, err)
  600. require.Len(t, items, 1)
  601. require.Equal(t, model.UserMigrationItemStatusFailed, items[0].Status)
  602. require.Contains(t, items[0].ErrorMessage, "identity drift")
  603. }
  604. func TestExecuteBatch_MergeExisting_IdempotentQuota(t *testing.T) {
  605. db := setupMigrationServiceDB(t)
  606. orig := model.DB
  607. model.DB = db
  608. defer func() {
  609. model.DB = orig
  610. }()
  611. require.NoError(t, db.Create(&model.User{
  612. Id: 1,
  613. Username: "cn-bob",
  614. Password: "hash",
  615. Email: "bob@example.com",
  616. AffCode: "CNB1",
  617. Quota: 500,
  618. }).Error)
  619. batch := &model.UserMigrationBatch{
  620. Name: "wave-merge",
  621. Status: model.UserMigrationBatchStatusReady,
  622. }
  623. batch.SourceRegion = "ov"
  624. batch.TargetRegion = "cn"
  625. batch.OperatorId = 1
  626. require.NoError(t, model.CreateUserMigrationBatch(batch))
  627. item := &model.UserMigrationItem{
  628. BatchId: batch.Id,
  629. SourceUserId: 10000002,
  630. TargetUserId: 1,
  631. SourceUsername: "ov-bob",
  632. SourceEmail: "bob@example.com",
  633. SourceQuota: 200,
  634. ResolutionStrategy: model.UserMigrationStrategyMergeExisting,
  635. Status: model.UserMigrationItemStatusReady,
  636. MigrationToken: "batch-1-user-10000002",
  637. }
  638. require.NoError(t, model.UpsertUserMigrationItem(item))
  639. client := &fakeClient{
  640. getUserResp: &RemoteUserSnapshot{
  641. Id: 10000002,
  642. Username: "ov-bob",
  643. Password: "hashed",
  644. Email: "bob@example.com",
  645. Quota: 200,
  646. },
  647. }
  648. svc := &Service{client: client}
  649. for i := 0; i < 2; i++ {
  650. require.NoError(t, db.Model(&model.UserMigrationBatch{}).Where("id = ?", batch.Id).
  651. Update("status", model.UserMigrationBatchStatusReady).Error)
  652. require.NoError(t, db.Model(&model.UserMigrationItem{}).Where("batch_id = ?", batch.Id).
  653. Update("status", model.UserMigrationItemStatusReady).Error)
  654. locked, err := model.TryLockBatchForExecution(batch.Id)
  655. require.NoError(t, err)
  656. require.True(t, locked)
  657. require.NoError(t, svc.ExecuteBatch(batch.Id))
  658. }
  659. var saved model.User
  660. require.NoError(t, db.First(&saved, 1).Error)
  661. require.Equal(t, 700, saved.Quota)
  662. require.Len(t, client.convertCalls, 1)
  663. }
  664. func TestExecuteBatch_MergeExisting_ExecutionTimeDriftFails(t *testing.T) {
  665. db := setupMigrationServiceDB(t)
  666. orig := model.DB
  667. model.DB = db
  668. defer func() {
  669. model.DB = orig
  670. }()
  671. require.NoError(t, db.Create(&model.User{
  672. Id: 1,
  673. Username: "cn-bob",
  674. Password: "hash",
  675. Email: "bob@example.com",
  676. AffCode: "CNB1",
  677. Quota: 500,
  678. }).Error)
  679. batch := &model.UserMigrationBatch{
  680. Name: "wave-merge-drift",
  681. SourceRegion: "ov",
  682. TargetRegion: "cn",
  683. OperatorId: 1,
  684. Status: model.UserMigrationBatchStatusReady,
  685. }
  686. require.NoError(t, model.CreateUserMigrationBatch(batch))
  687. item := &model.UserMigrationItem{
  688. BatchId: batch.Id,
  689. SourceUserId: 10000021,
  690. TargetUserId: 1,
  691. SourceUsername: "ov-bob",
  692. SourceEmail: "bob@example.com",
  693. SourceQuota: 200,
  694. ResolutionStrategy: model.UserMigrationStrategyMergeExisting,
  695. Status: model.UserMigrationItemStatusReady,
  696. MigrationToken: "batch-1-user-10000021",
  697. }
  698. require.NoError(t, model.UpsertUserMigrationItem(item))
  699. client := &fakeClient{
  700. getUserResp: &RemoteUserSnapshot{
  701. Id: 10000021,
  702. Username: "ov-bob",
  703. Password: "hash",
  704. Email: "changed@example.com",
  705. Quota: 200,
  706. AffCode: "OV21",
  707. },
  708. }
  709. svc := &Service{client: client}
  710. locked, err := model.TryLockBatchForExecution(batch.Id)
  711. require.NoError(t, err)
  712. require.True(t, locked)
  713. require.NoError(t, svc.ExecuteBatch(batch.Id))
  714. require.Len(t, client.convertCalls, 0)
  715. var cnUser model.User
  716. require.NoError(t, db.First(&cnUser, 1).Error)
  717. require.Equal(t, 500, cnUser.Quota)
  718. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  719. require.NoError(t, err)
  720. require.Len(t, items, 1)
  721. require.Equal(t, model.UserMigrationItemStatusFailed, items[0].Status)
  722. require.Contains(t, items[0].ErrorMessage, "merge target drift")
  723. }
  724. func TestExecuteBatch_SkipItem_DoesNotConvert(t *testing.T) {
  725. db := setupMigrationServiceDB(t)
  726. orig := model.DB
  727. model.DB = db
  728. defer func() {
  729. model.DB = orig
  730. }()
  731. batch := &model.UserMigrationBatch{
  732. Name: "skip-test",
  733. SourceRegion: "ov",
  734. TargetRegion: "cn",
  735. OperatorId: 1,
  736. Status: model.UserMigrationBatchStatusReady,
  737. }
  738. require.NoError(t, model.CreateUserMigrationBatch(batch))
  739. require.NoError(t, model.UpsertUserMigrationItem(&model.UserMigrationItem{
  740. BatchId: batch.Id,
  741. SourceUserId: 10000099,
  742. SourceUsername: "skip-user",
  743. SourceQuota: 50,
  744. ResolutionStrategy: model.UserMigrationStrategySkip,
  745. Status: model.UserMigrationItemStatusReady,
  746. MigrationToken: "batch-1-user-10000099",
  747. }))
  748. client := &fakeClient{}
  749. svc := &Service{client: client}
  750. locked, err := model.TryLockBatchForExecution(batch.Id)
  751. require.NoError(t, err)
  752. require.True(t, locked)
  753. require.NoError(t, svc.ExecuteBatch(batch.Id))
  754. require.Len(t, client.convertCalls, 0)
  755. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  756. require.NoError(t, err)
  757. require.Len(t, items, 1)
  758. require.Equal(t, model.UserMigrationItemStatusSkipped, items[0].Status)
  759. }
  760. func TestVerifyItem_CorrectlyChecksRemoteConverted(t *testing.T) {
  761. db := setupMigrationServiceDB(t)
  762. orig := model.DB
  763. model.DB = db
  764. defer func() {
  765. model.DB = orig
  766. }()
  767. require.NoError(t, db.Create(&model.User{
  768. Id: 789,
  769. Username: "cn-migrated",
  770. Password: "hash",
  771. AffCode: "CM1",
  772. Quota: 300,
  773. }).Error)
  774. item := &model.UserMigrationItem{
  775. BatchId: 1,
  776. SourceUserId: 10000001,
  777. TargetUserId: 789,
  778. SourceQuota: 300,
  779. Status: model.UserMigrationItemStatusMigrated,
  780. }
  781. client := &fakeClient{
  782. getUserResp: &RemoteUserSnapshot{
  783. Id: 10000001,
  784. Source: common.UserSourceSynced,
  785. RemoteUserId: 789,
  786. SyncedQuota: 300,
  787. },
  788. }
  789. svc := &Service{client: client}
  790. result, err := svc.VerifyItem(item)
  791. require.NoError(t, err)
  792. require.True(t, result.TargetUserExists)
  793. require.True(t, result.RemoteConverted)
  794. require.True(t, result.QuotaMatched)
  795. }
  796. func TestVerifyItem_QuotaMismatchDetected(t *testing.T) {
  797. db := setupMigrationServiceDB(t)
  798. orig := model.DB
  799. model.DB = db
  800. defer func() {
  801. model.DB = orig
  802. }()
  803. require.NoError(t, db.Create(&model.User{
  804. Id: 790,
  805. Username: "cn-mismatch",
  806. Password: "hash",
  807. AffCode: "CM2",
  808. Quota: 300,
  809. }).Error)
  810. item := &model.UserMigrationItem{
  811. BatchId: 1,
  812. SourceUserId: 10000002,
  813. TargetUserId: 790,
  814. SourceQuota: 300,
  815. Status: model.UserMigrationItemStatusMigrated,
  816. }
  817. client := &fakeClient{
  818. getUserResp: &RemoteUserSnapshot{
  819. Id: 10000002,
  820. Source: common.UserSourceSynced,
  821. RemoteUserId: 790,
  822. SyncedQuota: 250,
  823. },
  824. }
  825. svc := &Service{client: client}
  826. result, err := svc.VerifyItem(item)
  827. require.NoError(t, err)
  828. require.True(t, result.TargetUserExists)
  829. require.True(t, result.RemoteConverted)
  830. require.False(t, result.QuotaMatched)
  831. }