e2e_test_setup.mjs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  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 { connectionPromise, db } from '../app/src/infrastructure/mongodb.mjs'
  7. import GracefulShutdown from '../app/src/infrastructure/GracefulShutdown.mjs'
  8. import ProjectDeleter from '../app/src/Features/Project/ProjectDeleter.mjs'
  9. import SplitTestManager from '../app/src/Features/SplitTests/SplitTestManager.mjs'
  10. import UserDeleter from '../app/src/Features/User/UserDeleter.mjs'
  11. import UserRegistrationHandler from '../app/src/Features/User/UserRegistrationHandler.mjs'
  12. import HistoryManager from '../app/src/Features/History/HistoryManager.mjs'
  13. import ProjectCreationHandler from '../app/src/Features/Project/ProjectCreationHandler.mjs'
  14. import crypto from 'node:crypto'
  15. const MONOREPO = Path.dirname(
  16. Path.dirname(Path.dirname(Path.dirname(fileURLToPath(import.meta.url))))
  17. )
  18. /**
  19. * @param {string} email
  20. * @return {Promise<string>}
  21. */
  22. async function createUser(email) {
  23. const user = await UserRegistrationHandler.promises.registerNewUser({
  24. email,
  25. password: process.env.CYPRESS_DEFAULT_PASSWORD,
  26. analyticsId: crypto.randomUUID(),
  27. })
  28. const features = email.startsWith('free+')
  29. ? Settings.defaultFeatures
  30. : Settings.features.professional
  31. const isAdmin = email.startsWith('admin+')
  32. let adminRoles = []
  33. if (email.startsWith('admin+finance')) {
  34. adminRoles = ['finance']
  35. } else if (isAdmin) {
  36. adminRoles = ['engineering']
  37. }
  38. await db.users.updateOne(
  39. { _id: user._id },
  40. {
  41. $set: {
  42. // Set admin flag.
  43. isAdmin,
  44. adminRoles,
  45. // Disable spell-checking for performance and flakiness reasons.
  46. 'ace.spellCheckLanguage': '',
  47. // Override features.
  48. features,
  49. featuresOverrides: [{ features }],
  50. // disable AI features
  51. 'aiFeatures.enabled': false,
  52. },
  53. }
  54. )
  55. return user._id.toString()
  56. }
  57. /**
  58. * @param {string} email
  59. * @return {Promise<void>}
  60. */
  61. async function deleteUser(email) {
  62. const user = await db.users.findOne({ email })
  63. if (!user) return
  64. // Delete the subscriptions of the user
  65. await db.subscriptions.deleteMany({ admin_id: user._id })
  66. // Soft delete the user.
  67. await UserDeleter.promises.deleteUser(user._id, {
  68. force: true,
  69. ipAddress: '0.0.0.0',
  70. })
  71. // Hard-delete the users projects.
  72. const projects = await db.deletedProjects
  73. .find(
  74. { 'deleterData.deletedProjectOwnerId': user._id },
  75. { projection: { 'deleterData.deletedProjectId': 1 } }
  76. )
  77. .toArray()
  78. await promiseMapWithLimit(
  79. 10,
  80. projects.map(p => p.deleterData.deletedProjectId),
  81. ProjectDeleter.promises.expireDeletedProject
  82. )
  83. // Hard-delete the user.
  84. await UserDeleter.promises.expireDeletedUser(user._id)
  85. }
  86. export async function createProjectWithOldHistoryId(
  87. userId,
  88. projectName = 'old history id'
  89. ) {
  90. const historyId = parseInt(
  91. await HistoryManager.promises.initializeProject(),
  92. 10
  93. )
  94. await ProjectCreationHandler.promises.createExampleProject(
  95. userId,
  96. projectName,
  97. { overleaf: { history: { id: historyId } } }
  98. )
  99. }
  100. /**
  101. * @param {string} email
  102. * @return {Promise<void>}
  103. */
  104. async function provisionUser(email) {
  105. if (!email.includes('+')) {
  106. throw new Error(
  107. `email=${email} should include the test suite name, e.g. user+project-sharing@example.com`
  108. )
  109. }
  110. await deleteUser(email)
  111. const userId = await createUser(email)
  112. if (email === 'user+old-history-id@example.com') {
  113. await createProjectWithOldHistoryId(userId)
  114. }
  115. }
  116. async function provisionUsers() {
  117. const emails = Settings.recaptcha.trustedUsers
  118. console.log(`> Provisioning ${emails.length} E2E users.`)
  119. await promiseMapWithLimit(5, emails, provisionUser)
  120. }
  121. async function purgeNewUsers() {
  122. const users = await db.users
  123. .find(
  124. { email: Settings.recaptcha.trustedUsersRegex },
  125. { projection: { email: 1 } }
  126. )
  127. .toArray()
  128. console.log(`> Deleting ${users.length} newly created E2E users.`)
  129. await promiseMapWithLimit(
  130. 5,
  131. users.map(user => user.email),
  132. deleteUser
  133. )
  134. }
  135. export async function provisionSplitTests(merge = false, extraSplitTests = []) {
  136. const backup = Path.join(
  137. MONOREPO,
  138. 'backup',
  139. 'split-tests',
  140. new Date().toISOString() + '.json'
  141. )
  142. console.log(
  143. `> 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.`
  144. )
  145. const splitTests = await SplitTestManager.getRuntimeTests()
  146. await fs.promises.mkdir(Path.dirname(backup), { recursive: true })
  147. await fs.promises.writeFile(
  148. backup,
  149. JSON.stringify(splitTests.sort((a, b) => (a.name > b.name ? 1 : -1)))
  150. )
  151. // Imported from production via https://www.overleaf.com/admin/split-test -> "Copy all split tests" -> "Copy for E2E test setup"
  152. const SPLIT_TESTS = JSON.parse(
  153. await fs.promises.readFile(
  154. Path.join(MONOREPO, 'tools/saas-e2e/split-tests.json'),
  155. 'utf-8'
  156. )
  157. )
  158. // Add WIP split test, we can update the JSON blob once this is in production
  159. SPLIT_TESTS.push({
  160. name: 'zip-from-history',
  161. versions: [
  162. {
  163. versionNumber: 1,
  164. createdAt: '2026-02-25T14:55:31.260Z',
  165. active: true,
  166. analyticsEnabled: false,
  167. phase: 'release',
  168. variants: [
  169. {
  170. name: 'enabled',
  171. rolloutPercent: 0,
  172. rolloutStripes: [],
  173. },
  174. ],
  175. },
  176. ],
  177. })
  178. console.log(`> Importing ${SPLIT_TESTS.length} split-tests from production.`)
  179. if (merge) {
  180. await SplitTestManager.mergeSplitTests(SPLIT_TESTS, false)
  181. } else {
  182. await SplitTestManager.replaceSplitTests(SPLIT_TESTS)
  183. }
  184. if (extraSplitTests.length > 0) {
  185. await SplitTestManager.mergeSplitTests(extraSplitTests, false)
  186. }
  187. }
  188. async function checkNoTableScan() {
  189. const client = await connectionPromise
  190. const { notablescan } = await client
  191. .db()
  192. .admin()
  193. .command({ getParameter: 1, notablescan: 1 })
  194. if (!notablescan) {
  195. console.error()
  196. console.error('!!! mongo is running without --notablescan')
  197. console.error()
  198. console.error('To fix this, either')
  199. console.error('- run "internal$ bin/e2e_test_setup"')
  200. console.error(
  201. '- or add MONGO_EXTRA_ARGS=--notablescan in config/local.env and apply with "internal$ bin/up mongo"'
  202. )
  203. console.error()
  204. throw new Error('mongo is running without --notablescan')
  205. }
  206. }
  207. async function main() {
  208. if (process.env.NODE_ENV !== 'development') {
  209. throw new Error('only available in dev-env')
  210. }
  211. await checkNoTableScan()
  212. await Promise.all([purgeNewUsers(), provisionUsers(), provisionSplitTests()])
  213. }
  214. if (import.meta.main) {
  215. await main()
  216. await GracefulShutdown.gracefulShutdown()
  217. }