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

801 строка
27 KiB

  1. package e2e
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "testing"
  7. "time"
  8. "github.com/QuantumNous/new-api/common"
  9. "github.com/QuantumNous/new-api/model"
  10. user_migration "github.com/QuantumNous/new-api/service/user_migration"
  11. helpers "github.com/QuantumNous/new-api/test/e2e/helpers"
  12. "github.com/stretchr/testify/require"
  13. )
  14. var (
  15. repoRoot string
  16. artifactRoot string
  17. binaryPath string
  18. )
  19. type simpleResponse struct {
  20. Success bool `json:"success"`
  21. Message string `json:"message,omitempty"`
  22. Error string `json:"error,omitempty"`
  23. }
  24. type createBatchResponse struct {
  25. Success bool `json:"success"`
  26. Data model.UserMigrationBatch `json:"data"`
  27. }
  28. type adminUserResponse struct {
  29. Success bool `json:"success"`
  30. Data model.User `json:"data"`
  31. }
  32. type batchPageResponse struct {
  33. Success bool `json:"success"`
  34. Data model.UserMigrationBatch `json:"data"`
  35. Items []model.UserMigrationItem `json:"items"`
  36. Total int64 `json:"total"`
  37. }
  38. type preparedBatch struct {
  39. BatchID int
  40. Detail *helpers.BatchDetail
  41. Items map[int]*model.UserMigrationItem
  42. }
  43. type executedBatch struct {
  44. Prepared *preparedBatch
  45. Detail *helpers.BatchDetail
  46. CreatedUser *model.User
  47. SoftAffUser *model.User
  48. VerifyResult map[int]helpers.VerifyEntry
  49. }
  50. func TestMain(m *testing.M) {
  51. var err error
  52. wd, err := os.Getwd()
  53. if err != nil {
  54. fmt.Fprintln(os.Stderr, err)
  55. os.Exit(1)
  56. }
  57. repoRoot = filepath.Clean(filepath.Join(wd, "..", ".."))
  58. artifactRoot = filepath.Join(repoRoot, "test-artifacts", "user-migration-e2e")
  59. if err := os.MkdirAll(artifactRoot, 0o755); err != nil {
  60. fmt.Fprintln(os.Stderr, err)
  61. os.Exit(1)
  62. }
  63. binaryPath, err = helpers.BuildBinary(repoRoot, artifactRoot)
  64. if err != nil {
  65. fmt.Fprintln(os.Stderr, err)
  66. os.Exit(1)
  67. }
  68. os.Exit(m.Run())
  69. }
  70. func TestUserMigrationE2E_FullFlow_GrayScenarios(t *testing.T) {
  71. env := newEnvironment(t)
  72. result := runFullFlow(t, env)
  73. require.Equal(t, model.UserMigrationBatchStatusCompleted, result.Detail.Batch.Status)
  74. require.Equal(t, 3, result.Detail.Batch.MigratedUsers)
  75. require.Equal(t, 0, result.Detail.Batch.FailedUsers)
  76. require.Len(t, result.VerifyResult, 3)
  77. }
  78. func TestUserMigrationE2E_IdempotentRetry(t *testing.T) {
  79. env := newEnvironment(t)
  80. result := runFullFlow(t, env)
  81. forceRetryableState(t, env.CNDBPath, result.Prepared.BatchID, []int{
  82. result.Prepared.Items[helpers.OVCreateUserID].Id,
  83. result.Prepared.Items[helpers.OVMergeUserID].Id,
  84. result.Prepared.Items[helpers.OVSoftAffUserID].Id,
  85. })
  86. var retryResp simpleResponse
  87. require.NoError(t, env.CNClient.PostJSON(
  88. fmt.Sprintf("/api/user-migrations/batches/%d/retry", result.Prepared.BatchID),
  89. nil,
  90. 202,
  91. &retryResp,
  92. ))
  93. require.True(t, retryResp.Success)
  94. retried, err := helpers.WaitBatchStatus(env.CNClient, result.Prepared.BatchID, model.UserMigrationBatchStatusCompleted, 25*time.Second)
  95. require.NoError(t, err)
  96. retriedItems := indexItemsBySourceUserID(retried.Items)
  97. require.Equal(t, model.UserMigrationItemStatusMigrated, retriedItems[helpers.OVCreateUserID].Status)
  98. require.Equal(t, model.UserMigrationItemStatusMigrated, retriedItems[helpers.OVMergeUserID].Status)
  99. require.Equal(t, model.UserMigrationItemStatusMigrated, retriedItems[helpers.OVSoftAffUserID].Status)
  100. require.EqualValues(t, 1, helpers.CountUsersByUsername(t, env.CNDBPath, helpers.UsernameOVCreate))
  101. require.EqualValues(t, 1, helpers.CountUsersByUsername(t, env.CNDBPath, helpers.UsernameOVSoftAff))
  102. helpers.AssertQuota(t, env.CNDBPath, helpers.CNMergeTargetUserID, 700)
  103. helpers.AssertNoDuplicateQuotaGrant(t, env.CNDBPath, result.Prepared.Items[helpers.OVMergeUserID].MigrationToken)
  104. verifyEntries, err := helpers.VerifyBatch(env.CNClient, result.Prepared.BatchID)
  105. require.NoError(t, err)
  106. require.Len(t, verifyEntries, 3)
  107. for _, entry := range verifyEntries {
  108. require.Empty(t, entry.Error)
  109. require.NotNil(t, entry.Result)
  110. require.True(t, entry.Result.TargetUserExists)
  111. require.True(t, entry.Result.RemoteConverted)
  112. require.True(t, entry.Result.QuotaMatched)
  113. }
  114. }
  115. func TestUserMigrationE2E_ExecutionTimeDriftFails(t *testing.T) {
  116. env := newEnvironment(t)
  117. prepared := prepareGrayScenarioBatch(t, env)
  118. renameOVUser(t, env, helpers.OVCreateUserID, helpers.UsernameCNMergeTarget)
  119. var executeResp simpleResponse
  120. require.NoError(t, env.CNClient.PostJSON(
  121. fmt.Sprintf("/api/user-migrations/batches/%d/execute", prepared.BatchID),
  122. nil,
  123. 202,
  124. &executeResp,
  125. ))
  126. require.True(t, executeResp.Success)
  127. failedDetail, err := helpers.WaitBatchStatus(env.CNClient, prepared.BatchID, model.UserMigrationBatchStatusFailed, 25*time.Second)
  128. require.NoError(t, err)
  129. items := indexItemsBySourceUserID(failedDetail.Items)
  130. require.Equal(t, model.UserMigrationItemStatusFailed, items[helpers.OVCreateUserID].Status)
  131. require.Contains(t, items[helpers.OVCreateUserID].ErrorMessage, "drift")
  132. require.Equal(t, model.UserMigrationItemStatusMigrated, items[helpers.OVMergeUserID].Status)
  133. require.Equal(t, model.UserMigrationItemStatusMigrated, items[helpers.OVSoftAffUserID].Status)
  134. require.Equal(t, model.UserMigrationItemStatusSkipped, items[helpers.OVHardSyncedUserID].Status)
  135. require.Equal(t, model.UserMigrationItemStatusSkipped, items[helpers.OVUsernameConflictUserID].Status)
  136. require.EqualValues(t, 0, helpers.CountUsersByEmail(t, env.CNDBPath, helpers.EmailOVCreate))
  137. driftedRemote, err := helpers.GetRemoteUserSnapshot(env.Cluster.OV.BaseURL, helpers.OVCreateUserID)
  138. require.NoError(t, err)
  139. require.NotEqual(t, common.UserSourceSynced, driftedRemote.Source)
  140. }
  141. func TestUserMigrationE2E_RescanPreservesHumanDecision(t *testing.T) {
  142. env := newEnvironment(t)
  143. prepared := prepareGrayScenarioBatch(t, env)
  144. var scanResp simpleResponse
  145. require.NoError(t, env.CNClient.PostJSON(
  146. fmt.Sprintf("/api/user-migrations/batches/%d/scan", prepared.BatchID),
  147. nil,
  148. 202,
  149. &scanResp,
  150. ))
  151. require.True(t, scanResp.Success)
  152. rescannedDetail, err := helpers.WaitBatchStatus(env.CNClient, prepared.BatchID, model.UserMigrationBatchStatusScanned, 20*time.Second)
  153. require.NoError(t, err)
  154. items := indexItemsBySourceUserID(rescannedDetail.Items)
  155. require.Equal(t, model.UserMigrationStrategySkip, items[helpers.OVHardSyncedUserID].ResolutionStrategy)
  156. require.Equal(t, model.UserMigrationItemStatusConflict, items[helpers.OVHardSyncedUserID].Status)
  157. require.Equal(t, model.UserMigrationStrategySkip, items[helpers.OVUsernameConflictUserID].ResolutionStrategy)
  158. require.Equal(t, model.UserMigrationItemStatusConflict, items[helpers.OVUsernameConflictUserID].Status)
  159. require.Equal(t, model.UserMigrationStrategyMergeExisting, items[helpers.OVMergeUserID].ResolutionStrategy)
  160. require.Equal(t, helpers.CNMergeTargetUserID, items[helpers.OVMergeUserID].TargetUserId)
  161. }
  162. func TestUserMigrationE2E_ScanPagination(t *testing.T) {
  163. env := newEnvironment(t)
  164. createManagedUsers(t, env.OVClient, "ov-page", 97, common.RoleCommonUser)
  165. batchID := createBatch(t, env, "pagination")
  166. var scanResp simpleResponse
  167. require.NoError(t, env.CNClient.PostJSON(
  168. fmt.Sprintf("/api/user-migrations/batches/%d/scan", batchID),
  169. nil,
  170. 202,
  171. &scanResp,
  172. ))
  173. require.True(t, scanResp.Success)
  174. detail, err := helpers.WaitBatchStatus(env.CNClient, batchID, model.UserMigrationBatchStatusScanned, 60*time.Second)
  175. require.NoError(t, err)
  176. require.EqualValues(t, 102, detail.Total)
  177. require.Equal(t, 102, detail.Batch.TotalUsers)
  178. page0 := getBatchPage(t, env.CNClient, batchID, 0, 100)
  179. require.Len(t, page0.Items, 100)
  180. page1 := getBatchPage(t, env.CNClient, batchID, 1, 100)
  181. require.Len(t, page1.Items, 2)
  182. require.EqualValues(t, 102, page1.Total)
  183. }
  184. func TestUserMigrationE2E_RootPermission(t *testing.T) {
  185. env := newEnvironment(t)
  186. adminClient := createAndLoginManagedUser(t, env.Cluster.CN.BaseURL, env.CNClient, "migration-admin", helpers.DefaultRootPassword, common.RoleAdminUser)
  187. var listResp simpleResponse
  188. require.NoError(t, adminClient.GetJSON("/api/user-migrations/batches", 200, &listResp))
  189. require.False(t, listResp.Success)
  190. require.NotEmpty(t, listResp.Message)
  191. var createResp simpleResponse
  192. require.NoError(t, adminClient.PostJSON("/api/user-migrations/batches", map[string]any{
  193. "name": "no-access",
  194. "source_region": "ov",
  195. "target_region": "cn",
  196. }, 200, &createResp))
  197. require.False(t, createResp.Success)
  198. require.NotEmpty(t, createResp.Message)
  199. }
  200. func TestUserMigrationE2E_ResolveInvalidPayload(t *testing.T) {
  201. env := newEnvironment(t)
  202. scanned := scanGrayScenarioBatch(t, env, "resolve-invalid")
  203. conflictItem := scanned.Items[helpers.OVHardSyncedUserID]
  204. var invalidStrategyResp simpleResponse
  205. require.NoError(t, env.CNClient.PostJSON(
  206. fmt.Sprintf("/api/user-migrations/items/%d/resolve", conflictItem.Id),
  207. map[string]any{
  208. "resolution_strategy": "invalid",
  209. },
  210. 400,
  211. &invalidStrategyResp,
  212. ))
  213. require.False(t, invalidStrategyResp.Success)
  214. require.Contains(t, invalidStrategyResp.Error, "invalid resolution_strategy")
  215. var mergeWithoutTargetResp simpleResponse
  216. require.NoError(t, env.CNClient.PostJSON(
  217. fmt.Sprintf("/api/user-migrations/items/%d/resolve", conflictItem.Id),
  218. map[string]any{
  219. "resolution_strategy": model.UserMigrationStrategyMergeExisting,
  220. },
  221. 400,
  222. &mergeWithoutTargetResp,
  223. ))
  224. require.False(t, mergeWithoutTargetResp.Success)
  225. require.Contains(t, mergeWithoutTargetResp.Error, "target_user_id")
  226. }
  227. func TestUserMigrationE2E_ScanConflict_WhenAlreadyScanning(t *testing.T) {
  228. env := newEnvironment(t)
  229. batchID := createBatch(t, env, "scan-conflict")
  230. setBatchStatus(t, env.CNDBPath, batchID, model.UserMigrationBatchStatusScanning)
  231. var resp simpleResponse
  232. require.NoError(t, env.CNClient.PostJSON(
  233. fmt.Sprintf("/api/user-migrations/batches/%d/scan", batchID),
  234. nil,
  235. 409,
  236. &resp,
  237. ))
  238. require.False(t, resp.Success)
  239. require.Contains(t, resp.Error, "already being scanned")
  240. }
  241. func TestUserMigrationE2E_ExecuteConflict_WhenAlreadyRunning(t *testing.T) {
  242. env := newEnvironment(t)
  243. prepared := prepareGrayScenarioBatch(t, env)
  244. setBatchStatus(t, env.CNDBPath, prepared.BatchID, model.UserMigrationBatchStatusRunning)
  245. var resp simpleResponse
  246. require.NoError(t, env.CNClient.PostJSON(
  247. fmt.Sprintf("/api/user-migrations/batches/%d/execute", prepared.BatchID),
  248. nil,
  249. 409,
  250. &resp,
  251. ))
  252. require.False(t, resp.Success)
  253. require.Contains(t, resp.Error, "already running")
  254. }
  255. func TestUserMigrationE2E_VerifyQuotaMismatchDetected(t *testing.T) {
  256. env := newEnvironment(t)
  257. result := runFullFlow(t, env)
  258. convertRemoteUserToSynced(t, env.Cluster.OV.BaseURL, helpers.OVMergeUserID, helpers.CNMergeTargetUserID, 199)
  259. verifyEntries, err := helpers.VerifyBatch(env.CNClient, result.Prepared.BatchID)
  260. require.NoError(t, err)
  261. verifyBySource := make(map[int]helpers.VerifyEntry, len(verifyEntries))
  262. for _, entry := range verifyEntries {
  263. verifyBySource[entry.SourceUserID] = entry
  264. }
  265. require.NotNil(t, verifyBySource[helpers.OVMergeUserID].Result)
  266. require.False(t, verifyBySource[helpers.OVMergeUserID].Result.QuotaMatched)
  267. require.True(t, verifyBySource[helpers.OVCreateUserID].Result.QuotaMatched)
  268. require.True(t, verifyBySource[helpers.OVSoftAffUserID].Result.QuotaMatched)
  269. }
  270. func TestUserMigrationE2E_SkipNeverConvertsRemote(t *testing.T) {
  271. env := newEnvironment(t)
  272. prepared := prepareGrayScenarioBatch(t, env)
  273. resolveItem(t, env, prepared.Items[helpers.OVCreateUserID].Id, model.UserMigrationStrategySkip, 0)
  274. readyDetail, err := helpers.WaitBatchStatus(env.CNClient, prepared.BatchID, model.UserMigrationBatchStatusReady, 10*time.Second)
  275. require.NoError(t, err)
  276. var executeResp simpleResponse
  277. require.NoError(t, env.CNClient.PostJSON(
  278. fmt.Sprintf("/api/user-migrations/batches/%d/execute", prepared.BatchID),
  279. nil,
  280. 202,
  281. &executeResp,
  282. ))
  283. require.True(t, executeResp.Success)
  284. completedDetail, err := helpers.WaitBatchStatus(env.CNClient, prepared.BatchID, model.UserMigrationBatchStatusCompleted, 25*time.Second)
  285. require.NoError(t, err)
  286. items := indexItemsBySourceUserID(completedDetail.Items)
  287. require.Equal(t, model.UserMigrationItemStatusSkipped, items[helpers.OVCreateUserID].Status)
  288. require.EqualValues(t, 0, helpers.CountUsersByEmail(t, env.CNDBPath, helpers.EmailOVCreate))
  289. remoteUser, err := helpers.GetRemoteUserSnapshot(env.Cluster.OV.BaseURL, helpers.OVCreateUserID)
  290. require.NoError(t, err)
  291. require.NotEqual(t, common.UserSourceSynced, remoteUser.Source)
  292. require.Equal(t, readyDetail.Items[0].BatchId, items[helpers.OVCreateUserID].BatchId)
  293. }
  294. func TestUserMigrationE2E_ExplicitIDs_OnlySelectedUsersScanned(t *testing.T) {
  295. env := newEnvironment(t)
  296. batchID := createBatchWithSelection(t, env, "explicit-selected", []int{
  297. helpers.OVCreateUserID,
  298. helpers.OVMergeUserID,
  299. })
  300. var scanResp simpleResponse
  301. require.NoError(t, env.CNClient.PostJSON(
  302. fmt.Sprintf("/api/user-migrations/batches/%d/scan", batchID),
  303. nil,
  304. 202,
  305. &scanResp,
  306. ))
  307. require.True(t, scanResp.Success)
  308. detail, err := helpers.WaitBatchStatus(env.CNClient, batchID, model.UserMigrationBatchStatusReady, 20*time.Second)
  309. require.NoError(t, err)
  310. require.EqualValues(t, 2, detail.Total)
  311. require.Equal(t, 2, detail.Batch.TotalUsers)
  312. require.Equal(t, user_migration.SelectionModeExplicitIDs, detail.Batch.SelectionMode)
  313. require.Equal(t, 2, detail.Batch.RequestedUserCount)
  314. items := indexItemsBySourceUserID(detail.Items)
  315. require.Len(t, items, 2)
  316. require.Contains(t, items, helpers.OVCreateUserID)
  317. require.Contains(t, items, helpers.OVMergeUserID)
  318. require.NotContains(t, items, helpers.OVHardSyncedUserID)
  319. }
  320. func TestUserMigrationE2E_ExplicitIDs_SelectionSummaryRecorded(t *testing.T) {
  321. env := newEnvironment(t)
  322. batchID := createBatchWithSelection(t, env, "explicit-summary", []int{
  323. helpers.OVCreateUserID,
  324. helpers.OVSyncedCopyUserID,
  325. 1,
  326. 19999999,
  327. })
  328. var scanResp simpleResponse
  329. require.NoError(t, env.CNClient.PostJSON(
  330. fmt.Sprintf("/api/user-migrations/batches/%d/scan", batchID),
  331. nil,
  332. 202,
  333. &scanResp,
  334. ))
  335. require.True(t, scanResp.Success)
  336. detail, err := helpers.WaitBatchStatus(env.CNClient, batchID, model.UserMigrationBatchStatusReady, 20*time.Second)
  337. require.NoError(t, err)
  338. require.Equal(t, 4, detail.Batch.RequestedUserCount)
  339. require.Equal(t, 1, detail.Batch.MatchedUserCount)
  340. require.Equal(t, 3, detail.Batch.ExcludedUserCount)
  341. require.EqualValues(t, 1, detail.Total)
  342. var summary user_migration.SelectionSummary
  343. require.NoError(t, common.UnmarshalJsonStr(detail.Batch.SelectionSummary, &summary))
  344. require.Equal(t, 4, summary.Requested)
  345. require.Equal(t, 1, summary.Matched)
  346. require.Len(t, summary.Excluded, 3)
  347. reasonsByID := make(map[int]string, len(summary.Excluded))
  348. for _, item := range summary.Excluded {
  349. reasonsByID[item.UserID] = item.Reason
  350. }
  351. require.Equal(t, "already_synced", reasonsByID[helpers.OVSyncedCopyUserID])
  352. require.Equal(t, "root_user_not_migratable", reasonsByID[1])
  353. require.Equal(t, "not_found", reasonsByID[19999999])
  354. }
  355. func TestUserMigrationE2E_ExplicitIDs_ActiveBatchOverlapRejected(t *testing.T) {
  356. env := newEnvironment(t)
  357. firstBatchID := createBatchWithSelection(t, env, "explicit-overlap-first", []int{
  358. helpers.OVCreateUserID,
  359. })
  360. require.NotZero(t, firstBatchID)
  361. var resp struct {
  362. Success bool `json:"success"`
  363. Error string `json:"error"`
  364. Conflicts []struct {
  365. SourceUserID int `json:"source_user_id"`
  366. BatchID int `json:"batch_id"`
  367. BatchStatus string `json:"batch_status"`
  368. } `json:"conflicts"`
  369. }
  370. require.NoError(t, env.CNClient.PostJSON("/api/user-migrations/batches", map[string]any{
  371. "name": "explicit-overlap-second",
  372. "source_region": "ov",
  373. "target_region": "cn",
  374. "selection_mode": "explicit_ids",
  375. "source_user_ids": []int{helpers.OVCreateUserID, helpers.OVMergeUserID},
  376. }, 409, &resp))
  377. require.False(t, resp.Success)
  378. require.Equal(t, "some selected users already exist in active migration batches", resp.Error)
  379. require.Len(t, resp.Conflicts, 1)
  380. require.Equal(t, helpers.OVCreateUserID, resp.Conflicts[0].SourceUserID)
  381. require.Equal(t, firstBatchID, resp.Conflicts[0].BatchID)
  382. require.Equal(t, model.UserMigrationBatchStatusDraft, resp.Conflicts[0].BatchStatus)
  383. }
  384. func newEnvironment(t *testing.T) *helpers.Environment {
  385. t.Helper()
  386. env, err := helpers.PrepareEnvironment(repoRoot, artifactRoot, binaryPath, t.Name())
  387. require.NoError(t, err)
  388. t.Cleanup(func() {
  389. require.NoError(t, env.Close())
  390. })
  391. return env
  392. }
  393. func runFullFlow(t *testing.T, env *helpers.Environment) *executedBatch {
  394. t.Helper()
  395. prepared := prepareGrayScenarioBatch(t, env)
  396. var executeResp simpleResponse
  397. require.NoError(t, env.CNClient.PostJSON(
  398. fmt.Sprintf("/api/user-migrations/batches/%d/execute", prepared.BatchID),
  399. nil,
  400. 202,
  401. &executeResp,
  402. ))
  403. require.True(t, executeResp.Success)
  404. completedDetail, err := helpers.WaitBatchStatus(env.CNClient, prepared.BatchID, model.UserMigrationBatchStatusCompleted, 25*time.Second)
  405. require.NoError(t, err)
  406. require.EqualValues(t, 5, completedDetail.Total)
  407. items := indexItemsBySourceUserID(completedDetail.Items)
  408. require.Equal(t, model.UserMigrationItemStatusMigrated, items[helpers.OVCreateUserID].Status)
  409. require.Equal(t, model.UserMigrationItemStatusMigrated, items[helpers.OVMergeUserID].Status)
  410. require.Equal(t, model.UserMigrationItemStatusMigrated, items[helpers.OVSoftAffUserID].Status)
  411. require.Equal(t, model.UserMigrationItemStatusSkipped, items[helpers.OVHardSyncedUserID].Status)
  412. require.Equal(t, model.UserMigrationItemStatusSkipped, items[helpers.OVUsernameConflictUserID].Status)
  413. createdUser := helpers.AssertCNUserExists(t, env.CNDBPath, helpers.UsernameOVCreate)
  414. require.Equal(t, 100, createdUser.Quota)
  415. helpers.AssertQuota(t, env.CNDBPath, helpers.CNMergeTargetUserID, 700)
  416. softAffUser := helpers.AssertCNUserExists(t, env.CNDBPath, helpers.UsernameOVSoftAff)
  417. require.Equal(t, 150, softAffUser.Quota)
  418. require.NotEqual(t, helpers.AffCodeCNSoftAffOwner, softAffUser.AffCode)
  419. helpers.AssertOVUserSynced(t, env.Cluster.OV.BaseURL, helpers.OVCreateUserID, createdUser.Id, 100)
  420. helpers.AssertOVUserSynced(t, env.Cluster.OV.BaseURL, helpers.OVMergeUserID, helpers.CNMergeTargetUserID, 700)
  421. helpers.AssertOVUserSynced(t, env.Cluster.OV.BaseURL, helpers.OVSoftAffUserID, softAffUser.Id, 150)
  422. hardSnapshot, err := helpers.GetRemoteUserSnapshot(env.Cluster.OV.BaseURL, helpers.OVHardSyncedUserID)
  423. require.NoError(t, err)
  424. require.NotEqual(t, common.UserSourceSynced, hardSnapshot.Source)
  425. usernameSnapshot, err := helpers.GetRemoteUserSnapshot(env.Cluster.OV.BaseURL, helpers.OVUsernameConflictUserID)
  426. require.NoError(t, err)
  427. require.NotEqual(t, common.UserSourceSynced, usernameSnapshot.Source)
  428. verifyEntries, err := helpers.VerifyBatch(env.CNClient, prepared.BatchID)
  429. require.NoError(t, err)
  430. require.Len(t, verifyEntries, 3)
  431. verifyBySource := make(map[int]helpers.VerifyEntry, len(verifyEntries))
  432. for _, entry := range verifyEntries {
  433. require.Empty(t, entry.Error)
  434. require.NotNil(t, entry.Result)
  435. require.True(t, entry.Result.TargetUserExists)
  436. require.True(t, entry.Result.RemoteConverted)
  437. require.True(t, entry.Result.QuotaMatched)
  438. verifyBySource[entry.SourceUserID] = entry
  439. }
  440. return &executedBatch{
  441. Prepared: prepared,
  442. Detail: completedDetail,
  443. CreatedUser: createdUser,
  444. SoftAffUser: softAffUser,
  445. VerifyResult: verifyBySource,
  446. }
  447. }
  448. func prepareGrayScenarioBatch(t *testing.T, env *helpers.Environment) *preparedBatch {
  449. t.Helper()
  450. scanned := scanGrayScenarioBatch(t, env, "gray-scenarios")
  451. scannedItems := scanned.Items
  452. require.Equal(t, model.UserMigrationStrategyCreateNew, scannedItems[helpers.OVCreateUserID].ResolutionStrategy)
  453. require.Equal(t, model.UserMigrationItemStatusReady, scannedItems[helpers.OVCreateUserID].Status)
  454. require.Equal(t, model.UserMigrationStrategyMergeExisting, scannedItems[helpers.OVMergeUserID].ResolutionStrategy)
  455. require.Equal(t, model.UserMigrationItemStatusReady, scannedItems[helpers.OVMergeUserID].Status)
  456. require.Equal(t, helpers.CNMergeTargetUserID, scannedItems[helpers.OVMergeUserID].TargetUserId)
  457. require.Equal(t, model.UserMigrationItemStatusConflict, scannedItems[helpers.OVHardSyncedUserID].Status)
  458. require.Contains(t, decodeFlags(t, scannedItems[helpers.OVHardSyncedUserID].ConflictFlags), "cn_already_synced_to_ov")
  459. require.Equal(t, model.UserMigrationItemStatusConflict, scannedItems[helpers.OVUsernameConflictUserID].Status)
  460. require.Contains(t, decodeFlags(t, scannedItems[helpers.OVUsernameConflictUserID].ConflictFlags), "username")
  461. require.Equal(t, model.UserMigrationStrategyCreateNew, scannedItems[helpers.OVSoftAffUserID].ResolutionStrategy)
  462. require.Equal(t, model.UserMigrationItemStatusReady, scannedItems[helpers.OVSoftAffUserID].Status)
  463. require.Contains(t, decodeFlags(t, scannedItems[helpers.OVSoftAffUserID].ConflictFlags), "aff_code")
  464. resolveItem(t, env, scannedItems[helpers.OVHardSyncedUserID].Id, model.UserMigrationStrategySkip, 0)
  465. resolveItem(t, env, scannedItems[helpers.OVUsernameConflictUserID].Id, model.UserMigrationStrategySkip, 0)
  466. readyDetail, err := helpers.WaitBatchStatus(env.CNClient, scanned.BatchID, model.UserMigrationBatchStatusReady, 10*time.Second)
  467. require.NoError(t, err)
  468. readyItems := indexItemsBySourceUserID(readyDetail.Items)
  469. require.Equal(t, model.UserMigrationStrategySkip, readyItems[helpers.OVHardSyncedUserID].ResolutionStrategy)
  470. require.Equal(t, model.UserMigrationItemStatusReady, readyItems[helpers.OVHardSyncedUserID].Status)
  471. require.Equal(t, model.UserMigrationStrategySkip, readyItems[helpers.OVUsernameConflictUserID].ResolutionStrategy)
  472. require.Equal(t, model.UserMigrationItemStatusReady, readyItems[helpers.OVUsernameConflictUserID].Status)
  473. return &preparedBatch{
  474. BatchID: scanned.BatchID,
  475. Detail: readyDetail,
  476. Items: readyItems,
  477. }
  478. }
  479. func scanGrayScenarioBatch(t *testing.T, env *helpers.Environment, name string) *preparedBatch {
  480. t.Helper()
  481. batchID := createBatch(t, env, name)
  482. var scanResp simpleResponse
  483. require.NoError(t, env.CNClient.PostJSON(
  484. fmt.Sprintf("/api/user-migrations/batches/%d/scan", batchID),
  485. nil,
  486. 202,
  487. &scanResp,
  488. ))
  489. require.True(t, scanResp.Success)
  490. scannedDetail, err := helpers.WaitBatchStatus(env.CNClient, batchID, model.UserMigrationBatchStatusScanned, 20*time.Second)
  491. require.NoError(t, err)
  492. require.EqualValues(t, 5, scannedDetail.Total)
  493. scannedItems := indexItemsBySourceUserID(scannedDetail.Items)
  494. require.Len(t, scannedItems, 5)
  495. return &preparedBatch{
  496. BatchID: batchID,
  497. Detail: scannedDetail,
  498. Items: scannedItems,
  499. }
  500. }
  501. func createBatch(t *testing.T, env *helpers.Environment, name string) int {
  502. t.Helper()
  503. return createBatchWithSelection(t, env, name, nil)
  504. }
  505. func createBatchWithSelection(t *testing.T, env *helpers.Environment, name string, sourceUserIDs []int) int {
  506. t.Helper()
  507. payload := map[string]any{
  508. "name": name,
  509. "source_region": "ov",
  510. "target_region": "cn",
  511. }
  512. if len(sourceUserIDs) > 0 {
  513. payload["selection_mode"] = "explicit_ids"
  514. payload["source_user_ids"] = sourceUserIDs
  515. }
  516. var resp createBatchResponse
  517. require.NoError(t, env.CNClient.PostJSON("/api/user-migrations/batches", payload, 200, &resp))
  518. require.True(t, resp.Success)
  519. require.NotZero(t, resp.Data.Id)
  520. return resp.Data.Id
  521. }
  522. func resolveItem(t *testing.T, env *helpers.Environment, itemID int, strategy string, targetUserID int) {
  523. t.Helper()
  524. var resp simpleResponse
  525. require.NoError(t, env.CNClient.PostJSON(
  526. fmt.Sprintf("/api/user-migrations/items/%d/resolve", itemID),
  527. map[string]any{
  528. "resolution_strategy": strategy,
  529. "target_user_id": targetUserID,
  530. },
  531. 200,
  532. &resp,
  533. ))
  534. require.True(t, resp.Success)
  535. }
  536. func renameOVUser(t *testing.T, env *helpers.Environment, userID int, newUsername string) {
  537. t.Helper()
  538. var getResp adminUserResponse
  539. require.NoError(t, env.OVClient.GetJSON(fmt.Sprintf("/api/user/%d", userID), 200, &getResp))
  540. require.True(t, getResp.Success)
  541. var updateResp simpleResponse
  542. require.NoError(t, env.OVClient.PutJSON("/api/user", map[string]any{
  543. "id": getResp.Data.Id,
  544. "username": newUsername,
  545. "display_name": getResp.Data.DisplayName,
  546. "group": getResp.Data.Group,
  547. "quota": getResp.Data.Quota,
  548. "remark": getResp.Data.Remark,
  549. "capture_relay": getResp.Data.CaptureRelay,
  550. }, 200, &updateResp))
  551. require.True(t, updateResp.Success)
  552. }
  553. func createAndLoginManagedUser(t *testing.T, baseURL string, rootClient *helpers.APIClient, username, password string, role int) *helpers.APIClient {
  554. t.Helper()
  555. var resp simpleResponse
  556. require.NoError(t, rootClient.PostJSON("/api/user/", map[string]any{
  557. "username": username,
  558. "password": password,
  559. "display_name": username,
  560. "role": role,
  561. }, 200, &resp))
  562. require.True(t, resp.Success)
  563. client, err := helpers.NewAPIClient(baseURL)
  564. require.NoError(t, err)
  565. require.NoError(t, helpers.Login(client, username, password))
  566. return client
  567. }
  568. func createManagedUsers(t *testing.T, client *helpers.APIClient, prefix string, count int, role int) {
  569. t.Helper()
  570. for i := 0; i < count; i++ {
  571. username := prefix
  572. email := fmt.Sprintf("%s@example.com", prefix)
  573. displayName := prefix
  574. if count > 1 {
  575. username = fmt.Sprintf("%s-%03d", prefix, i)
  576. email = fmt.Sprintf("%s-%03d@example.com", prefix, i)
  577. displayName = fmt.Sprintf("%s %03d", prefix, i)
  578. }
  579. var resp simpleResponse
  580. require.NoError(t, client.PostJSON("/api/user/", map[string]any{
  581. "username": username,
  582. "password": helpers.DefaultRootPassword,
  583. "display_name": displayName,
  584. "email": email,
  585. "role": role,
  586. }, 200, &resp))
  587. require.True(t, resp.Success)
  588. }
  589. }
  590. func forceRetryableState(t *testing.T, dbPath string, batchID int, itemIDs []int) {
  591. t.Helper()
  592. db, err := helpers.OpenSQLite(dbPath)
  593. require.NoError(t, err)
  594. defer func() {
  595. require.NoError(t, helpers.CloseSQLite(db))
  596. }()
  597. require.NoError(t, db.Model(&model.UserMigrationBatch{}).Where("id = ?", batchID).
  598. Update("status", model.UserMigrationBatchStatusFailed).Error)
  599. require.NoError(t, db.Model(&model.UserMigrationItem{}).Where("id IN ?", itemIDs).
  600. Updates(map[string]any{
  601. "status": model.UserMigrationItemStatusFailed,
  602. "error_message": "forced retry for e2e",
  603. }).Error)
  604. }
  605. func setBatchStatus(t *testing.T, dbPath string, batchID int, status string) {
  606. t.Helper()
  607. db, err := helpers.OpenSQLite(dbPath)
  608. require.NoError(t, err)
  609. defer func() {
  610. require.NoError(t, helpers.CloseSQLite(db))
  611. }()
  612. require.NoError(t, db.Model(&model.UserMigrationBatch{}).Where("id = ?", batchID).Update("status", status).Error)
  613. }
  614. func getBatchPage(t *testing.T, client *helpers.APIClient, batchID, page, pageSize int) *batchPageResponse {
  615. t.Helper()
  616. var resp batchPageResponse
  617. require.NoError(t, client.GetJSON(
  618. fmt.Sprintf("/api/user-migrations/batches/%d?page=%d&page_size=%d", batchID, page, pageSize),
  619. 200,
  620. &resp,
  621. ))
  622. require.True(t, resp.Success)
  623. return &resp
  624. }
  625. func convertRemoteUserToSynced(t *testing.T, ovBaseURL string, sourceUserID, remoteUserID, syncedQuota int) {
  626. t.Helper()
  627. client, err := helpers.NewAPIClient(ovBaseURL)
  628. require.NoError(t, err)
  629. var resp simpleResponse
  630. require.NoError(t, client.DoJSONWithHeaders(
  631. "POST",
  632. fmt.Sprintf("/api/internal/migration/users/%d/convert-to-synced", sourceUserID),
  633. map[string]any{
  634. "remote_user_id": remoteUserID,
  635. "synced_quota": syncedQuota,
  636. },
  637. 200,
  638. map[string]string{
  639. "X-Sync-API-Key": helpers.DefaultSyncAPIKey,
  640. "X-Sync-Node": "e2e-test",
  641. },
  642. &resp,
  643. ))
  644. require.True(t, resp.Success)
  645. }
  646. func indexItemsBySourceUserID(items []model.UserMigrationItem) map[int]*model.UserMigrationItem {
  647. indexed := make(map[int]*model.UserMigrationItem, len(items))
  648. for i := range items {
  649. indexed[items[i].SourceUserId] = &items[i]
  650. }
  651. return indexed
  652. }
  653. func decodeFlags(t *testing.T, flagsJSON string) []string {
  654. t.Helper()
  655. var flags []string
  656. require.NoError(t, common.UnmarshalJsonStr(flagsJSON, &flags))
  657. return flags
  658. }