فهرست منبع

Convert tests to ESM

GitOrigin-RevId: 20585e01dee90e691476a0d47fd5c63b0412e4a6
Andrew Rumble 10 ماه پیش
والد
کامیت
b7c883ac38
37فایلهای تغییر یافته به همراه7326 افزوده شده و 6533 حذف شده
  1. 1 1
      services/web/modules/launchpad/test/unit/src/LaunchpadController.test.mjs
  2. 739 626
      services/web/test/unit/src/Authentication/AuthenticationController.test.mjs
  3. 332 291
      services/web/test/unit/src/Authorization/AuthorizationManager.test.mjs
  4. 4 7
      services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs
  5. 260 240
      services/web/test/unit/src/Collaborators/CollaboratorsGetter.test.mjs
  6. 359 321
      services/web/test/unit/src/Collaborators/CollaboratorsHandler.test.mjs
  7. 6 3
      services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs
  8. 351 350
      services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandler.test.mjs
  9. 6 3
      services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs
  10. 542 539
      services/web/test/unit/src/Editor/EditorController.test.mjs
  11. 6 3
      services/web/test/unit/src/Editor/EditorHttpController.test.mjs
  12. 151 132
      services/web/test/unit/src/FileStore/FileStoreHandler.test.mjs
  13. 165 167
      services/web/test/unit/src/HelperFiles/AdminAuthorizationHelper.test.mjs
  14. 6 3
      services/web/test/unit/src/History/HistoryController.test.mjs
  15. 8 6
      services/web/test/unit/src/History/HistoryManagerTests.mjs
  16. 51 48
      services/web/test/unit/src/History/RestoreManager.test.mjs
  17. 375 315
      services/web/test/unit/src/Project/ProjectDeleter.test.mjs
  18. 272 246
      services/web/test/unit/src/Project/ProjectDetailsHandler.test.mjs
  19. 207 186
      services/web/test/unit/src/Project/ProjectEntityHandler.test.mjs
  20. 319 282
      services/web/test/unit/src/Project/ProjectEntityMongoUpdateHandler.test.mjs
  21. 360 299
      services/web/test/unit/src/Project/ProjectEntityUpdateHandler.test.mjs
  22. 221 213
      services/web/test/unit/src/Project/ProjectGetter.test.mjs
  23. 8 5
      services/web/test/unit/src/Project/ProjectHistoryHandler.test.mjs
  24. 7 4
      services/web/test/unit/src/Project/ProjectListController.test.mjs
  25. 133 135
      services/web/test/unit/src/Project/ProjectLocator.test.mjs
  26. 315 304
      services/web/test/unit/src/Project/ProjectRootDocManager.test.mjs
  27. 296 259
      services/web/test/unit/src/Subscription/LimitationsManager.test.mjs
  28. 365 323
      services/web/test/unit/src/Subscription/SubscriptionHandler.test.mjs
  29. 303 244
      services/web/test/unit/src/Subscription/TeamInvitesHandler.test.mjs
  30. 155 140
      services/web/test/unit/src/Templates/TemplatesManager.test.mjs
  31. 136 118
      services/web/test/unit/src/ThirdPartyDataStore/TpdsProjectFlusher.test.mjs
  32. 111 93
      services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateSender.test.mjs
  33. 11 15
      services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs
  34. 123 118
      services/web/test/unit/src/Uploads/FileSystemImportManager.test.mjs
  35. 240 178
      services/web/test/unit/src/Uploads/ProjectUploadManager.test.mjs
  36. 355 295
      services/web/test/unit/src/User/UserDeleter.test.mjs
  37. 27 21
      services/web/test/unit/src/User/UserHandler.test.mjs

+ 1 - 1
services/web/modules/launchpad/test/unit/src/LaunchpadController.test.mjs

@@ -1,4 +1,4 @@
-import { expect, vi } from 'vitest'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
 import * as path from 'node:path'
 import * as path from 'node:path'
 import sinon from 'sinon'
 import sinon from 'sinon'
 import MockResponse from '../../../../../test/unit/src/helpers/MockResponse.js'
 import MockResponse from '../../../../../test/unit/src/helpers/MockResponse.js'

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 739 - 626
services/web/test/unit/src/Authentication/AuthenticationController.test.mjs


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 332 - 291
services/web/test/unit/src/Authorization/AuthorizationManager.test.mjs


+ 4 - 7
services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs

@@ -476,13 +476,10 @@ describe('CollaboratorsController', function () {
     })
     })
 
 
     it('returns 204 on success', async function (ctx) {
     it('returns 204 on success', async function (ctx) {
-      await new Promise(resolve => {
-        ctx.res.sendStatus = status => {
-          expect(status).to.equal(204)
-          resolve()
-        }
-        ctx.CollaboratorsController.transferOwnership(ctx.req, ctx.res)
-      })
+      ctx.res.sendStatus = vi.fn()
+
+      await ctx.CollaboratorsController.transferOwnership(ctx.req, ctx.res)
+      expect(ctx.res.sendStatus).toHaveBeenCalledWith(204)
     })
     })
 
 
     it('returns 404 if the project does not exist', async function (ctx) {
     it('returns 404 if the project does not exist', async function (ctx) {

+ 260 - 240
services/web/test/unit/src/Collaborators/CollaboratorsGetter.test.mjs

@@ -1,46 +1,53 @@
-const Path = require('path')
-const SandboxedModule = require('sandboxed-module')
-const sinon = require('sinon')
-const { expect } = require('chai')
-const { ObjectId } = require('mongodb-legacy')
-const { Project } = require('../helpers/models/Project')
-const Errors = require('../../../../app/src/Features/Errors/Errors')
+import { vi, expect } from 'vitest'
+import Path from 'path'
+import sinon from 'sinon'
+import mongodb from 'mongodb-legacy'
+import indirectlyImportModels from '../helpers/indirectlyImportModels.js'
+import Errors from '../../../../app/src/Features/Errors/Errors.js'
+
+const { Project } = indirectlyImportModels(['Project'])
+
+const { ObjectId } = mongodb
+
+vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
+  vi.importActual('../../../../app/src/Features/Errors/Errors.js')
+)
 
 
 const MODULE_PATH = Path.join(
 const MODULE_PATH = Path.join(
-  __dirname,
+  import.meta.dirname,
   '../../../../app/src/Features/Collaborators/CollaboratorsGetter'
   '../../../../app/src/Features/Collaborators/CollaboratorsGetter'
 )
 )
 
 
 describe('CollaboratorsGetter', function () {
 describe('CollaboratorsGetter', function () {
-  beforeEach(function () {
-    this.userId = 'efb93a186e9a06f15fea5abd'
-    this.ownerRef = new ObjectId()
-    this.readOnlyRef1 = new ObjectId()
-    this.readOnlyRef2 = new ObjectId()
-    this.pendingEditorRef = new ObjectId()
-    this.pendingReviewerRef = new ObjectId()
-    this.readWriteRef1 = new ObjectId()
-    this.readWriteRef2 = new ObjectId()
-    this.reviewer1Ref = new ObjectId()
-    this.reviewer2Ref = new ObjectId()
-    this.readOnlyTokenRef = new ObjectId()
-    this.readWriteTokenRef = new ObjectId()
-    this.nonMemberRef = new ObjectId()
-    this.project = {
+  beforeEach(async function (ctx) {
+    ctx.userId = 'efb93a186e9a06f15fea5abd'
+    ctx.ownerRef = new ObjectId()
+    ctx.readOnlyRef1 = new ObjectId()
+    ctx.readOnlyRef2 = new ObjectId()
+    ctx.pendingEditorRef = new ObjectId()
+    ctx.pendingReviewerRef = new ObjectId()
+    ctx.readWriteRef1 = new ObjectId()
+    ctx.readWriteRef2 = new ObjectId()
+    ctx.reviewer1Ref = new ObjectId()
+    ctx.reviewer2Ref = new ObjectId()
+    ctx.readOnlyTokenRef = new ObjectId()
+    ctx.readWriteTokenRef = new ObjectId()
+    ctx.nonMemberRef = new ObjectId()
+    ctx.project = {
       _id: new ObjectId(),
       _id: new ObjectId(),
-      owner_ref: [this.ownerRef],
+      owner_ref: [ctx.ownerRef],
       readOnly_refs: [
       readOnly_refs: [
-        this.readOnlyRef1,
-        this.readOnlyRef2,
-        this.pendingEditorRef,
-        this.pendingReviewerRef,
+        ctx.readOnlyRef1,
+        ctx.readOnlyRef2,
+        ctx.pendingEditorRef,
+        ctx.pendingReviewerRef,
       ],
       ],
-      pendingEditor_refs: [this.pendingEditorRef],
-      pendingReviewer_refs: [this.pendingReviewerRef],
-      collaberator_refs: [this.readWriteRef1, this.readWriteRef2],
-      reviewer_refs: [this.reviewer1Ref, this.reviewer2Ref],
-      tokenAccessReadAndWrite_refs: [this.readWriteTokenRef],
-      tokenAccessReadOnly_refs: [this.readOnlyTokenRef],
+      pendingEditor_refs: [ctx.pendingEditorRef],
+      pendingReviewer_refs: [ctx.pendingReviewerRef],
+      collaberator_refs: [ctx.readWriteRef1, ctx.readWriteRef2],
+      reviewer_refs: [ctx.reviewer1Ref, ctx.reviewer2Ref],
+      tokenAccessReadAndWrite_refs: [ctx.readWriteTokenRef],
+      tokenAccessReadOnly_refs: [ctx.readOnlyTokenRef],
       publicAccesLevel: 'tokenBased',
       publicAccesLevel: 'tokenBased',
       tokens: {
       tokens: {
         readOnly: 'ro',
         readOnly: 'ro',
@@ -49,98 +56,114 @@ describe('CollaboratorsGetter', function () {
       },
       },
     }
     }
 
 
-    this.UserGetter = {
+    ctx.UserGetter = {
       promises: {
       promises: {
         getUser: sinon.stub().resolves(null),
         getUser: sinon.stub().resolves(null),
         getUsers: sinon.stub().resolves([]),
         getUsers: sinon.stub().resolves([]),
       },
       },
     }
     }
-    this.ProjectMock = sinon.mock(Project)
-    this.ProjectGetter = {
+    ctx.ProjectMock = sinon.mock(Project)
+    ctx.ProjectGetter = {
       promises: {
       promises: {
-        getProject: sinon.stub().resolves(this.project),
+        getProject: sinon.stub().resolves(ctx.project),
       },
       },
     }
     }
-    this.ProjectEditorHandler = {
+    ctx.ProjectEditorHandler = {
       buildUserModelView: sinon.stub(),
       buildUserModelView: sinon.stub(),
     }
     }
-    this.CollaboratorsGetter = SandboxedModule.require(MODULE_PATH, {
-      requires: {
-        'mongodb-legacy': { ObjectId },
-        '../User/UserGetter': this.UserGetter,
-        '../../models/Project': { Project },
-        '../Project/ProjectGetter': this.ProjectGetter,
-        '../Project/ProjectEditorHandler': this.ProjectEditorHandler,
-      },
-    })
+
+    vi.doMock('mongodb-legacy', () => ({
+      default: { ObjectId },
+    }))
+
+    vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
+      default: ctx.UserGetter,
+    }))
+
+    vi.doMock('../../../../app/src/models/Project', () => ({
+      Project,
+    }))
+
+    vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
+      default: ctx.ProjectGetter,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectEditorHandler',
+      () => ({
+        default: ctx.ProjectEditorHandler,
+      })
+    )
+
+    ctx.CollaboratorsGetter = (await import(MODULE_PATH)).default
   })
   })
 
 
-  afterEach(function () {
-    this.ProjectMock.verify()
+  afterEach(function (ctx) {
+    ctx.ProjectMock.verify()
   })
   })
 
 
   describe('getMemberIdsWithPrivilegeLevels', function () {
   describe('getMemberIdsWithPrivilegeLevels', function () {
     describe('with project', function () {
     describe('with project', function () {
-      it('should return an array of member ids with their privilege levels', async function () {
+      it('should return an array of member ids with their privilege levels', async function (ctx) {
         const result =
         const result =
-          await this.CollaboratorsGetter.promises.getMemberIdsWithPrivilegeLevels(
-            this.project._id
+          await ctx.CollaboratorsGetter.promises.getMemberIdsWithPrivilegeLevels(
+            ctx.project._id
           )
           )
         expect(result).to.have.deep.members([
         expect(result).to.have.deep.members([
           {
           {
-            id: this.ownerRef.toString(),
+            id: ctx.ownerRef.toString(),
             privilegeLevel: 'owner',
             privilegeLevel: 'owner',
             source: 'owner',
             source: 'owner',
           },
           },
           {
           {
-            id: this.readWriteRef1.toString(),
+            id: ctx.readWriteRef1.toString(),
             privilegeLevel: 'readAndWrite',
             privilegeLevel: 'readAndWrite',
             source: 'invite',
             source: 'invite',
           },
           },
           {
           {
-            id: this.readWriteRef2.toString(),
+            id: ctx.readWriteRef2.toString(),
             privilegeLevel: 'readAndWrite',
             privilegeLevel: 'readAndWrite',
             source: 'invite',
             source: 'invite',
           },
           },
           {
           {
-            id: this.readOnlyRef1.toString(),
+            id: ctx.readOnlyRef1.toString(),
             privilegeLevel: 'readOnly',
             privilegeLevel: 'readOnly',
             source: 'invite',
             source: 'invite',
           },
           },
           {
           {
-            id: this.readOnlyRef2.toString(),
+            id: ctx.readOnlyRef2.toString(),
             privilegeLevel: 'readOnly',
             privilegeLevel: 'readOnly',
             source: 'invite',
             source: 'invite',
           },
           },
           {
           {
-            id: this.pendingEditorRef.toString(),
+            id: ctx.pendingEditorRef.toString(),
             privilegeLevel: 'readOnly',
             privilegeLevel: 'readOnly',
             source: 'invite',
             source: 'invite',
             pendingEditor: true,
             pendingEditor: true,
           },
           },
           {
           {
-            id: this.pendingReviewerRef.toString(),
+            id: ctx.pendingReviewerRef.toString(),
             privilegeLevel: 'readOnly',
             privilegeLevel: 'readOnly',
             source: 'invite',
             source: 'invite',
             pendingReviewer: true,
             pendingReviewer: true,
           },
           },
           {
           {
-            id: this.readOnlyTokenRef.toString(),
+            id: ctx.readOnlyTokenRef.toString(),
             privilegeLevel: 'readOnly',
             privilegeLevel: 'readOnly',
             source: 'token',
             source: 'token',
           },
           },
           {
           {
-            id: this.readWriteTokenRef.toString(),
+            id: ctx.readWriteTokenRef.toString(),
             privilegeLevel: 'readAndWrite',
             privilegeLevel: 'readAndWrite',
             source: 'token',
             source: 'token',
           },
           },
           {
           {
-            id: this.reviewer1Ref.toString(),
+            id: ctx.reviewer1Ref.toString(),
             privilegeLevel: 'review',
             privilegeLevel: 'review',
             source: 'invite',
             source: 'invite',
           },
           },
           {
           {
-            id: this.reviewer2Ref.toString(),
+            id: ctx.reviewer2Ref.toString(),
             privilegeLevel: 'review',
             privilegeLevel: 'review',
             source: 'invite',
             source: 'invite',
           },
           },
@@ -149,14 +172,14 @@ describe('CollaboratorsGetter', function () {
     })
     })
 
 
     describe('with a missing project', function () {
     describe('with a missing project', function () {
-      beforeEach(function () {
-        this.ProjectGetter.promises.getProject.resolves(null)
+      beforeEach(function (ctx) {
+        ctx.ProjectGetter.promises.getProject.resolves(null)
       })
       })
 
 
-      it('should return a NotFoundError', async function () {
+      it('should return a NotFoundError', async function (ctx) {
         await expect(
         await expect(
-          this.CollaboratorsGetter.promises.getMemberIdsWithPrivilegeLevels(
-            this.project._id
+          ctx.CollaboratorsGetter.promises.getMemberIdsWithPrivilegeLevels(
+            ctx.project._id
           )
           )
         ).to.be.rejectedWith(Errors.NotFoundError)
         ).to.be.rejectedWith(Errors.NotFoundError)
       })
       })
@@ -164,89 +187,89 @@ describe('CollaboratorsGetter', function () {
   })
   })
 
 
   describe('getMemberIds', function () {
   describe('getMemberIds', function () {
-    it('should return the ids', async function () {
-      const memberIds = await this.CollaboratorsGetter.promises.getMemberIds(
-        this.project._id
+    it('should return the ids', async function (ctx) {
+      const memberIds = await ctx.CollaboratorsGetter.promises.getMemberIds(
+        ctx.project._id
       )
       )
       expect(memberIds).to.have.members([
       expect(memberIds).to.have.members([
-        this.ownerRef.toString(),
-        this.readOnlyRef1.toString(),
-        this.readOnlyRef2.toString(),
-        this.readWriteRef1.toString(),
-        this.readWriteRef2.toString(),
-        this.pendingEditorRef.toString(),
-        this.pendingReviewerRef.toString(),
-        this.readWriteTokenRef.toString(),
-        this.readOnlyTokenRef.toString(),
-        this.reviewer1Ref.toString(),
-        this.reviewer2Ref.toString(),
+        ctx.ownerRef.toString(),
+        ctx.readOnlyRef1.toString(),
+        ctx.readOnlyRef2.toString(),
+        ctx.readWriteRef1.toString(),
+        ctx.readWriteRef2.toString(),
+        ctx.pendingEditorRef.toString(),
+        ctx.pendingReviewerRef.toString(),
+        ctx.readWriteTokenRef.toString(),
+        ctx.readOnlyTokenRef.toString(),
+        ctx.reviewer1Ref.toString(),
+        ctx.reviewer2Ref.toString(),
       ])
       ])
     })
     })
   })
   })
 
 
   describe('getInvitedMemberIds', function () {
   describe('getInvitedMemberIds', function () {
-    it('should return the invited ids', async function () {
+    it('should return the invited ids', async function (ctx) {
       const memberIds =
       const memberIds =
-        await this.CollaboratorsGetter.promises.getInvitedMemberIds(
-          this.project._id
+        await ctx.CollaboratorsGetter.promises.getInvitedMemberIds(
+          ctx.project._id
         )
         )
       expect(memberIds).to.have.members([
       expect(memberIds).to.have.members([
-        this.ownerRef.toString(),
-        this.readOnlyRef1.toString(),
-        this.readOnlyRef2.toString(),
-        this.readWriteRef1.toString(),
-        this.readWriteRef2.toString(),
-        this.pendingEditorRef.toString(),
-        this.pendingReviewerRef.toString(),
-        this.reviewer1Ref.toString(),
-        this.reviewer2Ref.toString(),
+        ctx.ownerRef.toString(),
+        ctx.readOnlyRef1.toString(),
+        ctx.readOnlyRef2.toString(),
+        ctx.readWriteRef1.toString(),
+        ctx.readWriteRef2.toString(),
+        ctx.pendingEditorRef.toString(),
+        ctx.pendingReviewerRef.toString(),
+        ctx.reviewer1Ref.toString(),
+        ctx.reviewer2Ref.toString(),
       ])
       ])
     })
     })
   })
   })
 
 
   describe('getMemberIdPrivilegeLevel', function () {
   describe('getMemberIdPrivilegeLevel', function () {
-    it('should return the privilege level if it exists', async function () {
+    it('should return the privilege level if it exists', async function (ctx) {
       const level =
       const level =
-        await this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
-          this.readOnlyRef1,
-          this.project._id
+        await ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
+          ctx.readOnlyRef1,
+          ctx.project._id
         )
         )
       expect(level).to.equal('readOnly')
       expect(level).to.equal('readOnly')
     })
     })
 
 
-    it('should return review privilege level', async function () {
+    it('should return review privilege level', async function (ctx) {
       const level =
       const level =
-        await this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
-          this.reviewer1Ref,
-          this.project._id
+        await ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
+          ctx.reviewer1Ref,
+          ctx.project._id
         )
         )
       expect(level).to.equal('review')
       expect(level).to.equal('review')
     })
     })
 
 
-    it('should return false if the member has no privilege level', async function () {
+    it('should return false if the member has no privilege level', async function (ctx) {
       const level =
       const level =
-        await this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
-          this.nonMemberRef,
-          this.project._id
+        await ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
+          ctx.nonMemberRef,
+          ctx.project._id
         )
         )
       expect(level).to.equal(false)
       expect(level).to.equal(false)
     })
     })
 
 
-    it('should return review privilege level when user is both reviewer and token member', async function () {
+    it('should return review privilege level when user is both reviewer and token member', async function (ctx) {
       const userWhoIsBothReviewerAndToken = new ObjectId()
       const userWhoIsBothReviewerAndToken = new ObjectId()
 
 
       const projectWithDuplicateUser = {
       const projectWithDuplicateUser = {
-        ...this.project,
+        ...ctx.project,
         reviewer_refs: [userWhoIsBothReviewerAndToken],
         reviewer_refs: [userWhoIsBothReviewerAndToken],
         tokenAccessReadAndWrite_refs: [userWhoIsBothReviewerAndToken],
         tokenAccessReadAndWrite_refs: [userWhoIsBothReviewerAndToken],
       }
       }
 
 
-      this.ProjectGetter.promises.getProject.resolves(projectWithDuplicateUser)
+      ctx.ProjectGetter.promises.getProject.resolves(projectWithDuplicateUser)
 
 
       const level =
       const level =
-        await this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
+        await ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
           userWhoIsBothReviewerAndToken,
           userWhoIsBothReviewerAndToken,
-          this.project._id
+          ctx.project._id
         )
         )
 
 
       expect(level).to.equal('review')
       expect(level).to.equal('review')
@@ -255,20 +278,20 @@ describe('CollaboratorsGetter', function () {
 
 
   describe('isUserInvitedMemberOfProject', function () {
   describe('isUserInvitedMemberOfProject', function () {
     describe('when user is a member of the project', function () {
     describe('when user is a member of the project', function () {
-      it('should return true and the privilegeLevel', async function () {
+      it('should return true and the privilegeLevel', async function (ctx) {
         const isMember =
         const isMember =
-          await this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject(
-            this.readOnlyRef1
+          await ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject(
+            ctx.readOnlyRef1
           )
           )
         expect(isMember).to.equal(true)
         expect(isMember).to.equal(true)
       })
       })
     })
     })
 
 
     describe('when user is not a member of the project', function () {
     describe('when user is not a member of the project', function () {
-      it('should return false', async function () {
+      it('should return false', async function (ctx) {
         const isMember =
         const isMember =
-          await this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject(
-            this.nonMemberRef
+          await ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject(
+            ctx.nonMemberRef
           )
           )
         expect(isMember).to.equal(false)
         expect(isMember).to.equal(false)
       })
       })
@@ -277,30 +300,30 @@ describe('CollaboratorsGetter', function () {
 
 
   describe('isUserInvitedReadWriteMemberOfProject', function () {
   describe('isUserInvitedReadWriteMemberOfProject', function () {
     describe('when user is a read write member of the project', function () {
     describe('when user is a read write member of the project', function () {
-      it('should return true', async function () {
+      it('should return true', async function (ctx) {
         const isMember =
         const isMember =
-          await this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
-            this.readWriteRef1
+          await ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
+            ctx.readWriteRef1
           )
           )
         expect(isMember).to.equal(true)
         expect(isMember).to.equal(true)
       })
       })
     })
     })
 
 
     describe('when user is a read only member of the project', function () {
     describe('when user is a read only member of the project', function () {
-      it('should return false', async function () {
+      it('should return false', async function (ctx) {
         const isMember =
         const isMember =
-          await this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
-            this.readOnlyRef1
+          await ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
+            ctx.readOnlyRef1
           )
           )
         expect(isMember).to.equal(false)
         expect(isMember).to.equal(false)
       })
       })
     })
     })
 
 
     describe('when user is not a member of the project', function () {
     describe('when user is not a member of the project', function () {
-      it('should return false', async function () {
+      it('should return false', async function (ctx) {
         const isMember =
         const isMember =
-          await this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
-            this.nonMemberRef
+          await ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
+            ctx.nonMemberRef
           )
           )
         expect(isMember).to.equal(false)
         expect(isMember).to.equal(false)
       })
       })
@@ -308,42 +331,42 @@ describe('CollaboratorsGetter', function () {
   })
   })
 
 
   describe('getProjectsUserIsMemberOf', function () {
   describe('getProjectsUserIsMemberOf', function () {
-    beforeEach(function () {
-      this.fields = 'mock fields'
-      this.ProjectMock.expects('find')
-        .withArgs({ collaberator_refs: this.userId }, this.fields)
+    beforeEach(function (ctx) {
+      ctx.fields = 'mock fields'
+      ctx.ProjectMock.expects('find')
+        .withArgs({ collaberator_refs: ctx.userId }, ctx.fields)
         .chain('exec')
         .chain('exec')
         .resolves(['mock-read-write-project-1', 'mock-read-write-project-2'])
         .resolves(['mock-read-write-project-1', 'mock-read-write-project-2'])
 
 
-      this.ProjectMock.expects('find')
-        .withArgs({ readOnly_refs: this.userId }, this.fields)
+      ctx.ProjectMock.expects('find')
+        .withArgs({ readOnly_refs: ctx.userId }, ctx.fields)
         .chain('exec')
         .chain('exec')
         .resolves(['mock-read-only-project-1', 'mock-read-only-project-2'])
         .resolves(['mock-read-only-project-1', 'mock-read-only-project-2'])
 
 
-      this.ProjectMock.expects('find')
-        .withArgs({ reviewer_refs: this.userId }, this.fields)
+      ctx.ProjectMock.expects('find')
+        .withArgs({ reviewer_refs: ctx.userId }, ctx.fields)
         .chain('exec')
         .chain('exec')
         .resolves(['mock-review-project-1', 'mock-review-project-2'])
         .resolves(['mock-review-project-1', 'mock-review-project-2'])
-      this.ProjectMock.expects('find')
+      ctx.ProjectMock.expects('find')
         .withArgs(
         .withArgs(
           {
           {
-            tokenAccessReadAndWrite_refs: this.userId,
+            tokenAccessReadAndWrite_refs: ctx.userId,
             publicAccesLevel: 'tokenBased',
             publicAccesLevel: 'tokenBased',
           },
           },
-          this.fields
+          ctx.fields
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves([
         .resolves([
           'mock-token-read-write-project-1',
           'mock-token-read-write-project-1',
           'mock-token-read-write-project-2',
           'mock-token-read-write-project-2',
         ])
         ])
-      this.ProjectMock.expects('find')
+      ctx.ProjectMock.expects('find')
         .withArgs(
         .withArgs(
           {
           {
-            tokenAccessReadOnly_refs: this.userId,
+            tokenAccessReadOnly_refs: ctx.userId,
             publicAccesLevel: 'tokenBased',
             publicAccesLevel: 'tokenBased',
           },
           },
-          this.fields
+          ctx.fields
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves([
         .resolves([
@@ -352,11 +375,11 @@ describe('CollaboratorsGetter', function () {
         ])
         ])
     })
     })
 
 
-    it('should call the callback with the projects', async function () {
+    it('should call the callback with the projects', async function (ctx) {
       const projects =
       const projects =
-        await this.CollaboratorsGetter.promises.getProjectsUserIsMemberOf(
-          this.userId,
-          this.fields
+        await ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf(
+          ctx.userId,
+          ctx.fields
         )
         )
       expect(projects).to.deep.equal({
       expect(projects).to.deep.equal({
         readAndWrite: [
         readAndWrite: [
@@ -378,101 +401,98 @@ describe('CollaboratorsGetter', function () {
   })
   })
 
 
   describe('getAllInvitedMembers', function () {
   describe('getAllInvitedMembers', function () {
-    beforeEach(async function () {
-      this.owningUser = {
-        _id: this.ownerRef,
+    beforeEach(async function (ctx) {
+      ctx.owningUser = {
+        _id: ctx.ownerRef,
         email: 'owner@example.com',
         email: 'owner@example.com',
         features: { a: 1 },
         features: { a: 1 },
       }
       }
-      this.readWriteUser = {
-        _id: this.readWriteRef1,
+      ctx.readWriteUser = {
+        _id: ctx.readWriteRef1,
         email: 'readwrite@example.com',
         email: 'readwrite@example.com',
       }
       }
-      this.reviewUser = {
-        _id: this.reviewer1Ref,
+      ctx.reviewUser = {
+        _id: ctx.reviewer1Ref,
         email: 'review@example.com',
         email: 'review@example.com',
       }
       }
-      this.members = [
-        { user: this.owningUser, privilegeLevel: 'owner' },
-        { user: this.readWriteUser, privilegeLevel: 'readAndWrite' },
-        { user: this.reviewUser, privilegeLevel: 'review' },
+      ctx.members = [
+        { user: ctx.owningUser, privilegeLevel: 'owner' },
+        { user: ctx.readWriteUser, privilegeLevel: 'readAndWrite' },
+        { user: ctx.reviewUser, privilegeLevel: 'review' },
       ]
       ]
-      this.memberViews = [
-        { _id: this.readWriteUser._id, email: this.readWriteUser.email },
-        { _id: this.reviewUser._id, email: this.reviewUser.email },
+      ctx.memberViews = [
+        { _id: ctx.readWriteUser._id, email: ctx.readWriteUser.email },
+        { _id: ctx.reviewUser._id, email: ctx.reviewUser.email },
       ]
       ]
-      this.UserGetter.promises.getUsers.resolves([
-        this.owningUser,
-        this.readWriteUser,
-        this.reviewUser,
+      ctx.UserGetter.promises.getUsers.resolves([
+        ctx.owningUser,
+        ctx.readWriteUser,
+        ctx.reviewUser,
       ])
       ])
-      this.ProjectEditorHandler.buildUserModelView
-        .withArgs(this.members[1])
-        .returns(this.memberViews[0])
-      this.ProjectEditorHandler.buildUserModelView
-        .withArgs(this.members[2])
-        .returns(this.memberViews[1])
-      this.result =
-        await this.CollaboratorsGetter.promises.getAllInvitedMembers(
-          this.project._id
-        )
+      ctx.ProjectEditorHandler.buildUserModelView
+        .withArgs(ctx.members[1])
+        .returns(ctx.memberViews[0])
+      ctx.ProjectEditorHandler.buildUserModelView
+        .withArgs(ctx.members[2])
+        .returns(ctx.memberViews[1])
+      ctx.result = await ctx.CollaboratorsGetter.promises.getAllInvitedMembers(
+        ctx.project._id
+      )
     })
     })
 
 
-    it('should produce a list of members', function () {
-      expect(this.result).to.deep.equal(this.memberViews)
+    it('should produce a list of members', function (ctx) {
+      expect(ctx.result).to.deep.equal(ctx.memberViews)
     })
     })
 
 
-    it('should call ProjectEditorHandler.buildUserModelView', function () {
-      expect(this.ProjectEditorHandler.buildUserModelView).to.have.been
+    it('should call ProjectEditorHandler.buildUserModelView', function (ctx) {
+      expect(ctx.ProjectEditorHandler.buildUserModelView).to.have.been
         .calledTwice
         .calledTwice
       expect(
       expect(
-        this.ProjectEditorHandler.buildUserModelView
-      ).to.have.been.calledWith(this.members[1])
+        ctx.ProjectEditorHandler.buildUserModelView
+      ).to.have.been.calledWith(ctx.members[1])
       expect(
       expect(
-        this.ProjectEditorHandler.buildUserModelView
-      ).to.have.been.calledWith(this.members[2])
+        ctx.ProjectEditorHandler.buildUserModelView
+      ).to.have.been.calledWith(ctx.members[2])
     })
     })
   })
   })
 
 
   describe('userIsTokenMember', function () {
   describe('userIsTokenMember', function () {
-    it('should return true when the project is found', async function () {
-      this.ProjectMock.expects('findOne').chain('exec').resolves(this.project)
-      const isMember =
-        await this.CollaboratorsGetter.promises.userIsTokenMember(
-          this.userId,
-          this.project._id
-        )
+    it('should return true when the project is found', async function (ctx) {
+      ctx.ProjectMock.expects('findOne').chain('exec').resolves(ctx.project)
+      const isMember = await ctx.CollaboratorsGetter.promises.userIsTokenMember(
+        ctx.userId,
+        ctx.project._id
+      )
       expect(isMember).to.be.true
       expect(isMember).to.be.true
     })
     })
 
 
-    it('should return false when the project is not found', async function () {
-      this.ProjectMock.expects('findOne').chain('exec').resolves(null)
-      const isMember =
-        await this.CollaboratorsGetter.promises.userIsTokenMember(
-          this.userId,
-          this.project._id
-        )
+    it('should return false when the project is not found', async function (ctx) {
+      ctx.ProjectMock.expects('findOne').chain('exec').resolves(null)
+      const isMember = await ctx.CollaboratorsGetter.promises.userIsTokenMember(
+        ctx.userId,
+        ctx.project._id
+      )
       expect(isMember).to.be.false
       expect(isMember).to.be.false
     })
     })
   })
   })
 
 
   describe('userIsReadWriteTokenMember', function () {
   describe('userIsReadWriteTokenMember', function () {
-    it('should return true when the project is found', async function () {
-      this.ProjectMock.expects('findOne').chain('exec').resolves(this.project)
+    it('should return true when the project is found', async function (ctx) {
+      ctx.ProjectMock.expects('findOne').chain('exec').resolves(ctx.project)
       const isMember =
       const isMember =
-        await this.CollaboratorsGetter.promises.userIsReadWriteTokenMember(
-          this.userId,
-          this.project._id
+        await ctx.CollaboratorsGetter.promises.userIsReadWriteTokenMember(
+          ctx.userId,
+          ctx.project._id
         )
         )
       expect(isMember).to.be.true
       expect(isMember).to.be.true
     })
     })
 
 
-    it('should return false when the project is not found', async function () {
-      this.ProjectMock.expects('findOne').chain('exec').resolves(null)
+    it('should return false when the project is not found', async function (ctx) {
+      ctx.ProjectMock.expects('findOne').chain('exec').resolves(null)
       const isMember =
       const isMember =
-        await this.CollaboratorsGetter.promises.userIsReadWriteTokenMember(
-          this.userId,
-          this.project._id
+        await ctx.CollaboratorsGetter.promises.userIsReadWriteTokenMember(
+          ctx.userId,
+          ctx.project._id
         )
         )
       expect(isMember).to.be.false
       expect(isMember).to.be.false
     })
     })
@@ -481,53 +501,53 @@ describe('CollaboratorsGetter', function () {
   describe('getPublicShareTokens', function () {
   describe('getPublicShareTokens', function () {
     const userMock = new ObjectId()
     const userMock = new ObjectId()
 
 
-    it('should return null when the project is not found', async function () {
-      this.ProjectMock.expects('findOne').chain('exec').resolves(undefined)
+    it('should return null when the project is not found', async function (ctx) {
+      ctx.ProjectMock.expects('findOne').chain('exec').resolves(undefined)
       const tokens =
       const tokens =
-        await this.CollaboratorsGetter.promises.getPublicShareTokens(
+        await ctx.CollaboratorsGetter.promises.getPublicShareTokens(
           userMock,
           userMock,
-          this.project._id
+          ctx.project._id
         )
         )
       expect(tokens).to.be.null
       expect(tokens).to.be.null
     })
     })
 
 
-    it('should return an empty object when the user is not owner or read-only collaborator', async function () {
-      this.ProjectMock.expects('findOne').chain('exec').resolves(this.project)
+    it('should return an empty object when the user is not owner or read-only collaborator', async function (ctx) {
+      ctx.ProjectMock.expects('findOne').chain('exec').resolves(ctx.project)
       const tokens =
       const tokens =
-        await this.CollaboratorsGetter.promises.getPublicShareTokens(
+        await ctx.CollaboratorsGetter.promises.getPublicShareTokens(
           userMock,
           userMock,
-          this.project._id
+          ctx.project._id
         )
         )
       expect(tokens).to.deep.equal({})
       expect(tokens).to.deep.equal({})
     })
     })
 
 
     describe('when the user is a read-only token collaborator', function () {
     describe('when the user is a read-only token collaborator', function () {
-      it('should return the read-only token', async function () {
-        this.ProjectMock.expects('findOne')
+      it('should return the read-only token', async function (ctx) {
+        ctx.ProjectMock.expects('findOne')
           .chain('exec')
           .chain('exec')
-          .resolves({ hasTokenReadOnlyAccess: true, ...this.project })
+          .resolves({ hasTokenReadOnlyAccess: true, ...ctx.project })
 
 
         const tokens =
         const tokens =
-          await this.CollaboratorsGetter.promises.getPublicShareTokens(
+          await ctx.CollaboratorsGetter.promises.getPublicShareTokens(
             userMock,
             userMock,
-            this.project._id
+            ctx.project._id
           )
           )
         expect(tokens).to.deep.equal({ readOnly: tokens.readOnly })
         expect(tokens).to.deep.equal({ readOnly: tokens.readOnly })
       })
       })
     })
     })
 
 
     describe('when the user is the owner of the project', function () {
     describe('when the user is the owner of the project', function () {
-      beforeEach(function () {
-        this.ProjectMock.expects('findOne')
+      beforeEach(function (ctx) {
+        ctx.ProjectMock.expects('findOne')
           .chain('exec')
           .chain('exec')
-          .resolves({ isOwner: true, ...this.project })
+          .resolves({ isOwner: true, ...ctx.project })
       })
       })
 
 
-      it('should return all the tokens', async function () {
+      it('should return all the tokens', async function (ctx) {
         const tokens =
         const tokens =
-          await this.CollaboratorsGetter.promises.getPublicShareTokens(
+          await ctx.CollaboratorsGetter.promises.getPublicShareTokens(
             userMock,
             userMock,
-            this.project._id
+            ctx.project._id
           )
           )
         expect(tokens).to.deep.equal(tokens)
         expect(tokens).to.deep.equal(tokens)
       })
       })
@@ -535,20 +555,20 @@ describe('CollaboratorsGetter', function () {
   })
   })
 
 
   describe('getInvitedEditCollaboratorCount', function () {
   describe('getInvitedEditCollaboratorCount', function () {
-    it('should return the count of invited edit collaborators (readAndWrite, review)', async function () {
+    it('should return the count of invited edit collaborators (readAndWrite, review)', async function (ctx) {
       const count =
       const count =
-        await this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
-          this.project._id
+        await ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
+          ctx.project._id
         )
         )
       expect(count).to.equal(4)
       expect(count).to.equal(4)
     })
     })
   })
   })
 
 
   describe('getInvitedPendingEditorCount', function () {
   describe('getInvitedPendingEditorCount', function () {
-    it('should return the count of pending editors and reviewers', async function () {
+    it('should return the count of pending editors and reviewers', async function (ctx) {
       const count =
       const count =
-        await this.CollaboratorsGetter.promises.getInvitedPendingEditorCount(
-          this.project._id
+        await ctx.CollaboratorsGetter.promises.getInvitedPendingEditorCount(
+          ctx.project._id
         )
         )
       expect(count).to.equal(2)
       expect(count).to.equal(2)
     })
     })
@@ -556,11 +576,11 @@ describe('CollaboratorsGetter', function () {
 
 
   describe('ProjectAccess', function () {
   describe('ProjectAccess', function () {
     describe('privilegeLevelForUser', function () {
     describe('privilegeLevelForUser', function () {
-      it('should return reviewer privilege when user is both reviewer and token member', function () {
+      it('should return reviewer privilege when user is both reviewer and token member', function (ctx) {
         const userWhoIsBothReviewerAndToken = new ObjectId()
         const userWhoIsBothReviewerAndToken = new ObjectId()
 
 
         const projectWithDuplicateUser = {
         const projectWithDuplicateUser = {
-          owner_ref: this.ownerRef,
+          owner_ref: ctx.ownerRef,
           collaberator_refs: [],
           collaberator_refs: [],
           readOnly_refs: [],
           readOnly_refs: [],
           tokenAccessReadAndWrite_refs: [userWhoIsBothReviewerAndToken],
           tokenAccessReadAndWrite_refs: [userWhoIsBothReviewerAndToken],
@@ -571,7 +591,7 @@ describe('CollaboratorsGetter', function () {
           pendingReviewer_refs: [],
           pendingReviewer_refs: [],
         }
         }
 
 
-        const projectAccess = new this.CollaboratorsGetter.ProjectAccess(
+        const projectAccess = new ctx.CollaboratorsGetter.ProjectAccess(
           projectWithDuplicateUser
           projectWithDuplicateUser
         )
         )
         const privilegeLevel = projectAccess.privilegeLevelForUser(
         const privilegeLevel = projectAccess.privilegeLevelForUser(
@@ -581,11 +601,11 @@ describe('CollaboratorsGetter', function () {
         expect(privilegeLevel).to.equal('review')
         expect(privilegeLevel).to.equal('review')
       })
       })
 
 
-      it('should return readOnly privilege when user is both readOnly and token readAndWrite member', function () {
+      it('should return readOnly privilege when user is both readOnly and token readAndWrite member', function (ctx) {
         const userWhoIsBothReadOnlyAndTokenRW = new ObjectId()
         const userWhoIsBothReadOnlyAndTokenRW = new ObjectId()
 
 
         const projectWithDuplicateUser = {
         const projectWithDuplicateUser = {
-          owner_ref: this.ownerRef,
+          owner_ref: ctx.ownerRef,
           collaberator_refs: [],
           collaberator_refs: [],
           readOnly_refs: [userWhoIsBothReadOnlyAndTokenRW],
           readOnly_refs: [userWhoIsBothReadOnlyAndTokenRW],
           tokenAccessReadAndWrite_refs: [userWhoIsBothReadOnlyAndTokenRW],
           tokenAccessReadAndWrite_refs: [userWhoIsBothReadOnlyAndTokenRW],
@@ -596,7 +616,7 @@ describe('CollaboratorsGetter', function () {
           pendingReviewer_refs: [],
           pendingReviewer_refs: [],
         }
         }
 
 
-        const projectAccess = new this.CollaboratorsGetter.ProjectAccess(
+        const projectAccess = new ctx.CollaboratorsGetter.ProjectAccess(
           projectWithDuplicateUser
           projectWithDuplicateUser
         )
         )
         const privilegeLevel = projectAccess.privilegeLevelForUser(
         const privilegeLevel = projectAccess.privilegeLevelForUser(
@@ -607,12 +627,12 @@ describe('CollaboratorsGetter', function () {
         expect(privilegeLevel).to.equal('readOnly')
         expect(privilegeLevel).to.equal('readOnly')
       })
       })
 
 
-      it('should return none for non-members', function () {
-        const projectAccess = new this.CollaboratorsGetter.ProjectAccess(
-          this.project
+      it('should return none for non-members', function (ctx) {
+        const projectAccess = new ctx.CollaboratorsGetter.ProjectAccess(
+          ctx.project
         )
         )
         const privilegeLevel = projectAccess.privilegeLevelForUser(
         const privilegeLevel = projectAccess.privilegeLevelForUser(
-          this.nonMemberRef
+          ctx.nonMemberRef
         )
         )
 
 
         expect(privilegeLevel).to.equal(false)
         expect(privilegeLevel).to.equal(false)

+ 359 - 321
services/web/test/unit/src/Collaborators/CollaboratorsHandler.test.mjs

@@ -1,106 +1,144 @@
-const { promisify } = require('util')
-const SandboxedModule = require('sandboxed-module')
-const path = require('path')
-const sinon = require('sinon')
-const { expect } = require('chai')
-const Errors = require('../../../../app/src/Features/Errors/Errors')
-const { Project } = require('../helpers/models/Project')
-const { ObjectId } = require('mongodb-legacy')
-
+import { vi, expect } from 'vitest'
+import path from 'path'
+import sinon from 'sinon'
+import Errors from '../../../../app/src/Features/Errors/Errors.js'
+import indirectlyImportModels from '../helpers/indirectlyImportModels.js'
+import mongodb from 'mongodb-legacy'
+import { setTimeout } from 'node:timers/promises'
+
+const { ObjectId } = mongodb
+const { Project } = indirectlyImportModels(['Project'])
+vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
+  vi.importActual('../../../../app/src/Features/Errors/Errors.js')
+)
 const MODULE_PATH = path.join(
 const MODULE_PATH = path.join(
-  __dirname,
+  import.meta.dirname,
   '../../../../app/src/Features/Collaborators/CollaboratorsHandler'
   '../../../../app/src/Features/Collaborators/CollaboratorsHandler'
 )
 )
 
 
-const sleep = promisify(setTimeout)
+const sleep = setTimeout
 
 
 describe('CollaboratorsHandler', function () {
 describe('CollaboratorsHandler', function () {
-  beforeEach(function () {
-    this.userId = new ObjectId()
-    this.addingUserId = new ObjectId()
-    this.project = {
+  beforeEach(async function (ctx) {
+    ctx.userId = new ObjectId()
+    ctx.addingUserId = new ObjectId()
+    ctx.project = {
       _id: new ObjectId(),
       _id: new ObjectId(),
-      owner_ref: this.addingUserId,
+      owner_ref: ctx.addingUserId,
       name: 'Foo',
       name: 'Foo',
     }
     }
 
 
-    this.UserGetter = {
+    ctx.UserGetter = {
       promises: {
       promises: {
         getUser: sinon.stub().resolves(null),
         getUser: sinon.stub().resolves(null),
       },
       },
     }
     }
-    this.ContactManager = {
+    ctx.ContactManager = {
       addContact: sinon.stub(),
       addContact: sinon.stub(),
     }
     }
-    this.ProjectMock = sinon.mock(Project)
-    this.TpdsProjectFlusher = {
+    ctx.ProjectMock = sinon.mock(Project)
+    ctx.TpdsProjectFlusher = {
       promises: {
       promises: {
         flushProjectToTpds: sinon.stub().resolves(),
         flushProjectToTpds: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.TpdsUpdateSender = {
+    ctx.TpdsUpdateSender = {
       promises: {
       promises: {
         createProject: sinon.stub().resolves(),
         createProject: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.ProjectGetter = {
+    ctx.ProjectGetter = {
       promises: {
       promises: {
-        getProject: sinon.stub().resolves(this.project),
+        getProject: sinon.stub().resolves(ctx.project),
       },
       },
     }
     }
 
 
-    this.CollaboratorsGetter = {
+    ctx.CollaboratorsGetter = {
       promises: {
       promises: {
         dangerouslyGetAllProjectsUserIsMemberOf: sinon.stub(),
         dangerouslyGetAllProjectsUserIsMemberOf: sinon.stub(),
         getMemberIdsWithPrivilegeLevels: sinon.stub().resolves([]),
         getMemberIdsWithPrivilegeLevels: sinon.stub().resolves([]),
       },
       },
     }
     }
-    this.EditorRealTimeController = { emitToRoom: sinon.stub() }
-    this.CollaboratorsHandler = SandboxedModule.require(MODULE_PATH, {
-      requires: {
-        '../User/UserGetter': this.UserGetter,
-        '../Contacts/ContactManager': this.ContactManager,
-        '../../models/Project': { Project },
-        '../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher,
-        '../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender,
-        '../Project/ProjectGetter': this.ProjectGetter,
-        '../Editor/EditorRealTimeController': this.EditorRealTimeController,
-        './CollaboratorsGetter': this.CollaboratorsGetter,
-      },
-    })
+    ctx.EditorRealTimeController = { emitToRoom: sinon.stub() }
+
+    vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
+      default: ctx.UserGetter,
+    }))
+
+    vi.doMock('../../../../app/src/Features/Contacts/ContactManager', () => ({
+      default: ctx.ContactManager,
+    }))
+
+    vi.doMock('../../../../app/src/models/Project', () => ({
+      Project,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher',
+      () => ({
+        default: ctx.TpdsProjectFlusher,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender',
+      () => ({
+        default: ctx.TpdsUpdateSender,
+      })
+    )
+
+    vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
+      default: ctx.ProjectGetter,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Editor/EditorRealTimeController',
+      () => ({
+        default: ctx.EditorRealTimeController,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/Collaborators/CollaboratorsGetter',
+      () => ({
+        default: ctx.CollaboratorsGetter,
+      })
+    )
+
+    ctx.CollaboratorsHandler = (await import(MODULE_PATH)).default
   })
   })
 
 
-  afterEach(function () {
-    this.ProjectMock.verify()
+  afterEach(function (ctx) {
+    ctx.ProjectMock.verify()
   })
   })
 
 
   describe('removeUserFromProject', function () {
   describe('removeUserFromProject', function () {
     describe('a non-archived project', function () {
     describe('a non-archived project', function () {
-      it('should remove the user from mongo', async function () {
-        this.ProjectMock.expects('updateOne')
+      it('should remove the user from mongo', async function (ctx) {
+        ctx.ProjectMock.expects('updateOne')
           .withArgs(
           .withArgs(
             {
             {
-              _id: this.project._id,
+              _id: ctx.project._id,
             },
             },
             {
             {
               $pull: {
               $pull: {
-                collaberator_refs: this.userId,
-                reviewer_refs: this.userId,
-                readOnly_refs: this.userId,
-                pendingEditor_refs: this.userId,
-                pendingReviewer_refs: this.userId,
-                tokenAccessReadOnly_refs: this.userId,
-                tokenAccessReadAndWrite_refs: this.userId,
-                archived: this.userId,
-                trashed: this.userId,
+                collaberator_refs: ctx.userId,
+                reviewer_refs: ctx.userId,
+                readOnly_refs: ctx.userId,
+                pendingEditor_refs: ctx.userId,
+                pendingReviewer_refs: ctx.userId,
+                tokenAccessReadOnly_refs: ctx.userId,
+                tokenAccessReadAndWrite_refs: ctx.userId,
+                archived: ctx.userId,
+                trashed: ctx.userId,
               },
               },
             }
             }
           )
           )
           .chain('exec')
           .chain('exec')
           .resolves()
           .resolves()
-        await this.CollaboratorsHandler.promises.removeUserFromProject(
-          this.project._id,
-          this.userId
+        await ctx.CollaboratorsHandler.promises.removeUserFromProject(
+          ctx.project._id,
+          ctx.userId
         )
         )
       })
       })
     })
     })
@@ -108,70 +146,70 @@ describe('CollaboratorsHandler', function () {
 
 
   describe('addUserIdToProject', function () {
   describe('addUserIdToProject', function () {
     describe('as readOnly', function () {
     describe('as readOnly', function () {
-      beforeEach(async function () {
-        this.ProjectMock.expects('updateOne')
+      beforeEach(async function (ctx) {
+        ctx.ProjectMock.expects('updateOne')
           .withArgs(
           .withArgs(
             {
             {
-              _id: this.project._id,
+              _id: ctx.project._id,
             },
             },
             {
             {
-              $addToSet: { readOnly_refs: this.userId },
+              $addToSet: { readOnly_refs: ctx.userId },
             }
             }
           )
           )
           .chain('exec')
           .chain('exec')
           .resolves()
           .resolves()
-        await this.CollaboratorsHandler.promises.addUserIdToProject(
-          this.project._id,
-          this.addingUserId,
-          this.userId,
+        await ctx.CollaboratorsHandler.promises.addUserIdToProject(
+          ctx.project._id,
+          ctx.addingUserId,
+          ctx.userId,
           'readOnly'
           'readOnly'
         )
         )
       })
       })
 
 
-      it('should create the project folder in dropbox', function () {
+      it('should create the project folder in dropbox', function (ctx) {
         expect(
         expect(
-          this.TpdsUpdateSender.promises.createProject
+          ctx.TpdsUpdateSender.promises.createProject
         ).to.have.been.calledWith({
         ).to.have.been.calledWith({
-          projectId: this.project._id,
-          projectName: this.project.name,
-          ownerId: this.addingUserId,
-          userId: this.userId,
+          projectId: ctx.project._id,
+          projectName: ctx.project.name,
+          ownerId: ctx.addingUserId,
+          userId: ctx.userId,
         })
         })
       })
       })
 
 
-      it('should flush the project to the TPDS', function () {
+      it('should flush the project to the TPDS', function (ctx) {
         expect(
         expect(
-          this.TpdsProjectFlusher.promises.flushProjectToTpds
-        ).to.have.been.calledWith(this.project._id)
+          ctx.TpdsProjectFlusher.promises.flushProjectToTpds
+        ).to.have.been.calledWith(ctx.project._id)
       })
       })
 
 
-      it('should add the user as a contact for the adding user', function () {
-        expect(this.ContactManager.addContact).to.have.been.calledWith(
-          this.addingUserId,
-          this.userId
+      it('should add the user as a contact for the adding user', function (ctx) {
+        expect(ctx.ContactManager.addContact).to.have.been.calledWith(
+          ctx.addingUserId,
+          ctx.userId
         )
         )
       })
       })
 
 
       describe('and with pendingEditor flag', function () {
       describe('and with pendingEditor flag', function () {
-        it('should add them to the pending editor refs', async function () {
-          this.ProjectMock.expects('updateOne')
+        it('should add them to the pending editor refs', async function (ctx) {
+          ctx.ProjectMock.expects('updateOne')
             .withArgs(
             .withArgs(
               {
               {
-                _id: this.project._id,
+                _id: ctx.project._id,
               },
               },
               {
               {
                 $addToSet: {
                 $addToSet: {
-                  readOnly_refs: this.userId,
-                  pendingEditor_refs: this.userId,
+                  readOnly_refs: ctx.userId,
+                  pendingEditor_refs: ctx.userId,
                 },
                 },
               }
               }
             )
             )
             .chain('exec')
             .chain('exec')
             .resolves()
             .resolves()
-          await this.CollaboratorsHandler.promises.addUserIdToProject(
-            this.project._id,
-            this.addingUserId,
-            this.userId,
+          await ctx.CollaboratorsHandler.promises.addUserIdToProject(
+            ctx.project._id,
+            ctx.addingUserId,
+            ctx.userId,
             'readOnly',
             'readOnly',
             { pendingEditor: true }
             { pendingEditor: true }
           )
           )
@@ -179,25 +217,25 @@ describe('CollaboratorsHandler', function () {
       })
       })
 
 
       describe('with pendingReviewer flag', function () {
       describe('with pendingReviewer flag', function () {
-        it('should add them to the pending reviewer refs', async function () {
-          this.ProjectMock.expects('updateOne')
+        it('should add them to the pending reviewer refs', async function (ctx) {
+          ctx.ProjectMock.expects('updateOne')
             .withArgs(
             .withArgs(
               {
               {
-                _id: this.project._id,
+                _id: ctx.project._id,
               },
               },
               {
               {
                 $addToSet: {
                 $addToSet: {
-                  readOnly_refs: this.userId,
-                  pendingReviewer_refs: this.userId,
+                  readOnly_refs: ctx.userId,
+                  pendingReviewer_refs: ctx.userId,
                 },
                 },
               }
               }
             )
             )
             .chain('exec')
             .chain('exec')
             .resolves()
             .resolves()
-          await this.CollaboratorsHandler.promises.addUserIdToProject(
-            this.project._id,
-            this.addingUserId,
-            this.userId,
+          await ctx.CollaboratorsHandler.promises.addUserIdToProject(
+            ctx.project._id,
+            ctx.addingUserId,
+            ctx.userId,
             'readOnly',
             'readOnly',
             { pendingReviewer: true }
             { pendingReviewer: true }
           )
           )
@@ -206,77 +244,77 @@ describe('CollaboratorsHandler', function () {
     })
     })
 
 
     describe('as readAndWrite', function () {
     describe('as readAndWrite', function () {
-      beforeEach(async function () {
-        this.ProjectMock.expects('updateOne')
+      beforeEach(async function (ctx) {
+        ctx.ProjectMock.expects('updateOne')
           .withArgs(
           .withArgs(
             {
             {
-              _id: this.project._id,
+              _id: ctx.project._id,
             },
             },
             {
             {
-              $addToSet: { collaberator_refs: this.userId },
+              $addToSet: { collaberator_refs: ctx.userId },
             }
             }
           )
           )
           .chain('exec')
           .chain('exec')
           .resolves()
           .resolves()
-        await this.CollaboratorsHandler.promises.addUserIdToProject(
-          this.project._id,
-          this.addingUserId,
-          this.userId,
+        await ctx.CollaboratorsHandler.promises.addUserIdToProject(
+          ctx.project._id,
+          ctx.addingUserId,
+          ctx.userId,
           'readAndWrite'
           'readAndWrite'
         )
         )
       })
       })
 
 
-      it('should flush the project to the TPDS', function () {
+      it('should flush the project to the TPDS', function (ctx) {
         expect(
         expect(
-          this.TpdsProjectFlusher.promises.flushProjectToTpds
-        ).to.have.been.calledWith(this.project._id)
+          ctx.TpdsProjectFlusher.promises.flushProjectToTpds
+        ).to.have.been.calledWith(ctx.project._id)
       })
       })
     })
     })
 
 
     describe('as reviewer', function () {
     describe('as reviewer', function () {
-      beforeEach(async function () {
-        this.ProjectMock.expects('updateOne')
+      beforeEach(async function (ctx) {
+        ctx.ProjectMock.expects('updateOne')
           .withArgs(
           .withArgs(
             {
             {
-              _id: this.project._id,
+              _id: ctx.project._id,
             },
             },
             {
             {
-              track_changes: { [this.userId]: true },
-              $addToSet: { reviewer_refs: this.userId },
+              track_changes: { [ctx.userId]: true },
+              $addToSet: { reviewer_refs: ctx.userId },
             }
             }
           )
           )
           .chain('exec')
           .chain('exec')
           .resolves()
           .resolves()
-        await this.CollaboratorsHandler.promises.addUserIdToProject(
-          this.project._id,
-          this.addingUserId,
-          this.userId,
+        await ctx.CollaboratorsHandler.promises.addUserIdToProject(
+          ctx.project._id,
+          ctx.addingUserId,
+          ctx.userId,
           'review'
           'review'
         )
         )
       })
       })
 
 
-      it('should update the client with new track changes settings', function () {
-        return this.EditorRealTimeController.emitToRoom
-          .calledWith(this.project._id, 'toggle-track-changes', {
-            [this.userId]: true,
+      it('should update the client with new track changes settings', function (ctx) {
+        return ctx.EditorRealTimeController.emitToRoom
+          .calledWith(ctx.project._id, 'toggle-track-changes', {
+            [ctx.userId]: true,
           })
           })
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should flush the project to the TPDS', function () {
+      it('should flush the project to the TPDS', function (ctx) {
         expect(
         expect(
-          this.TpdsProjectFlusher.promises.flushProjectToTpds
-        ).to.have.been.calledWith(this.project._id)
+          ctx.TpdsProjectFlusher.promises.flushProjectToTpds
+        ).to.have.been.calledWith(ctx.project._id)
       })
       })
     })
     })
 
 
     describe('with invalid privilegeLevel', function () {
     describe('with invalid privilegeLevel', function () {
-      it('should call the callback with an error', async function () {
+      it('should call the callback with an error', async function (ctx) {
         await expect(
         await expect(
-          this.CollaboratorsHandler.promises.addUserIdToProject(
-            this.project._id,
-            this.addingUserId,
-            this.userId,
+          ctx.CollaboratorsHandler.promises.addUserIdToProject(
+            ctx.project._id,
+            ctx.addingUserId,
+            ctx.userId,
             'notValid'
             'notValid'
           )
           )
         ).to.be.rejected
         ).to.be.rejected
@@ -284,15 +322,15 @@ describe('CollaboratorsHandler', function () {
     })
     })
 
 
     describe('when user already exists as a collaborator', function () {
     describe('when user already exists as a collaborator', function () {
-      beforeEach(function () {
-        this.project.collaberator_refs = [this.userId]
+      beforeEach(function (ctx) {
+        ctx.project.collaberator_refs = [ctx.userId]
       })
       })
 
 
-      it('should not add the user again', async function () {
-        await this.CollaboratorsHandler.promises.addUserIdToProject(
-          this.project._id,
-          this.addingUserId,
-          this.userId,
+      it('should not add the user again', async function (ctx) {
+        await ctx.CollaboratorsHandler.promises.addUserIdToProject(
+          ctx.project._id,
+          ctx.addingUserId,
+          ctx.userId,
           'readAndWrite'
           'readAndWrite'
         )
         )
         // Project.updateOne() should not be called. If it is, it will fail because
         // Project.updateOne() should not be called. If it is, it will fail because
@@ -301,71 +339,71 @@ describe('CollaboratorsHandler', function () {
     })
     })
 
 
     describe('when user already exists as a reviewer', function () {
     describe('when user already exists as a reviewer', function () {
-      beforeEach(function () {
-        this.project.collaberator_refs = []
-        this.project.reviewer_refs = [this.userId]
-        this.project.readOnly_refs = []
+      beforeEach(function (ctx) {
+        ctx.project.collaberator_refs = []
+        ctx.project.reviewer_refs = [ctx.userId]
+        ctx.project.readOnly_refs = []
       })
       })
 
 
-      it('should not add the user again', async function () {
-        await this.CollaboratorsHandler.promises.addUserIdToProject(
-          this.project._id,
-          this.addingUserId,
-          this.userId,
+      it('should not add the user again', async function (ctx) {
+        await ctx.CollaboratorsHandler.promises.addUserIdToProject(
+          ctx.project._id,
+          ctx.addingUserId,
+          ctx.userId,
           'readAndWrite'
           'readAndWrite'
         )
         )
       })
       })
     })
     })
 
 
     describe('when user already exists as a read-only user', function () {
     describe('when user already exists as a read-only user', function () {
-      beforeEach(function () {
-        this.project.collaberator_refs = []
-        this.project.reviewer_refs = []
-        this.project.readOnly_refs = [this.userId]
+      beforeEach(function (ctx) {
+        ctx.project.collaberator_refs = []
+        ctx.project.reviewer_refs = []
+        ctx.project.readOnly_refs = [ctx.userId]
       })
       })
 
 
-      it('should not add the user again', async function () {
-        await this.CollaboratorsHandler.promises.addUserIdToProject(
-          this.project._id,
-          this.addingUserId,
-          this.userId,
+      it('should not add the user again', async function (ctx) {
+        await ctx.CollaboratorsHandler.promises.addUserIdToProject(
+          ctx.project._id,
+          ctx.addingUserId,
+          ctx.userId,
           'readAndWrite'
           'readAndWrite'
         )
         )
       })
       })
     })
     })
 
 
     describe('with null addingUserId', function () {
     describe('with null addingUserId', function () {
-      beforeEach(async function () {
-        this.project.collaberator_refs = []
-        this.ProjectMock.expects('updateOne')
+      beforeEach(async function (ctx) {
+        ctx.project.collaberator_refs = []
+        ctx.ProjectMock.expects('updateOne')
           .withArgs(
           .withArgs(
             {
             {
-              _id: this.project._id,
+              _id: ctx.project._id,
             },
             },
             {
             {
-              $addToSet: { collaberator_refs: this.userId },
+              $addToSet: { collaberator_refs: ctx.userId },
             }
             }
           )
           )
           .chain('exec')
           .chain('exec')
           .resolves()
           .resolves()
-        await this.CollaboratorsHandler.promises.addUserIdToProject(
-          this.project._id,
+        await ctx.CollaboratorsHandler.promises.addUserIdToProject(
+          ctx.project._id,
           null,
           null,
-          this.userId,
+          ctx.userId,
           'readAndWrite'
           'readAndWrite'
         )
         )
       })
       })
 
 
-      it('should not add the adding user as a contact', function () {
-        expect(this.ContactManager.addContact).not.to.have.been.called
+      it('should not add the adding user as a contact', function (ctx) {
+        expect(ctx.ContactManager.addContact).not.to.have.been.called
       })
       })
     })
     })
   })
   })
 
 
   describe('removeUserFromAllProjects', function () {
   describe('removeUserFromAllProjects', function () {
-    it('should remove the user from each project', async function () {
-      this.CollaboratorsGetter.promises.dangerouslyGetAllProjectsUserIsMemberOf
-        .withArgs(this.userId, { _id: 1 })
+    it('should remove the user from each project', async function (ctx) {
+      ctx.CollaboratorsGetter.promises.dangerouslyGetAllProjectsUserIsMemberOf
+        .withArgs(ctx.userId, { _id: 1 })
         .resolves({
         .resolves({
           readAndWrite: [
           readAndWrite: [
             { _id: 'read-and-write-0' },
             { _id: 'read-and-write-0' },
@@ -392,38 +430,38 @@ describe('CollaboratorsHandler', function () {
         'token-read-only-1',
         'token-read-only-1',
       ]
       ]
       for (const projectId of expectedProjects) {
       for (const projectId of expectedProjects) {
-        this.ProjectMock.expects('updateOne')
+        ctx.ProjectMock.expects('updateOne')
           .withArgs(
           .withArgs(
             {
             {
               _id: projectId,
               _id: projectId,
             },
             },
             {
             {
               $pull: {
               $pull: {
-                collaberator_refs: this.userId,
-                reviewer_refs: this.userId,
-                readOnly_refs: this.userId,
-                pendingEditor_refs: this.userId,
-                pendingReviewer_refs: this.userId,
-                tokenAccessReadOnly_refs: this.userId,
-                tokenAccessReadAndWrite_refs: this.userId,
-                archived: this.userId,
-                trashed: this.userId,
+                collaberator_refs: ctx.userId,
+                reviewer_refs: ctx.userId,
+                readOnly_refs: ctx.userId,
+                pendingEditor_refs: ctx.userId,
+                pendingReviewer_refs: ctx.userId,
+                tokenAccessReadOnly_refs: ctx.userId,
+                tokenAccessReadAndWrite_refs: ctx.userId,
+                archived: ctx.userId,
+                trashed: ctx.userId,
               },
               },
             }
             }
           )
           )
           .resolves()
           .resolves()
       }
       }
-      await this.CollaboratorsHandler.promises.removeUserFromAllProjects(
-        this.userId
+      await ctx.CollaboratorsHandler.promises.removeUserFromAllProjects(
+        ctx.userId
       )
       )
     })
     })
   })
   })
 
 
   describe('transferProjects', function () {
   describe('transferProjects', function () {
-    beforeEach(function () {
-      this.fromUserId = new ObjectId()
-      this.toUserId = new ObjectId()
-      this.projects = [
+    beforeEach(function (ctx) {
+      ctx.fromUserId = new ObjectId()
+      ctx.toUserId = new ObjectId()
+      ctx.projects = [
         {
         {
           _id: new ObjectId(),
           _id: new ObjectId(),
         },
         },
@@ -431,91 +469,91 @@ describe('CollaboratorsHandler', function () {
           _id: new ObjectId(),
           _id: new ObjectId(),
         },
         },
       ]
       ]
-      this.ProjectMock.expects('find')
+      ctx.ProjectMock.expects('find')
         .withArgs({
         .withArgs({
           $or: [
           $or: [
-            { owner_ref: this.fromUserId },
-            { collaberator_refs: this.fromUserId },
-            { readOnly_refs: this.fromUserId },
+            { owner_ref: ctx.fromUserId },
+            { collaberator_refs: ctx.fromUserId },
+            { readOnly_refs: ctx.fromUserId },
           ],
           ],
         })
         })
         .chain('exec')
         .chain('exec')
-        .resolves(this.projects)
-      this.ProjectMock.expects('updateMany')
+        .resolves(ctx.projects)
+      ctx.ProjectMock.expects('updateMany')
         .withArgs(
         .withArgs(
-          { owner_ref: this.fromUserId },
-          { $set: { owner_ref: this.toUserId } }
+          { owner_ref: ctx.fromUserId },
+          { $set: { owner_ref: ctx.toUserId } }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves()
         .resolves()
-      this.ProjectMock.expects('updateMany')
+      ctx.ProjectMock.expects('updateMany')
         .withArgs(
         .withArgs(
-          { collaberator_refs: this.fromUserId },
+          { collaberator_refs: ctx.fromUserId },
           {
           {
-            $addToSet: { collaberator_refs: this.toUserId },
+            $addToSet: { collaberator_refs: ctx.toUserId },
           }
           }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves()
         .resolves()
-      this.ProjectMock.expects('updateMany')
+      ctx.ProjectMock.expects('updateMany')
         .withArgs(
         .withArgs(
-          { collaberator_refs: this.fromUserId },
+          { collaberator_refs: ctx.fromUserId },
           {
           {
-            $pull: { collaberator_refs: this.fromUserId },
+            $pull: { collaberator_refs: ctx.fromUserId },
           }
           }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves()
         .resolves()
-      this.ProjectMock.expects('updateMany')
+      ctx.ProjectMock.expects('updateMany')
         .withArgs(
         .withArgs(
-          { readOnly_refs: this.fromUserId },
+          { readOnly_refs: ctx.fromUserId },
           {
           {
-            $addToSet: { readOnly_refs: this.toUserId },
+            $addToSet: { readOnly_refs: ctx.toUserId },
           }
           }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves()
         .resolves()
-      this.ProjectMock.expects('updateMany')
+      ctx.ProjectMock.expects('updateMany')
         .withArgs(
         .withArgs(
-          { readOnly_refs: this.fromUserId },
+          { readOnly_refs: ctx.fromUserId },
           {
           {
-            $pull: { readOnly_refs: this.fromUserId },
+            $pull: { readOnly_refs: ctx.fromUserId },
           }
           }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves()
         .resolves()
-      this.ProjectMock.expects('updateMany')
+      ctx.ProjectMock.expects('updateMany')
         .withArgs(
         .withArgs(
-          { pendingEditor_refs: this.fromUserId },
+          { pendingEditor_refs: ctx.fromUserId },
           {
           {
-            $addToSet: { pendingEditor_refs: this.toUserId },
+            $addToSet: { pendingEditor_refs: ctx.toUserId },
           }
           }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves()
         .resolves()
-      this.ProjectMock.expects('updateMany')
+      ctx.ProjectMock.expects('updateMany')
         .withArgs(
         .withArgs(
-          { pendingEditor_refs: this.fromUserId },
+          { pendingEditor_refs: ctx.fromUserId },
           {
           {
-            $pull: { pendingEditor_refs: this.fromUserId },
+            $pull: { pendingEditor_refs: ctx.fromUserId },
           }
           }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves()
         .resolves()
-      this.ProjectMock.expects('updateMany')
+      ctx.ProjectMock.expects('updateMany')
         .withArgs(
         .withArgs(
-          { pendingReviewer_refs: this.fromUserId },
+          { pendingReviewer_refs: ctx.fromUserId },
           {
           {
-            $addToSet: { pendingReviewer_refs: this.toUserId },
+            $addToSet: { pendingReviewer_refs: ctx.toUserId },
           }
           }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves()
         .resolves()
-      this.ProjectMock.expects('updateMany')
+      ctx.ProjectMock.expects('updateMany')
         .withArgs(
         .withArgs(
-          { pendingReviewer_refs: this.fromUserId },
+          { pendingReviewer_refs: ctx.fromUserId },
           {
           {
-            $pull: { pendingReviewer_refs: this.fromUserId },
+            $pull: { pendingReviewer_refs: ctx.fromUserId },
           }
           }
         )
         )
         .chain('exec')
         .chain('exec')
@@ -523,254 +561,254 @@ describe('CollaboratorsHandler', function () {
     })
     })
 
 
     describe('successfully', function () {
     describe('successfully', function () {
-      it('should flush each project to the TPDS', async function () {
-        await this.CollaboratorsHandler.promises.transferProjects(
-          this.fromUserId,
-          this.toUserId
+      it('should flush each project to the TPDS', async function (ctx) {
+        await ctx.CollaboratorsHandler.promises.transferProjects(
+          ctx.fromUserId,
+          ctx.toUserId
         )
         )
         await sleep(10) // let the background tasks run
         await sleep(10) // let the background tasks run
-        for (const project of this.projects) {
+        for (const project of ctx.projects) {
           expect(
           expect(
-            this.TpdsProjectFlusher.promises.flushProjectToTpds
+            ctx.TpdsProjectFlusher.promises.flushProjectToTpds
           ).to.have.been.calledWith(project._id)
           ).to.have.been.calledWith(project._id)
         }
         }
       })
       })
     })
     })
 
 
     describe('when flushing to TPDS fails', function () {
     describe('when flushing to TPDS fails', function () {
-      it('should log an error but not fail', async function () {
-        this.TpdsProjectFlusher.promises.flushProjectToTpds.rejects(
+      it('should log an error but not fail', async function (ctx) {
+        ctx.TpdsProjectFlusher.promises.flushProjectToTpds.rejects(
           new Error('oops')
           new Error('oops')
         )
         )
-        await this.CollaboratorsHandler.promises.transferProjects(
-          this.fromUserId,
-          this.toUserId
+        await ctx.CollaboratorsHandler.promises.transferProjects(
+          ctx.fromUserId,
+          ctx.toUserId
         )
         )
         await sleep(10) // let the background tasks run
         await sleep(10) // let the background tasks run
-        expect(this.logger.err).to.have.been.called
+        expect(ctx.logger.err).toHaveBeenCalled()
       })
       })
     })
     })
   })
   })
 
 
   describe('setCollaboratorPrivilegeLevel', function () {
   describe('setCollaboratorPrivilegeLevel', function () {
-    it('sets a collaborator to read-only', async function () {
-      this.ProjectMock.expects('updateOne')
+    it('sets a collaborator to read-only', async function (ctx) {
+      ctx.ProjectMock.expects('updateOne')
         .withArgs(
         .withArgs(
           {
           {
-            _id: this.project._id,
+            _id: ctx.project._id,
             $or: [
             $or: [
-              { collaberator_refs: this.userId },
-              { readOnly_refs: this.userId },
-              { reviewer_refs: this.userId },
+              { collaberator_refs: ctx.userId },
+              { readOnly_refs: ctx.userId },
+              { reviewer_refs: ctx.userId },
             ],
             ],
           },
           },
           {
           {
             $pull: {
             $pull: {
-              collaberator_refs: this.userId,
-              pendingEditor_refs: this.userId,
-              pendingReviewer_refs: this.userId,
-              reviewer_refs: this.userId,
+              collaberator_refs: ctx.userId,
+              pendingEditor_refs: ctx.userId,
+              pendingReviewer_refs: ctx.userId,
+              reviewer_refs: ctx.userId,
             },
             },
-            $addToSet: { readOnly_refs: this.userId },
+            $addToSet: { readOnly_refs: ctx.userId },
           }
           }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves({ matchedCount: 1 })
         .resolves({ matchedCount: 1 })
-      await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
-        this.project._id,
-        this.userId,
+      await ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
+        ctx.project._id,
+        ctx.userId,
         'readOnly'
         'readOnly'
       )
       )
     })
     })
 
 
-    it('sets a collaborator to read-write', async function () {
-      this.ProjectMock.expects('updateOne')
+    it('sets a collaborator to read-write', async function (ctx) {
+      ctx.ProjectMock.expects('updateOne')
         .withArgs(
         .withArgs(
           {
           {
-            _id: this.project._id,
+            _id: ctx.project._id,
             $or: [
             $or: [
-              { collaberator_refs: this.userId },
-              { readOnly_refs: this.userId },
-              { reviewer_refs: this.userId },
+              { collaberator_refs: ctx.userId },
+              { readOnly_refs: ctx.userId },
+              { reviewer_refs: ctx.userId },
             ],
             ],
           },
           },
           {
           {
-            $addToSet: { collaberator_refs: this.userId },
+            $addToSet: { collaberator_refs: ctx.userId },
             $pull: {
             $pull: {
-              readOnly_refs: this.userId,
-              reviewer_refs: this.userId,
-              pendingEditor_refs: this.userId,
-              pendingReviewer_refs: this.userId,
+              readOnly_refs: ctx.userId,
+              reviewer_refs: ctx.userId,
+              pendingEditor_refs: ctx.userId,
+              pendingReviewer_refs: ctx.userId,
             },
             },
           }
           }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves({ matchedCount: 1 })
         .resolves({ matchedCount: 1 })
-      await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
-        this.project._id,
-        this.userId,
+      await ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
+        ctx.project._id,
+        ctx.userId,
         'readAndWrite'
         'readAndWrite'
       )
       )
     })
     })
 
 
     describe('sets a collaborator to reviewer when track changes is enabled for everyone', function () {
     describe('sets a collaborator to reviewer when track changes is enabled for everyone', function () {
-      beforeEach(function () {
-        this.ProjectGetter.promises.getProject = sinon.stub().resolves({
+      beforeEach(function (ctx) {
+        ctx.ProjectGetter.promises.getProject = sinon.stub().resolves({
           _id: new ObjectId(),
           _id: new ObjectId(),
-          owner_ref: this.addingUserId,
+          owner_ref: ctx.addingUserId,
           name: 'Foo',
           name: 'Foo',
           track_changes: true,
           track_changes: true,
         })
         })
       })
       })
-      it('should correctly update the project', async function () {
-        this.ProjectMock.expects('updateOne')
+      it('should correctly update the project', async function (ctx) {
+        ctx.ProjectMock.expects('updateOne')
           .withArgs(
           .withArgs(
             {
             {
-              _id: this.project._id,
+              _id: ctx.project._id,
               $or: [
               $or: [
-                { collaberator_refs: this.userId },
-                { readOnly_refs: this.userId },
-                { reviewer_refs: this.userId },
+                { collaberator_refs: ctx.userId },
+                { readOnly_refs: ctx.userId },
+                { reviewer_refs: ctx.userId },
               ],
               ],
             },
             },
             {
             {
-              $addToSet: { reviewer_refs: this.userId },
-              $set: { track_changes: { [this.userId]: true } },
+              $addToSet: { reviewer_refs: ctx.userId },
+              $set: { track_changes: { [ctx.userId]: true } },
               $pull: {
               $pull: {
-                readOnly_refs: this.userId,
-                collaberator_refs: this.userId,
-                pendingEditor_refs: this.userId,
-                pendingReviewer_refs: this.userId,
+                readOnly_refs: ctx.userId,
+                collaberator_refs: ctx.userId,
+                pendingEditor_refs: ctx.userId,
+                pendingReviewer_refs: ctx.userId,
               },
               },
             }
             }
           )
           )
           .chain('exec')
           .chain('exec')
           .resolves({ matchedCount: 1 })
           .resolves({ matchedCount: 1 })
-        await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
-          this.project._id,
-          this.userId,
+        await ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
+          ctx.project._id,
+          ctx.userId,
           'review'
           'review'
         )
         )
       })
       })
     })
     })
 
 
     describe('sets a collaborator to reviewer when track changes is not enabled for everyone', function () {
     describe('sets a collaborator to reviewer when track changes is not enabled for everyone', function () {
-      beforeEach(function () {
-        this.ProjectGetter.promises.getProject = sinon.stub().resolves({
+      beforeEach(function (ctx) {
+        ctx.ProjectGetter.promises.getProject = sinon.stub().resolves({
           _id: new ObjectId(),
           _id: new ObjectId(),
-          owner_ref: this.addingUserId,
+          owner_ref: ctx.addingUserId,
           name: 'Foo',
           name: 'Foo',
           track_changes: {
           track_changes: {
-            [this.userId]: true,
+            [ctx.userId]: true,
           },
           },
         })
         })
       })
       })
-      it('should correctly update the project', async function () {
-        this.ProjectMock.expects('updateOne')
+      it('should correctly update the project', async function (ctx) {
+        ctx.ProjectMock.expects('updateOne')
           .withArgs(
           .withArgs(
             {
             {
-              _id: this.project._id,
+              _id: ctx.project._id,
               $or: [
               $or: [
-                { collaberator_refs: this.userId },
-                { readOnly_refs: this.userId },
-                { reviewer_refs: this.userId },
+                { collaberator_refs: ctx.userId },
+                { readOnly_refs: ctx.userId },
+                { reviewer_refs: ctx.userId },
               ],
               ],
             },
             },
             {
             {
-              $addToSet: { reviewer_refs: this.userId },
-              $set: { [`track_changes.${this.userId}`]: true },
+              $addToSet: { reviewer_refs: ctx.userId },
+              $set: { [`track_changes.${ctx.userId}`]: true },
               $pull: {
               $pull: {
-                readOnly_refs: this.userId,
-                collaberator_refs: this.userId,
-                pendingEditor_refs: this.userId,
-                pendingReviewer_refs: this.userId,
+                readOnly_refs: ctx.userId,
+                collaberator_refs: ctx.userId,
+                pendingEditor_refs: ctx.userId,
+                pendingReviewer_refs: ctx.userId,
               },
               },
             }
             }
           )
           )
           .chain('exec')
           .chain('exec')
           .resolves({ matchedCount: 1 })
           .resolves({ matchedCount: 1 })
-        await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
-          this.project._id,
-          this.userId,
+        await ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
+          ctx.project._id,
+          ctx.userId,
           'review'
           'review'
         )
         )
       })
       })
     })
     })
 
 
-    it('sets a collaborator to read-only as a pendingEditor', async function () {
-      this.ProjectMock.expects('updateOne')
+    it('sets a collaborator to read-only as a pendingEditor', async function (ctx) {
+      ctx.ProjectMock.expects('updateOne')
         .withArgs(
         .withArgs(
           {
           {
-            _id: this.project._id,
+            _id: ctx.project._id,
             $or: [
             $or: [
-              { collaberator_refs: this.userId },
-              { readOnly_refs: this.userId },
-              { reviewer_refs: this.userId },
+              { collaberator_refs: ctx.userId },
+              { readOnly_refs: ctx.userId },
+              { reviewer_refs: ctx.userId },
             ],
             ],
           },
           },
           {
           {
             $addToSet: {
             $addToSet: {
-              readOnly_refs: this.userId,
-              pendingEditor_refs: this.userId,
+              readOnly_refs: ctx.userId,
+              pendingEditor_refs: ctx.userId,
             },
             },
             $pull: {
             $pull: {
-              collaberator_refs: this.userId,
-              reviewer_refs: this.userId,
-              pendingReviewer_refs: this.userId,
+              collaberator_refs: ctx.userId,
+              reviewer_refs: ctx.userId,
+              pendingReviewer_refs: ctx.userId,
             },
             },
           }
           }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves({ matchedCount: 1 })
         .resolves({ matchedCount: 1 })
-      await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
-        this.project._id,
-        this.userId,
+      await ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
+        ctx.project._id,
+        ctx.userId,
         'readOnly',
         'readOnly',
         { pendingEditor: true }
         { pendingEditor: true }
       )
       )
     })
     })
 
 
-    it('sets a collaborator to read-only as a pendingReviewer', async function () {
-      this.ProjectMock.expects('updateOne')
+    it('sets a collaborator to read-only as a pendingReviewer', async function (ctx) {
+      ctx.ProjectMock.expects('updateOne')
         .withArgs(
         .withArgs(
           {
           {
-            _id: this.project._id,
+            _id: ctx.project._id,
             $or: [
             $or: [
-              { collaberator_refs: this.userId },
-              { readOnly_refs: this.userId },
-              { reviewer_refs: this.userId },
+              { collaberator_refs: ctx.userId },
+              { readOnly_refs: ctx.userId },
+              { reviewer_refs: ctx.userId },
             ],
             ],
           },
           },
           {
           {
             $addToSet: {
             $addToSet: {
-              readOnly_refs: this.userId,
-              pendingReviewer_refs: this.userId,
+              readOnly_refs: ctx.userId,
+              pendingReviewer_refs: ctx.userId,
             },
             },
             $pull: {
             $pull: {
-              collaberator_refs: this.userId,
-              reviewer_refs: this.userId,
-              pendingEditor_refs: this.userId,
+              collaberator_refs: ctx.userId,
+              reviewer_refs: ctx.userId,
+              pendingEditor_refs: ctx.userId,
             },
             },
           }
           }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves({ matchedCount: 1 })
         .resolves({ matchedCount: 1 })
-      await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
-        this.project._id,
-        this.userId,
+      await ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
+        ctx.project._id,
+        ctx.userId,
         'readOnly',
         'readOnly',
         { pendingReviewer: true }
         { pendingReviewer: true }
       )
       )
     })
     })
 
 
-    it('throws a NotFoundError if the project or collaborator does not exist', async function () {
-      this.ProjectMock.expects('updateOne')
+    it('throws a NotFoundError if the project or collaborator does not exist', async function (ctx) {
+      ctx.ProjectMock.expects('updateOne')
         .chain('exec')
         .chain('exec')
         .resolves({ matchedCount: 0 })
         .resolves({ matchedCount: 0 })
       await expect(
       await expect(
-        this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
-          this.project._id,
-          this.userId,
+        ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
+          ctx.project._id,
+          ctx.userId,
           'readAndWrite'
           'readAndWrite'
         )
         )
       ).to.be.rejectedWith(Errors.NotFoundError)
       ).to.be.rejectedWith(Errors.NotFoundError)

+ 6 - 3
services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs

@@ -22,9 +22,12 @@ describe('DocumentUpdaterController', function () {
       default: ctx.settings,
       default: ctx.settings,
     }))
     }))
 
 
-    vi.doMock('../../../../app/src/Features/Project/ProjectLocator.mjs', () => ({
-      default: ctx.ProjectLocator,
-    }))
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectLocator.mjs',
+      () => ({
+        default: ctx.ProjectLocator,
+      })
+    )
 
 
     vi.doMock(
     vi.doMock(
       '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.mjs',
       '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.mjs',

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 351 - 350
services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandler.test.mjs


+ 6 - 3
services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs

@@ -43,9 +43,12 @@ describe('ProjectZipStreamManager', function () {
       })
       })
     )
     )
 
 
-    vi.doMock('../../../../app/src/Features/History/HistoryManager.mjs', () => ({
-      default: (ctx.HistoryManager = {}),
-    }))
+    vi.doMock(
+      '../../../../app/src/Features/History/HistoryManager.mjs',
+      () => ({
+        default: (ctx.HistoryManager = {}),
+      })
+    )
 
 
     vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
     vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
       default: (ctx.ProjectGetter = {}),
       default: (ctx.ProjectGetter = {}),

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 542 - 539
services/web/test/unit/src/Editor/EditorController.test.mjs


+ 6 - 3
services/web/test/unit/src/Editor/EditorHttpController.test.mjs

@@ -168,9 +168,12 @@ describe('EditorHttpController', function () {
     vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
     vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
       vi.importActual('../../../../app/src/Features/Errors/Errors.js')
       vi.importActual('../../../../app/src/Features/Errors/Errors.js')
     )
     )
-    vi.doMock('../../../../app/src/Features/Project/ProjectDeleter.mjs', () => ({
-      default: ctx.ProjectDeleter,
-    }))
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectDeleter.mjs',
+      () => ({
+        default: ctx.ProjectDeleter,
+      })
+    )
     vi.doMock('../../../../app/src/Features/Project/ProjectGetter.mjs', () => ({
     vi.doMock('../../../../app/src/Features/Project/ProjectGetter.mjs', () => ({
       default: ctx.ProjectGetter,
       default: ctx.ProjectGetter,
     }))
     }))

+ 151 - 132
services/web/test/unit/src/FileStore/FileStoreHandler.test.mjs

@@ -1,13 +1,13 @@
-const { expect } = require('chai')
-const sinon = require('sinon')
-const SandboxedModule = require('sandboxed-module')
+import { beforeEach, describe, it, vi, expect } from 'vitest'
+import sinon from 'sinon'
 
 
-const MODULE_PATH = '../../../../app/src/Features/FileStore/FileStoreHandler.js'
+const MODULE_PATH =
+  '../../../../app/src/Features/FileStore/FileStoreHandler.mjs'
 
 
 describe('FileStoreHandler', function () {
 describe('FileStoreHandler', function () {
-  beforeEach(function () {
-    this.fileSize = 999
-    this.fs = {
+  beforeEach(async function (ctx) {
+    ctx.fileSize = 999
+    ctx.fs = {
       createReadStream: sinon.stub(),
       createReadStream: sinon.stub(),
       lstat: sinon.stub().callsArgWith(1, null, {
       lstat: sinon.stub().callsArgWith(1, null, {
         isFile() {
         isFile() {
@@ -16,10 +16,10 @@ describe('FileStoreHandler', function () {
         isDirectory() {
         isDirectory() {
           return false
           return false
         },
         },
-        size: this.fileSize,
+        size: ctx.fileSize,
       }),
       }),
     }
     }
-    this.writeStream = {
+    ctx.writeStream = {
       my: 'writeStream',
       my: 'writeStream',
       on(type, fn) {
       on(type, fn) {
         if (type === 'response') {
         if (type === 'response') {
@@ -27,25 +27,24 @@ describe('FileStoreHandler', function () {
         }
         }
       },
       },
     }
     }
-    this.readStream = { my: 'readStream', on: sinon.stub() }
-    this.request = sinon.stub()
-    this.request.head = sinon.stub()
-    this.filestoreUrl = 'http://filestore.overleaf.test'
-    this.settings = {
-      apis: { filestore: { url: this.filestoreUrl } },
+    ctx.readStream = { my: 'readStream', on: sinon.stub() }
+    ctx.request = sinon.stub()
+    ctx.request.head = sinon.stub()
+    ctx.filestoreUrl = 'http://filestore.overleaf.test'
+    ctx.settings = {
+      apis: { filestore: { url: ctx.filestoreUrl } },
     }
     }
-    this.hashValue = '0123456789'
-    this.fileArgs = { name: 'upload-filename' }
-    this.fileId = 'file_id_here'
-    this.projectId = '1312312312'
-    this.historyId = 123
-    this.hashValue = '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed'
-    this.fsPath = 'uploads/myfile.eps'
-    this.getFileUrl = (projectId, fileId) =>
-      `${this.filestoreUrl}/project/${projectId}/file/${fileId}`
-    this.getProjectUrl = projectId =>
-      `${this.filestoreUrl}/project/${projectId}`
-    this.FileModel = class File {
+    ctx.hashValue = '0123456789'
+    ctx.fileArgs = { name: 'upload-filename' }
+    ctx.fileId = 'file_id_here'
+    ctx.projectId = '1312312312'
+    ctx.historyId = 123
+    ctx.hashValue = '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed'
+    ctx.fsPath = 'uploads/myfile.eps'
+    ctx.getFileUrl = (projectId, fileId) =>
+      `${ctx.filestoreUrl}/project/${projectId}/file/${fileId}`
+    ctx.getProjectUrl = projectId => `${ctx.filestoreUrl}/project/${projectId}`
+    ctx.FileModel = class File {
       constructor(options) {
       constructor(options) {
         ;({ name: this.name, hash: this.hash } = options)
         ;({ name: this.name, hash: this.hash } = options)
         this._id = 'file_id_here'
         this._id = 'file_id_here'
@@ -55,53 +54,75 @@ describe('FileStoreHandler', function () {
         }
         }
       }
       }
     }
     }
-    this.FileHashManager = {
-      computeHash: sinon.stub().callsArgWith(1, null, this.hashValue),
+    ctx.FileHashManager = {
+      computeHash: sinon.stub().callsArgWith(1, null, ctx.hashValue),
     }
     }
-    this.HistoryManager = {
+    ctx.HistoryManager = {
       uploadBlobFromDisk: sinon.stub().callsArg(4),
       uploadBlobFromDisk: sinon.stub().callsArg(4),
     }
     }
-    this.ProjectDetailsHandler = {
+    ctx.ProjectDetailsHandler = {
       getDetails: sinon.stub().callsArgWith(1, null, {
       getDetails: sinon.stub().callsArgWith(1, null, {
-        overleaf: { history: { id: this.historyId } },
+        overleaf: { history: { id: ctx.historyId } },
       }),
       }),
     }
     }
 
 
-    this.Features = {
+    ctx.Features = {
       hasFeature: sinon.stub(),
       hasFeature: sinon.stub(),
     }
     }
 
 
-    this.Modules = {
+    ctx.Modules = {
       hooks: {
       hooks: {
         fire: sinon.stub().callsArgWith(2, null),
         fire: sinon.stub().callsArgWith(2, null),
       },
       },
     }
     }
 
 
-    this.handler = SandboxedModule.require(MODULE_PATH, {
-      requires: {
-        '@overleaf/settings': this.settings,
-        request: this.request,
-        '../History/HistoryManager': this.HistoryManager,
-        '../Project/ProjectDetailsHandler': this.ProjectDetailsHandler,
-        './FileHashManager': this.FileHashManager,
-        '../../infrastructure/Features': this.Features,
-        '../../infrastructure/Modules': this.Modules,
-        // FIXME: need to stub File object here
-        '../../models/File': {
-          File: this.FileModel,
-        },
-        fs: this.fs,
-      },
-    })
+    vi.doMock('@overleaf/settings', () => ({
+      default: ctx.settings,
+    }))
+
+    vi.doMock('request', () => ({
+      default: ctx.request,
+    }))
+
+    vi.doMock('../../../../app/src/Features/History/HistoryManager', () => ({
+      default: ctx.HistoryManager,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectDetailsHandler',
+      () => ({
+        default: ctx.ProjectDetailsHandler,
+      })
+    )
+
+    vi.doMock('../../../../app/src/Features/FileStore/FileHashManager', () => ({
+      default: ctx.FileHashManager,
+    }))
+
+    vi.doMock('../../../../app/src/infrastructure/Features', () => ({
+      default: ctx.Features,
+    }))
+
+    vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
+      default: ctx.Modules,
+    }))
+
+    vi.doMock('../../../../app/src/models/File', () => ({
+      File: ctx.FileModel,
+    }))
+
+    vi.doMock('node:fs', () => ({ default: ctx.fs }))
+
+    ctx.handler = (await import(MODULE_PATH)).default
   })
   })
 
 
   describe('uploadFileFromDisk', function () {
   describe('uploadFileFromDisk', function () {
-    beforeEach(function () {
-      this.request.returns(this.writeStream)
+    beforeEach(function (ctx) {
+      ctx.request.returns(ctx.writeStream)
     })
     })
 
 
-    it('should get the project details', async function () {
-      this.fs.createReadStream.returns({
+    it('should get the project details', async function (ctx) {
+      ctx.fs.createReadStream.returns({
         pipe() {},
         pipe() {},
         on(type, cb) {
         on(type, cb) {
           if (type === 'open') {
           if (type === 'open') {
@@ -109,18 +130,18 @@ describe('FileStoreHandler', function () {
           }
           }
         },
         },
       })
       })
-      await this.handler.promises.uploadFileFromDisk(
-        this.projectId,
-        this.fileArgs,
-        this.fsPath
+      await ctx.handler.promises.uploadFileFromDisk(
+        ctx.projectId,
+        ctx.fileArgs,
+        ctx.fsPath
       )
       )
-      this.ProjectDetailsHandler.getDetails
-        .calledWith(this.projectId)
+      ctx.ProjectDetailsHandler.getDetails
+        .calledWith(ctx.projectId)
         .should.equal(true)
         .should.equal(true)
     })
     })
 
 
-    it('should compute the file hash', async function () {
-      this.fs.createReadStream.returns({
+    it('should compute the file hash', async function (ctx) {
+      ctx.fs.createReadStream.returns({
         pipe() {},
         pipe() {},
         on(type, cb) {
         on(type, cb) {
           if (type === 'open') {
           if (type === 'open') {
@@ -128,18 +149,16 @@ describe('FileStoreHandler', function () {
           }
           }
         },
         },
       })
       })
-      await this.handler.promises.uploadFileFromDisk(
-        this.projectId,
-        this.fileArgs,
-        this.fsPath
+      await ctx.handler.promises.uploadFileFromDisk(
+        ctx.projectId,
+        ctx.fileArgs,
+        ctx.fsPath
       )
       )
-      this.FileHashManager.computeHash
-        .calledWith(this.fsPath)
-        .should.equal(true)
+      ctx.FileHashManager.computeHash.calledWith(ctx.fsPath).should.equal(true)
     })
     })
 
 
-    it('should call the preUploadFile hook', async function () {
-      this.fs.createReadStream.returns({
+    it('should call the preUploadFile hook', async function (ctx) {
+      ctx.fs.createReadStream.returns({
         pipe() {},
         pipe() {},
         on(type, cb) {
         on(type, cb) {
           if (type === 'open') {
           if (type === 'open') {
@@ -147,24 +166,24 @@ describe('FileStoreHandler', function () {
           }
           }
         },
         },
       })
       })
-      await this.handler.promises.uploadFileFromDisk(
-        this.projectId,
-        this.fileArgs,
-        this.fsPath
+      await ctx.handler.promises.uploadFileFromDisk(
+        ctx.projectId,
+        ctx.fileArgs,
+        ctx.fsPath
       )
       )
-      this.Modules.hooks.fire
+      ctx.Modules.hooks.fire
         .calledWith('preUploadFile', {
         .calledWith('preUploadFile', {
-          projectId: this.projectId,
-          historyId: this.historyId,
-          fileArgs: this.fileArgs,
-          fsPath: this.fsPath,
-          size: this.fileSize,
+          projectId: ctx.projectId,
+          historyId: ctx.historyId,
+          fileArgs: ctx.fileArgs,
+          fsPath: ctx.fsPath,
+          size: ctx.fileSize,
         })
         })
         .should.equal(true)
         .should.equal(true)
     })
     })
 
 
-    it('should upload the file to the history store as a blob', async function () {
-      this.fs.createReadStream.returns({
+    it('should upload the file to the history store as a blob', async function (ctx) {
+      ctx.fs.createReadStream.returns({
         pipe() {},
         pipe() {},
         on(type, cb) {
         on(type, cb) {
           if (type === 'open') {
           if (type === 'open') {
@@ -172,37 +191,37 @@ describe('FileStoreHandler', function () {
           }
           }
         },
         },
       })
       })
-      await this.handler.promises.uploadFileFromDisk(
-        this.projectId,
-        this.fileArgs,
-        this.fsPath
+      await ctx.handler.promises.uploadFileFromDisk(
+        ctx.projectId,
+        ctx.fileArgs,
+        ctx.fsPath
       )
       )
-      this.HistoryManager.uploadBlobFromDisk
-        .calledWith(this.historyId, this.hashValue, this.fileSize, this.fsPath)
+      ctx.HistoryManager.uploadBlobFromDisk
+        .calledWith(ctx.historyId, ctx.hashValue, ctx.fileSize, ctx.fsPath)
         .should.equal(true)
         .should.equal(true)
     })
     })
 
 
-    it('should not open file handle', async function () {
-      await this.handler.promises.uploadFileFromDisk(
-        this.projectId,
-        this.fileArgs,
-        this.fsPath
+    it('should not open file handle', async function (ctx) {
+      await ctx.handler.promises.uploadFileFromDisk(
+        ctx.projectId,
+        ctx.fileArgs,
+        ctx.fsPath
       )
       )
-      expect(this.fs.createReadStream).to.not.have.been.called
+      expect(ctx.fs.createReadStream).to.not.have.been.called
     })
     })
 
 
-    it('should not talk to filestore', async function () {
-      await this.handler.promises.uploadFileFromDisk(
-        this.projectId,
-        this.fileArgs,
-        this.fsPath
+    it('should not talk to filestore', async function (ctx) {
+      await ctx.handler.promises.uploadFileFromDisk(
+        ctx.projectId,
+        ctx.fileArgs,
+        ctx.fsPath
       )
       )
 
 
-      expect(this.request).to.not.have.been.called
+      expect(ctx.request).to.not.have.been.called
     })
     })
 
 
-    it('should call the postUploadFile hook', async function () {
-      this.fs.createReadStream.returns({
+    it('should call the postUploadFile hook', async function (ctx) {
+      ctx.fs.createReadStream.returns({
         pipe() {},
         pipe() {},
         on(type, cb) {
         on(type, cb) {
           if (type === 'open') {
           if (type === 'open') {
@@ -210,33 +229,33 @@ describe('FileStoreHandler', function () {
           }
           }
         },
         },
       })
       })
-      await this.handler.promises.uploadFileFromDisk(
-        this.projectId,
-        this.fileArgs,
-        this.fsPath
+      await ctx.handler.promises.uploadFileFromDisk(
+        ctx.projectId,
+        ctx.fileArgs,
+        ctx.fsPath
       )
       )
-      this.Modules.hooks.fire
+      ctx.Modules.hooks.fire
         .calledWith('postUploadFile', {
         .calledWith('postUploadFile', {
-          projectId: this.projectId,
-          fileRef: sinon.match.instanceOf(this.FileModel),
-          size: this.fileSize,
+          projectId: ctx.projectId,
+          fileRef: sinon.match.instanceOf(ctx.FileModel),
+          size: ctx.fileSize,
         })
         })
         .should.equal(true)
         .should.equal(true)
     })
     })
 
 
-    it('should resolve with the url and fileRef', async function () {
-      const { fileRef } = await this.handler.promises.uploadFileFromDisk(
-        this.projectId,
-        this.fileArgs,
-        this.fsPath
+    it('should resolve with the url and fileRef', async function (ctx) {
+      const { fileRef } = await ctx.handler.promises.uploadFileFromDisk(
+        ctx.projectId,
+        ctx.fileArgs,
+        ctx.fsPath
       )
       )
-      expect(fileRef._id).to.equal(this.fileId)
-      expect(fileRef.hash).to.equal(this.hashValue)
+      expect(fileRef._id).to.equal(ctx.fileId)
+      expect(fileRef.hash).to.equal(ctx.hashValue)
     })
     })
 
 
     describe('symlink', function () {
     describe('symlink', function () {
-      it('should not read file if it is symlink', async function () {
-        this.fs.lstat = sinon.stub().callsArgWith(1, null, {
+      it('should not read file if it is symlink', async function (ctx) {
+        ctx.fs.lstat = sinon.stub().callsArgWith(1, null, {
           isFile() {
           isFile() {
             return false
             return false
           },
           },
@@ -248,10 +267,10 @@ describe('FileStoreHandler', function () {
         let error
         let error
 
 
         try {
         try {
-          await this.handler.promises.uploadFileFromDisk(
-            this.projectId,
-            this.fileArgs,
-            this.fsPath
+          await ctx.handler.promises.uploadFileFromDisk(
+            ctx.projectId,
+            ctx.fileArgs,
+            ctx.fsPath
           )
           )
         } catch (err) {
         } catch (err) {
           error = err
           error = err
@@ -259,18 +278,18 @@ describe('FileStoreHandler', function () {
 
 
         expect(error).to.exist
         expect(error).to.exist
 
 
-        this.fs.createReadStream.called.should.equal(false)
+        ctx.fs.createReadStream.called.should.equal(false)
       })
       })
 
 
-      it('should not read file stat returns nothing', async function () {
-        this.fs.lstat = sinon.stub().callsArgWith(1, null, null)
+      it('should not read file stat returns nothing', async function (ctx) {
+        ctx.fs.lstat = sinon.stub().callsArgWith(1, null, null)
         let error
         let error
 
 
         try {
         try {
-          await this.handler.promises.uploadFileFromDisk(
-            this.projectId,
-            this.fileArgs,
-            this.fsPath
+          await ctx.handler.promises.uploadFileFromDisk(
+            ctx.projectId,
+            ctx.fileArgs,
+            ctx.fsPath
           )
           )
         } catch (err) {
         } catch (err) {
           error = err
           error = err
@@ -278,7 +297,7 @@ describe('FileStoreHandler', function () {
 
 
         expect(error).to.exist
         expect(error).to.exist
 
 
-        this.fs.createReadStream.called.should.equal(false)
+        ctx.fs.createReadStream.called.should.equal(false)
       })
       })
     })
     })
   })
   })

+ 165 - 167
services/web/test/unit/src/HelperFiles/AdminAuthorizationHelper.test.mjs

@@ -1,32 +1,35 @@
-const { expect } = require('chai')
-const SandboxedModule = require('sandboxed-module')
-const sinon = require('sinon')
-const MockRequest = require('../helpers/MockRequest')
-const MockResponse = require('../helpers/MockResponse')
+import { vi, expect } from 'vitest'
+import sinon from 'sinon'
+import MockRequest from '../helpers/MockRequest.js'
+import MockResponse from '../helpers/MockResponse.js'
 
 
 const modulePath =
 const modulePath =
   '../../../../app/src/Features/Helpers/AdminAuthorizationHelper'
   '../../../../app/src/Features/Helpers/AdminAuthorizationHelper'
 
 
 describe('AdminAuthorizationHelper', function () {
 describe('AdminAuthorizationHelper', function () {
-  beforeEach(function () {
-    this.fireHook = sinon.stub().resolves([])
-    this.settings = {
+  beforeEach(async function (ctx) {
+    ctx.fireHook = sinon.stub().resolves([])
+    ctx.settings = {
       adminPrivilegeAvailable: true,
       adminPrivilegeAvailable: true,
       adminUrl: 'https://admin.overleaf.com',
       adminUrl: 'https://admin.overleaf.com',
       adminRolesEnabled: true,
       adminRolesEnabled: true,
     }
     }
-    this.AdminAuthorizationHelper = SandboxedModule.require(modulePath, {
-      requires: {
-        '@overleaf/settings': this.settings,
-        '../../infrastructure/Modules': {
-          promises: {
-            hooks: {
-              fire: this.fireHook,
-            },
+
+    vi.doMock('@overleaf/settings', () => ({
+      default: ctx.settings,
+    }))
+
+    vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
+      default: {
+        promises: {
+          hooks: {
+            fire: ctx.fireHook,
           },
           },
         },
         },
       },
       },
-    })
+    }))
+
+    ctx.AdminAuthorizationHelper = (await import(modulePath)).default
   })
   })
   describe('getAdminCapabilities', function () {
   describe('getAdminCapabilities', function () {
     describe('when modules return capabilities', function () {
     describe('when modules return capabilities', function () {
@@ -34,9 +37,9 @@ describe('AdminAuthorizationHelper', function () {
       const module1Capabilities = ['capability1', 'capability2']
       const module1Capabilities = ['capability1', 'capability2']
       const module2Capabilities = ['capability2', 'capability3']
       const module2Capabilities = ['capability2', 'capability3']
 
 
-      beforeEach(async function () {
-        this.fireHook.resolves([module1Capabilities, module2Capabilities])
-        result = await this.AdminAuthorizationHelper.getAdminCapabilities({})
+      beforeEach(async function (ctx) {
+        ctx.fireHook.resolves([module1Capabilities, module2Capabilities])
+        result = await ctx.AdminAuthorizationHelper.getAdminCapabilities({})
       })
       })
       it('returns true for adminCapabilitiesAvailable', async function () {
       it('returns true for adminCapabilitiesAvailable', async function () {
         expect(result.adminCapabilitiesAvailable).to.be.true
         expect(result.adminCapabilitiesAvailable).to.be.true
@@ -49,8 +52,8 @@ describe('AdminAuthorizationHelper', function () {
     })
     })
     describe('when no module returns capabilities', function () {
     describe('when no module returns capabilities', function () {
       let result
       let result
-      beforeEach(async function () {
-        result = await this.AdminAuthorizationHelper.getAdminCapabilities({})
+      beforeEach(async function (ctx) {
+        result = await ctx.AdminAuthorizationHelper.getAdminCapabilities({})
       })
       })
 
 
       it('returns false for adminCapabilitiesAvailable', function () {
       it('returns false for adminCapabilitiesAvailable', function () {
@@ -64,204 +67,203 @@ describe('AdminAuthorizationHelper', function () {
   describe('useAdminCapabilities', function () {
   describe('useAdminCapabilities', function () {
     describe('when admin capabilities are not available', function () {
     describe('when admin capabilities are not available', function () {
       describe('user is null', function () {
       describe('user is null', function () {
-        beforeEach(async function () {
-          this.req = new MockRequest()
-          this.res = new MockResponse()
-          this.next = sinon.stub()
+        beforeEach(async function (ctx) {
+          ctx.req = new MockRequest()
+          ctx.res = new MockResponse()
+          ctx.next = sinon.stub()
 
 
-          this.req.session = {
+          ctx.req.session = {
             user: null,
             user: null,
           }
           }
 
 
-          await this.AdminAuthorizationHelper.useAdminCapabilities(
-            this.req,
-            this.res,
-            this.next
+          await ctx.AdminAuthorizationHelper.useAdminCapabilities(
+            ctx.req,
+            ctx.res,
+            ctx.next
           )
           )
         })
         })
-        it('does not define adminCapabilitiesAvailable on req', function () {
-          expect(this.req).not.to.have.property('adminCapabilitiesAvailable')
+        it('does not define adminCapabilitiesAvailable on req', function (ctx) {
+          expect(ctx.req).not.to.have.property('adminCapabilitiesAvailable')
         })
         })
-        it('defines adminCapabilities as an empty array on req', function () {
-          expect(this.req).to.have.property('adminCapabilities')
-          expect(this.req.adminCapabilities).to.be.an('array')
-          expect(this.req.adminCapabilities).to.be.empty
+        it('defines adminCapabilities as an empty array on req', function (ctx) {
+          expect(ctx.req).to.have.property('adminCapabilities')
+          expect(ctx.req.adminCapabilities).to.be.an('array')
+          expect(ctx.req.adminCapabilities).to.be.empty
         })
         })
       })
       })
       describe('user is not an admin', function () {
       describe('user is not an admin', function () {
-        beforeEach(async function () {
-          this.req = new MockRequest()
-          this.res = new MockResponse()
-          this.next = sinon.stub()
+        beforeEach(async function (ctx) {
+          ctx.req = new MockRequest()
+          ctx.res = new MockResponse()
+          ctx.next = sinon.stub()
 
 
-          this.user = {
+          ctx.user = {
             isAdmin: false,
             isAdmin: false,
           }
           }
 
 
-          this.req.session = {
-            user: this.user,
+          ctx.req.session = {
+            user: ctx.user,
           }
           }
 
 
-          await this.AdminAuthorizationHelper.useAdminCapabilities(
-            this.req,
-            this.res,
-            this.next
+          await ctx.AdminAuthorizationHelper.useAdminCapabilities(
+            ctx.req,
+            ctx.res,
+            ctx.next
           )
           )
         })
         })
-        it('does not define adminCapabilitiesAvailable on req', function () {
-          expect(this.req).not.to.have.property('adminCapabilitiesAvailable')
+        it('does not define adminCapabilitiesAvailable on req', function (ctx) {
+          expect(ctx.req).not.to.have.property('adminCapabilitiesAvailable')
         })
         })
-        it('defines adminCapabilities as an empty array on req', function () {
-          expect(this.req).to.have.property('adminCapabilities')
-          expect(this.req.adminCapabilities).to.be.an('array')
-          expect(this.req.adminCapabilities).to.be.empty
+        it('defines adminCapabilities as an empty array on req', function (ctx) {
+          expect(ctx.req).to.have.property('adminCapabilities')
+          expect(ctx.req.adminCapabilities).to.be.an('array')
+          expect(ctx.req.adminCapabilities).to.be.empty
         })
         })
       })
       })
       describe('user is an admin', function () {
       describe('user is an admin', function () {
-        beforeEach(async function () {
-          this.req = new MockRequest()
-          this.res = new MockResponse()
-          this.next = sinon.stub()
+        beforeEach(async function (ctx) {
+          ctx.req = new MockRequest()
+          ctx.res = new MockResponse()
+          ctx.next = sinon.stub()
 
 
-          this.user = {
+          ctx.user = {
             isAdmin: true,
             isAdmin: true,
           }
           }
 
 
-          this.req.session = {
-            user: this.user,
+          ctx.req.session = {
+            user: ctx.user,
           }
           }
 
 
-          await this.AdminAuthorizationHelper.useAdminCapabilities(
-            this.req,
-            this.res,
-            this.next
+          await ctx.AdminAuthorizationHelper.useAdminCapabilities(
+            ctx.req,
+            ctx.res,
+            ctx.next
           )
           )
         })
         })
 
 
-        it('defines adminCapabilitiesAvailable as false on req', function () {
-          expect(this.req).to.have.property('adminCapabilitiesAvailable', false)
+        it('defines adminCapabilitiesAvailable as false on req', function (ctx) {
+          expect(ctx.req).to.have.property('adminCapabilitiesAvailable', false)
         })
         })
 
 
-        it('defines adminCapabilities as an empty array', function () {
-          expect(this.req).to.have.property('adminCapabilities')
-          expect(this.req.adminCapabilities).to.be.an('array')
-          expect(this.req.adminCapabilities).to.be.empty
+        it('defines adminCapabilities as an empty array', function (ctx) {
+          expect(ctx.req).to.have.property('adminCapabilities')
+          expect(ctx.req.adminCapabilities).to.be.an('array')
+          expect(ctx.req.adminCapabilities).to.be.empty
         })
         })
       })
       })
     })
     })
     describe('when admin capabilities are available', function () {
     describe('when admin capabilities are available', function () {
-      beforeEach(function () {
-        this.fireHook.resolves(['capability1', 'capability2'])
+      beforeEach(function (ctx) {
+        ctx.fireHook.resolves(['capability1', 'capability2'])
       })
       })
       describe('user is not an admin', function () {
       describe('user is not an admin', function () {
-        beforeEach(async function () {
-          this.req = new MockRequest()
-          this.res = new MockResponse()
-          this.next = sinon.stub()
+        beforeEach(async function (ctx) {
+          ctx.req = new MockRequest()
+          ctx.res = new MockResponse()
+          ctx.next = sinon.stub()
 
 
-          this.user = {
+          ctx.user = {
             isAdmin: false,
             isAdmin: false,
           }
           }
 
 
-          this.req.session = {
-            user: this.user,
+          ctx.req.session = {
+            user: ctx.user,
           }
           }
 
 
-          await this.AdminAuthorizationHelper.useAdminCapabilities(
-            this.req,
-            this.res,
-            this.next
+          await ctx.AdminAuthorizationHelper.useAdminCapabilities(
+            ctx.req,
+            ctx.res,
+            ctx.next
           )
           )
         })
         })
-        it('does not define adminCapabilitiesAvailable on req', function () {
-          expect(this.req).not.to.have.property('adminCapabilitiesAvailable')
+        it('does not define adminCapabilitiesAvailable on req', function (ctx) {
+          expect(ctx.req).not.to.have.property('adminCapabilitiesAvailable')
         })
         })
-        it('defines adminCapabilities as an empty array on req', function () {
-          expect(this.req).to.have.property('adminCapabilities')
-          expect(this.req.adminCapabilities).to.be.an('array')
-          expect(this.req.adminCapabilities).to.be.empty
+        it('defines adminCapabilities as an empty array on req', function (ctx) {
+          expect(ctx.req).to.have.property('adminCapabilities')
+          expect(ctx.req.adminCapabilities).to.be.an('array')
+          expect(ctx.req.adminCapabilities).to.be.empty
         })
         })
       })
       })
       describe('user is an admin', function () {
       describe('user is an admin', function () {
-        beforeEach(async function () {
-          this.req = new MockRequest()
-          this.res = new MockResponse()
-          this.next = sinon.stub()
+        beforeEach(async function (ctx) {
+          ctx.req = new MockRequest()
+          ctx.res = new MockResponse()
+          ctx.next = sinon.stub()
 
 
-          this.user = {
+          ctx.user = {
             isAdmin: true,
             isAdmin: true,
           }
           }
 
 
-          this.req.session = {
-            user: this.user,
+          ctx.req.session = {
+            user: ctx.user,
           }
           }
 
 
-          await this.AdminAuthorizationHelper.useAdminCapabilities(
-            this.req,
-            this.res,
-            this.next
+          await ctx.AdminAuthorizationHelper.useAdminCapabilities(
+            ctx.req,
+            ctx.res,
+            ctx.next
           )
           )
         })
         })
 
 
-        it('defines adminCapabilitiesAvailable as true on req', function () {
-          expect(this.req).to.have.property('adminCapabilitiesAvailable', true)
+        it('defines adminCapabilitiesAvailable as true on req', function (ctx) {
+          expect(ctx.req).to.have.property('adminCapabilitiesAvailable', true)
         })
         })
-        it('defines adminCapabilities with the capabilities returned from modules', function () {
-          expect(this.req).to.have.property('adminCapabilities')
-          expect(this.req.adminCapabilities).to.be.an('array')
-          expect(this.req.adminCapabilities).to.include('capability1')
-          expect(this.req.adminCapabilities).to.include('capability2')
+        it('defines adminCapabilities with the capabilities returned from modules', function (ctx) {
+          expect(ctx.req).to.have.property('adminCapabilities')
+          expect(ctx.req.adminCapabilities).to.be.an('array')
+          expect(ctx.req.adminCapabilities).to.include('capability1')
+          expect(ctx.req.adminCapabilities).to.include('capability2')
         })
         })
       })
       })
     })
     })
     describe('when getting capabilities from modules throws an error', function () {
     describe('when getting capabilities from modules throws an error', function () {
-      beforeEach(async function () {
-        this.fireHook.rejects(new Error('Module error'))
+      beforeEach(async function (ctx) {
+        ctx.fireHook.rejects(new Error('Module error'))
 
 
-        this.req = new MockRequest()
-        this.res = new MockResponse()
-        this.next = sinon.stub()
+        ctx.req = new MockRequest()
+        ctx.res = new MockResponse()
+        ctx.next = sinon.stub()
 
 
-        this.user = {
+        ctx.user = {
           isAdmin: true,
           isAdmin: true,
         }
         }
 
 
-        this.req.logger = {
+        ctx.req.logger = {
           warn: sinon.stub(),
           warn: sinon.stub(),
         }
         }
 
 
-        this.req.session = {
-          user: this.user,
+        ctx.req.session = {
+          user: ctx.user,
         }
         }
 
 
-        await this.AdminAuthorizationHelper.useAdminCapabilities(
-          this.req,
-          this.res,
-          this.next
+        await ctx.AdminAuthorizationHelper.useAdminCapabilities(
+          ctx.req,
+          ctx.res,
+          ctx.next
         )
         )
       })
       })
-      it('logs the error', function () {
-        expect(this.logger.warn).to.have.been.calledWith(
-          sinon.match.has('err', sinon.match.instanceOf(Error))
-        )
+      it('logs the error', function (ctx) {
+        expect(ctx.logger.warn).toHaveBeenCalled()
+        expect(ctx.logger.warn.mock.calls[0][0].err).toBeInstanceOf(Error)
       })
       })
-      it('defines adminCapabilitiesAvailable as true on req', function () {
-        expect(this.req).to.have.property('adminCapabilitiesAvailable', true)
+      it('defines adminCapabilitiesAvailable as true on req', function (ctx) {
+        expect(ctx.req).to.have.property('adminCapabilitiesAvailable', true)
       })
       })
-      it('defines adminCapabilities as an empty array', function () {
-        expect(this.req).to.have.property('adminCapabilities')
-        expect(this.req.adminCapabilities).to.be.an('array')
-        expect(this.req.adminCapabilities).to.be.empty
+      it('defines adminCapabilities as an empty array', function (ctx) {
+        expect(ctx.req).to.have.property('adminCapabilities')
+        expect(ctx.req.adminCapabilities).to.be.an('array')
+        expect(ctx.req.adminCapabilities).to.be.empty
       })
       })
     })
     })
   })
   })
   describe('useHasAdminCapability', function () {
   describe('useHasAdminCapability', function () {
-    it('adds hasAdminCapability to res.locals', function () {
+    it('adds hasAdminCapability to res.locals', function (ctx) {
       const req = new MockRequest()
       const req = new MockRequest()
       const res = new MockResponse()
       const res = new MockResponse()
       const next = sinon.stub()
       const next = sinon.stub()
 
 
-      this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
+      ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
 
 
       expect(res.locals).to.have.property('hasAdminCapability')
       expect(res.locals).to.have.property('hasAdminCapability')
       expect(res.locals.hasAdminCapability).to.be.a('function')
       expect(res.locals.hasAdminCapability).to.be.a('function')
@@ -269,7 +271,7 @@ describe('AdminAuthorizationHelper', function () {
 
 
     describe('when the user is not an admin', function () {
     describe('when the user is not an admin', function () {
       describe('when req.adminCapabilitiesAvailable is true', function () {
       describe('when req.adminCapabilitiesAvailable is true', function () {
-        it('returns false for any capability', function () {
+        it('returns false for any capability', function (ctx) {
           const req = new MockRequest()
           const req = new MockRequest()
           const res = new MockResponse()
           const res = new MockResponse()
           const next = sinon.stub()
           const next = sinon.stub()
@@ -279,14 +281,14 @@ describe('AdminAuthorizationHelper', function () {
 
 
           req.session.user = { isAdmin: false }
           req.session.user = { isAdmin: false }
 
 
-          this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
+          ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
 
 
           expect(res.locals.hasAdminCapability('capability1')).to.be.false
           expect(res.locals.hasAdminCapability('capability1')).to.be.false
         })
         })
       })
       })
 
 
       describe('when req.adminCapabilitiesAvailable is false', function () {
       describe('when req.adminCapabilitiesAvailable is false', function () {
-        it('returns false for any capability', function () {
+        it('returns false for any capability', function (ctx) {
           const req = new MockRequest()
           const req = new MockRequest()
           const res = new MockResponse()
           const res = new MockResponse()
           const next = sinon.stub()
           const next = sinon.stub()
@@ -296,21 +298,21 @@ describe('AdminAuthorizationHelper', function () {
 
 
           req.session.user = { isAdmin: false }
           req.session.user = { isAdmin: false }
 
 
-          this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
+          ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
 
 
           expect(res.locals.hasAdminCapability('capability1')).to.be.false
           expect(res.locals.hasAdminCapability('capability1')).to.be.false
         })
         })
       })
       })
 
 
       describe('when req.adminCapabilitiesAvailable is undefined', function () {
       describe('when req.adminCapabilitiesAvailable is undefined', function () {
-        it('returns false for any capability', function () {
+        it('returns false for any capability', function (ctx) {
           const req = new MockRequest()
           const req = new MockRequest()
           const res = new MockResponse()
           const res = new MockResponse()
           const next = sinon.stub()
           const next = sinon.stub()
 
 
           req.session.user = { isAdmin: false }
           req.session.user = { isAdmin: false }
 
 
-          this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
+          ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
 
 
           expect(res.locals.hasAdminCapability('capability1')).to.be.false
           expect(res.locals.hasAdminCapability('capability1')).to.be.false
         })
         })
@@ -319,7 +321,7 @@ describe('AdminAuthorizationHelper', function () {
 
 
     describe('user is an admin', function () {
     describe('user is an admin', function () {
       describe('when req.adminCapabilitiesAvailable is false', function () {
       describe('when req.adminCapabilitiesAvailable is false', function () {
-        it('returns true for any capability', function () {
+        it('returns true for any capability', function (ctx) {
           const req = new MockRequest()
           const req = new MockRequest()
           const res = new MockResponse()
           const res = new MockResponse()
           const next = sinon.stub()
           const next = sinon.stub()
@@ -327,21 +329,21 @@ describe('AdminAuthorizationHelper', function () {
           req.session.user = { isAdmin: true }
           req.session.user = { isAdmin: true }
           req.adminCapabilitiesAvailable = false
           req.adminCapabilitiesAvailable = false
 
 
-          this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
+          ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
 
 
           expect(res.locals.hasAdminCapability('capability1')).to.be.true
           expect(res.locals.hasAdminCapability('capability1')).to.be.true
         })
         })
       })
       })
 
 
       describe('when req.adminCapabilitiesAvailable is undefined', function () {
       describe('when req.adminCapabilitiesAvailable is undefined', function () {
-        it('returns true for any capability', function () {
+        it('returns true for any capability', function (ctx) {
           const req = new MockRequest()
           const req = new MockRequest()
           const res = new MockResponse()
           const res = new MockResponse()
           const next = sinon.stub()
           const next = sinon.stub()
 
 
           req.session.user = { isAdmin: true }
           req.session.user = { isAdmin: true }
 
 
-          this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
+          ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
 
 
           expect(res.locals.hasAdminCapability('capability1')).to.be.true
           expect(res.locals.hasAdminCapability('capability1')).to.be.true
         })
         })
@@ -349,7 +351,7 @@ describe('AdminAuthorizationHelper', function () {
 
 
       describe('when req.adminCapabilitiesAvailable is true', function () {
       describe('when req.adminCapabilitiesAvailable is true', function () {
         let req, res, next
         let req, res, next
-        beforeEach(function () {
+        beforeEach(function (ctx) {
           req = new MockRequest()
           req = new MockRequest()
           res = new MockResponse()
           res = new MockResponse()
           next = sinon.stub()
           next = sinon.stub()
@@ -358,7 +360,7 @@ describe('AdminAuthorizationHelper', function () {
           req.adminCapabilitiesAvailable = true
           req.adminCapabilitiesAvailable = true
           req.adminCapabilities = ['capability1', 'capability2']
           req.adminCapabilities = ['capability1', 'capability2']
 
 
-          this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
+          ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
         })
         })
 
 
         it('returns true for a capability the user has', function () {
         it('returns true for a capability the user has', function () {
@@ -373,20 +375,20 @@ describe('AdminAuthorizationHelper', function () {
   })
   })
   describe('hasAdminCapability', function () {
   describe('hasAdminCapability', function () {
     describe('when user is not an admin', function () {
     describe('when user is not an admin', function () {
-      it('returns false', function () {
+      it('returns false', function (ctx) {
         const req = {
         const req = {
           session: {
           session: {
             user: { isAdmin: false },
             user: { isAdmin: false },
           },
           },
         }
         }
         expect(
         expect(
-          this.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
+          ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
         ).to.be.false
         ).to.be.false
       })
       })
     })
     })
     describe('when user is an admin', function () {
     describe('when user is an admin', function () {
       describe('when adminCapabilitiesAvailable is falsey', function () {
       describe('when adminCapabilitiesAvailable is falsey', function () {
-        it('returns true', function () {
+        it('returns true', function (ctx) {
           const req = {
           const req = {
             session: {
             session: {
               user: { isAdmin: true },
               user: { isAdmin: true },
@@ -394,22 +396,22 @@ describe('AdminAuthorizationHelper', function () {
             adminCapabilitiesAvailable: false,
             adminCapabilitiesAvailable: false,
           }
           }
           expect(
           expect(
-            this.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
+            ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
           ).to.be.true
           ).to.be.true
         })
         })
-        it('ignores the "requireAdminRoles" argument', function () {
+        it('ignores the "requireAdminRoles" argument', function (ctx) {
           const req = {
           const req = {
             session: { user: { isAdmin: true } },
             session: { user: { isAdmin: true } },
             adminCapabilitiesAvailable: false,
             adminCapabilitiesAvailable: false,
           }
           }
           expect(
           expect(
-            this.AdminAuthorizationHelper.hasAdminCapability(
+            ctx.AdminAuthorizationHelper.hasAdminCapability(
               'capability',
               'capability',
               true
               true
             )(req)
             )(req)
           ).to.be.true
           ).to.be.true
           expect(
           expect(
-            this.AdminAuthorizationHelper.hasAdminCapability(
+            ctx.AdminAuthorizationHelper.hasAdminCapability(
               'capability',
               'capability',
               false
               false
             )(req)
             )(req)
@@ -418,30 +420,26 @@ describe('AdminAuthorizationHelper', function () {
       })
       })
       describe('when adminCapabilitiesAvailable is true', function () {
       describe('when adminCapabilitiesAvailable is true', function () {
         describe('when user has the requested capability', function () {
         describe('when user has the requested capability', function () {
-          it('returns true', function () {
+          it('returns true', function (ctx) {
             const req = {
             const req = {
               session: { user: { isAdmin: true } },
               session: { user: { isAdmin: true } },
               adminCapabilitiesAvailable: true,
               adminCapabilitiesAvailable: true,
               adminCapabilities: ['capability'],
               adminCapabilities: ['capability'],
             }
             }
             expect(
             expect(
-              this.AdminAuthorizationHelper.hasAdminCapability('capability')(
-                req
-              )
+              ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
             ).to.be.true
             ).to.be.true
           })
           })
         })
         })
         describe('when user does not have the requested capability', function () {
         describe('when user does not have the requested capability', function () {
-          it('returns false', function () {
+          it('returns false', function (ctx) {
             const req = {
             const req = {
               session: { user: { isAdmin: true } },
               session: { user: { isAdmin: true } },
               adminCapabilitiesAvailable: true,
               adminCapabilitiesAvailable: true,
               adminCapabilities: ['other-capability'],
               adminCapabilities: ['other-capability'],
             }
             }
             expect(
             expect(
-              this.AdminAuthorizationHelper.hasAdminCapability('capability')(
-                req
-              )
+              ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
             ).to.be.false
             ).to.be.false
           })
           })
         })
         })
@@ -449,26 +447,26 @@ describe('AdminAuthorizationHelper', function () {
     })
     })
 
 
     describe('when admin roles are not enabled', function () {
     describe('when admin roles are not enabled', function () {
-      beforeEach(function () {
-        this.settings.adminRolesEnabled = false
+      beforeEach(function (ctx) {
+        ctx.settings.adminRolesEnabled = false
       })
       })
 
 
-      it('returns false even for admins', function () {
+      it('returns false even for admins', function (ctx) {
         const req = { session: { user: { isAdmin: true } } }
         const req = { session: { user: { isAdmin: true } } }
         expect(
         expect(
-          this.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
+          ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
         ).to.be.false
         ).to.be.false
         expect(
         expect(
-          this.AdminAuthorizationHelper.hasAdminCapability(
+          ctx.AdminAuthorizationHelper.hasAdminCapability(
             'capability',
             'capability',
             true
             true
           )(req)
           )(req)
         ).to.be.false
         ).to.be.false
       })
       })
-      it('returns true when requireAdminRoles=false', function () {
+      it('returns true when requireAdminRoles=false', function (ctx) {
         const req = { session: { user: { isAdmin: true } } }
         const req = { session: { user: { isAdmin: true } } }
         expect(
         expect(
-          this.AdminAuthorizationHelper.hasAdminCapability(
+          ctx.AdminAuthorizationHelper.hasAdminCapability(
             'capability',
             'capability',
             false
             false
           )(req)
           )(req)

+ 6 - 3
services/web/test/unit/src/History/HistoryController.test.mjs

@@ -82,9 +82,12 @@ describe('HistoryController', function () {
       })
       })
     )
     )
 
 
-    vi.doMock('../../../../app/src/Features/History/HistoryManager.mjs', () => ({
-      default: ctx.HistoryManager,
-    }))
+    vi.doMock(
+      '../../../../app/src/Features/History/HistoryManager.mjs',
+      () => ({
+        default: ctx.HistoryManager,
+      })
+    )
 
 
     vi.doMock(
     vi.doMock(
       '../../../../app/src/Features/Project/ProjectDetailsHandler.mjs',
       '../../../../app/src/Features/Project/ProjectDetailsHandler.mjs',

+ 8 - 6
services/web/test/unit/src/History/HistoryManagerTests.mjs

@@ -1,12 +1,14 @@
-const { expect } = require('chai')
-const sinon = require('sinon')
-const SandboxedModule = require('sandboxed-module')
-const { ObjectId } = require('mongodb-legacy')
-const {
+import { expect } from 'chai'
+import sinon from 'sinon'
+import SandboxedModule from 'sandboxed-module'
+import mongodb from 'mongodb-legacy'
+import {
   cleanupTestDatabase,
   cleanupTestDatabase,
   db,
   db,
   waitForDb,
   waitForDb,
-} = require('../../../../app/src/infrastructure/mongodb')
+} from '../../../../app/src/infrastructure/mongodb.js'
+
+const { ObjectId } = mongodb
 
 
 const MODULE_PATH = '../../../../app/src/Features/History/HistoryManager'
 const MODULE_PATH = '../../../../app/src/Features/History/HistoryManager'
 
 

+ 51 - 48
services/web/test/unit/src/History/RestoreManager.test.mjs

@@ -23,61 +23,64 @@ describe('RestoreManager', function () {
       default: Errors,
       default: Errors,
     }))
     }))
 
 
-    vi.doMock('../../../../app/src/Features/History/HistoryManager.mjs', () => ({
-      default: (ctx.HistoryManager = {
-        promises: {
-          getContentAtVersion: sinon.stub().resolves({
-            // Raw snapshot data that will be passed to Snapshot.fromRaw
-            files: {
-              'main.tex': {
-                hash: 'abcdef1234567890abcdef1234567890abcdef12',
-                stringLength: 100,
-                metadata: {
-                  editorId: 'test-editor',
+    vi.doMock(
+      '../../../../app/src/Features/History/HistoryManager.mjs',
+      () => ({
+        default: (ctx.HistoryManager = {
+          promises: {
+            getContentAtVersion: sinon.stub().resolves({
+              // Raw snapshot data that will be passed to Snapshot.fromRaw
+              files: {
+                'main.tex': {
+                  hash: 'abcdef1234567890abcdef1234567890abcdef12',
+                  stringLength: 100,
+                  metadata: {
+                    editorId: 'test-editor',
+                  },
                 },
                 },
-              },
-              'foo.tex': {
-                hash: 'abcdef1234567890abcdef1234567890abcdef12',
-                stringLength: 100,
-                metadata: {
-                  editorId: 'test-editor',
+                'foo.tex': {
+                  hash: 'abcdef1234567890abcdef1234567890abcdef12',
+                  stringLength: 100,
+                  metadata: {
+                    editorId: 'test-editor',
+                  },
                 },
                 },
-              },
-              'folder/file.tex': {
-                hash: 'abcdef1234567890abcdef1234567890abcdef12',
-                stringLength: 100,
-                metadata: {
-                  editorId: 'test-editor',
+                'folder/file.tex': {
+                  hash: 'abcdef1234567890abcdef1234567890abcdef12',
+                  stringLength: 100,
+                  metadata: {
+                    editorId: 'test-editor',
+                  },
                 },
                 },
-              },
-              'foo.png': {
-                hash: 'abcdef1234567890abcdef1234567890abcdef12',
-                stringLength: 100,
-                metadata: {
-                  provider: 'bar',
+                'foo.png': {
+                  hash: 'abcdef1234567890abcdef1234567890abcdef12',
+                  stringLength: 100,
+                  metadata: {
+                    provider: 'bar',
+                  },
                 },
                 },
-              },
-              'linkedFile.bib': {
-                hash: 'abcdef1234567890abcdef1234567890abcdef12',
-                stringLength: 100,
-                metadata: {
-                  provider: 'mendeley',
+                'linkedFile.bib': {
+                  hash: 'abcdef1234567890abcdef1234567890abcdef12',
+                  stringLength: 100,
+                  metadata: {
+                    provider: 'mendeley',
+                  },
                 },
                 },
-              },
-              'withMainTrue.tex': {
-                hash: 'abcdef1234567890abcdef1234567890abcdef12',
-                stringLength: 100,
-                metadata: {
-                  main: true,
+                'withMainTrue.tex': {
+                  hash: 'abcdef1234567890abcdef1234567890abcdef12',
+                  stringLength: 100,
+                  metadata: {
+                    main: true,
+                  },
                 },
                 },
               },
               },
-            },
-            timestamp: new Date().toISOString(),
-          }),
-          requestBlob: sinon.stub().resolves({ stream: ctx.blobStream }),
-        },
-      }),
-    }))
+              timestamp: new Date().toISOString(),
+            }),
+            requestBlob: sinon.stub().resolves({ stream: ctx.blobStream }),
+          },
+        }),
+      })
+    )
 
 
     vi.doMock('../../../../app/src/infrastructure/Metrics.js', () => ({
     vi.doMock('../../../../app/src/infrastructure/Metrics.js', () => ({
       default: {
       default: {

+ 375 - 315
services/web/test/unit/src/Project/ProjectDeleter.test.mjs

@@ -1,33 +1,40 @@
+import { vi, expect } from 'vitest'
+import sinon from 'sinon'
+import tk from 'timekeeper'
+import moment from 'moment'
+import indirectlyImportModels from '../helpers/indirectlyImportModels.js'
+import mongodb from 'mongodb-legacy'
+import Errors from '../../../../app/src/Features/Errors/Errors.js'
 const modulePath = '../../../../app/src/Features/Project/ProjectDeleter'
 const modulePath = '../../../../app/src/Features/Project/ProjectDeleter'
-const SandboxedModule = require('sandboxed-module')
-const sinon = require('sinon')
-const { expect } = require('chai')
-const tk = require('timekeeper')
-const moment = require('moment')
-const { Project } = require('../helpers/models/Project')
-const { DeletedProject } = require('../helpers/models/DeletedProject')
-const { ObjectId, ReadPreference } = require('mongodb-legacy')
-const Errors = require('../../../../app/src/Features/Errors/Errors')
+
+const { Project, DeletedProject } = indirectlyImportModels([
+  'Project',
+  'DeletedProject',
+])
+const { ObjectId, ReadPreference } = mongodb
+vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
+  vi.importActual('../../../../app/src/Features/Errors/Errors.js')
+)
 
 
 describe('ProjectDeleter', function () {
 describe('ProjectDeleter', function () {
-  beforeEach(function () {
+  beforeEach(async function (ctx) {
     tk.freeze(Date.now())
     tk.freeze(Date.now())
-    this.ip = '192.170.18.1'
-    this.project = dummyProject()
-    this.user = {
+    ctx.ip = '192.170.18.1'
+    ctx.project = dummyProject()
+    ctx.user = {
       _id: '588f3ddae8ebc1bac07c9fa4',
       _id: '588f3ddae8ebc1bac07c9fa4',
       first_name: 'bjkdsjfk',
       first_name: 'bjkdsjfk',
       features: {},
       features: {},
     }
     }
 
 
-    this.doc = {
+    ctx.doc = {
       _id: '5bd975f54f62e803cb8a8fec',
       _id: '5bd975f54f62e803cb8a8fec',
       lines: ['a bunch of lines', 'for a sunny day', 'in London town'],
       lines: ['a bunch of lines', 'for a sunny day', 'in London town'],
       ranges: {},
       ranges: {},
       project_id: '5cf9270b4eff6e186cf8b05e',
       project_id: '5cf9270b4eff6e186cf8b05e',
     }
     }
 
 
-    this.deletedProjects = [
+    ctx.deletedProjects = [
       {
       {
         _id: '5cf7f145c1401f0ca0eb1aaa',
         _id: '5cf7f145c1401f0ca0eb1aaa',
         deleterData: {
         deleterData: {
@@ -36,7 +43,7 @@ describe('ProjectDeleter', function () {
           deleterId: '588f3ddae8ebc1bac07c9fa4',
           deleterId: '588f3ddae8ebc1bac07c9fa4',
           deleterIpAddress: '172.19.0.1',
           deleterIpAddress: '172.19.0.1',
           deletedProjectId: '5cf9270b4eff6e186cf8b05e',
           deletedProjectId: '5cf9270b4eff6e186cf8b05e',
-          deletedProjectOwnerId: this.user._id,
+          deletedProjectOwnerId: ctx.user._id,
         },
         },
         project: {
         project: {
           _id: '5cf9270b4eff6e186cf8b05e',
           _id: '5cf9270b4eff6e186cf8b05e',
@@ -62,171 +69,224 @@ describe('ProjectDeleter', function () {
       },
       },
     ]
     ]
 
 
-    this.DocumentUpdaterHandler = {
+    ctx.DocumentUpdaterHandler = {
       promises: {
       promises: {
         flushProjectToMongoAndDelete: sinon.stub().resolves(),
         flushProjectToMongoAndDelete: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.EditorRealTimeController = {
+    ctx.EditorRealTimeController = {
       emitToRoom: sinon.stub(),
       emitToRoom: sinon.stub(),
     }
     }
-    this.TagsHandler = {
+    ctx.TagsHandler = {
       promises: {
       promises: {
         removeProjectFromAllTags: sinon.stub().resolves(),
         removeProjectFromAllTags: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.CollaboratorsHandler = {
+    ctx.CollaboratorsHandler = {
       promises: {
       promises: {
         removeUserFromAllProjects: sinon.stub().resolves(),
         removeUserFromAllProjects: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.CollaboratorsGetter = {
+    ctx.CollaboratorsGetter = {
       promises: {
       promises: {
         getMemberIds: sinon
         getMemberIds: sinon
           .stub()
           .stub()
-          .withArgs(this.project._id)
+          .withArgs(ctx.project._id)
           .resolves(['member-id-1', 'member-id-2']),
           .resolves(['member-id-1', 'member-id-2']),
       },
       },
     }
     }
 
 
-    this.ProjectDetailsHandler = {
+    ctx.ProjectDetailsHandler = {
       promises: {
       promises: {
-        generateUniqueName: sinon.stub().resolves(this.project.name),
+        generateUniqueName: sinon.stub().resolves(ctx.project.name),
       },
       },
     }
     }
 
 
-    this.db = {
+    ctx.db = {
       projects: {
       projects: {
         insertOne: sinon.stub().resolves(),
         insertOne: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.DocstoreManager = {
+    ctx.DocstoreManager = {
       promises: {
       promises: {
         archiveProject: sinon.stub().resolves(),
         archiveProject: sinon.stub().resolves(),
         destroyProject: sinon.stub().resolves(),
         destroyProject: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.HistoryManager = {
+    ctx.HistoryManager = {
       promises: {
       promises: {
         deleteProject: sinon.stub().resolves(),
         deleteProject: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.ProjectMock = sinon.mock(Project)
-    this.DeletedProjectMock = sinon.mock(DeletedProject)
-    this.Features = {
+    ctx.ProjectMock = sinon.mock(Project)
+    ctx.DeletedProjectMock = sinon.mock(DeletedProject)
+    ctx.Features = {
       hasFeature: sinon.stub().returns(true),
       hasFeature: sinon.stub().returns(true),
     }
     }
-    this.ChatApiHandler = {
+    ctx.ChatApiHandler = {
       promises: {
       promises: {
         destroyProject: sinon.stub().resolves(),
         destroyProject: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.ProjectAuditLogEntry = {
+    ctx.ProjectAuditLogEntry = {
       deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
       deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
     }
     }
-    this.ProjectDeleter = SandboxedModule.require(modulePath, {
-      requires: {
-        '../../infrastructure/Modules': {
-          promises: { hooks: { fire: sinon.stub().resolves() } },
-        },
-        '../../infrastructure/Features': this.Features,
-        '../Editor/EditorRealTimeController': this.EditorRealTimeController,
-        '../../models/Project': { Project },
-        '../../models/DeletedProject': { DeletedProject },
-        '../DocumentUpdater/DocumentUpdaterHandler':
-          this.DocumentUpdaterHandler,
-        '../Tags/TagsHandler': this.TagsHandler,
-        '../Chat/ChatApiHandler': this.ChatApiHandler,
-        '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler,
-        '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
-        '../Docstore/DocstoreManager': this.DocstoreManager,
-        './ProjectDetailsHandler': this.ProjectDetailsHandler,
-        '../../infrastructure/mongodb': {
-          db: this.db,
-          ObjectId,
-          READ_PREFERENCE_SECONDARY: ReadPreference.secondaryPreferred.mode,
-        },
-        '../History/HistoryManager': this.HistoryManager,
-        '../../models/ProjectAuditLogEntry': {
-          ProjectAuditLogEntry: this.ProjectAuditLogEntry,
-        },
+
+    vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
+      default: {
+        promises: { hooks: { fire: sinon.stub().resolves() } },
       },
       },
-    })
+    }))
+
+    vi.doMock('../../../../app/src/infrastructure/Features', () => ({
+      default: ctx.Features,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Editor/EditorRealTimeController',
+      () => ({
+        default: ctx.EditorRealTimeController,
+      })
+    )
+
+    vi.doMock('../../../../app/src/models/Project', () => ({
+      Project,
+    }))
+
+    vi.doMock('../../../../app/src/models/DeletedProject', () => ({
+      DeletedProject,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler',
+      () => ({
+        default: ctx.DocumentUpdaterHandler,
+      })
+    )
+
+    vi.doMock('../../../../app/src/Features/Tags/TagsHandler', () => ({
+      default: ctx.TagsHandler,
+    }))
+
+    vi.doMock('../../../../app/src/Features/Chat/ChatApiHandler', () => ({
+      default: ctx.ChatApiHandler,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Collaborators/CollaboratorsHandler',
+      () => ({
+        default: ctx.CollaboratorsHandler,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/Collaborators/CollaboratorsGetter',
+      () => ({
+        default: ctx.CollaboratorsGetter,
+      })
+    )
+
+    vi.doMock('../../../../app/src/Features/Docstore/DocstoreManager', () => ({
+      default: ctx.DocstoreManager,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectDetailsHandler',
+      () => ({
+        default: ctx.ProjectDetailsHandler,
+      })
+    )
+
+    vi.doMock('../../../../app/src/infrastructure/mongodb', () => ({
+      db: ctx.db,
+      ObjectId,
+      READ_PREFERENCE_SECONDARY: ReadPreference.secondaryPreferred.mode,
+    }))
+
+    vi.doMock('../../../../app/src/Features/History/HistoryManager', () => ({
+      default: ctx.HistoryManager,
+    }))
+
+    vi.doMock('../../../../app/src/models/ProjectAuditLogEntry', () => ({
+      ProjectAuditLogEntry: ctx.ProjectAuditLogEntry,
+    }))
+
+    ctx.ProjectDeleter = (await import(modulePath)).default
   })
   })
 
 
-  afterEach(function () {
+  afterEach(function (ctx) {
     tk.reset()
     tk.reset()
-    this.DeletedProjectMock.restore()
-    this.ProjectMock.restore()
+    ctx.DeletedProjectMock.restore()
+    ctx.ProjectMock.restore()
   })
   })
 
 
   describe('mark as deleted by external source', function () {
   describe('mark as deleted by external source', function () {
-    beforeEach(function () {
-      this.ProjectMock.expects('updateOne')
+    beforeEach(function (ctx) {
+      ctx.ProjectMock.expects('updateOne')
         .withArgs(
         .withArgs(
-          { _id: this.project._id },
+          { _id: ctx.project._id },
           { deletedByExternalDataSource: true }
           { deletedByExternalDataSource: true }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves()
         .resolves()
     })
     })
 
 
-    it('should update the project with the flag set to true', async function () {
-      await this.ProjectDeleter.promises.markAsDeletedByExternalSource(
-        this.project._id
+    it('should update the project with the flag set to true', async function (ctx) {
+      await ctx.ProjectDeleter.promises.markAsDeletedByExternalSource(
+        ctx.project._id
       )
       )
-      this.ProjectMock.verify()
+      ctx.ProjectMock.verify()
     })
     })
 
 
-    it('should tell the editor controler so users are notified', async function () {
-      await this.ProjectDeleter.promises.markAsDeletedByExternalSource(
-        this.project._id
+    it('should tell the editor controler so users are notified', async function (ctx) {
+      await ctx.ProjectDeleter.promises.markAsDeletedByExternalSource(
+        ctx.project._id
       )
       )
-      expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith(
-        this.project._id,
+      expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith(
+        ctx.project._id,
         'projectRenamedOrDeletedByExternalSource'
         'projectRenamedOrDeletedByExternalSource'
       )
       )
     })
     })
   })
   })
 
 
   describe('unmarkAsDeletedByExternalSource', function () {
   describe('unmarkAsDeletedByExternalSource', function () {
-    beforeEach(async function () {
-      this.ProjectMock.expects('updateOne')
+    beforeEach(async function (ctx) {
+      ctx.ProjectMock.expects('updateOne')
         .withArgs(
         .withArgs(
-          { _id: this.project._id },
+          { _id: ctx.project._id },
           { deletedByExternalDataSource: false }
           { deletedByExternalDataSource: false }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves()
         .resolves()
-      await this.ProjectDeleter.promises.unmarkAsDeletedByExternalSource(
-        this.project._id
+      await ctx.ProjectDeleter.promises.unmarkAsDeletedByExternalSource(
+        ctx.project._id
       )
       )
     })
     })
 
 
-    it('should remove the flag from the project', function () {
-      this.ProjectMock.verify()
+    it('should remove the flag from the project', function (ctx) {
+      ctx.ProjectMock.verify()
     })
     })
   })
   })
 
 
   describe('deleteUsersProjects', function () {
   describe('deleteUsersProjects', function () {
-    beforeEach(function () {
-      this.projects = [dummyProject(), dummyProject()]
-      this.ProjectMock.expects('find')
-        .withArgs({ owner_ref: this.user._id })
+    beforeEach(function (ctx) {
+      ctx.projects = [dummyProject(), dummyProject()]
+      ctx.ProjectMock.expects('find')
+        .withArgs({ owner_ref: ctx.user._id })
         .chain('exec')
         .chain('exec')
-        .resolves(this.projects)
-      for (const project of this.projects) {
-        this.ProjectMock.expects('findOne')
+        .resolves(ctx.projects)
+      for (const project of ctx.projects) {
+        ctx.ProjectMock.expects('findOne')
           .withArgs({ _id: project._id })
           .withArgs({ _id: project._id })
           .chain('exec')
           .chain('exec')
           .resolves(project)
           .resolves(project)
-        this.ProjectMock.expects('deleteOne')
+        ctx.ProjectMock.expects('deleteOne')
           .withArgs({ _id: project._id })
           .withArgs({ _id: project._id })
           .chain('exec')
           .chain('exec')
           .resolves()
           .resolves()
-        this.DeletedProjectMock.expects('updateOne')
+        ctx.DeletedProjectMock.expects('updateOne')
           .withArgs(
           .withArgs(
             { 'deleterData.deletedProjectId': project._id },
             { 'deleterData.deletedProjectId': project._id },
             {
             {
@@ -239,146 +299,146 @@ describe('ProjectDeleter', function () {
       }
       }
     })
     })
 
 
-    it('should delete all projects owned by the user', async function () {
-      await this.ProjectDeleter.promises.deleteUsersProjects(this.user._id)
-      this.ProjectMock.verify()
-      this.DeletedProjectMock.verify()
+    it('should delete all projects owned by the user', async function (ctx) {
+      await ctx.ProjectDeleter.promises.deleteUsersProjects(ctx.user._id)
+      ctx.ProjectMock.verify()
+      ctx.DeletedProjectMock.verify()
     })
     })
 
 
-    it('should remove any collaboration from this user', async function () {
-      await this.ProjectDeleter.promises.deleteUsersProjects(this.user._id)
+    it('should remove any collaboration from this user', async function (ctx) {
+      await ctx.ProjectDeleter.promises.deleteUsersProjects(ctx.user._id)
       sinon.assert.calledWith(
       sinon.assert.calledWith(
-        this.CollaboratorsHandler.promises.removeUserFromAllProjects,
-        this.user._id
+        ctx.CollaboratorsHandler.promises.removeUserFromAllProjects,
+        ctx.user._id
       )
       )
       sinon.assert.calledOnce(
       sinon.assert.calledOnce(
-        this.CollaboratorsHandler.promises.removeUserFromAllProjects
+        ctx.CollaboratorsHandler.promises.removeUserFromAllProjects
       )
       )
     })
     })
   })
   })
 
 
   describe('deleteProject', function () {
   describe('deleteProject', function () {
-    beforeEach(function () {
-      this.deleterData = {
+    beforeEach(function (ctx) {
+      ctx.deleterData = {
         deletedAt: new Date(),
         deletedAt: new Date(),
-        deletedProjectId: this.project._id,
-        deletedProjectOwnerId: this.project.owner_ref,
-        deletedProjectCollaboratorIds: this.project.collaberator_refs,
-        deletedProjectReadOnlyIds: this.project.readOnly_refs,
-        deletedProjectReviewerIds: this.project.reviewer_refs,
+        deletedProjectId: ctx.project._id,
+        deletedProjectOwnerId: ctx.project.owner_ref,
+        deletedProjectCollaboratorIds: ctx.project.collaberator_refs,
+        deletedProjectReadOnlyIds: ctx.project.readOnly_refs,
+        deletedProjectReviewerIds: ctx.project.reviewer_refs,
         deletedProjectReadWriteTokenAccessIds:
         deletedProjectReadWriteTokenAccessIds:
-          this.project.tokenAccessReadAndWrite_refs,
+          ctx.project.tokenAccessReadAndWrite_refs,
         deletedProjectReadOnlyTokenAccessIds:
         deletedProjectReadOnlyTokenAccessIds:
-          this.project.tokenAccessReadOnly_refs,
-        deletedProjectReadWriteToken: this.project.tokens.readAndWrite,
-        deletedProjectReadOnlyToken: this.project.tokens.readOnly,
-        deletedProjectOverleafId: this.project.overleaf.id,
-        deletedProjectOverleafHistoryId: this.project.overleaf.history.id,
-        deletedProjectLastUpdatedAt: this.project.lastUpdated,
+          ctx.project.tokenAccessReadOnly_refs,
+        deletedProjectReadWriteToken: ctx.project.tokens.readAndWrite,
+        deletedProjectReadOnlyToken: ctx.project.tokens.readOnly,
+        deletedProjectOverleafId: ctx.project.overleaf.id,
+        deletedProjectOverleafHistoryId: ctx.project.overleaf.history.id,
+        deletedProjectLastUpdatedAt: ctx.project.lastUpdated,
       }
       }
 
 
-      this.ProjectMock.expects('findOne')
-        .withArgs({ _id: this.project._id })
+      ctx.ProjectMock.expects('findOne')
+        .withArgs({ _id: ctx.project._id })
         .chain('exec')
         .chain('exec')
-        .resolves(this.project)
+        .resolves(ctx.project)
     })
     })
 
 
-    it('should save a DeletedProject with additional deleterData', async function () {
-      this.deleterData.deleterIpAddress = this.ip
-      this.deleterData.deleterId = this.user._id
+    it('should save a DeletedProject with additional deleterData', async function (ctx) {
+      ctx.deleterData.deleterIpAddress = ctx.ip
+      ctx.deleterData.deleterId = ctx.user._id
 
 
-      this.ProjectMock.expects('deleteOne').chain('exec').resolves()
-      this.DeletedProjectMock.expects('updateOne')
+      ctx.ProjectMock.expects('deleteOne').chain('exec').resolves()
+      ctx.DeletedProjectMock.expects('updateOne')
         .withArgs(
         .withArgs(
-          { 'deleterData.deletedProjectId': this.project._id },
+          { 'deleterData.deletedProjectId': ctx.project._id },
           {
           {
-            project: this.project,
-            deleterData: this.deleterData,
+            project: ctx.project,
+            deleterData: ctx.deleterData,
           },
           },
           { upsert: true }
           { upsert: true }
         )
         )
         .resolves()
         .resolves()
 
 
-      await this.ProjectDeleter.promises.deleteProject(this.project._id, {
-        deleterUser: this.user,
-        ipAddress: this.ip,
+      await ctx.ProjectDeleter.promises.deleteProject(ctx.project._id, {
+        deleterUser: ctx.user,
+        ipAddress: ctx.ip,
       })
       })
-      this.DeletedProjectMock.verify()
+      ctx.DeletedProjectMock.verify()
     })
     })
 
 
-    it('should flushProjectToMongoAndDelete in doc updater', async function () {
-      this.ProjectMock.expects('deleteOne').chain('exec').resolves()
-      this.DeletedProjectMock.expects('updateOne').resolves()
+    it('should flushProjectToMongoAndDelete in doc updater', async function (ctx) {
+      ctx.ProjectMock.expects('deleteOne').chain('exec').resolves()
+      ctx.DeletedProjectMock.expects('updateOne').resolves()
 
 
-      await this.ProjectDeleter.promises.deleteProject(this.project._id, {
-        deleterUser: this.user,
-        ipAddress: this.ip,
+      await ctx.ProjectDeleter.promises.deleteProject(ctx.project._id, {
+        deleterUser: ctx.user,
+        ipAddress: ctx.ip,
       })
       })
-      this.DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete
-        .calledWith(this.project._id)
+      ctx.DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete
+        .calledWith(ctx.project._id)
         .should.equal(true)
         .should.equal(true)
     })
     })
 
 
-    it('should flush docs out of mongo', async function () {
-      this.ProjectMock.expects('deleteOne').chain('exec').resolves()
-      this.DeletedProjectMock.expects('updateOne').resolves()
-      await this.ProjectDeleter.promises.deleteProject(this.project._id, {
-        deleterUser: this.user,
-        ipAddress: this.ip,
+    it('should flush docs out of mongo', async function (ctx) {
+      ctx.ProjectMock.expects('deleteOne').chain('exec').resolves()
+      ctx.DeletedProjectMock.expects('updateOne').resolves()
+      await ctx.ProjectDeleter.promises.deleteProject(ctx.project._id, {
+        deleterUser: ctx.user,
+        ipAddress: ctx.ip,
       })
       })
       expect(
       expect(
-        this.DocstoreManager.promises.archiveProject
-      ).to.have.been.calledWith(this.project._id)
+        ctx.DocstoreManager.promises.archiveProject
+      ).to.have.been.calledWith(ctx.project._id)
     })
     })
 
 
-    it('should flush docs out of mongo and ignore errors', async function () {
-      this.ProjectMock.expects('deleteOne').chain('exec').resolves()
-      this.DeletedProjectMock.expects('updateOne').resolves()
-      this.DocstoreManager.promises.archiveProject.rejects(new Error('foo'))
-      await this.ProjectDeleter.promises.deleteProject(this.project._id, {
-        deleterUser: this.user,
-        ipAddress: this.ip,
+    it('should flush docs out of mongo and ignore errors', async function (ctx) {
+      ctx.ProjectMock.expects('deleteOne').chain('exec').resolves()
+      ctx.DeletedProjectMock.expects('updateOne').resolves()
+      ctx.DocstoreManager.promises.archiveProject.rejects(new Error('foo'))
+      await ctx.ProjectDeleter.promises.deleteProject(ctx.project._id, {
+        deleterUser: ctx.user,
+        ipAddress: ctx.ip,
       })
       })
     })
     })
 
 
-    it('should removeProjectFromAllTags', async function () {
-      this.ProjectMock.expects('deleteOne').chain('exec').resolves()
-      this.DeletedProjectMock.expects('updateOne').resolves()
+    it('should removeProjectFromAllTags', async function (ctx) {
+      ctx.ProjectMock.expects('deleteOne').chain('exec').resolves()
+      ctx.DeletedProjectMock.expects('updateOne').resolves()
 
 
-      await this.ProjectDeleter.promises.deleteProject(this.project._id)
+      await ctx.ProjectDeleter.promises.deleteProject(ctx.project._id)
       sinon.assert.calledWith(
       sinon.assert.calledWith(
-        this.TagsHandler.promises.removeProjectFromAllTags,
+        ctx.TagsHandler.promises.removeProjectFromAllTags,
         'member-id-1',
         'member-id-1',
-        this.project._id
+        ctx.project._id
       )
       )
       sinon.assert.calledWith(
       sinon.assert.calledWith(
-        this.TagsHandler.promises.removeProjectFromAllTags,
+        ctx.TagsHandler.promises.removeProjectFromAllTags,
         'member-id-2',
         'member-id-2',
-        this.project._id
+        ctx.project._id
       )
       )
     })
     })
 
 
-    it('should remove the project from Mongo', async function () {
-      this.ProjectMock.expects('deleteOne')
-        .withArgs({ _id: this.project._id })
+    it('should remove the project from Mongo', async function (ctx) {
+      ctx.ProjectMock.expects('deleteOne')
+        .withArgs({ _id: ctx.project._id })
         .chain('exec')
         .chain('exec')
         .resolves()
         .resolves()
-      this.DeletedProjectMock.expects('updateOne').resolves()
+      ctx.DeletedProjectMock.expects('updateOne').resolves()
 
 
-      await this.ProjectDeleter.promises.deleteProject(this.project._id)
-      this.ProjectMock.verify()
+      await ctx.ProjectDeleter.promises.deleteProject(ctx.project._id)
+      ctx.ProjectMock.verify()
     })
     })
   })
   })
 
 
   describe('expireDeletedProjectsAfterDuration', function () {
   describe('expireDeletedProjectsAfterDuration', function () {
-    beforeEach(async function () {
-      for (const deletedProject of this.deletedProjects) {
-        this.ProjectMock.expects('findById')
+    beforeEach(async function (ctx) {
+      for (const deletedProject of ctx.deletedProjects) {
+        ctx.ProjectMock.expects('findById')
           .withArgs(deletedProject.deleterData.deletedProjectId)
           .withArgs(deletedProject.deleterData.deletedProjectId)
           .chain('exec')
           .chain('exec')
           .resolves(null)
           .resolves(null)
       }
       }
-      this.DeletedProjectMock.expects('find')
+      ctx.DeletedProjectMock.expects('find')
         .withArgs({
         .withArgs({
           'deleterData.deletedAt': {
           'deleterData.deletedAt': {
             $lt: new Date(moment().subtract(90, 'days')),
             $lt: new Date(moment().subtract(90, 'days')),
@@ -388,16 +448,16 @@ describe('ProjectDeleter', function () {
           },
           },
         })
         })
         .chain('exec')
         .chain('exec')
-        .resolves(this.deletedProjects)
+        .resolves(ctx.deletedProjects)
 
 
-      for (const deletedProject of this.deletedProjects) {
-        this.DeletedProjectMock.expects('findOne')
+      for (const deletedProject of ctx.deletedProjects) {
+        ctx.DeletedProjectMock.expects('findOne')
           .withArgs({
           .withArgs({
             'deleterData.deletedProjectId': deletedProject.project._id,
             'deleterData.deletedProjectId': deletedProject.project._id,
           })
           })
           .chain('exec')
           .chain('exec')
           .resolves(deletedProject)
           .resolves(deletedProject)
-        this.DeletedProjectMock.expects('updateOne')
+        ctx.DeletedProjectMock.expects('updateOne')
           .withArgs(
           .withArgs(
             {
             {
               _id: deletedProject._id,
               _id: deletedProject._id,
@@ -413,25 +473,25 @@ describe('ProjectDeleter', function () {
           .resolves()
           .resolves()
       }
       }
 
 
-      await this.ProjectDeleter.promises.expireDeletedProjectsAfterDuration()
+      await ctx.ProjectDeleter.promises.expireDeletedProjectsAfterDuration()
     })
     })
 
 
-    it('should expire projects older than 90 days', function () {
-      this.DeletedProjectMock.verify()
+    it('should expire projects older than 90 days', function (ctx) {
+      ctx.DeletedProjectMock.verify()
     })
     })
   })
   })
 
 
   describe('expireDeletedProject', function () {
   describe('expireDeletedProject', function () {
     describe('on an inactive project', function () {
     describe('on an inactive project', function () {
-      beforeEach(async function () {
-        this.ProjectMock.expects('findById')
-          .withArgs(this.deletedProjects[0].deleterData.deletedProjectId)
+      beforeEach(async function (ctx) {
+        ctx.ProjectMock.expects('findById')
+          .withArgs(ctx.deletedProjects[0].deleterData.deletedProjectId)
           .chain('exec')
           .chain('exec')
           .resolves(null)
           .resolves(null)
-        this.DeletedProjectMock.expects('updateOne')
+        ctx.DeletedProjectMock.expects('updateOne')
           .withArgs(
           .withArgs(
             {
             {
-              _id: this.deletedProjects[0]._id,
+              _id: ctx.deletedProjects[0]._id,
             },
             },
             {
             {
               $set: {
               $set: {
@@ -443,54 +503,54 @@ describe('ProjectDeleter', function () {
           .chain('exec')
           .chain('exec')
           .resolves()
           .resolves()
 
 
-        this.DeletedProjectMock.expects('findOne')
+        ctx.DeletedProjectMock.expects('findOne')
           .withArgs({
           .withArgs({
-            'deleterData.deletedProjectId': this.deletedProjects[0].project._id,
+            'deleterData.deletedProjectId': ctx.deletedProjects[0].project._id,
           })
           })
           .chain('exec')
           .chain('exec')
-          .resolves(this.deletedProjects[0])
+          .resolves(ctx.deletedProjects[0])
 
 
-        await this.ProjectDeleter.promises.expireDeletedProject(
-          this.deletedProjects[0].project._id
+        await ctx.ProjectDeleter.promises.expireDeletedProject(
+          ctx.deletedProjects[0].project._id
         )
         )
       })
       })
 
 
-      it('should find the specified deletedProject and remove its project and ip address', function () {
-        this.DeletedProjectMock.verify()
+      it('should find the specified deletedProject and remove its project and ip address', function (ctx) {
+        ctx.DeletedProjectMock.verify()
       })
       })
 
 
-      it('should destroy the docs in docstore', function () {
+      it('should destroy the docs in docstore', function (ctx) {
         expect(
         expect(
-          this.DocstoreManager.promises.destroyProject
-        ).to.have.been.calledWith(this.deletedProjects[0].project._id)
+          ctx.DocstoreManager.promises.destroyProject
+        ).to.have.been.calledWith(ctx.deletedProjects[0].project._id)
       })
       })
 
 
-      it('should delete the project in history', function () {
+      it('should delete the project in history', function (ctx) {
         expect(
         expect(
-          this.HistoryManager.promises.deleteProject
+          ctx.HistoryManager.promises.deleteProject
         ).to.have.been.calledWith(
         ).to.have.been.calledWith(
-          this.deletedProjects[0].project._id,
-          this.deletedProjects[0].project.overleaf.history.id
+          ctx.deletedProjects[0].project._id,
+          ctx.deletedProjects[0].project.overleaf.history.id
         )
         )
       })
       })
 
 
-      it('should destroy the chat threads and messages', function () {
+      it('should destroy the chat threads and messages', function (ctx) {
         expect(
         expect(
-          this.ChatApiHandler.promises.destroyProject
-        ).to.have.been.calledWith(this.deletedProjects[0].project._id)
+          ctx.ChatApiHandler.promises.destroyProject
+        ).to.have.been.calledWith(ctx.deletedProjects[0].project._id)
       })
       })
 
 
-      it('should delete audit logs', async function () {
-        expect(this.ProjectAuditLogEntry.deleteMany).to.have.been.calledWith({
-          projectId: this.deletedProjects[0].project._id,
+      it('should delete audit logs', async function (ctx) {
+        expect(ctx.ProjectAuditLogEntry.deleteMany).to.have.been.calledWith({
+          projectId: ctx.deletedProjects[0].project._id,
         })
         })
       })
       })
 
 
-      it('should log a completed deletion', async function () {
-        expect(this.logger.info).to.have.been.calledWith(
+      it('should log a completed deletion', async function (ctx) {
+        expect(ctx.logger.info).toHaveBeenCalledWith(
           {
           {
-            projectId: this.deletedProjects[0].project._id,
-            userId: this.user._id,
+            projectId: ctx.deletedProjects[0].project._id,
+            userId: ctx.user._id,
           },
           },
           'expired deleted project successfully'
           'expired deleted project successfully'
         )
         )
@@ -498,131 +558,131 @@ describe('ProjectDeleter', function () {
     })
     })
 
 
     describe('on an active project (from an incomplete delete)', function () {
     describe('on an active project (from an incomplete delete)', function () {
-      beforeEach(async function () {
-        this.ProjectMock.expects('findById')
-          .withArgs(this.deletedProjects[0].deleterData.deletedProjectId)
+      beforeEach(async function (ctx) {
+        ctx.ProjectMock.expects('findById')
+          .withArgs(ctx.deletedProjects[0].deleterData.deletedProjectId)
           .chain('exec')
           .chain('exec')
-          .resolves(this.deletedProjects[0].project)
-        this.DeletedProjectMock.expects('deleteOne')
+          .resolves(ctx.deletedProjects[0].project)
+        ctx.DeletedProjectMock.expects('deleteOne')
           .withArgs({
           .withArgs({
-            'deleterData.deletedProjectId': this.deletedProjects[0].project._id,
+            'deleterData.deletedProjectId': ctx.deletedProjects[0].project._id,
           })
           })
           .chain('exec')
           .chain('exec')
           .resolves()
           .resolves()
-        await this.ProjectDeleter.promises.expireDeletedProject(
-          this.deletedProjects[0].project._id
+        await ctx.ProjectDeleter.promises.expireDeletedProject(
+          ctx.deletedProjects[0].project._id
         )
         )
       })
       })
 
 
-      it('should delete the spurious deleted project record', function () {
-        this.DeletedProjectMock.verify()
+      it('should delete the spurious deleted project record', function (ctx) {
+        ctx.DeletedProjectMock.verify()
       })
       })
 
 
-      it('should not destroy the docs in docstore', function () {
-        expect(this.DocstoreManager.promises.destroyProject).to.not.have.been
+      it('should not destroy the docs in docstore', function (ctx) {
+        expect(ctx.DocstoreManager.promises.destroyProject).to.not.have.been
           .called
           .called
       })
       })
 
 
-      it('should not delete the project in history', function () {
-        expect(this.HistoryManager.promises.deleteProject).to.not.have.been
+      it('should not delete the project in history', function (ctx) {
+        expect(ctx.HistoryManager.promises.deleteProject).to.not.have.been
           .called
           .called
       })
       })
 
 
-      it('should not destroy the chat threads and messages', function () {
-        expect(this.ChatApiHandler.promises.destroyProject).to.not.have.been
+      it('should not destroy the chat threads and messages', function (ctx) {
+        expect(ctx.ChatApiHandler.promises.destroyProject).to.not.have.been
           .called
           .called
       })
       })
     })
     })
   })
   })
 
 
   describe('archiveProject', function () {
   describe('archiveProject', function () {
-    beforeEach(function () {
-      this.ProjectMock.expects('updateOne')
+    beforeEach(function (ctx) {
+      ctx.ProjectMock.expects('updateOne')
         .withArgs(
         .withArgs(
-          { _id: this.project._id },
+          { _id: ctx.project._id },
           {
           {
-            $addToSet: { archived: new ObjectId(this.user._id) },
-            $pull: { trashed: new ObjectId(this.user._id) },
+            $addToSet: { archived: new ObjectId(ctx.user._id) },
+            $pull: { trashed: new ObjectId(ctx.user._id) },
           }
           }
         )
         )
         .resolves()
         .resolves()
     })
     })
 
 
-    it('should update the project', async function () {
-      await this.ProjectDeleter.promises.archiveProject(
-        this.project._id,
-        this.user._id
+    it('should update the project', async function (ctx) {
+      await ctx.ProjectDeleter.promises.archiveProject(
+        ctx.project._id,
+        ctx.user._id
       )
       )
-      this.ProjectMock.verify()
+      ctx.ProjectMock.verify()
     })
     })
   })
   })
 
 
   describe('unarchiveProject', function () {
   describe('unarchiveProject', function () {
-    beforeEach(function () {
-      this.ProjectMock.expects('updateOne')
+    beforeEach(function (ctx) {
+      ctx.ProjectMock.expects('updateOne')
         .withArgs(
         .withArgs(
-          { _id: this.project._id },
-          { $pull: { archived: new ObjectId(this.user._id) } }
+          { _id: ctx.project._id },
+          { $pull: { archived: new ObjectId(ctx.user._id) } }
         )
         )
         .resolves()
         .resolves()
     })
     })
 
 
-    it('should update the project', async function () {
-      await this.ProjectDeleter.promises.unarchiveProject(
-        this.project._id,
-        this.user._id
+    it('should update the project', async function (ctx) {
+      await ctx.ProjectDeleter.promises.unarchiveProject(
+        ctx.project._id,
+        ctx.user._id
       )
       )
-      this.ProjectMock.verify()
+      ctx.ProjectMock.verify()
     })
     })
   })
   })
 
 
   describe('trashProject', function () {
   describe('trashProject', function () {
-    beforeEach(function () {
-      this.ProjectMock.expects('updateOne')
+    beforeEach(function (ctx) {
+      ctx.ProjectMock.expects('updateOne')
         .withArgs(
         .withArgs(
-          { _id: this.project._id },
+          { _id: ctx.project._id },
           {
           {
-            $addToSet: { trashed: new ObjectId(this.user._id) },
-            $pull: { archived: new ObjectId(this.user._id) },
+            $addToSet: { trashed: new ObjectId(ctx.user._id) },
+            $pull: { archived: new ObjectId(ctx.user._id) },
           }
           }
         )
         )
         .resolves()
         .resolves()
     })
     })
 
 
-    it('should update the project', async function () {
-      await this.ProjectDeleter.promises.trashProject(
-        this.project._id,
-        this.user._id
+    it('should update the project', async function (ctx) {
+      await ctx.ProjectDeleter.promises.trashProject(
+        ctx.project._id,
+        ctx.user._id
       )
       )
-      this.ProjectMock.verify()
+      ctx.ProjectMock.verify()
     })
     })
   })
   })
 
 
   describe('untrashProject', function () {
   describe('untrashProject', function () {
-    beforeEach(function () {
-      this.ProjectMock.expects('updateOne')
+    beforeEach(function (ctx) {
+      ctx.ProjectMock.expects('updateOne')
         .withArgs(
         .withArgs(
-          { _id: this.project._id },
-          { $pull: { trashed: new ObjectId(this.user._id) } }
+          { _id: ctx.project._id },
+          { $pull: { trashed: new ObjectId(ctx.user._id) } }
         )
         )
         .resolves()
         .resolves()
     })
     })
 
 
-    it('should update the project', async function () {
-      await this.ProjectDeleter.promises.untrashProject(
-        this.project._id,
-        this.user._id
+    it('should update the project', async function (ctx) {
+      await ctx.ProjectDeleter.promises.untrashProject(
+        ctx.project._id,
+        ctx.user._id
       )
       )
-      this.ProjectMock.verify()
+      ctx.ProjectMock.verify()
     })
     })
   })
   })
 
 
   describe('restoreProject', function () {
   describe('restoreProject', function () {
-    beforeEach(function () {
-      this.ProjectMock.expects('updateOne')
+    beforeEach(function (ctx) {
+      ctx.ProjectMock.expects('updateOne')
         .withArgs(
         .withArgs(
           {
           {
-            _id: this.project._id,
+            _id: ctx.project._id,
           },
           },
           {
           {
             $unset: { archived: true },
             $unset: { archived: true },
@@ -632,115 +692,115 @@ describe('ProjectDeleter', function () {
         .resolves()
         .resolves()
     })
     })
 
 
-    it('should unset the archive attribute', async function () {
-      await this.ProjectDeleter.promises.restoreProject(this.project._id)
+    it('should unset the archive attribute', async function (ctx) {
+      await ctx.ProjectDeleter.promises.restoreProject(ctx.project._id)
     })
     })
   })
   })
 
 
   describe('undeleteProject', function () {
   describe('undeleteProject', function () {
-    beforeEach(function () {
-      this.unknownProjectId = new ObjectId()
-      this.purgedProjectId = new ObjectId()
+    beforeEach(function (ctx) {
+      ctx.unknownProjectId = new ObjectId()
+      ctx.purgedProjectId = new ObjectId()
 
 
-      this.deletedProject = {
+      ctx.deletedProject = {
         _id: 'deleted',
         _id: 'deleted',
-        project: this.project,
+        project: ctx.project,
         deleterData: {
         deleterData: {
-          deletedProjectId: this.project._id,
-          deletedProjectOwnerId: this.project.owner_ref,
+          deletedProjectId: ctx.project._id,
+          deletedProjectOwnerId: ctx.project.owner_ref,
         },
         },
       }
       }
-      this.purgedProject = {
+      ctx.purgedProject = {
         _id: 'purged',
         _id: 'purged',
         deleterData: {
         deleterData: {
-          deletedProjectId: this.purgedProjectId,
+          deletedProjectId: ctx.purgedProjectId,
           deletedProjectOwnerId: 'potato',
           deletedProjectOwnerId: 'potato',
         },
         },
       }
       }
 
 
-      this.DeletedProjectMock.expects('findOne')
-        .withArgs({ 'deleterData.deletedProjectId': this.project._id })
+      ctx.DeletedProjectMock.expects('findOne')
+        .withArgs({ 'deleterData.deletedProjectId': ctx.project._id })
         .chain('exec')
         .chain('exec')
-        .resolves(this.deletedProject)
-      this.DeletedProjectMock.expects('findOne')
-        .withArgs({ 'deleterData.deletedProjectId': this.purgedProjectId })
+        .resolves(ctx.deletedProject)
+      ctx.DeletedProjectMock.expects('findOne')
+        .withArgs({ 'deleterData.deletedProjectId': ctx.purgedProjectId })
         .chain('exec')
         .chain('exec')
-        .resolves(this.purgedProject)
-      this.DeletedProjectMock.expects('findOne')
-        .withArgs({ 'deleterData.deletedProjectId': this.unknownProjectId })
+        .resolves(ctx.purgedProject)
+      ctx.DeletedProjectMock.expects('findOne')
+        .withArgs({ 'deleterData.deletedProjectId': ctx.unknownProjectId })
         .chain('exec')
         .chain('exec')
         .resolves(null)
         .resolves(null)
-      this.DeletedProjectMock.expects('deleteOne').chain('exec').resolves()
+      ctx.DeletedProjectMock.expects('deleteOne').chain('exec').resolves()
     })
     })
 
 
-    it('should return not found if the project does not exist', async function () {
+    it('should return not found if the project does not exist', async function (ctx) {
       await expect(
       await expect(
-        this.ProjectDeleter.promises.undeleteProject(
-          this.unknownProjectId.toString()
+        ctx.ProjectDeleter.promises.undeleteProject(
+          ctx.unknownProjectId.toString()
         )
         )
       ).to.be.rejectedWith(Errors.NotFoundError, 'project_not_found')
       ).to.be.rejectedWith(Errors.NotFoundError, 'project_not_found')
     })
     })
 
 
-    it('should return not found if the project has been expired', async function () {
+    it('should return not found if the project has been expired', async function (ctx) {
       await expect(
       await expect(
-        this.ProjectDeleter.promises.undeleteProject(
-          this.purgedProjectId.toString()
+        ctx.ProjectDeleter.promises.undeleteProject(
+          ctx.purgedProjectId.toString()
         )
         )
       ).to.be.rejectedWith(Errors.NotFoundError, 'project_too_old_to_restore')
       ).to.be.rejectedWith(Errors.NotFoundError, 'project_too_old_to_restore')
     })
     })
 
 
-    it('should insert the project into the collection', async function () {
-      await this.ProjectDeleter.promises.undeleteProject(this.project._id)
+    it('should insert the project into the collection', async function (ctx) {
+      await ctx.ProjectDeleter.promises.undeleteProject(ctx.project._id)
       sinon.assert.calledWith(
       sinon.assert.calledWith(
-        this.db.projects.insertOne,
+        ctx.db.projects.insertOne,
         sinon.match({
         sinon.match({
-          _id: this.project._id,
-          name: this.project.name,
+          _id: ctx.project._id,
+          name: ctx.project.name,
         })
         })
       )
       )
     })
     })
 
 
-    it('should clear the archive bit', async function () {
-      this.project.archived = true
-      await this.ProjectDeleter.promises.undeleteProject(this.project._id)
+    it('should clear the archive bit', async function (ctx) {
+      ctx.project.archived = true
+      await ctx.ProjectDeleter.promises.undeleteProject(ctx.project._id)
       sinon.assert.calledWith(
       sinon.assert.calledWith(
-        this.db.projects.insertOne,
+        ctx.db.projects.insertOne,
         sinon.match({ archived: undefined })
         sinon.match({ archived: undefined })
       )
       )
     })
     })
 
 
-    it('should generate a unique name for the project', async function () {
-      await this.ProjectDeleter.promises.undeleteProject(this.project._id)
+    it('should generate a unique name for the project', async function (ctx) {
+      await ctx.ProjectDeleter.promises.undeleteProject(ctx.project._id)
       sinon.assert.calledWith(
       sinon.assert.calledWith(
-        this.ProjectDetailsHandler.promises.generateUniqueName,
-        this.project.owner_ref
+        ctx.ProjectDetailsHandler.promises.generateUniqueName,
+        ctx.project.owner_ref
       )
       )
     })
     })
 
 
-    it('should add a suffix to the project name', async function () {
-      await this.ProjectDeleter.promises.undeleteProject(this.project._id)
+    it('should add a suffix to the project name', async function (ctx) {
+      await ctx.ProjectDeleter.promises.undeleteProject(ctx.project._id)
       sinon.assert.calledWith(
       sinon.assert.calledWith(
-        this.ProjectDetailsHandler.promises.generateUniqueName,
-        this.project.owner_ref,
-        this.project.name + ' (Restored)'
+        ctx.ProjectDetailsHandler.promises.generateUniqueName,
+        ctx.project.owner_ref,
+        ctx.project.name + ' (Restored)'
       )
       )
     })
     })
 
 
-    it('should remove the DeletedProject', async function () {
+    it('should remove the DeletedProject', async function (ctx) {
       // need to change the mock just to include the methods we want
       // need to change the mock just to include the methods we want
-      this.DeletedProjectMock.restore()
-      this.DeletedProjectMock = sinon.mock(DeletedProject)
-      this.DeletedProjectMock.expects('findOne')
-        .withArgs({ 'deleterData.deletedProjectId': this.project._id })
+      ctx.DeletedProjectMock.restore()
+      ctx.DeletedProjectMock = sinon.mock(DeletedProject)
+      ctx.DeletedProjectMock.expects('findOne')
+        .withArgs({ 'deleterData.deletedProjectId': ctx.project._id })
         .chain('exec')
         .chain('exec')
-        .resolves(this.deletedProject)
-      this.DeletedProjectMock.expects('deleteOne')
+        .resolves(ctx.deletedProject)
+      ctx.DeletedProjectMock.expects('deleteOne')
         .withArgs({ _id: 'deleted' })
         .withArgs({ _id: 'deleted' })
         .chain('exec')
         .chain('exec')
         .resolves()
         .resolves()
 
 
-      await this.ProjectDeleter.promises.undeleteProject(this.project._id)
-      this.DeletedProjectMock.verify()
+      await ctx.ProjectDeleter.promises.undeleteProject(ctx.project._id)
+      ctx.DeletedProjectMock.verify()
     })
     })
   })
   })
 })
 })

+ 272 - 246
services/web/test/unit/src/Project/ProjectDetailsHandler.test.mjs

@@ -1,36 +1,41 @@
-const SandboxedModule = require('sandboxed-module')
-const sinon = require('sinon')
-const { expect } = require('chai')
-const { ObjectId } = require('mongodb-legacy')
-const Errors = require('../../../../app/src/Features/Errors/Errors')
-const ProjectHelper = require('../../../../app/src/Features/Project/ProjectHelper')
+import { vi, expect } from 'vitest'
+import sinon from 'sinon'
+import mongodb from 'mongodb-legacy'
+import Errors from '../../../../app/src/Features/Errors/Errors.js'
+import ProjectHelper from '../../../../app/src/Features/Project/ProjectHelper.js'
+
+const { ObjectId } = mongodb
 
 
 const MODULE_PATH = '../../../../app/src/Features/Project/ProjectDetailsHandler'
 const MODULE_PATH = '../../../../app/src/Features/Project/ProjectDetailsHandler'
 
 
+vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
+  vi.importActual('../../../../app/src/Features/Errors/Errors.js')
+)
+
 describe('ProjectDetailsHandler', function () {
 describe('ProjectDetailsHandler', function () {
-  beforeEach(function () {
-    this.user = {
+  beforeEach(async function (ctx) {
+    ctx.user = {
       _id: new ObjectId(),
       _id: new ObjectId(),
       email: 'user@example.com',
       email: 'user@example.com',
       features: 'mock-features',
       features: 'mock-features',
     }
     }
-    this.collaborator = {
+    ctx.collaborator = {
       _id: new ObjectId(),
       _id: new ObjectId(),
       email: 'collaborator@example.com',
       email: 'collaborator@example.com',
     }
     }
-    this.project = {
+    ctx.project = {
       _id: new ObjectId(),
       _id: new ObjectId(),
       name: 'project',
       name: 'project',
       description: 'this is a great project',
       description: 'this is a great project',
       something: 'should not exist',
       something: 'should not exist',
       compiler: 'latexxxxxx',
       compiler: 'latexxxxxx',
-      owner_ref: this.user._id,
-      collaberator_refs: [this.collaborator._id],
+      owner_ref: ctx.user._id,
+      collaberator_refs: [ctx.collaborator._id],
     }
     }
-    this.ProjectGetter = {
+    ctx.ProjectGetter = {
       promises: {
       promises: {
-        getProjectWithoutDocLines: sinon.stub().resolves(this.project),
-        getProject: sinon.stub().resolves(this.project),
+        getProjectWithoutDocLines: sinon.stub().resolves(ctx.project),
+        getProject: sinon.stub().resolves(ctx.project),
         findAllUsersProjects: sinon.stub().resolves({
         findAllUsersProjects: sinon.stub().resolves({
           owned: [],
           owned: [],
           readAndWrite: [],
           readAndWrite: [],
@@ -40,200 +45,221 @@ describe('ProjectDetailsHandler', function () {
         }),
         }),
       },
       },
     }
     }
-    this.ProjectModelUpdateQuery = {
+    ctx.ProjectModelUpdateQuery = {
       exec: sinon.stub().resolves(),
       exec: sinon.stub().resolves(),
     }
     }
-    this.ProjectModel = {
-      updateOne: sinon.stub().returns(this.ProjectModelUpdateQuery),
+    ctx.ProjectModel = {
+      updateOne: sinon.stub().returns(ctx.ProjectModelUpdateQuery),
     }
     }
-    this.UserGetter = {
+    ctx.UserGetter = {
       promises: {
       promises: {
-        getUser: sinon.stub().resolves(this.user),
+        getUser: sinon.stub().resolves(ctx.user),
       },
       },
     }
     }
-    this.TpdsUpdateSender = {
+    ctx.TpdsUpdateSender = {
       promises: {
       promises: {
         moveEntity: sinon.stub().resolves(),
         moveEntity: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.TokenGenerator = {
+    ctx.TokenGenerator = {
       readAndWriteToken: sinon.stub(),
       readAndWriteToken: sinon.stub(),
       promises: {
       promises: {
         generateUniqueReadOnlyToken: sinon.stub(),
         generateUniqueReadOnlyToken: sinon.stub(),
       },
       },
     }
     }
-    this.settings = {
+    ctx.settings = {
       defaultFeatures: 'default-features',
       defaultFeatures: 'default-features',
     }
     }
-    this.handler = SandboxedModule.require(MODULE_PATH, {
-      requires: {
-        './ProjectHelper': ProjectHelper,
-        './ProjectGetter': this.ProjectGetter,
-        '../../models/Project': {
-          Project: this.ProjectModel,
-        },
-        '../User/UserGetter': this.UserGetter,
-        '../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender,
-        '../TokenGenerator/TokenGenerator': this.TokenGenerator,
-        '@overleaf/settings': this.settings,
-      },
-    })
+
+    vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({
+      default: ProjectHelper,
+    }))
+
+    vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
+      default: ctx.ProjectGetter,
+    }))
+
+    vi.doMock('../../../../app/src/models/Project', () => ({
+      Project: ctx.ProjectModel,
+    }))
+
+    vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
+      default: ctx.UserGetter,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender',
+      () => ({
+        default: ctx.TpdsUpdateSender,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/TokenGenerator/TokenGenerator',
+      () => ({
+        default: ctx.TokenGenerator,
+      })
+    )
+
+    vi.doMock('@overleaf/settings', () => ({
+      default: ctx.settings,
+    }))
+
+    ctx.handler = (await import(MODULE_PATH)).default
   })
   })
 
 
   describe('getDetails', function () {
   describe('getDetails', function () {
-    it('should find the project and owner', async function () {
-      const details = await this.handler.promises.getDetails(this.project._id)
-      details.name.should.equal(this.project.name)
-      details.description.should.equal(this.project.description)
-      details.compiler.should.equal(this.project.compiler)
-      details.features.should.equal(this.user.features)
+    it('should find the project and owner', async function (ctx) {
+      const details = await ctx.handler.promises.getDetails(ctx.project._id)
+      details.name.should.equal(ctx.project.name)
+      details.description.should.equal(ctx.project.description)
+      details.compiler.should.equal(ctx.project.compiler)
+      details.features.should.equal(ctx.user.features)
       expect(details.something).to.be.undefined
       expect(details.something).to.be.undefined
     })
     })
 
 
-    it('should find overleaf metadata if it exists', async function () {
-      this.project.overleaf = { id: 'id' }
-      const details = await this.handler.promises.getDetails(this.project._id)
-      details.overleaf.should.equal(this.project.overleaf)
+    it('should find overleaf metadata if it exists', async function (ctx) {
+      ctx.project.overleaf = { id: 'id' }
+      const details = await ctx.handler.promises.getDetails(ctx.project._id)
+      details.overleaf.should.equal(ctx.project.overleaf)
       expect(details.something).to.be.undefined
       expect(details.something).to.be.undefined
     })
     })
 
 
-    it('should return an error for a non-existent project', async function () {
-      this.ProjectGetter.promises.getProject.resolves(null)
+    it('should return an error for a non-existent project', async function (ctx) {
+      ctx.ProjectGetter.promises.getProject.resolves(null)
       await expect(
       await expect(
-        this.handler.promises.getDetails('0123456789012345678901234')
+        ctx.handler.promises.getDetails('0123456789012345678901234')
       ).to.be.rejectedWith(Errors.NotFoundError)
       ).to.be.rejectedWith(Errors.NotFoundError)
     })
     })
 
 
-    it('should return the default features if no owner found', async function () {
-      this.UserGetter.promises.getUser.resolves(null)
-      const details = await this.handler.promises.getDetails(this.project._id)
-      details.features.should.equal(this.settings.defaultFeatures)
+    it('should return the default features if no owner found', async function (ctx) {
+      ctx.UserGetter.promises.getUser.resolves(null)
+      const details = await ctx.handler.promises.getDetails(ctx.project._id)
+      details.features.should.equal(ctx.settings.defaultFeatures)
     })
     })
 
 
-    it('should rethrow any error', async function () {
-      this.ProjectGetter.promises.getProject.rejects(new Error('boom'))
-      await expect(this.handler.promises.getDetails(this.project._id)).to.be
+    it('should rethrow any error', async function (ctx) {
+      ctx.ProjectGetter.promises.getProject.rejects(new Error('boom'))
+      await expect(ctx.handler.promises.getDetails(ctx.project._id)).to.be
         .rejected
         .rejected
     })
     })
   })
   })
 
 
   describe('getProjectDescription', function () {
   describe('getProjectDescription', function () {
-    it('should make a call to mongo just for the description', async function () {
-      this.ProjectGetter.promises.getProject.resolves()
-      await this.handler.promises.getProjectDescription(this.project._id)
-      expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith(
-        this.project._id,
+    it('should make a call to mongo just for the description', async function (ctx) {
+      ctx.ProjectGetter.promises.getProject.resolves()
+      await ctx.handler.promises.getProjectDescription(ctx.project._id)
+      expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith(
+        ctx.project._id,
         { description: true }
         { description: true }
       )
       )
     })
     })
 
 
-    it('should return what the mongo call returns', async function () {
+    it('should return what the mongo call returns', async function (ctx) {
       const expectedDescription = 'cool project'
       const expectedDescription = 'cool project'
-      this.ProjectGetter.promises.getProject.resolves({
+      ctx.ProjectGetter.promises.getProject.resolves({
         description: expectedDescription,
         description: expectedDescription,
       })
       })
-      const description = await this.handler.promises.getProjectDescription(
-        this.project._id
+      const description = await ctx.handler.promises.getProjectDescription(
+        ctx.project._id
       )
       )
       expect(description).to.equal(expectedDescription)
       expect(description).to.equal(expectedDescription)
     })
     })
   })
   })
 
 
   describe('setProjectDescription', function () {
   describe('setProjectDescription', function () {
-    beforeEach(function () {
-      this.description = 'updated teh description'
+    beforeEach(function (ctx) {
+      ctx.description = 'updated teh description'
     })
     })
 
 
-    it('should update the project detials', async function () {
-      await this.handler.promises.setProjectDescription(
-        this.project._id,
-        this.description
+    it('should update the project detials', async function (ctx) {
+      await ctx.handler.promises.setProjectDescription(
+        ctx.project._id,
+        ctx.description
       )
       )
-      expect(this.ProjectModel.updateOne).to.have.been.calledWith(
-        { _id: this.project._id },
-        { description: this.description }
+      expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
+        { _id: ctx.project._id },
+        { description: ctx.description }
       )
       )
     })
     })
   })
   })
 
 
   describe('renameProject', function () {
   describe('renameProject', function () {
-    beforeEach(function () {
-      this.newName = 'new name here'
+    beforeEach(function (ctx) {
+      ctx.newName = 'new name here'
     })
     })
 
 
-    it('should update the project with the new name', async function () {
-      await this.handler.promises.renameProject(this.project._id, this.newName)
-      expect(this.ProjectModel.updateOne).to.have.been.calledWith(
-        { _id: this.project._id },
-        { name: this.newName }
+    it('should update the project with the new name', async function (ctx) {
+      await ctx.handler.promises.renameProject(ctx.project._id, ctx.newName)
+      expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
+        { _id: ctx.project._id },
+        { name: ctx.newName }
       )
       )
     })
     })
 
 
-    it('should tell the TpdsUpdateSender', async function () {
-      await this.handler.promises.renameProject(this.project._id, this.newName)
-      expect(this.TpdsUpdateSender.promises.moveEntity).to.have.been.calledWith(
-        {
-          projectId: this.project._id,
-          projectName: this.project.name,
-          newProjectName: this.newName,
-        }
-      )
+    it('should tell the TpdsUpdateSender', async function (ctx) {
+      await ctx.handler.promises.renameProject(ctx.project._id, ctx.newName)
+      expect(ctx.TpdsUpdateSender.promises.moveEntity).to.have.been.calledWith({
+        projectId: ctx.project._id,
+        projectName: ctx.project.name,
+        newProjectName: ctx.newName,
+      })
     })
     })
 
 
-    it('should not do anything with an invalid name', async function () {
-      await expect(this.handler.promises.renameProject(this.project._id)).to.be
+    it('should not do anything with an invalid name', async function (ctx) {
+      await expect(ctx.handler.promises.renameProject(ctx.project._id)).to.be
         .rejected
         .rejected
-      expect(this.TpdsUpdateSender.promises.moveEntity).not.to.have.been.called
-      expect(this.ProjectModel.updateOne).not.to.have.been.called
+      expect(ctx.TpdsUpdateSender.promises.moveEntity).not.to.have.been.called
+      expect(ctx.ProjectModel.updateOne).not.to.have.been.called
     })
     })
 
 
-    it('should trim whitespace around name', async function () {
-      await this.handler.promises.renameProject(
-        this.project._id,
-        `   ${this.newName}   `
+    it('should trim whitespace around name', async function (ctx) {
+      await ctx.handler.promises.renameProject(
+        ctx.project._id,
+        `   ${ctx.newName}   `
       )
       )
-      expect(this.ProjectModel.updateOne).to.have.been.calledWith(
-        { _id: this.project._id },
-        { name: this.newName }
+      expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
+        { _id: ctx.project._id },
+        { name: ctx.newName }
       )
       )
     })
     })
   })
   })
 
 
   describe('validateProjectName', function () {
   describe('validateProjectName', function () {
-    it('should reject undefined names', async function () {
-      await expect(this.handler.promises.validateProjectName(undefined)).to.be
+    it('should reject undefined names', async function (ctx) {
+      await expect(ctx.handler.promises.validateProjectName(undefined)).to.be
         .rejected
         .rejected
     })
     })
 
 
-    it('should reject empty names', async function () {
-      await expect(this.handler.promises.validateProjectName('')).to.be.rejected
+    it('should reject empty names', async function (ctx) {
+      await expect(ctx.handler.promises.validateProjectName('')).to.be.rejected
     })
     })
 
 
-    it('should reject names with /s', async function () {
-      await expect(this.handler.promises.validateProjectName('foo/bar')).to.be
+    it('should reject names with /s', async function (ctx) {
+      await expect(ctx.handler.promises.validateProjectName('foo/bar')).to.be
         .rejected
         .rejected
     })
     })
 
 
-    it('should reject names with \\s', async function () {
-      await expect(this.handler.promises.validateProjectName('foo\\bar')).to.be
+    it('should reject names with \\s', async function (ctx) {
+      await expect(ctx.handler.promises.validateProjectName('foo\\bar')).to.be
         .rejected
         .rejected
     })
     })
 
 
-    it('should reject long names', async function () {
-      await expect(this.handler.promises.validateProjectName('a'.repeat(1000)))
+    it('should reject long names', async function (ctx) {
+      await expect(ctx.handler.promises.validateProjectName('a'.repeat(1000)))
         .to.be.rejected
         .to.be.rejected
     })
     })
 
 
-    it('should accept normal names', async function () {
-      await expect(this.handler.promises.validateProjectName('foobar')).to.be
+    it('should accept normal names', async function (ctx) {
+      await expect(ctx.handler.promises.validateProjectName('foobar')).to.be
         .fulfilled
         .fulfilled
     })
     })
   })
   })
 
 
   describe('generateUniqueName', function () {
   describe('generateUniqueName', function () {
     // actually testing `ProjectHelper.promises.ensureNameIsUnique()`
     // actually testing `ProjectHelper.promises.ensureNameIsUnique()`
-    beforeEach(function () {
-      this.longName = 'x'.repeat(this.handler.MAX_PROJECT_NAME_LENGTH - 5)
+    beforeEach(function (ctx) {
+      ctx.longName = 'x'.repeat(ctx.handler.MAX_PROJECT_NAME_LENGTH - 5)
       const usersProjects = {
       const usersProjects = {
         owned: [
         owned: [
           { _id: 1, name: 'name' },
           { _id: 1, name: 'name' },
@@ -290,116 +316,116 @@ describe('ProjectDetailsHandler', function () {
         tokenReadOnly: [
         tokenReadOnly: [
           { _id: 10, name: 'name5' },
           { _id: 10, name: 'name5' },
           { _id: 11, name: 'name55' },
           { _id: 11, name: 'name55' },
-          { _id: 12, name: this.longName },
+          { _id: 12, name: ctx.longName },
         ],
         ],
       }
       }
-      this.ProjectGetter.promises.findAllUsersProjects.resolves(usersProjects)
+      ctx.ProjectGetter.promises.findAllUsersProjects.resolves(usersProjects)
     })
     })
 
 
-    it('should leave a unique name unchanged', async function () {
-      const name = await this.handler.promises.generateUniqueName(
-        this.user._id,
+    it('should leave a unique name unchanged', async function (ctx) {
+      const name = await ctx.handler.promises.generateUniqueName(
+        ctx.user._id,
         'unique-name',
         'unique-name',
         ['-test-suffix']
         ['-test-suffix']
       )
       )
       expect(name).to.equal('unique-name')
       expect(name).to.equal('unique-name')
     })
     })
 
 
-    it('should append a suffix to an existing name', async function () {
-      const name = await this.handler.promises.generateUniqueName(
-        this.user._id,
+    it('should append a suffix to an existing name', async function (ctx) {
+      const name = await ctx.handler.promises.generateUniqueName(
+        ctx.user._id,
         'name1',
         'name1',
         ['-test-suffix']
         ['-test-suffix']
       )
       )
       expect(name).to.equal('name1-test-suffix')
       expect(name).to.equal('name1-test-suffix')
     })
     })
 
 
-    it('should fallback to a second suffix when needed', async function () {
-      const name = await this.handler.promises.generateUniqueName(
-        this.user._id,
+    it('should fallback to a second suffix when needed', async function (ctx) {
+      const name = await ctx.handler.promises.generateUniqueName(
+        ctx.user._id,
         'name1',
         'name1',
         ['1', '-test-suffix']
         ['1', '-test-suffix']
       )
       )
       expect(name).to.equal('name1-test-suffix')
       expect(name).to.equal('name1-test-suffix')
     })
     })
 
 
-    it('should truncate the name when append a suffix if the result is too long', async function () {
-      const name = await this.handler.promises.generateUniqueName(
-        this.user._id,
-        this.longName,
+    it('should truncate the name when append a suffix if the result is too long', async function (ctx) {
+      const name = await ctx.handler.promises.generateUniqueName(
+        ctx.user._id,
+        ctx.longName,
         ['-test-suffix']
         ['-test-suffix']
       )
       )
       expect(name).to.equal(
       expect(name).to.equal(
-        this.longName.substr(0, this.handler.MAX_PROJECT_NAME_LENGTH - 12) +
+        ctx.longName.substr(0, ctx.handler.MAX_PROJECT_NAME_LENGTH - 12) +
           '-test-suffix'
           '-test-suffix'
       )
       )
     })
     })
 
 
-    it('should use a numeric index if no suffix is supplied', async function () {
-      const name = await this.handler.promises.generateUniqueName(
-        this.user._id,
+    it('should use a numeric index if no suffix is supplied', async function (ctx) {
+      const name = await ctx.handler.promises.generateUniqueName(
+        ctx.user._id,
         'name1',
         'name1',
         []
         []
       )
       )
       expect(name).to.equal('name1 (1)')
       expect(name).to.equal('name1 (1)')
     })
     })
 
 
-    it('should use a numeric index if all suffixes are exhausted', async function () {
-      const name = await this.handler.promises.generateUniqueName(
-        this.user._id,
+    it('should use a numeric index if all suffixes are exhausted', async function (ctx) {
+      const name = await ctx.handler.promises.generateUniqueName(
+        ctx.user._id,
         'name',
         'name',
         ['1', '11']
         ['1', '11']
       )
       )
       expect(name).to.equal('name (1)')
       expect(name).to.equal('name (1)')
     })
     })
 
 
-    it('should find the next lowest available numeric index for the base name', async function () {
-      const name = await this.handler.promises.generateUniqueName(
-        this.user._id,
+    it('should find the next lowest available numeric index for the base name', async function (ctx) {
+      const name = await ctx.handler.promises.generateUniqueName(
+        ctx.user._id,
         'numeric',
         'numeric',
         []
         []
       )
       )
       expect(name).to.equal('numeric (21)')
       expect(name).to.equal('numeric (21)')
     })
     })
 
 
-    it('should not find a numeric index lower than the one already present', async function () {
-      const name = await this.handler.promises.generateUniqueName(
-        this.user._id,
+    it('should not find a numeric index lower than the one already present', async function (ctx) {
+      const name = await ctx.handler.promises.generateUniqueName(
+        ctx.user._id,
         'numeric (31)',
         'numeric (31)',
         []
         []
       )
       )
       expect(name).to.equal('numeric (41)')
       expect(name).to.equal('numeric (41)')
     })
     })
 
 
-    it('should handle years in name', async function () {
-      const name = await this.handler.promises.generateUniqueName(
-        this.user._id,
+    it('should handle years in name', async function (ctx) {
+      const name = await ctx.handler.promises.generateUniqueName(
+        ctx.user._id,
         'unique-name (2021)',
         'unique-name (2021)',
         []
         []
       )
       )
       expect(name).to.equal('unique-name (2021)')
       expect(name).to.equal('unique-name (2021)')
     })
     })
 
 
-    it('should handle duplicating with year in name', async function () {
-      const name = await this.handler.promises.generateUniqueName(
-        this.user._id,
+    it('should handle duplicating with year in name', async function (ctx) {
+      const name = await ctx.handler.promises.generateUniqueName(
+        ctx.user._id,
         'Yearbook (2021)',
         'Yearbook (2021)',
         []
         []
       )
       )
       expect(name).to.equal('Yearbook (2021) (2)')
       expect(name).to.equal('Yearbook (2021) (2)')
     })
     })
     describe('title with that causes invalid regex', function () {
     describe('title with that causes invalid regex', function () {
-      it('should create the project with a suffix when project name exists', async function () {
-        const name = await this.handler.promises.generateUniqueName(
-          this.user._id,
+      it('should create the project with a suffix when project name exists', async function (ctx) {
+        const name = await ctx.handler.promises.generateUniqueName(
+          ctx.user._id,
           'Resume (2020',
           'Resume (2020',
           []
           []
         )
         )
         expect(name).to.equal('Resume (2020 (1)')
         expect(name).to.equal('Resume (2020 (1)')
       })
       })
-      it('should create the project with the provided name', async function () {
-        const name = await this.handler.promises.generateUniqueName(
-          this.user._id,
+      it('should create the project with the provided name', async function (ctx) {
+        const name = await ctx.handler.promises.generateUniqueName(
+          ctx.user._id,
           'Yearbook (2021',
           'Yearbook (2021',
           []
           []
         )
         )
@@ -409,18 +435,18 @@ describe('ProjectDetailsHandler', function () {
 
 
     describe('numeric index is already present', function () {
     describe('numeric index is already present', function () {
       describe('when there is 1 project "x (2)"', function () {
       describe('when there is 1 project "x (2)"', function () {
-        beforeEach(function () {
+        beforeEach(function (ctx) {
           const usersProjects = {
           const usersProjects = {
             owned: [{ _id: 1, name: 'x (2)' }],
             owned: [{ _id: 1, name: 'x (2)' }],
           }
           }
-          this.ProjectGetter.promises.findAllUsersProjects.resolves(
+          ctx.ProjectGetter.promises.findAllUsersProjects.resolves(
             usersProjects
             usersProjects
           )
           )
         })
         })
 
 
-        it('should produce "x (3)" uploading a zip with name "x (2)"', async function () {
-          const name = await this.handler.promises.generateUniqueName(
-            this.user._id,
+        it('should produce "x (3)" uploading a zip with name "x (2)"', async function (ctx) {
+          const name = await ctx.handler.promises.generateUniqueName(
+            ctx.user._id,
             'x (2)',
             'x (2)',
             []
             []
           )
           )
@@ -429,21 +455,21 @@ describe('ProjectDetailsHandler', function () {
       })
       })
 
 
       describe('when there are 2 projects "x (2)" and "x (3)"', function () {
       describe('when there are 2 projects "x (2)" and "x (3)"', function () {
-        beforeEach(function () {
+        beforeEach(function (ctx) {
           const usersProjects = {
           const usersProjects = {
             owned: [
             owned: [
               { _id: 1, name: 'x (2)' },
               { _id: 1, name: 'x (2)' },
               { _id: 2, name: 'x (3)' },
               { _id: 2, name: 'x (3)' },
             ],
             ],
           }
           }
-          this.ProjectGetter.promises.findAllUsersProjects.resolves(
+          ctx.ProjectGetter.promises.findAllUsersProjects.resolves(
             usersProjects
             usersProjects
           )
           )
         })
         })
 
 
-        it('should produce "x (4)" when uploading a zip with name "x (2)"', async function () {
-          const name = await this.handler.promises.generateUniqueName(
-            this.user._id,
+        it('should produce "x (4)" when uploading a zip with name "x (2)"', async function (ctx) {
+          const name = await ctx.handler.promises.generateUniqueName(
+            ctx.user._id,
             'x (2)',
             'x (2)',
             []
             []
           )
           )
@@ -452,30 +478,30 @@ describe('ProjectDetailsHandler', function () {
       })
       })
 
 
       describe('when there are 2 projects "x (2)" and "x (4)"', function () {
       describe('when there are 2 projects "x (2)" and "x (4)"', function () {
-        beforeEach(function () {
+        beforeEach(function (ctx) {
           const usersProjects = {
           const usersProjects = {
             owned: [
             owned: [
               { _id: 1, name: 'x (2)' },
               { _id: 1, name: 'x (2)' },
               { _id: 2, name: 'x (4)' },
               { _id: 2, name: 'x (4)' },
             ],
             ],
           }
           }
-          this.ProjectGetter.promises.findAllUsersProjects.resolves(
+          ctx.ProjectGetter.promises.findAllUsersProjects.resolves(
             usersProjects
             usersProjects
           )
           )
         })
         })
 
 
-        it('should produce "x (3)" when uploading a zip with name "x (2)"', async function () {
-          const name = await this.handler.promises.generateUniqueName(
-            this.user._id,
+        it('should produce "x (3)" when uploading a zip with name "x (2)"', async function (ctx) {
+          const name = await ctx.handler.promises.generateUniqueName(
+            ctx.user._id,
             'x (2)',
             'x (2)',
             []
             []
           )
           )
           expect(name).to.equal('x (3)')
           expect(name).to.equal('x (3)')
         })
         })
 
 
-        it('should produce "x (5)" when uploading a zip with name "x (4)"', async function () {
-          const name = await this.handler.promises.generateUniqueName(
-            this.user._id,
+        it('should produce "x (5)" when uploading a zip with name "x (4)"', async function (ctx) {
+          const name = await ctx.handler.promises.generateUniqueName(
+            ctx.user._id,
             'x (4)',
             'x (4)',
             []
             []
           )
           )
@@ -486,70 +512,70 @@ describe('ProjectDetailsHandler', function () {
   })
   })
 
 
   describe('fixProjectName', function () {
   describe('fixProjectName', function () {
-    it('should change empty names to Untitled', function () {
-      expect(this.handler.fixProjectName('')).to.equal('Untitled')
+    it('should change empty names to Untitled', function (ctx) {
+      expect(ctx.handler.fixProjectName('')).to.equal('Untitled')
     })
     })
 
 
-    it('should replace / with -', function () {
-      expect(this.handler.fixProjectName('foo/bar')).to.equal('foo-bar')
+    it('should replace / with -', function (ctx) {
+      expect(ctx.handler.fixProjectName('foo/bar')).to.equal('foo-bar')
     })
     })
 
 
-    it("should replace \\ with ''", function () {
-      expect(this.handler.fixProjectName('foo \\ bar')).to.equal('foo  bar')
+    it("should replace \\ with ''", function (ctx) {
+      expect(ctx.handler.fixProjectName('foo \\ bar')).to.equal('foo  bar')
     })
     })
 
 
-    it('should truncate long names', function () {
-      expect(this.handler.fixProjectName('a'.repeat(1000))).to.equal(
+    it('should truncate long names', function (ctx) {
+      expect(ctx.handler.fixProjectName('a'.repeat(1000))).to.equal(
         'a'.repeat(150)
         'a'.repeat(150)
       )
       )
     })
     })
 
 
-    it('should accept normal names', function () {
-      expect(this.handler.fixProjectName('foobar')).to.equal('foobar')
+    it('should accept normal names', function (ctx) {
+      expect(ctx.handler.fixProjectName('foobar')).to.equal('foobar')
     })
     })
 
 
-    it('should trim name after truncation', function () {
-      expect(this.handler.fixProjectName('a'.repeat(149) + ' a')).to.equal(
+    it('should trim name after truncation', function (ctx) {
+      expect(ctx.handler.fixProjectName('a'.repeat(149) + ' a')).to.equal(
         'a'.repeat(149)
         'a'.repeat(149)
       )
       )
     })
     })
   })
   })
 
 
   describe('setPublicAccessLevel', function () {
   describe('setPublicAccessLevel', function () {
-    beforeEach(function () {
-      this.accessLevel = 'tokenBased'
+    beforeEach(function (ctx) {
+      ctx.accessLevel = 'tokenBased'
     })
     })
 
 
-    it('should update the project with the new level', async function () {
-      await this.handler.promises.setPublicAccessLevel(
-        this.project._id,
-        this.accessLevel
+    it('should update the project with the new level', async function (ctx) {
+      await ctx.handler.promises.setPublicAccessLevel(
+        ctx.project._id,
+        ctx.accessLevel
       )
       )
-      expect(this.ProjectModel.updateOne).to.have.been.calledWith(
-        { _id: this.project._id },
-        { publicAccesLevel: this.accessLevel }
+      expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
+        { _id: ctx.project._id },
+        { publicAccesLevel: ctx.accessLevel }
       )
       )
     })
     })
 
 
-    it('should not produce an error', async function () {
+    it('should not produce an error', async function (ctx) {
       await expect(
       await expect(
-        this.handler.promises.setPublicAccessLevel(
-          this.project._id,
-          this.accessLevel
+        ctx.handler.promises.setPublicAccessLevel(
+          ctx.project._id,
+          ctx.accessLevel
         )
         )
       ).to.be.fulfilled
       ).to.be.fulfilled
     })
     })
 
 
     describe('when update produces an error', function () {
     describe('when update produces an error', function () {
-      beforeEach(function () {
-        this.ProjectModelUpdateQuery.exec.rejects(new Error('woops'))
+      beforeEach(function (ctx) {
+        ctx.ProjectModelUpdateQuery.exec.rejects(new Error('woops'))
       })
       })
 
 
-      it('should produce an error', async function () {
+      it('should produce an error', async function (ctx) {
         await expect(
         await expect(
-          this.handler.promises.setPublicAccessLevel(
-            this.project._id,
-            this.accessLevel
+          ctx.handler.promises.setPublicAccessLevel(
+            ctx.project._id,
+            ctx.accessLevel
           )
           )
         ).to.be.rejected
         ).to.be.rejected
       })
       })
@@ -558,76 +584,76 @@ describe('ProjectDetailsHandler', function () {
 
 
   describe('ensureTokensArePresent', function () {
   describe('ensureTokensArePresent', function () {
     describe('when the project has tokens', function () {
     describe('when the project has tokens', function () {
-      beforeEach(function () {
-        this.project = {
-          _id: this.project._id,
+      beforeEach(function (ctx) {
+        ctx.project = {
+          _id: ctx.project._id,
           tokens: {
           tokens: {
             readOnly: 'aaa',
             readOnly: 'aaa',
             readAndWrite: '42bbb',
             readAndWrite: '42bbb',
             readAndWritePrefix: '42',
             readAndWritePrefix: '42',
           },
           },
         }
         }
-        this.ProjectGetter.promises.getProject.resolves(this.project)
+        ctx.ProjectGetter.promises.getProject.resolves(ctx.project)
       })
       })
 
 
-      it('should get the project', async function () {
-        await this.handler.promises.ensureTokensArePresent(this.project._id)
-        expect(this.ProjectGetter.promises.getProject).to.have.been.calledOnce
-        expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith(
-          this.project._id,
+      it('should get the project', async function (ctx) {
+        await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
+        expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledOnce
+        expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith(
+          ctx.project._id,
           {
           {
             tokens: 1,
             tokens: 1,
           }
           }
         )
         )
       })
       })
 
 
-      it('should not update the project with new tokens', async function () {
-        await this.handler.promises.ensureTokensArePresent(this.project._id)
-        expect(this.ProjectModel.updateOne).not.to.have.been.called
+      it('should not update the project with new tokens', async function (ctx) {
+        await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
+        expect(ctx.ProjectModel.updateOne).not.to.have.been.called
       })
       })
     })
     })
 
 
     describe('when tokens are missing', function () {
     describe('when tokens are missing', function () {
-      beforeEach(function () {
-        this.project = { _id: this.project._id }
-        this.ProjectGetter.promises.getProject.resolves(this.project)
-        this.readOnlyToken = 'abc'
-        this.readAndWriteToken = '42def'
-        this.readAndWriteTokenPrefix = '42'
-        this.TokenGenerator.promises.generateUniqueReadOnlyToken.resolves(
-          this.readOnlyToken
+      beforeEach(function (ctx) {
+        ctx.project = { _id: ctx.project._id }
+        ctx.ProjectGetter.promises.getProject.resolves(ctx.project)
+        ctx.readOnlyToken = 'abc'
+        ctx.readAndWriteToken = '42def'
+        ctx.readAndWriteTokenPrefix = '42'
+        ctx.TokenGenerator.promises.generateUniqueReadOnlyToken.resolves(
+          ctx.readOnlyToken
         )
         )
-        this.TokenGenerator.readAndWriteToken.returns({
-          token: this.readAndWriteToken,
-          numericPrefix: this.readAndWriteTokenPrefix,
+        ctx.TokenGenerator.readAndWriteToken.returns({
+          token: ctx.readAndWriteToken,
+          numericPrefix: ctx.readAndWriteTokenPrefix,
         })
         })
       })
       })
 
 
-      it('should get the project', async function () {
-        await this.handler.promises.ensureTokensArePresent(this.project._id)
-        expect(this.ProjectGetter.promises.getProject).to.have.been.calledOnce
-        expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith(
-          this.project._id,
+      it('should get the project', async function (ctx) {
+        await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
+        expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledOnce
+        expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith(
+          ctx.project._id,
           {
           {
             tokens: 1,
             tokens: 1,
           }
           }
         )
         )
       })
       })
 
 
-      it('should update the project with new tokens', async function () {
-        await this.handler.promises.ensureTokensArePresent(this.project._id)
-        expect(this.TokenGenerator.promises.generateUniqueReadOnlyToken).to.have
+      it('should update the project with new tokens', async function (ctx) {
+        await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
+        expect(ctx.TokenGenerator.promises.generateUniqueReadOnlyToken).to.have
           .been.calledOnce
           .been.calledOnce
-        expect(this.TokenGenerator.readAndWriteToken).to.have.been.calledOnce
-        expect(this.ProjectModel.updateOne).to.have.been.calledOnce
-        expect(this.ProjectModel.updateOne).to.have.been.calledWith(
-          { _id: this.project._id },
+        expect(ctx.TokenGenerator.readAndWriteToken).to.have.been.calledOnce
+        expect(ctx.ProjectModel.updateOne).to.have.been.calledOnce
+        expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
+          { _id: ctx.project._id },
           {
           {
             $set: {
             $set: {
               tokens: {
               tokens: {
-                readOnly: this.readOnlyToken,
-                readAndWrite: this.readAndWriteToken,
-                readAndWritePrefix: this.readAndWriteTokenPrefix,
+                readOnly: ctx.readOnlyToken,
+                readAndWrite: ctx.readAndWriteToken,
+                readAndWritePrefix: ctx.readAndWriteTokenPrefix,
               },
               },
             },
             },
           }
           }
@@ -637,10 +663,10 @@ describe('ProjectDetailsHandler', function () {
   })
   })
 
 
   describe('clearTokens', function () {
   describe('clearTokens', function () {
-    it('clears the tokens from the project', async function () {
-      await this.handler.promises.clearTokens(this.project._id)
-      expect(this.ProjectModel.updateOne).to.have.been.calledWith(
-        { _id: this.project._id },
+    it('clears the tokens from the project', async function (ctx) {
+      await ctx.handler.promises.clearTokens(ctx.project._id)
+      expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
+        { _id: ctx.project._id },
         { $unset: { tokens: 1 }, $set: { publicAccesLevel: 'private' } }
         { $unset: { tokens: 1 }, $set: { publicAccesLevel: 'private' } }
       )
       )
     })
     })

+ 207 - 186
services/web/test/unit/src/Project/ProjectEntityHandler.test.mjs

@@ -1,19 +1,22 @@
-const { expect } = require('chai')
-const sinon = require('sinon')
+import { vi, expect } from 'vitest'
+import sinon from 'sinon'
+import Errors from '../../../../app/src/Features/Errors/Errors.js'
 const modulePath = '../../../../app/src/Features/Project/ProjectEntityHandler'
 const modulePath = '../../../../app/src/Features/Project/ProjectEntityHandler'
-const SandboxedModule = require('sandboxed-module')
-const Errors = require('../../../../app/src/Features/Errors/Errors')
+
+vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
+  vi.importActual('../../../../app/src/Features/Errors/Errors.js')
+)
 
 
 describe('ProjectEntityHandler', function () {
 describe('ProjectEntityHandler', function () {
   const projectId = '4eecb1c1bffa66588e0000a1'
   const projectId = '4eecb1c1bffa66588e0000a1'
   const docId = '4eecb1c1bffa66588e0000a2'
   const docId = '4eecb1c1bffa66588e0000a2'
 
 
-  beforeEach(function () {
-    this.TpdsUpdateSender = {
+  beforeEach(async function (ctx) {
+    ctx.TpdsUpdateSender = {
       addDoc: sinon.stub().callsArg(1),
       addDoc: sinon.stub().callsArg(1),
       addFile: sinon.stub().callsArg(1),
       addFile: sinon.stub().callsArg(1),
     }
     }
-    this.ProjectModel = class Project {
+    ctx.ProjectModel = class Project {
       constructor(options) {
       constructor(options) {
         this._id = projectId
         this._id = projectId
         this.name = 'project_name_here'
         this.name = 'project_name_here'
@@ -21,59 +24,77 @@ describe('ProjectEntityHandler', function () {
         this.rootFolder = [this.rootFolder]
         this.rootFolder = [this.rootFolder]
       }
       }
     }
     }
-    this.project = new this.ProjectModel()
+    ctx.project = new ctx.ProjectModel()
 
 
-    this.ProjectLocator = { findElement: sinon.stub() }
-    this.DocumentUpdaterHandler = {
+    ctx.ProjectLocator = { findElement: sinon.stub() }
+    ctx.DocumentUpdaterHandler = {
       updateProjectStructure: sinon.stub().yields(),
       updateProjectStructure: sinon.stub().yields(),
     }
     }
-    this.callback = sinon.stub()
-
-    this.ProjectEntityHandler = SandboxedModule.require(modulePath, {
-      requires: {
-        '../Docstore/DocstoreManager': (this.DocstoreManager = {
-          promises: {},
-        }),
-        '../../Features/DocumentUpdater/DocumentUpdaterHandler':
-          this.DocumentUpdaterHandler,
-        '../../models/Project': {
-          Project: this.ProjectModel,
-        },
-        './ProjectLocator': this.ProjectLocator,
-        './ProjectGetter': (this.ProjectGetter = { promises: {} }),
-        '../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender,
-      },
-    })
+    ctx.callback = sinon.stub()
+
+    vi.doMock('../../../../app/src/Features/Docstore/DocstoreManager', () => ({
+      default: (ctx.DocstoreManager = {
+        promises: {},
+      }),
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler',
+      () => ({
+        default: ctx.DocumentUpdaterHandler,
+      })
+    )
+
+    vi.doMock('../../../../app/src/models/Project', () => ({
+      Project: ctx.ProjectModel,
+    }))
+
+    vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({
+      default: ctx.ProjectLocator,
+    }))
+
+    vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
+      default: (ctx.ProjectGetter = { promises: {} }),
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender',
+      () => ({
+        default: ctx.TpdsUpdateSender,
+      })
+    )
+
+    ctx.ProjectEntityHandler = (await import(modulePath)).default
   })
   })
 
 
   describe('getting folders, docs and files', function () {
   describe('getting folders, docs and files', function () {
-    beforeEach(function () {
-      this.project.rootFolder = [
+    beforeEach(function (ctx) {
+      ctx.project.rootFolder = [
         {
         {
           docs: [
           docs: [
-            (this.doc1 = {
+            (ctx.doc1 = {
               name: 'doc1',
               name: 'doc1',
               _id: 'doc1_id',
               _id: 'doc1_id',
             }),
             }),
           ],
           ],
           fileRefs: [
           fileRefs: [
-            (this.file1 = {
+            (ctx.file1 = {
               rev: 1,
               rev: 1,
               _id: 'file1_id',
               _id: 'file1_id',
               name: 'file1',
               name: 'file1',
             }),
             }),
           ],
           ],
           folders: [
           folders: [
-            (this.folder1 = {
+            (ctx.folder1 = {
               name: 'folder1',
               name: 'folder1',
               docs: [
               docs: [
-                (this.doc2 = {
+                (ctx.doc2 = {
                   name: 'doc2',
                   name: 'doc2',
                   _id: 'doc2_id',
                   _id: 'doc2_id',
                 }),
                 }),
               ],
               ],
               fileRefs: [
               fileRefs: [
-                (this.file2 = {
+                (ctx.file2 = {
                   rev: 2,
                   rev: 2,
                   name: 'file2',
                   name: 'file2',
                   _id: 'file2_id',
                   _id: 'file2_id',
@@ -84,54 +105,54 @@ describe('ProjectEntityHandler', function () {
           ],
           ],
         },
         },
       ]
       ]
-      this.ProjectGetter.promises.getProjectWithoutDocLines = sinon
+      ctx.ProjectGetter.promises.getProjectWithoutDocLines = sinon
         .stub()
         .stub()
-        .resolves(this.project)
+        .resolves(ctx.project)
     })
     })
 
 
     describe('getAllDocs', function () {
     describe('getAllDocs', function () {
       let fetchedDocs
       let fetchedDocs
-      beforeEach(async function () {
-        this.docs = [
+      beforeEach(async function (ctx) {
+        ctx.docs = [
           {
           {
-            _id: this.doc1._id,
-            lines: (this.lines1 = ['one']),
-            rev: (this.rev1 = 1),
+            _id: ctx.doc1._id,
+            lines: (ctx.lines1 = ['one']),
+            rev: (ctx.rev1 = 1),
           },
           },
           {
           {
-            _id: this.doc2._id,
-            lines: (this.lines2 = ['two']),
-            rev: (this.rev2 = 2),
+            _id: ctx.doc2._id,
+            lines: (ctx.lines2 = ['two']),
+            rev: (ctx.rev2 = 2),
           },
           },
         ]
         ]
-        this.DocstoreManager.promises.getAllDocs = sinon
+        ctx.DocstoreManager.promises.getAllDocs = sinon
           .stub()
           .stub()
-          .resolves(this.docs)
+          .resolves(ctx.docs)
         fetchedDocs =
         fetchedDocs =
-          await this.ProjectEntityHandler.promises.getAllDocs(projectId)
+          await ctx.ProjectEntityHandler.promises.getAllDocs(projectId)
       })
       })
 
 
-      it('should get the doc lines and rev from the docstore', function () {
-        this.DocstoreManager.promises.getAllDocs
+      it('should get the doc lines and rev from the docstore', function (ctx) {
+        ctx.DocstoreManager.promises.getAllDocs
           .calledWith(projectId)
           .calledWith(projectId)
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should call the callback with the docs with the lines and rev included', function () {
+      it('should call the callback with the docs with the lines and rev included', function (ctx) {
         expect(fetchedDocs).to.deep.equal({
         expect(fetchedDocs).to.deep.equal({
           '/doc1': {
           '/doc1': {
-            _id: this.doc1._id,
-            lines: this.lines1,
-            name: this.doc1.name,
-            rev: this.rev1,
-            folder: this.project.rootFolder[0],
+            _id: ctx.doc1._id,
+            lines: ctx.lines1,
+            name: ctx.doc1.name,
+            rev: ctx.rev1,
+            folder: ctx.project.rootFolder[0],
           },
           },
           '/folder1/doc2': {
           '/folder1/doc2': {
-            _id: this.doc2._id,
-            lines: this.lines2,
-            name: this.doc2.name,
-            rev: this.rev2,
-            folder: this.folder1,
+            _id: ctx.doc2._id,
+            lines: ctx.lines2,
+            name: ctx.doc2.name,
+            rev: ctx.rev2,
+            folder: ctx.folder1,
           },
           },
         })
         })
       })
       })
@@ -139,85 +160,85 @@ describe('ProjectEntityHandler', function () {
 
 
     describe('getAllFiles', function () {
     describe('getAllFiles', function () {
       let allFiles
       let allFiles
-      beforeEach(async function () {
-        this.callback = sinon.stub()
-        allFiles = await this.ProjectEntityHandler.promises.getAllFiles(
+      beforeEach(async function (ctx) {
+        ctx.callback = sinon.stub()
+        allFiles = await ctx.ProjectEntityHandler.promises.getAllFiles(
           projectId,
           projectId,
-          this.callback
+          ctx.callback
         )
         )
       })
       })
 
 
-      it('should call the callback with the files', function () {
+      it('should call the callback with the files', function (ctx) {
         expect(allFiles).to.deep.equal({
         expect(allFiles).to.deep.equal({
-          '/file1': { ...this.file1, folder: this.project.rootFolder[0] },
-          '/folder1/file2': { ...this.file2, folder: this.folder1 },
+          '/file1': { ...ctx.file1, folder: ctx.project.rootFolder[0] },
+          '/folder1/file2': { ...ctx.file2, folder: ctx.folder1 },
         })
         })
       })
       })
     })
     })
 
 
     describe('getAllDocPathsFromProject', function () {
     describe('getAllDocPathsFromProject', function () {
-      beforeEach(function () {
-        this.docs = [
+      beforeEach(function (ctx) {
+        ctx.docs = [
           {
           {
-            _id: this.doc1._id,
-            lines: (this.lines1 = ['one']),
-            rev: (this.rev1 = 1),
+            _id: ctx.doc1._id,
+            lines: (ctx.lines1 = ['one']),
+            rev: (ctx.rev1 = 1),
           },
           },
           {
           {
-            _id: this.doc2._id,
-            lines: (this.lines2 = ['two']),
-            rev: (this.rev2 = 2),
+            _id: ctx.doc2._id,
+            lines: (ctx.lines2 = ['two']),
+            rev: (ctx.rev2 = 2),
           },
           },
         ]
         ]
       })
       })
 
 
-      it('should call the callback with the path for each docId', function () {
+      it('should call the callback with the path for each docId', function (ctx) {
         const expected = {
         const expected = {
-          [this.doc1._id]: `/${this.doc1.name}`,
-          [this.doc2._id]: `/folder1/${this.doc2.name}`,
+          [ctx.doc1._id]: `/${ctx.doc1.name}`,
+          [ctx.doc2._id]: `/folder1/${ctx.doc2.name}`,
         }
         }
         expect(
         expect(
-          this.ProjectEntityHandler.getAllDocPathsFromProject(
-            this.project,
-            this.callback
+          ctx.ProjectEntityHandler.getAllDocPathsFromProject(
+            ctx.project,
+            ctx.callback
           )
           )
         ).to.deep.equal(expected)
         ).to.deep.equal(expected)
       })
       })
     })
     })
 
 
     describe('getDocPathByProjectIdAndDocId', function () {
     describe('getDocPathByProjectIdAndDocId', function () {
-      it('should call the callback with the path for an existing doc id at the root level', async function () {
+      it('should call the callback with the path for an existing doc id at the root level', async function (ctx) {
         const path =
         const path =
-          await this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
+          await ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
             projectId,
             projectId,
-            this.doc1._id
+            ctx.doc1._id
           )
           )
-        expect(path).to.deep.equal(`/${this.doc1.name}`)
+        expect(path).to.deep.equal(`/${ctx.doc1.name}`)
       })
       })
 
 
-      it('should call the callback with the path for an existing doc id nested within a folder', async function () {
+      it('should call the callback with the path for an existing doc id nested within a folder', async function (ctx) {
         const path =
         const path =
-          await this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
+          await ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
             projectId,
             projectId,
-            this.doc2._id
+            ctx.doc2._id
           )
           )
-        expect(path).to.deep.equal(`/folder1/${this.doc2.name}`)
+        expect(path).to.deep.equal(`/folder1/${ctx.doc2.name}`)
       })
       })
 
 
-      it('should call the callback with a NotFoundError for a non-existing doc', async function () {
+      it('should call the callback with a NotFoundError for a non-existing doc', async function (ctx) {
         await expect(
         await expect(
-          this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
+          ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
             projectId,
             projectId,
             'non-existing-id'
             'non-existing-id'
           )
           )
         ).to.be.rejectedWith(Errors.NotFoundError)
         ).to.be.rejectedWith(Errors.NotFoundError)
       })
       })
 
 
-      it('should call the callback with a NotFoundError for an existing file', async function () {
+      it('should call the callback with a NotFoundError for an existing file', async function (ctx) {
         await expect(
         await expect(
-          this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
+          ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
             projectId,
             projectId,
-            this.file1._id
+            ctx.file1._id
           )
           )
         ).to.be.rejectedWith(Errors.NotFoundError)
         ).to.be.rejectedWith(Errors.NotFoundError)
       })
       })
@@ -225,66 +246,66 @@ describe('ProjectEntityHandler', function () {
 
 
     describe('_getAllFolders', async function () {
     describe('_getAllFolders', async function () {
       let folders
       let folders
-      beforeEach(async function () {
-        this.callback = sinon.stub()
+      beforeEach(async function (ctx) {
+        ctx.callback = sinon.stub()
         folders =
         folders =
-          await this.ProjectEntityHandler.promises._getAllFolders(projectId)
+          await ctx.ProjectEntityHandler.promises._getAllFolders(projectId)
       })
       })
 
 
-      it('should get the project without the docs lines', function () {
-        this.ProjectGetter.promises.getProjectWithoutDocLines
+      it('should get the project without the docs lines', function (ctx) {
+        ctx.ProjectGetter.promises.getProjectWithoutDocLines
           .calledWith(projectId)
           .calledWith(projectId)
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should call the callback with the folders', function () {
+      it('should call the callback with the folders', function (ctx) {
         expect(folders).to.deep.equal([
         expect(folders).to.deep.equal([
-          { path: '/', folder: this.project.rootFolder[0] },
-          { path: '/folder1', folder: this.folder1 },
+          { path: '/', folder: ctx.project.rootFolder[0] },
+          { path: '/folder1', folder: ctx.folder1 },
         ])
         ])
       })
       })
     })
     })
 
 
     describe('_getAllFoldersFromProject', function () {
     describe('_getAllFoldersFromProject', function () {
-      it('should return the folders', function () {
+      it('should return the folders', function (ctx) {
         expect(
         expect(
-          this.ProjectEntityHandler._getAllFoldersFromProject(this.project)
+          ctx.ProjectEntityHandler._getAllFoldersFromProject(ctx.project)
         ).to.deep.equal([
         ).to.deep.equal([
-          { path: '/', folder: this.project.rootFolder[0] },
-          { path: '/folder1', folder: this.folder1 },
+          { path: '/', folder: ctx.project.rootFolder[0] },
+          { path: '/folder1', folder: ctx.folder1 },
         ])
         ])
       })
       })
     })
     })
   })
   })
 
 
   describe('with an invalid file tree', function () {
   describe('with an invalid file tree', function () {
-    beforeEach(function () {
-      this.project.rootFolder = [
+    beforeEach(function (ctx) {
+      ctx.project.rootFolder = [
         {
         {
           docs: [
           docs: [
-            (this.doc1 = {
+            (ctx.doc1 = {
               name: null, // invalid doc name
               name: null, // invalid doc name
               _id: 'doc1_id',
               _id: 'doc1_id',
             }),
             }),
           ],
           ],
           fileRefs: [
           fileRefs: [
-            (this.file1 = {
+            (ctx.file1 = {
               rev: 1,
               rev: 1,
               _id: 'file1_id',
               _id: 'file1_id',
               name: null, // invalid file name
               name: null, // invalid file name
             }),
             }),
           ],
           ],
           folders: [
           folders: [
-            (this.folder1 = {
+            (ctx.folder1 = {
               name: null, // invalid folder name
               name: null, // invalid folder name
               docs: [
               docs: [
-                (this.doc2 = {
+                (ctx.doc2 = {
                   name: 'doc2',
                   name: 'doc2',
                   _id: 'doc2_id',
                   _id: 'doc2_id',
                 }),
                 }),
               ],
               ],
               fileRefs: [
               fileRefs: [
-                (this.file2 = {
+                (ctx.file2 = {
                   rev: 2,
                   rev: 2,
                   name: 'file2',
                   name: 'file2',
                   _id: 'file2_id',
                   _id: 'file2_id',
@@ -296,107 +317,107 @@ describe('ProjectEntityHandler', function () {
           ],
           ],
         },
         },
       ]
       ]
-      this.ProjectGetter.promises.getProjectWithoutDocLines = sinon
+      ctx.ProjectGetter.promises.getProjectWithoutDocLines = sinon
         .stub()
         .stub()
-        .resolves(this.project)
+        .resolves(ctx.project)
     })
     })
 
 
     describe('getAllDocs', function () {
     describe('getAllDocs', function () {
-      beforeEach(async function () {
-        this.docs = [
+      beforeEach(async function (ctx) {
+        ctx.docs = [
           {
           {
-            _id: this.doc1._id,
-            lines: (this.lines1 = ['one']),
-            rev: (this.rev1 = 1),
+            _id: ctx.doc1._id,
+            lines: (ctx.lines1 = ['one']),
+            rev: (ctx.rev1 = 1),
           },
           },
           {
           {
-            _id: this.doc2._id,
-            lines: (this.lines2 = ['two']),
-            rev: (this.rev2 = 2),
+            _id: ctx.doc2._id,
+            lines: (ctx.lines2 = ['two']),
+            rev: (ctx.rev2 = 2),
           },
           },
         ]
         ]
-        this.DocstoreManager.promises.getAllDocs = sinon
+        ctx.DocstoreManager.promises.getAllDocs = sinon
           .stub()
           .stub()
-          .resolves(this.docs)
+          .resolves(ctx.docs)
       })
       })
 
 
-      it('should call the callback with an error', async function () {
-        await expect(this.ProjectEntityHandler.promises.getAllDocs(projectId))
-          .to.be.rejected
+      it('should call the callback with an error', async function (ctx) {
+        await expect(ctx.ProjectEntityHandler.promises.getAllDocs(projectId)).to
+          .be.rejected
       })
       })
     })
     })
 
 
     describe('getAllFiles', function () {
     describe('getAllFiles', function () {
-      it('should call the callback with and error', async function () {
-        await expect(this.ProjectEntityHandler.promises.getAllFiles(projectId))
+      it('should call the callback with and error', async function (ctx) {
+        await expect(ctx.ProjectEntityHandler.promises.getAllFiles(projectId))
           .to.be.rejected
           .to.be.rejected
       })
       })
     })
     })
 
 
     describe('getDocPathByProjectIdAndDocId', function () {
     describe('getDocPathByProjectIdAndDocId', function () {
-      it('should call the callback with an error for an existing doc id at the root level', async function () {
+      it('should call the callback with an error for an existing doc id at the root level', async function (ctx) {
         await expect(
         await expect(
-          this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
+          ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
             projectId,
             projectId,
-            this.doc1._id
+            ctx.doc1._id
           )
           )
         ).to.be.rejectedWith(Error)
         ).to.be.rejectedWith(Error)
       })
       })
 
 
-      it('should call the callback with an error for an existing doc id nested within a folder', async function () {
+      it('should call the callback with an error for an existing doc id nested within a folder', async function (ctx) {
         await expect(
         await expect(
-          this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
+          ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
             projectId,
             projectId,
-            this.doc2._id
+            ctx.doc2._id
           )
           )
         ).to.be.rejectedWith(Error)
         ).to.be.rejectedWith(Error)
       })
       })
 
 
-      it('should call the callback with an error for a non-existing doc', async function () {
+      it('should call the callback with an error for a non-existing doc', async function (ctx) {
         await expect(
         await expect(
-          this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
+          ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
             projectId,
             projectId,
             'non-existing-id'
             'non-existing-id'
           )
           )
         ).to.be.rejectedWith(Error)
         ).to.be.rejectedWith(Error)
       })
       })
 
 
-      it('should call the callback with an error for an existing file', async function () {
+      it('should call the callback with an error for an existing file', async function (ctx) {
         await expect(
         await expect(
-          this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
+          ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
             projectId,
             projectId,
-            this.file1._id
+            ctx.file1._id
           )
           )
         ).to.be.rejectedWith(Error)
         ).to.be.rejectedWith(Error)
       })
       })
     })
     })
 
 
     describe('_getAllFolders', function () {
     describe('_getAllFolders', function () {
-      it('should call the callback with an error', async function () {
+      it('should call the callback with an error', async function (ctx) {
         await expect(
         await expect(
-          this.ProjectEntityHandler.promises._getAllFolders(projectId)
+          ctx.ProjectEntityHandler.promises._getAllFolders(projectId)
         ).to.be.rejected
         ).to.be.rejected
       })
       })
     })
     })
 
 
     describe('getAllEntities', function () {
     describe('getAllEntities', function () {
-      beforeEach(function () {
-        this.ProjectGetter.promises.getProject = sinon
+      beforeEach(function (ctx) {
+        ctx.ProjectGetter.promises.getProject = sinon
           .stub()
           .stub()
-          .resolves(this.project)
+          .resolves(ctx.project)
       })
       })
 
 
-      it('should call the callback with an error', async function () {
+      it('should call the callback with an error', async function (ctx) {
         await expect(
         await expect(
-          this.ProjectEntityHandler.promises.getAllEntities(projectId)
+          ctx.ProjectEntityHandler.promises.getAllEntities(projectId)
         ).to.be.rejected
         ).to.be.rejected
       })
       })
     })
     })
 
 
     describe('getAllDocPathsFromProjectById', function () {
     describe('getAllDocPathsFromProjectById', function () {
-      it('should call the callback with an error', async function () {
+      it('should call the callback with an error', async function (ctx) {
         await expect(
         await expect(
-          this.ProjectEntityHandler.promises.getAllDocPathsFromProjectById(
+          ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById(
             projectId
             projectId
           )
           )
         ).to.be.rejected
         ).to.be.rejected
@@ -404,11 +425,11 @@ describe('ProjectEntityHandler', function () {
     })
     })
 
 
     describe('getDocPathFromProjectByDocId', function () {
     describe('getDocPathFromProjectByDocId', function () {
-      it('should call the callback with an error', async function () {
+      it('should call the callback with an error', async function (ctx) {
         await expect(
         await expect(
-          this.ProjectEntityHandler.promises.getDocPathFromProjectByDocId(
+          ctx.ProjectEntityHandler.promises.getDocPathFromProjectByDocId(
             projectId,
             projectId,
-            this.doc1._id
+            ctx.doc1._id
           )
           )
         ).to.be.rejected
         ).to.be.rejected
       })
       })
@@ -416,26 +437,26 @@ describe('ProjectEntityHandler', function () {
   })
   })
 
 
   describe('getDoc', function () {
   describe('getDoc', function () {
-    beforeEach(function () {
-      this.lines = ['mock', 'doc', 'lines']
-      this.rev = 5
-      this.version = 42
-      this.ranges = { mock: 'ranges' }
-      this.callback = sinon.stub()
-      this.DocstoreManager.promises.getDoc = sinon.stub().resolves({
-        lines: this.lines,
-        rev: this.rev,
-        version: this.version,
-        ranges: this.ranges,
+    beforeEach(function (ctx) {
+      ctx.lines = ['mock', 'doc', 'lines']
+      ctx.rev = 5
+      ctx.version = 42
+      ctx.ranges = { mock: 'ranges' }
+      ctx.callback = sinon.stub()
+      ctx.DocstoreManager.promises.getDoc = sinon.stub().resolves({
+        lines: ctx.lines,
+        rev: ctx.rev,
+        version: ctx.version,
+        ranges: ctx.ranges,
       })
       })
     })
     })
 
 
-    it('should call the callback with the lines, version and rev', async function () {
-      const doc = await this.ProjectEntityHandler.promises.getDoc(
+    it('should call the callback with the lines, version and rev', async function (ctx) {
+      const doc = await ctx.ProjectEntityHandler.promises.getDoc(
         projectId,
         projectId,
         docId
         docId
       )
       )
-      this.DocstoreManager.promises.getDoc
+      ctx.DocstoreManager.promises.getDoc
         .calledWith(projectId, docId)
         .calledWith(projectId, docId)
         .should.equal(true)
         .should.equal(true)
       expect(doc).to.exist
       expect(doc).to.exist
@@ -445,32 +466,32 @@ describe('ProjectEntityHandler', function () {
   describe('promises.getDoc', function () {
   describe('promises.getDoc', function () {
     let result
     let result
 
 
-    beforeEach(async function () {
-      this.lines = ['mock', 'doc', 'lines']
-      this.rev = 5
-      this.version = 42
-      this.ranges = { mock: 'ranges' }
-
-      this.DocstoreManager.promises.getDoc = sinon.stub().resolves({
-        lines: this.lines,
-        rev: this.rev,
-        version: this.version,
-        ranges: this.ranges,
+    beforeEach(async function (ctx) {
+      ctx.lines = ['mock', 'doc', 'lines']
+      ctx.rev = 5
+      ctx.version = 42
+      ctx.ranges = { mock: 'ranges' }
+
+      ctx.DocstoreManager.promises.getDoc = sinon.stub().resolves({
+        lines: ctx.lines,
+        rev: ctx.rev,
+        version: ctx.version,
+        ranges: ctx.ranges,
       })
       })
-      result = await this.ProjectEntityHandler.promises.getDoc(projectId, docId)
+      result = await ctx.ProjectEntityHandler.promises.getDoc(projectId, docId)
     })
     })
 
 
-    it('should call the docstore', function () {
-      this.DocstoreManager.promises.getDoc
+    it('should call the docstore', function (ctx) {
+      ctx.DocstoreManager.promises.getDoc
         .calledWith(projectId, docId)
         .calledWith(projectId, docId)
         .should.equal(true)
         .should.equal(true)
     })
     })
 
 
-    it('should return the lines, rev, version and ranges', function () {
-      expect(result.lines).to.equal(this.lines)
-      expect(result.rev).to.equal(this.rev)
-      expect(result.version).to.equal(this.version)
-      expect(result.ranges).to.equal(this.ranges)
+    it('should return the lines, rev, version and ranges', function (ctx) {
+      expect(result.lines).to.equal(ctx.lines)
+      expect(result.rev).to.equal(ctx.rev)
+      expect(result.version).to.equal(ctx.version)
+      expect(result.ranges).to.equal(ctx.ranges)
     })
     })
   })
   })
 })
 })

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 319 - 282
services/web/test/unit/src/Project/ProjectEntityMongoUpdateHandler.test.mjs


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 360 - 299
services/web/test/unit/src/Project/ProjectEntityUpdateHandler.test.mjs


+ 221 - 213
services/web/test/unit/src/Project/ProjectGetter.test.mjs

@@ -1,30 +1,31 @@
-const sinon = require('sinon')
-const { expect } = require('chai')
-const modulePath = '../../../../app/src/Features/Project/ProjectGetter.js'
-const SandboxedModule = require('sandboxed-module')
-const { ObjectId } = require('mongodb-legacy')
+import { vi, expect } from 'vitest'
+import sinon from 'sinon'
+import mongodb from 'mongodb-legacy'
+const modulePath = '../../../../app/src/Features/Project/ProjectGetter.mjs'
+
+const { ObjectId } = mongodb
 
 
 describe('ProjectGetter', function () {
 describe('ProjectGetter', function () {
-  beforeEach(function () {
-    this.project = { _id: new ObjectId() }
-    this.projectIdStr = this.project._id.toString()
-    this.deletedProject = { deleterData: { wombat: 'potato' } }
-    this.userId = new ObjectId()
+  beforeEach(async function (ctx) {
+    ctx.project = { _id: new ObjectId() }
+    ctx.projectIdStr = ctx.project._id.toString()
+    ctx.deletedProject = { deleterData: { wombat: 'potato' } }
+    ctx.userId = new ObjectId()
 
 
-    this.DeletedProject = {
+    ctx.DeletedProject = {
       find: sinon.stub().returns({
       find: sinon.stub().returns({
-        exec: sinon.stub().resolves([this.deletedProject]),
+        exec: sinon.stub().resolves([ctx.deletedProject]),
       }),
       }),
     }
     }
-    this.Project = {
+    ctx.Project = {
       find: sinon.stub().returns({
       find: sinon.stub().returns({
         exec: sinon.stub().resolves(),
         exec: sinon.stub().resolves(),
       }),
       }),
       findOne: sinon.stub().returns({
       findOne: sinon.stub().returns({
-        exec: sinon.stub().resolves(this.project),
+        exec: sinon.stub().resolves(ctx.project),
       }),
       }),
     }
     }
-    this.CollaboratorsGetter = {
+    ctx.CollaboratorsGetter = {
       promises: {
       promises: {
         getProjectsUserIsMemberOf: sinon.stub().resolves({
         getProjectsUserIsMemberOf: sinon.stub().resolves({
           readAndWrite: [],
           readAndWrite: [],
@@ -34,58 +35,76 @@ describe('ProjectGetter', function () {
         }),
         }),
       },
       },
     }
     }
-    this.LockManager = {
+    ctx.LockManager = {
       promises: {
       promises: {
         runWithLock: sinon
         runWithLock: sinon
           .stub()
           .stub()
           .callsFake((namespace, id, runner) => runner()),
           .callsFake((namespace, id, runner) => runner()),
       },
       },
     }
     }
-    this.db = {
+    ctx.db = {
       projects: {
       projects: {
-        findOne: sinon.stub().resolves(this.project),
+        findOne: sinon.stub().resolves(ctx.project),
       },
       },
       users: {},
       users: {},
     }
     }
-    this.ProjectEntityMongoUpdateHandler = {
+    ctx.ProjectEntityMongoUpdateHandler = {
       lockKey: sinon.stub().returnsArg(0),
       lockKey: sinon.stub().returnsArg(0),
     }
     }
-    this.ProjectGetter = SandboxedModule.require(modulePath, {
-      requires: {
-        '../../infrastructure/mongodb': { db: this.db, ObjectId },
-        '../../models/Project': {
-          Project: this.Project,
-        },
-        '../../models/DeletedProject': {
-          DeletedProject: this.DeletedProject,
-        },
-        '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
-        '../../infrastructure/LockManager': this.LockManager,
-        './ProjectEntityMongoUpdateHandler':
-          this.ProjectEntityMongoUpdateHandler,
-      },
-    })
+
+    vi.doMock('../../../../app/src/infrastructure/mongodb', () => ({
+      db: ctx.db,
+      ObjectId,
+    }))
+
+    vi.doMock('../../../../app/src/models/Project', () => ({
+      Project: ctx.Project,
+    }))
+
+    vi.doMock('../../../../app/src/models/DeletedProject', () => ({
+      DeletedProject: ctx.DeletedProject,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Collaborators/CollaboratorsGetter',
+      () => ({
+        default: ctx.CollaboratorsGetter,
+      })
+    )
+
+    vi.doMock('../../../../app/src/infrastructure/LockManager', () => ({
+      default: ctx.LockManager,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectEntityMongoUpdateHandler',
+      () => ({
+        default: ctx.ProjectEntityMongoUpdateHandler,
+      })
+    )
+
+    ctx.ProjectGetter = (await import(modulePath)).default
   })
   })
 
 
   describe('getProjectWithoutDocLines', function () {
   describe('getProjectWithoutDocLines', function () {
-    beforeEach(function () {
-      this.ProjectGetter.promises.getProject = sinon.stub().resolves()
+    beforeEach(function (ctx) {
+      ctx.ProjectGetter.promises.getProject = sinon.stub().resolves()
     })
     })
 
 
     describe('passing an id', function () {
     describe('passing an id', function () {
-      beforeEach(async function () {
-        await this.ProjectGetter.promises.getProjectWithoutDocLines(
-          this.project._id
+      beforeEach(async function (ctx) {
+        await ctx.ProjectGetter.promises.getProjectWithoutDocLines(
+          ctx.project._id
         )
         )
       })
       })
 
 
-      it('should call find with the project id', function () {
-        this.ProjectGetter.promises.getProject
-          .calledWith(this.project._id)
+      it('should call find with the project id', function (ctx) {
+        ctx.ProjectGetter.promises.getProject
+          .calledWith(ctx.project._id)
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should exclude the doc lines', function () {
+      it('should exclude the doc lines', function (ctx) {
         const excludes = {
         const excludes = {
           'rootFolder.docs.lines': 0,
           'rootFolder.docs.lines': 0,
           'rootFolder.folders.docs.lines': 0,
           'rootFolder.folders.docs.lines': 0,
@@ -97,32 +116,32 @@ describe('ProjectGetter', function () {
           'rootFolder.folders.folders.folders.folders.folders.folders.folders.docs.lines': 0,
           'rootFolder.folders.folders.folders.folders.folders.folders.folders.docs.lines': 0,
         }
         }
 
 
-        this.ProjectGetter.promises.getProject
-          .calledWith(this.project._id, excludes)
+        ctx.ProjectGetter.promises.getProject
+          .calledWith(ctx.project._id, excludes)
           .should.equal(true)
           .should.equal(true)
       })
       })
     })
     })
   })
   })
 
 
   describe('getProjectWithOnlyFolders', function () {
   describe('getProjectWithOnlyFolders', function () {
-    beforeEach(function () {
-      this.ProjectGetter.promises.getProject = sinon.stub().resolves()
+    beforeEach(function (ctx) {
+      ctx.ProjectGetter.promises.getProject = sinon.stub().resolves()
     })
     })
 
 
     describe('passing an id', function () {
     describe('passing an id', function () {
-      beforeEach(async function () {
-        await this.ProjectGetter.promises.getProjectWithOnlyFolders(
-          this.project._id
+      beforeEach(async function (ctx) {
+        await ctx.ProjectGetter.promises.getProjectWithOnlyFolders(
+          ctx.project._id
         )
         )
       })
       })
 
 
-      it('should call find with the project id', function () {
-        this.ProjectGetter.promises.getProject
-          .calledWith(this.project._id)
+      it('should call find with the project id', function (ctx) {
+        ctx.ProjectGetter.promises.getProject
+          .calledWith(ctx.project._id)
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should exclude the docs and files lines', function () {
+      it('should exclude the docs and files lines', function (ctx) {
         const excludes = {
         const excludes = {
           'rootFolder.docs': 0,
           'rootFolder.docs': 0,
           'rootFolder.fileRefs': 0,
           'rootFolder.fileRefs': 0,
@@ -141,8 +160,8 @@ describe('ProjectGetter', function () {
           'rootFolder.folders.folders.folders.folders.folders.folders.folders.docs': 0,
           'rootFolder.folders.folders.folders.folders.folders.folders.folders.docs': 0,
           'rootFolder.folders.folders.folders.folders.folders.folders.folders.fileRefs': 0,
           'rootFolder.folders.folders.folders.folders.folders.folders.folders.fileRefs': 0,
         }
         }
-        this.ProjectGetter.promises.getProject
-          .calledWith(this.project._id, excludes)
+        ctx.ProjectGetter.promises.getProject
+          .calledWith(ctx.project._id, excludes)
           .should.equal(true)
           .should.equal(true)
       })
       })
     })
     })
@@ -151,58 +170,58 @@ describe('ProjectGetter', function () {
   describe('getProject', function () {
   describe('getProject', function () {
     describe('without projection', function () {
     describe('without projection', function () {
       describe('with project id', function () {
       describe('with project id', function () {
-        beforeEach(async function () {
-          await this.ProjectGetter.promises.getProject(this.projectIdStr)
+        beforeEach(async function (ctx) {
+          await ctx.ProjectGetter.promises.getProject(ctx.projectIdStr)
         })
         })
 
 
-        it('should call findOne with the project id', function () {
-          expect(this.db.projects.findOne.callCount).to.equal(1)
+        it('should call findOne with the project id', function (ctx) {
+          expect(ctx.db.projects.findOne.callCount).to.equal(1)
           expect(
           expect(
-            this.db.projects.findOne.lastCall.args[0]._id.toString()
-          ).to.equal(this.projectIdStr)
+            ctx.db.projects.findOne.lastCall.args[0]._id.toString()
+          ).to.equal(ctx.projectIdStr)
         })
         })
       })
       })
 
 
       describe('without project id', function () {
       describe('without project id', function () {
-        it('should be rejected', function () {
+        it('should be rejected', function (ctx) {
           expect(
           expect(
-            this.ProjectGetter.promises.getProject(null)
+            ctx.ProjectGetter.promises.getProject(null)
           ).to.be.rejectedWith('no project id provided')
           ).to.be.rejectedWith('no project id provided')
-          expect(this.db.projects.findOne.callCount).to.equal(0)
+          expect(ctx.db.projects.findOne.callCount).to.equal(0)
         })
         })
       })
       })
     })
     })
 
 
     describe('with projection', function () {
     describe('with projection', function () {
-      beforeEach(function () {
-        this.projection = { _id: 1 }
+      beforeEach(function (ctx) {
+        ctx.projection = { _id: 1 }
       })
       })
 
 
       describe('with project id', function () {
       describe('with project id', function () {
-        beforeEach(async function () {
-          await this.ProjectGetter.promises.getProject(
-            this.projectIdStr,
-            this.projection
+        beforeEach(async function (ctx) {
+          await ctx.ProjectGetter.promises.getProject(
+            ctx.projectIdStr,
+            ctx.projection
           )
           )
         })
         })
 
 
-        it('should call findOne with the project id', function () {
-          expect(this.db.projects.findOne.callCount).to.equal(1)
+        it('should call findOne with the project id', function (ctx) {
+          expect(ctx.db.projects.findOne.callCount).to.equal(1)
           expect(
           expect(
-            this.db.projects.findOne.lastCall.args[0]._id.toString()
-          ).to.equal(this.projectIdStr)
-          expect(this.db.projects.findOne.lastCall.args[1]).to.deep.equal({
-            projection: this.projection,
+            ctx.db.projects.findOne.lastCall.args[0]._id.toString()
+          ).to.equal(ctx.projectIdStr)
+          expect(ctx.db.projects.findOne.lastCall.args[1]).to.deep.equal({
+            projection: ctx.projection,
           })
           })
         })
         })
       })
       })
 
 
       describe('without project id', function () {
       describe('without project id', function () {
-        it('should be rejected', function () {
+        it('should be rejected', function (ctx) {
           expect(
           expect(
-            this.ProjectGetter.promises.getProject(null)
+            ctx.ProjectGetter.promises.getProject(null)
           ).to.be.rejectedWith('no project id provided')
           ).to.be.rejectedWith('no project id provided')
-          expect(this.db.projects.findOne.callCount).to.equal(0)
+          expect(ctx.db.projects.findOne.callCount).to.equal(0)
         })
         })
       })
       })
     })
     })
@@ -211,155 +230,151 @@ describe('ProjectGetter', function () {
   describe('getProjectWithoutLock', function () {
   describe('getProjectWithoutLock', function () {
     describe('without projection', function () {
     describe('without projection', function () {
       describe('with project id', function () {
       describe('with project id', function () {
-        beforeEach(async function () {
-          await this.ProjectGetter.promises.getProjectWithoutLock(
-            this.projectIdStr
+        beforeEach(async function (ctx) {
+          await ctx.ProjectGetter.promises.getProjectWithoutLock(
+            ctx.projectIdStr
           )
           )
         })
         })
 
 
-        it('should call findOne with the project id', function () {
-          expect(this.db.projects.findOne.callCount).to.equal(1)
+        it('should call findOne with the project id', function (ctx) {
+          expect(ctx.db.projects.findOne.callCount).to.equal(1)
           expect(
           expect(
-            this.db.projects.findOne.lastCall.args[0]._id.toString()
-          ).to.equal(this.projectIdStr)
+            ctx.db.projects.findOne.lastCall.args[0]._id.toString()
+          ).to.equal(ctx.projectIdStr)
         })
         })
       })
       })
 
 
       describe('without project id', function () {
       describe('without project id', function () {
-        it('should be rejected', function () {
+        it('should be rejected', function (ctx) {
           expect(
           expect(
-            this.ProjectGetter.promises.getProjectWithoutLock(null)
+            ctx.ProjectGetter.promises.getProjectWithoutLock(null)
           ).to.be.rejectedWith('no project id provided')
           ).to.be.rejectedWith('no project id provided')
-          expect(this.db.projects.findOne.callCount).to.equal(0)
+          expect(ctx.db.projects.findOne.callCount).to.equal(0)
         })
         })
       })
       })
     })
     })
 
 
     describe('with projection', function () {
     describe('with projection', function () {
-      beforeEach(function () {
-        this.projection = { _id: 1 }
+      beforeEach(function (ctx) {
+        ctx.projection = { _id: 1 }
       })
       })
 
 
       describe('with project id', function () {
       describe('with project id', function () {
-        beforeEach(async function () {
-          await this.ProjectGetter.promises.getProjectWithoutLock(
-            this.project._id,
-            this.projection
+        beforeEach(async function (ctx) {
+          await ctx.ProjectGetter.promises.getProjectWithoutLock(
+            ctx.project._id,
+            ctx.projection
           )
           )
         })
         })
 
 
-        it('should call findOne with the project id', function () {
-          expect(this.db.projects.findOne.callCount).to.equal(1)
+        it('should call findOne with the project id', function (ctx) {
+          expect(ctx.db.projects.findOne.callCount).to.equal(1)
           expect(
           expect(
-            this.db.projects.findOne.lastCall.args[0]._id.toString()
-          ).to.equal(this.projectIdStr)
-          expect(this.db.projects.findOne.lastCall.args[1]).to.deep.equal({
-            projection: this.projection,
+            ctx.db.projects.findOne.lastCall.args[0]._id.toString()
+          ).to.equal(ctx.projectIdStr)
+          expect(ctx.db.projects.findOne.lastCall.args[1]).to.deep.equal({
+            projection: ctx.projection,
           })
           })
         })
         })
       })
       })
 
 
       describe('without project id', function () {
       describe('without project id', function () {
-        it('should be rejected', function () {
+        it('should be rejected', function (ctx) {
           expect(
           expect(
-            this.ProjectGetter.promises.getProjectWithoutLock(null)
+            ctx.ProjectGetter.promises.getProjectWithoutLock(null)
           ).to.be.rejectedWith('no project id provided')
           ).to.be.rejectedWith('no project id provided')
-          expect(this.db.projects.findOne.callCount).to.equal(0)
+          expect(ctx.db.projects.findOne.callCount).to.equal(0)
         })
         })
       })
       })
     })
     })
   })
   })
 
 
   describe('findAllUsersProjects', function () {
   describe('findAllUsersProjects', function () {
-    beforeEach(function () {
-      this.fields = { mock: 'fields' }
-      this.projectOwned = { _id: 'mock-owned-projects' }
-      this.projectRW = { _id: 'mock-rw-projects' }
-      this.projectReview = { _id: 'mock-review-projects' }
-      this.projectRO = { _id: 'mock-ro-projects' }
-      this.projectTokenRW = { _id: 'mock-token-rw-projects' }
-      this.projectTokenRO = { _id: 'mock-token-ro-projects' }
-      this.Project.find
-        .withArgs({ owner_ref: this.userId }, this.fields)
-        .returns({ exec: sinon.stub().resolves([this.projectOwned]) })
+    beforeEach(function (ctx) {
+      ctx.fields = { mock: 'fields' }
+      ctx.projectOwned = { _id: 'mock-owned-projects' }
+      ctx.projectRW = { _id: 'mock-rw-projects' }
+      ctx.projectReview = { _id: 'mock-review-projects' }
+      ctx.projectRO = { _id: 'mock-ro-projects' }
+      ctx.projectTokenRW = { _id: 'mock-token-rw-projects' }
+      ctx.projectTokenRO = { _id: 'mock-token-ro-projects' }
+      ctx.Project.find
+        .withArgs({ owner_ref: ctx.userId }, ctx.fields)
+        .returns({ exec: sinon.stub().resolves([ctx.projectOwned]) })
     })
     })
 
 
-    it('should return a promise with all the projects', async function () {
-      this.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
-        readAndWrite: [this.projectRW],
-        readOnly: [this.projectRO],
-        tokenReadAndWrite: [this.projectTokenRW],
-        tokenReadOnly: [this.projectTokenRO],
-        review: [this.projectReview],
+    it('should return a promise with all the projects', async function (ctx) {
+      ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
+        readAndWrite: [ctx.projectRW],
+        readOnly: [ctx.projectRO],
+        tokenReadAndWrite: [ctx.projectTokenRW],
+        tokenReadOnly: [ctx.projectTokenRO],
+        review: [ctx.projectReview],
       })
       })
-      const projects = await this.ProjectGetter.promises.findAllUsersProjects(
-        this.userId,
-        this.fields
+      const projects = await ctx.ProjectGetter.promises.findAllUsersProjects(
+        ctx.userId,
+        ctx.fields
       )
       )
 
 
       expect(projects).to.deep.equal({
       expect(projects).to.deep.equal({
-        owned: [this.projectOwned],
-        readAndWrite: [this.projectRW],
-        readOnly: [this.projectRO],
-        tokenReadAndWrite: [this.projectTokenRW],
-        tokenReadOnly: [this.projectTokenRO],
-        review: [this.projectReview],
+        owned: [ctx.projectOwned],
+        readAndWrite: [ctx.projectRW],
+        readOnly: [ctx.projectRO],
+        tokenReadAndWrite: [ctx.projectTokenRW],
+        tokenReadOnly: [ctx.projectTokenRO],
+        review: [ctx.projectReview],
       })
       })
     })
     })
 
 
-    it('should remove duplicate projects', async function () {
-      this.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
-        readAndWrite: [this.projectRW, this.projectOwned],
-        readOnly: [this.projectRO, this.projectRW],
-        tokenReadAndWrite: [this.projectTokenRW, this.projectRO],
-        tokenReadOnly: [
-          this.projectTokenRW,
-          this.projectTokenRO,
-          this.projectRO,
-        ],
-        review: [this.projectReview],
+    it('should remove duplicate projects', async function (ctx) {
+      ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
+        readAndWrite: [ctx.projectRW, ctx.projectOwned],
+        readOnly: [ctx.projectRO, ctx.projectRW],
+        tokenReadAndWrite: [ctx.projectTokenRW, ctx.projectRO],
+        tokenReadOnly: [ctx.projectTokenRW, ctx.projectTokenRO, ctx.projectRO],
+        review: [ctx.projectReview],
       })
       })
-      const projects = await this.ProjectGetter.promises.findAllUsersProjects(
-        this.userId,
-        this.fields
+      const projects = await ctx.ProjectGetter.promises.findAllUsersProjects(
+        ctx.userId,
+        ctx.fields
       )
       )
 
 
       expect(projects).to.deep.equal({
       expect(projects).to.deep.equal({
-        owned: [this.projectOwned],
-        readAndWrite: [this.projectRW],
-        readOnly: [this.projectRO],
-        tokenReadAndWrite: [this.projectTokenRW],
-        tokenReadOnly: [this.projectTokenRO],
-        review: [this.projectReview],
+        owned: [ctx.projectOwned],
+        readAndWrite: [ctx.projectRW],
+        readOnly: [ctx.projectRO],
+        tokenReadAndWrite: [ctx.projectTokenRW],
+        tokenReadOnly: [ctx.projectTokenRO],
+        review: [ctx.projectReview],
       })
       })
     })
     })
   })
   })
 
 
   describe('getProjectIdByReadAndWriteToken', function () {
   describe('getProjectIdByReadAndWriteToken', function () {
     describe('when project find returns project', function () {
     describe('when project find returns project', function () {
-      this.beforeEach(async function () {
-        this.projectIdFound =
-          await this.ProjectGetter.promises.getProjectIdByReadAndWriteToken(
+      beforeEach(async function (ctx) {
+        ctx.projectIdFound =
+          await ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken(
             'token'
             'token'
           )
           )
       })
       })
 
 
-      it('should find project with token', function () {
-        this.Project.findOne
+      it('should find project with token', function (ctx) {
+        ctx.Project.findOne
           .calledWithMatch({ 'tokens.readAndWrite': 'token' })
           .calledWithMatch({ 'tokens.readAndWrite': 'token' })
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should return the project id', function () {
-        expect(this.projectIdFound).to.equal(this.project._id)
+      it('should return the project id', function (ctx) {
+        expect(ctx.projectIdFound).to.equal(ctx.project._id)
       })
       })
     })
     })
 
 
     describe('when project not found', function () {
     describe('when project not found', function () {
-      it('should return undefined', async function () {
-        this.Project.findOne.returns({ exec: sinon.stub().resolves(null) })
+      it('should return undefined', async function (ctx) {
+        ctx.Project.findOne.returns({ exec: sinon.stub().resolves(null) })
         const projectId =
         const projectId =
-          await this.ProjectGetter.promises.getProjectIdByReadAndWriteToken(
+          await ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken(
             'token'
             'token'
           )
           )
 
 
@@ -368,86 +383,79 @@ describe('ProjectGetter', function () {
     })
     })
 
 
     describe('when project find returns error', function () {
     describe('when project find returns error', function () {
-      this.beforeEach(async function () {
-        this.Project.findOne.returns({ exec: sinon.stub().rejects() })
+      beforeEach(async function (ctx) {
+        ctx.Project.findOne.returns({ exec: sinon.stub().rejects() })
       })
       })
 
 
-      it('should be rejected', function () {
+      it('should be rejected', function (ctx) {
         expect(
         expect(
-          this.ProjectGetter.promises.getProjectIdByReadAndWriteToken('token')
+          ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken('token')
         ).to.be.rejected
         ).to.be.rejected
       })
       })
     })
     })
   })
   })
 
 
   describe('findUsersProjectsByName', function () {
   describe('findUsersProjectsByName', function () {
-    it('should perform a case-insensitive search', async function () {
-      this.project1 = { _id: 1, name: 'find me!' }
-      this.project2 = { _id: 2, name: 'not me!' }
-      this.project3 = { _id: 3, name: 'FIND ME!' }
-      this.project4 = { _id: 4, name: 'Find Me!' }
-      this.Project.find.withArgs({ owner_ref: this.userId }).returns({
+    it('should perform a case-insensitive search', async function (ctx) {
+      ctx.project1 = { _id: 1, name: 'find me!' }
+      ctx.project2 = { _id: 2, name: 'not me!' }
+      ctx.project3 = { _id: 3, name: 'FIND ME!' }
+      ctx.project4 = { _id: 4, name: 'Find Me!' }
+      ctx.Project.find.withArgs({ owner_ref: ctx.userId }).returns({
         exec: sinon
         exec: sinon
           .stub()
           .stub()
-          .resolves([
-            this.project1,
-            this.project2,
-            this.project3,
-            this.project4,
-          ]),
+          .resolves([ctx.project1, ctx.project2, ctx.project3, ctx.project4]),
       })
       })
-      const projects =
-        await this.ProjectGetter.promises.findUsersProjectsByName(
-          this.userId,
-          this.project1.name
-        )
+      const projects = await ctx.ProjectGetter.promises.findUsersProjectsByName(
+        ctx.userId,
+        ctx.project1.name
+      )
       const projectNames = projects.map(project => project.name)
       const projectNames = projects.map(project => project.name)
       expect(projectNames).to.have.members([
       expect(projectNames).to.have.members([
-        this.project1.name,
-        this.project3.name,
-        this.project4.name,
+        ctx.project1.name,
+        ctx.project3.name,
+        ctx.project4.name,
       ])
       ])
     })
     })
 
 
-    it('should search collaborations as well', async function () {
-      this.project1 = { _id: 1, name: 'find me!' }
-      this.project2 = { _id: 2, name: 'FIND ME!' }
-      this.project3 = { _id: 3, name: 'Find Me!' }
-      this.project4 = { _id: 4, name: 'find ME!' }
-      this.project5 = { _id: 5, name: 'FIND me!' }
-      this.Project.find
-        .withArgs({ owner_ref: this.userId })
-        .returns({ exec: sinon.stub().resolves([this.project1]) })
-      this.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
-        readAndWrite: [this.project2],
-        readOnly: [this.project3],
-        tokenReadAndWrite: [this.project4],
-        tokenReadOnly: [this.project5],
+    it('should search collaborations as well', async function (ctx) {
+      ctx.project1 = { _id: 1, name: 'find me!' }
+      ctx.project2 = { _id: 2, name: 'FIND ME!' }
+      ctx.project3 = { _id: 3, name: 'Find Me!' }
+      ctx.project4 = { _id: 4, name: 'find ME!' }
+      ctx.project5 = { _id: 5, name: 'FIND me!' }
+      ctx.Project.find
+        .withArgs({ owner_ref: ctx.userId })
+        .returns({ exec: sinon.stub().resolves([ctx.project1]) })
+      ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
+        readAndWrite: [ctx.project2],
+        readOnly: [ctx.project3],
+        tokenReadAndWrite: [ctx.project4],
+        tokenReadOnly: [ctx.project5],
       })
       })
-      const projects =
-        await this.ProjectGetter.promises.findUsersProjectsByName(
-          this.userId,
-          this.project1.name
-        )
+      const projects = await ctx.ProjectGetter.promises.findUsersProjectsByName(
+        ctx.userId,
+        ctx.project1.name
+      )
       expect(projects.map(project => project.name)).to.have.members([
       expect(projects.map(project => project.name)).to.have.members([
-        this.project1.name,
-        this.project2.name,
+        ctx.project1.name,
+        ctx.project2.name,
       ])
       ])
     })
     })
   })
   })
 
 
   describe('getUsersDeletedProjects', function () {
   describe('getUsersDeletedProjects', function () {
-    it('should look up the deleted projects by deletedProjectOwnerId', async function () {
-      await this.ProjectGetter.promises.getUsersDeletedProjects('giraffe')
-      sinon.assert.calledWith(this.DeletedProject.find, {
+    it('should look up the deleted projects by deletedProjectOwnerId', async function (ctx) {
+      await ctx.ProjectGetter.promises.getUsersDeletedProjects('giraffe')
+      sinon.assert.calledWith(ctx.DeletedProject.find, {
         'deleterData.deletedProjectOwnerId': 'giraffe',
         'deleterData.deletedProjectOwnerId': 'giraffe',
       })
       })
     })
     })
 
 
-    it('should pass the found projects to the callback', async function () {
+    it('should pass the found projects to the callback', async function (ctx) {
       const docs =
       const docs =
-        await this.ProjectGetter.promises.getUsersDeletedProjects('giraffe')
-      expect(docs).to.deep.equal([this.deletedProject])
+        await ctx.ProjectGetter.promises.getUsersDeletedProjects('giraffe')
+      expect(docs).to.deep.equal([ctx.deletedProject])
     })
     })
   })
   })
 })
 })

+ 8 - 5
services/web/test/unit/src/Project/ProjectHistoryHandler.test.mjs

@@ -46,11 +46,14 @@ describe('ProjectHistoryHandler', function () {
       })
       })
     )
     )
 
 
-    vi.doMock('../../../../app/src/Features/History/HistoryManager.mjs', () => ({
-      default: (ctx.HistoryManager = {
-        promises: {},
-      }),
-    }))
+    vi.doMock(
+      '../../../../app/src/Features/History/HistoryManager.mjs',
+      () => ({
+        default: (ctx.HistoryManager = {
+          promises: {},
+        }),
+      })
+    )
 
 
     vi.doMock(
     vi.doMock(
       '../../../../app/src/Features/Project/ProjectEntityUpdateHandler',
       '../../../../app/src/Features/Project/ProjectEntityUpdateHandler',

+ 7 - 4
services/web/test/unit/src/Project/ProjectListController.test.mjs

@@ -5,10 +5,13 @@ import Errors from '../../../../app/src/Features/Errors/Errors.js'
 
 
 const ObjectId = mongodb.ObjectId
 const ObjectId = mongodb.ObjectId
 
 
-const MODULE_PATH = new URL(
-  '../../../../app/src/Features/Project/ProjectListController',
-  import.meta.url
-).pathname
+const MODULE_PATH = `${import.meta.dirname}/../../../../app/src/Features/Project/ProjectListController`
+
+// Mock AnalyticsManager as it isn't used in these tests but causes the User model to be imported
+// TODO: remove this once all models are ESM and this kind of mocking is no longer necessary
+vi.mock('../../../../app/src/Features/Analytics/AnalyticsManager.js', () => {
+  return {}
+})
 
 
 describe('ProjectListController', function () {
 describe('ProjectListController', function () {
   beforeEach(async function (ctx) {
   beforeEach(async function (ctx) {

+ 133 - 135
services/web/test/unit/src/Project/ProjectLocator.test.mjs

@@ -1,8 +1,11 @@
-const { expect } = require('chai')
+import { vi, expect } from 'vitest'
+import sinon from 'sinon'
+import Errors from '../../../../app/src/Features/Errors/Errors.js'
 const modulePath = '../../../../app/src/Features/Project/ProjectLocator'
 const modulePath = '../../../../app/src/Features/Project/ProjectLocator'
-const SandboxedModule = require('sandboxed-module')
-const sinon = require('sinon')
-const Errors = require('../../../../app/src/Features/Errors/Errors')
+
+vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
+  vi.importActual('../../../../app/src/Features/Errors/Errors.js')
+)
 
 
 const project = { _id: '1234566', rootFolder: [] }
 const project = { _id: '1234566', rootFolder: [] }
 const rootDoc = { name: 'rootDoc', _id: 'das239djd' }
 const rootDoc = { name: 'rootDoc', _id: 'das239djd' }
@@ -38,47 +41,50 @@ project.rootFolder[0] = rootFolder
 project.rootDoc_id = rootDoc._id
 project.rootDoc_id = rootDoc._id
 
 
 describe('ProjectLocator', function () {
 describe('ProjectLocator', function () {
-  beforeEach(function () {
-    this.ProjectGetter = {
+  beforeEach(async function (ctx) {
+    ctx.ProjectGetter = {
       getProject: sinon.stub().callsArgWith(2, null, project),
       getProject: sinon.stub().callsArgWith(2, null, project),
     }
     }
-    this.ProjectHelper = {
+    ctx.ProjectHelper = {
       isArchived: sinon.stub(),
       isArchived: sinon.stub(),
       isTrashed: sinon.stub(),
       isTrashed: sinon.stub(),
       isArchivedOrTrashed: sinon.stub(),
       isArchivedOrTrashed: sinon.stub(),
     }
     }
-    this.locator = SandboxedModule.require(modulePath, {
-      requires: {
-        '../../models/User': { User: this.User },
-        './ProjectGetter': this.ProjectGetter,
-        './ProjectHelper': this.ProjectHelper,
-      },
-    })
+
+    vi.doMock('../../../../app/src/models/User', () => ({
+      default: { User: ctx.User },
+    }))
+
+    vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
+      default: ctx.ProjectGetter,
+    }))
+
+    vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({
+      default: ctx.ProjectHelper,
+    }))
+
+    ctx.locator = (await import(modulePath)).default
   })
   })
 
 
   describe('finding a doc', function () {
   describe('finding a doc', function () {
-    it('finds one at the root level', async function () {
-      const { element, path, folder } = await this.locator.promises.findElement(
-        {
-          project_id: project._id,
-          element_id: doc2._id,
-          type: 'docs',
-        }
-      )
+    it('finds one at the root level', async function (ctx) {
+      const { element, path, folder } = await ctx.locator.promises.findElement({
+        project_id: project._id,
+        element_id: doc2._id,
+        type: 'docs',
+      })
       element._id.should.equal(doc2._id)
       element._id.should.equal(doc2._id)
       path.fileSystem.should.equal(`/${doc2.name}`)
       path.fileSystem.should.equal(`/${doc2.name}`)
       folder._id.should.equal(project.rootFolder[0]._id)
       folder._id.should.equal(project.rootFolder[0]._id)
       path.mongo.should.equal('rootFolder.0.docs.1')
       path.mongo.should.equal('rootFolder.0.docs.1')
     })
     })
 
 
-    it('when it is nested', async function () {
-      const { element, path, folder } = await this.locator.promises.findElement(
-        {
-          project_id: project._id,
-          element_id: subSubDoc._id,
-          type: 'doc',
-        }
-      )
+    it('when it is nested', async function (ctx) {
+      const { element, path, folder } = await ctx.locator.promises.findElement({
+        project_id: project._id,
+        element_id: subSubDoc._id,
+        type: 'doc',
+      })
       expect(element._id).to.equal(subSubDoc._id)
       expect(element._id).to.equal(subSubDoc._id)
       path.fileSystem.should.equal(
       path.fileSystem.should.equal(
         `/${subFolder.name}/${secondSubFolder.name}/${subSubDoc.name}`
         `/${subFolder.name}/${secondSubFolder.name}/${subSubDoc.name}`
@@ -87,9 +93,9 @@ describe('ProjectLocator', function () {
       path.mongo.should.equal('rootFolder.0.folders.1.folders.0.docs.0')
       path.mongo.should.equal('rootFolder.0.folders.1.folders.0.docs.0')
     })
     })
 
 
-    it('should give error if element could not be found', async function () {
+    it('should give error if element could not be found', async function (ctx) {
       await expect(
       await expect(
-        this.locator.promises.findElement({
+        ctx.locator.promises.findElement({
           project_id: project._id,
           project_id: project._id,
           element_id: 'ddsd432nj42',
           element_id: 'ddsd432nj42',
           type: 'docs',
           type: 'docs',
@@ -101,20 +107,18 @@ describe('ProjectLocator', function () {
   })
   })
 
 
   describe('finding a folder', function () {
   describe('finding a folder', function () {
-    it('should return root folder when looking for root folder', async function () {
-      const { element: foundElement } = await this.locator.promises.findElement(
-        {
-          project_id: project._id,
-          element_id: rootFolder._id,
-          type: 'folder',
-        }
-      )
+    it('should return root folder when looking for root folder', async function (ctx) {
+      const { element: foundElement } = await ctx.locator.promises.findElement({
+        project_id: project._id,
+        element_id: rootFolder._id,
+        type: 'folder',
+      })
       foundElement._id.should.equal(rootFolder._id)
       foundElement._id.should.equal(rootFolder._id)
     })
     })
 
 
-    it('should not return root folder when searching for docs', async function () {
+    it('should not return root folder when searching for docs', async function (ctx) {
       await expect(
       await expect(
-        this.locator.promises.findElement({
+        ctx.locator.promises.findElement({
           project_id: project._id,
           project_id: project._id,
           element_id: rootFolder._id,
           element_id: rootFolder._id,
           type: 'docs',
           type: 'docs',
@@ -124,9 +128,9 @@ describe('ProjectLocator', function () {
         .and.eventually.have.property('message', 'entity not found')
         .and.eventually.have.property('message', 'entity not found')
     })
     })
 
 
-    it('should not return root folder when searching for files', async function () {
+    it('should not return root folder when searching for files', async function (ctx) {
       await expect(
       await expect(
-        this.locator.promises.findElement({
+        ctx.locator.promises.findElement({
           project_id: project._id,
           project_id: project._id,
           element_id: rootFolder._id,
           element_id: rootFolder._id,
           type: 'files',
           type: 'files',
@@ -136,12 +140,12 @@ describe('ProjectLocator', function () {
         .and.eventually.have.property('message', 'entity not found')
         .and.eventually.have.property('message', 'entity not found')
     })
     })
 
 
-    it('when at root', async function () {
+    it('when at root', async function (ctx) {
       const {
       const {
         element: foundElement,
         element: foundElement,
         path,
         path,
         folder: parentFolder,
         folder: parentFolder,
-      } = await this.locator.promises.findElement({
+      } = await ctx.locator.promises.findElement({
         project_id: project._id,
         project_id: project._id,
         element_id: subFolder._id,
         element_id: subFolder._id,
         type: 'folder',
         type: 'folder',
@@ -152,12 +156,12 @@ describe('ProjectLocator', function () {
       path.mongo.should.equal('rootFolder.0.folders.1')
       path.mongo.should.equal('rootFolder.0.folders.1')
     })
     })
 
 
-    it('when deeply nested', async function () {
+    it('when deeply nested', async function (ctx) {
       const {
       const {
         element: foundElement,
         element: foundElement,
         path,
         path,
         folder: parentFolder,
         folder: parentFolder,
-      } = await this.locator.promises.findElement({
+      } = await ctx.locator.promises.findElement({
         project_id: project._id,
         project_id: project._id,
         element_id: secondSubFolder._id,
         element_id: secondSubFolder._id,
         type: 'folder',
         type: 'folder',
@@ -171,12 +175,12 @@ describe('ProjectLocator', function () {
   })
   })
 
 
   describe('finding a file', function () {
   describe('finding a file', function () {
-    it('when at root', async function () {
+    it('when at root', async function (ctx) {
       const {
       const {
         element: foundElement,
         element: foundElement,
         path,
         path,
         folder: parentFolder,
         folder: parentFolder,
-      } = await this.locator.promises.findElement({
+      } = await ctx.locator.promises.findElement({
         project_id: project._id,
         project_id: project._id,
         element_id: file1._id,
         element_id: file1._id,
         type: 'fileRefs',
         type: 'fileRefs',
@@ -187,12 +191,12 @@ describe('ProjectLocator', function () {
       path.mongo.should.equal('rootFolder.0.fileRefs.0')
       path.mongo.should.equal('rootFolder.0.fileRefs.0')
     })
     })
 
 
-    it('when deeply nested', async function () {
+    it('when deeply nested', async function (ctx) {
       const {
       const {
         element: foundElement,
         element: foundElement,
         path,
         path,
         folder: parentFolder,
         folder: parentFolder,
-      } = await this.locator.promises.findElement({
+      } = await ctx.locator.promises.findElement({
         project_id: project._id,
         project_id: project._id,
         element_id: subSubFile._id,
         element_id: subSubFile._id,
         type: 'fileRefs',
         type: 'fileRefs',
@@ -207,25 +211,21 @@ describe('ProjectLocator', function () {
   })
   })
 
 
   describe('finding an element with wrong element type', function () {
   describe('finding an element with wrong element type', function () {
-    it('should add an s onto the element type', async function () {
-      const { element: foundElement } = await this.locator.promises.findElement(
-        {
-          project_id: project._id,
-          element_id: subSubDoc._id,
-          type: 'doc',
-        }
-      )
+    it('should add an s onto the element type', async function (ctx) {
+      const { element: foundElement } = await ctx.locator.promises.findElement({
+        project_id: project._id,
+        element_id: subSubDoc._id,
+        type: 'doc',
+      })
       foundElement._id.should.equal(subSubDoc._id)
       foundElement._id.should.equal(subSubDoc._id)
     })
     })
 
 
-    it('should convert file to fileRefs', async function () {
-      const { element: foundElement } = await this.locator.promises.findElement(
-        {
-          project_id: project._id,
-          element_id: file1._id,
-          type: 'fileRefs',
-        }
-      )
+    it('should convert file to fileRefs', async function (ctx) {
+      const { element: foundElement } = await ctx.locator.promises.findElement({
+        project_id: project._id,
+        element_id: file1._id,
+        type: 'fileRefs',
+      })
       foundElement._id.should.equal(file1._id)
       foundElement._id.should.equal(file1._id)
     })
     })
   })
   })
@@ -243,12 +243,12 @@ describe('ProjectLocator', function () {
       _id: '1234566',
       _id: '1234566',
       rootFolder: [rootFolder2],
       rootFolder: [rootFolder2],
     }
     }
-    it('should find doc in project', async function () {
+    it('should find doc in project', async function (ctx) {
       const {
       const {
         element: foundElement,
         element: foundElement,
         path,
         path,
         folder: parentFolder,
         folder: parentFolder,
-      } = await this.locator.promises.findElement({
+      } = await ctx.locator.promises.findElement({
         project: project2,
         project: project2,
         element_id: doc3._id,
         element_id: doc3._id,
         type: 'docs',
         type: 'docs',
@@ -261,38 +261,38 @@ describe('ProjectLocator', function () {
   })
   })
 
 
   describe('finding root doc', function () {
   describe('finding root doc', function () {
-    it('should return root doc when passed project', async function () {
-      const { element: doc } = await this.locator.promises.findRootDoc(project)
+    it('should return root doc when passed project', async function (ctx) {
+      const { element: doc } = await ctx.locator.promises.findRootDoc(project)
       doc._id.should.equal(rootDoc._id)
       doc._id.should.equal(rootDoc._id)
     })
     })
 
 
-    it('should return root doc when passed project_id', async function () {
-      const { element: doc } = await this.locator.promises.findRootDoc(
+    it('should return root doc when passed project_id', async function (ctx) {
+      const { element: doc } = await ctx.locator.promises.findRootDoc(
         project._id
         project._id
       )
       )
       doc._id.should.equal(rootDoc._id)
       doc._id.should.equal(rootDoc._id)
     })
     })
 
 
-    it('should return null when the project has no rootDoc', async function () {
+    it('should return null when the project has no rootDoc', async function (ctx) {
       project.rootDoc_id = null
       project.rootDoc_id = null
       const { element: rootDoc } =
       const { element: rootDoc } =
-        await this.locator.promises.findRootDoc(project)
+        await ctx.locator.promises.findRootDoc(project)
       expect(rootDoc).to.equal(null)
       expect(rootDoc).to.equal(null)
     })
     })
 
 
-    it('should return null when the rootDoc_id no longer exists', async function () {
+    it('should return null when the rootDoc_id no longer exists', async function (ctx) {
       project.rootDoc_id = 'doesntexist'
       project.rootDoc_id = 'doesntexist'
       const { element: rootDoc } =
       const { element: rootDoc } =
-        await this.locator.promises.findRootDoc(project)
+        await ctx.locator.promises.findRootDoc(project)
       expect(rootDoc).to.equal(null)
       expect(rootDoc).to.equal(null)
     })
     })
   })
   })
 
 
   describe('findElementByPath', function () {
   describe('findElementByPath', function () {
-    it('should take a doc path and return the element for a root level document', async function () {
+    it('should take a doc path and return the element for a root level document', async function (ctx) {
       const path = `${doc1.name}`
       const path = `${doc1.name}`
       const { element, type, folder } =
       const { element, type, folder } =
-        await this.locator.promises.findElementByPath({
+        await ctx.locator.promises.findElementByPath({
           project,
           project,
           path,
           path,
         })
         })
@@ -301,10 +301,10 @@ describe('ProjectLocator', function () {
       expect(folder).to.equal(rootFolder)
       expect(folder).to.equal(rootFolder)
     })
     })
 
 
-    it('should take a doc path and return the element for a root level document with a starting slash', async function () {
+    it('should take a doc path and return the element for a root level document with a starting slash', async function (ctx) {
       const path = `/${doc1.name}`
       const path = `/${doc1.name}`
       const { element, type, folder } =
       const { element, type, folder } =
-        await this.locator.promises.findElementByPath({
+        await ctx.locator.promises.findElementByPath({
           project,
           project,
           path,
           path,
         })
         })
@@ -313,10 +313,10 @@ describe('ProjectLocator', function () {
       expect(folder).to.equal(rootFolder)
       expect(folder).to.equal(rootFolder)
     })
     })
 
 
-    it('should take a doc path and return the element for a nested document', async function () {
+    it('should take a doc path and return the element for a nested document', async function (ctx) {
       const path = `${subFolder.name}/${secondSubFolder.name}/${subSubDoc.name}`
       const path = `${subFolder.name}/${secondSubFolder.name}/${subSubDoc.name}`
       const { element, type, folder } =
       const { element, type, folder } =
-        await this.locator.promises.findElementByPath({
+        await ctx.locator.promises.findElementByPath({
           project,
           project,
           path,
           path,
         })
         })
@@ -325,10 +325,10 @@ describe('ProjectLocator', function () {
       expect(folder).to.equal(secondSubFolder)
       expect(folder).to.equal(secondSubFolder)
     })
     })
 
 
-    it('should take a file path and return the element for a root level document', async function () {
+    it('should take a file path and return the element for a root level document', async function (ctx) {
       const path = `${file1.name}`
       const path = `${file1.name}`
       const { element, type, folder } =
       const { element, type, folder } =
-        await this.locator.promises.findElementByPath({
+        await ctx.locator.promises.findElementByPath({
           project,
           project,
           path,
           path,
         })
         })
@@ -337,10 +337,10 @@ describe('ProjectLocator', function () {
       expect(folder).to.equal(rootFolder)
       expect(folder).to.equal(rootFolder)
     })
     })
 
 
-    it('should take a file path and return the element for a nested document', async function () {
+    it('should take a file path and return the element for a nested document', async function (ctx) {
       const path = `${subFolder.name}/${secondSubFolder.name}/${subSubFile.name}`
       const path = `${subFolder.name}/${secondSubFolder.name}/${subSubFile.name}`
       const { element, type, folder } =
       const { element, type, folder } =
-        await this.locator.promises.findElementByPath({
+        await ctx.locator.promises.findElementByPath({
           project,
           project,
           path,
           path,
         })
         })
@@ -349,10 +349,10 @@ describe('ProjectLocator', function () {
       expect(folder).to.equal(secondSubFolder)
       expect(folder).to.equal(secondSubFolder)
     })
     })
 
 
-    it('should take a file path and return the element for a nested document case insenstive', async function () {
+    it('should take a file path and return the element for a nested document case insenstive', async function (ctx) {
       const path = `${subFolder.name.toUpperCase()}/${secondSubFolder.name.toUpperCase()}/${subSubFile.name.toUpperCase()}`
       const path = `${subFolder.name.toUpperCase()}/${secondSubFolder.name.toUpperCase()}/${subSubFile.name.toUpperCase()}`
       const { element, type, folder } =
       const { element, type, folder } =
-        await this.locator.promises.findElementByPath({
+        await ctx.locator.promises.findElementByPath({
           project,
           project,
           path,
           path,
         })
         })
@@ -361,10 +361,10 @@ describe('ProjectLocator', function () {
       expect(folder).to.equal(secondSubFolder)
       expect(folder).to.equal(secondSubFolder)
     })
     })
 
 
-    it('should not return elements with a case-insensitive match when exactCaseMatch is true', async function () {
+    it('should not return elements with a case-insensitive match when exactCaseMatch is true', async function (ctx) {
       const path = `${subFolder.name.toUpperCase()}/${secondSubFolder.name.toUpperCase()}/${subSubFile.name.toUpperCase()}`
       const path = `${subFolder.name.toUpperCase()}/${secondSubFolder.name.toUpperCase()}/${subSubFile.name.toUpperCase()}`
       await expect(
       await expect(
-        this.locator.promises.findElementByPath({
+        ctx.locator.promises.findElementByPath({
           project,
           project,
           path,
           path,
           exactCaseMatch: true,
           exactCaseMatch: true,
@@ -372,10 +372,10 @@ describe('ProjectLocator', function () {
       ).to.eventually.be.rejected
       ).to.eventually.be.rejected
     })
     })
 
 
-    it('should take a file path and return the element for a nested folder', async function () {
+    it('should take a file path and return the element for a nested folder', async function (ctx) {
       const path = `${subFolder.name}/${secondSubFolder.name}`
       const path = `${subFolder.name}/${secondSubFolder.name}`
       const { element, type, folder } =
       const { element, type, folder } =
-        await this.locator.promises.findElementByPath({
+        await ctx.locator.promises.findElementByPath({
           project,
           project,
           path,
           path,
         })
         })
@@ -384,10 +384,10 @@ describe('ProjectLocator', function () {
       expect(folder).to.equal(subFolder)
       expect(folder).to.equal(subFolder)
     })
     })
 
 
-    it('should take a file path and return the root folder', async function () {
+    it('should take a file path and return the root folder', async function (ctx) {
       const path = '/'
       const path = '/'
       const { element, type, folder } =
       const { element, type, folder } =
-        await this.locator.promises.findElementByPath({
+        await ctx.locator.promises.findElementByPath({
           project,
           project,
           path,
           path,
         })
         })
@@ -396,16 +396,16 @@ describe('ProjectLocator', function () {
       expect(folder).to.equal(null)
       expect(folder).to.equal(null)
     })
     })
 
 
-    it('should return an error if the file can not be found inside know folder', async function () {
+    it('should return an error if the file can not be found inside know folder', async function (ctx) {
       const path = `${subFolder.name}/${secondSubFolder.name}/exist.txt`
       const path = `${subFolder.name}/${secondSubFolder.name}/exist.txt`
-      await expect(this.locator.promises.findElementByPath({ project, path }))
-        .to.eventually.be.rejected
+      await expect(ctx.locator.promises.findElementByPath({ project, path })).to
+        .eventually.be.rejected
     })
     })
 
 
-    it('should return an error if the file can not be found inside unknown folder', async function () {
+    it('should return an error if the file can not be found inside unknown folder', async function (ctx) {
       const path = 'this/does/not/exist.txt'
       const path = 'this/does/not/exist.txt'
       await expect(
       await expect(
-        this.locator.promises.findElementByPath({
+        ctx.locator.promises.findElementByPath({
           project,
           project,
           path,
           path,
         })
         })
@@ -413,8 +413,8 @@ describe('ProjectLocator', function () {
     })
     })
 
 
     describe('where duplicate folder exists', function () {
     describe('where duplicate folder exists', function () {
-      beforeEach(function () {
-        this.duplicateFolder = {
+      beforeEach(function (ctx) {
+        ctx.duplicateFolder = {
           name: 'duplicate1',
           name: 'duplicate1',
           _id: '1234',
           _id: '1234',
           folders: [
           folders: [
@@ -425,13 +425,13 @@ describe('ProjectLocator', function () {
               fileRefs: [],
               fileRefs: [],
             },
             },
           ],
           ],
-          docs: [(this.doc = { name: 'main.tex', _id: '456' })],
+          docs: [(ctx.doc = { name: 'main.tex', _id: '456' })],
           fileRefs: [],
           fileRefs: [],
         }
         }
-        this.project = {
+        ctx.project = {
           rootFolder: [
           rootFolder: [
             {
             {
-              folders: [this.duplicateFolder, this.duplicateFolder],
+              folders: [ctx.duplicateFolder, ctx.duplicateFolder],
               fileRefs: [],
               fileRefs: [],
               docs: [],
               docs: [],
             },
             },
@@ -439,26 +439,26 @@ describe('ProjectLocator', function () {
         }
         }
       })
       })
 
 
-      it('should not call the callback more than once', async function () {
-        const path = `${this.duplicateFolder.name}/${this.doc.name}`
-        await this.locator.promises.findElementByPath({
-          project: this.project,
+      it('should not call the callback more than once', async function (ctx) {
+        const path = `${ctx.duplicateFolder.name}/${ctx.doc.name}`
+        await ctx.locator.promises.findElementByPath({
+          project: ctx.project,
           path,
           path,
         })
         })
       }) // mocha will throw exception if done called multiple times
       }) // mocha will throw exception if done called multiple times
 
 
-      it('should not call the callback more than once when the path is longer than 1 level below the duplicate level', async function () {
-        const path = `${this.duplicateFolder.name}/1/main.tex`
-        await this.locator.promises.findElementByPath({
-          project: this.project,
+      it('should not call the callback more than once when the path is longer than 1 level below the duplicate level', async function (ctx) {
+        const path = `${ctx.duplicateFolder.name}/1/main.tex`
+        await ctx.locator.promises.findElementByPath({
+          project: ctx.project,
           path,
           path,
         })
         })
       })
       })
     }) // mocha will throw exception if done called multiple times
     }) // mocha will throw exception if done called multiple times
 
 
     describe('with a null doc', function () {
     describe('with a null doc', function () {
-      beforeEach(function () {
-        this.project = {
+      beforeEach(function (ctx) {
+        ctx.project = {
           rootFolder: [
           rootFolder: [
             {
             {
               folders: [],
               folders: [],
@@ -469,10 +469,10 @@ describe('ProjectLocator', function () {
         }
         }
       })
       })
 
 
-      it('should not crash with a null', async function () {
+      it('should not crash with a null', async function (ctx) {
         const path = '/other.tex'
         const path = '/other.tex'
-        const { element } = await this.locator.promises.findElementByPath({
-          project: this.project,
+        const { element } = await ctx.locator.promises.findElementByPath({
+          project: ctx.project,
           path,
           path,
         })
         })
         element.name.should.equal('other.tex')
         element.name.should.equal('other.tex')
@@ -480,14 +480,14 @@ describe('ProjectLocator', function () {
     })
     })
 
 
     describe('with a null project', function () {
     describe('with a null project', function () {
-      beforeEach(function () {
-        this.ProjectGetter = { getProject: sinon.stub().callsArg(2) }
+      beforeEach(function (ctx) {
+        ctx.ProjectGetter = { getProject: sinon.stub().callsArg(2) }
       })
       })
 
 
-      it('should not crash with a null', async function () {
+      it('should not crash with a null', async function (ctx) {
         const path = '/other.tex'
         const path = '/other.tex'
         await expect(
         await expect(
-          this.locator.promises.findElementByPath({
+          ctx.locator.promises.findElementByPath({
             project_id: project._id,
             project_id: project._id,
             path,
             path,
           })
           })
@@ -496,15 +496,13 @@ describe('ProjectLocator', function () {
     })
     })
 
 
     describe('with a project_id', function () {
     describe('with a project_id', function () {
-      it('should take a doc path and return the element for a root level document', async function () {
+      it('should take a doc path and return the element for a root level document', async function (ctx) {
         const path = `${doc1.name}`
         const path = `${doc1.name}`
-        const { element, type } = await this.locator.promises.findElementByPath(
-          {
-            project_id: project._id,
-            path,
-          }
-        )
-        this.ProjectGetter.getProject
+        const { element, type } = await ctx.locator.promises.findElementByPath({
+          project_id: project._id,
+          path,
+        })
+        ctx.ProjectGetter.getProject
           .calledWith(project._id, { rootFolder: true, rootDoc_id: true })
           .calledWith(project._id, { rootFolder: true, rootDoc_id: true })
           .should.equal(true)
           .should.equal(true)
         element.should.deep.equal(doc1)
         element.should.deep.equal(doc1)
@@ -514,17 +512,17 @@ describe('ProjectLocator', function () {
   })
   })
 
 
   describe('findElementByMongoPath', function () {
   describe('findElementByMongoPath', function () {
-    it('traverses the file tree like Mongo would do', function () {
-      const element = this.locator.findElementByMongoPath(
+    it('traverses the file tree like Mongo would do', function (ctx) {
+      const element = ctx.locator.findElementByMongoPath(
         project,
         project,
         'rootFolder.0.folders.1.folders.0.fileRefs.0'
         'rootFolder.0.folders.1.folders.0.fileRefs.0'
       )
       )
       expect(element).to.equal(subSubFile)
       expect(element).to.equal(subSubFile)
     })
     })
 
 
-    it('throws an error if no element is found', function () {
+    it('throws an error if no element is found', function (ctx) {
       expect(() =>
       expect(() =>
-        this.locator.findElementByMongoPath(
+        ctx.locator.findElementByMongoPath(
           project,
           project,
           'rootolder.0.folders.0.folders.0.fileRefs.0'
           'rootolder.0.folders.0.folders.0.fileRefs.0'
         )
         )

+ 315 - 304
services/web/test/unit/src/Project/ProjectRootDocManager.test.mjs

@@ -1,60 +1,81 @@
-const { expect } = require('chai')
-const { ObjectId } = require('mongodb-legacy')
-const sinon = require('sinon')
+import { vi, expect } from 'vitest'
+import mongodb from 'mongodb-legacy'
+import sinon from 'sinon'
 const modulePath =
 const modulePath =
-  '../../../../app/src/Features/Project/ProjectRootDocManager.js'
-const SandboxedModule = require('sandboxed-module')
+  '../../../../app/src/Features/Project/ProjectRootDocManager.mjs'
+
+const { ObjectId } = mongodb
 
 
 describe('ProjectRootDocManager', function () {
 describe('ProjectRootDocManager', function () {
-  beforeEach(function () {
-    this.project_id = 'project-123'
-    this.docPaths = {}
-    this.docId1 = new ObjectId()
-    this.docId2 = new ObjectId()
-    this.docId3 = new ObjectId()
-    this.docId4 = new ObjectId()
-    this.docPaths[this.docId1] = '/chapter1.tex'
-    this.docPaths[this.docId2] = '/main.tex'
-    this.docPaths[this.docId3] = '/nested/chapter1a.tex'
-    this.docPaths[this.docId4] = '/nested/chapter1b.tex'
-    this.sl_req_id = 'sl-req-id-123'
-    this.callback = sinon.stub()
-    this.globbyFiles = ['a.tex', 'b.tex', 'main.tex']
-    this.globby = sinon.stub().resolves(this.globbyFiles)
-
-    this.fs = {
+  beforeEach(async function (ctx) {
+    ctx.project_id = 'project-123'
+    ctx.docPaths = {}
+    ctx.docId1 = new ObjectId()
+    ctx.docId2 = new ObjectId()
+    ctx.docId3 = new ObjectId()
+    ctx.docId4 = new ObjectId()
+    ctx.docPaths[ctx.docId1] = '/chapter1.tex'
+    ctx.docPaths[ctx.docId2] = '/main.tex'
+    ctx.docPaths[ctx.docId3] = '/nested/chapter1a.tex'
+    ctx.docPaths[ctx.docId4] = '/nested/chapter1b.tex'
+    ctx.sl_req_id = 'sl-req-id-123'
+    ctx.callback = sinon.stub()
+    ctx.globbyFiles = ['a.tex', 'b.tex', 'main.tex']
+    ctx.globby = sinon.stub().resolves(ctx.globbyFiles)
+
+    ctx.fs = {
       readFile: sinon.stub().callsArgWith(2, new Error('file not found')),
       readFile: sinon.stub().callsArgWith(2, new Error('file not found')),
       stat: sinon.stub().callsArgWith(1, null, { size: 100 }),
       stat: sinon.stub().callsArgWith(1, null, { size: 100 }),
     }
     }
-    this.ProjectRootDocManager = SandboxedModule.require(modulePath, {
-      requires: {
-        './ProjectEntityHandler': (this.ProjectEntityHandler = {}),
-        './ProjectEntityUpdateHandler': (this.ProjectEntityUpdateHandler = {}),
-        './ProjectGetter': (this.ProjectGetter = {}),
-        '../../infrastructure/GracefulShutdown': {
-          BackgroundTaskTracker: class {
-            add() {}
-            done() {}
-          },
-        },
-        globby: this.globby,
-        fs: this.fs,
+
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectEntityHandler',
+      () => ({
+        default: (ctx.ProjectEntityHandler = {}),
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectEntityUpdateHandler',
+      () => ({
+        default: (ctx.ProjectEntityUpdateHandler = {}),
+      })
+    )
+
+    vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
+      default: (ctx.ProjectGetter = {}),
+    }))
+
+    vi.doMock('../../../../app/src/infrastructure/GracefulShutdown', () => ({
+      BackgroundTaskTracker: class {
+        add() {}
+        done() {}
       },
       },
-    })
+    }))
+
+    vi.doMock('globby', () => ({
+      default: ctx.globby,
+    }))
+
+    vi.doMock('fs', () => ({
+      default: ctx.fs,
+    }))
+
+    ctx.ProjectRootDocManager = (await import(modulePath)).default
   })
   })
 
 
   describe('setRootDocAutomatically', function () {
   describe('setRootDocAutomatically', function () {
-    beforeEach(function () {
-      this.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
-      this.ProjectEntityUpdateHandler.isPathValidForRootDoc = sinon
+    beforeEach(function (ctx) {
+      ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
+      ctx.ProjectEntityUpdateHandler.isPathValidForRootDoc = sinon
         .stub()
         .stub()
         .returns(true)
         .returns(true)
     })
     })
     describe('when there is a suitable root doc', function () {
     describe('when there is a suitable root doc', function () {
-      beforeEach(async function () {
-        this.docs = {
+      beforeEach(async function (ctx) {
+        ctx.docs = {
           '/chapter1.tex': {
           '/chapter1.tex': {
-            _id: this.docId1,
+            _id: ctx.docId1,
             lines: [
             lines: [
               'something else',
               'something else',
               '\\begin{document}',
               '\\begin{document}',
@@ -63,7 +84,7 @@ describe('ProjectRootDocManager', function () {
             ],
             ],
           },
           },
           '/main.tex': {
           '/main.tex': {
-            _id: this.docId2,
+            _id: ctx.docId2,
             lines: [
             lines: [
               'different line',
               'different line',
               '\\documentclass{article}',
               '\\documentclass{article}',
@@ -71,114 +92,114 @@ describe('ProjectRootDocManager', function () {
             ],
             ],
           },
           },
           '/nested/chapter1a.tex': {
           '/nested/chapter1a.tex': {
-            _id: this.docId3,
+            _id: ctx.docId3,
             lines: ['Hello world'],
             lines: ['Hello world'],
           },
           },
           '/nested/chapter1b.tex': {
           '/nested/chapter1b.tex': {
-            _id: this.docId4,
+            _id: ctx.docId4,
             lines: ['Hello world'],
             lines: ['Hello world'],
           },
           },
         }
         }
-        this.ProjectEntityHandler.getAllDocs = sinon
+        ctx.ProjectEntityHandler.getAllDocs = sinon
           .stub()
           .stub()
-          .callsArgWith(1, null, this.docs)
-        await this.ProjectRootDocManager.promises.setRootDocAutomatically(
-          this.project_id
+          .callsArgWith(1, null, ctx.docs)
+        await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
+          ctx.project_id
         )
         )
       })
       })
 
 
-      it('should check the docs of the project', function () {
-        this.ProjectEntityHandler.getAllDocs
-          .calledWith(this.project_id)
+      it('should check the docs of the project', function (ctx) {
+        ctx.ProjectEntityHandler.getAllDocs
+          .calledWith(ctx.project_id)
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should set the root doc to the doc containing a documentclass', function () {
-        this.ProjectEntityUpdateHandler.setRootDoc
-          .calledWith(this.project_id, this.docId2)
+      it('should set the root doc to the doc containing a documentclass', function (ctx) {
+        ctx.ProjectEntityUpdateHandler.setRootDoc
+          .calledWith(ctx.project_id, ctx.docId2)
           .should.equal(true)
           .should.equal(true)
       })
       })
     })
     })
 
 
     describe('when the root doc is an Rtex file', function () {
     describe('when the root doc is an Rtex file', function () {
-      beforeEach(async function () {
-        this.docs = {
+      beforeEach(async function (ctx) {
+        ctx.docs = {
           '/chapter1.tex': {
           '/chapter1.tex': {
-            _id: this.docId1,
+            _id: ctx.docId1,
             lines: ['\\begin{document}', 'Hello world', '\\end{document}'],
             lines: ['\\begin{document}', 'Hello world', '\\end{document}'],
           },
           },
           '/main.Rtex': {
           '/main.Rtex': {
-            _id: this.docId2,
+            _id: ctx.docId2,
             lines: ['\\documentclass{article}', '\\input{chapter1}'],
             lines: ['\\documentclass{article}', '\\input{chapter1}'],
           },
           },
         }
         }
-        this.ProjectEntityHandler.getAllDocs = sinon
+        ctx.ProjectEntityHandler.getAllDocs = sinon
           .stub()
           .stub()
-          .callsArgWith(1, null, this.docs)
-        await this.ProjectRootDocManager.promises.setRootDocAutomatically(
-          this.project_id
+          .callsArgWith(1, null, ctx.docs)
+        await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
+          ctx.project_id
         )
         )
       })
       })
 
 
-      it('should set the root doc to the doc containing a documentclass', function () {
-        this.ProjectEntityUpdateHandler.setRootDoc
-          .calledWith(this.project_id, this.docId2)
+      it('should set the root doc to the doc containing a documentclass', function (ctx) {
+        ctx.ProjectEntityUpdateHandler.setRootDoc
+          .calledWith(ctx.project_id, ctx.docId2)
           .should.equal(true)
           .should.equal(true)
       })
       })
     })
     })
 
 
     describe('when there is no suitable root doc', function () {
     describe('when there is no suitable root doc', function () {
-      beforeEach(async function () {
-        this.docs = {
+      beforeEach(async function (ctx) {
+        ctx.docs = {
           '/chapter1.tex': {
           '/chapter1.tex': {
-            _id: this.docId1,
+            _id: ctx.docId1,
             lines: ['\\begin{document}', 'Hello world', '\\end{document}'],
             lines: ['\\begin{document}', 'Hello world', '\\end{document}'],
           },
           },
           '/style.bst': {
           '/style.bst': {
-            _id: this.docId2,
+            _id: ctx.docId2,
             lines: ['%Example: \\documentclass{article}'],
             lines: ['%Example: \\documentclass{article}'],
           },
           },
         }
         }
-        this.ProjectEntityHandler.getAllDocs = sinon
+        ctx.ProjectEntityHandler.getAllDocs = sinon
           .stub()
           .stub()
-          .callsArgWith(1, null, this.docs)
-        await this.ProjectRootDocManager.promises.setRootDocAutomatically(
-          this.project_id
+          .callsArgWith(1, null, ctx.docs)
+        await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
+          ctx.project_id
         )
         )
       })
       })
 
 
-      it('should not set the root doc to the doc containing a documentclass', function () {
-        this.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false)
+      it('should not set the root doc to the doc containing a documentclass', function (ctx) {
+        ctx.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false)
       })
       })
     })
     })
   })
   })
 
 
   describe('findRootDocFileFromDirectory', function () {
   describe('findRootDocFileFromDirectory', function () {
-    beforeEach(function () {
-      this.fs.readFile
+    beforeEach(function (ctx) {
+      ctx.fs.readFile
         .withArgs('/foo/a.tex')
         .withArgs('/foo/a.tex')
         .callsArgWith(2, null, 'Hello World!')
         .callsArgWith(2, null, 'Hello World!')
-      this.fs.readFile
+      ctx.fs.readFile
         .withArgs('/foo/b.tex')
         .withArgs('/foo/b.tex')
         .callsArgWith(2, null, "I'm a little teapot, get me out of here.")
         .callsArgWith(2, null, "I'm a little teapot, get me out of here.")
-      this.fs.readFile
+      ctx.fs.readFile
         .withArgs('/foo/main.tex')
         .withArgs('/foo/main.tex')
         .callsArgWith(2, null, "Help, I'm trapped in a unit testing factory")
         .callsArgWith(2, null, "Help, I'm trapped in a unit testing factory")
-      this.fs.readFile
+      ctx.fs.readFile
         .withArgs('/foo/c.tex')
         .withArgs('/foo/c.tex')
         .callsArgWith(2, null, 'Tomato, tomahto.')
         .callsArgWith(2, null, 'Tomato, tomahto.')
-      this.fs.readFile
+      ctx.fs.readFile
         .withArgs('/foo/a/a.tex')
         .withArgs('/foo/a/a.tex')
         .callsArgWith(2, null, 'Potato? Potahto. Potootee!')
         .callsArgWith(2, null, 'Potato? Potahto. Potootee!')
-      this.documentclassContent = '% test\n\\documentclass\n% test'
+      ctx.documentclassContent = '% test\n\\documentclass\n% test'
     })
     })
 
 
     describe('when there is a file in a subfolder', function () {
     describe('when there is a file in a subfolder', function () {
-      beforeEach(function () {
+      beforeEach(function (ctx) {
         // have to splice globbyFiles weirdly because of the way the stubbed globby method handles references
         // have to splice globbyFiles weirdly because of the way the stubbed globby method handles references
-        this.globbyFiles.splice(
+        ctx.globbyFiles.splice(
           0,
           0,
-          this.globbyFiles.length,
+          ctx.globbyFiles.length,
           'c.tex',
           'c.tex',
           'a.tex',
           'a.tex',
           'a/a.tex',
           'a/a.tex',
@@ -186,90 +207,90 @@ describe('ProjectRootDocManager', function () {
         )
         )
       })
       })
 
 
-      it('processes the root folder files first, and then the subfolder, in alphabetical order', async function () {
+      it('processes the root folder files first, and then the subfolder, in alphabetical order', async function (ctx) {
         const { path } =
         const { path } =
-          await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
+          await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
             '/foo'
             '/foo'
           )
           )
         expect(path).to.equal('a.tex')
         expect(path).to.equal('a.tex')
         sinon.assert.callOrder(
         sinon.assert.callOrder(
-          this.fs.readFile.withArgs('/foo/a.tex'),
-          this.fs.readFile.withArgs('/foo/b.tex'),
-          this.fs.readFile.withArgs('/foo/c.tex'),
-          this.fs.readFile.withArgs('/foo/a/a.tex')
+          ctx.fs.readFile.withArgs('/foo/a.tex'),
+          ctx.fs.readFile.withArgs('/foo/b.tex'),
+          ctx.fs.readFile.withArgs('/foo/c.tex'),
+          ctx.fs.readFile.withArgs('/foo/a/a.tex')
         )
         )
       })
       })
 
 
-      it('processes smaller files first', async function () {
-        this.fs.stat.withArgs('/foo/c.tex').callsArgWith(1, null, { size: 1 })
+      it('processes smaller files first', async function (ctx) {
+        ctx.fs.stat.withArgs('/foo/c.tex').callsArgWith(1, null, { size: 1 })
         const { path } =
         const { path } =
-          await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
+          await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
             '/foo'
             '/foo'
           )
           )
         expect(path).to.equal('c.tex')
         expect(path).to.equal('c.tex')
         sinon.assert.callOrder(
         sinon.assert.callOrder(
-          this.fs.readFile.withArgs('/foo/c.tex'),
-          this.fs.readFile.withArgs('/foo/a.tex'),
-          this.fs.readFile.withArgs('/foo/b.tex'),
-          this.fs.readFile.withArgs('/foo/a/a.tex')
+          ctx.fs.readFile.withArgs('/foo/c.tex'),
+          ctx.fs.readFile.withArgs('/foo/a.tex'),
+          ctx.fs.readFile.withArgs('/foo/b.tex'),
+          ctx.fs.readFile.withArgs('/foo/a/a.tex')
         )
         )
       })
       })
     })
     })
 
 
     describe('when main.tex contains a documentclass', function () {
     describe('when main.tex contains a documentclass', function () {
-      beforeEach(function () {
-        this.fs.readFile
+      beforeEach(function (ctx) {
+        ctx.fs.readFile
           .withArgs('/foo/main.tex')
           .withArgs('/foo/main.tex')
-          .callsArgWith(2, null, this.documentclassContent)
+          .callsArgWith(2, null, ctx.documentclassContent)
       })
       })
 
 
-      it('returns main.tex', async function () {
+      it('returns main.tex', async function (ctx) {
         const { path, content } =
         const { path, content } =
-          await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
+          await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
             '/foo'
             '/foo'
           )
           )
         expect(path).to.equal('main.tex')
         expect(path).to.equal('main.tex')
-        expect(content).to.equal(this.documentclassContent)
+        expect(content).to.equal(ctx.documentclassContent)
       })
       })
 
 
-      it('processes main.text first and stops processing when it finds the content', async function () {
-        await this.ProjectRootDocManager.findRootDocFileFromDirectory('/foo')
-        expect(this.fs.readFile).to.be.calledWith('/foo/main.tex')
-        expect(this.fs.readFile).not.to.be.calledWith('/foo/a.tex')
+      it('processes main.text first and stops processing when it finds the content', async function (ctx) {
+        await ctx.ProjectRootDocManager.findRootDocFileFromDirectory('/foo')
+        expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex')
+        expect(ctx.fs.readFile).not.to.be.calledWith('/foo/a.tex')
       })
       })
     })
     })
 
 
     describe('when main.tex does not contain a line starting with \\documentclass', function () {
     describe('when main.tex does not contain a line starting with \\documentclass', function () {
-      beforeEach(function () {
-        this.fs.readFile.withArgs('/foo/a.tex').callsArgWith(2, null, 'foo')
-        this.fs.readFile.withArgs('/foo/main.tex').callsArgWith(2, null, 'foo')
-        this.fs.readFile.withArgs('/foo/z.tex').callsArgWith(2, null, 'foo')
-        this.fs.readFile
+      beforeEach(function (ctx) {
+        ctx.fs.readFile.withArgs('/foo/a.tex').callsArgWith(2, null, 'foo')
+        ctx.fs.readFile.withArgs('/foo/main.tex').callsArgWith(2, null, 'foo')
+        ctx.fs.readFile.withArgs('/foo/z.tex').callsArgWith(2, null, 'foo')
+        ctx.fs.readFile
           .withArgs('/foo/nested/chapter1a.tex')
           .withArgs('/foo/nested/chapter1a.tex')
           .callsArgWith(2, null, 'foo')
           .callsArgWith(2, null, 'foo')
       })
       })
 
 
-      it('returns the first .tex file from the root folder', async function () {
-        this.globbyFiles.splice(
+      it('returns the first .tex file from the root folder', async function (ctx) {
+        ctx.globbyFiles.splice(
           0,
           0,
-          this.globbyFiles.length,
+          ctx.globbyFiles.length,
           'a.tex',
           'a.tex',
           'z.tex',
           'z.tex',
           'nested/chapter1a.tex'
           'nested/chapter1a.tex'
         )
         )
 
 
         const { path, content } =
         const { path, content } =
-          await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
+          await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
             '/foo'
             '/foo'
           )
           )
         expect(path).to.equal('a.tex')
         expect(path).to.equal('a.tex')
         expect(content).to.equal('foo')
         expect(content).to.equal('foo')
       })
       })
 
 
-      it('returns main.tex file from the root folder', async function () {
-        this.globbyFiles.splice(
+      it('returns main.tex file from the root folder', async function (ctx) {
+        ctx.globbyFiles.splice(
           0,
           0,
-          this.globbyFiles.length,
+          ctx.globbyFiles.length,
           'a.tex',
           'a.tex',
           'z.tex',
           'z.tex',
           'main.tex',
           'main.tex',
@@ -277,7 +298,7 @@ describe('ProjectRootDocManager', function () {
         )
         )
 
 
         const { path, content } =
         const { path, content } =
-          await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
+          await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
             '/foo'
             '/foo'
           )
           )
         expect(path).to.equal('main.tex')
         expect(path).to.equal('main.tex')
@@ -286,60 +307,60 @@ describe('ProjectRootDocManager', function () {
     })
     })
 
 
     describe('when a.tex contains a documentclass', function () {
     describe('when a.tex contains a documentclass', function () {
-      beforeEach(function () {
-        this.fs.readFile
+      beforeEach(function (ctx) {
+        ctx.fs.readFile
           .withArgs('/foo/a.tex')
           .withArgs('/foo/a.tex')
-          .callsArgWith(2, null, this.documentclassContent)
+          .callsArgWith(2, null, ctx.documentclassContent)
       })
       })
 
 
-      it('returns a.tex', async function () {
+      it('returns a.tex', async function (ctx) {
         const { path, content } =
         const { path, content } =
-          await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
+          await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
             '/foo'
             '/foo'
           )
           )
         expect(path).to.equal('a.tex')
         expect(path).to.equal('a.tex')
-        expect(content).to.equal(this.documentclassContent)
+        expect(content).to.equal(ctx.documentclassContent)
       })
       })
 
 
-      it('processes main.text first and stops processing when it finds the content', async function () {
-        await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
+      it('processes main.text first and stops processing when it finds the content', async function (ctx) {
+        await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
           '/foo'
           '/foo'
         )
         )
-        expect(this.fs.readFile).to.be.calledWith('/foo/main.tex')
-        expect(this.fs.readFile).to.be.calledWith('/foo/a.tex')
-        expect(this.fs.readFile).not.to.be.calledWith('/foo/b.tex')
+        expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex')
+        expect(ctx.fs.readFile).to.be.calledWith('/foo/a.tex')
+        expect(ctx.fs.readFile).not.to.be.calledWith('/foo/b.tex')
       })
       })
     })
     })
 
 
     describe('when there is no documentclass', function () {
     describe('when there is no documentclass', function () {
-      it('returns with no error', async function () {
-        await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
+      it('returns with no error', async function (ctx) {
+        await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
           '/foo'
           '/foo'
         )
         )
       })
       })
 
 
-      it('processes all the files', async function () {
-        await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
+      it('processes all the files', async function (ctx) {
+        await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
           '/foo'
           '/foo'
         )
         )
-        expect(this.fs.readFile).to.be.calledWith('/foo/main.tex')
-        expect(this.fs.readFile).to.be.calledWith('/foo/a.tex')
-        expect(this.fs.readFile).to.be.calledWith('/foo/b.tex')
+        expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex')
+        expect(ctx.fs.readFile).to.be.calledWith('/foo/a.tex')
+        expect(ctx.fs.readFile).to.be.calledWith('/foo/b.tex')
       })
       })
     })
     })
 
 
     describe('when there is an error reading a file', function () {
     describe('when there is an error reading a file', function () {
-      beforeEach(function () {
-        this.fs.readFile
+      beforeEach(function (ctx) {
+        ctx.fs.readFile
           .withArgs('/foo/a.tex')
           .withArgs('/foo/a.tex')
           .callsArgWith(2, new Error('something went wrong'))
           .callsArgWith(2, new Error('something went wrong'))
       })
       })
 
 
-      it('returns an error', async function () {
+      it('returns an error', async function (ctx) {
         let error
         let error
 
 
         try {
         try {
-          await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
+          await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
             '/foo'
             '/foo'
           )
           )
         } catch (err) {
         } catch (err) {
@@ -353,206 +374,196 @@ describe('ProjectRootDocManager', function () {
 
 
   describe('setRootDocFromName', function () {
   describe('setRootDocFromName', function () {
     describe('when there is a suitable root doc', function () {
     describe('when there is a suitable root doc', function () {
-      beforeEach(async function () {
-        this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
-          .stub()
-          .callsArgWith(1, null, this.docPaths)
-        this.ProjectEntityUpdateHandler.setRootDoc = sinon
+      beforeEach(async function (ctx) {
+        ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
           .stub()
           .stub()
-          .callsArgWith(2)
-        await this.ProjectRootDocManager.promises.setRootDocFromName(
-          this.project_id,
+          .callsArgWith(1, null, ctx.docPaths)
+        ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
+        await ctx.ProjectRootDocManager.promises.setRootDocFromName(
+          ctx.project_id,
           '/main.tex'
           '/main.tex'
         )
         )
       })
       })
 
 
-      it('should check the docs of the project', function () {
-        this.ProjectEntityHandler.getAllDocPathsFromProjectById
-          .calledWith(this.project_id)
+      it('should check the docs of the project', function (ctx) {
+        ctx.ProjectEntityHandler.getAllDocPathsFromProjectById
+          .calledWith(ctx.project_id)
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should set the root doc to main.tex', function () {
-        this.ProjectEntityUpdateHandler.setRootDoc
-          .calledWith(this.project_id, this.docId2.toString())
+      it('should set the root doc to main.tex', function (ctx) {
+        ctx.ProjectEntityUpdateHandler.setRootDoc
+          .calledWith(ctx.project_id, ctx.docId2.toString())
           .should.equal(true)
           .should.equal(true)
       })
       })
     })
     })
 
 
     describe('when there is a suitable root doc but the leading slash is missing', function () {
     describe('when there is a suitable root doc but the leading slash is missing', function () {
-      beforeEach(async function () {
-        this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
+      beforeEach(async function (ctx) {
+        ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
           .stub()
           .stub()
-          .callsArgWith(1, null, this.docPaths)
-        this.ProjectEntityUpdateHandler.setRootDoc = sinon
-          .stub()
-          .callsArgWith(2)
-        await this.ProjectRootDocManager.promises.setRootDocFromName(
-          this.project_id,
+          .callsArgWith(1, null, ctx.docPaths)
+        ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
+        await ctx.ProjectRootDocManager.promises.setRootDocFromName(
+          ctx.project_id,
           'main.tex'
           'main.tex'
         )
         )
       })
       })
 
 
-      it('should check the docs of the project', function () {
-        this.ProjectEntityHandler.getAllDocPathsFromProjectById
-          .calledWith(this.project_id)
+      it('should check the docs of the project', function (ctx) {
+        ctx.ProjectEntityHandler.getAllDocPathsFromProjectById
+          .calledWith(ctx.project_id)
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should set the root doc to main.tex', function () {
-        this.ProjectEntityUpdateHandler.setRootDoc
-          .calledWith(this.project_id, this.docId2.toString())
+      it('should set the root doc to main.tex', function (ctx) {
+        ctx.ProjectEntityUpdateHandler.setRootDoc
+          .calledWith(ctx.project_id, ctx.docId2.toString())
           .should.equal(true)
           .should.equal(true)
       })
       })
     })
     })
 
 
     describe('when there is a suitable root doc with a basename match', function () {
     describe('when there is a suitable root doc with a basename match', function () {
-      beforeEach(async function () {
-        this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
-          .stub()
-          .callsArgWith(1, null, this.docPaths)
-        this.ProjectEntityUpdateHandler.setRootDoc = sinon
+      beforeEach(async function (ctx) {
+        ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
           .stub()
           .stub()
-          .callsArgWith(2)
-        await this.ProjectRootDocManager.promises.setRootDocFromName(
-          this.project_id,
+          .callsArgWith(1, null, ctx.docPaths)
+        ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
+        await ctx.ProjectRootDocManager.promises.setRootDocFromName(
+          ctx.project_id,
           'chapter1a.tex'
           'chapter1a.tex'
         )
         )
       })
       })
 
 
-      it('should check the docs of the project', function () {
-        this.ProjectEntityHandler.getAllDocPathsFromProjectById
-          .calledWith(this.project_id)
+      it('should check the docs of the project', function (ctx) {
+        ctx.ProjectEntityHandler.getAllDocPathsFromProjectById
+          .calledWith(ctx.project_id)
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should set the root doc using the basename', function () {
-        this.ProjectEntityUpdateHandler.setRootDoc
-          .calledWith(this.project_id, this.docId3.toString())
+      it('should set the root doc using the basename', function (ctx) {
+        ctx.ProjectEntityUpdateHandler.setRootDoc
+          .calledWith(ctx.project_id, ctx.docId3.toString())
           .should.equal(true)
           .should.equal(true)
       })
       })
     })
     })
 
 
     describe('when there is a suitable root doc but the filename is in quotes', function () {
     describe('when there is a suitable root doc but the filename is in quotes', function () {
-      beforeEach(async function () {
-        this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
+      beforeEach(async function (ctx) {
+        ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
           .stub()
           .stub()
-          .callsArgWith(1, null, this.docPaths)
-        this.ProjectEntityUpdateHandler.setRootDoc = sinon
-          .stub()
-          .callsArgWith(2)
-        await this.ProjectRootDocManager.promises.setRootDocFromName(
-          this.project_id,
+          .callsArgWith(1, null, ctx.docPaths)
+        ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
+        await ctx.ProjectRootDocManager.promises.setRootDocFromName(
+          ctx.project_id,
           "'main.tex'"
           "'main.tex'"
         )
         )
       })
       })
 
 
-      it('should check the docs of the project', function () {
-        this.ProjectEntityHandler.getAllDocPathsFromProjectById
-          .calledWith(this.project_id)
+      it('should check the docs of the project', function (ctx) {
+        ctx.ProjectEntityHandler.getAllDocPathsFromProjectById
+          .calledWith(ctx.project_id)
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should set the root doc to main.tex', function () {
-        this.ProjectEntityUpdateHandler.setRootDoc
-          .calledWith(this.project_id, this.docId2.toString())
+      it('should set the root doc to main.tex', function (ctx) {
+        ctx.ProjectEntityUpdateHandler.setRootDoc
+          .calledWith(ctx.project_id, ctx.docId2.toString())
           .should.equal(true)
           .should.equal(true)
       })
       })
     })
     })
 
 
     describe('when there is no suitable root doc', function () {
     describe('when there is no suitable root doc', function () {
-      beforeEach(async function () {
-        this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
-          .stub()
-          .callsArgWith(1, null, this.docPaths)
-        this.ProjectEntityUpdateHandler.setRootDoc = sinon
+      beforeEach(async function (ctx) {
+        ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
           .stub()
           .stub()
-          .callsArgWith(2)
-        await this.ProjectRootDocManager.promises.setRootDocFromName(
-          this.project_id,
+          .callsArgWith(1, null, ctx.docPaths)
+        ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
+        await ctx.ProjectRootDocManager.promises.setRootDocFromName(
+          ctx.project_id,
           'other.tex'
           'other.tex'
         )
         )
       })
       })
 
 
-      it('should not set the root doc', function () {
-        this.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false)
+      it('should not set the root doc', function (ctx) {
+        ctx.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false)
       })
       })
     })
     })
   })
   })
 
 
   describe('ensureRootDocumentIsSet', function () {
   describe('ensureRootDocumentIsSet', function () {
-    beforeEach(function () {
-      this.project = {}
-      this.ProjectGetter.getProject = sinon
+    beforeEach(function (ctx) {
+      ctx.project = {}
+      ctx.ProjectGetter.getProject = sinon
         .stub()
         .stub()
-        .callsArgWith(2, null, this.project)
-      this.ProjectRootDocManager.setRootDocAutomatically = sinon
+        .callsArgWith(2, null, ctx.project)
+      ctx.ProjectRootDocManager.setRootDocAutomatically = sinon
         .stub()
         .stub()
         .callsArgWith(1, null)
         .callsArgWith(1, null)
     })
     })
 
 
     describe('when the root doc is set', function () {
     describe('when the root doc is set', function () {
-      beforeEach(function () {
-        this.project.rootDoc_id = this.docId2
-        this.ProjectRootDocManager.ensureRootDocumentIsSet(
-          this.project_id,
-          this.callback
+      beforeEach(function (ctx) {
+        ctx.project.rootDoc_id = ctx.docId2
+        ctx.ProjectRootDocManager.ensureRootDocumentIsSet(
+          ctx.project_id,
+          ctx.callback
         )
         )
       })
       })
 
 
-      it('should find the project fetching only the rootDoc_id field', function () {
-        this.ProjectGetter.getProject
-          .calledWith(this.project_id, { rootDoc_id: 1 })
+      it('should find the project fetching only the rootDoc_id field', function (ctx) {
+        ctx.ProjectGetter.getProject
+          .calledWith(ctx.project_id, { rootDoc_id: 1 })
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should not try to update the project rootDoc_id', function () {
-        this.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
+      it('should not try to update the project rootDoc_id', function (ctx) {
+        ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
           false
           false
         )
         )
       })
       })
 
 
-      it('should call the callback', function () {
-        this.callback.called.should.equal(true)
+      it('should call the callback', function (ctx) {
+        ctx.callback.called.should.equal(true)
       })
       })
     })
     })
 
 
     describe('when the root doc is not set', function () {
     describe('when the root doc is not set', function () {
-      beforeEach(function () {
-        this.ProjectRootDocManager.ensureRootDocumentIsSet(
-          this.project_id,
-          this.callback
+      beforeEach(function (ctx) {
+        ctx.ProjectRootDocManager.ensureRootDocumentIsSet(
+          ctx.project_id,
+          ctx.callback
         )
         )
       })
       })
 
 
-      it('should find the project with only the rootDoc_id field', function () {
-        this.ProjectGetter.getProject
-          .calledWith(this.project_id, { rootDoc_id: 1 })
+      it('should find the project with only the rootDoc_id field', function (ctx) {
+        ctx.ProjectGetter.getProject
+          .calledWith(ctx.project_id, { rootDoc_id: 1 })
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should update the project rootDoc_id', function () {
-        this.ProjectRootDocManager.setRootDocAutomatically
-          .calledWith(this.project_id)
+      it('should update the project rootDoc_id', function (ctx) {
+        ctx.ProjectRootDocManager.setRootDocAutomatically
+          .calledWith(ctx.project_id)
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should call the callback', function () {
-        this.callback.called.should.equal(true)
+      it('should call the callback', function (ctx) {
+        ctx.callback.called.should.equal(true)
       })
       })
     })
     })
 
 
     describe('when the project does not exist', function () {
     describe('when the project does not exist', function () {
-      beforeEach(function () {
-        this.ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null)
-        this.ProjectRootDocManager.ensureRootDocumentIsSet(
-          this.project_id,
-          this.callback
+      beforeEach(function (ctx) {
+        ctx.ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null)
+        ctx.ProjectRootDocManager.ensureRootDocumentIsSet(
+          ctx.project_id,
+          ctx.callback
         )
         )
       })
       })
 
 
-      it('should call the callback with an error', function () {
-        this.callback
+      it('should call the callback with an error', function (ctx) {
+        ctx.callback
           .calledWith(
           .calledWith(
             sinon.match
             sinon.match
               .instanceOf(Error)
               .instanceOf(Error)
@@ -564,125 +575,125 @@ describe('ProjectRootDocManager', function () {
   })
   })
 
 
   describe('ensureRootDocumentIsValid', function () {
   describe('ensureRootDocumentIsValid', function () {
-    beforeEach(function () {
-      this.project = {}
-      this.ProjectGetter.getProject = sinon
+    beforeEach(function (ctx) {
+      ctx.project = {}
+      ctx.ProjectGetter.getProject = sinon
         .stub()
         .stub()
-        .callsArgWith(2, null, this.project)
-      this.ProjectGetter.getProjectWithoutDocLines = sinon
+        .callsArgWith(2, null, ctx.project)
+      ctx.ProjectGetter.getProjectWithoutDocLines = sinon
         .stub()
         .stub()
-        .callsArgWith(1, null, this.project)
-      this.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields()
-      this.ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields()
-      this.ProjectRootDocManager.setRootDocAutomatically = sinon
+        .callsArgWith(1, null, ctx.project)
+      ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields()
+      ctx.ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields()
+      ctx.ProjectRootDocManager.setRootDocAutomatically = sinon
         .stub()
         .stub()
         .callsArgWith(1, null)
         .callsArgWith(1, null)
     })
     })
 
 
     describe('when the root doc is set', function () {
     describe('when the root doc is set', function () {
       describe('when the root doc is valid', function () {
       describe('when the root doc is valid', function () {
-        beforeEach(function () {
-          this.project.rootDoc_id = this.docId2
-          this.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon
+        beforeEach(function (ctx) {
+          ctx.project.rootDoc_id = ctx.docId2
+          ctx.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon
             .stub()
             .stub()
-            .callsArgWith(2, null, this.docPaths[this.docId2])
-          this.ProjectRootDocManager.ensureRootDocumentIsValid(
-            this.project_id,
-            this.callback
+            .callsArgWith(2, null, ctx.docPaths[ctx.docId2])
+          ctx.ProjectRootDocManager.ensureRootDocumentIsValid(
+            ctx.project_id,
+            ctx.callback
           )
           )
         })
         })
 
 
-        it('should find the project without doc lines', function () {
-          this.ProjectGetter.getProjectWithoutDocLines
-            .calledWith(this.project_id)
+        it('should find the project without doc lines', function (ctx) {
+          ctx.ProjectGetter.getProjectWithoutDocLines
+            .calledWith(ctx.project_id)
             .should.equal(true)
             .should.equal(true)
         })
         })
 
 
-        it('should not try to update the project rootDoc_id', function () {
-          this.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
+        it('should not try to update the project rootDoc_id', function (ctx) {
+          ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
             false
             false
           )
           )
         })
         })
 
 
-        it('should call the callback', function () {
-          this.callback.called.should.equal(true)
+        it('should call the callback', function (ctx) {
+          ctx.callback.called.should.equal(true)
         })
         })
       })
       })
 
 
       describe('when the root doc is not valid', function () {
       describe('when the root doc is not valid', function () {
-        beforeEach(function () {
-          this.project.rootDoc_id = new ObjectId()
-          this.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon
+        beforeEach(function (ctx) {
+          ctx.project.rootDoc_id = new ObjectId()
+          ctx.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon
             .stub()
             .stub()
             .callsArgWith(2, null, null)
             .callsArgWith(2, null, null)
-          this.ProjectRootDocManager.ensureRootDocumentIsValid(
-            this.project_id,
-            this.callback
+          ctx.ProjectRootDocManager.ensureRootDocumentIsValid(
+            ctx.project_id,
+            ctx.callback
           )
           )
         })
         })
 
 
-        it('should find the project without doc lines', function () {
-          this.ProjectGetter.getProjectWithoutDocLines
-            .calledWith(this.project_id)
+        it('should find the project without doc lines', function (ctx) {
+          ctx.ProjectGetter.getProjectWithoutDocLines
+            .calledWith(ctx.project_id)
             .should.equal(true)
             .should.equal(true)
         })
         })
 
 
-        it('should unset the root doc', function () {
-          this.ProjectEntityUpdateHandler.unsetRootDoc
-            .calledWith(this.project_id)
+        it('should unset the root doc', function (ctx) {
+          ctx.ProjectEntityUpdateHandler.unsetRootDoc
+            .calledWith(ctx.project_id)
             .should.equal(true)
             .should.equal(true)
         })
         })
 
 
-        it('should try to find a new rootDoc', function () {
-          this.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
+        it('should try to find a new rootDoc', function (ctx) {
+          ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
             true
             true
           )
           )
         })
         })
 
 
-        it('should call the callback', function () {
-          this.callback.called.should.equal(true)
+        it('should call the callback', function (ctx) {
+          ctx.callback.called.should.equal(true)
         })
         })
       })
       })
     })
     })
 
 
     describe('when the root doc is not set', function () {
     describe('when the root doc is not set', function () {
-      beforeEach(function () {
-        this.ProjectRootDocManager.ensureRootDocumentIsValid(
-          this.project_id,
-          this.callback
+      beforeEach(function (ctx) {
+        ctx.ProjectRootDocManager.ensureRootDocumentIsValid(
+          ctx.project_id,
+          ctx.callback
         )
         )
       })
       })
 
 
-      it('should find the project without doc lines', function () {
-        this.ProjectGetter.getProjectWithoutDocLines
-          .calledWith(this.project_id)
+      it('should find the project without doc lines', function (ctx) {
+        ctx.ProjectGetter.getProjectWithoutDocLines
+          .calledWith(ctx.project_id)
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should update the project rootDoc_id', function () {
-        this.ProjectRootDocManager.setRootDocAutomatically
-          .calledWith(this.project_id)
+      it('should update the project rootDoc_id', function (ctx) {
+        ctx.ProjectRootDocManager.setRootDocAutomatically
+          .calledWith(ctx.project_id)
           .should.equal(true)
           .should.equal(true)
       })
       })
 
 
-      it('should call the callback', function () {
-        this.callback.called.should.equal(true)
+      it('should call the callback', function (ctx) {
+        ctx.callback.called.should.equal(true)
       })
       })
     })
     })
 
 
     describe('when the project does not exist', function () {
     describe('when the project does not exist', function () {
-      beforeEach(function () {
-        this.ProjectGetter.getProjectWithoutDocLines = sinon
+      beforeEach(function (ctx) {
+        ctx.ProjectGetter.getProjectWithoutDocLines = sinon
           .stub()
           .stub()
           .callsArgWith(1, null, null)
           .callsArgWith(1, null, null)
-        this.ProjectRootDocManager.ensureRootDocumentIsValid(
-          this.project_id,
-          this.callback
+        ctx.ProjectRootDocManager.ensureRootDocumentIsValid(
+          ctx.project_id,
+          ctx.callback
         )
         )
       })
       })
 
 
-      it('should call the callback with an error', function () {
-        this.callback
+      it('should call the callback with an error', function (ctx) {
+        ctx.callback
           .calledWith(
           .calledWith(
             sinon.match
             sinon.match
               .instanceOf(Error)
               .instanceOf(Error)

+ 296 - 259
services/web/test/unit/src/Subscription/LimitationsManager.test.mjs

@@ -1,37 +1,34 @@
-const SandboxedModule = require('sandboxed-module')
-const sinon = require('sinon')
-const { expect } = require('chai')
-const modulePath = require('path').join(
-  __dirname,
+import { vi, expect } from 'vitest'
+import sinon from 'sinon'
+const modulePath =
   '../../../../app/src/Features/Subscription/LimitationsManager'
   '../../../../app/src/Features/Subscription/LimitationsManager'
-)
 
 
 describe('LimitationsManager', function () {
 describe('LimitationsManager', function () {
-  beforeEach(function () {
-    this.user = {
-      _id: (this.userId = 'user-id'),
+  beforeEach(async function (ctx) {
+    ctx.user = {
+      _id: (ctx.userId = 'user-id'),
       features: { collaborators: 1 },
       features: { collaborators: 1 },
     }
     }
-    this.project = {
-      _id: (this.projectId = 'project-id'),
-      owner_ref: this.userId,
+    ctx.project = {
+      _id: (ctx.projectId = 'project-id'),
+      owner_ref: ctx.userId,
     }
     }
-    this.ProjectGetter = {
+    ctx.ProjectGetter = {
       promises: {
       promises: {
         getProject: sinon.stub().callsFake(async (projectId, fields) => {
         getProject: sinon.stub().callsFake(async (projectId, fields) => {
-          if (projectId === this.projectId) {
-            return this.project
+          if (projectId === ctx.projectId) {
+            return ctx.project
           } else {
           } else {
             return null
             return null
           }
           }
         }),
         }),
       },
       },
     }
     }
-    this.UserGetter = {
+    ctx.UserGetter = {
       promises: {
       promises: {
         getUser: sinon.stub().callsFake(async (userId, filter) => {
         getUser: sinon.stub().callsFake(async (userId, filter) => {
-          if (userId === this.userId) {
-            return this.user
+          if (userId === ctx.userId) {
+            return ctx.user
           } else {
           } else {
             return null
             return null
           }
           }
@@ -39,7 +36,7 @@ describe('LimitationsManager', function () {
       },
       },
     }
     }
 
 
-    this.SubscriptionLocator = {
+    ctx.SubscriptionLocator = {
       promises: {
       promises: {
         getUsersSubscription: sinon.stub().resolves(),
         getUsersSubscription: sinon.stub().resolves(),
         getSubscription: sinon.stub().resolves(),
         getSubscription: sinon.stub().resolves(),
@@ -47,161 +44,194 @@ describe('LimitationsManager', function () {
       },
       },
     }
     }
 
 
-    this.CollaboratorsGetter = {
+    ctx.CollaboratorsGetter = {
       promises: {
       promises: {
         getInvitedEditCollaboratorCount: sinon.stub().resolves(0),
         getInvitedEditCollaboratorCount: sinon.stub().resolves(0),
         getMemberIdPrivilegeLevel: sinon.stub(),
         getMemberIdPrivilegeLevel: sinon.stub(),
       },
       },
     }
     }
 
 
-    this.CollaboratorsInviteGetter = {
+    ctx.CollaboratorsInviteGetter = {
       promises: {
       promises: {
         getEditInviteCount: sinon.stub().resolves(0),
         getEditInviteCount: sinon.stub().resolves(0),
       },
       },
     }
     }
 
 
-    this.LimitationsManager = SandboxedModule.require(modulePath, {
-      requires: {
-        '../Project/ProjectGetter': this.ProjectGetter,
-        '../User/UserGetter': this.UserGetter,
-        './SubscriptionLocator': this.SubscriptionLocator,
-        '@overleaf/settings': (this.Settings = {}),
-        '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
-        '../Collaborators/CollaboratorsInviteGetter':
-          this.CollaboratorsInviteGetter,
-        './V1SubscriptionManager': this.V1SubscriptionManager,
-      },
-    })
+    vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
+      default: ctx.ProjectGetter,
+    }))
+
+    vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
+      default: ctx.UserGetter,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Subscription/SubscriptionLocator',
+      () => ({
+        default: ctx.SubscriptionLocator,
+      })
+    )
+
+    vi.doMock('@overleaf/settings', () => ({
+      default: (ctx.Settings = {}),
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Collaborators/CollaboratorsGetter',
+      () => ({
+        default: ctx.CollaboratorsGetter,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter',
+      () => ({
+        default: ctx.CollaboratorsInviteGetter,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/Subscription/V1SubscriptionManager',
+      () => ({
+        default: ctx.V1SubscriptionManager,
+      })
+    )
+
+    ctx.LimitationsManager = (await import(modulePath)).default
   })
   })
 
 
   describe('allowedNumberOfCollaboratorsInProject', function () {
   describe('allowedNumberOfCollaboratorsInProject', function () {
     describe('when the project is owned by a user without a subscription', function () {
     describe('when the project is owned by a user without a subscription', function () {
-      beforeEach(function () {
-        this.Settings.defaultFeatures = { collaborators: 23 }
-        this.project.owner_ref = this.userId
-        delete this.user.features
+      beforeEach(function (ctx) {
+        ctx.Settings.defaultFeatures = { collaborators: 23 }
+        ctx.project.owner_ref = ctx.userId
+        delete ctx.user.features
       })
       })
 
 
-      it('should return the default number', async function () {
+      it('should return the default number', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject(
-            this.projectId
+          await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject(
+            ctx.projectId
           )
           )
-        expect(result).to.equal(this.Settings.defaultFeatures.collaborators)
+        expect(result).to.equal(ctx.Settings.defaultFeatures.collaborators)
       })
       })
     })
     })
 
 
     describe('when the project is owned by a user with a subscription', function () {
     describe('when the project is owned by a user with a subscription', function () {
-      beforeEach(function () {
-        this.project.owner_ref = this.userId
-        this.user.features = { collaborators: 21 }
+      beforeEach(function (ctx) {
+        ctx.project.owner_ref = ctx.userId
+        ctx.user.features = { collaborators: 21 }
       })
       })
 
 
-      it('should return the number of collaborators the user is allowed', async function () {
+      it('should return the number of collaborators the user is allowed', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject(
-            this.projectId
+          await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject(
+            ctx.projectId
           )
           )
-        expect(result).to.equal(this.user.features.collaborators)
+        expect(result).to.equal(ctx.user.features.collaborators)
       })
       })
     })
     })
   })
   })
 
 
   describe('allowedNumberOfCollaboratorsForUser', function () {
   describe('allowedNumberOfCollaboratorsForUser', function () {
     describe('when the user has no features', function () {
     describe('when the user has no features', function () {
-      beforeEach(function () {
-        this.Settings.defaultFeatures = { collaborators: 23 }
-        delete this.user.features
+      beforeEach(function (ctx) {
+        ctx.Settings.defaultFeatures = { collaborators: 23 }
+        delete ctx.user.features
       })
       })
 
 
-      it('should return the default number', async function () {
+      it('should return the default number', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
-            this.userId
+          await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
+            ctx.userId
           )
           )
-        expect(result).to.equal(this.Settings.defaultFeatures.collaborators)
+        expect(result).to.equal(ctx.Settings.defaultFeatures.collaborators)
       })
       })
     })
     })
 
 
     describe('when the user has features', function () {
     describe('when the user has features', function () {
-      beforeEach(async function () {
-        this.user.features = { collaborators: 21 }
-        this.result =
-          await this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
-            this.userId
+      beforeEach(async function (ctx) {
+        ctx.user.features = { collaborators: 21 }
+        ctx.result =
+          await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
+            ctx.userId
           )
           )
       })
       })
 
 
-      it('should return the number of collaborators the user is allowed', function () {
-        expect(this.result).to.equal(this.user.features.collaborators)
+      it('should return the number of collaborators the user is allowed', function (ctx) {
+        expect(ctx.result).to.equal(ctx.user.features.collaborators)
       })
       })
     })
     })
   })
   })
 
 
   describe('canAcceptEditCollaboratorInvite', function () {
   describe('canAcceptEditCollaboratorInvite', function () {
     describe('when the project has fewer collaborators than allowed', function () {
     describe('when the project has fewer collaborators than allowed', function () {
-      beforeEach(function () {
-        this.current_number = 1
-        this.user.features.collaborators = 2
-        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
-          sinon.stub().resolves(this.current_number)
+      beforeEach(function (ctx) {
+        ctx.current_number = 1
+        ctx.user.features.collaborators = 2
+        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
+          .stub()
+          .resolves(ctx.current_number)
       })
       })
 
 
-      it('should return true', async function () {
+      it('should return true', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
-            this.projectId
+          await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
+            ctx.projectId
           )
           )
         expect(result).to.be.true
         expect(result).to.be.true
       })
       })
     })
     })
 
 
     describe('when accepting the invite would exceed the collaborator limit', function () {
     describe('when accepting the invite would exceed the collaborator limit', function () {
-      beforeEach(function () {
-        this.current_number = 2
-        this.user.features.collaborators = 2
-        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
-          sinon.stub().resolves(this.current_number)
+      beforeEach(function (ctx) {
+        ctx.current_number = 2
+        ctx.user.features.collaborators = 2
+        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
+          .stub()
+          .resolves(ctx.current_number)
       })
       })
 
 
-      it('should return false', async function () {
+      it('should return false', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
-            this.projectId
+          await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
+            ctx.projectId
           )
           )
         expect(result).to.be.false
         expect(result).to.be.false
       })
       })
     })
     })
 
 
     describe('when the project has more collaborators than allowed', function () {
     describe('when the project has more collaborators than allowed', function () {
-      beforeEach(function () {
-        this.current_number = 3
-        this.user.features.collaborators = 2
-        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
-          sinon.stub().resolves(this.current_number)
+      beforeEach(function (ctx) {
+        ctx.current_number = 3
+        ctx.user.features.collaborators = 2
+        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
+          .stub()
+          .resolves(ctx.current_number)
       })
       })
 
 
-      it('should return false', async function () {
+      it('should return false', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
-            this.projectId
+          await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
+            ctx.projectId
           )
           )
         expect(result).to.be.false
         expect(result).to.be.false
       })
       })
     })
     })
 
 
     describe('when the project has infinite collaborators', function () {
     describe('when the project has infinite collaborators', function () {
-      beforeEach(function () {
-        this.current_number = 100
-        this.user.features.collaborators = -1
-        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
-          sinon.stub().resolves(this.current_number)
+      beforeEach(function (ctx) {
+        ctx.current_number = 100
+        ctx.user.features.collaborators = -1
+        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
+          .stub()
+          .resolves(ctx.current_number)
       })
       })
 
 
-      it('should return true', async function () {
+      it('should return true', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
-            this.projectId
+          await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
+            ctx.projectId
           )
           )
         expect(result).to.be.true
         expect(result).to.be.true
       })
       })
@@ -210,21 +240,22 @@ describe('LimitationsManager', function () {
 
 
   describe('canAddXEditCollaborators', function () {
   describe('canAddXEditCollaborators', function () {
     describe('when the project has fewer collaborators than allowed', function () {
     describe('when the project has fewer collaborators than allowed', function () {
-      beforeEach(function () {
-        this.current_number = 1
-        this.user.features.collaborators = 2
-        this.invite_count = 0
-        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
-          sinon.stub().resolves(this.current_number)
-        this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
+      beforeEach(function (ctx) {
+        ctx.current_number = 1
+        ctx.user.features.collaborators = 2
+        ctx.invite_count = 0
+        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
+          .stub()
+          .resolves(ctx.current_number)
+        ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
           .stub()
           .stub()
-          .resolves(this.invite_count)
+          .resolves(ctx.invite_count)
       })
       })
 
 
-      it('should return true', async function () {
+      it('should return true', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.canAddXEditCollaborators(
-            this.projectId,
+          await ctx.LimitationsManager.promises.canAddXEditCollaborators(
+            ctx.projectId,
             1
             1
           )
           )
         expect(result).to.be.true
         expect(result).to.be.true
@@ -232,21 +263,22 @@ describe('LimitationsManager', function () {
     })
     })
 
 
     describe('when the project has fewer collaborators and invites than allowed', function () {
     describe('when the project has fewer collaborators and invites than allowed', function () {
-      beforeEach(function () {
-        this.current_number = 1
-        this.user.features.collaborators = 4
-        this.invite_count = 1
-        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
-          sinon.stub().resolves(this.current_number)
-        this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
+      beforeEach(function (ctx) {
+        ctx.current_number = 1
+        ctx.user.features.collaborators = 4
+        ctx.invite_count = 1
+        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
           .stub()
           .stub()
-          .resolves(this.invite_count)
+          .resolves(ctx.current_number)
+        ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
+          .stub()
+          .resolves(ctx.invite_count)
       })
       })
 
 
-      it('should return true', async function () {
+      it('should return true', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.canAddXEditCollaborators(
-            this.projectId,
+          await ctx.LimitationsManager.promises.canAddXEditCollaborators(
+            ctx.projectId,
             1
             1
           )
           )
         expect(result).to.be.true
         expect(result).to.be.true
@@ -254,21 +286,22 @@ describe('LimitationsManager', function () {
     })
     })
 
 
     describe('when the project has fewer collaborators than allowed but I want to add more than allowed', function () {
     describe('when the project has fewer collaborators than allowed but I want to add more than allowed', function () {
-      beforeEach(function () {
-        this.current_number = 1
-        this.user.features.collaborators = 2
-        this.invite_count = 0
-        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
-          sinon.stub().resolves(this.current_number)
-        this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
+      beforeEach(function (ctx) {
+        ctx.current_number = 1
+        ctx.user.features.collaborators = 2
+        ctx.invite_count = 0
+        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
+          .stub()
+          .resolves(ctx.current_number)
+        ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
           .stub()
           .stub()
-          .resolves(this.invite_count)
+          .resolves(ctx.invite_count)
       })
       })
 
 
-      it('should return false', async function () {
+      it('should return false', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.canAddXEditCollaborators(
-            this.projectId,
+          await ctx.LimitationsManager.promises.canAddXEditCollaborators(
+            ctx.projectId,
             2
             2
           )
           )
         expect(result).to.be.false
         expect(result).to.be.false
@@ -276,21 +309,22 @@ describe('LimitationsManager', function () {
     })
     })
 
 
     describe('when the project has more collaborators than allowed', function () {
     describe('when the project has more collaborators than allowed', function () {
-      beforeEach(function () {
-        this.current_number = 3
-        this.user.features.collaborators = 2
-        this.invite_count = 0
-        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
-          sinon.stub().resolves(this.current_number)
-        this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
+      beforeEach(function (ctx) {
+        ctx.current_number = 3
+        ctx.user.features.collaborators = 2
+        ctx.invite_count = 0
+        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
+          .stub()
+          .resolves(ctx.current_number)
+        ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
           .stub()
           .stub()
-          .resolves(this.invite_count)
+          .resolves(ctx.invite_count)
       })
       })
 
 
-      it('should return false', async function () {
+      it('should return false', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.canAddXEditCollaborators(
-            this.projectId,
+          await ctx.LimitationsManager.promises.canAddXEditCollaborators(
+            ctx.projectId,
             1
             1
           )
           )
         expect(result).to.be.false
         expect(result).to.be.false
@@ -298,21 +332,22 @@ describe('LimitationsManager', function () {
     })
     })
 
 
     describe('when the project has infinite collaborators', function () {
     describe('when the project has infinite collaborators', function () {
-      beforeEach(function () {
-        this.current_number = 100
-        this.user.features.collaborators = -1
-        this.invite_count = 0
-        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
-          sinon.stub().resolves(this.current_number)
-        this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
+      beforeEach(function (ctx) {
+        ctx.current_number = 100
+        ctx.user.features.collaborators = -1
+        ctx.invite_count = 0
+        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
           .stub()
           .stub()
-          .resolves(this.invite_count)
+          .resolves(ctx.current_number)
+        ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
+          .stub()
+          .resolves(ctx.invite_count)
       })
       })
 
 
-      it('should return true', async function () {
+      it('should return true', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.canAddXEditCollaborators(
-            this.projectId,
+          await ctx.LimitationsManager.promises.canAddXEditCollaborators(
+            ctx.projectId,
             1
             1
           )
           )
         expect(result).to.be.true
         expect(result).to.be.true
@@ -320,21 +355,22 @@ describe('LimitationsManager', function () {
     })
     })
 
 
     describe('when the project has more invites than allowed', function () {
     describe('when the project has more invites than allowed', function () {
-      beforeEach(function () {
-        this.current_number = 0
-        this.user.features.collaborators = 2
-        this.invite_count = 2
-        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
-          sinon.stub().resolves(this.current_number)
-        this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
+      beforeEach(function (ctx) {
+        ctx.current_number = 0
+        ctx.user.features.collaborators = 2
+        ctx.invite_count = 2
+        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
+          .stub()
+          .resolves(ctx.current_number)
+        ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
           .stub()
           .stub()
-          .resolves(this.invite_count)
+          .resolves(ctx.invite_count)
       })
       })
 
 
-      it('should return false', async function () {
+      it('should return false', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.canAddXEditCollaborators(
-            this.projectId,
+          await ctx.LimitationsManager.promises.canAddXEditCollaborators(
+            ctx.projectId,
             1
             1
           )
           )
         expect(result).to.be.false
         expect(result).to.be.false
@@ -342,21 +378,22 @@ describe('LimitationsManager', function () {
     })
     })
 
 
     describe('when the project has more invites and collaborators than allowed', function () {
     describe('when the project has more invites and collaborators than allowed', function () {
-      beforeEach(function () {
-        this.current_number = 1
-        this.user.features.collaborators = 2
-        this.invite_count = 1
-        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
-          sinon.stub().resolves(this.current_number)
-        this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
+      beforeEach(function (ctx) {
+        ctx.current_number = 1
+        ctx.user.features.collaborators = 2
+        ctx.invite_count = 1
+        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
+          .stub()
+          .resolves(ctx.current_number)
+        ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
           .stub()
           .stub()
-          .resolves(this.invite_count)
+          .resolves(ctx.invite_count)
       })
       })
 
 
-      it('should return false', async function () {
+      it('should return false', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.canAddXEditCollaborators(
-            this.projectId,
+          await ctx.LimitationsManager.promises.canAddXEditCollaborators(
+            ctx.projectId,
             1
             1
           )
           )
         expect(result).to.be.false
         expect(result).to.be.false
@@ -365,19 +402,19 @@ describe('LimitationsManager', function () {
   })
   })
 
 
   describe('canChangeCollaboratorPrivilegeLevel', function () {
   describe('canChangeCollaboratorPrivilegeLevel', function () {
-    beforeEach(function () {
-      this.collaboratorId = 'collaborator-id'
-      this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves(
+    beforeEach(function (ctx) {
+      ctx.collaboratorId = 'collaborator-id'
+      ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves(
         'readOnly'
         'readOnly'
       )
       )
     })
     })
 
 
     describe("when the limit hasn't been reached", function () {
     describe("when the limit hasn't been reached", function () {
-      it('accepts changing a viewer to an editor', async function () {
+      it('accepts changing a viewer to an editor', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
-            this.projectId,
-            this.collaboratorId,
+          await ctx.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
+            ctx.projectId,
+            ctx.collaboratorId,
             'readAndWrite'
             'readAndWrite'
           )
           )
         expect(result).to.be.true
         expect(result).to.be.true
@@ -385,30 +422,30 @@ describe('LimitationsManager', function () {
     })
     })
 
 
     describe('when the limit has been reached', function () {
     describe('when the limit has been reached', function () {
-      beforeEach(function () {
-        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount.resolves(
+      beforeEach(function (ctx) {
+        ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount.resolves(
           1
           1
         )
         )
       })
       })
 
 
-      it('accepts changing a reviewer to an editor', async function () {
-        this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves(
+      it('accepts changing a reviewer to an editor', async function (ctx) {
+        ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves(
           'review'
           'review'
         )
         )
         const result =
         const result =
-          await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
-            this.projectId,
-            this.collaboratorId,
+          await ctx.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
+            ctx.projectId,
+            ctx.collaboratorId,
             'readAndWrite'
             'readAndWrite'
           )
           )
         expect(result).to.be.true
         expect(result).to.be.true
       })
       })
 
 
-      it('rejects changing a viewer to a reviewer', async function () {
+      it('rejects changing a viewer to a reviewer', async function (ctx) {
         const result =
         const result =
-          await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
-            this.projectId,
-            this.collaboratorId,
+          await ctx.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
+            ctx.projectId,
+            ctx.collaboratorId,
             'review'
             'review'
           )
           )
         expect(result).to.be.false
         expect(result).to.be.false
@@ -417,25 +454,25 @@ describe('LimitationsManager', function () {
   })
   })
 
 
   describe('userHasSubscription', function () {
   describe('userHasSubscription', function () {
-    beforeEach(function () {
-      this.SubscriptionLocator.promises.getUsersSubscription = sinon
+    beforeEach(function (ctx) {
+      ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
         .stub()
         .stub()
         .resolves()
         .resolves()
     })
     })
 
 
-    it('should return true if the recurly token is set', async function () {
-      this.SubscriptionLocator.promises.getUsersSubscription = sinon
+    it('should return true if the recurly token is set', async function (ctx) {
+      ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
         .stub()
         .stub()
         .resolves({
         .resolves({
           recurlySubscription_id: '1234',
           recurlySubscription_id: '1234',
         })
         })
       const { hasSubscription } =
       const { hasSubscription } =
-        await this.LimitationsManager.promises.userHasSubscription(this.user)
+        await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
       expect(hasSubscription).to.be.true
       expect(hasSubscription).to.be.true
     })
     })
 
 
-    it('should return true if the paymentProvider field is set', async function () {
-      this.SubscriptionLocator.promises.getUsersSubscription = sinon
+    it('should return true if the paymentProvider field is set', async function (ctx) {
+      ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
         .stub()
         .stub()
         .resolves({
         .resolves({
           paymentProvider: {
           paymentProvider: {
@@ -443,80 +480,80 @@ describe('LimitationsManager', function () {
           },
           },
         })
         })
       const { hasSubscription } =
       const { hasSubscription } =
-        await this.LimitationsManager.promises.userHasSubscription(this.user)
+        await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
       expect(hasSubscription).to.be.true
       expect(hasSubscription).to.be.true
     })
     })
 
 
-    it('should return false if the recurly token is not set', async function () {
-      this.SubscriptionLocator.promises.getUsersSubscription.resolves({})
+    it('should return false if the recurly token is not set', async function (ctx) {
+      ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({})
       const { hasSubscription } =
       const { hasSubscription } =
-        await this.LimitationsManager.promises.userHasSubscription(this.user)
+        await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
       expect(hasSubscription).to.be.false
       expect(hasSubscription).to.be.false
     })
     })
 
 
-    it('should return false if the subscription is undefined', async function () {
-      this.SubscriptionLocator.promises.getUsersSubscription.resolves()
+    it('should return false if the subscription is undefined', async function (ctx) {
+      ctx.SubscriptionLocator.promises.getUsersSubscription.resolves()
       const { hasSubscription } =
       const { hasSubscription } =
-        await this.LimitationsManager.promises.userHasSubscription(this.user)
+        await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
       expect(hasSubscription).to.be.false
       expect(hasSubscription).to.be.false
     })
     })
 
 
-    it('should return the subscription', async function () {
+    it('should return the subscription', async function (ctx) {
       const stubbedSubscription = { freeTrial: {}, token: '' }
       const stubbedSubscription = { freeTrial: {}, token: '' }
-      this.SubscriptionLocator.promises.getUsersSubscription.resolves(
+      ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
         stubbedSubscription
         stubbedSubscription
       )
       )
       const { subscription } =
       const { subscription } =
-        await this.LimitationsManager.promises.userHasSubscription(this.user)
+        await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
       expect(subscription).to.deep.equal(stubbedSubscription)
       expect(subscription).to.deep.equal(stubbedSubscription)
     })
     })
 
 
     describe('when user has a custom account', function () {
     describe('when user has a custom account', function () {
-      beforeEach(function () {
-        this.fakeSubscription = { customAccount: true }
-        this.SubscriptionLocator.promises.getUsersSubscription.resolves(
-          this.fakeSubscription
+      beforeEach(function (ctx) {
+        ctx.fakeSubscription = { customAccount: true }
+        ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
+          ctx.fakeSubscription
         )
         )
       })
       })
 
 
-      it('should return true', async function () {
+      it('should return true', async function (ctx) {
         const { hasSubscription } =
         const { hasSubscription } =
-          await this.LimitationsManager.promises.userHasSubscription(this.user)
+          await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
         expect(hasSubscription).to.be.true
         expect(hasSubscription).to.be.true
       })
       })
 
 
-      it('should return the subscription', async function () {
+      it('should return the subscription', async function (ctx) {
         const { subscription } =
         const { subscription } =
-          await this.LimitationsManager.promises.userHasSubscription(this.user)
-        expect(subscription).to.deep.equal(this.fakeSubscription)
+          await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
+        expect(subscription).to.deep.equal(ctx.fakeSubscription)
       })
       })
     })
     })
   })
   })
 
 
   describe('userIsMemberOfGroupSubscription', function () {
   describe('userIsMemberOfGroupSubscription', function () {
-    beforeEach(function () {
-      this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
+    beforeEach(function (ctx) {
+      ctx.SubscriptionLocator.promises.getMemberSubscriptions = sinon
         .stub()
         .stub()
         .resolves()
         .resolves()
     })
     })
 
 
-    it('should return false if there are no groups subcriptions', async function () {
-      this.SubscriptionLocator.promises.getMemberSubscriptions.resolves([])
+    it('should return false if there are no groups subcriptions', async function (ctx) {
+      ctx.SubscriptionLocator.promises.getMemberSubscriptions.resolves([])
       const { isMember } =
       const { isMember } =
-        await this.LimitationsManager.promises.userIsMemberOfGroupSubscription(
-          this.user
+        await ctx.LimitationsManager.promises.userIsMemberOfGroupSubscription(
+          ctx.user
         )
         )
       expect(isMember).to.be.false
       expect(isMember).to.be.false
     })
     })
 
 
-    it('should return true if there are no groups subcriptions', async function () {
+    it('should return true if there are no groups subcriptions', async function (ctx) {
       const subscriptions = ['mock-subscription']
       const subscriptions = ['mock-subscription']
-      this.SubscriptionLocator.promises.getMemberSubscriptions.resolves(
+      ctx.SubscriptionLocator.promises.getMemberSubscriptions.resolves(
         subscriptions
         subscriptions
       )
       )
       const { isMember, subscriptions: retSubscriptions } =
       const { isMember, subscriptions: retSubscriptions } =
-        await this.LimitationsManager.promises.userIsMemberOfGroupSubscription(
-          this.user
+        await ctx.LimitationsManager.promises.userIsMemberOfGroupSubscription(
+          ctx.user
         )
         )
       expect(isMember).to.be.true
       expect(isMember).to.be.true
       expect(retSubscriptions).to.deep.equal(subscriptions)
       expect(retSubscriptions).to.deep.equal(subscriptions)
@@ -524,52 +561,52 @@ describe('LimitationsManager', function () {
   })
   })
 
 
   describe('hasPaidSubscription', function () {
   describe('hasPaidSubscription', function () {
-    beforeEach(function () {
-      this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
+    beforeEach(function (ctx) {
+      ctx.SubscriptionLocator.promises.getMemberSubscriptions = sinon
         .stub()
         .stub()
         .resolves([])
         .resolves([])
-      this.SubscriptionLocator.promises.getUsersSubscription = sinon
+      ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
         .stub()
         .stub()
         .resolves(null)
         .resolves(null)
     })
     })
 
 
-    it('should return true if userIsMemberOfGroupSubscription', async function () {
-      this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
+    it('should return true if userIsMemberOfGroupSubscription', async function (ctx) {
+      ctx.SubscriptionLocator.promises.getMemberSubscriptions = sinon
         .stub()
         .stub()
         .resolves([{ _id: '123' }])
         .resolves([{ _id: '123' }])
       const { hasPaidSubscription } =
       const { hasPaidSubscription } =
-        await this.LimitationsManager.promises.hasPaidSubscription(this.user)
+        await ctx.LimitationsManager.promises.hasPaidSubscription(ctx.user)
       expect(hasPaidSubscription).to.be.true
       expect(hasPaidSubscription).to.be.true
     })
     })
 
 
-    it('should return true if userHasSubscription', async function () {
-      this.SubscriptionLocator.promises.getUsersSubscription = sinon
+    it('should return true if userHasSubscription', async function (ctx) {
+      ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
         .stub()
         .stub()
         .resolves({ recurlySubscription_id: '123' })
         .resolves({ recurlySubscription_id: '123' })
       const { hasPaidSubscription } =
       const { hasPaidSubscription } =
-        await this.LimitationsManager.promises.hasPaidSubscription(this.user)
+        await ctx.LimitationsManager.promises.hasPaidSubscription(ctx.user)
       expect(hasPaidSubscription).to.be.true
       expect(hasPaidSubscription).to.be.true
     })
     })
 
 
-    it('should return false if none are true', async function () {
+    it('should return false if none are true', async function (ctx) {
       const { hasPaidSubscription } =
       const { hasPaidSubscription } =
-        await this.LimitationsManager.promises.hasPaidSubscription(this.user)
+        await ctx.LimitationsManager.promises.hasPaidSubscription(ctx.user)
       expect(hasPaidSubscription).to.be.false
       expect(hasPaidSubscription).to.be.false
     })
     })
 
 
-    it('should have userHasSubscriptionOrIsGroupMember alias', async function () {
+    it('should have userHasSubscriptionOrIsGroupMember alias', async function (ctx) {
       const { hasPaidSubscription } =
       const { hasPaidSubscription } =
-        await this.LimitationsManager.promises.userHasSubscriptionOrIsGroupMember(
-          this.user
+        await ctx.LimitationsManager.promises.userHasSubscriptionOrIsGroupMember(
+          ctx.user
         )
         )
       expect(hasPaidSubscription).to.be.false
       expect(hasPaidSubscription).to.be.false
     })
     })
   })
   })
 
 
   describe('hasGroupMembersLimitReached', function () {
   describe('hasGroupMembersLimitReached', function () {
-    beforeEach(function () {
-      this.subscriptionId = '12312'
-      this.subscription = {
+    beforeEach(function (ctx) {
+      ctx.subscriptionId = '12312'
+      ctx.subscription = {
         membersLimit: 3,
         membersLimit: 3,
         member_ids: ['', ''],
         member_ids: ['', ''],
         teamInvites: [
         teamInvites: [
@@ -578,37 +615,37 @@ describe('LimitationsManager', function () {
       }
       }
     })
     })
 
 
-    it('should return true if the limit is hit (including members and invites)', async function () {
-      this.SubscriptionLocator.promises.getSubscription.resolves(
-        this.subscription
+    it('should return true if the limit is hit (including members and invites)', async function (ctx) {
+      ctx.SubscriptionLocator.promises.getSubscription.resolves(
+        ctx.subscription
       )
       )
       const { limitReached } =
       const { limitReached } =
-        await this.LimitationsManager.promises.hasGroupMembersLimitReached(
-          this.subscriptionId
+        await ctx.LimitationsManager.promises.hasGroupMembersLimitReached(
+          ctx.subscriptionId
         )
         )
       expect(limitReached).to.be.true
       expect(limitReached).to.be.true
     })
     })
 
 
-    it('should return false if the limit is not hit (including members and invites)', async function () {
-      this.subscription.membersLimit = 4
-      this.SubscriptionLocator.promises.getSubscription.resolves(
-        this.subscription
+    it('should return false if the limit is not hit (including members and invites)', async function (ctx) {
+      ctx.subscription.membersLimit = 4
+      ctx.SubscriptionLocator.promises.getSubscription.resolves(
+        ctx.subscription
       )
       )
       const { limitReached } =
       const { limitReached } =
-        await this.LimitationsManager.promises.hasGroupMembersLimitReached(
-          this.subscriptionId
+        await ctx.LimitationsManager.promises.hasGroupMembersLimitReached(
+          ctx.subscriptionId
         )
         )
       expect(limitReached).to.be.false
       expect(limitReached).to.be.false
     })
     })
 
 
-    it('should return true if the limit has been exceded (including members and invites)', async function () {
-      this.subscription.membersLimit = 2
-      this.SubscriptionLocator.promises.getSubscription.resolves(
-        this.subscription
+    it('should return true if the limit has been exceded (including members and invites)', async function (ctx) {
+      ctx.subscription.membersLimit = 2
+      ctx.SubscriptionLocator.promises.getSubscription.resolves(
+        ctx.subscription
       )
       )
       const { limitReached } =
       const { limitReached } =
-        await this.LimitationsManager.promises.hasGroupMembersLimitReached(
-          this.subscriptionId
+        await ctx.LimitationsManager.promises.hasGroupMembersLimitReached(
+          ctx.subscriptionId
         )
         )
       expect(limitReached).to.be.true
       expect(limitReached).to.be.true
     })
     })

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 365 - 323
services/web/test/unit/src/Subscription/SubscriptionHandler.test.mjs


+ 303 - 244
services/web/test/unit/src/Subscription/TeamInvitesHandler.test.mjs

@@ -1,15 +1,20 @@
-const SandboxedModule = require('sandboxed-module')
-const sinon = require('sinon')
-const { expect } = require('chai')
+import { vi, expect } from 'vitest'
+import sinon from 'sinon'
+
+import mongodb from 'mongodb-legacy'
+import Errors from '../../../../app/src/Features/Errors/Errors.js'
 const modulePath =
 const modulePath =
   '../../../../app/src/Features/Subscription/TeamInvitesHandler'
   '../../../../app/src/Features/Subscription/TeamInvitesHandler'
 
 
-const { ObjectId } = require('mongodb-legacy')
-const Errors = require('../../../../app/src/Features/Errors/Errors')
+vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
+  vi.importActual('../../../../app/src/Features/Errors/Errors.js')
+)
+
+const { ObjectId } = mongodb
 
 
 describe('TeamInvitesHandler', function () {
 describe('TeamInvitesHandler', function () {
-  beforeEach(function () {
-    this.manager = {
+  beforeEach(async function (ctx) {
+    ctx.manager = {
       _id: '666666',
       _id: '666666',
       first_name: 'Daenerys',
       first_name: 'Daenerys',
       last_name: 'Targaryen',
       last_name: 'Targaryen',
@@ -17,34 +22,34 @@ describe('TeamInvitesHandler', function () {
       emails: [{ email: 'daenerys@example.com' }],
       emails: [{ email: 'daenerys@example.com' }],
     }
     }
 
 
-    this.token = 'aaaaaaaaaaaaaaaaaaaaaa'
+    ctx.token = 'aaaaaaaaaaaaaaaaaaaaaa'
 
 
-    this.teamInvite = {
+    ctx.teamInvite = {
       email: 'jorah@example.com',
       email: 'jorah@example.com',
-      token: this.token,
+      token: ctx.token,
     }
     }
     // ensure teamInvite can be converted from Document to Object
     // ensure teamInvite can be converted from Document to Object
-    this.teamInvite.toObject = () => this.teamInvite
+    ctx.teamInvite.toObject = () => ctx.teamInvite
 
 
-    this.subscription = {
+    ctx.subscription = {
       id: '55153a8014829a865bbf700d',
       id: '55153a8014829a865bbf700d',
       _id: new ObjectId('55153a8014829a865bbf700d'),
       _id: new ObjectId('55153a8014829a865bbf700d'),
       recurlySubscription_id: '1a2b3c4d5e6f7g',
       recurlySubscription_id: '1a2b3c4d5e6f7g',
-      admin_id: this.manager._id,
+      admin_id: ctx.manager._id,
       groupPlan: true,
       groupPlan: true,
       member_ids: [],
       member_ids: [],
-      teamInvites: [this.teamInvite],
+      teamInvites: [ctx.teamInvite],
       save: sinon.stub().resolves(),
       save: sinon.stub().resolves(),
     }
     }
 
 
-    this.SubscriptionLocator = {
+    ctx.SubscriptionLocator = {
       promises: {
       promises: {
         getUsersSubscription: sinon.stub(),
         getUsersSubscription: sinon.stub(),
-        getSubscription: sinon.stub().resolves(this.subscription),
+        getSubscription: sinon.stub().resolves(ctx.subscription),
       },
       },
     }
     }
 
 
-    this.UserGetter = {
+    ctx.UserGetter = {
       promises: {
       promises: {
         getUser: sinon.stub().resolves(),
         getUser: sinon.stub().resolves(),
         getUserByAnyEmail: sinon.stub().resolves(),
         getUserByAnyEmail: sinon.stub().resolves(),
@@ -52,55 +57,55 @@ describe('TeamInvitesHandler', function () {
       },
       },
     }
     }
 
 
-    this.SubscriptionUpdater = {
+    ctx.SubscriptionUpdater = {
       promises: {
       promises: {
         addUserToGroup: sinon.stub().resolves(),
         addUserToGroup: sinon.stub().resolves(),
         deleteSubscription: sinon.stub().resolves(),
         deleteSubscription: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.LimitationsManager = {
+    ctx.LimitationsManager = {
       teamHasReachedMemberLimit: sinon.stub().returns(false),
       teamHasReachedMemberLimit: sinon.stub().returns(false),
     }
     }
 
 
-    this.Subscription = {
+    ctx.Subscription = {
       findOne: sinon.stub().resolves(),
       findOne: sinon.stub().resolves(),
       updateOne: sinon.stub().resolves(),
       updateOne: sinon.stub().resolves(),
     }
     }
 
 
-    this.SSOConfig = {
+    ctx.SSOConfig = {
       findById: sinon.stub().resolves(),
       findById: sinon.stub().resolves(),
     }
     }
 
 
-    this.EmailHandler = {
+    ctx.EmailHandler = {
       promises: {
       promises: {
         sendEmail: sinon.stub().resolves(null),
         sendEmail: sinon.stub().resolves(null),
       },
       },
     }
     }
 
 
-    this.newToken = 'bbbbbbbbb'
+    ctx.newToken = 'bbbbbbbbb'
 
 
-    this.crypto = {
+    ctx.crypto = {
       randomBytes: () => {
       randomBytes: () => {
-        return { toString: sinon.stub().returns(this.newToken) }
+        return { toString: sinon.stub().returns(ctx.newToken) }
       },
       },
     }
     }
 
 
-    this.UserGetter.promises.getUser
-      .withArgs(this.manager._id)
-      .resolves(this.manager)
-    this.UserGetter.promises.getUserByAnyEmail
-      .withArgs(this.manager.email)
-      .resolves(this.manager)
-    this.UserGetter.promises.getUserByMainEmail
-      .withArgs(this.manager.email)
-      .resolves(this.manager)
-
-    this.SubscriptionLocator.promises.getUsersSubscription.resolves(
-      this.subscription
+    ctx.UserGetter.promises.getUser
+      .withArgs(ctx.manager._id)
+      .resolves(ctx.manager)
+    ctx.UserGetter.promises.getUserByAnyEmail
+      .withArgs(ctx.manager.email)
+      .resolves(ctx.manager)
+    ctx.UserGetter.promises.getUserByMainEmail
+      .withArgs(ctx.manager.email)
+      .resolves(ctx.manager)
+
+    ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
+      ctx.subscription
     )
     )
 
 
-    this.NotificationsBuilder = {
+    ctx.NotificationsBuilder = {
       promises: {
       promises: {
         groupInvitation: sinon.stub().returns({
         groupInvitation: sinon.stub().returns({
           create: sinon.stub().resolves(),
           create: sinon.stub().resolves(),
@@ -109,51 +114,105 @@ describe('TeamInvitesHandler', function () {
       },
       },
     }
     }
 
 
-    this.Subscription.findOne.resolves(this.subscription)
+    ctx.Subscription.findOne.resolves(ctx.subscription)
 
 
-    this.RecurlyClient = {
+    ctx.RecurlyClient = {
       promises: {
       promises: {
         terminateSubscriptionByUuid: sinon.stub().resolves(),
         terminateSubscriptionByUuid: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.TeamInvitesHandler = SandboxedModule.require(modulePath, {
-      requires: {
-        'mongodb-legacy': { ObjectId },
-        crypto: this.crypto,
-        '@overleaf/settings': { siteUrl: 'http://example.com' },
-        '../../models/TeamInvite': { TeamInvite: (this.TeamInvite = {}) },
-        '../../models/Subscription': { Subscription: this.Subscription },
-        '../../models/SSOConfig': { SSOConfig: this.SSOConfig },
-        '../User/UserGetter': this.UserGetter,
-        './SubscriptionLocator': this.SubscriptionLocator,
-        './SubscriptionUpdater': this.SubscriptionUpdater,
-        './LimitationsManager': this.LimitationsManager,
-        '../Email/EmailHandler': this.EmailHandler,
-        '../Notifications/NotificationsBuilder': this.NotificationsBuilder,
-        '../../infrastructure/Modules': (this.Modules = {
-          promises: { hooks: { fire: sinon.stub().resolves() } },
-        }),
-        './RecurlyClient': this.RecurlyClient,
-      },
-    })
+    vi.doMock('mongodb-legacy', () => ({
+      default: { ObjectId },
+    }))
+
+    vi.doMock('crypto', () => ({
+      default: ctx.crypto,
+    }))
+
+    vi.doMock('@overleaf/settings', () => ({
+      default: { siteUrl: 'http://example.com' },
+    }))
+
+    vi.doMock('../../../../app/src/models/TeamInvite', () => ({
+      TeamInvite: (ctx.TeamInvite = {}),
+    }))
+
+    vi.doMock('../../../../app/src/models/Subscription', () => ({
+      Subscription: ctx.Subscription,
+    }))
+
+    vi.doMock('../../../../app/src/models/SSOConfig', () => ({
+      SSOConfig: ctx.SSOConfig,
+    }))
+
+    vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
+      default: ctx.UserGetter,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Subscription/SubscriptionLocator',
+      () => ({
+        default: ctx.SubscriptionLocator,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/Subscription/SubscriptionUpdater',
+      () => ({
+        default: ctx.SubscriptionUpdater,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/Subscription/LimitationsManager',
+      () => ({
+        default: ctx.LimitationsManager,
+      })
+    )
+
+    vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({
+      default: ctx.EmailHandler,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Notifications/NotificationsBuilder',
+      () => ({
+        default: ctx.NotificationsBuilder,
+      })
+    )
+
+    vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
+      default: (ctx.Modules = {
+        promises: { hooks: { fire: sinon.stub().resolves() } },
+      }),
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Subscription/RecurlyClient',
+      () => ({
+        default: ctx.RecurlyClient,
+      })
+    )
+
+    ctx.TeamInvitesHandler = (await import(modulePath)).default
   })
   })
 
 
   describe('getInvite', function () {
   describe('getInvite', function () {
-    it("returns the invite if there's one", async function () {
+    it("returns the invite if there's one", async function (ctx) {
       const { invite, subscription } =
       const { invite, subscription } =
-        await this.TeamInvitesHandler.promises.getInvite(this.token)
+        await ctx.TeamInvitesHandler.promises.getInvite(ctx.token)
 
 
-      expect(invite).to.deep.eq(this.teamInvite)
-      expect(subscription).to.deep.eq(this.subscription)
+      expect(invite).to.deep.eq(ctx.teamInvite)
+      expect(subscription).to.deep.eq(ctx.subscription)
     })
     })
 
 
-    it("returns teamNotFound if there's none", async function () {
-      this.Subscription.findOne = sinon.stub().resolves(null)
+    it("returns teamNotFound if there's none", async function (ctx) {
+      ctx.Subscription.findOne = sinon.stub().resolves(null)
 
 
       let error
       let error
       try {
       try {
-        await this.TeamInvitesHandler.promises.getInvite(this.token)
+        await ctx.TeamInvitesHandler.promises.getInvite(ctx.token)
       } catch (err) {
       } catch (err) {
         error = err
         error = err
       }
       }
@@ -163,66 +222,66 @@ describe('TeamInvitesHandler', function () {
   })
   })
 
 
   describe('createInvite', function () {
   describe('createInvite', function () {
-    it('adds the team invite to the subscription', async function () {
-      const invite = await this.TeamInvitesHandler.promises.createInvite(
-        this.manager._id,
-        this.subscription,
+    it('adds the team invite to the subscription', async function (ctx) {
+      const invite = await ctx.TeamInvitesHandler.promises.createInvite(
+        ctx.manager._id,
+        ctx.subscription,
         'John.Snow@example.com'
         'John.Snow@example.com'
       )
       )
-      expect(invite.token).to.eq(this.newToken)
+      expect(invite.token).to.eq(ctx.newToken)
       expect(invite.email).to.eq('john.snow@example.com')
       expect(invite.email).to.eq('john.snow@example.com')
       expect(invite.inviterName).to.eq(
       expect(invite.inviterName).to.eq(
         'Daenerys Targaryen (daenerys@example.com)'
         'Daenerys Targaryen (daenerys@example.com)'
       )
       )
       expect(invite.invite).to.be.true
       expect(invite.invite).to.be.true
-      expect(this.subscription.teamInvites).to.deep.include(invite)
+      expect(ctx.subscription.teamInvites).to.deep.include(invite)
     })
     })
 
 
-    it('sends an email', async function () {
-      await this.TeamInvitesHandler.promises.createInvite(
-        this.manager._id,
-        this.subscription,
+    it('sends an email', async function (ctx) {
+      await ctx.TeamInvitesHandler.promises.createInvite(
+        ctx.manager._id,
+        ctx.subscription,
         'John.Snow@example.com'
         'John.Snow@example.com'
       )
       )
 
 
-      this.EmailHandler.promises.sendEmail
+      ctx.EmailHandler.promises.sendEmail
         .calledWith(
         .calledWith(
           'verifyEmailToJoinTeam',
           'verifyEmailToJoinTeam',
           sinon.match({
           sinon.match({
             to: 'john.snow@example.com',
             to: 'john.snow@example.com',
-            inviter: this.manager,
-            acceptInviteUrl: `http://example.com/subscription/invites/${this.newToken}/`,
+            inviter: ctx.manager,
+            acceptInviteUrl: `http://example.com/subscription/invites/${ctx.newToken}/`,
           })
           })
         )
         )
         .should.equal(true)
         .should.equal(true)
     })
     })
 
 
-    it('refreshes the existing invite if the email has already been invited', async function () {
-      const originalInvite = Object.assign({}, this.teamInvite)
+    it('refreshes the existing invite if the email has already been invited', async function (ctx) {
+      const originalInvite = Object.assign({}, ctx.teamInvite)
 
 
-      const invite = await this.TeamInvitesHandler.promises.createInvite(
-        this.manager._id,
-        this.subscription,
+      const invite = await ctx.TeamInvitesHandler.promises.createInvite(
+        ctx.manager._id,
+        ctx.subscription,
         originalInvite.email
         originalInvite.email
       )
       )
       expect(invite).to.exist
       expect(invite).to.exist
 
 
-      expect(this.subscription.teamInvites.length).to.eq(1)
-      expect(this.subscription.teamInvites).to.deep.include(invite)
+      expect(ctx.subscription.teamInvites.length).to.eq(1)
+      expect(ctx.subscription.teamInvites).to.deep.include(invite)
 
 
       expect(invite.email).to.eq(originalInvite.email)
       expect(invite.email).to.eq(originalInvite.email)
 
 
-      this.subscription.save.calledOnce.should.eq(true)
+      ctx.subscription.save.calledOnce.should.eq(true)
     })
     })
 
 
-    it('removes any legacy invite from the subscription', async function () {
-      await this.TeamInvitesHandler.promises.createInvite(
-        this.manager._id,
-        this.subscription,
+    it('removes any legacy invite from the subscription', async function (ctx) {
+      await ctx.TeamInvitesHandler.promises.createInvite(
+        ctx.manager._id,
+        ctx.subscription,
         'John.Snow@example.com'
         'John.Snow@example.com'
       )
       )
 
 
-      this.Subscription.updateOne
+      ctx.Subscription.updateOne
         .calledWith(
         .calledWith(
           { _id: new ObjectId('55153a8014829a865bbf700d') },
           { _id: new ObjectId('55153a8014829a865bbf700d') },
           { $pull: { invited_emails: 'john.snow@example.com' } }
           { $pull: { invited_emails: 'john.snow@example.com' } }
@@ -230,77 +289,77 @@ describe('TeamInvitesHandler', function () {
         .should.eq(true)
         .should.eq(true)
     })
     })
 
 
-    it('add user to subscription if inviting self', async function () {
-      const invite = await this.TeamInvitesHandler.promises.createInvite(
-        this.manager._id,
-        this.subscription,
-        this.manager.email
+    it('add user to subscription if inviting self', async function (ctx) {
+      const invite = await ctx.TeamInvitesHandler.promises.createInvite(
+        ctx.manager._id,
+        ctx.subscription,
+        ctx.manager.email
       )
       )
       sinon.assert.calledWith(
       sinon.assert.calledWith(
-        this.SubscriptionUpdater.promises.addUserToGroup,
-        this.subscription._id,
-        this.manager._id
+        ctx.SubscriptionUpdater.promises.addUserToGroup,
+        ctx.subscription._id,
+        ctx.manager._id
       )
       )
-      sinon.assert.notCalled(this.subscription.save)
+      sinon.assert.notCalled(ctx.subscription.save)
       expect(invite.token).to.not.exist
       expect(invite.token).to.not.exist
-      expect(invite.email).to.eq(this.manager.email)
-      expect(invite.first_name).to.eq(this.manager.first_name)
-      expect(invite.last_name).to.eq(this.manager.last_name)
+      expect(invite.email).to.eq(ctx.manager.email)
+      expect(invite.first_name).to.eq(ctx.manager.first_name)
+      expect(invite.last_name).to.eq(ctx.manager.last_name)
       expect(invite.invite).to.be.false
       expect(invite.invite).to.be.false
     })
     })
 
 
-    it('sends an SSO invite if SSO is enabled and inviting self', async function () {
-      this.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
-      this.SSOConfig.findById
-        .withArgs(this.subscription.ssoConfig)
+    it('sends an SSO invite if SSO is enabled and inviting self', async function (ctx) {
+      ctx.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
+      ctx.SSOConfig.findById
+        .withArgs(ctx.subscription.ssoConfig)
         .resolves({ enabled: true })
         .resolves({ enabled: true })
 
 
-      await this.TeamInvitesHandler.promises.createInvite(
-        this.manager._id,
-        this.subscription,
-        this.manager.email
+      await ctx.TeamInvitesHandler.promises.createInvite(
+        ctx.manager._id,
+        ctx.subscription,
+        ctx.manager.email
       )
       )
       sinon.assert.calledWith(
       sinon.assert.calledWith(
-        this.Modules.promises.hooks.fire,
+        ctx.Modules.promises.hooks.fire,
         'sendGroupSSOReminder',
         'sendGroupSSOReminder',
-        this.manager._id,
-        this.subscription._id
+        ctx.manager._id,
+        ctx.subscription._id
       )
       )
     })
     })
 
 
-    it('does not send an SSO invite if SSO is disabled and inviting self', async function () {
-      this.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
-      this.SSOConfig.findById
-        .withArgs(this.subscription.ssoConfig)
+    it('does not send an SSO invite if SSO is disabled and inviting self', async function (ctx) {
+      ctx.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
+      ctx.SSOConfig.findById
+        .withArgs(ctx.subscription.ssoConfig)
         .resolves({ enabled: false })
         .resolves({ enabled: false })
 
 
-      await this.TeamInvitesHandler.promises.createInvite(
-        this.manager._id,
-        this.subscription,
-        this.manager.email
+      await ctx.TeamInvitesHandler.promises.createInvite(
+        ctx.manager._id,
+        ctx.subscription,
+        ctx.manager.email
       )
       )
-      sinon.assert.notCalled(this.Modules.promises.hooks.fire)
+      sinon.assert.notCalled(ctx.Modules.promises.hooks.fire)
     })
     })
 
 
-    it('sends a notification if inviting registered user', async function () {
+    it('sends a notification if inviting registered user', async function (ctx) {
       const id = new ObjectId('6a6b3a8014829a865bbf700d')
       const id = new ObjectId('6a6b3a8014829a865bbf700d')
       const managedUsersEnabled = false
       const managedUsersEnabled = false
 
 
-      this.UserGetter.promises.getUserByMainEmail
+      ctx.UserGetter.promises.getUserByMainEmail
         .withArgs('john.snow@example.com')
         .withArgs('john.snow@example.com')
         .resolves({
         .resolves({
           _id: id,
           _id: id,
         })
         })
 
 
-      const invite = await this.TeamInvitesHandler.promises.createInvite(
-        this.manager._id,
-        this.subscription,
+      const invite = await ctx.TeamInvitesHandler.promises.createInvite(
+        ctx.manager._id,
+        ctx.subscription,
         'John.Snow@example.com'
         'John.Snow@example.com'
       )
       )
-      this.NotificationsBuilder.promises
+      ctx.NotificationsBuilder.promises
         .groupInvitation(
         .groupInvitation(
           id.toString(),
           id.toString(),
-          this.subscription._id,
+          ctx.subscription._id,
           managedUsersEnabled
           managedUsersEnabled
         )
         )
         .create.calledWith(invite)
         .create.calledWith(invite)
@@ -309,42 +368,42 @@ describe('TeamInvitesHandler', function () {
   })
   })
 
 
   describe('importInvite', function () {
   describe('importInvite', function () {
-    beforeEach(function () {
-      this.sentAt = new Date()
+    beforeEach(function (ctx) {
+      ctx.sentAt = new Date()
     })
     })
 
 
-    it('can imports an invite from v1', function () {
-      this.TeamInvitesHandler.importInvite(
-        this.subscription,
+    it('can imports an invite from v1', function (ctx) {
+      ctx.TeamInvitesHandler.importInvite(
+        ctx.subscription,
         'A-Team',
         'A-Team',
         'hannibal@a-team.org',
         'hannibal@a-team.org',
         'secret',
         'secret',
-        this.sentAt,
+        ctx.sentAt,
         error => {
         error => {
           expect(error).not.to.exist
           expect(error).not.to.exist
 
 
-          this.subscription.save.calledOnce.should.eq(true)
+          ctx.subscription.save.calledOnce.should.eq(true)
 
 
-          const invite = this.subscription.teamInvites.find(
+          const invite = ctx.subscription.teamInvites.find(
             i => i.email === 'hannibal@a-team.org'
             i => i.email === 'hannibal@a-team.org'
           )
           )
           expect(invite.token).to.eq('secret')
           expect(invite.token).to.eq('secret')
-          expect(invite.sentAt).to.eq(this.sentAt)
+          expect(invite.sentAt).to.eq(ctx.sentAt)
         }
         }
       )
       )
     })
     })
   })
   })
 
 
   describe('acceptInvite', function () {
   describe('acceptInvite', function () {
-    beforeEach(function () {
-      this.user = {
+    beforeEach(function (ctx) {
+      ctx.user = {
         id: '123456789',
         id: '123456789',
         first_name: 'Tyrion',
         first_name: 'Tyrion',
         last_name: 'Lannister',
         last_name: 'Lannister',
         email: 'tyrion@example.com',
         email: 'tyrion@example.com',
       }
       }
 
 
-      this.user_subscription = {
+      ctx.user_subscription = {
         id: '66264b9125930b976cc0811e',
         id: '66264b9125930b976cc0811e',
         _id: new ObjectId('66264b9125930b976cc0811e'),
         _id: new ObjectId('66264b9125930b976cc0811e'),
         groupPlan: false,
         groupPlan: false,
@@ -355,17 +414,17 @@ describe('TeamInvitesHandler', function () {
         save: sinon.stub().resolves(),
         save: sinon.stub().resolves(),
       }
       }
 
 
-      this.ipAddress = '127.0.0.1'
+      ctx.ipAddress = '127.0.0.1'
 
 
-      this.UserGetter.promises.getUserByAnyEmail
-        .withArgs(this.user.email)
-        .resolves(this.user)
+      ctx.UserGetter.promises.getUserByAnyEmail
+        .withArgs(ctx.user.email)
+        .resolves(ctx.user)
 
 
-      this.SubscriptionLocator.promises.getUsersSubscription
-        .withArgs(this.user.id)
-        .resolves(this.user_subscription)
+      ctx.SubscriptionLocator.promises.getUsersSubscription
+        .withArgs(ctx.user.id)
+        .resolves(ctx.user_subscription)
 
 
-      this.subscription.teamInvites.push({
+      ctx.subscription.teamInvites.push({
         email: 'john.snow@example.com',
         email: 'john.snow@example.com',
         token: 'dddddddd',
         token: 'dddddddd',
         inviterName: 'Daenerys Targaryen (daenerys@example.com)',
         inviterName: 'Daenerys Targaryen (daenerys@example.com)',
@@ -373,24 +432,24 @@ describe('TeamInvitesHandler', function () {
     })
     })
 
 
     describe('with standard group', function () {
     describe('with standard group', function () {
-      it('adds the user to the team', async function () {
-        await this.TeamInvitesHandler.promises.acceptInvite(
+      it('adds the user to the team', async function (ctx) {
+        await ctx.TeamInvitesHandler.promises.acceptInvite(
           'dddddddd',
           'dddddddd',
-          this.user.id,
-          this.ipAddress
+          ctx.user.id,
+          ctx.ipAddress
         )
         )
-        this.SubscriptionUpdater.promises.addUserToGroup
-          .calledWith(this.subscription._id, this.user.id)
+        ctx.SubscriptionUpdater.promises.addUserToGroup
+          .calledWith(ctx.subscription._id, ctx.user.id)
           .should.eq(true)
           .should.eq(true)
       })
       })
 
 
-      it('removes the invite from the subscription', async function () {
-        await this.TeamInvitesHandler.promises.acceptInvite(
+      it('removes the invite from the subscription', async function (ctx) {
+        await ctx.TeamInvitesHandler.promises.acceptInvite(
           'dddddddd',
           'dddddddd',
-          this.user.id,
-          this.ipAddress
+          ctx.user.id,
+          ctx.ipAddress
         )
         )
-        this.Subscription.updateOne
+        ctx.Subscription.updateOne
           .calledWith(
           .calledWith(
             { _id: new ObjectId('55153a8014829a865bbf700d') },
             { _id: new ObjectId('55153a8014829a865bbf700d') },
             { $pull: { teamInvites: { email: 'john.snow@example.com' } } }
             { $pull: { teamInvites: { email: 'john.snow@example.com' } } }
@@ -398,114 +457,114 @@ describe('TeamInvitesHandler', function () {
           .should.eq(true)
           .should.eq(true)
       })
       })
 
 
-      it('removes dashboard notification after they accepted group invitation', async function () {
+      it('removes dashboard notification after they accepted group invitation', async function (ctx) {
         const managedUsersEnabled = false
         const managedUsersEnabled = false
 
 
-        await this.TeamInvitesHandler.promises.acceptInvite(
+        await ctx.TeamInvitesHandler.promises.acceptInvite(
           'dddddddd',
           'dddddddd',
-          this.user.id,
-          this.ipAddress
+          ctx.user.id,
+          ctx.ipAddress
         )
         )
         sinon.assert.called(
         sinon.assert.called(
-          this.NotificationsBuilder.promises.groupInvitation(
-            this.user.id,
-            this.subscription._id,
+          ctx.NotificationsBuilder.promises.groupInvitation(
+            ctx.user.id,
+            ctx.subscription._id,
             managedUsersEnabled
             managedUsersEnabled
           ).read
           ).read
         )
         )
       })
       })
 
 
-      it('should not schedule an SSO invite reminder', async function () {
-        await this.TeamInvitesHandler.promises.acceptInvite(
+      it('should not schedule an SSO invite reminder', async function (ctx) {
+        await ctx.TeamInvitesHandler.promises.acceptInvite(
           'dddddddd',
           'dddddddd',
-          this.user.id,
-          this.ipAddress
+          ctx.user.id,
+          ctx.ipAddress
         )
         )
-        sinon.assert.notCalled(this.Modules.promises.hooks.fire)
+        sinon.assert.notCalled(ctx.Modules.promises.hooks.fire)
       })
       })
     })
     })
 
 
     describe('with managed group', function () {
     describe('with managed group', function () {
-      it('should enroll the group member', async function () {
-        this.subscription.managedUsersEnabled = true
+      it('should enroll the group member', async function (ctx) {
+        ctx.subscription.managedUsersEnabled = true
 
 
-        await this.TeamInvitesHandler.promises.acceptInvite(
+        await ctx.TeamInvitesHandler.promises.acceptInvite(
           'dddddddd',
           'dddddddd',
-          this.user.id,
-          this.ipAddress
+          ctx.user.id,
+          ctx.ipAddress
         )
         )
         sinon.assert.calledWith(
         sinon.assert.calledWith(
-          this.SubscriptionUpdater.promises.deleteSubscription,
-          this.user_subscription,
-          { id: this.user.id, ip: this.ipAddress }
+          ctx.SubscriptionUpdater.promises.deleteSubscription,
+          ctx.user_subscription,
+          { id: ctx.user.id, ip: ctx.ipAddress }
         )
         )
         sinon.assert.calledWith(
         sinon.assert.calledWith(
-          this.RecurlyClient.promises.terminateSubscriptionByUuid,
-          this.user_subscription.recurlySubscription_id
+          ctx.RecurlyClient.promises.terminateSubscriptionByUuid,
+          ctx.user_subscription.recurlySubscription_id
         )
         )
         sinon.assert.calledWith(
         sinon.assert.calledWith(
-          this.Modules.promises.hooks.fire,
+          ctx.Modules.promises.hooks.fire,
           'enrollInManagedSubscription',
           'enrollInManagedSubscription',
-          this.user.id,
-          this.subscription
+          ctx.user.id,
+          ctx.subscription
         )
         )
       })
       })
 
 
-      it('should not delete the users subscription if that subscription is also the join target', async function () {
-        this.subscription.managedUsersEnabled = true
-        this.SubscriptionLocator.promises.getUsersSubscription
-          .withArgs(this.user.id)
-          .resolves(this.subscription)
+      it('should not delete the users subscription if that subscription is also the join target', async function (ctx) {
+        ctx.subscription.managedUsersEnabled = true
+        ctx.SubscriptionLocator.promises.getUsersSubscription
+          .withArgs(ctx.user.id)
+          .resolves(ctx.subscription)
 
 
-        await this.TeamInvitesHandler.promises.acceptInvite(
+        await ctx.TeamInvitesHandler.promises.acceptInvite(
           'dddddddd',
           'dddddddd',
-          this.user.id,
-          this.ipAddress
+          ctx.user.id,
+          ctx.ipAddress
         )
         )
 
 
         sinon.assert.notCalled(
         sinon.assert.notCalled(
-          this.SubscriptionUpdater.promises.deleteSubscription
+          ctx.SubscriptionUpdater.promises.deleteSubscription
         )
         )
       })
       })
     })
     })
 
 
     describe('with group SSO enabled', function () {
     describe('with group SSO enabled', function () {
-      it('should schedule an SSO invite reminder', async function () {
-        this.subscription.ssoConfig = 'ssoconfig1'
-        this.SSOConfig.findById
+      it('should schedule an SSO invite reminder', async function (ctx) {
+        ctx.subscription.ssoConfig = 'ssoconfig1'
+        ctx.SSOConfig.findById
           .withArgs('ssoconfig1')
           .withArgs('ssoconfig1')
           .resolves({ enabled: true })
           .resolves({ enabled: true })
 
 
-        await this.TeamInvitesHandler.promises.acceptInvite(
+        await ctx.TeamInvitesHandler.promises.acceptInvite(
           'dddddddd',
           'dddddddd',
-          this.user.id,
-          this.ipAddress
+          ctx.user.id,
+          ctx.ipAddress
         )
         )
         sinon.assert.calledWith(
         sinon.assert.calledWith(
-          this.Modules.promises.hooks.fire,
+          ctx.Modules.promises.hooks.fire,
           'scheduleGroupSSOReminder',
           'scheduleGroupSSOReminder',
-          this.user.id,
-          this.subscription._id
+          ctx.user.id,
+          ctx.subscription._id
         )
         )
       })
       })
     })
     })
   })
   })
 
 
   describe('revokeInvite', function () {
   describe('revokeInvite', function () {
-    it('removes the team invite from the subscription', async function () {
-      await this.TeamInvitesHandler.promises.revokeInvite(
-        this.manager._id,
-        this.subscription,
+    it('removes the team invite from the subscription', async function (ctx) {
+      await ctx.TeamInvitesHandler.promises.revokeInvite(
+        ctx.manager._id,
+        ctx.subscription,
         'jorah@example.com'
         'jorah@example.com'
       )
       )
-      this.Subscription.updateOne
+      ctx.Subscription.updateOne
         .calledWith(
         .calledWith(
           { _id: new ObjectId('55153a8014829a865bbf700d') },
           { _id: new ObjectId('55153a8014829a865bbf700d') },
           { $pull: { teamInvites: { email: 'jorah@example.com' } } }
           { $pull: { teamInvites: { email: 'jorah@example.com' } } }
         )
         )
         .should.eq(true)
         .should.eq(true)
 
 
-      this.Subscription.updateOne
+      ctx.Subscription.updateOne
         .calledWith(
         .calledWith(
           { _id: new ObjectId('55153a8014829a865bbf700d') },
           { _id: new ObjectId('55153a8014829a865bbf700d') },
           { $pull: { invited_emails: 'jorah@example.com' } }
           { $pull: { invited_emails: 'jorah@example.com' } }
@@ -513,7 +572,7 @@ describe('TeamInvitesHandler', function () {
         .should.eq(true)
         .should.eq(true)
     })
     })
 
 
-    it('removes dashboard notification for pending group invitation', async function () {
+    it('removes dashboard notification for pending group invitation', async function (ctx) {
       const managedUsersEnabled = false
       const managedUsersEnabled = false
 
 
       const pendingUser = {
       const pendingUser = {
@@ -521,20 +580,20 @@ describe('TeamInvitesHandler', function () {
         email: 'tyrion@example.com',
         email: 'tyrion@example.com',
       }
       }
 
 
-      this.UserGetter.promises.getUserByAnyEmail
+      ctx.UserGetter.promises.getUserByAnyEmail
         .withArgs(pendingUser.email)
         .withArgs(pendingUser.email)
         .resolves(pendingUser)
         .resolves(pendingUser)
 
 
-      await this.TeamInvitesHandler.promises.revokeInvite(
-        this.manager._id,
-        this.subscription,
+      await ctx.TeamInvitesHandler.promises.revokeInvite(
+        ctx.manager._id,
+        ctx.subscription,
         pendingUser.email
         pendingUser.email
       )
       )
 
 
       sinon.assert.called(
       sinon.assert.called(
-        this.NotificationsBuilder.promises.groupInvitation(
+        ctx.NotificationsBuilder.promises.groupInvitation(
           pendingUser.id,
           pendingUser.id,
-          this.subscription._id,
+          ctx.subscription._id,
           managedUsersEnabled
           managedUsersEnabled
         ).read
         ).read
       )
       )
@@ -542,47 +601,47 @@ describe('TeamInvitesHandler', function () {
   })
   })
 
 
   describe('createTeamInvitesForLegacyInvitedEmail', function () {
   describe('createTeamInvitesForLegacyInvitedEmail', function () {
-    beforeEach(function () {
-      this.subscription.invited_emails = [
+    beforeEach(function (ctx) {
+      ctx.subscription.invited_emails = [
         'eddard@example.com',
         'eddard@example.com',
         'robert@example.com',
         'robert@example.com',
       ]
       ]
-      this.TeamInvitesHandler.createInvite = sinon.stub().resolves(null)
-      this.SubscriptionLocator.promises.getGroupsWithEmailInvite = sinon
+      ctx.TeamInvitesHandler.createInvite = sinon.stub().resolves(null)
+      ctx.SubscriptionLocator.promises.getGroupsWithEmailInvite = sinon
         .stub()
         .stub()
-        .resolves([this.subscription])
+        .resolves([ctx.subscription])
     })
     })
 
 
-    it('sends an invitation email to addresses in the legacy invited_emails field', async function () {
+    it('sends an invitation email to addresses in the legacy invited_emails field', async function (ctx) {
       const invites =
       const invites =
-        await this.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail(
+        await ctx.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail(
           'eddard@example.com'
           'eddard@example.com'
         )
         )
 
 
       expect(invites.length).to.eq(1)
       expect(invites.length).to.eq(1)
 
 
       const [invite] = invites
       const [invite] = invites
-      expect(invite.token).to.eq(this.newToken)
+      expect(invite.token).to.eq(ctx.newToken)
       expect(invite.email).to.eq('eddard@example.com')
       expect(invite.email).to.eq('eddard@example.com')
       expect(invite.inviterName).to.eq(
       expect(invite.inviterName).to.eq(
         'Daenerys Targaryen (daenerys@example.com)'
         'Daenerys Targaryen (daenerys@example.com)'
       )
       )
       expect(invite.invite).to.be.true
       expect(invite.invite).to.be.true
-      expect(this.subscription.teamInvites).to.deep.include(invite)
+      expect(ctx.subscription.teamInvites).to.deep.include(invite)
     })
     })
   })
   })
 
 
   describe('validation', function () {
   describe('validation', function () {
-    it("doesn't create an invite if the team limit has been reached", async function () {
-      this.LimitationsManager.teamHasReachedMemberLimit = sinon
+    it("doesn't create an invite if the team limit has been reached", async function (ctx) {
+      ctx.LimitationsManager.teamHasReachedMemberLimit = sinon
         .stub()
         .stub()
         .returns(true)
         .returns(true)
       let error
       let error
 
 
       try {
       try {
-        await this.TeamInvitesHandler.promises.createInvite(
-          this.manager._id,
-          this.subscription,
+        await ctx.TeamInvitesHandler.promises.createInvite(
+          ctx.manager._id,
+          ctx.subscription,
           'John.Snow@example.com'
           'John.Snow@example.com'
         )
         )
       } catch (err) {
       } catch (err) {
@@ -596,14 +655,14 @@ describe('TeamInvitesHandler', function () {
       })
       })
     })
     })
 
 
-    it("doesn't create an invite if the subscription is not in a group plan", async function () {
-      this.subscription.groupPlan = false
+    it("doesn't create an invite if the subscription is not in a group plan", async function (ctx) {
+      ctx.subscription.groupPlan = false
       let error
       let error
 
 
       try {
       try {
-        await this.TeamInvitesHandler.promises.createInvite(
-          this.manager._id,
-          this.subscription,
+        await ctx.TeamInvitesHandler.promises.createInvite(
+          ctx.manager._id,
+          ctx.subscription,
           'John.Snow@example.com'
           'John.Snow@example.com'
         )
         )
       } catch (err) {
       } catch (err) {
@@ -617,24 +676,24 @@ describe('TeamInvitesHandler', function () {
       })
       })
     })
     })
 
 
-    it("doesn't create an invite if the user is already part of the team", async function () {
+    it("doesn't create an invite if the user is already part of the team", async function (ctx) {
       const member = {
       const member = {
         id: '1a2b',
         id: '1a2b',
         _id: '1a2b',
         _id: '1a2b',
         email: 'tyrion@example.com',
         email: 'tyrion@example.com',
       }
       }
 
 
-      this.subscription.member_ids = [member.id]
-      this.UserGetter.promises.getUserByAnyEmail
+      ctx.subscription.member_ids = [member.id]
+      ctx.UserGetter.promises.getUserByAnyEmail
         .withArgs(member.email)
         .withArgs(member.email)
         .resolves(member)
         .resolves(member)
 
 
       let error
       let error
 
 
       try {
       try {
-        await this.TeamInvitesHandler.promises.createInvite(
-          this.manager._id,
-          this.subscription,
+        await ctx.TeamInvitesHandler.promises.createInvite(
+          ctx.manager._id,
+          ctx.subscription,
           'tyrion@example.com'
           'tyrion@example.com'
         )
         )
       } catch (err) {
       } catch (err) {

+ 155 - 140
services/web/test/unit/src/Templates/TemplatesManager.test.mjs

@@ -1,236 +1,251 @@
-/* eslint-disable
-    max-len,
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const SandboxedModule = require('sandboxed-module')
-const sinon = require('sinon')
-const { RequestFailedError } = require('@overleaf/fetch-utils')
-const { ReadableString } = require('@overleaf/stream-utils')
+import { beforeEach, describe, it, vi } from 'vitest'
+import sinon from 'sinon'
+import { RequestFailedError } from '@overleaf/fetch-utils'
+import { ReadableString } from '@overleaf/stream-utils'
 
 
 const modulePath = '../../../../app/src/Features/Templates/TemplatesManager'
 const modulePath = '../../../../app/src/Features/Templates/TemplatesManager'
 
 
 describe('TemplatesManager', function () {
 describe('TemplatesManager', function () {
-  beforeEach(function () {
-    this.project_id = 'project-id'
-    this.brandVariationId = 'brand-variation-id'
-    this.compiler = 'pdflatex'
-    this.imageName = 'TL2017'
-    this.mainFile = 'main.tex'
-    this.templateId = 'template-id'
-    this.templateName = 'template name'
-    this.templateVersionId = 'template-version-id'
-    this.user_id = 'user-id'
-    this.dumpPath = `${this.dumpFolder}/${this.uuid}`
-    this.callback = sinon.stub()
-    this.pipeline = sinon.stub().callsFake(async (stream, res) => {
+  beforeEach(async function (ctx) {
+    ctx.project_id = 'project-id'
+    ctx.brandVariationId = 'brand-variation-id'
+    ctx.compiler = 'pdflatex'
+    ctx.imageName = 'TL2017'
+    ctx.mainFile = 'main.tex'
+    ctx.templateId = 'template-id'
+    ctx.templateName = 'template name'
+    ctx.templateVersionId = 'template-version-id'
+    ctx.user_id = 'user-id'
+    ctx.dumpFolder = 'dump/path'
+    ctx.uuid = '1234'
+    ctx.dumpPath = `${ctx.dumpFolder}/${ctx.uuid}`
+    ctx.callback = sinon.stub()
+    ctx.pipeline = sinon.stub().callsFake(async (stream, res) => {
       if (res.callback) res.callback()
       if (res.callback) res.callback()
     })
     })
-    this.request = sinon.stub().returns({
+    ctx.request = sinon.stub().returns({
       pipe() {},
       pipe() {},
       on() {},
       on() {},
       response: {
       response: {
         statusCode: 200,
         statusCode: 200,
       },
       },
     })
     })
-    this.fs = {
+    ctx.fs = {
       promises: { unlink: sinon.stub() },
       promises: { unlink: sinon.stub() },
       unlink: sinon.stub(),
       unlink: sinon.stub(),
       createWriteStream: sinon.stub().returns({ on: sinon.stub().yields() }),
       createWriteStream: sinon.stub().returns({ on: sinon.stub().yields() }),
     }
     }
-    this.ProjectUploadManager = {
+    ctx.ProjectUploadManager = {
       promises: {
       promises: {
         createProjectFromZipArchiveWithName: sinon
         createProjectFromZipArchiveWithName: sinon
           .stub()
           .stub()
-          .resolves({ _id: this.project_id }),
+          .resolves({ _id: ctx.project_id }),
       },
       },
     }
     }
-    this.dumpFolder = 'dump/path'
-    this.ProjectOptionsHandler = {
+    ctx.ProjectOptionsHandler = {
       promises: {
       promises: {
         setCompiler: sinon.stub().resolves(),
         setCompiler: sinon.stub().resolves(),
         setImageName: sinon.stub().resolves(),
         setImageName: sinon.stub().resolves(),
         setBrandVariationId: sinon.stub().resolves(),
         setBrandVariationId: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.uuid = '1234'
-    this.ProjectRootDocManager = {
+    ctx.ProjectRootDocManager = {
       promises: {
       promises: {
         setRootDocFromName: sinon.stub().resolves(),
         setRootDocFromName: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.ProjectDetailsHandler = {
+    ctx.ProjectDetailsHandler = {
       getProjectDescription: sinon.stub(),
       getProjectDescription: sinon.stub(),
-      fixProjectName: sinon.stub().returns(this.templateName),
+      fixProjectName: sinon.stub().returns(ctx.templateName),
     }
     }
-    this.Project = { updateOne: sinon.stub().resolves() }
-    this.mockStream = new ReadableString('{}')
-    this.mockResponse = {
+    ctx.Project = { updateOne: sinon.stub().resolves() }
+    ctx.mockStream = new ReadableString('{}')
+    ctx.mockResponse = {
       status: 200,
       status: 200,
       headers: new Headers({
       headers: new Headers({
         'Content-Length': '2',
         'Content-Length': '2',
         'Content-Type': 'application/json',
         'Content-Type': 'application/json',
       }),
       }),
     }
     }
-    this.FetchUtils = {
+    ctx.FetchUtils = {
       fetchJson: sinon.stub(),
       fetchJson: sinon.stub(),
       fetchStreamWithResponse: sinon.stub().resolves({
       fetchStreamWithResponse: sinon.stub().resolves({
-        stream: this.mockStream,
-        response: this.mockResponse,
+        stream: ctx.mockStream,
+        response: ctx.mockResponse,
       }),
       }),
       RequestFailedError,
       RequestFailedError,
     }
     }
-    this.TemplatesManager = SandboxedModule.require(modulePath, {
-      requires: {
-        '@overleaf/fetch-utils': this.FetchUtils,
-        '../Uploads/ProjectUploadManager': this.ProjectUploadManager,
-        '../Project/ProjectOptionsHandler': this.ProjectOptionsHandler,
-        '../Project/ProjectRootDocManager': this.ProjectRootDocManager,
-        '../Project/ProjectDetailsHandler': this.ProjectDetailsHandler,
-        '../Authentication/SessionManager': (this.SessionManager = {
-          getLoggedInUserId: sinon.stub(),
-        }),
-        '@overleaf/settings': {
-          path: {
-            dumpFolder: this.dumpFolder,
-          },
-          siteUrl: (this.siteUrl = 'http://127.0.0.1:3000'),
-          apis: {
-            v1: {
-              url: (this.v1Url = 'http://overleaf.com'),
-              user: 'overleaf',
-              pass: 'password',
-              timeout: 10,
-            },
-          },
-          overleaf: {
-            host: this.v1Url,
-          },
+    vi.doMock('@overleaf/fetch-utils', () => ctx.FetchUtils)
+    vi.doMock(
+      '../../../../app/src/Features/Uploads/ProjectUploadManager',
+      () => ({ default: ctx.ProjectUploadManager })
+    )
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectOptionsHandler',
+      () => ({ default: ctx.ProjectOptionsHandler })
+    )
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectRootDocManager',
+      () => ({ default: ctx.ProjectRootDocManager })
+    )
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectDetailsHandler',
+      () => ({ default: ctx.ProjectDetailsHandler })
+    )
+
+    ctx.SessionManager = {
+      getLoggedInUserId: sinon.stub(),
+    }
+
+    vi.doMock(
+      '../../../../app/src/Features/Authentication/SessionManager',
+      () => ({ default: ctx.SessionManager })
+    )
+
+    vi.doMock('@overleaf/settings', () => ({
+      default: {
+        path: {
+          dumpFolder: ctx.dumpFolder,
         },
         },
-        crypto: {
-          randomUUID: () => this.uuid,
+        siteUrl: (ctx.siteUrl = 'http://127.0.0.1:3000'),
+        apis: {
+          v1: {
+            url: (ctx.v1Url = 'http://overleaf.com'),
+            user: 'overleaf',
+            pass: 'password',
+            timeout: 10,
+          },
         },
         },
-        request: this.request,
-        fs: this.fs,
-        '../../models/Project': { Project: this.Project },
-        'stream/promises': { pipeline: this.pipeline },
-        '../Compile/ClsiCacheManager': {
-          prepareClsiCache: sinon.stub().rejects(new Error('ignore this')),
+        overleaf: {
+          host: ctx.v1Url,
         },
         },
       },
       },
-    }).promises
-    return (this.zipUrl =
-      '%2Ftemplates%2F52fb86a81ae1e566597a25f6%2Fv%2F4%2Fzip&templateName=Moderncv%20Banking&compiler=pdflatex')
+    }))
+
+    vi.doMock('node:crypto', () => ({
+      default: {
+        randomUUID: () => ctx.uuid,
+      },
+    }))
+
+    vi.doMock('node:fs', () => ({ default: ctx.fs }))
+
+    vi.doMock('request', () => ({ default: ctx.request }))
+
+    vi.doMock('../../../../app/src/models/Project', () => ({
+      Project: ctx.Project,
+    }))
+
+    vi.doMock('node:stream/promises', () => ({ pipeline: ctx.pipeline }))
+
+    vi.doMock('../../../../app/src/Features/Compile/ClsiCacheManager', () => ({
+      default: {
+        prepareClsiCache: sinon.stub().rejects(new Error('ignore this')),
+      },
+    }))
+
+    ctx.TemplatesManager = (await import(modulePath)).default.promises
+    ctx.zipUrl =
+      '%2Ftemplates%2F52fb86a81ae1e566597a25f6%2Fv%2F4%2Fzip&templateName=Moderncv%20Banking&compiler=pdflatex'
   })
   })
 
 
   describe('createProjectFromV1Template', function () {
   describe('createProjectFromV1Template', function () {
     describe('when all options passed', function () {
     describe('when all options passed', function () {
-      beforeEach(function () {
-        return this.TemplatesManager.createProjectFromV1Template(
-          this.brandVariationId,
-          this.compiler,
-          this.mainFile,
-          this.templateId,
-          this.templateName,
-          this.templateVersionId,
-          this.user_id,
-          this.imageName
+      beforeEach(async function (ctx) {
+        await ctx.TemplatesManager.createProjectFromV1Template(
+          ctx.brandVariationId,
+          ctx.compiler,
+          ctx.mainFile,
+          ctx.templateId,
+          ctx.templateName,
+          ctx.templateVersionId,
+          ctx.user_id,
+          ctx.imageName
         )
         )
       })
       })
 
 
-      it('should fetch zip from v1 based on template id', function () {
-        return this.FetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
-          `${this.v1Url}/api/v1/overleaf/templates/${this.templateVersionId}`
+      it('should fetch zip from v1 based on template id', function (ctx) {
+        ctx.FetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
+          `${ctx.v1Url}/api/v1/overleaf/templates/${ctx.templateVersionId}`
         )
         )
       })
       })
 
 
-      it('should save temporary file', function () {
-        return this.fs.createWriteStream.should.have.been.calledWith(
-          this.dumpPath
-        )
+      it('should save temporary file', function (ctx) {
+        ctx.fs.createWriteStream.should.have.been.calledWith(ctx.dumpPath)
       })
       })
 
 
-      it('should create project', function () {
-        return this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName.should.have.been.calledWithMatch(
-          this.user_id,
-          this.templateName,
-          this.dumpPath,
+      it('should create project', function (ctx) {
+        ctx.ProjectUploadManager.promises.createProjectFromZipArchiveWithName.should.have.been.calledWithMatch(
+          ctx.user_id,
+          ctx.templateName,
+          ctx.dumpPath,
           {
           {
-            fromV1TemplateId: this.templateId,
-            fromV1TemplateVersionId: this.templateVersionId,
+            fromV1TemplateId: ctx.templateId,
+            fromV1TemplateVersionId: ctx.templateVersionId,
           }
           }
         )
         )
       })
       })
 
 
-      it('should unlink file', function () {
-        return this.fs.promises.unlink.should.have.been.calledWith(
-          this.dumpPath
-        )
+      it('should unlink file', function (ctx) {
+        ctx.fs.promises.unlink.should.have.been.calledWith(ctx.dumpPath)
       })
       })
 
 
-      it('should set project options when passed', function () {
-        this.ProjectOptionsHandler.promises.setCompiler.should.have.been.calledWithMatch(
-          this.project_id,
-          this.compiler
+      it('should set project options when passed', function (ctx) {
+        ctx.ProjectOptionsHandler.promises.setCompiler.should.have.been.calledWithMatch(
+          ctx.project_id,
+          ctx.compiler
         )
         )
-        this.ProjectOptionsHandler.promises.setImageName.should.have.been.calledWithMatch(
-          this.project_id,
-          this.imageName
+        ctx.ProjectOptionsHandler.promises.setImageName.should.have.been.calledWithMatch(
+          ctx.project_id,
+          ctx.imageName
         )
         )
-        this.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWithMatch(
-          this.project_id,
-          this.mainFile
+        ctx.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWithMatch(
+          ctx.project_id,
+          ctx.mainFile
         )
         )
-        return this.ProjectOptionsHandler.promises.setBrandVariationId.should.have.been.calledWithMatch(
-          this.project_id,
-          this.brandVariationId
+        ctx.ProjectOptionsHandler.promises.setBrandVariationId.should.have.been.calledWithMatch(
+          ctx.project_id,
+          ctx.brandVariationId
         )
         )
       })
       })
 
 
-      it('should update project', function () {
-        return this.Project.updateOne.should.have.been.calledWithMatch(
-          { _id: this.project_id },
+      it('should update project', function (ctx) {
+        ctx.Project.updateOne.should.have.been.calledWithMatch(
+          { _id: ctx.project_id },
           {
           {
-            fromV1TemplateId: this.templateId,
-            fromV1TemplateVersionId: this.templateVersionId,
+            fromV1TemplateId: ctx.templateId,
+            fromV1TemplateVersionId: ctx.templateVersionId,
           }
           }
         )
         )
       })
       })
     })
     })
 
 
     describe('when some options not set', function () {
     describe('when some options not set', function () {
-      beforeEach(function () {
-        return this.TemplatesManager.createProjectFromV1Template(
+      beforeEach(async function (ctx) {
+        await ctx.TemplatesManager.createProjectFromV1Template(
           null,
           null,
           null,
           null,
           null,
           null,
-          this.templateId,
-          this.templateName,
-          this.templateVersionId,
-          this.user_id,
+          ctx.templateId,
+          ctx.templateName,
+          ctx.templateVersionId,
+          ctx.user_id,
           null
           null
         )
         )
       })
       })
 
 
-      it('should not set missing project options', function () {
-        this.ProjectOptionsHandler.promises.setCompiler.called.should.equal(
+      it('should not set missing project options', function (ctx) {
+        ctx.ProjectOptionsHandler.promises.setCompiler.called.should.equal(
           false
           false
         )
         )
-        this.ProjectRootDocManager.promises.setRootDocFromName.called.should.equal(
+        ctx.ProjectRootDocManager.promises.setRootDocFromName.called.should.equal(
           false
           false
         )
         )
-        this.ProjectOptionsHandler.promises.setBrandVariationId.called.should.equal(
+        ctx.ProjectOptionsHandler.promises.setBrandVariationId.called.should.equal(
           false
           false
         )
         )
-        return this.ProjectOptionsHandler.promises.setImageName.should.have.been.calledWithMatch(
-          this.project_id,
+        ctx.ProjectOptionsHandler.promises.setImageName.should.have.been.calledWithMatch(
+          ctx.project_id,
           'wl_texlive:2018.1'
           'wl_texlive:2018.1'
         )
         )
       })
       })

+ 136 - 118
services/web/test/unit/src/ThirdPartyDataStore/TpdsProjectFlusher.test.mjs

@@ -1,176 +1,194 @@
-const { expect } = require('chai')
-const sinon = require('sinon')
-const SandboxedModule = require('sandboxed-module')
-const { ObjectId } = require('mongodb-legacy')
-const { Project } = require('../helpers/models/Project')
+import { vi, expect } from 'vitest'
+import sinon from 'sinon'
+import mongodb from 'mongodb-legacy'
+import indirectlyImportModels from '../helpers/indirectlyImportModels.js'
+
+const { Project } = indirectlyImportModels(['Project'])
+
+const { ObjectId } = mongodb
 
 
 const MODULE_PATH =
 const MODULE_PATH =
   '../../../../app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher'
   '../../../../app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher'
 
 
 describe('TpdsProjectFlusher', function () {
 describe('TpdsProjectFlusher', function () {
-  beforeEach(function () {
-    this.project = { _id: new ObjectId(), overleaf: { history: { id: 42 } } }
-    this.folder = { _id: new ObjectId() }
-    this.docs = {
+  beforeEach(async function (ctx) {
+    ctx.project = { _id: new ObjectId(), overleaf: { history: { id: 42 } } }
+    ctx.folder = { _id: new ObjectId() }
+    ctx.docs = {
       '/doc/one': {
       '/doc/one': {
         _id: 'mock-doc-1',
         _id: 'mock-doc-1',
         lines: ['one'],
         lines: ['one'],
         rev: 5,
         rev: 5,
-        folder: this.folder,
+        folder: ctx.folder,
       },
       },
       '/doc/two': {
       '/doc/two': {
         _id: 'mock-doc-2',
         _id: 'mock-doc-2',
         lines: ['two'],
         lines: ['two'],
         rev: 6,
         rev: 6,
-        folder: this.folder,
+        folder: ctx.folder,
       },
       },
     }
     }
-    this.files = {
+    ctx.files = {
       '/file/one': {
       '/file/one': {
         _id: 'mock-file-1',
         _id: 'mock-file-1',
         rev: 7,
         rev: 7,
-        folder: this.folder,
+        folder: ctx.folder,
         hash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
         hash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
       },
       },
       '/file/two': {
       '/file/two': {
         _id: 'mock-file-2',
         _id: 'mock-file-2',
         rev: 8,
         rev: 8,
-        folder: this.folder,
+        folder: ctx.folder,
         hash: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
         hash: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
       },
       },
     }
     }
-    this.DocumentUpdaterHandler = {
+    ctx.DocumentUpdaterHandler = {
       promises: {
       promises: {
         flushProjectToMongo: sinon.stub().resolves(),
         flushProjectToMongo: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.ProjectGetter = {
+    ctx.ProjectGetter = {
       promises: {
       promises: {
-        getProject: sinon.stub().resolves(this.project),
+        getProject: sinon.stub().resolves(ctx.project),
       },
       },
     }
     }
-    this.ProjectEntityHandler = {
+    ctx.ProjectEntityHandler = {
       promises: {
       promises: {
-        getAllDocs: sinon.stub().withArgs(this.project._id).resolves(this.docs),
-        getAllFiles: sinon
-          .stub()
-          .withArgs(this.project._id)
-          .resolves(this.files),
+        getAllDocs: sinon.stub().withArgs(ctx.project._id).resolves(ctx.docs),
+        getAllFiles: sinon.stub().withArgs(ctx.project._id).resolves(ctx.files),
       },
       },
     }
     }
-    this.TpdsUpdateSender = {
+    ctx.TpdsUpdateSender = {
       promises: {
       promises: {
         addDoc: sinon.stub().resolves(),
         addDoc: sinon.stub().resolves(),
         addFile: sinon.stub().resolves(),
         addFile: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.ProjectMock = sinon.mock(Project)
+    ctx.ProjectMock = sinon.mock(Project)
 
 
-    this.TpdsProjectFlusher = SandboxedModule.require(MODULE_PATH, {
-      requires: {
-        '../DocumentUpdater/DocumentUpdaterHandler':
-          this.DocumentUpdaterHandler,
-        '../Project/ProjectGetter': this.ProjectGetter,
-        '../Project/ProjectEntityHandler': this.ProjectEntityHandler,
-        '../../models/Project': { Project },
-        './TpdsUpdateSender': this.TpdsUpdateSender,
-      },
-    })
+    vi.doMock(
+      '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler',
+      () => ({
+        default: ctx.DocumentUpdaterHandler,
+      })
+    )
+
+    vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
+      default: ctx.ProjectGetter,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectEntityHandler',
+      () => ({
+        default: ctx.ProjectEntityHandler,
+      })
+    )
+
+    vi.doMock('../../../../app/src/models/Project', () => ({
+      Project,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender',
+      () => ({
+        default: ctx.TpdsUpdateSender,
+      })
+    )
+
+    ctx.TpdsProjectFlusher = (await import(MODULE_PATH)).default
   })
   })
 
 
-  afterEach(function () {
-    this.ProjectMock.restore()
+  afterEach(function (ctx) {
+    ctx.ProjectMock.restore()
   })
   })
 
 
   describe('flushProjectToTpds', function () {
   describe('flushProjectToTpds', function () {
     describe('usually', function () {
     describe('usually', function () {
-      beforeEach(async function () {
-        await this.TpdsProjectFlusher.promises.flushProjectToTpds(
-          this.project._id
+      beforeEach(async function (ctx) {
+        await ctx.TpdsProjectFlusher.promises.flushProjectToTpds(
+          ctx.project._id
         )
         )
       })
       })
 
 
-      it('should flush the project from the doc updater', function () {
+      it('should flush the project from the doc updater', function (ctx) {
         expect(
         expect(
-          this.DocumentUpdaterHandler.promises.flushProjectToMongo
-        ).to.have.been.calledWith(this.project._id)
+          ctx.DocumentUpdaterHandler.promises.flushProjectToMongo
+        ).to.have.been.calledWith(ctx.project._id)
+      })
+
+      it('should flush each doc to the TPDS', function (ctx) {
+        for (const [path, doc] of Object.entries(ctx.docs)) {
+          expect(ctx.TpdsUpdateSender.promises.addDoc).to.have.been.calledWith({
+            projectId: ctx.project._id,
+            docId: doc._id,
+            projectName: ctx.project.name,
+            rev: doc.rev,
+            path,
+            folderId: ctx.folder._id,
+          })
+        }
       })
       })
 
 
-      it('should flush each doc to the TPDS', function () {
-        for (const [path, doc] of Object.entries(this.docs)) {
-          expect(this.TpdsUpdateSender.promises.addDoc).to.have.been.calledWith(
+      it('should flush each file to the TPDS', function (ctx) {
+        for (const [path, file] of Object.entries(ctx.files)) {
+          expect(ctx.TpdsUpdateSender.promises.addFile).to.have.been.calledWith(
             {
             {
-              projectId: this.project._id,
-              docId: doc._id,
-              projectName: this.project.name,
-              rev: doc.rev,
+              projectId: ctx.project._id,
+              historyId: ctx.project.overleaf.history.id,
+              fileId: file._id,
+              hash: file.hash,
+              projectName: ctx.project.name,
+              rev: file.rev,
               path,
               path,
-              folderId: this.folder._id,
+              folderId: ctx.folder._id,
             }
             }
           )
           )
         }
         }
       })
       })
-
-      it('should flush each file to the TPDS', function () {
-        for (const [path, file] of Object.entries(this.files)) {
-          expect(
-            this.TpdsUpdateSender.promises.addFile
-          ).to.have.been.calledWith({
-            projectId: this.project._id,
-            historyId: this.project.overleaf.history.id,
-            fileId: file._id,
-            hash: file.hash,
-            projectName: this.project.name,
-            rev: file.rev,
-            path,
-            folderId: this.folder._id,
-          })
-        }
-      })
     })
     })
 
 
     describe('when a TPDS flush is pending', function () {
     describe('when a TPDS flush is pending', function () {
-      beforeEach(async function () {
-        this.project.deferredTpdsFlushCounter = 2
-        this.ProjectMock.expects('updateOne')
+      beforeEach(async function (ctx) {
+        ctx.project.deferredTpdsFlushCounter = 2
+        ctx.ProjectMock.expects('updateOne')
           .withArgs(
           .withArgs(
             {
             {
-              _id: this.project._id,
+              _id: ctx.project._id,
               deferredTpdsFlushCounter: { $lte: 2 },
               deferredTpdsFlushCounter: { $lte: 2 },
             },
             },
             { $set: { deferredTpdsFlushCounter: 0 } }
             { $set: { deferredTpdsFlushCounter: 0 } }
           )
           )
           .chain('exec')
           .chain('exec')
           .resolves()
           .resolves()
-        await this.TpdsProjectFlusher.promises.flushProjectToTpds(
-          this.project._id
+        await ctx.TpdsProjectFlusher.promises.flushProjectToTpds(
+          ctx.project._id
         )
         )
       })
       })
 
 
-      it('resets the deferred flush counter', function () {
-        this.ProjectMock.verify()
+      it('resets the deferred flush counter', function (ctx) {
+        ctx.ProjectMock.verify()
       })
       })
     })
     })
   })
   })
 
 
   describe('deferProjectFlushToTpds', function () {
   describe('deferProjectFlushToTpds', function () {
-    beforeEach(async function () {
-      this.ProjectMock.expects('updateOne')
+    beforeEach(async function (ctx) {
+      ctx.ProjectMock.expects('updateOne')
         .withArgs(
         .withArgs(
           {
           {
-            _id: this.project._id,
+            _id: ctx.project._id,
           },
           },
           { $inc: { deferredTpdsFlushCounter: 1 } }
           { $inc: { deferredTpdsFlushCounter: 1 } }
         )
         )
         .chain('exec')
         .chain('exec')
         .resolves()
         .resolves()
-      await this.TpdsProjectFlusher.promises.deferProjectFlushToTpds(
-        this.project._id
+      await ctx.TpdsProjectFlusher.promises.deferProjectFlushToTpds(
+        ctx.project._id
       )
       )
     })
     })
 
 
-    it('increments the deferred flush counter', function () {
-      this.ProjectMock.verify()
+    it('increments the deferred flush counter', function (ctx) {
+      ctx.ProjectMock.verify()
     })
     })
   })
   })
 
 
@@ -178,24 +196,24 @@ describe('TpdsProjectFlusher', function () {
     let cases = [0, undefined]
     let cases = [0, undefined]
     cases.forEach(counterValue => {
     cases.forEach(counterValue => {
       describe(`when the deferred flush counter is ${counterValue}`, function () {
       describe(`when the deferred flush counter is ${counterValue}`, function () {
-        beforeEach(async function () {
-          this.project.deferredTpdsFlushCounter = counterValue
-          await this.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded(
-            this.project._id
+        beforeEach(async function (ctx) {
+          ctx.project.deferredTpdsFlushCounter = counterValue
+          await ctx.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded(
+            ctx.project._id
           )
           )
         })
         })
 
 
-        it("doesn't flush the project from the doc updater", function () {
-          expect(this.DocumentUpdaterHandler.promises.flushProjectToMongo).not
-            .to.have.been.called
+        it("doesn't flush the project from the doc updater", function (ctx) {
+          expect(ctx.DocumentUpdaterHandler.promises.flushProjectToMongo).not.to
+            .have.been.called
         })
         })
 
 
-        it("doesn't flush any doc", function () {
-          expect(this.TpdsUpdateSender.promises.addDoc).not.to.have.been.called
+        it("doesn't flush any doc", function (ctx) {
+          expect(ctx.TpdsUpdateSender.promises.addDoc).not.to.have.been.called
         })
         })
 
 
-        it("doesn't flush any file", function () {
-          expect(this.TpdsUpdateSender.promises.addFile).not.to.have.been.called
+        it("doesn't flush any file", function (ctx) {
+          expect(ctx.TpdsUpdateSender.promises.addFile).not.to.have.been.called
         })
         })
       })
       })
     })
     })
@@ -203,63 +221,63 @@ describe('TpdsProjectFlusher', function () {
     cases = [1, 2]
     cases = [1, 2]
     cases.forEach(counterValue => {
     cases.forEach(counterValue => {
       describe(`when the deferred flush counter is ${counterValue}`, function () {
       describe(`when the deferred flush counter is ${counterValue}`, function () {
-        beforeEach(async function () {
-          this.project.deferredTpdsFlushCounter = counterValue
-          this.ProjectMock.expects('updateOne')
+        beforeEach(async function (ctx) {
+          ctx.project.deferredTpdsFlushCounter = counterValue
+          ctx.ProjectMock.expects('updateOne')
             .withArgs(
             .withArgs(
               {
               {
-                _id: this.project._id,
+                _id: ctx.project._id,
                 deferredTpdsFlushCounter: { $lte: counterValue },
                 deferredTpdsFlushCounter: { $lte: counterValue },
               },
               },
               { $set: { deferredTpdsFlushCounter: 0 } }
               { $set: { deferredTpdsFlushCounter: 0 } }
             )
             )
             .chain('exec')
             .chain('exec')
             .resolves()
             .resolves()
-          await this.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded(
-            this.project._id
+          await ctx.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded(
+            ctx.project._id
           )
           )
         })
         })
 
 
-        it('flushes the project from the doc updater', function () {
+        it('flushes the project from the doc updater', function (ctx) {
           expect(
           expect(
-            this.DocumentUpdaterHandler.promises.flushProjectToMongo
-          ).to.have.been.calledWith(this.project._id)
+            ctx.DocumentUpdaterHandler.promises.flushProjectToMongo
+          ).to.have.been.calledWith(ctx.project._id)
         })
         })
 
 
-        it('flushes each doc to the TPDS', function () {
-          for (const [path, doc] of Object.entries(this.docs)) {
+        it('flushes each doc to the TPDS', function (ctx) {
+          for (const [path, doc] of Object.entries(ctx.docs)) {
             expect(
             expect(
-              this.TpdsUpdateSender.promises.addDoc
+              ctx.TpdsUpdateSender.promises.addDoc
             ).to.have.been.calledWith({
             ).to.have.been.calledWith({
-              projectId: this.project._id,
+              projectId: ctx.project._id,
               docId: doc._id,
               docId: doc._id,
-              projectName: this.project.name,
+              projectName: ctx.project.name,
               rev: doc.rev,
               rev: doc.rev,
               path,
               path,
-              folderId: this.folder._id,
+              folderId: ctx.folder._id,
             })
             })
           }
           }
         })
         })
 
 
-        it('flushes each file to the TPDS', function () {
-          for (const [path, file] of Object.entries(this.files)) {
+        it('flushes each file to the TPDS', function (ctx) {
+          for (const [path, file] of Object.entries(ctx.files)) {
             expect(
             expect(
-              this.TpdsUpdateSender.promises.addFile
+              ctx.TpdsUpdateSender.promises.addFile
             ).to.have.been.calledWith({
             ).to.have.been.calledWith({
-              projectId: this.project._id,
-              historyId: this.project.overleaf.history.id,
+              projectId: ctx.project._id,
+              historyId: ctx.project.overleaf.history.id,
               fileId: file._id,
               fileId: file._id,
               hash: file.hash,
               hash: file.hash,
-              projectName: this.project.name,
+              projectName: ctx.project.name,
               rev: file.rev,
               rev: file.rev,
               path,
               path,
-              folderId: this.folder._id,
+              folderId: ctx.folder._id,
             })
             })
           }
           }
         })
         })
 
 
-        it('resets the deferred flush counter', function () {
-          this.ProjectMock.verify()
+        it('resets the deferred flush counter', function (ctx) {
+          ctx.ProjectMock.verify()
         })
         })
       })
       })
     })
     })

+ 111 - 93
services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateSender.test.mjs

@@ -1,12 +1,13 @@
-const { ObjectId } = require('mongodb-legacy')
-const SandboxedModule = require('sandboxed-module')
-const path = require('path')
-const sinon = require('sinon')
-const { expect } = require('chai')
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import mongodb from 'mongodb-legacy'
+import path from 'path'
+import sinon from 'sinon'
+
+const { ObjectId } = mongodb
 
 
 const modulePath = path.join(
 const modulePath = path.join(
-  __dirname,
-  '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js'
+  import.meta.dirname,
+  '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.mjs'
 )
 )
 
 
 const projectId = 'project_id_here'
 const projectId = 'project_id_here'
@@ -21,29 +22,29 @@ const filestoreUrl = 'filestore.overleaf.com'
 const projectHistoryUrl = 'http://project-history:3054'
 const projectHistoryUrl = 'http://project-history:3054'
 
 
 describe('TpdsUpdateSender', function () {
 describe('TpdsUpdateSender', function () {
-  beforeEach(function () {
-    this.fakeUser = {
+  beforeEach(async function (ctx) {
+    ctx.fakeUser = {
       _id: '12390i',
       _id: '12390i',
     }
     }
-    this.memberIds = [userId, collaberatorRef, readOnlyRef]
-    this.enqueueUrl = new URL(
+    ctx.memberIds = [userId, collaberatorRef, readOnlyRef]
+    ctx.enqueueUrl = new URL(
       'http://tpdsworker/enqueue/web_to_tpds_http_requests'
       'http://tpdsworker/enqueue/web_to_tpds_http_requests'
     )
     )
 
 
-    this.CollaboratorsGetter = {
+    ctx.CollaboratorsGetter = {
       promises: {
       promises: {
-        getInvitedMemberIds: sinon.stub().resolves(this.memberIds),
+        getInvitedMemberIds: sinon.stub().resolves(ctx.memberIds),
       },
       },
     }
     }
-    this.docstoreUrl = 'docstore.overleaf.env'
-    this.response = {
+    ctx.docstoreUrl = 'docstore.overleaf.env'
+    ctx.response = {
       ok: true,
       ok: true,
       json: sinon.stub(),
       json: sinon.stub(),
     }
     }
-    this.FetchUtils = {
+    ctx.FetchUtils = {
       fetchNothing: sinon.stub().resolves(),
       fetchNothing: sinon.stub().resolves(),
     }
     }
-    this.settings = {
+    ctx.settings = {
       siteUrl,
       siteUrl,
       apis: {
       apis: {
         thirdPartyDataStore: { url: thirdPartyDataStoreApiUrl },
         thirdPartyDataStore: { url: thirdPartyDataStoreApiUrl },
@@ -51,7 +52,7 @@ describe('TpdsUpdateSender', function () {
           url: filestoreUrl,
           url: filestoreUrl,
         },
         },
         docstore: {
         docstore: {
-          pubUrl: this.docstoreUrl,
+          pubUrl: ctx.docstoreUrl,
         },
         },
         project_history: {
         project_history: {
           url: projectHistoryUrl,
           url: projectHistoryUrl,
@@ -62,46 +63,63 @@ describe('TpdsUpdateSender', function () {
     getUsers
     getUsers
       .withArgs({
       .withArgs({
         _id: {
         _id: {
-          $in: this.memberIds,
+          $in: ctx.memberIds,
         },
         },
         'dropbox.access_token.uid': { $ne: null },
         'dropbox.access_token.uid': { $ne: null },
       })
       })
       .resolves(
       .resolves(
-        this.memberIds.map(userId => {
+        ctx.memberIds.map(userId => {
           return { _id: userId }
           return { _id: userId }
         })
         })
       )
       )
-    this.UserGetter = {
+    ctx.UserGetter = {
       promises: { getUsers },
       promises: { getUsers },
     }
     }
-    this.TpdsUpdateSender = SandboxedModule.require(modulePath, {
-      requires: {
-        'mongodb-legacy': { ObjectId },
-        '@overleaf/settings': this.settings,
-        '@overleaf/fetch-utils': this.FetchUtils,
-        '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
-        '../User/UserGetter.js': this.UserGetter,
-        '@overleaf/metrics': {
-          inc() {},
-        },
+
+    vi.doMock('mongodb-legacy', () => ({
+      default: { ObjectId },
+    }))
+
+    vi.doMock('@overleaf/settings', () => ({
+      default: ctx.settings,
+    }))
+
+    vi.doMock('@overleaf/fetch-utils', () => ctx.FetchUtils)
+
+    vi.doMock(
+      '../../../../app/src/Features/Collaborators/CollaboratorsGetter',
+      () => ({
+        default: ctx.CollaboratorsGetter,
+      })
+    )
+
+    vi.doMock('../../../../app/src/Features/User/UserGetter.js', () => ({
+      default: ctx.UserGetter,
+    }))
+
+    vi.doMock('@overleaf/metrics', () => ({
+      default: {
+        inc() {},
       },
       },
-    })
+    }))
+
+    ctx.TpdsUpdateSender = (await import(modulePath)).default
   })
   })
 
 
   describe('enqueue', function () {
   describe('enqueue', function () {
-    it('should not call request if there is no tpdsworker url', async function () {
-      await this.TpdsUpdateSender.promises.enqueue(null, null, null)
-      this.FetchUtils.fetchNothing.should.not.have.been.called
+    it('should not call request if there is no tpdsworker url', async function (ctx) {
+      await ctx.TpdsUpdateSender.promises.enqueue(null, null, null)
+      ctx.FetchUtils.fetchNothing.should.not.have.been.called
     })
     })
 
 
-    it('should post the message to the tpdsworker', async function () {
-      this.settings.apis.tpdsworker = { url: 'http://tpdsworker' }
+    it('should post the message to the tpdsworker', async function (ctx) {
+      ctx.settings.apis.tpdsworker = { url: 'http://tpdsworker' }
       const group0 = 'myproject'
       const group0 = 'myproject'
       const method0 = 'somemethod0'
       const method0 = 'somemethod0'
       const job0 = 'do something'
       const job0 = 'do something'
-      await this.TpdsUpdateSender.promises.enqueue(group0, method0, job0)
-      this.FetchUtils.fetchNothing.should.have.been.calledWithMatch(
-        this.enqueueUrl,
+      await ctx.TpdsUpdateSender.promises.enqueue(group0, method0, job0)
+      ctx.FetchUtils.fetchNothing.should.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           method: 'POST',
           method: 'POST',
           json: { group: group0, job: job0, method: method0 },
           json: { group: group0, job: job0, method: method0 },
@@ -111,17 +129,17 @@ describe('TpdsUpdateSender', function () {
   })
   })
 
 
   describe('sending updates', function () {
   describe('sending updates', function () {
-    beforeEach(function () {
-      this.settings.apis.tpdsworker = { url: 'http://tpdsworker' }
+    beforeEach(function (ctx) {
+      ctx.settings.apis.tpdsworker = { url: 'http://tpdsworker' }
     })
     })
 
 
-    it('queues a post the file with user and file id and hash', async function () {
+    it('queues a post the file with user and file id and hash', async function (ctx) {
       const fileId = '4545345'
       const fileId = '4545345'
       const hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
       const hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
       const historyId = 91525
       const historyId = 91525
       const path = '/some/path/here.jpg'
       const path = '/some/path/here.jpg'
 
 
-      await this.TpdsUpdateSender.promises.addFile({
+      await ctx.TpdsUpdateSender.promises.addFile({
         projectId,
         projectId,
         historyId,
         historyId,
         fileId,
         fileId,
@@ -130,8 +148,8 @@ describe('TpdsUpdateSender', function () {
         projectName,
         projectName,
       })
       })
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: userId,
             group: userId,
@@ -148,8 +166,8 @@ describe('TpdsUpdateSender', function () {
         }
         }
       )
       )
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: collaberatorRef,
             group: collaberatorRef,
@@ -157,8 +175,8 @@ describe('TpdsUpdateSender', function () {
         }
         }
       )
       )
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: readOnlyRef,
             group: readOnlyRef,
@@ -168,12 +186,12 @@ describe('TpdsUpdateSender', function () {
       )
       )
     })
     })
 
 
-    it('post doc with stream origin of docstore', async function () {
+    it('post doc with stream origin of docstore', async function (ctx) {
       const docId = '4545345'
       const docId = '4545345'
       const path = '/some/path/here.tex'
       const path = '/some/path/here.tex'
       const lines = ['line1', 'line2', 'line3']
       const lines = ['line1', 'line2', 'line3']
 
 
-      await this.TpdsUpdateSender.promises.addDoc({
+      await ctx.TpdsUpdateSender.promises.addDoc({
         projectId,
         projectId,
         docId,
         docId,
         path,
         path,
@@ -181,8 +199,8 @@ describe('TpdsUpdateSender', function () {
         projectName,
         projectName,
       })
       })
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: userId,
             group: userId,
@@ -192,15 +210,15 @@ describe('TpdsUpdateSender', function () {
               uri: `${thirdPartyDataStoreApiUrl}/user/${userId}/entity/${encodeURIComponent(
               uri: `${thirdPartyDataStoreApiUrl}/user/${userId}/entity/${encodeURIComponent(
                 projectName
                 projectName
               )}${encodeURIComponent(path)}`,
               )}${encodeURIComponent(path)}`,
-              streamOrigin: `${this.docstoreUrl}/project/${projectId}/doc/${docId}/raw`,
+              streamOrigin: `${ctx.docstoreUrl}/project/${projectId}/doc/${docId}/raw`,
               headers: {},
               headers: {},
             },
             },
           },
           },
         }
         }
       )
       )
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: collaberatorRef,
             group: collaberatorRef,
@@ -211,8 +229,8 @@ describe('TpdsUpdateSender', function () {
         }
         }
       )
       )
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: readOnlyRef,
             group: readOnlyRef,
@@ -224,19 +242,19 @@ describe('TpdsUpdateSender', function () {
       )
       )
     })
     })
 
 
-    it('deleting entity', async function () {
+    it('deleting entity', async function (ctx) {
       const path = '/path/here/t.tex'
       const path = '/path/here/t.tex'
       const subtreeEntityIds = ['id1', 'id2']
       const subtreeEntityIds = ['id1', 'id2']
 
 
-      await this.TpdsUpdateSender.promises.deleteEntity({
+      await ctx.TpdsUpdateSender.promises.deleteEntity({
         projectId,
         projectId,
         path,
         path,
         projectName,
         projectName,
         subtreeEntityIds,
         subtreeEntityIds,
       })
       })
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: userId,
             group: userId,
@@ -253,8 +271,8 @@ describe('TpdsUpdateSender', function () {
         }
         }
       )
       )
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: collaberatorRef,
             group: collaberatorRef,
@@ -265,8 +283,8 @@ describe('TpdsUpdateSender', function () {
         }
         }
       )
       )
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: readOnlyRef,
             group: readOnlyRef,
@@ -278,19 +296,19 @@ describe('TpdsUpdateSender', function () {
       )
       )
     })
     })
 
 
-    it('moving entity', async function () {
+    it('moving entity', async function (ctx) {
       const startPath = 'staring/here/file.tex'
       const startPath = 'staring/here/file.tex'
       const endPath = 'ending/here/file.tex'
       const endPath = 'ending/here/file.tex'
 
 
-      await this.TpdsUpdateSender.promises.moveEntity({
+      await ctx.TpdsUpdateSender.promises.moveEntity({
         projectId,
         projectId,
         startPath,
         startPath,
         endPath,
         endPath,
         projectName,
         projectName,
       })
       })
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: userId,
             group: userId,
@@ -308,8 +326,8 @@ describe('TpdsUpdateSender', function () {
         }
         }
       )
       )
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: collaberatorRef,
             group: collaberatorRef,
@@ -320,8 +338,8 @@ describe('TpdsUpdateSender', function () {
         }
         }
       )
       )
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: readOnlyRef,
             group: readOnlyRef,
@@ -333,18 +351,18 @@ describe('TpdsUpdateSender', function () {
       )
       )
     })
     })
 
 
-    it('should be able to rename a project using the move entity func', async function () {
+    it('should be able to rename a project using the move entity func', async function (ctx) {
       const oldProjectName = '/oldProjectName/'
       const oldProjectName = '/oldProjectName/'
       const newProjectName = '/newProjectName/'
       const newProjectName = '/newProjectName/'
 
 
-      await this.TpdsUpdateSender.promises.moveEntity({
+      await ctx.TpdsUpdateSender.promises.moveEntity({
         projectId,
         projectId,
         projectName: oldProjectName,
         projectName: oldProjectName,
         newProjectName,
         newProjectName,
       })
       })
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: userId,
             group: userId,
@@ -362,8 +380,8 @@ describe('TpdsUpdateSender', function () {
         }
         }
       )
       )
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: collaberatorRef,
             group: collaberatorRef,
@@ -374,8 +392,8 @@ describe('TpdsUpdateSender', function () {
         }
         }
       )
       )
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: readOnlyRef,
             group: readOnlyRef,
@@ -387,11 +405,11 @@ describe('TpdsUpdateSender', function () {
       )
       )
     })
     })
 
 
-    it('pollDropboxForUser', async function () {
-      await this.TpdsUpdateSender.promises.pollDropboxForUser(userId)
+    it('pollDropboxForUser', async function (ctx) {
+      await ctx.TpdsUpdateSender.promises.pollDropboxForUser(userId)
 
 
-      expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
-        this.enqueueUrl,
+      expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
+        ctx.enqueueUrl,
         {
         {
           json: {
           json: {
             group: userId,
             group: userId,
@@ -410,24 +428,24 @@ describe('TpdsUpdateSender', function () {
   })
   })
 
 
   describe('user not linked to dropbox', function () {
   describe('user not linked to dropbox', function () {
-    beforeEach(function () {
-      this.UserGetter.promises.getUsers
+    beforeEach(function (ctx) {
+      ctx.UserGetter.promises.getUsers
         .withArgs({
         .withArgs({
           _id: {
           _id: {
-            $in: this.memberIds,
+            $in: ctx.memberIds,
           },
           },
           'dropbox.access_token.uid': { $ne: null },
           'dropbox.access_token.uid': { $ne: null },
         })
         })
         .resolves([])
         .resolves([])
     })
     })
 
 
-    it('does not make request to tpds', async function () {
+    it('does not make request to tpds', async function (ctx) {
       const fileId = '4545345'
       const fileId = '4545345'
       const hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
       const hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
       const historyId = 91525
       const historyId = 91525
       const path = '/some/path/here.jpg'
       const path = '/some/path/here.jpg'
 
 
-      await this.TpdsUpdateSender.promises.addFile({
+      await ctx.TpdsUpdateSender.promises.addFile({
         projectId,
         projectId,
         historyId,
         historyId,
         hash,
         hash,
@@ -435,7 +453,7 @@ describe('TpdsUpdateSender', function () {
         path,
         path,
         projectName,
         projectName,
       })
       })
-      this.FetchUtils.fetchNothing.should.not.have.been.called
+      ctx.FetchUtils.fetchNothing.should.not.have.been.called
     })
     })
   })
   })
 })
 })

+ 11 - 15
services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs

@@ -236,12 +236,13 @@ describe('TokenAccessController', function () {
       }),
       }),
     }))
     }))
 
 
+    ctx.AdminAuthorizationHelper = {
+      canRedirectToAdminDomain: sinon.stub(),
+    }
+
     vi.doMock(
     vi.doMock(
       '../../../../app/src/Features/Helpers/AdminAuthorizationHelper',
       '../../../../app/src/Features/Helpers/AdminAuthorizationHelper',
-      () =>
-        (ctx.AdminAuthorizationHelper = {
-          canRedirectToAdminDomain: sinon.stub(),
-        })
+      () => ({ default: ctx.AdminAuthorizationHelper })
     )
     )
 
 
     vi.doMock(
     vi.doMock(
@@ -764,17 +765,12 @@ describe('TokenAccessController', function () {
           .stub()
           .stub()
           .resolves([{ email: 'test@not-overleaf.com' }])
           .resolves([{ email: 'test@not-overleaf.com' }])
 
 
-        await new Promise(resolve => {
-          ctx.res.callback = () => {
-            expect(ctx.res.json).to.have.been.calledWith({
-              redirect: `${ctx.Settings.adminUrl}/#prefix`,
-            })
-            resolve()
-          }
-          ctx.TokenAccessController.grantTokenAccessReadAndWrite(
-            ctx.req,
-            ctx.res
-          )
+        await ctx.TokenAccessController.grantTokenAccessReadAndWrite(
+          ctx.req,
+          ctx.res
+        )
+        expect(ctx.res.json).to.have.been.calledWith({
+          redirect: `${ctx.Settings.adminUrl}/#prefix`,
         })
         })
       })
       })
 
 

+ 123 - 118
services/web/test/unit/src/Uploads/FileSystemImportManager.test.mjs

@@ -1,48 +1,53 @@
-const sinon = require('sinon')
-const { expect } = require('chai')
-const mockFs = require('mock-fs')
-const SandboxedModule = require('sandboxed-module')
-const { ObjectId } = require('mongodb-legacy')
-const Settings = require('@overleaf/settings')
+import { vi, expect } from 'vitest'
+import sinon from 'sinon'
+import mockFs from 'mock-fs'
+import mongodb from 'mongodb-legacy'
+import Settings from '@overleaf/settings'
+
+const { ObjectId } = mongodb
 
 
 const MODULE_PATH =
 const MODULE_PATH =
-  '../../../../app/src/Features/Uploads/FileSystemImportManager.js'
+  '../../../../app/src/Features/Uploads/FileSystemImportManager.mjs'
 
 
 describe('FileSystemImportManager', function () {
 describe('FileSystemImportManager', function () {
-  beforeEach(function () {
-    this.projectId = new ObjectId()
-    this.folderId = new ObjectId()
-    this.newFolderId = new ObjectId()
-    this.userId = new ObjectId()
+  beforeEach(async function (ctx) {
+    ctx.projectId = new ObjectId()
+    ctx.folderId = new ObjectId()
+    ctx.newFolderId = new ObjectId()
+    ctx.userId = new ObjectId()
 
 
-    this.EditorController = {
+    ctx.EditorController = {
       promises: {
       promises: {
         addDoc: sinon.stub().resolves(),
         addDoc: sinon.stub().resolves(),
         addFile: sinon.stub().resolves(),
         addFile: sinon.stub().resolves(),
         upsertDoc: sinon.stub().resolves(),
         upsertDoc: sinon.stub().resolves(),
         upsertFile: sinon.stub().resolves(),
         upsertFile: sinon.stub().resolves(),
-        addFolder: sinon.stub().resolves({ _id: this.newFolderId }),
+        addFolder: sinon.stub().resolves({ _id: ctx.newFolderId }),
       },
       },
     }
     }
-    this.FileSystemImportManager = SandboxedModule.require(MODULE_PATH, {
-      requires: {
-        '@overleaf/settings': {
-          textExtensions: ['tex', 'txt'],
-          editableFilenames: [
-            'latexmkrc',
-            '.latexmkrc',
-            'makefile',
-            'gnumakefile',
-          ],
-          fileIgnorePattern: Settings.fileIgnorePattern, // use the real pattern from the default settings
-        },
-        '../Editor/EditorController': this.EditorController,
+
+    vi.doMock('@overleaf/settings', () => ({
+      default: {
+        textExtensions: ['tex', 'txt'],
+        editableFilenames: [
+          'latexmkrc',
+          '.latexmkrc',
+          'makefile',
+          'gnumakefile',
+        ],
+        fileIgnorePattern: Settings.fileIgnorePattern, // use the real pattern from the default settings
       },
       },
-    })
+    }))
+
+    vi.doMock('../../../../app/src/Features/Editor/EditorController', () => ({
+      default: ctx.EditorController,
+    }))
+
+    ctx.FileSystemImportManager = (await import(MODULE_PATH)).default
   })
   })
 
 
   describe('importDir', function () {
   describe('importDir', function () {
-    beforeEach(async function () {
+    beforeEach(async function (ctx) {
       mockFs({
       mockFs({
         'import-test': {
         'import-test': {
           'main.tex': 'My thesis',
           'main.tex': 'My thesis',
@@ -64,87 +69,87 @@ describe('FileSystemImportManager', function () {
         },
         },
         symlink: mockFs.symlink({ path: 'import-test' }),
         symlink: mockFs.symlink({ path: 'import-test' }),
       })
       })
-      this.entries =
-        await this.FileSystemImportManager.promises.importDir('import-test')
-      this.projectPaths = this.entries.map(x => x.projectPath)
+      ctx.entries =
+        await ctx.FileSystemImportManager.promises.importDir('import-test')
+      ctx.projectPaths = ctx.entries.map(x => x.projectPath)
     })
     })
 
 
     afterEach(function () {
     afterEach(function () {
       mockFs.restore()
       mockFs.restore()
     })
     })
 
 
-    it('should import regular docs', function () {
-      expect(this.entries).to.deep.include({
+    it('should import regular docs', function (ctx) {
+      expect(ctx.entries).to.deep.include({
         type: 'doc',
         type: 'doc',
         projectPath: '/main.tex',
         projectPath: '/main.tex',
         lines: ['My thesis'],
         lines: ['My thesis'],
       })
       })
     })
     })
 
 
-    it('should skip symlinks inside the import folder', function () {
-      expect(this.projectPaths).not.to.include('/link-to-main.tex')
+    it('should skip symlinks inside the import folder', function (ctx) {
+      expect(ctx.projectPaths).not.to.include('/link-to-main.tex')
     })
     })
 
 
-    it('should skip ignored files', function () {
-      expect(this.projectPaths).not.to.include('/.DS_Store')
+    it('should skip ignored files', function (ctx) {
+      expect(ctx.projectPaths).not.to.include('/.DS_Store')
     })
     })
 
 
-    it('should import binary files', function () {
-      expect(this.entries).to.deep.include({
+    it('should import binary files', function (ctx) {
+      expect(ctx.entries).to.deep.include({
         type: 'file',
         type: 'file',
         projectPath: '/images/cat.jpg',
         projectPath: '/images/cat.jpg',
         fsPath: 'import-test/images/cat.jpg',
         fsPath: 'import-test/images/cat.jpg',
       })
       })
     })
     })
 
 
-    it('should deal with Mac/Windows/Unix line endings', function () {
-      expect(this.entries).to.deep.include({
+    it('should deal with Mac/Windows/Unix line endings', function (ctx) {
+      expect(ctx.entries).to.deep.include({
         type: 'doc',
         type: 'doc',
         projectPath: '/line-endings/unix.txt',
         projectPath: '/line-endings/unix.txt',
         lines: ['one', 'two', 'three'],
         lines: ['one', 'two', 'three'],
       })
       })
-      expect(this.entries).to.deep.include({
+      expect(ctx.entries).to.deep.include({
         type: 'doc',
         type: 'doc',
         projectPath: '/line-endings/mac.txt',
         projectPath: '/line-endings/mac.txt',
         lines: ['uno', 'dos', 'tres'],
         lines: ['uno', 'dos', 'tres'],
       })
       })
-      expect(this.entries).to.deep.include({
+      expect(ctx.entries).to.deep.include({
         type: 'doc',
         type: 'doc',
         projectPath: '/line-endings/windows.txt',
         projectPath: '/line-endings/windows.txt',
         lines: ['ein', 'zwei', 'drei'],
         lines: ['ein', 'zwei', 'drei'],
       })
       })
-      expect(this.entries).to.deep.include({
+      expect(ctx.entries).to.deep.include({
         type: 'doc',
         type: 'doc',
         projectPath: '/line-endings/mixed.txt',
         projectPath: '/line-endings/mixed.txt',
         lines: ['uno', 'due', 'tre', 'quattro'],
         lines: ['uno', 'due', 'tre', 'quattro'],
       })
       })
     })
     })
 
 
-    it('should import documents with latin1 encoding', function () {
-      expect(this.entries).to.deep.include({
+    it('should import documents with latin1 encoding', function (ctx) {
+      expect(ctx.entries).to.deep.include({
         type: 'doc',
         type: 'doc',
         projectPath: '/encodings/latin1.txt',
         projectPath: '/encodings/latin1.txt',
         lines: ['tétanisant!'],
         lines: ['tétanisant!'],
       })
       })
     })
     })
 
 
-    it('should import documents with utf16-le encoding', function () {
-      expect(this.entries).to.deep.include({
+    it('should import documents with utf16-le encoding', function (ctx) {
+      expect(ctx.entries).to.deep.include({
         type: 'doc',
         type: 'doc',
         projectPath: '/encodings/utf16le.txt',
         projectPath: '/encodings/utf16le.txt',
         lines: ['\ufeffétonnant!'],
         lines: ['\ufeffétonnant!'],
       })
       })
     })
     })
 
 
-    it('should error when the root folder is a symlink', async function () {
-      await expect(this.FileSystemImportManager.promises.importDir('symlink'))
-        .to.be.rejected
+    it('should error when the root folder is a symlink', async function (ctx) {
+      await expect(ctx.FileSystemImportManager.promises.importDir('symlink')).to
+        .be.rejected
     })
     })
   })
   })
 
 
   describe('addEntity', function () {
   describe('addEntity', function () {
     describe('with directory', function () {
     describe('with directory', function () {
-      beforeEach(async function () {
+      beforeEach(async function (ctx) {
         mockFs({
         mockFs({
           path: {
           path: {
             to: {
             to: {
@@ -156,10 +161,10 @@ describe('FileSystemImportManager', function () {
           },
           },
         })
         })
 
 
-        await this.FileSystemImportManager.promises.addEntity(
-          this.userId,
-          this.projectId,
-          this.folderId,
+        await ctx.FileSystemImportManager.promises.addEntity(
+          ctx.userId,
+          ctx.projectId,
+          ctx.folderId,
           'folder',
           'folder',
           'path/to/folder',
           'path/to/folder',
           false
           false
@@ -170,32 +175,32 @@ describe('FileSystemImportManager', function () {
         mockFs.restore()
         mockFs.restore()
       })
       })
 
 
-      it('should add a folder to the project', function () {
-        this.EditorController.promises.addFolder.should.have.been.calledWith(
-          this.projectId,
-          this.folderId,
+      it('should add a folder to the project', function (ctx) {
+        ctx.EditorController.promises.addFolder.should.have.been.calledWith(
+          ctx.projectId,
+          ctx.folderId,
           'folder',
           'folder',
           'upload'
           'upload'
         )
         )
       })
       })
 
 
-      it("should add the folder's contents", function () {
-        this.EditorController.promises.addDoc.should.have.been.calledWith(
-          this.projectId,
-          this.newFolderId,
+      it("should add the folder's contents", function (ctx) {
+        ctx.EditorController.promises.addDoc.should.have.been.calledWith(
+          ctx.projectId,
+          ctx.newFolderId,
           'doc.tex',
           'doc.tex',
           ['one', 'two', 'three'],
           ['one', 'two', 'three'],
           'upload',
           'upload',
-          this.userId
+          ctx.userId
         )
         )
-        this.EditorController.promises.addFile.should.have.been.calledWith(
-          this.projectId,
-          this.newFolderId,
+        ctx.EditorController.promises.addFile.should.have.been.calledWith(
+          ctx.projectId,
+          ctx.newFolderId,
           'image.jpg',
           'image.jpg',
           'path/to/folder/image.jpg',
           'path/to/folder/image.jpg',
           null,
           null,
           'upload',
           'upload',
-          this.userId
+          ctx.userId
         )
         )
       })
       })
     })
     })
@@ -210,51 +215,51 @@ describe('FileSystemImportManager', function () {
       })
       })
 
 
       describe('with replace set to false', function () {
       describe('with replace set to false', function () {
-        beforeEach(async function () {
-          await this.FileSystemImportManager.promises.addEntity(
-            this.userId,
-            this.projectId,
-            this.folderId,
+        beforeEach(async function (ctx) {
+          await ctx.FileSystemImportManager.promises.addEntity(
+            ctx.userId,
+            ctx.projectId,
+            ctx.folderId,
             'image.jpg',
             'image.jpg',
             'uploaded-file',
             'uploaded-file',
             false
             false
           )
           )
         })
         })
 
 
-        it('should add the file', function () {
-          this.EditorController.promises.addFile.should.have.been.calledWith(
-            this.projectId,
-            this.folderId,
+        it('should add the file', function (ctx) {
+          ctx.EditorController.promises.addFile.should.have.been.calledWith(
+            ctx.projectId,
+            ctx.folderId,
             'image.jpg',
             'image.jpg',
             'uploaded-file',
             'uploaded-file',
             null,
             null,
             'upload',
             'upload',
-            this.userId
+            ctx.userId
           )
           )
         })
         })
       })
       })
 
 
       describe('with replace set to true', function () {
       describe('with replace set to true', function () {
-        beforeEach(async function () {
-          await this.FileSystemImportManager.promises.addEntity(
-            this.userId,
-            this.projectId,
-            this.folderId,
+        beforeEach(async function (ctx) {
+          await ctx.FileSystemImportManager.promises.addEntity(
+            ctx.userId,
+            ctx.projectId,
+            ctx.folderId,
             'image.jpg',
             'image.jpg',
             'uploaded-file',
             'uploaded-file',
             true
             true
           )
           )
         })
         })
 
 
-        it('should add the file', function () {
-          this.EditorController.promises.upsertFile.should.have.been.calledWith(
-            this.projectId,
-            this.folderId,
+        it('should add the file', function (ctx) {
+          ctx.EditorController.promises.upsertFile.should.have.been.calledWith(
+            ctx.projectId,
+            ctx.folderId,
             'image.jpg',
             'image.jpg',
             'uploaded-file',
             'uploaded-file',
             null,
             null,
             'upload',
             'upload',
-            this.userId
+            ctx.userId
           )
           )
         })
         })
       })
       })
@@ -279,49 +284,49 @@ describe('FileSystemImportManager', function () {
         })
         })
 
 
         describe('with replace set to false', function () {
         describe('with replace set to false', function () {
-          beforeEach(async function () {
-            await this.FileSystemImportManager.promises.addEntity(
-              this.userId,
-              this.projectId,
-              this.folderId,
+          beforeEach(async function (ctx) {
+            await ctx.FileSystemImportManager.promises.addEntity(
+              ctx.userId,
+              ctx.projectId,
+              ctx.folderId,
               'doc.tex',
               'doc.tex',
               'path/to/uploaded-file',
               'path/to/uploaded-file',
               false
               false
             )
             )
           })
           })
 
 
-          it('should insert the doc', function () {
-            this.EditorController.promises.addDoc.should.have.been.calledWith(
-              this.projectId,
-              this.folderId,
+          it('should insert the doc', function (ctx) {
+            ctx.EditorController.promises.addDoc.should.have.been.calledWith(
+              ctx.projectId,
+              ctx.folderId,
               'doc.tex',
               'doc.tex',
               ['one', 'two', 'three'],
               ['one', 'two', 'three'],
               'upload',
               'upload',
-              this.userId
+              ctx.userId
             )
             )
           })
           })
         })
         })
 
 
         describe('with replace set to true', function () {
         describe('with replace set to true', function () {
-          beforeEach(async function () {
-            await this.FileSystemImportManager.promises.addEntity(
-              this.userId,
-              this.projectId,
-              this.folderId,
+          beforeEach(async function (ctx) {
+            await ctx.FileSystemImportManager.promises.addEntity(
+              ctx.userId,
+              ctx.projectId,
+              ctx.folderId,
               'doc.tex',
               'doc.tex',
               'path/to/uploaded-file',
               'path/to/uploaded-file',
               true
               true
             )
             )
           })
           })
 
 
-          it('should upsert the doc', function () {
-            this.EditorController.promises.upsertDoc.should.have.been.calledWith(
-              this.projectId,
-              this.folderId,
+          it('should upsert the doc', function (ctx) {
+            ctx.EditorController.promises.upsertDoc.should.have.been.calledWith(
+              ctx.projectId,
+              ctx.folderId,
               'doc.tex',
               'doc.tex',
               ['one', 'two', 'three'],
               ['one', 'two', 'three'],
               'upload',
               'upload',
-              this.userId
+              ctx.userId
             )
             )
           })
           })
         })
         })
@@ -339,20 +344,20 @@ describe('FileSystemImportManager', function () {
         mockFs.restore()
         mockFs.restore()
       })
       })
 
 
-      it('should stop with an error', async function () {
+      it('should stop with an error', async function (ctx) {
         await expect(
         await expect(
-          this.FileSystemImportManager.promises.addEntity(
-            this.userId,
-            this.projectId,
-            this.folderId,
+          ctx.FileSystemImportManager.promises.addEntity(
+            ctx.userId,
+            ctx.projectId,
+            ctx.folderId,
             'main.tex',
             'main.tex',
             'path/to/symlink',
             'path/to/symlink',
             false
             false
           )
           )
         ).to.be.rejectedWith('path is symlink')
         ).to.be.rejectedWith('path is symlink')
-        this.EditorController.promises.addFolder.should.not.have.been.called
-        this.EditorController.promises.addDoc.should.not.have.been.called
-        this.EditorController.promises.addFile.should.not.have.been.called
+        ctx.EditorController.promises.addFolder.should.not.have.been.called
+        ctx.EditorController.promises.addDoc.should.not.have.been.called
+        ctx.EditorController.promises.addFile.should.not.have.been.called
       })
       })
     })
     })
   })
   })

+ 240 - 178
services/web/test/unit/src/Uploads/ProjectUploadManager.test.mjs

@@ -1,176 +1,238 @@
-const sinon = require('sinon')
-const { expect } = require('chai')
-const timekeeper = require('timekeeper')
-const SandboxedModule = require('sandboxed-module')
-const { ObjectId } = require('mongodb-legacy')
+import { vi, expect } from 'vitest'
+import sinon from 'sinon'
+import timekeeper from 'timekeeper'
+import mongodb from 'mongodb-legacy'
+
+const { ObjectId } = mongodb
 
 
 const MODULE_PATH =
 const MODULE_PATH =
-  '../../../../app/src/Features/Uploads/ProjectUploadManager.js'
+  '../../../../app/src/Features/Uploads/ProjectUploadManager.mjs'
 
 
 describe('ProjectUploadManager', function () {
 describe('ProjectUploadManager', function () {
-  beforeEach(function () {
-    this.now = Date.now()
-    timekeeper.freeze(this.now)
-    this.rootFolderId = new ObjectId()
-    this.ownerId = new ObjectId()
-    this.zipPath = '/path/to/zip/file-name.zip'
-    this.extractedZipPath = `/path/to/zip/file-name-${this.now}`
-    this.mainContent = 'Contents of main.tex'
-    this.projectName = 'My project*'
-    this.fixedProjectName = 'My project'
-    this.uniqueProjectName = 'My project (1)'
-    this.project = {
+  beforeEach(async function (ctx) {
+    ctx.now = Date.now()
+    timekeeper.freeze(ctx.now)
+    ctx.rootFolderId = new ObjectId()
+    ctx.ownerId = new ObjectId()
+    ctx.zipPath = '/path/to/zip/file-name.zip'
+    ctx.extractedZipPath = `/path/to/zip/file-name-${ctx.now}`
+    ctx.mainContent = 'Contents of main.tex'
+    ctx.projectName = 'My project*'
+    ctx.fixedProjectName = 'My project'
+    ctx.uniqueProjectName = 'My project (1)'
+    ctx.project = {
       _id: new ObjectId(),
       _id: new ObjectId(),
-      rootFolder: [{ _id: this.rootFolderId }],
+      rootFolder: [{ _id: ctx.rootFolderId }],
       overleaf: { history: { id: 12345 } },
       overleaf: { history: { id: 12345 } },
     }
     }
-    this.doc = {
+    ctx.doc = {
       _id: new ObjectId(),
       _id: new ObjectId(),
       name: 'main.tex',
       name: 'main.tex',
     }
     }
-    this.docFsPath = '/path/to/doc'
-    this.docLines = ['My thesis', 'by A. U. Thor']
-    this.file = {
+    ctx.docFsPath = '/path/to/doc'
+    ctx.docLines = ['My thesis', 'by A. U. Thor']
+    ctx.file = {
       _id: new ObjectId(),
       _id: new ObjectId(),
       name: 'image.png',
       name: 'image.png',
     }
     }
-    this.fileFsPath = '/path/to/file'
+    ctx.fileFsPath = '/path/to/file'
 
 
-    this.topLevelDestination = '/path/to/zip/file-extracted/nested'
-    this.newProjectVersion = 123
-    this.importEntries = [
+    ctx.topLevelDestination = '/path/to/zip/file-extracted/nested'
+    ctx.newProjectVersion = 123
+    ctx.importEntries = [
       {
       {
         type: 'doc',
         type: 'doc',
         projectPath: '/main.tex',
         projectPath: '/main.tex',
-        lines: this.docLines,
+        lines: ctx.docLines,
       },
       },
       {
       {
         type: 'file',
         type: 'file',
-        projectPath: `/${this.file.name}`,
-        fsPath: this.fileFsPath,
+        projectPath: `/${ctx.file.name}`,
+        fsPath: ctx.fileFsPath,
       },
       },
     ]
     ]
-    this.docEntries = [
+    ctx.docEntries = [
       {
       {
-        doc: this.doc,
-        path: `/${this.doc.name}`,
-        docLines: this.docLines.join('\n'),
+        doc: ctx.doc,
+        path: `/${ctx.doc.name}`,
+        docLines: ctx.docLines.join('\n'),
       },
       },
     ]
     ]
-    this.fileEntries = [
+    ctx.fileEntries = [
       {
       {
-        file: this.file,
-        path: `/${this.file.name}`,
+        file: ctx.file,
+        path: `/${ctx.file.name}`,
         createdBlob: true,
         createdBlob: true,
       },
       },
     ]
     ]
 
 
-    this.fs = {
+    ctx.fs = {
       promises: {
       promises: {
         rm: sinon.stub().resolves(),
         rm: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.ArchiveManager = {
+    ctx.ArchiveManager = {
       promises: {
       promises: {
         extractZipArchive: sinon.stub().resolves(),
         extractZipArchive: sinon.stub().resolves(),
         findTopLevelDirectory: sinon
         findTopLevelDirectory: sinon
           .stub()
           .stub()
-          .withArgs(this.extractedZipPath)
-          .resolves(this.topLevelDestination),
+          .withArgs(ctx.extractedZipPath)
+          .resolves(ctx.topLevelDestination),
       },
       },
     }
     }
-    this.Doc = sinon.stub().returns(this.doc)
-    this.DocstoreManager = {
+    ctx.Doc = sinon.stub().returns(ctx.doc)
+    ctx.DocstoreManager = {
       promises: {
       promises: {
         updateDoc: sinon.stub().resolves(),
         updateDoc: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.DocumentHelper = {
+    ctx.DocumentHelper = {
       getTitleFromTexContent: sinon
       getTitleFromTexContent: sinon
         .stub()
         .stub()
-        .withArgs(this.mainContent)
-        .returns(this.projectName),
+        .withArgs(ctx.mainContent)
+        .returns(ctx.projectName),
     }
     }
-    this.DocumentUpdaterHandler = {
+    ctx.DocumentUpdaterHandler = {
       promises: {
       promises: {
         updateProjectStructure: sinon.stub().resolves(),
         updateProjectStructure: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.FileStoreHandler = {
+    ctx.FileStoreHandler = {
       promises: {
       promises: {
         uploadFileFromDiskWithHistoryId: sinon.stub().resolves({
         uploadFileFromDiskWithHistoryId: sinon.stub().resolves({
-          fileRef: this.file,
+          fileRef: ctx.file,
           createdBlob: true,
           createdBlob: true,
         }),
         }),
       },
       },
     }
     }
-    this.FileSystemImportManager = {
+    ctx.FileSystemImportManager = {
       promises: {
       promises: {
         importDir: sinon
         importDir: sinon
           .stub()
           .stub()
-          .withArgs(this.topLevelDestination)
-          .resolves(this.importEntries),
+          .withArgs(ctx.topLevelDestination)
+          .resolves(ctx.importEntries),
       },
       },
     }
     }
-    this.ProjectCreationHandler = {
+    ctx.ProjectCreationHandler = {
       promises: {
       promises: {
-        createBlankProject: sinon.stub().resolves(this.project),
+        createBlankProject: sinon.stub().resolves(ctx.project),
       },
       },
     }
     }
-    this.ProjectEntityMongoUpdateHandler = {
+    ctx.ProjectEntityMongoUpdateHandler = {
       promises: {
       promises: {
-        createNewFolderStructure: sinon.stub().resolves(this.newProjectVersion),
+        createNewFolderStructure: sinon.stub().resolves(ctx.newProjectVersion),
       },
       },
     }
     }
-    this.ProjectRootDocManager = {
+    ctx.ProjectRootDocManager = {
       promises: {
       promises: {
         setRootDocAutomatically: sinon.stub().resolves(),
         setRootDocAutomatically: sinon.stub().resolves(),
         findRootDocFileFromDirectory: sinon
         findRootDocFileFromDirectory: sinon
           .stub()
           .stub()
-          .resolves({ path: 'main.tex', content: this.mainContent }),
+          .resolves({ path: 'main.tex', content: ctx.mainContent }),
         setRootDocFromName: sinon.stub().resolves(),
         setRootDocFromName: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.ProjectDetailsHandler = {
+    ctx.ProjectDetailsHandler = {
       fixProjectName: sinon
       fixProjectName: sinon
         .stub()
         .stub()
-        .withArgs(this.projectName)
-        .returns(this.fixedProjectName),
+        .withArgs(ctx.projectName)
+        .returns(ctx.fixedProjectName),
       promises: {
       promises: {
-        generateUniqueName: sinon.stub().resolves(this.uniqueProjectName),
+        generateUniqueName: sinon.stub().resolves(ctx.uniqueProjectName),
       },
       },
     }
     }
-    this.ProjectDeleter = {
+    ctx.ProjectDeleter = {
       promises: {
       promises: {
         deleteProject: sinon.stub().resolves(),
         deleteProject: sinon.stub().resolves(),
       },
       },
     }
     }
-    this.TpdsProjectFlusher = {
+    ctx.TpdsProjectFlusher = {
       promises: {
       promises: {
         flushProjectToTpds: sinon.stub().resolves(),
         flushProjectToTpds: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.ProjectUploadManager = SandboxedModule.require(MODULE_PATH, {
-      requires: {
-        fs: this.fs,
-        './ArchiveManager': this.ArchiveManager,
-        '../../models/Doc': { Doc: this.Doc },
-        '../Docstore/DocstoreManager': this.DocstoreManager,
-        '../Documents/DocumentHelper': this.DocumentHelper,
-        '../DocumentUpdater/DocumentUpdaterHandler':
-          this.DocumentUpdaterHandler,
-        '../FileStore/FileStoreHandler': this.FileStoreHandler,
-        './FileSystemImportManager': this.FileSystemImportManager,
-        '../Project/ProjectCreationHandler': this.ProjectCreationHandler,
-        '../Project/ProjectEntityMongoUpdateHandler':
-          this.ProjectEntityMongoUpdateHandler,
-        '../Project/ProjectRootDocManager': this.ProjectRootDocManager,
-        '../Project/ProjectDetailsHandler': this.ProjectDetailsHandler,
-        '../Project/ProjectDeleter': this.ProjectDeleter,
-        '../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher,
-      },
-    })
+    vi.doMock('fs', () => ({
+      default: ctx.fs,
+    }))
+
+    vi.doMock('../../../../app/src/Features/Uploads/ArchiveManager', () => ({
+      default: ctx.ArchiveManager,
+    }))
+
+    vi.doMock('../../../../app/src/models/Doc', () => ({
+      Doc: ctx.Doc,
+    }))
+
+    vi.doMock('../../../../app/src/Features/Docstore/DocstoreManager', () => ({
+      default: ctx.DocstoreManager,
+    }))
+
+    vi.doMock('../../../../app/src/Features/Documents/DocumentHelper', () => ({
+      default: ctx.DocumentHelper,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler',
+      () => ({
+        default: ctx.DocumentUpdaterHandler,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/FileStore/FileStoreHandler',
+      () => ({
+        default: ctx.FileStoreHandler,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/Uploads/FileSystemImportManager',
+      () => ({
+        default: ctx.FileSystemImportManager,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectCreationHandler',
+      () => ({
+        default: ctx.ProjectCreationHandler,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectEntityMongoUpdateHandler',
+      () => ({
+        default: ctx.ProjectEntityMongoUpdateHandler,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectRootDocManager',
+      () => ({
+        default: ctx.ProjectRootDocManager,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/Project/ProjectDetailsHandler',
+      () => ({
+        default: ctx.ProjectDetailsHandler,
+      })
+    )
+
+    vi.doMock('../../../../app/src/Features/Project/ProjectDeleter', () => ({
+      default: ctx.ProjectDeleter,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher',
+      () => ({
+        default: ctx.TpdsProjectFlusher,
+      })
+    )
+
+    ctx.ProjectUploadManager = (await import(MODULE_PATH)).default
   })
   })
 
 
   afterEach(function () {
   afterEach(function () {
@@ -179,65 +241,65 @@ describe('ProjectUploadManager', function () {
 
 
   describe('createProjectFromZipArchive', function () {
   describe('createProjectFromZipArchive', function () {
     describe('when the title can be read from the root document', function () {
     describe('when the title can be read from the root document', function () {
-      beforeEach(async function () {
-        await this.ProjectUploadManager.promises.createProjectFromZipArchive(
-          this.ownerId,
-          this.projectName,
-          this.zipPath
+      beforeEach(async function (ctx) {
+        await ctx.ProjectUploadManager.promises.createProjectFromZipArchive(
+          ctx.ownerId,
+          ctx.projectName,
+          ctx.zipPath
         )
         )
       })
       })
 
 
-      it('should extract the archive', function () {
-        this.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith(
-          this.zipPath,
-          this.extractedZipPath
+      it('should extract the archive', function (ctx) {
+        ctx.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith(
+          ctx.zipPath,
+          ctx.extractedZipPath
         )
         )
       })
       })
 
 
-      it('should create a project', function () {
-        this.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
-          this.ownerId,
-          this.uniqueProjectName
+      it('should create a project', function (ctx) {
+        ctx.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
+          ctx.ownerId,
+          ctx.uniqueProjectName
         )
         )
       })
       })
 
 
-      it('should initialize the file tree', function () {
-        this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
-          this.project._id,
-          this.docEntries,
-          this.fileEntries
+      it('should initialize the file tree', function (ctx) {
+        ctx.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
+          ctx.project._id,
+          ctx.docEntries,
+          ctx.fileEntries
         )
         )
       })
       })
 
 
-      it('should notify document updater', function () {
-        this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
-          this.project._id,
-          this.project.overleaf.history.id,
-          this.ownerId,
+      it('should notify document updater', function (ctx) {
+        ctx.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
+          ctx.project._id,
+          ctx.project.overleaf.history.id,
+          ctx.ownerId,
           {
           {
-            newDocs: this.docEntries,
-            newFiles: this.fileEntries,
-            newProject: { version: this.newProjectVersion },
+            newDocs: ctx.docEntries,
+            newFiles: ctx.fileEntries,
+            newProject: { version: ctx.newProjectVersion },
           },
           },
           null
           null
         )
         )
       })
       })
 
 
-      it('should flush the project to TPDS', function () {
-        this.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
-          this.project._id
+      it('should flush the project to TPDS', function (ctx) {
+        ctx.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
+          ctx.project._id
         )
         )
       })
       })
 
 
-      it('should set the root document', function () {
-        this.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWith(
-          this.project._id,
+      it('should set the root document', function (ctx) {
+        ctx.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWith(
+          ctx.project._id,
           'main.tex'
           'main.tex'
         )
         )
       })
       })
 
 
-      it('should remove the destination directory afterwards', function () {
-        this.fs.promises.rm.should.have.been.calledWith(this.extractedZipPath, {
+      it('should remove the destination directory afterwards', function (ctx) {
+        ctx.fs.promises.rm.should.have.been.calledWith(ctx.extractedZipPath, {
           recursive: true,
           recursive: true,
           force: true,
           force: true,
         })
         })
@@ -245,122 +307,122 @@ describe('ProjectUploadManager', function () {
     })
     })
 
 
     describe("when the root document can't be determined", function () {
     describe("when the root document can't be determined", function () {
-      beforeEach(async function () {
-        this.ProjectRootDocManager.promises.findRootDocFileFromDirectory.resolves(
+      beforeEach(async function (ctx) {
+        ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory.resolves(
           {}
           {}
         )
         )
-        await this.ProjectUploadManager.promises.createProjectFromZipArchive(
-          this.ownerId,
-          this.projectName,
-          this.zipPath
+        await ctx.ProjectUploadManager.promises.createProjectFromZipArchive(
+          ctx.ownerId,
+          ctx.projectName,
+          ctx.zipPath
         )
         )
       })
       })
 
 
-      it('should not try to set the root doc', function () {
-        this.ProjectRootDocManager.promises.setRootDocFromName.should.not.have
+      it('should not try to set the root doc', function (ctx) {
+        ctx.ProjectRootDocManager.promises.setRootDocFromName.should.not.have
           .been.called
           .been.called
       })
       })
     })
     })
   })
   })
 
 
   describe('createProjectFromZipArchiveWithName', function () {
   describe('createProjectFromZipArchiveWithName', function () {
-    beforeEach(async function () {
-      await this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
-        this.ownerId,
-        this.projectName,
-        this.zipPath
+    beforeEach(async function (ctx) {
+      await ctx.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
+        ctx.ownerId,
+        ctx.projectName,
+        ctx.zipPath
       )
       )
     })
     })
 
 
-    it('should extract the archive', function () {
-      this.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith(
-        this.zipPath,
-        this.extractedZipPath
+    it('should extract the archive', function (ctx) {
+      ctx.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith(
+        ctx.zipPath,
+        ctx.extractedZipPath
       )
       )
     })
     })
 
 
-    it('should create a project owned by the owner_id', function () {
-      this.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
-        this.ownerId,
-        this.uniqueProjectName
+    it('should create a project owned by the owner_id', function (ctx) {
+      ctx.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
+        ctx.ownerId,
+        ctx.uniqueProjectName
       )
       )
     })
     })
 
 
-    it('should automatically set the root doc', function () {
-      this.ProjectRootDocManager.promises.setRootDocAutomatically.should.have.been.calledWith(
-        this.project._id
+    it('should automatically set the root doc', function (ctx) {
+      ctx.ProjectRootDocManager.promises.setRootDocAutomatically.should.have.been.calledWith(
+        ctx.project._id
       )
       )
     })
     })
 
 
-    it('should initialize the file tree', function () {
-      this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
-        this.project._id,
-        this.docEntries,
-        this.fileEntries
+    it('should initialize the file tree', function (ctx) {
+      ctx.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
+        ctx.project._id,
+        ctx.docEntries,
+        ctx.fileEntries
       )
       )
     })
     })
 
 
-    it('should notify document updater', function () {
-      this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
-        this.project._id,
-        this.project.overleaf.history.id,
-        this.ownerId,
+    it('should notify document updater', function (ctx) {
+      ctx.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
+        ctx.project._id,
+        ctx.project.overleaf.history.id,
+        ctx.ownerId,
         {
         {
-          newDocs: this.docEntries,
-          newFiles: this.fileEntries,
-          newProject: { version: this.newProjectVersion },
+          newDocs: ctx.docEntries,
+          newFiles: ctx.fileEntries,
+          newProject: { version: ctx.newProjectVersion },
         },
         },
         null
         null
       )
       )
     })
     })
 
 
-    it('should flush the project to TPDS', function () {
-      this.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
-        this.project._id
+    it('should flush the project to TPDS', function (ctx) {
+      ctx.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
+        ctx.project._id
       )
       )
     })
     })
 
 
-    it('should remove the destination directory afterwards', function () {
-      this.fs.promises.rm.should.have.been.calledWith(this.extractedZipPath, {
+    it('should remove the destination directory afterwards', function (ctx) {
+      ctx.fs.promises.rm.should.have.been.calledWith(ctx.extractedZipPath, {
         recursive: true,
         recursive: true,
         force: true,
         force: true,
       })
       })
     })
     })
 
 
     describe('when initializing the folder structure fails', function () {
     describe('when initializing the folder structure fails', function () {
-      beforeEach(async function () {
-        this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.rejects()
+      beforeEach(async function (ctx) {
+        ctx.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.rejects()
         await expect(
         await expect(
-          this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
-            this.ownerId,
-            this.projectName,
-            this.zipPath
+          ctx.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
+            ctx.ownerId,
+            ctx.projectName,
+            ctx.zipPath
           )
           )
         ).to.be.rejected
         ).to.be.rejected
       })
       })
 
 
-      it('should cleanup the blank project created', async function () {
-        this.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
-          this.project._id
+      it('should cleanup the blank project created', async function (ctx) {
+        ctx.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
+          ctx.project._id
         )
         )
       })
       })
     })
     })
 
 
     describe('when setting automatically the root doc fails', function () {
     describe('when setting automatically the root doc fails', function () {
-      beforeEach(async function () {
-        this.ProjectRootDocManager.promises.setRootDocAutomatically.rejects()
+      beforeEach(async function (ctx) {
+        ctx.ProjectRootDocManager.promises.setRootDocAutomatically.rejects()
         await expect(
         await expect(
-          this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
-            this.ownerId,
-            this.projectName,
-            this.zipPath
+          ctx.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
+            ctx.ownerId,
+            ctx.projectName,
+            ctx.zipPath
           )
           )
         ).to.be.rejected
         ).to.be.rejected
       })
       })
 
 
-      it('should cleanup the blank project created', function () {
-        this.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
-          this.project._id
+      it('should cleanup the blank project created', function (ctx) {
+        ctx.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
+          ctx.project._id
         )
         )
       })
       })
     })
     })

+ 355 - 295
services/web/test/unit/src/User/UserDeleter.test.mjs

@@ -1,28 +1,34 @@
-const { expect } = require('chai')
-const sinon = require('sinon')
-const tk = require('timekeeper')
-const moment = require('moment')
-const SandboxedModule = require('sandboxed-module')
-const Errors = require('../../../../app/src/Features/Errors/Errors')
-const ObjectId = require('mongoose').Types.ObjectId
-const { User } = require('../helpers/models/User')
-const { DeletedUser } = require('../helpers/models/DeletedUser')
-
-const modulePath = '../../../../app/src/Features/User/UserDeleter.js'
+import { beforeEach, describe, expect, vi, it } from 'vitest'
+import sinon from 'sinon'
+import tk from 'timekeeper'
+import moment from 'moment'
+import Errors from '../../../../app/src/Features/Errors/Errors.js'
+import mongoose from 'mongoose'
+import indirectlyImportModels from '../helpers/indirectlyImportModels.js'
+
+const { User, DeletedUser } = indirectlyImportModels(['User', 'DeletedUser'])
+
+const ObjectId = mongoose.Types.ObjectId
+
+vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
+  vi.importActual('../../../../app/src/Features/Errors/Errors.js')
+)
+
+const modulePath = '../../../../app/src/Features/User/UserDeleter.mjs'
 
 
 describe('UserDeleter', function () {
 describe('UserDeleter', function () {
-  beforeEach(function () {
-    tk.freeze(Date.now())
+  beforeEach(async function (ctx) {
+    ctx.userId = new ObjectId()
+    ctx.ipAddress = '1.2.3.4'
 
 
-    this.userId = new ObjectId()
-    this.ipAddress = '1.2.3.4'
+    ctx.UserMock = sinon.mock(User)
+    ctx.DeletedUserMock = sinon.mock(DeletedUser)
 
 
-    this.UserMock = sinon.mock(User)
-    this.DeletedUserMock = sinon.mock(DeletedUser)
+    tk.freeze(Date.now())
 
 
-    this.mockedUser = sinon.mock(
+    ctx.mockedUser = sinon.mock(
       new User({
       new User({
-        _id: this.userId,
+        _id: ctx.userId,
         email: 'bob@bob.com',
         email: 'bob@bob.com',
         lastLoggedIn: Date.now() + 1000,
         lastLoggedIn: Date.now() + 1000,
         signUpDate: Date.now() + 2000,
         signUpDate: Date.now() + 2000,
@@ -35,150 +41,213 @@ describe('UserDeleter', function () {
         referal_id: ['giraffe'],
         referal_id: ['giraffe'],
       })
       })
     )
     )
-    this.user = this.mockedUser.object
+    ctx.user = ctx.mockedUser.object
 
 
-    this.NewsletterManager = {
+    ctx.NewsletterManager = {
       promises: {
       promises: {
         unsubscribe: sinon.stub().resolves(),
         unsubscribe: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.ProjectDeleter = {
+    ctx.ProjectDeleter = {
       promises: {
       promises: {
         deleteUsersProjects: sinon.stub().resolves(),
         deleteUsersProjects: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.SubscriptionHandler = {
+    ctx.SubscriptionHandler = {
       promises: {
       promises: {
         cancelSubscription: sinon.stub().resolves(),
         cancelSubscription: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.SubscriptionUpdater = {
+    ctx.SubscriptionUpdater = {
       promises: {
       promises: {
         removeUserFromAllGroups: sinon.stub().resolves(),
         removeUserFromAllGroups: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.SubscriptionLocator = {
+    ctx.SubscriptionLocator = {
       promises: {
       promises: {
         getUsersSubscription: sinon.stub().resolves(),
         getUsersSubscription: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.UserMembershipsHandler = {
+    ctx.UserMembershipsHandler = {
       promises: {
       promises: {
         removeUserFromAllEntities: sinon.stub().resolves(),
         removeUserFromAllEntities: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.UserSessionsManager = {
+    ctx.UserSessionsManager = {
       promises: {
       promises: {
         removeSessionsFromRedis: sinon.stub().resolves(),
         removeSessionsFromRedis: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.InstitutionsApi = {
+    ctx.InstitutionsApi = {
       promises: {
       promises: {
         deleteAffiliations: sinon.stub().resolves(),
         deleteAffiliations: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.UserAuditLogEntry = {
+    ctx.UserAuditLogEntry = {
       deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
       deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
     }
     }
 
 
-    this.Modules = {
+    ctx.Modules = {
       promises: { hooks: { fire: sinon.stub().resolves() } },
       promises: { hooks: { fire: sinon.stub().resolves() } },
     }
     }
 
 
-    this.Feedback = {
+    ctx.Feedback = {
       deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
       deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
     }
     }
 
 
-    this.OnboardingDataCollectionManager = {
+    ctx.OnboardingDataCollectionManager = {
       deleteOnboardingDataCollection: sinon.stub().resolves(),
       deleteOnboardingDataCollection: sinon.stub().resolves(),
     }
     }
 
 
-    this.EmailHandler = {
+    ctx.EmailHandler = {
       promises: {
       promises: {
         sendEmail: sinon.stub().resolves(),
         sendEmail: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.UserAuditLogHandler = {
+    ctx.UserAuditLogHandler = {
       promises: {
       promises: {
         addEntry: sinon.stub().resolves(),
         addEntry: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.UserDeleter = SandboxedModule.require(modulePath, {
-      requires: {
-        '../../models/User': { User },
-        '../../models/DeletedUser': { DeletedUser },
-        '../../models/Feedback': { Feedback: this.Feedback },
-        '../Newsletter/NewsletterManager': this.NewsletterManager,
-        './UserSessionsManager': this.UserSessionsManager,
-        '../Subscription/SubscriptionHandler': this.SubscriptionHandler,
-        '../Subscription/SubscriptionUpdater': this.SubscriptionUpdater,
-        '../Subscription/SubscriptionLocator': this.SubscriptionLocator,
-        '../UserMembership/UserMembershipsHandler': this.UserMembershipsHandler,
-        '../Project/ProjectDeleter': this.ProjectDeleter,
-        '../Institutions/InstitutionsAPI': this.InstitutionsApi,
-        '../../models/UserAuditLogEntry': {
-          UserAuditLogEntry: this.UserAuditLogEntry,
-        },
-        './UserAuditLogHandler': this.UserAuditLogHandler,
-        '../../infrastructure/Modules': this.Modules,
-        '../OnboardingDataCollection/OnboardingDataCollectionManager':
-          this.OnboardingDataCollectionManager,
-        '../Email/EmailHandler': this.EmailHandler,
-      },
-    })
+    vi.doMock('../../../../app/src/models/User', () => ({
+      User,
+    }))
+
+    vi.doMock('../../../../app/src/models/DeletedUser', () => ({
+      DeletedUser,
+    }))
+
+    vi.doMock('../../../../app/src/models/Feedback', () => ({
+      Feedback: ctx.Feedback,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Newsletter/NewsletterManager',
+      () => ({
+        default: ctx.NewsletterManager,
+      })
+    )
+
+    vi.doMock('../../../../app/src/Features/User/UserSessionsManager', () => ({
+      default: ctx.UserSessionsManager,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Subscription/SubscriptionHandler',
+      () => ({
+        default: ctx.SubscriptionHandler,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/Subscription/SubscriptionUpdater',
+      () => ({
+        default: ctx.SubscriptionUpdater,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/Subscription/SubscriptionLocator',
+      () => ({
+        default: ctx.SubscriptionLocator,
+      })
+    )
+
+    vi.doMock(
+      '../../../../app/src/Features/UserMembership/UserMembershipsHandler',
+      () => ({
+        default: ctx.UserMembershipsHandler,
+      })
+    )
+
+    vi.doMock('../../../../app/src/Features/Project/ProjectDeleter', () => ({
+      default: ctx.ProjectDeleter,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/Institutions/InstitutionsAPI',
+      () => ({
+        default: ctx.InstitutionsApi,
+      })
+    )
+
+    vi.doMock('../../../../app/src/models/UserAuditLogEntry', () => ({
+      UserAuditLogEntry: ctx.UserAuditLogEntry,
+    }))
+
+    vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({
+      default: ctx.UserAuditLogHandler,
+    }))
+
+    vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
+      default: ctx.Modules,
+    }))
+
+    vi.doMock(
+      '../../../../app/src/Features/OnboardingDataCollection/OnboardingDataCollectionManager',
+      () => ({
+        default: ctx.OnboardingDataCollectionManager,
+      })
+    )
+
+    vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({
+      default: ctx.EmailHandler,
+    }))
+
+    ctx.UserDeleter = (await import(modulePath)).default
   })
   })
 
 
-  afterEach(function () {
-    this.DeletedUserMock.restore()
-    this.UserMock.restore()
-    this.mockedUser.restore()
+  afterEach(function (ctx) {
+    ctx.DeletedUserMock.restore()
+    ctx.UserMock.restore()
+    ctx.mockedUser.restore()
     tk.reset()
     tk.reset()
   })
   })
 
 
   describe('deleteUser', function () {
   describe('deleteUser', function () {
-    beforeEach(function () {
-      this.UserMock.expects('findById')
-        .withArgs(this.userId)
+    beforeEach(function (ctx) {
+      ctx.UserMock.expects('findById')
+        .withArgs(ctx.userId)
         .chain('exec')
         .chain('exec')
-        .resolves(this.user)
+        .resolves(ctx.user)
     })
     })
 
 
     describe('when the user can be deleted', function () {
     describe('when the user can be deleted', function () {
-      beforeEach(function () {
-        this.deletedUser = {
-          user: this.user,
+      beforeEach(function (ctx) {
+        ctx.deletedUser = {
+          user: ctx.user,
           deleterData: {
           deleterData: {
             deletedAt: new Date(),
             deletedAt: new Date(),
-            deletedUserId: this.userId,
-            deleterIpAddress: this.ipAddress,
+            deletedUserId: ctx.userId,
+            deleterIpAddress: ctx.ipAddress,
             deleterId: undefined,
             deleterId: undefined,
-            deletedUserLastLoggedIn: this.user.lastLoggedIn,
-            deletedUserSignUpDate: this.user.signUpDate,
-            deletedUserLoginCount: this.user.loginCount,
-            deletedUserReferralId: this.user.referal_id,
-            deletedUserReferredUsers: this.user.refered_users,
-            deletedUserReferredUserCount: this.user.refered_user_count,
-            deletedUserOverleafId: this.user.overleaf.id,
+            deletedUserLastLoggedIn: ctx.user.lastLoggedIn,
+            deletedUserSignUpDate: ctx.user.signUpDate,
+            deletedUserLoginCount: ctx.user.loginCount,
+            deletedUserReferralId: ctx.user.referal_id,
+            deletedUserReferredUsers: ctx.user.refered_users,
+            deletedUserReferredUserCount: ctx.user.refered_user_count,
+            deletedUserOverleafId: ctx.user.overleaf.id,
           },
           },
         }
         }
       })
       })
 
 
       describe('when only the ip address is passed', function () {
       describe('when only the ip address is passed', function () {
-        beforeEach(function () {
-          this.DeletedUserMock.expects('updateOne')
+        beforeEach(function (ctx) {
+          ctx.DeletedUserMock.expects('updateOne')
             .withArgs(
             .withArgs(
-              { 'deleterData.deletedUserId': this.userId },
-              this.deletedUser,
+              { 'deleterData.deletedUserId': ctx.userId },
+              ctx.deletedUser,
               { upsert: true }
               { upsert: true }
             )
             )
             .chain('exec')
             .chain('exec')
@@ -186,123 +255,123 @@ describe('UserDeleter', function () {
         })
         })
 
 
         describe('when unsubscribing in Mailchimp succeeds', function () {
         describe('when unsubscribing in Mailchimp succeeds', function () {
-          beforeEach(function () {
-            this.UserMock.expects('deleteOne')
-              .withArgs({ _id: this.userId })
+          beforeEach(function (ctx) {
+            ctx.UserMock.expects('deleteOne')
+              .withArgs({ _id: ctx.userId })
               .chain('exec')
               .chain('exec')
               .resolves()
               .resolves()
           })
           })
 
 
-          it('should find and the user in mongo by its id', async function () {
-            await this.UserDeleter.promises.deleteUser(this.userId, {
-              ipAddress: this.ipAddress,
+          it('should find and the user in mongo by its id', async function (ctx) {
+            await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+              ipAddress: ctx.ipAddress,
             })
             })
-            this.UserMock.verify()
+            ctx.UserMock.verify()
           })
           })
 
 
-          it('should delete the user from mailchimp', async function () {
-            await this.UserDeleter.promises.deleteUser(this.userId, {
-              ipAddress: this.ipAddress,
+          it('should delete the user from mailchimp', async function (ctx) {
+            await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+              ipAddress: ctx.ipAddress,
             })
             })
             expect(
             expect(
-              this.NewsletterManager.promises.unsubscribe
-            ).to.have.been.calledWith(this.user, { delete: true })
+              ctx.NewsletterManager.promises.unsubscribe
+            ).to.have.been.calledWith(ctx.user, { delete: true })
           })
           })
 
 
-          it('should delete all the projects of a user', async function () {
-            await this.UserDeleter.promises.deleteUser(this.userId, {
-              ipAddress: this.ipAddress,
+          it('should delete all the projects of a user', async function (ctx) {
+            await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+              ipAddress: ctx.ipAddress,
             })
             })
             expect(
             expect(
-              this.ProjectDeleter.promises.deleteUsersProjects
-            ).to.have.been.calledWith(this.userId)
+              ctx.ProjectDeleter.promises.deleteUsersProjects
+            ).to.have.been.calledWith(ctx.userId)
           })
           })
 
 
-          it("should cancel the user's subscription", async function () {
-            await this.UserDeleter.promises.deleteUser(this.userId, {
-              ipAddress: this.ipAddress,
+          it("should cancel the user's subscription", async function (ctx) {
+            await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+              ipAddress: ctx.ipAddress,
             })
             })
             expect(
             expect(
-              this.SubscriptionHandler.promises.cancelSubscription
-            ).to.have.been.calledWith(this.user)
+              ctx.SubscriptionHandler.promises.cancelSubscription
+            ).to.have.been.calledWith(ctx.user)
           })
           })
 
 
-          it('should delete user affiliations', async function () {
-            await this.UserDeleter.promises.deleteUser(this.userId, {
-              ipAddress: this.ipAddress,
+          it('should delete user affiliations', async function (ctx) {
+            await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+              ipAddress: ctx.ipAddress,
             })
             })
             expect(
             expect(
-              this.InstitutionsApi.promises.deleteAffiliations
-            ).to.have.been.calledWith(this.userId)
+              ctx.InstitutionsApi.promises.deleteAffiliations
+            ).to.have.been.calledWith(ctx.userId)
           })
           })
 
 
-          it('should cleanup collabratec access tokens', async function () {
-            await this.UserDeleter.promises.deleteUser(this.userId, {
-              ipAddress: this.ipAddress,
+          it('should cleanup collabratec access tokens', async function (ctx) {
+            await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+              ipAddress: ctx.ipAddress,
             })
             })
-            expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
+            expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
               'cleanupPersonalAccessTokens',
               'cleanupPersonalAccessTokens',
-              this.userId,
+              ctx.userId,
               ['collabratec', 'git_bridge']
               ['collabratec', 'git_bridge']
             )
             )
           })
           })
 
 
-          it('should fire the deleteUser hook for modules', async function () {
-            await this.UserDeleter.promises.deleteUser(this.userId, {
-              ipAddress: this.ipAddress,
+          it('should fire the deleteUser hook for modules', async function (ctx) {
+            await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+              ipAddress: ctx.ipAddress,
             })
             })
-            expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
+            expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
               'deleteUser',
               'deleteUser',
-              this.userId
+              ctx.userId
             )
             )
           })
           })
 
 
-          it('should stop the user sessions', async function () {
-            await this.UserDeleter.promises.deleteUser(this.userId, {
-              ipAddress: this.ipAddress,
+          it('should stop the user sessions', async function (ctx) {
+            await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+              ipAddress: ctx.ipAddress,
             })
             })
             expect(
             expect(
-              this.UserSessionsManager.promises.removeSessionsFromRedis
-            ).to.have.been.calledWith(this.user)
+              ctx.UserSessionsManager.promises.removeSessionsFromRedis
+            ).to.have.been.calledWith(ctx.user)
           })
           })
 
 
-          it('should remove user from group subscriptions', async function () {
-            await this.UserDeleter.promises.deleteUser(this.userId, {
-              ipAddress: this.ipAddress,
+          it('should remove user from group subscriptions', async function (ctx) {
+            await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+              ipAddress: ctx.ipAddress,
             })
             })
             expect(
             expect(
-              this.SubscriptionUpdater.promises.removeUserFromAllGroups
-            ).to.have.been.calledWith(this.userId)
+              ctx.SubscriptionUpdater.promises.removeUserFromAllGroups
+            ).to.have.been.calledWith(ctx.userId)
           })
           })
 
 
-          it('should remove user memberships', async function () {
-            await this.UserDeleter.promises.deleteUser(this.userId, {
-              ipAddress: this.ipAddress,
+          it('should remove user memberships', async function (ctx) {
+            await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+              ipAddress: ctx.ipAddress,
             })
             })
             expect(
             expect(
-              this.UserMembershipsHandler.promises.removeUserFromAllEntities
-            ).to.have.been.calledWith(this.userId)
+              ctx.UserMembershipsHandler.promises.removeUserFromAllEntities
+            ).to.have.been.calledWith(ctx.userId)
           })
           })
 
 
-          it('rejects if the user is a subscription admin', async function () {
-            this.SubscriptionLocator.promises.getUsersSubscription.rejects({
+          it('rejects if the user is a subscription admin', async function (ctx) {
+            ctx.SubscriptionLocator.promises.getUsersSubscription.rejects({
               _id: 'some-subscription',
               _id: 'some-subscription',
             })
             })
-            await expect(this.UserDeleter.promises.deleteUser(this.userId, {}))
-              .to.be.rejected
+            await expect(ctx.UserDeleter.promises.deleteUser(ctx.userId, {})).to
+              .be.rejected
           })
           })
 
 
-          it('should create a deletedUser', async function () {
-            await this.UserDeleter.promises.deleteUser(this.userId, {
-              ipAddress: this.ipAddress,
+          it('should create a deletedUser', async function (ctx) {
+            await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+              ipAddress: ctx.ipAddress,
             })
             })
-            this.DeletedUserMock.verify()
+            ctx.DeletedUserMock.verify()
           })
           })
 
 
           describe('email notifications', function () {
           describe('email notifications', function () {
-            it('should email the user', async function () {
-              await this.UserDeleter.promises.deleteUser(this.userId, {
-                ipAddress: this.ipAddress,
+            it('should email the user', async function (ctx) {
+              await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+                ipAddress: ctx.ipAddress,
               })
               })
               const emailOptions = {
               const emailOptions = {
                 to: 'bob@bob.com',
                 to: 'bob@bob.com',
@@ -310,38 +379,38 @@ describe('UserDeleter', function () {
                 actionDescribed: 'your Overleaf account was deleted',
                 actionDescribed: 'your Overleaf account was deleted',
               }
               }
               expect(
               expect(
-                this.EmailHandler.promises.sendEmail
+                ctx.EmailHandler.promises.sendEmail
               ).to.have.been.calledWith('securityAlert', emailOptions)
               ).to.have.been.calledWith('securityAlert', emailOptions)
             })
             })
 
 
-            it('should not email the user with skipEmail === true', async function () {
-              await this.UserDeleter.promises.deleteUser(this.userId, {
-                ipAddress: this.ipAddress,
+            it('should not email the user with skipEmail === true', async function (ctx) {
+              await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+                ipAddress: ctx.ipAddress,
                 skipEmail: true,
                 skipEmail: true,
               })
               })
-              expect(this.EmailHandler.promises.sendEmail).not.to.have.been
+              expect(ctx.EmailHandler.promises.sendEmail).not.to.have.been
                 .called
                 .called
             })
             })
 
 
-            it('should fail when the email service fails', async function () {
-              this.EmailHandler.promises.sendEmail = sinon
+            it('should fail when the email service fails', async function (ctx) {
+              ctx.EmailHandler.promises.sendEmail = sinon
                 .stub()
                 .stub()
                 .rejects(new Error('email failed'))
                 .rejects(new Error('email failed'))
               await expect(
               await expect(
-                this.UserDeleter.promises.deleteUser(this.userId, {
-                  ipAddress: this.ipAddress,
+                ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+                  ipAddress: ctx.ipAddress,
                 })
                 })
               ).to.be.rejectedWith('email failed')
               ).to.be.rejectedWith('email failed')
             })
             })
 
 
             describe('with "force: true" option', function () {
             describe('with "force: true" option', function () {
-              it('should succeed when the email service fails', async function () {
-                this.EmailHandler.promises.sendEmail = sinon
+              it('should succeed when the email service fails', async function (ctx) {
+                ctx.EmailHandler.promises.sendEmail = sinon
                   .stub()
                   .stub()
                   .rejects(new Error('email failed'))
                   .rejects(new Error('email failed'))
                 await expect(
                 await expect(
-                  this.UserDeleter.promises.deleteUser(this.userId, {
-                    ipAddress: this.ipAddress,
+                  ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+                    ipAddress: ctx.ipAddress,
                     force: true,
                     force: true,
                   })
                   })
                 ).to.be.fulfilled
                 ).to.be.fulfilled
@@ -349,159 +418,148 @@ describe('UserDeleter', function () {
             })
             })
           })
           })
 
 
-          it('should add an audit log entry', async function () {
-            await this.UserDeleter.promises.deleteUser(this.userId, {
-              ipAddress: this.ipAddress,
+          it('should add an audit log entry', async function (ctx) {
+            await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+              ipAddress: ctx.ipAddress,
             })
             })
             expect(
             expect(
-              this.UserAuditLogHandler.promises.addEntry
+              ctx.UserAuditLogHandler.promises.addEntry
             ).to.have.been.calledWith(
             ).to.have.been.calledWith(
-              this.userId,
+              ctx.userId,
               'delete-account',
               'delete-account',
-              this.userId,
-              this.ipAddress
+              ctx.userId,
+              ctx.ipAddress
             )
             )
           })
           })
         })
         })
 
 
         describe('when unsubscribing from mailchimp fails', function () {
         describe('when unsubscribing from mailchimp fails', function () {
-          beforeEach(function () {
-            this.NewsletterManager.promises.unsubscribe.rejects(
+          beforeEach(function (ctx) {
+            ctx.NewsletterManager.promises.unsubscribe.rejects(
               new Error('something went wrong')
               new Error('something went wrong')
             )
             )
           })
           })
 
 
-          it('should return an error and not delete the user', async function () {
+          it('should return an error and not delete the user', async function (ctx) {
             await expect(
             await expect(
-              this.UserDeleter.promises.deleteUser(this.userId, {
-                ipAddress: this.ipAddress,
+              ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+                ipAddress: ctx.ipAddress,
               })
               })
             ).to.be.rejected
             ).to.be.rejected
-            this.UserMock.verify()
+            ctx.UserMock.verify()
           })
           })
         })
         })
 
 
         describe('when called as a callback', function () {
         describe('when called as a callback', function () {
-          beforeEach(function () {
-            this.UserMock.expects('deleteOne')
-              .withArgs({ _id: this.userId })
+          beforeEach(function (ctx) {
+            ctx.UserMock.expects('deleteOne')
+              .withArgs({ _id: ctx.userId })
               .chain('exec')
               .chain('exec')
               .resolves()
               .resolves()
           })
           })
 
 
-          it('should delete the user', function (done) {
-            this.UserDeleter.deleteUser(
-              this.userId,
-              { ipAddress: this.ipAddress },
-              err => {
-                expect(err).not.to.exist
-                this.UserMock.verify()
-                this.DeletedUserMock.verify()
-                done()
-              }
-            )
+          it('should delete the user', async function (ctx) {
+            await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+              ipAddress: ctx.ipAddress,
+            })
+            ctx.UserMock.verify()
+            ctx.DeletedUserMock.verify()
           })
           })
         })
         })
       })
       })
 
 
       describe('when a user and IP address are specified', function () {
       describe('when a user and IP address are specified', function () {
-        beforeEach(function () {
-          this.ipAddress = '1.2.3.4'
-          this.deleterId = new ObjectId()
+        beforeEach(function (ctx) {
+          ctx.ipAddress = '1.2.3.4'
+          ctx.deleterId = new ObjectId()
 
 
-          this.deletedUser.deleterData.deleterIpAddress = this.ipAddress
-          this.deletedUser.deleterData.deleterId = this.deleterId
+          ctx.deletedUser.deleterData.deleterIpAddress = ctx.ipAddress
+          ctx.deletedUser.deleterData.deleterId = ctx.deleterId
 
 
-          this.DeletedUserMock.expects('updateOne')
+          ctx.DeletedUserMock.expects('updateOne')
             .withArgs(
             .withArgs(
-              { 'deleterData.deletedUserId': this.userId },
-              this.deletedUser,
+              { 'deleterData.deletedUserId': ctx.userId },
+              ctx.deletedUser,
               { upsert: true }
               { upsert: true }
             )
             )
             .chain('exec')
             .chain('exec')
             .resolves()
             .resolves()
-          this.UserMock.expects('deleteOne')
-            .withArgs({ _id: this.userId })
+          ctx.UserMock.expects('deleteOne')
+            .withArgs({ _id: ctx.userId })
             .chain('exec')
             .chain('exec')
             .resolves()
             .resolves()
         })
         })
 
 
-        it('should add the deleted user id and ip address to the deletedUser', async function () {
-          await this.UserDeleter.promises.deleteUser(this.userId, {
-            deleterUser: { _id: this.deleterId },
-            ipAddress: this.ipAddress,
+        it('should add the deleted user id and ip address to the deletedUser', async function (ctx) {
+          await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+            deleterUser: { _id: ctx.deleterId },
+            ipAddress: ctx.ipAddress,
           })
           })
-          this.DeletedUserMock.verify()
+          ctx.DeletedUserMock.verify()
         })
         })
 
 
-        it('should add an audit log entry', async function () {
-          await this.UserDeleter.promises.deleteUser(this.userId, {
-            deleterUser: { _id: this.deleterId },
-            ipAddress: this.ipAddress,
+        it('should add an audit log entry', async function (ctx) {
+          await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+            deleterUser: { _id: ctx.deleterId },
+            ipAddress: ctx.ipAddress,
           })
           })
           expect(
           expect(
-            this.UserAuditLogHandler.promises.addEntry
+            ctx.UserAuditLogHandler.promises.addEntry
           ).to.have.been.calledWith(
           ).to.have.been.calledWith(
-            this.userId,
+            ctx.userId,
             'delete-account',
             'delete-account',
-            this.deleterId,
-            this.ipAddress
+            ctx.deleterId,
+            ctx.ipAddress
           )
           )
         })
         })
 
 
         describe('when called as a callback', function () {
         describe('when called as a callback', function () {
-          it('should delete the user', function (done) {
-            this.UserDeleter.deleteUser(
-              this.userId,
-              {
-                deleterUser: { _id: this.deleterId },
-                ipAddress: this.ipAddress,
-              },
-              err => {
-                expect(err).not.to.exist
-                this.UserMock.verify()
-                this.DeletedUserMock.verify()
-                done()
-              }
-            )
+          it('should delete the user', async function (ctx) {
+            await ctx.UserDeleter.promises.deleteUser(ctx.userId, {
+              deleterUser: { _id: ctx.deleterId },
+              ipAddress: ctx.ipAddress,
+            })
+
+            ctx.UserMock.verify()
+            ctx.DeletedUserMock.verify()
           })
           })
         })
         })
       })
       })
     })
     })
 
 
     describe('when the user cannot be deleted because they are a subscription admin', function () {
     describe('when the user cannot be deleted because they are a subscription admin', function () {
-      beforeEach(function () {
-        this.SubscriptionLocator.promises.getUsersSubscription.resolves({
+      beforeEach(function (ctx) {
+        ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({
           _id: 'some-subscription',
           _id: 'some-subscription',
         })
         })
       })
       })
 
 
-      it('fails with a SubscriptionAdminDeletionError', async function () {
+      it('fails with a SubscriptionAdminDeletionError', async function (ctx) {
         await expect(
         await expect(
-          this.UserDeleter.promises.deleteUser(this.userId)
+          ctx.UserDeleter.promises.deleteUser(ctx.userId)
         ).to.be.rejectedWith(Errors.SubscriptionAdminDeletionError)
         ).to.be.rejectedWith(Errors.SubscriptionAdminDeletionError)
       })
       })
 
 
-      it('should not create a deletedUser', async function () {
-        await expect(this.UserDeleter.promises.deleteUser(this.userId)).to.be
+      it('should not create a deletedUser', async function (ctx) {
+        await expect(ctx.UserDeleter.promises.deleteUser(ctx.userId)).to.be
           .rejected
           .rejected
-        this.DeletedUserMock.verify()
+        ctx.DeletedUserMock.verify()
       })
       })
 
 
-      it('should not remove the user from mongo', async function () {
-        await expect(this.UserDeleter.promises.deleteUser(this.userId)).to.be
+      it('should not remove the user from mongo', async function (ctx) {
+        await expect(ctx.UserDeleter.promises.deleteUser(ctx.userId)).to.be
           .rejected
           .rejected
-        this.UserMock.verify()
+        ctx.UserMock.verify()
       })
       })
     })
     })
   })
   })
 
 
   describe('ensureCanDeleteUser', function () {
   describe('ensureCanDeleteUser', function () {
-    it('should not return error when user can be deleted', async function () {
-      this.SubscriptionLocator.promises.getUsersSubscription.resolves(null)
+    it('should not return error when user can be deleted', async function (ctx) {
+      ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(null)
       let error
       let error
       try {
       try {
-        await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
+        await ctx.UserDeleter.promises.ensureCanDeleteUser(ctx.user)
       } catch (e) {
       } catch (e) {
         error = e
         error = e
       } finally {
       } finally {
@@ -509,13 +567,13 @@ describe('UserDeleter', function () {
       }
       }
     })
     })
 
 
-    it('should return custom error when user is group admin', async function () {
-      this.SubscriptionLocator.promises.getUsersSubscription.resolves({
+    it('should return custom error when user is group admin', async function (ctx) {
+      ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({
         _id: '123abc',
         _id: '123abc',
       })
       })
       let error
       let error
       try {
       try {
-        await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
+        await ctx.UserDeleter.promises.ensureCanDeleteUser(ctx.user)
       } catch (e) {
       } catch (e) {
         error = e
         error = e
       } finally {
       } finally {
@@ -523,13 +581,13 @@ describe('UserDeleter', function () {
       }
       }
     })
     })
 
 
-    it('propagates errors', async function () {
-      this.SubscriptionLocator.promises.getUsersSubscription.rejects(
+    it('propagates errors', async function (ctx) {
+      ctx.SubscriptionLocator.promises.getUsersSubscription.rejects(
         new Error('Some error')
         new Error('Some error')
       )
       )
       let error
       let error
       try {
       try {
-        await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
+        await ctx.UserDeleter.promises.ensureCanDeleteUser(ctx.user)
       } catch (e) {
       } catch (e) {
         error = e
         error = e
       } finally {
       } finally {
@@ -542,8 +600,8 @@ describe('UserDeleter', function () {
     const userId1 = new ObjectId()
     const userId1 = new ObjectId()
     const userId2 = new ObjectId()
     const userId2 = new ObjectId()
 
 
-    beforeEach(function () {
-      this.deletedUsers = [
+    beforeEach(function (ctx) {
+      ctx.deletedUsers = [
         {
         {
           user: { _id: userId1 },
           user: { _id: userId1 },
           deleterData: { deletedUserId: userId1 },
           deleterData: { deletedUserId: userId1 },
@@ -556,7 +614,7 @@ describe('UserDeleter', function () {
         },
         },
       ]
       ]
 
 
-      this.DeletedUserMock.expects('find')
+      ctx.DeletedUserMock.expects('find')
         .withArgs({
         .withArgs({
           'deleterData.deletedAt': {
           'deleterData.deletedAt': {
             $lt: new Date(moment().subtract(90, 'days')),
             $lt: new Date(moment().subtract(90, 'days')),
@@ -566,9 +624,9 @@ describe('UserDeleter', function () {
           },
           },
         })
         })
         .chain('exec')
         .chain('exec')
-        .resolves(this.deletedUsers)
-      for (const deletedUser of this.deletedUsers) {
-        this.DeletedUserMock.expects('findOne')
+        .resolves(ctx.deletedUsers)
+      for (const deletedUser of ctx.deletedUsers) {
+        ctx.DeletedUserMock.expects('findOne')
           .withArgs({
           .withArgs({
             'deleterData.deletedUserId': deletedUser.deleterData.deletedUserId,
             'deleterData.deletedUserId': deletedUser.deleterData.deletedUserId,
           })
           })
@@ -577,18 +635,18 @@ describe('UserDeleter', function () {
       }
       }
     })
     })
 
 
-    it('clears data from all deleted users', async function () {
-      await this.UserDeleter.promises.expireDeletedUsersAfterDuration()
-      for (const deletedUser of this.deletedUsers) {
+    it('clears data from all deleted users', async function (ctx) {
+      await ctx.UserDeleter.promises.expireDeletedUsersAfterDuration()
+      for (const deletedUser of ctx.deletedUsers) {
         expect(deletedUser.user).to.be.undefined
         expect(deletedUser.user).to.be.undefined
         expect(deletedUser.save.called).to.be.true
         expect(deletedUser.save.called).to.be.true
       }
       }
     })
     })
 
 
-    it('deletes audit logs for all deleted users', async function () {
-      await this.UserDeleter.promises.expireDeletedUsersAfterDuration()
-      for (const deletedUser of this.deletedUsers) {
-        expect(this.UserAuditLogEntry.deleteMany).to.have.been.calledWith({
+    it('deletes audit logs for all deleted users', async function (ctx) {
+      await ctx.UserDeleter.promises.expireDeletedUsersAfterDuration()
+      for (const deletedUser of ctx.deletedUsers) {
+        expect(ctx.UserAuditLogEntry.deleteMany).to.have.been.calledWith({
           userId: deletedUser.deleterData.deletedUserId,
           userId: deletedUser.deleterData.deletedUserId,
         })
         })
       }
       }
@@ -596,79 +654,81 @@ describe('UserDeleter', function () {
   })
   })
 
 
   describe('expireDeletedUser', function () {
   describe('expireDeletedUser', function () {
-    beforeEach(function () {
-      this.mockedDeletedUser = sinon.mock(
+    beforeEach(function (ctx) {
+      ctx.mockedDeletedUser = sinon.mock(
         new DeletedUser({
         new DeletedUser({
-          user: this.user,
+          user: ctx.user,
           deleterData: {
           deleterData: {
             deleterIpAddress: '1.1.1.1',
             deleterIpAddress: '1.1.1.1',
-            deletedUserId: this.userId,
+            deletedUserId: ctx.userId,
           },
           },
         })
         })
       )
       )
-      this.deletedUser = this.mockedDeletedUser.object
+      ctx.deletedUser = ctx.mockedDeletedUser.object
 
 
-      this.mockedDeletedUser.expects('save').resolves()
+      ctx.mockedDeletedUser.expects('save').resolves()
 
 
-      this.DeletedUserMock.expects('findOne')
-        .withArgs({ 'deleterData.deletedUserId': this.userId })
+      ctx.DeletedUserMock.expects('findOne')
+        .withArgs({ 'deleterData.deletedUserId': ctx.userId })
         .chain('exec')
         .chain('exec')
-        .resolves(this.deletedUser)
+        .resolves(ctx.deletedUser)
     })
     })
 
 
-    afterEach(function () {
-      this.mockedDeletedUser.restore()
+    afterEach(function (ctx) {
+      ctx.mockedDeletedUser.restore()
     })
     })
 
 
-    it('should find the user by user ID', async function () {
-      await this.UserDeleter.promises.expireDeletedUser(this.userId)
-      this.DeletedUserMock.verify()
+    it('should find the user by user ID', async function (ctx) {
+      await ctx.UserDeleter.promises.expireDeletedUser(ctx.userId)
+      ctx.DeletedUserMock.verify()
     })
     })
 
 
-    it('should remove the user data from mongo', async function () {
-      await this.UserDeleter.promises.expireDeletedUser(this.userId)
-      expect(this.deletedUser.user).not.to.exist
+    it('should remove the user data from mongo', async function (ctx) {
+      await ctx.UserDeleter.promises.expireDeletedUser(ctx.userId)
+      expect(ctx.deletedUser.user).not.to.exist
     })
     })
 
 
-    it('should remove the IP address from mongo', async function () {
-      await this.UserDeleter.promises.expireDeletedUser(this.userId)
-      expect(this.deletedUser.deleterData.ipAddress).not.to.exist
+    it('should remove the IP address from mongo', async function (ctx) {
+      await ctx.UserDeleter.promises.expireDeletedUser(ctx.userId)
+      expect(ctx.deletedUser.deleterData.ipAddress).not.to.exist
     })
     })
 
 
-    it('should not delete other deleterData fields', async function () {
-      await this.UserDeleter.promises.expireDeletedUser(this.userId)
-      expect(this.deletedUser.deleterData.deletedUserId).to.equal(this.userId)
+    it('should not delete other deleterData fields', async function (ctx) {
+      await ctx.UserDeleter.promises.expireDeletedUser(ctx.userId)
+      expect(ctx.deletedUser.deleterData.deletedUserId).to.equal(ctx.userId)
     })
     })
 
 
-    it('should save the record to mongo', async function () {
-      await this.UserDeleter.promises.expireDeletedUser(this.userId)
-      this.mockedDeletedUser.verify()
+    it('should save the record to mongo', async function (ctx) {
+      await ctx.UserDeleter.promises.expireDeletedUser(ctx.userId)
+      ctx.mockedDeletedUser.verify()
     })
     })
 
 
-    it('should fire the expireDeletedUser hook for modules', async function () {
-      await this.UserDeleter.promises.expireDeletedUser(this.userId)
-      expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
+    it('should fire the expireDeletedUser hook for modules', async function (ctx) {
+      await ctx.UserDeleter.promises.expireDeletedUser(ctx.userId)
+      expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
         'expireDeletedUser',
         'expireDeletedUser',
-        this.userId
+        ctx.userId
       )
       )
     })
     })
 
 
-    it('should delete Feeback', async function () {
-      await this.UserDeleter.promises.expireDeletedUser(this.userId)
-      expect(this.Feedback.deleteMany).to.have.been.calledWith({
-        userId: this.userId,
+    it('should delete Feeback', async function (ctx) {
+      await ctx.UserDeleter.promises.expireDeletedUser(ctx.userId)
+      expect(ctx.Feedback.deleteMany).to.have.been.calledWith({
+        userId: ctx.userId,
       })
       })
     })
     })
 
 
     describe('when called as a callback', function () {
     describe('when called as a callback', function () {
-      it('should expire the user', function (done) {
-        this.UserDeleter.expireDeletedUser(this.userId, err => {
-          expect(err).not.to.exist
-          this.DeletedUserMock.verify()
-          this.mockedDeletedUser.verify()
-          expect(this.deletedUser.user).not.to.exist
-          expect(this.deletedUser.deleterData.ipAddress).not.to.exist
-          done()
+      it('should expire the user', async function (ctx) {
+        await new Promise(resolve => {
+          ctx.UserDeleter.expireDeletedUser(ctx.userId, err => {
+            expect(err).not.to.exist
+            ctx.DeletedUserMock.verify()
+            ctx.mockedDeletedUser.verify()
+            expect(ctx.deletedUser.user).not.to.exist
+            expect(ctx.deletedUser.deleterData.ipAddress).not.to.exist
+            resolve()
+          })
         })
         })
       })
       })
     })
     })

+ 27 - 21
services/web/test/unit/src/User/UserHandler.test.mjs

@@ -1,51 +1,57 @@
-const sinon = require('sinon')
-const { expect } = require('chai')
-const modulePath = '../../../../app/src/Features/User/UserHandler.js'
-const SandboxedModule = require('sandboxed-module')
+import { vi, expect } from 'vitest'
+import sinon from 'sinon'
+const modulePath = '../../../../app/src/Features/User/UserHandler.mjs'
 
 
 describe('UserHandler', function () {
 describe('UserHandler', function () {
-  beforeEach(function () {
-    this.user = {
+  beforeEach(async function (ctx) {
+    ctx.user = {
       _id: '12390i',
       _id: '12390i',
       email: 'bob@bob.com',
       email: 'bob@bob.com',
       remove: sinon.stub().callsArgWith(0),
       remove: sinon.stub().callsArgWith(0),
     }
     }
 
 
-    this.TeamInvitesHandler = {
+    ctx.TeamInvitesHandler = {
       promises: {
       promises: {
         createTeamInvitesForLegacyInvitedEmail: sinon.stub().resolves(),
         createTeamInvitesForLegacyInvitedEmail: sinon.stub().resolves(),
       },
       },
     }
     }
 
 
-    this.db = {
+    ctx.db = {
       users: {
       users: {
         countDocuments: sinon.stub().resolves(2),
         countDocuments: sinon.stub().resolves(2),
       },
       },
     }
     }
 
 
-    this.UserHandler = SandboxedModule.require(modulePath, {
-      requires: {
-        '../Subscription/TeamInvitesHandler': this.TeamInvitesHandler,
-        '../../infrastructure/mongodb': { db: this.db },
-      },
-    })
+    vi.doMock(
+      '../../../../app/src/Features/Subscription/TeamInvitesHandler',
+      () => ({
+        default: ctx.TeamInvitesHandler,
+      })
+    )
+
+    vi.doMock('../../../../app/src/infrastructure/mongodb', () => ({
+      db: ctx.db,
+      READ_PREFERENCE_SECONDARY: 'read-preference-secondary',
+    }))
+
+    ctx.UserHandler = (await import(modulePath)).default
   })
   })
 
 
   describe('populateTeamInvites', function () {
   describe('populateTeamInvites', function () {
-    beforeEach(async function () {
-      await this.UserHandler.promises.populateTeamInvites(this.user)
+    beforeEach(async function (ctx) {
+      await ctx.UserHandler.promises.populateTeamInvites(ctx.user)
     })
     })
 
 
-    it('notifies the user about legacy team invites', function () {
-      this.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail
-        .calledWith(this.user.email)
+    it('notifies the user about legacy team invites', function (ctx) {
+      ctx.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail
+        .calledWith(ctx.user.email)
         .should.eq(true)
         .should.eq(true)
     })
     })
   })
   })
 
 
   describe('countActiveUsers', function () {
   describe('countActiveUsers', function () {
-    it('return user count from DB lookup', async function () {
-      expect(await this.UserHandler.promises.countActiveUsers()).to.equal(2)
+    it('return user count from DB lookup', async function (ctx) {
+      expect(await ctx.UserHandler.promises.countActiveUsers()).to.equal(2)
     })
     })
   })
   })
 })
 })

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است