| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- import fs from 'node:fs'
- import Path from 'node:path'
- import { fileURLToPath } from 'node:url'
- import { promiseMapWithLimit } from '@overleaf/promise-utils'
- import Settings from '@overleaf/settings'
- import { connectionPromise, db } from '../app/src/infrastructure/mongodb.mjs'
- import GracefulShutdown from '../app/src/infrastructure/GracefulShutdown.mjs'
- import ProjectDeleter from '../app/src/Features/Project/ProjectDeleter.mjs'
- import SplitTestManager from '../app/src/Features/SplitTests/SplitTestManager.mjs'
- import UserDeleter from '../app/src/Features/User/UserDeleter.mjs'
- import UserRegistrationHandler from '../app/src/Features/User/UserRegistrationHandler.mjs'
- import HistoryManager from '../app/src/Features/History/HistoryManager.mjs'
- import ProjectCreationHandler from '../app/src/Features/Project/ProjectCreationHandler.mjs'
- const MONOREPO = Path.dirname(
- Path.dirname(Path.dirname(Path.dirname(fileURLToPath(import.meta.url))))
- )
- /**
- * @param {string} email
- * @return {Promise<string>}
- */
- async function createUser(email) {
- const user = await UserRegistrationHandler.promises.registerNewUser({
- email,
- password: process.env.CYPRESS_DEFAULT_PASSWORD,
- })
- const features = email.startsWith('free+')
- ? Settings.defaultFeatures
- : Settings.features.professional
- await db.users.updateOne(
- { _id: user._id },
- {
- $set: {
- // Set admin flag.
- isAdmin: email.startsWith('admin+'),
- adminRoles: email.startsWith('admin+') ? ['engineering'] : [],
- // Disable spell-checking for performance and flakiness reasons.
- 'ace.spellCheckLanguage': '',
- // Override features.
- features,
- featuresOverrides: [{ features }],
- // disable AI features
- 'aiFeatures.enabled': false,
- },
- }
- )
- return user._id.toString()
- }
- /**
- * @param {string} email
- * @return {Promise<void>}
- */
- async function deleteUser(email) {
- const user = await db.users.findOne({ email })
- if (!user) return
- // Delete the subscriptions of the user
- await db.subscriptions.deleteMany({ admin_id: user._id })
- // Soft delete the user.
- await UserDeleter.promises.deleteUser(user._id, {
- force: true,
- ipAddress: '0.0.0.0',
- })
- // Hard-delete the users projects.
- const projects = await db.deletedProjects
- .find(
- { 'deleterData.deletedProjectOwnerId': user._id },
- { projection: { 'deleterData.deletedProjectId': 1 } }
- )
- .toArray()
- await promiseMapWithLimit(
- 10,
- projects.map(p => p.deleterData.deletedProjectId),
- ProjectDeleter.promises.expireDeletedProject
- )
- // Hard-delete the user.
- await UserDeleter.promises.expireDeletedUser(user._id)
- }
- async function createProjectWithOldHistoryId(userId) {
- const projectName = 'old history id'
- const historyId = parseInt(
- await HistoryManager.promises.initializeProject(),
- 10
- )
- await ProjectCreationHandler.promises.createExampleProject(
- userId,
- projectName,
- { overleaf: { history: { id: historyId } } }
- )
- }
- /**
- * @param {string} email
- * @return {Promise<void>}
- */
- async function provisionUser(email) {
- if (!email.includes('+')) {
- throw new Error(
- `email=${email} should include the test suite name, e.g. user+project-sharing@example.com`
- )
- }
- await deleteUser(email)
- const userId = await createUser(email)
- if (email === 'user+old-history-id@example.com') {
- await createProjectWithOldHistoryId(userId)
- }
- }
- async function provisionUsers() {
- const emails = Settings.recaptcha.trustedUsers
- console.log(`> Provisioning ${emails.length} E2E users.`)
- await promiseMapWithLimit(5, emails, provisionUser)
- }
- async function purgeNewUsers() {
- const users = await db.users
- .find(
- { email: Settings.recaptcha.trustedUsersRegex },
- { projection: { email: 1 } }
- )
- .toArray()
- console.log(`> Deleting ${users.length} newly created E2E users.`)
- await promiseMapWithLimit(
- 5,
- users.map(user => user.email),
- deleteUser
- )
- }
- async function provisionSplitTests() {
- const backup = Path.join(
- MONOREPO,
- 'backup',
- 'split-tests',
- new Date().toISOString() + '.json'
- )
- console.log(
- `> 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.`
- )
- const splitTests = await SplitTestManager.getRuntimeTests()
- await fs.promises.mkdir(Path.dirname(backup), { recursive: true })
- await fs.promises.writeFile(
- backup,
- JSON.stringify(splitTests.sort((a, b) => (a.name > b.name ? 1 : -1)))
- )
- // Imported from production via https://www.overleaf.com/admin/split-test -> "Copy all split tests" -> "Copy for E2E test setup"
- const SPLIT_TESTS = JSON.parse(
- await fs.promises.readFile(
- Path.join(MONOREPO, 'tools/saas-e2e/split-tests.json')
- )
- )
- // Add WIP split test, we can update the JSON blob once this is in production
- SPLIT_TESTS.push({
- name: 'compile-from-history',
- versions: [
- {
- versionNumber: 1,
- createdAt: '2026-02-25T14:55:31.260Z',
- active: true,
- analyticsEnabled: false,
- phase: 'release',
- variants: [
- {
- name: 'enabled',
- rolloutPercent: 0,
- rolloutStripes: [],
- },
- ],
- },
- ],
- })
- console.log(`> Importing ${SPLIT_TESTS.length} split-tests from production.`)
- await SplitTestManager.replaceSplitTests(SPLIT_TESTS)
- }
- async function checkNoTableScan() {
- const client = await connectionPromise
- const { notablescan } = await client
- .db()
- .admin()
- .command({ getParameter: 1, notablescan: 1 })
- if (!notablescan) {
- console.error()
- console.error('!!! mongo is running without --notablescan')
- console.error()
- console.error('To fix this, either')
- console.error('- run "internal$ bin/e2e_test_setup"')
- console.error(
- '- or add MONGO_EXTRA_ARGS=--notablescan in config/local.env and apply with "internal$ bin/up mongo"'
- )
- console.error()
- throw new Error('mongo is running without --notablescan')
- }
- }
- async function main() {
- if (process.env.NODE_ENV !== 'development') {
- throw new Error('only available in dev-env')
- }
- await checkNoTableScan()
- await Promise.all([purgeNewUsers(), provisionUsers(), provisionSplitTests()])
- }
- await main()
- await GracefulShutdown.gracefulShutdown(
- {
- close(cb) {
- cb()
- },
- },
- 'SIGTERM'
- )
|