From 5a5763884e2d3799ddec4f8d301672cceb70743a Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Wed, 15 Apr 2026 15:15:30 +0200 Subject: [PATCH] [github] code spaces: tweaks from initial feedback (#32834) * [github] code spaces: add more standard users with a common password * [github] code spaces: automatically share dev-env with Overleaf org * [github] code spaces: merge split tests rather than replacing them * [github] code spaces: ts-check the setup script in web GitOrigin-RevId: 1a86ac7e0304d47e68290352f58a0e193eed1d77 --- .../Features/User/UserRegistrationHandler.mjs | 4 + services/web/scripts/devcontainer_setup.mjs | 118 ++++++++++++++---- services/web/scripts/e2e_test_setup.mjs | 14 ++- 3 files changed, 105 insertions(+), 31 deletions(-) diff --git a/services/web/app/src/Features/User/UserRegistrationHandler.mjs b/services/web/app/src/Features/User/UserRegistrationHandler.mjs index ab78de2bf7..0011774c70 100644 --- a/services/web/app/src/Features/User/UserRegistrationHandler.mjs +++ b/services/web/app/src/Features/User/UserRegistrationHandler.mjs @@ -39,6 +39,10 @@ const UserRegistrationHandler = { return user }, + /** + * @param {Object} userDetails + * @return {Promise<{ _id: import('mongodb-legacy').ObjectId }>} + */ async registerNewUser(userDetails) { const requestIsValid = UserRegistrationHandler._registrationRequestIsValid(userDetails) diff --git a/services/web/scripts/devcontainer_setup.mjs b/services/web/scripts/devcontainer_setup.mjs index 3e6d0bed1a..9affdb4288 100644 --- a/services/web/scripts/devcontainer_setup.mjs +++ b/services/web/scripts/devcontainer_setup.mjs @@ -1,13 +1,16 @@ -import { promiseMapWithLimit } from '@overleaf/promise-utils' +// @ts-check import Settings from '@overleaf/settings' -import { waitForDb, db } from '../app/src/infrastructure/mongodb.mjs' +import { waitForDb, db, ObjectId } from '../app/src/infrastructure/mongodb.mjs' import GracefulShutdown from '../app/src/infrastructure/GracefulShutdown.mjs' import UserRegistrationHandler from '../app/src/Features/User/UserRegistrationHandler.mjs' +import { Subscription } from '../app/src/models/Subscription.mjs' import minimist from 'minimist' import { createProjectWithOldHistoryId, provisionSplitTests, } from './e2e_test_setup.mjs' +import { Project } from '../app/src/models/Project.mjs' +import OError from '@overleaf/o-error' const { email: USER_EMAIL, password: PASSWORD } = minimist( process.argv.slice(2), @@ -16,9 +19,17 @@ const { email: USER_EMAIL, password: PASSWORD } = minimist( /** * @param {string} email + * @param {Object} opts + * @param {boolean?} opts.isAdmin + * @param {boolean?} opts.forceProfessional * @return {Promise} */ -async function createUser(email) { +async function createUser( + email, + opts = { isAdmin: false, forceProfessional: false } +) { + const { isAdmin = false, forceProfessional = false } = opts + /** @type {import('mongodb-legacy').ObjectId} */ let userId try { const user = await UserRegistrationHandler.promises.registerNewUser({ @@ -27,16 +38,19 @@ async function createUser(email) { }) userId = user._id } catch (err) { - if (err.message.includes('EmailAlreadyRegistered')) { + if ( + err instanceof OError && + err.message.includes('EmailAlreadyRegistered') && + err.info && + 'userId' in err.info && + err.info.userId instanceof ObjectId + ) { userId = err.info.userId } else { throw err } } - const features = email.startsWith('free') - ? Settings.defaultFeatures - : Settings.features.professional - const isAdmin = email === USER_EMAIL || email === 'admin@overleaf.com' + /** @type {string[]} */ let adminRoles = [] if (isAdmin) { adminRoles = ['engineering'] @@ -48,34 +62,84 @@ async function createUser(email) { // Set admin flag. isAdmin, adminRoles, - // Override features. - features, - featuresOverrides: [{ features }], // disable AI features, does not work with custom GH Code Spaces domain. 'aiFeatures.enabled': false, + // Override features. + ...(forceProfessional + ? { + features: Settings.features.professional, + featuresOverrides: [{ features: Settings.features.professional }], + } + : {}), }, } ) return userId.toString() } -/** - * @param {string} email - * @return {Promise} - */ -async function provisionUser(email) { - const userId = await createUser(email) - await createProjectWithOldHistoryId(userId) +async function provisionUsers() { + await Promise.all([ + createUser(USER_EMAIL, { isAdmin: true, forceProfessional: true }), + createUser('admin@overleaf.com', { + isAdmin: true, + forceProfessional: true, + }), + createUser('free@overleaf.com'), + createUser('premium@overleaf.com').then(async userId => { + const subscription = new Subscription({ + admin_id: userId, + member_ids: [userId], + manager_ids: [userId], + planCode: 'professional', + customAccount: true, + }) + try { + await subscription.save() + } catch (err) { + if (!isAlreadyExistsErr(err)) throw err // ignore already exists error + } + }), + createUser('group-owner@overleaf.com').then(async userId => { + const memberId = await createUser('group-member@overleaf.com') + const subscription = new Subscription({ + admin_id: userId, + member_ids: [memberId], + manager_ids: [userId], + groupPlan: true, + planCode: 'group_professional_10_enterprise', + membersLimit: 10, + teamName: 'Test Team', + customAccount: true, + }) + try { + await subscription.save() + } catch (err) { + if (!isAlreadyExistsErr(err)) throw err // ignore already exists error + } + }), + createUser('with-old-history@overleaf.com', { + isAdmin: true, + forceProfessional: true, + }).then(async userId => { + const projectName = 'old history id (Uses v1 postgres storage)' + const ownedProjects = await Project.find( + { owner_ref: userId }, + { name: true } + ).exec() + for (const project of ownedProjects) { + if (project.name === projectName) return + } + await createProjectWithOldHistoryId(userId, projectName) + }), + ]) } -async function provisionUsers() { - const emails = [ - USER_EMAIL, - 'admin@overleaf.com', - 'free@overleaf.com', - 'premium@overleaf.com', - ] - await promiseMapWithLimit(5, emails, provisionUser) +/** + * @param {unknown} err + * @return {boolean} + */ +function isAlreadyExistsErr(err) { + return err instanceof Error && 'code' in err && err.code === 11000 } async function main() { @@ -83,7 +147,7 @@ async function main() { throw new Error('only available in dev-env') } await waitForDb() - await Promise.all([provisionUsers(), provisionSplitTests()]) + await Promise.all([provisionUsers(), provisionSplitTests(true)]) } if (import.meta.main) { diff --git a/services/web/scripts/e2e_test_setup.mjs b/services/web/scripts/e2e_test_setup.mjs index 7c1a13d5fe..fe66c9e2d0 100644 --- a/services/web/scripts/e2e_test_setup.mjs +++ b/services/web/scripts/e2e_test_setup.mjs @@ -85,8 +85,10 @@ async function deleteUser(email) { await UserDeleter.promises.expireDeletedUser(user._id) } -export async function createProjectWithOldHistoryId(userId) { - const projectName = 'old history id' +export async function createProjectWithOldHistoryId( + userId, + projectName = 'old history id' +) { const historyId = parseInt( await HistoryManager.promises.initializeProject(), 10 @@ -137,7 +139,7 @@ async function purgeNewUsers() { ) } -export async function provisionSplitTests() { +export async function provisionSplitTests(merge = false) { const backup = Path.join( MONOREPO, 'backup', @@ -181,7 +183,11 @@ export async function provisionSplitTests() { ], }) console.log(`> Importing ${SPLIT_TESTS.length} split-tests from production.`) - await SplitTestManager.replaceSplitTests(SPLIT_TESTS) + if (merge) { + await SplitTestManager.mergeSplitTests(SPLIT_TESTS, false) + } else { + await SplitTestManager.replaceSplitTests(SPLIT_TESTS) + } } async function checkNoTableScan() {