Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 

840 Zeilen
24 KiB

  1. package controller
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "errors"
  6. "net/http"
  7. "net/http/httptest"
  8. "testing"
  9. "github.com/QuantumNous/new-api/model"
  10. user_migration "github.com/QuantumNous/new-api/service/user_migration"
  11. "github.com/gin-gonic/gin"
  12. "github.com/glebarez/sqlite"
  13. "github.com/stretchr/testify/require"
  14. "gorm.io/gorm"
  15. )
  16. func setupUserMigrationControllerDB(t *testing.T) *gorm.DB {
  17. t.Helper()
  18. db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
  19. require.NoError(t, err)
  20. sqlDB, err := db.DB()
  21. require.NoError(t, err)
  22. sqlDB.SetMaxOpenConns(1)
  23. require.NoError(t, db.AutoMigrate(
  24. &model.User{},
  25. &model.CustomOAuthProvider{},
  26. &model.UserOAuthBinding{},
  27. &model.UserMigrationBatch{},
  28. &model.UserMigrationItem{},
  29. &model.MigrationQuotaGrant{},
  30. ))
  31. return db
  32. }
  33. func testRootAuthMiddleware() gin.HandlerFunc {
  34. return func(c *gin.Context) {
  35. c.Set("id", 1)
  36. c.Set("role", 100)
  37. c.Next()
  38. }
  39. }
  40. type fakeMigrationCandidateClient struct {
  41. resp *user_migration.ListRemoteUsersResponse
  42. err error
  43. gotKeyword string
  44. gotPage int
  45. gotPageSize int
  46. }
  47. func (f *fakeMigrationCandidateClient) ListUsers(page, pageSize int, keyword string) (*user_migration.ListRemoteUsersResponse, error) {
  48. f.gotKeyword = keyword
  49. f.gotPage = page
  50. f.gotPageSize = pageSize
  51. if f.err != nil {
  52. return nil, f.err
  53. }
  54. return f.resp, nil
  55. }
  56. type fakeMigrationVerifier struct {
  57. results map[int]*user_migration.VerifyResult
  58. errs map[int]error
  59. }
  60. func (f *fakeMigrationVerifier) VerifyItem(item *model.UserMigrationItem) (*user_migration.VerifyResult, error) {
  61. if err, ok := f.errs[item.SourceUserId]; ok {
  62. return nil, err
  63. }
  64. if result, ok := f.results[item.SourceUserId]; ok {
  65. return result, nil
  66. }
  67. return nil, errors.New("unexpected source user")
  68. }
  69. func TestCreateUserMigrationBatch_Success(t *testing.T) {
  70. gin.SetMode(gin.TestMode)
  71. router := gin.New()
  72. router.Use(testRootAuthMiddleware())
  73. router.POST("/api/user-migrations/batches", CreateUserMigrationBatch)
  74. db := setupUserMigrationControllerDB(t)
  75. orig := model.DB
  76. model.DB = db
  77. defer func() {
  78. model.DB = orig
  79. }()
  80. body := map[string]any{
  81. "name": "wave-root-1",
  82. "source_region": "overseas",
  83. "target_region": "cn",
  84. }
  85. bodyBytes, err := json.Marshal(body)
  86. require.NoError(t, err)
  87. req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches", bytes.NewBuffer(bodyBytes))
  88. req.Header.Set("Content-Type", "application/json")
  89. w := httptest.NewRecorder()
  90. router.ServeHTTP(w, req)
  91. require.Equal(t, http.StatusOK, w.Code)
  92. var resp map[string]any
  93. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  94. require.True(t, resp["success"].(bool))
  95. require.NotNil(t, resp["data"])
  96. }
  97. func TestCreateUserMigrationBatch_ExplicitIDsSuccess(t *testing.T) {
  98. gin.SetMode(gin.TestMode)
  99. router := gin.New()
  100. router.Use(testRootAuthMiddleware())
  101. router.POST("/api/user-migrations/batches", CreateUserMigrationBatch)
  102. db := setupUserMigrationControllerDB(t)
  103. orig := model.DB
  104. model.DB = db
  105. defer func() {
  106. model.DB = orig
  107. }()
  108. req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches", bytes.NewBufferString(`{
  109. "name":"explicit-batch",
  110. "source_region":"ov",
  111. "target_region":"cn",
  112. "selection_mode":"explicit_ids",
  113. "source_user_ids":[1001,1002,1002]
  114. }`))
  115. req.Header.Set("Content-Type", "application/json")
  116. w := httptest.NewRecorder()
  117. router.ServeHTTP(w, req)
  118. require.Equal(t, http.StatusOK, w.Code)
  119. var batch model.UserMigrationBatch
  120. require.NoError(t, db.First(&batch, 1).Error)
  121. require.Equal(t, "explicit_ids", batch.SelectionMode)
  122. require.Equal(t, 2, batch.RequestedUserCount)
  123. }
  124. func TestCreateUserMigrationBatch_ExplicitIDsValidation(t *testing.T) {
  125. gin.SetMode(gin.TestMode)
  126. router := gin.New()
  127. router.Use(testRootAuthMiddleware())
  128. router.POST("/api/user-migrations/batches", CreateUserMigrationBatch)
  129. cases := []string{
  130. `{"name":"b1","source_region":"ov","target_region":"cn","selection_mode":"explicit_ids","source_user_ids":[]}`,
  131. `{"name":"b2","source_region":"ov","target_region":"cn","selection_mode":"explicit_ids","source_user_ids":[0]}`,
  132. `{"name":"b3","source_region":"ov","target_region":"cn","selection_mode":"invalid","source_user_ids":[1]}`,
  133. }
  134. db := setupUserMigrationControllerDB(t)
  135. orig := model.DB
  136. model.DB = db
  137. defer func() {
  138. model.DB = orig
  139. }()
  140. for _, body := range cases {
  141. req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches", bytes.NewBufferString(body))
  142. req.Header.Set("Content-Type", "application/json")
  143. w := httptest.NewRecorder()
  144. router.ServeHTTP(w, req)
  145. require.Equal(t, http.StatusBadRequest, w.Code)
  146. }
  147. }
  148. func TestCreateUserMigrationBatch_ExplicitIDs_RejectsActiveBatchOverlap(t *testing.T) {
  149. gin.SetMode(gin.TestMode)
  150. router := gin.New()
  151. router.Use(testRootAuthMiddleware())
  152. router.POST("/api/user-migrations/batches", CreateUserMigrationBatch)
  153. db := setupUserMigrationControllerDB(t)
  154. orig := model.DB
  155. model.DB = db
  156. defer func() {
  157. model.DB = orig
  158. }()
  159. existingBatch := &model.UserMigrationBatch{
  160. Name: "existing-draft-batch",
  161. SourceRegion: "ov",
  162. TargetRegion: "cn",
  163. SelectionMode: "explicit_ids",
  164. SelectionPayload: `{"source_user_ids":[1002]}`,
  165. Status: model.UserMigrationBatchStatusDraft,
  166. OperatorId: 1,
  167. }
  168. require.NoError(t, db.Create(existingBatch).Error)
  169. req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches", bytes.NewBufferString(`{
  170. "name":"explicit-batch",
  171. "source_region":"ov",
  172. "target_region":"cn",
  173. "selection_mode":"explicit_ids",
  174. "source_user_ids":[1001,1002]
  175. }`))
  176. req.Header.Set("Content-Type", "application/json")
  177. w := httptest.NewRecorder()
  178. router.ServeHTTP(w, req)
  179. require.Equal(t, http.StatusConflict, w.Code)
  180. var resp struct {
  181. Success bool `json:"success"`
  182. Error string `json:"error"`
  183. Conflicts []struct {
  184. SourceUserID int `json:"source_user_id"`
  185. BatchID int `json:"batch_id"`
  186. BatchStatus string `json:"batch_status"`
  187. } `json:"conflicts"`
  188. }
  189. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  190. require.False(t, resp.Success)
  191. require.Equal(t, "some selected users already exist in active migration batches", resp.Error)
  192. require.Len(t, resp.Conflicts, 1)
  193. require.Equal(t, 1002, resp.Conflicts[0].SourceUserID)
  194. require.Equal(t, existingBatch.Id, resp.Conflicts[0].BatchID)
  195. require.Equal(t, model.UserMigrationBatchStatusDraft, resp.Conflicts[0].BatchStatus)
  196. }
  197. func TestListMigrationCandidateUsers_Success(t *testing.T) {
  198. gin.SetMode(gin.TestMode)
  199. router := gin.New()
  200. router.Use(testRootAuthMiddleware())
  201. origFactory := newMigrationCandidateClient
  202. defer func() {
  203. newMigrationCandidateClient = origFactory
  204. }()
  205. newMigrationCandidateClient = func() migrationCandidateClient {
  206. return &fakeMigrationCandidateClient{
  207. resp: &user_migration.ListRemoteUsersResponse{
  208. Success: true,
  209. Data: []user_migration.RemoteUserSnapshot{
  210. {Id: 10000001, Username: "ov-a", Email: "a@example.com", Quota: 100},
  211. },
  212. Total: 1,
  213. },
  214. }
  215. }
  216. router.GET("/api/user-migrations/candidate-users", ListMigrationCandidateUsers)
  217. req := httptest.NewRequest(http.MethodGet, "/api/user-migrations/candidate-users?page=0&page_size=50", nil)
  218. w := httptest.NewRecorder()
  219. router.ServeHTTP(w, req)
  220. require.Equal(t, http.StatusOK, w.Code)
  221. var resp map[string]any
  222. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  223. require.True(t, resp["success"].(bool))
  224. require.EqualValues(t, 1, resp["total"])
  225. }
  226. func TestListMigrationCandidateUsers_UpstreamFailure(t *testing.T) {
  227. gin.SetMode(gin.TestMode)
  228. router := gin.New()
  229. router.Use(testRootAuthMiddleware())
  230. origFactory := newMigrationCandidateClient
  231. defer func() {
  232. newMigrationCandidateClient = origFactory
  233. }()
  234. newMigrationCandidateClient = func() migrationCandidateClient {
  235. return &fakeMigrationCandidateClient{err: errors.New("upstream unavailable")}
  236. }
  237. router.GET("/api/user-migrations/candidate-users", ListMigrationCandidateUsers)
  238. req := httptest.NewRequest(http.MethodGet, "/api/user-migrations/candidate-users?page=0&page_size=50", nil)
  239. w := httptest.NewRecorder()
  240. router.ServeHTTP(w, req)
  241. require.Equal(t, http.StatusInternalServerError, w.Code)
  242. }
  243. func TestListMigrationCandidateUsers_ForwardsKeyword(t *testing.T) {
  244. gin.SetMode(gin.TestMode)
  245. router := gin.New()
  246. router.Use(testRootAuthMiddleware())
  247. client := &fakeMigrationCandidateClient{
  248. resp: &user_migration.ListRemoteUsersResponse{
  249. Success: true,
  250. Data: []user_migration.RemoteUserSnapshot{},
  251. Total: 0,
  252. },
  253. }
  254. origFactory := newMigrationCandidateClient
  255. defer func() {
  256. newMigrationCandidateClient = origFactory
  257. }()
  258. newMigrationCandidateClient = func() migrationCandidateClient {
  259. return client
  260. }
  261. router.GET("/api/user-migrations/candidate-users", ListMigrationCandidateUsers)
  262. req := httptest.NewRequest(http.MethodGet, "/api/user-migrations/candidate-users?page=2&page_size=30&keyword=alice", nil)
  263. w := httptest.NewRecorder()
  264. router.ServeHTTP(w, req)
  265. require.Equal(t, http.StatusOK, w.Code)
  266. require.Equal(t, 2, client.gotPage)
  267. require.Equal(t, 30, client.gotPageSize)
  268. require.Equal(t, "alice", client.gotKeyword)
  269. }
  270. func TestListUserMigrationBatches_Success(t *testing.T) {
  271. gin.SetMode(gin.TestMode)
  272. router := gin.New()
  273. router.Use(testRootAuthMiddleware())
  274. router.GET("/api/user-migrations/batches", ListUserMigrationBatches)
  275. db := setupUserMigrationControllerDB(t)
  276. orig := model.DB
  277. model.DB = db
  278. defer func() {
  279. model.DB = orig
  280. }()
  281. require.NoError(t, db.Create(&model.UserMigrationBatch{
  282. Id: 1,
  283. Name: "batch-1",
  284. SourceRegion: "ov",
  285. TargetRegion: "cn",
  286. OperatorId: 1,
  287. }).Error)
  288. require.NoError(t, db.Create(&model.UserMigrationBatch{
  289. Id: 2,
  290. Name: "batch-2",
  291. SourceRegion: "ov",
  292. TargetRegion: "cn",
  293. OperatorId: 1,
  294. }).Error)
  295. req := httptest.NewRequest(http.MethodGet, "/api/user-migrations/batches", nil)
  296. w := httptest.NewRecorder()
  297. router.ServeHTTP(w, req)
  298. require.Equal(t, http.StatusOK, w.Code)
  299. var resp map[string]any
  300. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  301. require.True(t, resp["success"].(bool))
  302. require.Len(t, resp["data"], 2)
  303. }
  304. func TestGetUserMigrationBatch_Success(t *testing.T) {
  305. gin.SetMode(gin.TestMode)
  306. router := gin.New()
  307. router.Use(testRootAuthMiddleware())
  308. router.GET("/api/user-migrations/batches/:id", GetUserMigrationBatch)
  309. db := setupUserMigrationControllerDB(t)
  310. orig := model.DB
  311. model.DB = db
  312. defer func() {
  313. model.DB = orig
  314. }()
  315. require.NoError(t, db.Create(&model.UserMigrationBatch{
  316. Id: 1,
  317. Name: "detail-batch",
  318. SourceRegion: "ov",
  319. TargetRegion: "cn",
  320. OperatorId: 1,
  321. }).Error)
  322. require.NoError(t, db.Create(&model.UserMigrationItem{
  323. BatchId: 1,
  324. SourceUserId: 1001,
  325. SourceUsername: "ov-u1",
  326. Status: model.UserMigrationItemStatusReady,
  327. }).Error)
  328. req := httptest.NewRequest(http.MethodGet, "/api/user-migrations/batches/1?page=0&page_size=50", nil)
  329. w := httptest.NewRecorder()
  330. router.ServeHTTP(w, req)
  331. require.Equal(t, http.StatusOK, w.Code)
  332. var resp map[string]any
  333. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  334. require.True(t, resp["success"].(bool))
  335. require.NotNil(t, resp["data"])
  336. require.Len(t, resp["items"], 1)
  337. require.EqualValues(t, 1, resp["total"])
  338. }
  339. func TestGetUserMigrationBatch_NotFound(t *testing.T) {
  340. gin.SetMode(gin.TestMode)
  341. router := gin.New()
  342. router.Use(testRootAuthMiddleware())
  343. router.GET("/api/user-migrations/batches/:id", GetUserMigrationBatch)
  344. db := setupUserMigrationControllerDB(t)
  345. orig := model.DB
  346. model.DB = db
  347. defer func() {
  348. model.DB = orig
  349. }()
  350. req := httptest.NewRequest(http.MethodGet, "/api/user-migrations/batches/999", nil)
  351. w := httptest.NewRecorder()
  352. router.ServeHTTP(w, req)
  353. require.Equal(t, http.StatusNotFound, w.Code)
  354. }
  355. func TestExecuteUserMigrationBatch_AlreadyRunning(t *testing.T) {
  356. gin.SetMode(gin.TestMode)
  357. router := gin.New()
  358. router.Use(testRootAuthMiddleware())
  359. router.POST("/api/user-migrations/batches/:id/execute", ExecuteUserMigrationBatch)
  360. db := setupUserMigrationControllerDB(t)
  361. orig := model.DB
  362. model.DB = db
  363. defer func() {
  364. model.DB = orig
  365. }()
  366. batch := &model.UserMigrationBatch{
  367. Name: "b",
  368. SourceRegion: "ov",
  369. TargetRegion: "cn",
  370. OperatorId: 1,
  371. Status: model.UserMigrationBatchStatusRunning,
  372. }
  373. require.NoError(t, db.Create(batch).Error)
  374. req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches/1/execute", nil)
  375. w := httptest.NewRecorder()
  376. router.ServeHTTP(w, req)
  377. require.Equal(t, http.StatusConflict, w.Code)
  378. }
  379. func TestExecuteUserMigrationBatch_InvalidStateRejected(t *testing.T) {
  380. gin.SetMode(gin.TestMode)
  381. router := gin.New()
  382. router.Use(testRootAuthMiddleware())
  383. router.POST("/api/user-migrations/batches/:id/execute", ExecuteUserMigrationBatch)
  384. db := setupUserMigrationControllerDB(t)
  385. orig := model.DB
  386. model.DB = db
  387. defer func() {
  388. model.DB = orig
  389. }()
  390. for _, status := range []string{model.UserMigrationBatchStatusDraft, model.UserMigrationBatchStatusScanned} {
  391. require.NoError(t, db.Exec("DELETE FROM user_migration_batches").Error)
  392. require.NoError(t, db.Create(&model.UserMigrationBatch{
  393. Id: 1,
  394. Name: "invalid-exec",
  395. SourceRegion: "ov",
  396. TargetRegion: "cn",
  397. OperatorId: 1,
  398. Status: status,
  399. }).Error)
  400. req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches/1/execute", nil)
  401. w := httptest.NewRecorder()
  402. router.ServeHTTP(w, req)
  403. require.Equal(t, http.StatusConflict, w.Code)
  404. }
  405. }
  406. func TestResolveUserMigrationItem_InvalidStrategy(t *testing.T) {
  407. gin.SetMode(gin.TestMode)
  408. router := gin.New()
  409. router.Use(testRootAuthMiddleware())
  410. router.POST("/api/user-migrations/items/:id/resolve", ResolveUserMigrationItem)
  411. db := setupUserMigrationControllerDB(t)
  412. orig := model.DB
  413. model.DB = db
  414. defer func() {
  415. model.DB = orig
  416. }()
  417. require.NoError(t, db.Create(&model.UserMigrationItem{
  418. BatchId: 1,
  419. SourceUserId: 10000001,
  420. SourceUsername: "u",
  421. Status: model.UserMigrationItemStatusConflict,
  422. }).Error)
  423. req := httptest.NewRequest(
  424. http.MethodPost,
  425. "/api/user-migrations/items/1/resolve",
  426. bytes.NewBufferString(`{"resolution_strategy":"invalid_strategy"}`),
  427. )
  428. req.Header.Set("Content-Type", "application/json")
  429. w := httptest.NewRecorder()
  430. router.ServeHTTP(w, req)
  431. require.Equal(t, http.StatusBadRequest, w.Code)
  432. }
  433. func TestResolveUserMigrationItem_RefreshesBatchStatusToReady(t *testing.T) {
  434. gin.SetMode(gin.TestMode)
  435. router := gin.New()
  436. router.Use(testRootAuthMiddleware())
  437. router.POST("/api/user-migrations/items/:id/resolve", ResolveUserMigrationItem)
  438. db := setupUserMigrationControllerDB(t)
  439. orig := model.DB
  440. model.DB = db
  441. defer func() {
  442. model.DB = orig
  443. }()
  444. batch := &model.UserMigrationBatch{
  445. Name: "resolve-ready",
  446. SourceRegion: "ov",
  447. TargetRegion: "cn",
  448. OperatorId: 1,
  449. Status: model.UserMigrationBatchStatusScanned,
  450. }
  451. require.NoError(t, model.CreateUserMigrationBatch(batch))
  452. require.NoError(t, db.Create(&model.UserMigrationItem{
  453. BatchId: batch.Id,
  454. SourceUserId: 1001,
  455. SourceUsername: "ov-u1",
  456. Status: model.UserMigrationItemStatusConflict,
  457. }).Error)
  458. req := httptest.NewRequest(
  459. http.MethodPost,
  460. "/api/user-migrations/items/1/resolve",
  461. bytes.NewBufferString(`{"resolution_strategy":"create_new"}`),
  462. )
  463. req.Header.Set("Content-Type", "application/json")
  464. w := httptest.NewRecorder()
  465. router.ServeHTTP(w, req)
  466. require.Equal(t, http.StatusOK, w.Code)
  467. var refreshed model.UserMigrationBatch
  468. require.NoError(t, db.First(&refreshed, batch.Id).Error)
  469. require.Equal(t, model.UserMigrationBatchStatusReady, refreshed.Status)
  470. }
  471. func TestResolveUserMigrationItem_RejectsMergeWhenCNAlreadyHasSyncedCopy(t *testing.T) {
  472. gin.SetMode(gin.TestMode)
  473. router := gin.New()
  474. router.Use(testRootAuthMiddleware())
  475. router.POST("/api/user-migrations/items/:id/resolve", ResolveUserMigrationItem)
  476. db := setupUserMigrationControllerDB(t)
  477. orig := model.DB
  478. model.DB = db
  479. defer func() {
  480. model.DB = orig
  481. }()
  482. require.NoError(t, db.Create(&model.UserMigrationItem{
  483. Id: 1,
  484. BatchId: 1,
  485. SourceUserId: 1001,
  486. SourceUsername: "ov-u1",
  487. Status: model.UserMigrationItemStatusConflict,
  488. ConflictFlags: `["email","cn_already_synced_to_ov"]`,
  489. TargetUserId: 24,
  490. }).Error)
  491. req := httptest.NewRequest(
  492. http.MethodPost,
  493. "/api/user-migrations/items/1/resolve",
  494. bytes.NewBufferString(`{"resolution_strategy":"merge_into_existing","target_user_id":24}`),
  495. )
  496. req.Header.Set("Content-Type", "application/json")
  497. w := httptest.NewRecorder()
  498. router.ServeHTTP(w, req)
  499. require.Equal(t, http.StatusConflict, w.Code)
  500. var item model.UserMigrationItem
  501. require.NoError(t, db.First(&item, 1).Error)
  502. require.Equal(t, model.UserMigrationItemStatusConflict, item.Status)
  503. require.Equal(t, 24, item.TargetUserId)
  504. }
  505. func TestRetryUserMigrationBatch_OnlyFailedItemsReset(t *testing.T) {
  506. gin.SetMode(gin.TestMode)
  507. router := gin.New()
  508. router.Use(testRootAuthMiddleware())
  509. router.POST("/api/user-migrations/batches/:id/retry", RetryUserMigrationBatch)
  510. db := setupUserMigrationControllerDB(t)
  511. orig := model.DB
  512. model.DB = db
  513. defer func() {
  514. model.DB = orig
  515. }()
  516. require.NoError(t, db.Create(&model.UserMigrationBatch{
  517. Id: 1,
  518. Name: "retry-batch",
  519. SourceRegion: "ov",
  520. TargetRegion: "cn",
  521. OperatorId: 1,
  522. Status: model.UserMigrationBatchStatusRunning,
  523. }).Error)
  524. require.NoError(t, db.Create(&model.UserMigrationItem{
  525. Id: 1,
  526. BatchId: 1,
  527. SourceUserId: 1001,
  528. SourceUsername: "failed-user",
  529. Status: model.UserMigrationItemStatusFailed,
  530. ErrorMessage: "boom",
  531. }).Error)
  532. require.NoError(t, db.Create(&model.UserMigrationItem{
  533. Id: 2,
  534. BatchId: 1,
  535. SourceUserId: 1002,
  536. SourceUsername: "migrated-user",
  537. Status: model.UserMigrationItemStatusMigrated,
  538. }).Error)
  539. require.NoError(t, db.Create(&model.UserMigrationItem{
  540. Id: 3,
  541. BatchId: 1,
  542. SourceUserId: 1003,
  543. SourceUsername: "skipped-user",
  544. Status: model.UserMigrationItemStatusSkipped,
  545. }).Error)
  546. req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches/1/retry", nil)
  547. w := httptest.NewRecorder()
  548. router.ServeHTTP(w, req)
  549. require.Equal(t, http.StatusConflict, w.Code)
  550. var failedItem, migratedItem, skippedItem model.UserMigrationItem
  551. require.NoError(t, db.First(&failedItem, 1).Error)
  552. require.NoError(t, db.First(&migratedItem, 2).Error)
  553. require.NoError(t, db.First(&skippedItem, 3).Error)
  554. require.Equal(t, model.UserMigrationItemStatusReady, failedItem.Status)
  555. require.Empty(t, failedItem.ErrorMessage)
  556. require.Equal(t, model.UserMigrationItemStatusMigrated, migratedItem.Status)
  557. require.Equal(t, model.UserMigrationItemStatusSkipped, skippedItem.Status)
  558. }
  559. func TestCancelUserMigrationBatch_SoftDeleteSuccess(t *testing.T) {
  560. gin.SetMode(gin.TestMode)
  561. router := gin.New()
  562. router.Use(testRootAuthMiddleware())
  563. router.POST("/api/user-migrations/batches/:id/cancel", CancelUserMigrationBatch)
  564. db := setupUserMigrationControllerDB(t)
  565. orig := model.DB
  566. model.DB = db
  567. defer func() {
  568. model.DB = orig
  569. }()
  570. require.NoError(t, db.Create(&model.UserMigrationBatch{
  571. Id: 1,
  572. Name: "cancel-me",
  573. SourceRegion: "ov",
  574. TargetRegion: "cn",
  575. SelectionMode: "explicit_ids",
  576. SelectionPayload: `{"source_user_ids":[11,12]}`,
  577. OperatorId: 1,
  578. Status: model.UserMigrationBatchStatusScanned,
  579. }).Error)
  580. req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches/1/cancel", nil)
  581. w := httptest.NewRecorder()
  582. router.ServeHTTP(w, req)
  583. require.Equal(t, http.StatusOK, w.Code)
  584. var batch model.UserMigrationBatch
  585. require.NoError(t, db.First(&batch, 1).Error)
  586. require.Equal(t, model.UserMigrationBatchStatusCancelled, batch.Status)
  587. }
  588. func TestCancelUserMigrationBatch_InvalidStateRejected(t *testing.T) {
  589. gin.SetMode(gin.TestMode)
  590. router := gin.New()
  591. router.Use(testRootAuthMiddleware())
  592. router.POST("/api/user-migrations/batches/:id/cancel", CancelUserMigrationBatch)
  593. db := setupUserMigrationControllerDB(t)
  594. orig := model.DB
  595. model.DB = db
  596. defer func() {
  597. model.DB = orig
  598. }()
  599. for _, status := range []string{
  600. model.UserMigrationBatchStatusScanning,
  601. model.UserMigrationBatchStatusRunning,
  602. model.UserMigrationBatchStatusCompleted,
  603. } {
  604. require.NoError(t, db.Exec("DELETE FROM user_migration_batches").Error)
  605. require.NoError(t, db.Create(&model.UserMigrationBatch{
  606. Id: 1,
  607. Name: "cannot-cancel",
  608. SourceRegion: "ov",
  609. TargetRegion: "cn",
  610. OperatorId: 1,
  611. Status: status,
  612. }).Error)
  613. req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches/1/cancel", nil)
  614. w := httptest.NewRecorder()
  615. router.ServeHTTP(w, req)
  616. require.Equal(t, http.StatusConflict, w.Code)
  617. }
  618. }
  619. func TestCreateUserMigrationBatch_ExplicitIDs_AllowsReuseAfterCancel(t *testing.T) {
  620. gin.SetMode(gin.TestMode)
  621. router := gin.New()
  622. router.Use(testRootAuthMiddleware())
  623. router.POST("/api/user-migrations/batches", CreateUserMigrationBatch)
  624. router.POST("/api/user-migrations/batches/:id/cancel", CancelUserMigrationBatch)
  625. db := setupUserMigrationControllerDB(t)
  626. orig := model.DB
  627. model.DB = db
  628. defer func() {
  629. model.DB = orig
  630. }()
  631. existingBatch := &model.UserMigrationBatch{
  632. Name: "existing-scanned-batch",
  633. SourceRegion: "ov",
  634. TargetRegion: "cn",
  635. SelectionMode: "explicit_ids",
  636. SelectionPayload: `{"source_user_ids":[11,12]}`,
  637. Status: model.UserMigrationBatchStatusScanned,
  638. OperatorId: 1,
  639. }
  640. require.NoError(t, db.Create(existingBatch).Error)
  641. cancelReq := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches/1/cancel", nil)
  642. cancelW := httptest.NewRecorder()
  643. router.ServeHTTP(cancelW, cancelReq)
  644. require.Equal(t, http.StatusOK, cancelW.Code)
  645. createReq := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches", bytes.NewBufferString(`{
  646. "name":"recreated-batch",
  647. "source_region":"ov",
  648. "target_region":"cn",
  649. "selection_mode":"explicit_ids",
  650. "source_user_ids":[11,12]
  651. }`))
  652. createReq.Header.Set("Content-Type", "application/json")
  653. createW := httptest.NewRecorder()
  654. router.ServeHTTP(createW, createReq)
  655. require.Equal(t, http.StatusOK, createW.Code)
  656. }
  657. func TestVerifyUserMigrationBatch_AggregatesResult(t *testing.T) {
  658. gin.SetMode(gin.TestMode)
  659. router := gin.New()
  660. router.Use(testRootAuthMiddleware())
  661. router.POST("/api/user-migrations/batches/:id/verify", VerifyUserMigrationBatch)
  662. db := setupUserMigrationControllerDB(t)
  663. orig := model.DB
  664. origFactory := newMigrationVerifier
  665. model.DB = db
  666. defer func() {
  667. model.DB = orig
  668. newMigrationVerifier = origFactory
  669. }()
  670. require.NoError(t, db.Create(&model.UserMigrationItem{
  671. Id: 1,
  672. BatchId: 1,
  673. SourceUserId: 1001,
  674. TargetUserId: 1,
  675. Status: model.UserMigrationItemStatusMigrated,
  676. }).Error)
  677. require.NoError(t, db.Create(&model.UserMigrationItem{
  678. Id: 2,
  679. BatchId: 1,
  680. SourceUserId: 1002,
  681. TargetUserId: 2,
  682. Status: model.UserMigrationItemStatusMigrated,
  683. }).Error)
  684. require.NoError(t, db.Create(&model.UserMigrationItem{
  685. Id: 3,
  686. BatchId: 1,
  687. SourceUserId: 1003,
  688. TargetUserId: 3,
  689. Status: model.UserMigrationItemStatusMigrated,
  690. }).Error)
  691. newMigrationVerifier = func() migrationVerifier {
  692. return &fakeMigrationVerifier{
  693. results: map[int]*user_migration.VerifyResult{
  694. 1001: {
  695. TargetUserExists: true,
  696. RemoteConverted: true,
  697. QuotaMatched: true,
  698. },
  699. 1002: {
  700. TargetUserExists: false,
  701. RemoteConverted: true,
  702. QuotaMatched: false,
  703. },
  704. },
  705. errs: map[int]error{
  706. 1003: errors.New("remote user not found"),
  707. },
  708. }
  709. }
  710. req := httptest.NewRequest(http.MethodPost, "/api/user-migrations/batches/1/verify", nil)
  711. w := httptest.NewRecorder()
  712. router.ServeHTTP(w, req)
  713. require.Equal(t, http.StatusOK, w.Code)
  714. var resp struct {
  715. Success bool `json:"success"`
  716. Data []map[string]any `json:"data"`
  717. }
  718. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  719. require.True(t, resp.Success)
  720. require.Len(t, resp.Data, 3)
  721. bySource := make(map[int]map[string]any, len(resp.Data))
  722. for _, entry := range resp.Data {
  723. bySource[int(entry["source_user_id"].(float64))] = entry
  724. }
  725. require.NotNil(t, bySource[1001]["result"])
  726. require.NotNil(t, bySource[1002]["result"])
  727. require.Equal(t, "remote user not found", bySource[1003]["error"])
  728. }