e2e_test_setup.mjs 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import fs from 'node:fs'
  2. import Path from 'node:path'
  3. import { fileURLToPath } from 'node:url'
  4. import { promiseMapWithLimit } from '@overleaf/promise-utils'
  5. import Settings from '@overleaf/settings'
  6. import { db } from '../app/src/infrastructure/mongodb.js'
  7. import GracefulShutdown from '../app/src/infrastructure/GracefulShutdown.js'
  8. import ProjectDeleter from '../app/src/Features/Project/ProjectDeleter.js'
  9. import SplitTestManager from '../app/src/Features/SplitTests/SplitTestManager.js'
  10. import UserDeleter from '../app/src/Features/User/UserDeleter.js'
  11. import UserRegistrationHandler from '../app/src/Features/User/UserRegistrationHandler.js'
  12. import { scriptRunner } from './lib/ScriptRunner.mjs'
  13. const MONOREPO = Path.dirname(
  14. Path.dirname(Path.dirname(Path.dirname(fileURLToPath(import.meta.url))))
  15. )
  16. /**
  17. * @param {string} email
  18. * @return {Promise<void>}
  19. */
  20. async function createUser(email) {
  21. const user = await UserRegistrationHandler.promises.registerNewUser({
  22. email,
  23. password: process.env.CYPRESS_DEFAULT_PASSWORD,
  24. })
  25. const features = email.startsWith('free@')
  26. ? Settings.defaultFeatures
  27. : Settings.features.professional
  28. await db.users.updateOne(
  29. { _id: user._id },
  30. {
  31. $set: {
  32. // Set admin flag.
  33. isAdmin: email.startsWith('admin@'),
  34. // Disable spell-checking for performance and flakiness reasons.
  35. 'ace.spellCheckLanguage': '',
  36. // Override features.
  37. features,
  38. featuresOverrides: [{ features }],
  39. },
  40. }
  41. )
  42. }
  43. /**
  44. * @param {string} email
  45. * @return {Promise<void>}
  46. */
  47. async function deleteUser(email) {
  48. const user = await db.users.findOne({ email })
  49. if (!user) return
  50. // Soft delete the user.
  51. await UserDeleter.promises.deleteUser(user._id, {
  52. force: true,
  53. ipAddress: '0.0.0.0',
  54. })
  55. // Hard-delete the users projects.
  56. const projects = await db.deletedProjects
  57. .find(
  58. { deletedProjectOwnerId: user._id },
  59. { projection: { deletedProjectId: 1 } }
  60. )
  61. .toArray()
  62. await promiseMapWithLimit(
  63. 10,
  64. projects.map(p => p.deletedProjectId),
  65. ProjectDeleter.promises.expireDeletedProject
  66. )
  67. // Hard-delete the user.
  68. await UserDeleter.promises.expireDeletedUser(user._id)
  69. }
  70. /**
  71. * @param {string} email
  72. * @return {Promise<void>}
  73. */
  74. async function provisionUser(email) {
  75. await deleteUser(email)
  76. await createUser(email)
  77. }
  78. async function provisionUsers() {
  79. const emails = Settings.recaptcha.trustedUsers
  80. console.log(`> Provisioning ${emails.length} E2E users.`)
  81. await promiseMapWithLimit(3, emails, provisionUser)
  82. }
  83. async function purgeNewUsers() {
  84. const users = await db.users
  85. .find(
  86. { email: Settings.recaptcha.trustedUsersRegex },
  87. { projection: { email: 1 } }
  88. )
  89. .toArray()
  90. console.log(`> Deleting ${users.length} newly created E2E users.`)
  91. await promiseMapWithLimit(
  92. 3,
  93. users.map(user => user.email),
  94. deleteUser
  95. )
  96. }
  97. const SPLIT_TEST_OVERRIDES = [
  98. // disable writefull, oauth registration does not work in dev-env and their banners hide our buttons.
  99. {
  100. name: 'writefull-auto-account-creation',
  101. versions: [
  102. {
  103. versionNumber: 1,
  104. phase: 'release',
  105. active: true,
  106. analyticsEnabled: false,
  107. variants: [{ name: 'enabled', rolloutPercent: 0, rolloutStripes: [] }],
  108. createdAt: new Date(),
  109. },
  110. ],
  111. },
  112. ]
  113. async function provisionSplitTests() {
  114. const backup = Path.join(
  115. MONOREPO,
  116. 'backup',
  117. 'split-tests',
  118. new Date().toISOString() + '.json'
  119. )
  120. console.log(
  121. `> Backing up previous split-tests into ${backup}. You can import them again on https://www.dev-overleaf.com/admin/split-test via the [Import] button.`
  122. )
  123. const splitTests = await SplitTestManager.getRuntimeTests()
  124. await fs.promises.mkdir(Path.dirname(backup), { recursive: true })
  125. await fs.promises.writeFile(
  126. backup,
  127. JSON.stringify(splitTests.sort((a, b) => (a.name > b.name ? 1 : -1)))
  128. )
  129. // Imported from production via https://www.overleaf.com/admin/split-test -> "Copy all split tests" -> "Copy for E2E test setup"
  130. const SPLIT_TESTS = JSON.parse(
  131. await fs.promises.readFile(
  132. Path.join(MONOREPO, 'tools/saas-e2e/split-tests.json')
  133. )
  134. )
  135. console.log(`> Importing ${SPLIT_TESTS.length} split-tests from production.`)
  136. await SplitTestManager.replaceSplitTests(SPLIT_TESTS)
  137. console.log(
  138. `> Importing ${SPLIT_TEST_OVERRIDES.length} split-tests for test compatibility.`
  139. )
  140. await SplitTestManager.mergeSplitTests(SPLIT_TEST_OVERRIDES, true)
  141. }
  142. async function main() {
  143. if (process.env.NODE_ENV !== 'development') {
  144. throw new Error('only available in dev-env')
  145. }
  146. await purgeNewUsers()
  147. await provisionUsers()
  148. await provisionSplitTests()
  149. }
  150. await scriptRunner(main)
  151. await GracefulShutdown.gracefulShutdown(
  152. {
  153. close(cb) {
  154. cb()
  155. },
  156. },
  157. 'SIGTERM'
  158. )