Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

1503 строки
42 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_GitHubId_SameUser_RecommendsMerge(t *testing.T) {
  186. db := setupMigrationServiceDB(t)
  187. orig := model.DB
  188. model.DB = db
  189. defer func() {
  190. model.DB = orig
  191. }()
  192. require.NoError(t, db.Create(&model.User{
  193. Id: 11,
  194. Username: "cn-github",
  195. Password: "hash",
  196. AffCode: "CGH1",
  197. GitHubId: "gh-123",
  198. }).Error)
  199. batch := &model.UserMigrationBatch{Name: "wave-github-merge", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
  200. require.NoError(t, model.CreateUserMigrationBatch(batch))
  201. client := &fakeClient{
  202. listPages: map[int][]RemoteUserSnapshot{
  203. 0: {{
  204. Id: 10000011,
  205. Username: "ov-github",
  206. GitHubId: "gh-123",
  207. Quota: 88,
  208. AffCode: "OVGH1",
  209. }},
  210. },
  211. hasCopyByID: map[int]bool{11: false},
  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, 1)
  218. require.Equal(t, model.UserMigrationStrategyMergeExisting, items[0].ResolutionStrategy)
  219. require.Equal(t, model.UserMigrationItemStatusReady, items[0].Status)
  220. require.Equal(t, 11, items[0].TargetUserId)
  221. require.Contains(t, items[0].ConflictFlags, "github_id")
  222. }
  223. func TestScanBatch_OAuthId_DifferentUsers_MultipleMergeTargets(t *testing.T) {
  224. db := setupMigrationServiceDB(t)
  225. orig := model.DB
  226. model.DB = db
  227. defer func() {
  228. model.DB = orig
  229. }()
  230. require.NoError(t, db.Create(&model.User{
  231. Id: 21,
  232. Username: "cn-github-owner",
  233. Password: "hash",
  234. AffCode: "COA1",
  235. GitHubId: "gh-shared",
  236. }).Error)
  237. require.NoError(t, db.Create(&model.User{
  238. Id: 22,
  239. Username: "cn-discord-owner",
  240. Password: "hash",
  241. AffCode: "COA2",
  242. DiscordId: "dc-shared",
  243. }).Error)
  244. batch := &model.UserMigrationBatch{Name: "wave-oauth-conflict", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
  245. require.NoError(t, model.CreateUserMigrationBatch(batch))
  246. client := &fakeClient{
  247. listPages: map[int][]RemoteUserSnapshot{
  248. 0: {{
  249. Id: 10000012,
  250. Username: "ov-oauth-conflict",
  251. GitHubId: "gh-shared",
  252. DiscordId: "dc-shared",
  253. Quota: 66,
  254. AffCode: "OVOA1",
  255. }},
  256. },
  257. }
  258. svc := &Service{client: client}
  259. require.NoError(t, svc.ScanBatch(batch.Id))
  260. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  261. require.NoError(t, err)
  262. require.Len(t, items, 1)
  263. require.Equal(t, model.UserMigrationItemStatusConflict, items[0].Status)
  264. require.Contains(t, items[0].ConflictFlags, "github_id")
  265. require.Contains(t, items[0].ConflictFlags, "discord_id")
  266. require.Contains(t, items[0].ConflictFlags, "multiple_merge_targets")
  267. }
  268. func TestScanBatch_CustomOAuthBinding_MatchesExistingUser_RecommendsMerge(t *testing.T) {
  269. db := setupMigrationServiceDB(t)
  270. orig := model.DB
  271. model.DB = db
  272. defer func() {
  273. model.DB = orig
  274. }()
  275. require.NoError(t, db.Create(&model.CustomOAuthProvider{
  276. Id: 31,
  277. Name: "GitHub Enterprise",
  278. Slug: "gh-enterprise",
  279. Enabled: true,
  280. ClientId: "cid",
  281. AuthorizationEndpoint: "https://auth.example.com",
  282. TokenEndpoint: "https://token.example.com",
  283. UserInfoEndpoint: "https://userinfo.example.com",
  284. }).Error)
  285. require.NoError(t, db.Create(&model.User{
  286. Id: 32,
  287. Username: "cn-custom-oauth",
  288. Password: "hash",
  289. AffCode: "CCO1",
  290. }).Error)
  291. require.NoError(t, db.Create(&model.UserOAuthBinding{
  292. UserId: 32,
  293. ProviderId: 31,
  294. ProviderUserId: "provider-user-1",
  295. }).Error)
  296. batch := &model.UserMigrationBatch{Name: "wave-custom-oauth-merge", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
  297. require.NoError(t, model.CreateUserMigrationBatch(batch))
  298. client := &fakeClient{
  299. listPages: map[int][]RemoteUserSnapshot{
  300. 0: {{
  301. Id: 10000013,
  302. Username: "ov-custom-oauth",
  303. Quota: 55,
  304. AffCode: "OVCO1",
  305. OAuthBindings: []RemoteOAuthBinding{{
  306. ProviderSlug: "gh-enterprise",
  307. ProviderUserId: "provider-user-1",
  308. }},
  309. }},
  310. },
  311. hasCopyByID: map[int]bool{32: false},
  312. }
  313. svc := &Service{client: client}
  314. require.NoError(t, svc.ScanBatch(batch.Id))
  315. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  316. require.NoError(t, err)
  317. require.Len(t, items, 1)
  318. require.Equal(t, model.UserMigrationStrategyMergeExisting, items[0].ResolutionStrategy)
  319. require.Equal(t, model.UserMigrationItemStatusReady, items[0].Status)
  320. require.Equal(t, 32, items[0].TargetUserId)
  321. require.Contains(t, items[0].ConflictFlags, "oauth:gh-enterprise")
  322. }
  323. func TestScanBatch_CustomOAuthBinding_UnknownSlug_Ignored(t *testing.T) {
  324. db := setupMigrationServiceDB(t)
  325. orig := model.DB
  326. model.DB = db
  327. defer func() {
  328. model.DB = orig
  329. }()
  330. batch := &model.UserMigrationBatch{Name: "wave-custom-oauth-unknown", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
  331. require.NoError(t, model.CreateUserMigrationBatch(batch))
  332. client := &fakeClient{
  333. listPages: map[int][]RemoteUserSnapshot{
  334. 0: {{
  335. Id: 10000014,
  336. Username: "ov-unknown-oauth",
  337. Quota: 77,
  338. AffCode: "OVUK1",
  339. OAuthBindings: []RemoteOAuthBinding{{
  340. ProviderSlug: "unknown-provider",
  341. ProviderUserId: "u-1",
  342. }},
  343. }},
  344. },
  345. }
  346. svc := &Service{client: client}
  347. require.NoError(t, svc.ScanBatch(batch.Id))
  348. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  349. require.NoError(t, err)
  350. require.Len(t, items, 1)
  351. require.Equal(t, model.UserMigrationStrategyCreateNew, items[0].ResolutionStrategy)
  352. require.Equal(t, model.UserMigrationItemStatusReady, items[0].Status)
  353. require.NotContains(t, items[0].ConflictFlags, "oauth:")
  354. }
  355. func TestScanBatch_EmailAndOAuth_SameUser_RecommendsMerge(t *testing.T) {
  356. db := setupMigrationServiceDB(t)
  357. orig := model.DB
  358. model.DB = db
  359. defer func() {
  360. model.DB = orig
  361. }()
  362. require.NoError(t, db.Create(&model.User{
  363. Id: 41,
  364. Username: "cn-same-user",
  365. Password: "hash",
  366. Email: "same@example.com",
  367. AffCode: "CSU1",
  368. GitHubId: "gh-same",
  369. }).Error)
  370. batch := &model.UserMigrationBatch{Name: "wave-email-oauth-same", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
  371. require.NoError(t, model.CreateUserMigrationBatch(batch))
  372. client := &fakeClient{
  373. listPages: map[int][]RemoteUserSnapshot{
  374. 0: {{
  375. Id: 10000015,
  376. Username: "ov-same-user",
  377. Email: "same@example.com",
  378. GitHubId: "gh-same",
  379. Quota: 91,
  380. AffCode: "OVSU1",
  381. }},
  382. },
  383. hasCopyByID: map[int]bool{41: false},
  384. }
  385. svc := &Service{client: client}
  386. require.NoError(t, svc.ScanBatch(batch.Id))
  387. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  388. require.NoError(t, err)
  389. require.Len(t, items, 1)
  390. require.Equal(t, model.UserMigrationStrategyMergeExisting, items[0].ResolutionStrategy)
  391. require.Equal(t, model.UserMigrationItemStatusReady, items[0].Status)
  392. require.Equal(t, 41, items[0].TargetUserId)
  393. require.Contains(t, items[0].ConflictFlags, "email")
  394. require.Contains(t, items[0].ConflictFlags, "github_id")
  395. require.NotContains(t, items[0].ConflictFlags, "multiple_merge_targets")
  396. }
  397. func TestScanBatch_EmailAndOAuth_DifferentUsers_Conflicts(t *testing.T) {
  398. db := setupMigrationServiceDB(t)
  399. orig := model.DB
  400. model.DB = db
  401. defer func() {
  402. model.DB = orig
  403. }()
  404. require.NoError(t, db.Create(&model.User{
  405. Id: 51,
  406. Username: "cn-email-owner",
  407. Password: "hash",
  408. Email: "combo@example.com",
  409. AffCode: "CEO1",
  410. }).Error)
  411. require.NoError(t, db.Create(&model.User{
  412. Id: 52,
  413. Username: "cn-oauth-owner",
  414. Password: "hash",
  415. AffCode: "COO1",
  416. GitHubId: "gh-combo",
  417. }).Error)
  418. batch := &model.UserMigrationBatch{Name: "wave-email-oauth-diff", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
  419. require.NoError(t, model.CreateUserMigrationBatch(batch))
  420. client := &fakeClient{
  421. listPages: map[int][]RemoteUserSnapshot{
  422. 0: {{
  423. Id: 10000016,
  424. Username: "ov-combo-user",
  425. Email: "combo@example.com",
  426. GitHubId: "gh-combo",
  427. Quota: 92,
  428. AffCode: "OVCO2",
  429. }},
  430. },
  431. }
  432. svc := &Service{client: client}
  433. require.NoError(t, svc.ScanBatch(batch.Id))
  434. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  435. require.NoError(t, err)
  436. require.Len(t, items, 1)
  437. require.Equal(t, model.UserMigrationItemStatusConflict, items[0].Status)
  438. require.Contains(t, items[0].ConflictFlags, "email")
  439. require.Contains(t, items[0].ConflictFlags, "github_id")
  440. require.Contains(t, items[0].ConflictFlags, "multiple_merge_targets")
  441. }
  442. func TestScanBatch_UsernameConflict_OverridesMergeCandidate(t *testing.T) {
  443. db := setupMigrationServiceDB(t)
  444. orig := model.DB
  445. model.DB = db
  446. defer func() {
  447. model.DB = orig
  448. }()
  449. require.NoError(t, db.Create(&model.User{
  450. Id: 61,
  451. Username: "same-name",
  452. Password: "hash",
  453. AffCode: "CSN1",
  454. }).Error)
  455. require.NoError(t, db.Create(&model.User{
  456. Id: 62,
  457. Username: "other-user",
  458. Password: "hash",
  459. Email: "mergeable@example.com",
  460. AffCode: "CME1",
  461. }).Error)
  462. batch := &model.UserMigrationBatch{Name: "wave-username-overrides", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
  463. require.NoError(t, model.CreateUserMigrationBatch(batch))
  464. client := &fakeClient{
  465. listPages: map[int][]RemoteUserSnapshot{
  466. 0: {{
  467. Id: 10000017,
  468. Username: "same-name",
  469. Email: "mergeable@example.com",
  470. Quota: 93,
  471. AffCode: "OVUN1",
  472. }},
  473. },
  474. hasCopyByID: map[int]bool{62: false},
  475. }
  476. svc := &Service{client: client}
  477. require.NoError(t, svc.ScanBatch(batch.Id))
  478. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  479. require.NoError(t, err)
  480. require.Len(t, items, 1)
  481. require.Equal(t, model.UserMigrationItemStatusConflict, items[0].Status)
  482. require.Contains(t, items[0].ConflictFlags, "username")
  483. }
  484. func TestScanBatch_Paginated(t *testing.T) {
  485. db := setupMigrationServiceDB(t)
  486. orig := model.DB
  487. model.DB = db
  488. defer func() {
  489. model.DB = orig
  490. }()
  491. batch := &model.UserMigrationBatch{Name: "wave-big", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
  492. require.NoError(t, model.CreateUserMigrationBatch(batch))
  493. page0 := make([]RemoteUserSnapshot, scanPageSize)
  494. for i := range page0 {
  495. page0[i] = RemoteUserSnapshot{
  496. Id: 10000001 + i,
  497. Username: fmt.Sprintf("u%d", i),
  498. AffCode: fmt.Sprintf("A%d", i),
  499. }
  500. }
  501. page1 := []RemoteUserSnapshot{{
  502. Id: 10000001 + scanPageSize,
  503. Username: "last",
  504. AffCode: "LAST",
  505. }}
  506. client := &fakeClient{
  507. listPages: map[int][]RemoteUserSnapshot{
  508. 0: page0,
  509. 1: page1,
  510. },
  511. }
  512. svc := &Service{client: client}
  513. require.NoError(t, svc.ScanBatch(batch.Id))
  514. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  515. require.NoError(t, err)
  516. require.Len(t, items, scanPageSize+1)
  517. }
  518. func TestScanBatch_Rescan_PreservesHumanStrategy(t *testing.T) {
  519. db := setupMigrationServiceDB(t)
  520. orig := model.DB
  521. model.DB = db
  522. defer func() {
  523. model.DB = orig
  524. }()
  525. batch := &model.UserMigrationBatch{Name: "wave-rescan", SourceRegion: "ov", TargetRegion: "cn", OperatorId: 1}
  526. require.NoError(t, model.CreateUserMigrationBatch(batch))
  527. client := &fakeClient{
  528. listPages: map[int][]RemoteUserSnapshot{
  529. 0: {{
  530. Id: 10000001,
  531. Username: "ov-dave",
  532. Email: "dave@example.com",
  533. Quota: 100,
  534. AffCode: "OVD1",
  535. }},
  536. },
  537. hasCopyResult: false,
  538. }
  539. svc := &Service{client: client}
  540. require.NoError(t, svc.ScanBatch(batch.Id))
  541. require.NoError(t, db.Model(&model.UserMigrationItem{}).Where("batch_id = ?", batch.Id).
  542. Update("resolution_strategy", model.UserMigrationStrategySkip).Error)
  543. require.NoError(t, svc.ScanBatch(batch.Id))
  544. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  545. require.NoError(t, err)
  546. require.Len(t, items, 1)
  547. require.Equal(t, model.UserMigrationStrategySkip, items[0].ResolutionStrategy)
  548. }
  549. func TestMigrationFlow_GrayScenarios_EndToEnd(t *testing.T) {
  550. db := setupMigrationServiceDB(t)
  551. orig := model.DB
  552. model.DB = db
  553. defer func() {
  554. model.DB = orig
  555. }()
  556. require.NoError(t, db.Create(&model.User{
  557. Id: 1,
  558. Username: "cn-merge",
  559. Password: "hash",
  560. Email: "merge@example.com",
  561. AffCode: "CNM1",
  562. Quota: 500,
  563. }).Error)
  564. require.NoError(t, db.Create(&model.User{
  565. Id: 2,
  566. Username: "taken-name",
  567. Password: "hash",
  568. Email: "taken@example.com",
  569. AffCode: "CNT2",
  570. Quota: 50,
  571. }).Error)
  572. require.NoError(t, db.Create(&model.User{
  573. Id: 3,
  574. Username: "cn-hard",
  575. Password: "hash",
  576. Email: "synced@example.com",
  577. AffCode: "CNH3",
  578. Quota: 300,
  579. }).Error)
  580. require.NoError(t, db.Create(&model.User{
  581. Id: 4,
  582. Username: "cn-soft",
  583. Password: "hash",
  584. Email: "soft-owner@example.com",
  585. AffCode: "SOFT1",
  586. Quota: 80,
  587. }).Error)
  588. batch := &model.UserMigrationBatch{
  589. Name: "gray-scenarios",
  590. SourceRegion: "ov",
  591. TargetRegion: "cn",
  592. OperatorId: 1,
  593. }
  594. require.NoError(t, model.CreateUserMigrationBatch(batch))
  595. remoteUsers := []RemoteUserSnapshot{
  596. {
  597. Id: 10000001,
  598. Username: "ov-create",
  599. Email: "create@example.com",
  600. Quota: 100,
  601. AffCode: "OVC1",
  602. },
  603. {
  604. Id: 10000002,
  605. Username: "ov-merge",
  606. Email: "merge@example.com",
  607. Quota: 200,
  608. AffCode: "OVM2",
  609. },
  610. {
  611. Id: 10000003,
  612. Username: "ov-hard",
  613. Email: "synced@example.com",
  614. Quota: 300,
  615. AffCode: "OVH3",
  616. },
  617. {
  618. Id: 10000004,
  619. Username: "taken-name",
  620. Email: "username-conflict@example.com",
  621. Quota: 120,
  622. AffCode: "OVU4",
  623. },
  624. {
  625. Id: 10000005,
  626. Username: "ov-soft",
  627. Email: "soft@example.com",
  628. Quota: 150,
  629. AffCode: "SOFT1",
  630. },
  631. }
  632. getUserByID := make(map[int]*RemoteUserSnapshot, len(remoteUsers))
  633. for i := range remoteUsers {
  634. snapshot := remoteUsers[i]
  635. getUserByID[snapshot.Id] = &snapshot
  636. }
  637. client := &fakeClient{
  638. listPages: map[int][]RemoteUserSnapshot{
  639. 0: remoteUsers,
  640. },
  641. getUserByID: getUserByID,
  642. hasCopyByID: map[int]bool{
  643. 1: false,
  644. 3: true,
  645. },
  646. }
  647. svc := &Service{client: client}
  648. require.NoError(t, svc.ScanBatch(batch.Id))
  649. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  650. require.NoError(t, err)
  651. require.Len(t, items, 5)
  652. bySourceUserID := make(map[int]*model.UserMigrationItem, len(items))
  653. for _, item := range items {
  654. bySourceUserID[item.SourceUserId] = item
  655. }
  656. require.Equal(t, model.UserMigrationStrategyCreateNew, bySourceUserID[10000001].ResolutionStrategy)
  657. require.Equal(t, model.UserMigrationItemStatusReady, bySourceUserID[10000001].Status)
  658. require.Equal(t, model.UserMigrationStrategyMergeExisting, bySourceUserID[10000002].ResolutionStrategy)
  659. require.Equal(t, 1, bySourceUserID[10000002].TargetUserId)
  660. require.Equal(t, model.UserMigrationItemStatusReady, bySourceUserID[10000002].Status)
  661. require.Equal(t, model.UserMigrationItemStatusConflict, bySourceUserID[10000003].Status)
  662. require.Contains(t, bySourceUserID[10000003].ConflictFlags, "cn_already_synced_to_ov")
  663. require.Equal(t, model.UserMigrationItemStatusConflict, bySourceUserID[10000004].Status)
  664. require.Contains(t, bySourceUserID[10000004].ConflictFlags, "username")
  665. require.Equal(t, model.UserMigrationStrategyCreateNew, bySourceUserID[10000005].ResolutionStrategy)
  666. require.Equal(t, model.UserMigrationItemStatusReady, bySourceUserID[10000005].Status)
  667. require.Contains(t, bySourceUserID[10000005].ConflictFlags, "aff_code")
  668. require.NoError(t, db.Model(&model.UserMigrationItem{}).
  669. Where("id IN ?", []int{bySourceUserID[10000003].Id, bySourceUserID[10000004].Id}).
  670. Updates(map[string]any{
  671. "resolution_strategy": model.UserMigrationStrategySkip,
  672. "status": model.UserMigrationItemStatusReady,
  673. "error_message": "",
  674. }).Error)
  675. require.NoError(t, model.RefreshUserMigrationBatchStats(batch.Id))
  676. var readyBatch model.UserMigrationBatch
  677. require.NoError(t, db.First(&readyBatch, batch.Id).Error)
  678. require.Equal(t, model.UserMigrationBatchStatusReady, readyBatch.Status)
  679. locked, err := model.TryLockBatchForExecution(batch.Id)
  680. require.NoError(t, err)
  681. require.True(t, locked)
  682. require.NoError(t, svc.ExecuteBatch(batch.Id))
  683. items, err = model.ListUserMigrationItemsByBatch(batch.Id)
  684. require.NoError(t, err)
  685. bySourceUserID = make(map[int]*model.UserMigrationItem, len(items))
  686. for _, item := range items {
  687. bySourceUserID[item.SourceUserId] = item
  688. }
  689. require.Equal(t, model.UserMigrationItemStatusMigrated, bySourceUserID[10000001].Status)
  690. require.Equal(t, model.UserMigrationItemStatusMigrated, bySourceUserID[10000002].Status)
  691. require.Equal(t, model.UserMigrationItemStatusSkipped, bySourceUserID[10000003].Status)
  692. require.Equal(t, model.UserMigrationItemStatusSkipped, bySourceUserID[10000004].Status)
  693. require.Equal(t, model.UserMigrationItemStatusMigrated, bySourceUserID[10000005].Status)
  694. var createdUser model.User
  695. require.NoError(t, db.Where("username = ?", "ov-create").First(&createdUser).Error)
  696. require.Equal(t, 100, createdUser.Quota)
  697. var mergedUser model.User
  698. require.NoError(t, db.First(&mergedUser, 1).Error)
  699. require.Equal(t, 700, mergedUser.Quota)
  700. var softUser model.User
  701. require.NoError(t, db.Where("username = ?", "ov-soft").First(&softUser).Error)
  702. require.Equal(t, 150, softUser.Quota)
  703. require.NotEqual(t, "SOFT1", softUser.AffCode)
  704. require.Equal(t, common.UserSourceSynced, client.getUserByID[10000001].Source)
  705. require.Equal(t, createdUser.Id, client.getUserByID[10000001].RemoteUserId)
  706. require.Equal(t, 100, client.getUserByID[10000001].SyncedQuota)
  707. require.Equal(t, common.UserSourceSynced, client.getUserByID[10000002].Source)
  708. require.Equal(t, 1, client.getUserByID[10000002].RemoteUserId)
  709. require.Equal(t, 700, client.getUserByID[10000002].SyncedQuota)
  710. require.NotEqual(t, common.UserSourceSynced, client.getUserByID[10000003].Source)
  711. require.NotEqual(t, common.UserSourceSynced, client.getUserByID[10000004].Source)
  712. require.Equal(t, common.UserSourceSynced, client.getUserByID[10000005].Source)
  713. require.Equal(t, softUser.Id, client.getUserByID[10000005].RemoteUserId)
  714. require.Equal(t, 150, client.getUserByID[10000005].SyncedQuota)
  715. for _, sourceUserID := range []int{10000001, 10000002, 10000005} {
  716. result, verifyErr := svc.VerifyItem(bySourceUserID[sourceUserID])
  717. require.NoError(t, verifyErr)
  718. require.True(t, result.TargetUserExists)
  719. require.True(t, result.RemoteConverted)
  720. require.True(t, result.QuotaMatched)
  721. }
  722. require.NoError(t, db.Model(&model.UserMigrationBatch{}).Where("id = ?", batch.Id).
  723. Update("status", model.UserMigrationBatchStatusReady).Error)
  724. require.NoError(t, db.Model(&model.UserMigrationItem{}).
  725. Where("id IN ?", []int{
  726. bySourceUserID[10000001].Id,
  727. bySourceUserID[10000002].Id,
  728. bySourceUserID[10000005].Id,
  729. }).
  730. Update("status", model.UserMigrationItemStatusReady).Error)
  731. locked, err = model.TryLockBatchForExecution(batch.Id)
  732. require.NoError(t, err)
  733. require.True(t, locked)
  734. require.NoError(t, svc.ExecuteBatch(batch.Id))
  735. require.Len(t, client.convertCalls, 3)
  736. require.NoError(t, db.First(&mergedUser, 1).Error)
  737. require.Equal(t, 700, mergedUser.Quota)
  738. }
  739. func TestExecuteBatch_CreateNew_CreatesUserAndConvertsRemote(t *testing.T) {
  740. db := setupMigrationServiceDB(t)
  741. orig := model.DB
  742. model.DB = db
  743. defer func() {
  744. model.DB = orig
  745. }()
  746. batch := &model.UserMigrationBatch{
  747. Name: "wave-exec",
  748. Status: model.UserMigrationBatchStatusReady,
  749. }
  750. batch.SourceRegion = "ov"
  751. batch.TargetRegion = "cn"
  752. batch.OperatorId = 1
  753. require.NoError(t, model.CreateUserMigrationBatch(batch))
  754. item := &model.UserMigrationItem{
  755. BatchId: batch.Id,
  756. SourceUserId: 10000001,
  757. SourceUsername: "ov-alice",
  758. SourceEmail: "alice@example.com",
  759. SourceQuota: 300,
  760. ResolutionStrategy: model.UserMigrationStrategyCreateNew,
  761. Status: model.UserMigrationItemStatusReady,
  762. MigrationToken: "batch-1-user-10000001",
  763. }
  764. require.NoError(t, model.UpsertUserMigrationItem(item))
  765. client := &fakeClient{
  766. getUserResp: &RemoteUserSnapshot{
  767. Id: 10000001,
  768. Username: "ov-alice",
  769. Password: "hashed-password",
  770. Email: "alice@example.com",
  771. DisplayName: "Alice",
  772. Status: 1,
  773. Role: 1,
  774. Group: "default",
  775. Quota: 300,
  776. AffCode: "OVA1",
  777. },
  778. }
  779. svc := &Service{client: client}
  780. locked, err := model.TryLockBatchForExecution(batch.Id)
  781. require.NoError(t, err)
  782. require.True(t, locked)
  783. require.NoError(t, svc.ExecuteBatch(batch.Id))
  784. var cnUser model.User
  785. require.NoError(t, db.Where("username = ?", "ov-alice").First(&cnUser).Error)
  786. require.Equal(t, 300, cnUser.Quota)
  787. require.Less(t, cnUser.Id, 10000000)
  788. require.Len(t, client.convertCalls, 1)
  789. require.Equal(t, 10000001, client.convertCalls[0])
  790. }
  791. func TestExecuteBatch_CreateNew_IdempotentOnRetry(t *testing.T) {
  792. db := setupMigrationServiceDB(t)
  793. orig := model.DB
  794. model.DB = db
  795. defer func() {
  796. model.DB = orig
  797. }()
  798. batch := &model.UserMigrationBatch{
  799. Name: "wave-retry",
  800. Status: model.UserMigrationBatchStatusReady,
  801. }
  802. batch.SourceRegion = "ov"
  803. batch.TargetRegion = "cn"
  804. batch.OperatorId = 1
  805. require.NoError(t, model.CreateUserMigrationBatch(batch))
  806. item := &model.UserMigrationItem{
  807. BatchId: batch.Id,
  808. SourceUserId: 10000002,
  809. SourceUsername: "ov-carol",
  810. SourceEmail: "carol@example.com",
  811. SourceQuota: 150,
  812. ResolutionStrategy: model.UserMigrationStrategyCreateNew,
  813. Status: model.UserMigrationItemStatusReady,
  814. MigrationToken: "batch-1-user-10000002",
  815. }
  816. require.NoError(t, model.UpsertUserMigrationItem(item))
  817. snapshot := &RemoteUserSnapshot{
  818. Id: 10000002,
  819. Username: "ov-carol",
  820. Password: "hash",
  821. Email: "carol@example.com",
  822. Quota: 150,
  823. AffCode: "OVC1",
  824. }
  825. client := &fakeClient{getUserResp: snapshot}
  826. svc := &Service{client: client}
  827. locked, err := model.TryLockBatchForExecution(batch.Id)
  828. require.NoError(t, err)
  829. require.True(t, locked)
  830. require.NoError(t, svc.ExecuteBatch(batch.Id))
  831. require.NoError(t, db.Model(&model.UserMigrationBatch{}).Where("id = ?", batch.Id).
  832. Update("status", model.UserMigrationBatchStatusFailed).Error)
  833. require.NoError(t, db.Model(&model.UserMigrationItem{}).Where("batch_id = ?", batch.Id).
  834. Update("status", model.UserMigrationItemStatusReady).Error)
  835. locked, err = model.TryLockBatchForExecution(batch.Id)
  836. require.NoError(t, err)
  837. require.True(t, locked)
  838. require.NoError(t, svc.ExecuteBatch(batch.Id))
  839. var count int64
  840. require.NoError(t, db.Model(&model.User{}).Where("username = ?", "ov-carol").Count(&count).Error)
  841. require.EqualValues(t, 1, count)
  842. require.Len(t, client.convertCalls, 1)
  843. }
  844. func TestExecuteBatch_CreateNew_ExecutionTimeDriftFails(t *testing.T) {
  845. db := setupMigrationServiceDB(t)
  846. orig := model.DB
  847. model.DB = db
  848. defer func() {
  849. model.DB = orig
  850. }()
  851. require.NoError(t, db.Create(&model.User{
  852. Id: 7,
  853. Username: "cn-alice",
  854. Password: "hash",
  855. Email: "alice@example.com",
  856. AffCode: "C007",
  857. }).Error)
  858. batch := &model.UserMigrationBatch{
  859. Name: "wave-create-drift",
  860. SourceRegion: "ov",
  861. TargetRegion: "cn",
  862. OperatorId: 1,
  863. Status: model.UserMigrationBatchStatusReady,
  864. }
  865. require.NoError(t, model.CreateUserMigrationBatch(batch))
  866. item := &model.UserMigrationItem{
  867. BatchId: batch.Id,
  868. SourceUserId: 10000020,
  869. SourceUsername: "ov-drift",
  870. SourceEmail: "old@example.com",
  871. SourceQuota: 120,
  872. ResolutionStrategy: model.UserMigrationStrategyCreateNew,
  873. Status: model.UserMigrationItemStatusReady,
  874. MigrationToken: "batch-1-user-10000020",
  875. }
  876. require.NoError(t, model.UpsertUserMigrationItem(item))
  877. client := &fakeClient{
  878. getUserResp: &RemoteUserSnapshot{
  879. Id: 10000020,
  880. Username: "ov-drift",
  881. Password: "hash",
  882. Email: "alice@example.com",
  883. Quota: 120,
  884. AffCode: "OV20",
  885. },
  886. hasCopyByID: map[int]bool{7: false},
  887. }
  888. svc := &Service{client: client}
  889. locked, err := model.TryLockBatchForExecution(batch.Id)
  890. require.NoError(t, err)
  891. require.True(t, locked)
  892. require.NoError(t, svc.ExecuteBatch(batch.Id))
  893. require.Len(t, client.convertCalls, 0)
  894. var count int64
  895. require.NoError(t, db.Model(&model.User{}).Where("username = ?", "ov-drift").Count(&count).Error)
  896. require.EqualValues(t, 0, count)
  897. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  898. require.NoError(t, err)
  899. require.Len(t, items, 1)
  900. require.Equal(t, model.UserMigrationItemStatusFailed, items[0].Status)
  901. require.Contains(t, items[0].ErrorMessage, "identity drift")
  902. }
  903. func TestExecuteBatch_MergeExisting_IdempotentQuota(t *testing.T) {
  904. db := setupMigrationServiceDB(t)
  905. orig := model.DB
  906. model.DB = db
  907. defer func() {
  908. model.DB = orig
  909. }()
  910. require.NoError(t, db.Create(&model.User{
  911. Id: 1,
  912. Username: "cn-bob",
  913. Password: "hash",
  914. Email: "bob@example.com",
  915. AffCode: "CNB1",
  916. Quota: 500,
  917. }).Error)
  918. batch := &model.UserMigrationBatch{
  919. Name: "wave-merge",
  920. Status: model.UserMigrationBatchStatusReady,
  921. }
  922. batch.SourceRegion = "ov"
  923. batch.TargetRegion = "cn"
  924. batch.OperatorId = 1
  925. require.NoError(t, model.CreateUserMigrationBatch(batch))
  926. item := &model.UserMigrationItem{
  927. BatchId: batch.Id,
  928. SourceUserId: 10000002,
  929. TargetUserId: 1,
  930. SourceUsername: "ov-bob",
  931. SourceEmail: "bob@example.com",
  932. SourceQuota: 200,
  933. ResolutionStrategy: model.UserMigrationStrategyMergeExisting,
  934. Status: model.UserMigrationItemStatusReady,
  935. MigrationToken: "batch-1-user-10000002",
  936. }
  937. require.NoError(t, model.UpsertUserMigrationItem(item))
  938. client := &fakeClient{
  939. getUserResp: &RemoteUserSnapshot{
  940. Id: 10000002,
  941. Username: "ov-bob",
  942. Password: "hashed",
  943. Email: "bob@example.com",
  944. Quota: 200,
  945. },
  946. }
  947. svc := &Service{client: client}
  948. for i := 0; i < 2; i++ {
  949. require.NoError(t, db.Model(&model.UserMigrationBatch{}).Where("id = ?", batch.Id).
  950. Update("status", model.UserMigrationBatchStatusReady).Error)
  951. require.NoError(t, db.Model(&model.UserMigrationItem{}).Where("batch_id = ?", batch.Id).
  952. Update("status", model.UserMigrationItemStatusReady).Error)
  953. locked, err := model.TryLockBatchForExecution(batch.Id)
  954. require.NoError(t, err)
  955. require.True(t, locked)
  956. require.NoError(t, svc.ExecuteBatch(batch.Id))
  957. }
  958. var saved model.User
  959. require.NoError(t, db.First(&saved, 1).Error)
  960. require.Equal(t, 700, saved.Quota)
  961. require.Len(t, client.convertCalls, 1)
  962. }
  963. func TestExecuteBatch_MergeExisting_ExecutionTimeDriftFails(t *testing.T) {
  964. db := setupMigrationServiceDB(t)
  965. orig := model.DB
  966. model.DB = db
  967. defer func() {
  968. model.DB = orig
  969. }()
  970. require.NoError(t, db.Create(&model.User{
  971. Id: 1,
  972. Username: "cn-bob",
  973. Password: "hash",
  974. Email: "bob@example.com",
  975. AffCode: "CNB1",
  976. Quota: 500,
  977. }).Error)
  978. batch := &model.UserMigrationBatch{
  979. Name: "wave-merge-drift",
  980. SourceRegion: "ov",
  981. TargetRegion: "cn",
  982. OperatorId: 1,
  983. Status: model.UserMigrationBatchStatusReady,
  984. }
  985. require.NoError(t, model.CreateUserMigrationBatch(batch))
  986. item := &model.UserMigrationItem{
  987. BatchId: batch.Id,
  988. SourceUserId: 10000021,
  989. TargetUserId: 1,
  990. SourceUsername: "ov-bob",
  991. SourceEmail: "bob@example.com",
  992. SourceQuota: 200,
  993. ResolutionStrategy: model.UserMigrationStrategyMergeExisting,
  994. Status: model.UserMigrationItemStatusReady,
  995. MigrationToken: "batch-1-user-10000021",
  996. }
  997. require.NoError(t, model.UpsertUserMigrationItem(item))
  998. client := &fakeClient{
  999. getUserResp: &RemoteUserSnapshot{
  1000. Id: 10000021,
  1001. Username: "ov-bob",
  1002. Password: "hash",
  1003. Email: "changed@example.com",
  1004. Quota: 200,
  1005. AffCode: "OV21",
  1006. },
  1007. }
  1008. svc := &Service{client: client}
  1009. locked, err := model.TryLockBatchForExecution(batch.Id)
  1010. require.NoError(t, err)
  1011. require.True(t, locked)
  1012. require.NoError(t, svc.ExecuteBatch(batch.Id))
  1013. require.Len(t, client.convertCalls, 0)
  1014. var cnUser model.User
  1015. require.NoError(t, db.First(&cnUser, 1).Error)
  1016. require.Equal(t, 500, cnUser.Quota)
  1017. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  1018. require.NoError(t, err)
  1019. require.Len(t, items, 1)
  1020. require.Equal(t, model.UserMigrationItemStatusFailed, items[0].Status)
  1021. require.Contains(t, items[0].ErrorMessage, "merge target drift")
  1022. }
  1023. func TestExecuteBatch_SkipItem_DoesNotConvert(t *testing.T) {
  1024. db := setupMigrationServiceDB(t)
  1025. orig := model.DB
  1026. model.DB = db
  1027. defer func() {
  1028. model.DB = orig
  1029. }()
  1030. batch := &model.UserMigrationBatch{
  1031. Name: "skip-test",
  1032. SourceRegion: "ov",
  1033. TargetRegion: "cn",
  1034. OperatorId: 1,
  1035. Status: model.UserMigrationBatchStatusReady,
  1036. }
  1037. require.NoError(t, model.CreateUserMigrationBatch(batch))
  1038. require.NoError(t, model.UpsertUserMigrationItem(&model.UserMigrationItem{
  1039. BatchId: batch.Id,
  1040. SourceUserId: 10000099,
  1041. SourceUsername: "skip-user",
  1042. SourceQuota: 50,
  1043. ResolutionStrategy: model.UserMigrationStrategySkip,
  1044. Status: model.UserMigrationItemStatusReady,
  1045. MigrationToken: "batch-1-user-10000099",
  1046. }))
  1047. client := &fakeClient{}
  1048. svc := &Service{client: client}
  1049. locked, err := model.TryLockBatchForExecution(batch.Id)
  1050. require.NoError(t, err)
  1051. require.True(t, locked)
  1052. require.NoError(t, svc.ExecuteBatch(batch.Id))
  1053. require.Len(t, client.convertCalls, 0)
  1054. items, err := model.ListUserMigrationItemsByBatch(batch.Id)
  1055. require.NoError(t, err)
  1056. require.Len(t, items, 1)
  1057. require.Equal(t, model.UserMigrationItemStatusSkipped, items[0].Status)
  1058. }
  1059. func TestVerifyItem_CorrectlyChecksRemoteConverted(t *testing.T) {
  1060. db := setupMigrationServiceDB(t)
  1061. orig := model.DB
  1062. model.DB = db
  1063. defer func() {
  1064. model.DB = orig
  1065. }()
  1066. require.NoError(t, db.Create(&model.User{
  1067. Id: 789,
  1068. Username: "cn-migrated",
  1069. Password: "hash",
  1070. AffCode: "CM1",
  1071. Quota: 300,
  1072. }).Error)
  1073. item := &model.UserMigrationItem{
  1074. BatchId: 1,
  1075. SourceUserId: 10000001,
  1076. TargetUserId: 789,
  1077. SourceQuota: 300,
  1078. Status: model.UserMigrationItemStatusMigrated,
  1079. }
  1080. client := &fakeClient{
  1081. getUserResp: &RemoteUserSnapshot{
  1082. Id: 10000001,
  1083. Source: common.UserSourceSynced,
  1084. RemoteUserId: 789,
  1085. SyncedQuota: 300,
  1086. },
  1087. }
  1088. svc := &Service{client: client}
  1089. result, err := svc.VerifyItem(item)
  1090. require.NoError(t, err)
  1091. require.True(t, result.TargetUserExists)
  1092. require.True(t, result.RemoteConverted)
  1093. require.True(t, result.QuotaMatched)
  1094. }
  1095. func TestVerifyItem_QuotaMismatchDetected(t *testing.T) {
  1096. db := setupMigrationServiceDB(t)
  1097. orig := model.DB
  1098. model.DB = db
  1099. defer func() {
  1100. model.DB = orig
  1101. }()
  1102. require.NoError(t, db.Create(&model.User{
  1103. Id: 790,
  1104. Username: "cn-mismatch",
  1105. Password: "hash",
  1106. AffCode: "CM2",
  1107. Quota: 300,
  1108. }).Error)
  1109. item := &model.UserMigrationItem{
  1110. BatchId: 1,
  1111. SourceUserId: 10000002,
  1112. TargetUserId: 790,
  1113. SourceQuota: 300,
  1114. Status: model.UserMigrationItemStatusMigrated,
  1115. }
  1116. client := &fakeClient{
  1117. getUserResp: &RemoteUserSnapshot{
  1118. Id: 10000002,
  1119. Source: common.UserSourceSynced,
  1120. RemoteUserId: 790,
  1121. SyncedQuota: 250,
  1122. },
  1123. }
  1124. svc := &Service{client: client}
  1125. result, err := svc.VerifyItem(item)
  1126. require.NoError(t, err)
  1127. require.True(t, result.TargetUserExists)
  1128. require.True(t, result.RemoteConverted)
  1129. require.False(t, result.QuotaMatched)
  1130. }
  1131. func TestVerifyItem_AllowsPostMigrationQuotaConsumption(t *testing.T) {
  1132. db := setupMigrationServiceDB(t)
  1133. orig := model.DB
  1134. model.DB = db
  1135. defer func() {
  1136. model.DB = orig
  1137. }()
  1138. require.NoError(t, db.Create(&model.User{
  1139. Id: 791,
  1140. Username: "cn-consumed",
  1141. Password: "hash",
  1142. AffCode: "CM3",
  1143. Quota: 250,
  1144. }).Error)
  1145. item := &model.UserMigrationItem{
  1146. BatchId: 1,
  1147. SourceUserId: 10000003,
  1148. TargetUserId: 791,
  1149. SourceQuota: 200,
  1150. Status: model.UserMigrationItemStatusMigrated,
  1151. }
  1152. client := &fakeClient{
  1153. getUserResp: &RemoteUserSnapshot{
  1154. Id: 10000003,
  1155. Source: common.UserSourceSynced,
  1156. RemoteUserId: 791,
  1157. SyncedQuota: 200,
  1158. },
  1159. }
  1160. svc := &Service{client: client}
  1161. result, err := svc.VerifyItem(item)
  1162. require.NoError(t, err)
  1163. require.True(t, result.TargetUserExists)
  1164. require.True(t, result.RemoteConverted)
  1165. require.True(t, result.QuotaMatched)
  1166. }
  1167. func TestExecuteBatch_CreateNewFastPathRevalidatesAndMarksMigrated(t *testing.T) {
  1168. db := setupMigrationServiceDB(t)
  1169. orig := model.DB
  1170. model.DB = db
  1171. defer func() {
  1172. model.DB = orig
  1173. }()
  1174. require.NoError(t, db.Create(&model.User{
  1175. Id: 900,
  1176. Username: "cn-existing",
  1177. Password: "hash",
  1178. AffCode: "CN900",
  1179. Quota: 300,
  1180. }).Error)
  1181. require.NoError(t, db.Create(&model.UserMigrationItem{
  1182. Id: 1,
  1183. BatchId: 10,
  1184. SourceUserId: 10000010,
  1185. TargetUserId: 900,
  1186. SourceUsername: "ov-existing",
  1187. SourceQuota: 300,
  1188. ResolutionStrategy: model.UserMigrationStrategyCreateNew,
  1189. Status: model.UserMigrationItemStatusFailed,
  1190. MigrationToken: "token-fast-path",
  1191. }).Error)
  1192. require.NoError(t, db.Create(&model.UserMigrationBatch{
  1193. Id: 10,
  1194. Name: "batch-fast-path",
  1195. SourceRegion: "ov",
  1196. TargetRegion: "cn",
  1197. Status: model.UserMigrationBatchStatusFailed,
  1198. OperatorId: 1,
  1199. }).Error)
  1200. client := &fakeClient{
  1201. getUserByID: map[int]*RemoteUserSnapshot{
  1202. 10000010: {
  1203. Id: 10000010,
  1204. Username: "ov-existing",
  1205. Quota: 300,
  1206. Source: common.UserSourceLocal,
  1207. OAuthBindings: nil,
  1208. },
  1209. },
  1210. }
  1211. svc := &Service{client: client}
  1212. require.NoError(t, svc.ExecuteBatch(10))
  1213. require.Len(t, client.convertCalls, 1)
  1214. require.Equal(t, 10000010, client.convertCalls[0])
  1215. items, err := model.ListUserMigrationItemsByBatch(10)
  1216. require.NoError(t, err)
  1217. require.Len(t, items, 1)
  1218. require.Equal(t, model.UserMigrationItemStatusMigrated, items[0].Status)
  1219. }
  1220. func TestExecuteBatch_CreateNewFastPath_DriftFails(t *testing.T) {
  1221. db := setupMigrationServiceDB(t)
  1222. orig := model.DB
  1223. model.DB = db
  1224. defer func() {
  1225. model.DB = orig
  1226. }()
  1227. require.NoError(t, db.Create(&model.User{
  1228. Id: 901,
  1229. Username: "cn-existing-drift",
  1230. Password: "hash",
  1231. AffCode: "CND901",
  1232. Quota: 320,
  1233. }).Error)
  1234. require.NoError(t, db.Create(&model.UserMigrationItem{
  1235. Id: 2,
  1236. BatchId: 11,
  1237. SourceUserId: 10000018,
  1238. TargetUserId: 901,
  1239. SourceUsername: "ov-fast-drift",
  1240. SourceQuota: 320,
  1241. ResolutionStrategy: model.UserMigrationStrategyCreateNew,
  1242. Status: model.UserMigrationItemStatusFailed,
  1243. MigrationToken: "token-fast-path-drift",
  1244. }).Error)
  1245. require.NoError(t, db.Create(&model.UserMigrationBatch{
  1246. Id: 11,
  1247. Name: "batch-fast-path-drift",
  1248. SourceRegion: "ov",
  1249. TargetRegion: "cn",
  1250. Status: model.UserMigrationBatchStatusFailed,
  1251. OperatorId: 1,
  1252. }).Error)
  1253. client := &fakeClient{
  1254. getUserByID: map[int]*RemoteUserSnapshot{
  1255. 10000018: {
  1256. Id: 10000018,
  1257. Username: "ov-fast-drift",
  1258. Quota: 320,
  1259. Source: common.UserSourceSynced,
  1260. RemoteUserId: 999,
  1261. SyncedQuota: 320,
  1262. },
  1263. },
  1264. }
  1265. svc := &Service{client: client}
  1266. require.NoError(t, svc.ExecuteBatch(11))
  1267. require.Empty(t, client.convertCalls)
  1268. items, err := model.ListUserMigrationItemsByBatch(11)
  1269. require.NoError(t, err)
  1270. require.Len(t, items, 1)
  1271. require.Equal(t, model.UserMigrationItemStatusFailed, items[0].Status)
  1272. require.Contains(t, items[0].ErrorMessage, "already synced")
  1273. }
  1274. func TestExecuteBatch_CreateNewFastPath_AlreadySyncedToSameTargetSucceeds(t *testing.T) {
  1275. db := setupMigrationServiceDB(t)
  1276. orig := model.DB
  1277. model.DB = db
  1278. defer func() {
  1279. model.DB = orig
  1280. }()
  1281. require.NoError(t, db.Create(&model.User{
  1282. Id: 902,
  1283. Username: "cn-existing-same",
  1284. Password: "hash",
  1285. AffCode: "CND902",
  1286. Quota: 321,
  1287. }).Error)
  1288. require.NoError(t, db.Create(&model.UserMigrationItem{
  1289. Id: 3,
  1290. BatchId: 12,
  1291. SourceUserId: 10000019,
  1292. TargetUserId: 902,
  1293. SourceUsername: "ov-fast-same",
  1294. SourceQuota: 321,
  1295. ResolutionStrategy: model.UserMigrationStrategyCreateNew,
  1296. Status: model.UserMigrationItemStatusFailed,
  1297. MigrationToken: "token-fast-path-same",
  1298. }).Error)
  1299. require.NoError(t, db.Create(&model.UserMigrationBatch{
  1300. Id: 12,
  1301. Name: "batch-fast-path-same",
  1302. SourceRegion: "ov",
  1303. TargetRegion: "cn",
  1304. Status: model.UserMigrationBatchStatusFailed,
  1305. OperatorId: 1,
  1306. }).Error)
  1307. client := &fakeClient{
  1308. getUserByID: map[int]*RemoteUserSnapshot{
  1309. 10000019: {
  1310. Id: 10000019,
  1311. Username: "ov-fast-same",
  1312. Quota: 321,
  1313. Source: common.UserSourceSynced,
  1314. RemoteUserId: 902,
  1315. SyncedQuota: 321,
  1316. },
  1317. },
  1318. }
  1319. svc := &Service{client: client}
  1320. require.NoError(t, svc.ExecuteBatch(12))
  1321. require.Empty(t, client.convertCalls)
  1322. items, err := model.ListUserMigrationItemsByBatch(12)
  1323. require.NoError(t, err)
  1324. require.Len(t, items, 1)
  1325. require.Equal(t, model.UserMigrationItemStatusMigrated, items[0].Status)
  1326. }