Bladeren bron

[web] Promisify ProjectController (#18477)

* Create `promiseAuto` util to replace `async.auto`

* Promisify `BrandVariationsHandler.getBrandVariationById`

* Promisify `updateProjectSettings`

* Promisify `updateProjectAdminSettings`

* Promisify `newProject`

* Promisify `deleteProject`

* Promisify `loadEditor`

* Fix brandVariation loading in promise auto

* Promisify `_refreshFeatures`

* Promisify `_injectProjectUsers`

* Fix `no-inner-declarations`

* Promisify `cloneProject`

* Promisify `userProjectsJson`

* Promisify `projectEntitiesJson`

* Promisify `restoreProject`

* Promisify `renameProject`

* Additional warning fix

* Update unit tests

* Fixup `updateProjectSettings`: call jobs inside the Promise.all

* Use `expressify(...)` instead of manually call `next(err)`

https://github.com/overleaf/internal/pull/18477#discussion_r1613611987
https://github.com/overleaf/internal/pull/18477#discussion_r1613621146
https://github.com/overleaf/internal/pull/18477#discussion_r1613634000
...

* Replace Promise.all by sequencial awaits

https://github.com/overleaf/internal/pull/18477#discussion_r1613852746
https://github.com/overleaf/internal/pull/18477#discussion_r1613611987

* Remove manual throws of 500. Let the generic error handler catch them.

https://github.com/overleaf/internal/pull/18477#discussion_r1613623446
https://github.com/overleaf/internal/pull/18477#discussion_r1613628955

* Promisify `untrashProject`

https://github.com/overleaf/internal/pull/18477#discussion_r1613627783

* Promisify `expireDeletedProjectsAfterDuration`

* Promisify `archiveProject`

* Promisify `unarchiveProject`

* Promisify `trashProject`

* Promisify `expireDeletedProject`

* Use async `setTimeout` from `timers/promise`

https://github.com/overleaf/internal/pull/18477#discussion_r1613843085

* Remove unused `_injectProjectUsers`

https://github.com/overleaf/internal/pull/18477#discussion_r1613855766

* Add missing exec in queries (?)

Not sure if that makes a real difference but it's more consistent with the rest of the code

* Catch floating promises

https://github.com/overleaf/internal/pull/18477#discussion_r1613868876

* Replace custom `promiseAuto` by `p-props` from NPM

https://github.com/overleaf/internal/pull/18477#discussion_r1613393294

* Downgrade `p-props` to v4. Later versions require ESM

* Simplify code around `splitTestAssignments`

GitOrigin-RevId: 84d37f7aa9227b5b9acf9eeb5db1b78afc01b6ee
Antoine Clausse 2 jaren geleden
bovenliggende
commit
36f0a3e01a

+ 2 - 0
package-lock.json

@@ -44498,6 +44498,7 @@
         "openai": "^4.36.0",
         "otplib": "^12.0.1",
         "p-limit": "^2.3.0",
+        "p-props": "4.0.0",
         "parse-data-url": "^2.0.0",
         "passport": "^0.6.0",
         "passport-google-oauth20": "^2.0.0",
@@ -53119,6 +53120,7 @@
         "openai": "^4.36.0",
         "otplib": "^12.0.1",
         "p-limit": "^2.3.0",
+        "p-props": "4.0.0",
         "parse-data-url": "^2.0.0",
         "passport": "^0.6.0",
         "passport-google-oauth20": "^2.0.0",

+ 1 - 0
services/web/app/src/Features/Authorization/AuthorizationManager.js

@@ -70,6 +70,7 @@ async function getPublicAccessLevel(projectId) {
  *
  * @param userId - The id of the user that wants to access the project.
  * @param projectId - The id of the project to be accessed.
+ * @param {string} token
  * @param {Object} opts
  * @param {boolean} opts.ignoreSiteAdmin - Do not consider whether the user is
  *     a site admin.

+ 4 - 0
services/web/app/src/Features/BrandVariations/BrandVariationsHandler.js

@@ -4,9 +4,13 @@ const settings = require('@overleaf/settings')
 const logger = require('@overleaf/logger')
 const V1Api = require('../V1/V1Api')
 const sanitizeHtml = require('sanitize-html')
+const { promisify } = require('@overleaf/promise-utils')
 
 module.exports = {
   getBrandVariationById,
+  promises: {
+    getBrandVariationById: promisify(getBrandVariationById),
+  },
 }
 
 function getBrandVariationById(brandVariationId, callback) {

File diff suppressed because it is too large
+ 491 - 645
services/web/app/src/Features/Project/ProjectController.js


+ 1 - 0
services/web/package.json

@@ -142,6 +142,7 @@
     "openai": "^4.36.0",
     "otplib": "^12.0.1",
     "p-limit": "^2.3.0",
+    "p-props": "4.0.0",
     "parse-data-url": "^2.0.0",
     "passport": "^0.6.0",
     "passport-google-oauth20": "^2.0.0",

+ 1 - 0
services/web/test/unit/bootstrap.js

@@ -50,6 +50,7 @@ SandboxedModule.configure({
   ignoreMissing: true,
   requires: getSandboxedModuleRequires(),
   globals: {
+    AbortController,
     AbortSignal,
     Buffer,
     Promise,

+ 149 - 101
services/web/test/unit/src/Project/ProjectControllerTests.js

@@ -38,48 +38,73 @@ describe('ProjectController', function () {
     }
     this.token = 'some-token'
     this.ProjectDeleter = {
-      deleteProject: sinon.stub().callsArg(2),
-      restoreProject: sinon.stub().callsArg(1),
+      promises: {
+        deleteProject: sinon.stub().resolves(),
+        restoreProject: sinon.stub().resolves(),
+      },
       findArchivedProjects: sinon.stub(),
     }
     this.ProjectDuplicator = {
-      duplicate: sinon.stub().callsArgWith(4, null, { _id: this.project_id }),
+      promises: {
+        duplicate: sinon.stub().resolves({ _id: this.project_id }),
+      },
     }
     this.ProjectCreationHandler = {
-      createExampleProject: sinon
-        .stub()
-        .callsArgWith(2, null, { _id: this.project_id }),
-      createBasicProject: sinon
-        .stub()
-        .callsArgWith(2, null, { _id: this.project_id }),
+      promises: {
+        createExampleProject: sinon.stub().resolves({ _id: this.project_id }),
+        createBasicProject: sinon.stub().resolves({ _id: this.project_id }),
+      },
+    }
+    this.SubscriptionLocator = {
+      promises: {
+        getUsersSubscription: sinon.stub().resolves(),
+      },
     }
-    this.SubscriptionLocator = { getUsersSubscription: sinon.stub() }
     this.LimitationsManager = {
       hasPaidSubscription: sinon.stub(),
-      userIsMemberOfGroupSubscription: sinon
-        .stub()
-        .callsArgWith(1, null, false),
+      promises: {
+        userIsMemberOfGroupSubscription: sinon.stub().resolves(false),
+      },
     }
     this.TagsHandler = {
-      getTagsForProject: sinon.stub().callsArgWith(2, null, [
-        {
-          name: 'test',
-          project_ids: [this.project_id],
-        },
-      ]),
+      promises: {
+        getTagsForProject: sinon.stub().resolves([
+          {
+            name: 'test',
+            project_ids: [this.project_id],
+          },
+        ]),
+      },
       addProjectToTags: sinon.stub(),
     }
-    this.UserModel = { findById: sinon.stub(), updateOne: sinon.stub() }
+    this.UserModel = {
+      findById: sinon.stub().returns({ exec: sinon.stub().resolves() }),
+      updateOne: sinon.stub().returns({ exec: sinon.stub().resolves() }),
+    }
     this.AuthorizationManager = {
-      getPrivilegeLevelForProject: sinon.stub(),
+      promises: {
+        getPrivilegeLevelForProject: sinon.stub(),
+      },
       isRestrictedUser: sinon.stub().returns(false),
     }
-    this.EditorController = { renameProject: sinon.stub() }
-    this.InactiveProjectManager = { reactivateProjectIfRequired: sinon.stub() }
-    this.ProjectUpdateHandler = { markAsOpened: sinon.stub() }
+    this.EditorController = {
+      promises: {
+        renameProject: sinon.stub().resolves(),
+      },
+    }
+    this.InactiveProjectManager = {
+      promises: { reactivateProjectIfRequired: sinon.stub() },
+    }
+    this.ProjectUpdateHandler = {
+      promises: {
+        markAsOpened: sinon.stub().resolves(),
+      },
+    }
     this.ProjectGetter = {
-      findAllUsersProjects: sinon.stub(),
-      getProject: sinon.stub(),
+      promises: {
+        findAllUsersProjects: sinon.stub().resolves(),
+        getProject: sinon.stub().resolves(),
+      },
     }
     this.ProjectHelper = {
       isArchived: sinon.stub(),
@@ -88,7 +113,6 @@ describe('ProjectController', function () {
       getAllowedImagesForUser: sinon.stub().returns([]),
     }
     this.SessionManager = {
-      getLoggedInUser: sinon.stub().callsArgWith(1, null, this.user),
       getLoggedInUserId: sinon.stub().returns(this.user._id),
       getSessionUser: sinon.stub().returns(this.user),
       isUserLoggedIn: sinon.stub().returns(true),
@@ -100,30 +124,36 @@ describe('ProjectController', function () {
       getRequestToken: sinon.stub().returns(this.token),
     }
     this.CollaboratorsGetter = {
-      userIsTokenMember: sinon.stub().callsArgWith(2, null, false),
-      isUserInvitedMemberOfProject: sinon.stub().callsArgWith(2, null, true),
+      promises: {
+        userIsTokenMember: sinon.stub().resolves(false),
+        isUserInvitedMemberOfProject: sinon.stub().resolves(true),
+      },
     }
     this.ProjectEntityHandler = {}
     this.UserGetter = {
       getUserFullEmails: sinon.stub().yields(null, []),
-      getUser: sinon
-        .stub()
-        .callsArgWith(2, null, { lastLoginIp: '192.170.18.2' }),
+      getUser: sinon.stub().resolves({ lastLoginIp: '192.170.18.2' }),
     }
     this.Features = {
       hasFeature: sinon.stub(),
     }
     this.FeaturesUpdater = {
       featuresEpochIsCurrent: sinon.stub().returns(true),
-      refreshFeatures: sinon.stub().yields(null, this.user),
+      promises: {
+        refreshFeatures: sinon.stub().resolves(this.user),
+      },
     }
     this.BrandVariationsHandler = {
-      getBrandVariationById: sinon
-        .stub()
-        .callsArgWith(1, null, this.brandVariationDetails),
+      promises: {
+        getBrandVariationById: sinon
+          .stub()
+          .resolves(this.brandVariationDetails),
+      },
     }
     this.TpdsProjectFlusher = {
-      flushProjectToTpdsIfNeeded: sinon.stub().yields(),
+      promises: {
+        flushProjectToTpdsIfNeeded: sinon.stub().resolves(),
+      },
     }
     this.Metrics = {
       Timer: class {
@@ -138,10 +168,14 @@ describe('ProjectController', function () {
       getAssignment: sinon.stub().yields(null, { variant: 'default' }),
     }
     this.SplitTestSessionHandler = {
-      sessionMaintenance: sinon.stub().yields(),
+      promises: {
+        sessionMaintenance: sinon.stub().resolves(),
+      },
     }
     this.InstitutionsFeatures = {
-      hasLicence: sinon.stub().callsArgWith(1, null, false),
+      promises: {
+        hasLicence: sinon.stub().resolves(false),
+      },
     }
     this.SubscriptionViewModelBuilder = {
       getBestSubscription: sinon.stub().yields(null, { type: 'free' }),
@@ -150,7 +184,9 @@ describe('ProjectController', function () {
       getSurvey: sinon.stub().yields(null, {}),
     }
     this.ProjectAuditLogHandler = {
-      addEntry: sinon.stub().yields(),
+      promises: {
+        addEntry: sinon.stub().resolves(),
+      },
     }
     this.TutorialHandler = {
       getInactiveTutorials: sinon.stub().returns([]),
@@ -195,7 +231,9 @@ describe('ProjectController', function () {
         '../Subscription/SubscriptionViewModelBuilder':
           this.SubscriptionViewModelBuilder,
         '../Spelling/SpellingHandler': {
-          getUserDictionary: sinon.stub().yields(null, []),
+          promises: {
+            getUserDictionary: sinon.stub().resolves([]),
+          },
         },
         '../Institutions/InstitutionsFeatures': this.InstitutionsFeatures,
         '../Survey/SurveyHandler': this.SurveyHandler,
@@ -235,10 +273,10 @@ describe('ProjectController', function () {
 
   describe('updateProjectSettings', function () {
     it('should update the name', function (done) {
-      this.EditorController.renameProject = sinon.stub().callsArg(2)
+      this.EditorController.promises.renameProject = sinon.stub().resolves()
       this.req.body = { name: (this.name = 'New name') }
       this.res.sendStatus = code => {
-        this.EditorController.renameProject
+        this.EditorController.promises.renameProject
           .calledWith(this.project_id, this.name)
           .should.equal(true)
         code.should.equal(204)
@@ -248,10 +286,10 @@ describe('ProjectController', function () {
     })
 
     it('should update the compiler', function (done) {
-      this.EditorController.setCompiler = sinon.stub().callsArg(2)
+      this.EditorController.promises.setCompiler = sinon.stub().resolves()
       this.req.body = { compiler: (this.compiler = 'pdflatex') }
       this.res.sendStatus = code => {
-        this.EditorController.setCompiler
+        this.EditorController.promises.setCompiler
           .calledWith(this.project_id, this.compiler)
           .should.equal(true)
         code.should.equal(204)
@@ -261,10 +299,10 @@ describe('ProjectController', function () {
     })
 
     it('should update the imageName', function (done) {
-      this.EditorController.setImageName = sinon.stub().callsArg(2)
+      this.EditorController.promises.setImageName = sinon.stub().resolves()
       this.req.body = { imageName: (this.imageName = 'texlive-1234.5') }
       this.res.sendStatus = code => {
-        this.EditorController.setImageName
+        this.EditorController.promises.setImageName
           .calledWith(this.project_id, this.imageName)
           .should.equal(true)
         code.should.equal(204)
@@ -274,10 +312,12 @@ describe('ProjectController', function () {
     })
 
     it('should update the spell check language', function (done) {
-      this.EditorController.setSpellCheckLanguage = sinon.stub().callsArg(2)
+      this.EditorController.promises.setSpellCheckLanguage = sinon
+        .stub()
+        .resolves()
       this.req.body = { spellCheckLanguage: (this.languageCode = 'fr') }
       this.res.sendStatus = code => {
-        this.EditorController.setSpellCheckLanguage
+        this.EditorController.promises.setSpellCheckLanguage
           .calledWith(this.project_id, this.languageCode)
           .should.equal(true)
         code.should.equal(204)
@@ -287,10 +327,10 @@ describe('ProjectController', function () {
     })
 
     it('should update the root doc', function (done) {
-      this.EditorController.setRootDoc = sinon.stub().callsArg(2)
+      this.EditorController.promises.setRootDoc = sinon.stub().resolves()
       this.req.body = { rootDocId: (this.rootDocId = 'root-doc-id') }
       this.res.sendStatus = code => {
-        this.EditorController.setRootDoc
+        this.EditorController.promises.setRootDoc
           .calledWith(this.project_id, this.rootDocId)
           .should.equal(true)
         code.should.equal(204)
@@ -302,12 +342,14 @@ describe('ProjectController', function () {
 
   describe('updateProjectAdminSettings', function () {
     it('should update the public access level', function (done) {
-      this.EditorController.setPublicAccessLevel = sinon.stub().callsArg(2)
+      this.EditorController.promises.setPublicAccessLevel = sinon
+        .stub()
+        .resolves()
       this.req.body = {
         publicAccessLevel: 'readOnly',
       }
       this.res.sendStatus = code => {
-        this.EditorController.setPublicAccessLevel
+        this.EditorController.promises.setPublicAccessLevel
           .calledWith(this.project_id, 'readOnly')
           .should.equal(true)
         code.should.equal(204)
@@ -317,12 +359,14 @@ describe('ProjectController', function () {
     })
 
     it('should record the change in the project audit log', function (done) {
-      this.EditorController.setPublicAccessLevel = sinon.stub().callsArg(2)
+      this.EditorController.promises.setPublicAccessLevel = sinon
+        .stub()
+        .resolves()
       this.req.body = {
         publicAccessLevel: 'readOnly',
       }
       this.res.sendStatus = code => {
-        this.ProjectAuditLogHandler.addEntry
+        this.ProjectAuditLogHandler.promises.addEntry
           .calledWith(
             this.project_id,
             'toggle-access-level',
@@ -343,7 +387,7 @@ describe('ProjectController', function () {
   describe('deleteProject', function () {
     it('should call the project deleter', function (done) {
       this.res.sendStatus = code => {
-        this.ProjectDeleter.deleteProject
+        this.ProjectDeleter.promises.deleteProject
           .calledWith(this.project_id, {
             deleterUser: this.user,
             ipAddress: this.req.ip,
@@ -359,7 +403,7 @@ describe('ProjectController', function () {
   describe('restoreProject', function () {
     it('should tell the project deleter', function (done) {
       this.res.sendStatus = code => {
-        this.ProjectDeleter.restoreProject
+        this.ProjectDeleter.promises.restoreProject
           .calledWith(this.project_id)
           .should.equal(true)
         code.should.equal(200)
@@ -372,7 +416,7 @@ describe('ProjectController', function () {
   describe('cloneProject', function () {
     it('should call the project duplicator', function (done) {
       this.res.json = json => {
-        this.ProjectDuplicator.duplicate
+        this.ProjectDuplicator.promises.duplicate
           .calledWith(this.user, this.project_id, this.projectName)
           .should.equal(true)
         json.project_id.should.equal(this.project_id)
@@ -386,10 +430,10 @@ describe('ProjectController', function () {
     it('should call the projectCreationHandler with createExampleProject', function (done) {
       this.req.body.template = 'example'
       this.res.json = json => {
-        this.ProjectCreationHandler.createExampleProject
+        this.ProjectCreationHandler.promises.createExampleProject
           .calledWith(this.user._id, this.projectName)
           .should.equal(true)
-        this.ProjectCreationHandler.createBasicProject.called.should.equal(
+        this.ProjectCreationHandler.promises.createBasicProject.called.should.equal(
           false
         )
         done()
@@ -400,10 +444,10 @@ describe('ProjectController', function () {
     it('should call the projectCreationHandler with createBasicProject', function (done) {
       this.req.body.template = 'basic'
       this.res.json = json => {
-        this.ProjectCreationHandler.createExampleProject.called.should.equal(
+        this.ProjectCreationHandler.promises.createExampleProject.called.should.equal(
           false
         )
-        this.ProjectCreationHandler.createBasicProject
+        this.ProjectCreationHandler.promises.createBasicProject
           .calledWith(this.user._id, this.projectName)
           .should.equal(true)
         done()
@@ -419,10 +463,10 @@ describe('ProjectController', function () {
     })
 
     it('should call the editor controller', function (done) {
-      this.EditorController.renameProject.callsArgWith(2)
+      this.EditorController.promises.renameProject.resolves()
       this.res.sendStatus = code => {
         code.should.equal(200)
-        this.EditorController.renameProject
+        this.EditorController.promises.renameProject
           .calledWith(this.project_id, this.newProjectName)
           .should.equal(true)
         done()
@@ -432,8 +476,7 @@ describe('ProjectController', function () {
 
     it('should send an error to next() if there is a problem', function (done) {
       let error
-      this.EditorController.renameProject.callsArgWith(
-        2,
+      this.EditorController.promises.renameProject.rejects(
         (error = new Error('problem'))
       )
       const next = e => {
@@ -470,17 +513,17 @@ describe('ProjectController', function () {
           zotero: { encrypted: 'bbbb' },
         },
       }
-      this.ProjectGetter.getProject.callsArgWith(2, null, this.project)
-      this.UserModel.findById.callsArgWith(2, null, this.user)
-      this.SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {})
-      this.AuthorizationManager.getPrivilegeLevelForProject.callsArgWith(
-        3,
-        null,
+      this.ProjectGetter.promises.getProject.resolves(this.project)
+      this.UserModel.findById.returns({
+        exec: sinon.stub().resolves(this.user),
+      })
+      this.SubscriptionLocator.promises.getUsersSubscription.resolves({})
+      this.AuthorizationManager.promises.getPrivilegeLevelForProject.resolves(
         'owner'
       )
       this.ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub()
-      this.InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1)
-      this.ProjectUpdateHandler.markAsOpened.callsArgWith(1)
+      this.InactiveProjectManager.promises.reactivateProjectIfRequired.resolves()
+      this.ProjectUpdateHandler.promises.markAsOpened.resolves()
     })
 
     it('should render the project/editor page', function (done) {
@@ -526,7 +569,7 @@ describe('ProjectController', function () {
         opts.isRestrictedTokenMember.should.equal(false)
         return done()
       }
-      return this.ProjectController.loadEditor(this.req, this.res)
+      this.ProjectController.loadEditor(this.req, this.res)
     })
 
     it('should set isRestrictedTokenMember when appropriate', function (done) {
@@ -536,12 +579,12 @@ describe('ProjectController', function () {
         opts.isRestrictedTokenMember.should.equal(true)
         return done()
       }
-      return this.ProjectController.loadEditor(this.req, this.res)
+      this.ProjectController.loadEditor(this.req, this.res)
     })
 
     it('should invoke the session maintenance for logged in user', function (done) {
       this.res.render = () => {
-        this.SplitTestSessionHandler.sessionMaintenance.should.have.been.calledWith(
+        this.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith(
           this.req,
           this.user
         )
@@ -553,7 +596,7 @@ describe('ProjectController', function () {
     it('should invoke the session maintenance for anonymous user', function (done) {
       this.SessionManager.getLoggedInUserId.returns(null)
       this.res.render = () => {
-        this.SplitTestSessionHandler.sessionMaintenance.should.have.been.calledWith(
+        this.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith(
           this.req
         )
         done()
@@ -571,11 +614,16 @@ describe('ProjectController', function () {
     })
 
     it('should not render the page if the project can not be accessed', function (done) {
-      this.AuthorizationManager.getPrivilegeLevelForProject = sinon
+      this.AuthorizationManager.promises.getPrivilegeLevelForProject = sinon
         .stub()
-        .callsArgWith(3, null, null)
+        .resolves(null)
       this.res.sendStatus = (resCode, opts) => {
         resCode.should.equal(401)
+        this.AuthorizationManager.promises.getPrivilegeLevelForProject.should.have.been.calledWith(
+          this.user._id,
+          this.project_id,
+          'some-token'
+        )
         done()
       }
       this.ProjectController.loadEditor(this.req, this.res)
@@ -583,7 +631,7 @@ describe('ProjectController', function () {
 
     it('should reactivateProjectIfRequired', function (done) {
       this.res.render = (pageName, opts) => {
-        this.InactiveProjectManager.reactivateProjectIfRequired
+        this.InactiveProjectManager.promises.reactivateProjectIfRequired
           .calledWith(this.project_id)
           .should.equal(true)
         done()
@@ -605,7 +653,7 @@ describe('ProjectController', function () {
 
     it('should mark project as opened', function (done) {
       this.res.render = (pageName, opts) => {
-        this.ProjectUpdateHandler.markAsOpened
+        this.ProjectUpdateHandler.promises.markAsOpened
           .calledWith(this.project_id)
           .should.equal(true)
         done()
@@ -614,9 +662,9 @@ describe('ProjectController', function () {
     })
 
     it('should call the brand variations handler for branded projects', function (done) {
-      this.ProjectGetter.getProject.callsArgWith(2, null, this.brandedProject)
+      this.ProjectGetter.promises.getProject.resolves(this.brandedProject)
       this.res.render = (pageName, opts) => {
-        this.BrandVariationsHandler.getBrandVariationById
+        this.BrandVariationsHandler.promises.getBrandVariationById
           .calledWith()
           .should.equal(true)
         done()
@@ -626,7 +674,7 @@ describe('ProjectController', function () {
 
     it('should not call the brand variations handler for unbranded projects', function (done) {
       this.res.render = (pageName, opts) => {
-        this.BrandVariationsHandler.getBrandVariationById.called.should.equal(
+        this.BrandVariationsHandler.promises.getBrandVariationById.called.should.equal(
           false
         )
         done()
@@ -635,7 +683,7 @@ describe('ProjectController', function () {
     })
 
     it('should expose the brand variation details as locals for branded projects', function (done) {
-      this.ProjectGetter.getProject.callsArgWith(2, null, this.brandedProject)
+      this.ProjectGetter.promises.getProject.resolves(this.brandedProject)
       this.res.render = (pageName, opts) => {
         opts.brandVariation.should.deep.equal(this.brandVariationDetails)
         done()
@@ -645,7 +693,7 @@ describe('ProjectController', function () {
 
     it('flushes the project to TPDS if a flush is pending', function (done) {
       this.res.render = () => {
-        this.TpdsProjectFlusher.flushProjectToTpdsIfNeeded.should.have.been.calledWith(
+        this.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded.should.have.been.calledWith(
           this.project_id
         )
         done()
@@ -656,7 +704,7 @@ describe('ProjectController', function () {
     it('should refresh the user features if the epoch is outdated', function (done) {
       this.FeaturesUpdater.featuresEpochIsCurrent = sinon.stub().returns(false)
       this.res.render = () => {
-        this.FeaturesUpdater.refreshFeatures.should.have.been.calledWith(
+        this.FeaturesUpdater.promises.refreshFeatures.should.have.been.calledWith(
           this.user._id,
           'load-editor'
         )
@@ -898,9 +946,9 @@ describe('ProjectController', function () {
         // default to saas enabled
         this.Features.hasFeature.withArgs('saas').returns(true)
         // default to without a subscription
-        this.SubscriptionLocator.getUsersSubscription = sinon
+        this.SubscriptionLocator.promises.getUsersSubscription = sinon
           .stub()
-          .callsArgWith(1, null, null)
+          .resolves(null)
       })
       it('should not show without the saas feature', function (done) {
         this.Features.hasFeature.withArgs('saas').returns(false)
@@ -918,9 +966,9 @@ describe('ProjectController', function () {
         this.ProjectController.loadEditor(this.req, this.res)
       })
       it('should not show for a user with a personal subscription', function (done) {
-        this.SubscriptionLocator.getUsersSubscription = sinon
+        this.SubscriptionLocator.promises.getUsersSubscription = sinon
           .stub()
-          .callsArgWith(1, null, {})
+          .resolves({})
         this.res.render = (pageName, opts) => {
           expect(opts.showUpgradePrompt).to.equal(false)
           done()
@@ -928,9 +976,9 @@ describe('ProjectController', function () {
         this.ProjectController.loadEditor(this.req, this.res)
       })
       it('should not show for a user who is a member of a group subscription', function (done) {
-        this.LimitationsManager.userIsMemberOfGroupSubscription = sinon
+        this.LimitationsManager.promises.userIsMemberOfGroupSubscription = sinon
           .stub()
-          .callsArgWith(1, null, true)
+          .resolves(true)
         this.res.render = (pageName, opts) => {
           expect(opts.showUpgradePrompt).to.equal(false)
           done()
@@ -938,9 +986,9 @@ describe('ProjectController', function () {
         this.ProjectController.loadEditor(this.req, this.res)
       })
       it('should not show for a user with an affiliated paid university', function (done) {
-        this.InstitutionsFeatures.hasLicence = sinon
+        this.InstitutionsFeatures.promises.hasLicence = sinon
           .stub()
-          .callsArgWith(1, null, true)
+          .resolves(true)
         this.res.render = (pageName, opts) => {
           expect(opts.showUpgradePrompt).to.equal(false)
           done()
@@ -999,9 +1047,9 @@ describe('ProjectController', function () {
         .withArgs(projects[3], this.user._id)
         .returns(false)
 
-      this.ProjectGetter.findAllUsersProjects = sinon
+      this.ProjectGetter.promises.findAllUsersProjects = sinon
         .stub()
-        .callsArgWith(2, null, [])
+        .resolves([])
       this.ProjectController._buildProjectList = sinon.stub().returns(projects)
       this.SessionManager.getLoggedInUserId = sinon
         .stub()
@@ -1033,9 +1081,9 @@ describe('ProjectController', function () {
         { path: '/main.tex', doc: true },
       ]
       this.files = [{ path: '/things/a.txt' }]
-      this.ProjectGetter.getProject = sinon
+      this.ProjectGetter.promises.getProject = sinon
         .stub()
-        .callsArgWith(1, null, this.project)
+        .resolves(this.project)
       this.ProjectEntityHandler.getAllEntitiesFromProject = sinon
         .stub()
         .returns({ docs: this.docs, files: this.files })
@@ -1051,7 +1099,7 @@ describe('ProjectController', function () {
             { path: '/things/b.txt', type: 'doc' },
           ],
         })
-        expect(this.ProjectGetter.getProject.callCount).to.equal(1)
+        expect(this.ProjectGetter.promises.getProject.callCount).to.equal(1)
         expect(
           this.ProjectEntityHandler.getAllEntitiesFromProject.callCount
         ).to.equal(1)

Some files were not shown because too many files changed in this diff