e2e_test_setup.mjs 6.4 KB

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