فهرست منبع

[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
Jakob Ackermann 4 ماه پیش
والد
کامیت
5a5763884e

+ 4 - 0
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)

+ 91 - 27
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<string>}
  */
-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<void>}
- */
-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) {

+ 10 - 4
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() {