Просмотр исходного кода

[web] Promisify LaunchpadController (#18497)

* Remove unused exports and decaffeinate comment

* Update export format

* Make `launchpadPage` async

* Make `_atLeastOneAdminExists` async

* Make `sendTestEmail` async

* Make `registerExternalAuthAdmin` async

* Make `registerAdmin` async

* Update tests LaunchpadControllerTests.js

* Use `expressify(...)`

* Fix unit tests: export `LaunchpadController`

* Fix unit tests by awaiting expressified functions (could include breaking changes!)

* Call `.exec()` after `User.updateOne(...)`

* Revert promisification of `expressify`

* Revert some `return` removal in tests

GitOrigin-RevId: c73a38bf1cf798830df046fe96d3516178655ea6
Antoine Clausse 2 лет назад
Родитель
Сommit
258289e65a

+ 146 - 185
services/web/modules/launchpad/app/src/LaunchpadController.js

@@ -1,25 +1,10 @@
-/* eslint-disable
-    n/handle-callback-err,
-    max-len,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let LaunchpadController
 const OError = require('@overleaf/o-error')
+const { expressify } = require('@overleaf/promise-utils')
 const Settings = require('@overleaf/settings')
 const Path = require('path')
-const Url = require('url')
 const logger = require('@overleaf/logger')
-const metrics = require('@overleaf/metrics')
 const UserRegistrationHandler = require('../../../../app/src/Features/User/UserRegistrationHandler')
 const EmailHandler = require('../../../../app/src/Features/Email/EmailHandler')
-const _ = require('lodash')
 const UserGetter = require('../../../../app/src/Features/User/UserGetter')
 const { User } = require('../../../../app/src/models/User')
 const AuthenticationManager = require('../../../../app/src/Features/Authentication/AuthenticationManager')
@@ -29,7 +14,7 @@ const {
   hasAdminAccess,
 } = require('../../../../app/src/Features/Helpers/AdminAuthorizationHelper')
 
-module.exports = LaunchpadController = {
+const _LaunchpadController = {
   _getAuthMethod() {
     if (Settings.ldap) {
       return 'ldap'
@@ -40,65 +25,47 @@ module.exports = LaunchpadController = {
     }
   },
 
-  launchpadPage(req, res, next) {
+  async launchpadPage(req, res) {
     // TODO: check if we're using external auth?
     //   * how does all this work with ldap and saml?
     const sessionUser = SessionManager.getSessionUser(req.session)
     const authMethod = LaunchpadController._getAuthMethod()
-    LaunchpadController._atLeastOneAdminExists(function (err, adminUserExists) {
-      if (err != null) {
-        return next(err)
+    const adminUserExists = await LaunchpadController._atLeastOneAdminExists()
+    if (!sessionUser) {
+      if (!adminUserExists) {
+        res.render(Path.resolve(__dirname, '../views/launchpad'), {
+          adminUserExists,
+          authMethod,
+        })
+      } else {
+        AuthenticationController.setRedirectInSession(req)
+        res.redirect('/login')
       }
-      if (!sessionUser) {
-        if (!adminUserExists) {
-          res.render(Path.resolve(__dirname, '../views/launchpad'), {
-            adminUserExists,
-            authMethod,
-          })
-        } else {
-          AuthenticationController.setRedirectInSession(req)
-          res.redirect('/login')
-        }
+    } else {
+      const user = await UserGetter.promises.getUser(sessionUser._id, {
+        isAdmin: 1,
+      })
+      if (hasAdminAccess(user)) {
+        res.render(Path.resolve(__dirname, '../views/launchpad'), {
+          wsUrl: Settings.wsUrl,
+          adminUserExists,
+          authMethod,
+        })
       } else {
-        UserGetter.getUser(
-          sessionUser._id,
-          { isAdmin: 1 },
-          function (err, user) {
-            if (err != null) {
-              return next(err)
-            }
-            if (hasAdminAccess(user)) {
-              res.render(Path.resolve(__dirname, '../views/launchpad'), {
-                wsUrl: Settings.wsUrl,
-                adminUserExists,
-                authMethod,
-              })
-            } else {
-              res.redirect('/restricted')
-            }
-          }
-        )
+        res.redirect('/restricted')
       }
-    })
+    }
   },
 
-  _atLeastOneAdminExists(callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    UserGetter.getUser(
+  async _atLeastOneAdminExists() {
+    const user = await UserGetter.promises.getUser(
       { isAdmin: true },
-      { _id: 1, isAdmin: 1 },
-      function (err, user) {
-        if (err != null) {
-          return callback(err)
-        }
-        callback(null, user != null)
-      }
+      { _id: 1, isAdmin: 1 }
     )
+    return Boolean(user)
   },
 
-  sendTestEmail(req, res, next) {
+  async sendTestEmail(req, res) {
     const { email } = req.body
     if (!email) {
       logger.debug({}, 'no email address supplied')
@@ -108,20 +75,20 @@ module.exports = LaunchpadController = {
     }
     logger.debug({ email }, 'sending test email')
     const emailOptions = { to: email }
-    EmailHandler.sendEmail('testEmail', emailOptions, function (err) {
-      if (err != null) {
-        OError.tag(err, 'error sending test email', {
-          email,
-        })
-        return next(err)
-      }
+    try {
+      await EmailHandler.promises.sendEmail('testEmail', emailOptions)
       logger.debug({ email }, 'sent test email')
       res.json({ message: res.locals.translate('email_sent') })
-    })
+    } catch (err) {
+      OError.tag(err, 'error sending test email', {
+        email,
+      })
+      throw err
+    }
   },
 
   registerExternalAuthAdmin(authMethod) {
-    return function (req, res, next) {
+    return expressify(async function (req, res) {
       if (LaunchpadController._getAuthMethod() !== authMethod) {
         logger.debug(
           { authMethod },
@@ -136,70 +103,65 @@ module.exports = LaunchpadController = {
       }
 
       logger.debug({ email }, 'attempted register first admin user')
-      LaunchpadController._atLeastOneAdminExists(function (err, exists) {
-        if (err != null) {
-          return next(err)
-        }
 
-        if (exists) {
-          logger.debug(
-            { email },
-            'already have at least one admin user, disallow'
-          )
-          return res.sendStatus(403)
-        }
+      const exists = await LaunchpadController._atLeastOneAdminExists()
 
-        const body = {
-          email,
-          password: 'password_here',
-          first_name: email,
-          last_name: '',
-        }
+      if (exists) {
         logger.debug(
-          { body, authMethod },
-          'creating admin account for specified external-auth user'
+          { email },
+          'already have at least one admin user, disallow'
         )
+        return res.sendStatus(403)
+      }
 
-        UserRegistrationHandler.registerNewUser(body, function (err, user) {
-          if (err != null) {
-            OError.tag(err, 'error with registerNewUser', {
-              email,
-              authMethod,
-            })
-            return next(err)
-          }
-
-          // Ignore spurious floating promises warning until we promisify
-          // eslint-disable-next-line @typescript-eslint/no-floating-promises
-          User.updateOne(
-            { _id: user._id },
-            {
-              $set: { isAdmin: true },
-              emails: [{ email }],
-            },
-            function (err) {
-              if (err != null) {
-                OError.tag(err, 'error setting user to admin', {
-                  user_id: user._id,
-                })
-                return next(err)
-              }
+      const body = {
+        email,
+        password: 'password_here',
+        first_name: email,
+        last_name: '',
+      }
+      logger.debug(
+        { body, authMethod },
+        'creating admin account for specified external-auth user'
+      )
 
-              AuthenticationController.setRedirectInSession(req, '/launchpad')
-              logger.debug(
-                { email, userId: user._id, authMethod },
-                'created first admin account'
-              )
+      let user
+      try {
+        user = await UserRegistrationHandler.promises.registerNewUser(body)
+      } catch (err) {
+        OError.tag(err, 'error with registerNewUser', {
+          email,
+          authMethod,
+        })
+        throw err
+      }
 
-              res.json({ redir: '/launchpad', email })
-            }
-          )
+      try {
+        await User.updateOne(
+          { _id: user._id },
+          {
+            $set: { isAdmin: true },
+            emails: [{ email }],
+          }
+        ).exec()
+      } catch (err) {
+        OError.tag(err, 'error setting user to admin', {
+          user_id: user._id,
         })
-      })
-    }
+        throw err
+      }
+
+      AuthenticationController.setRedirectInSession(req, '/launchpad')
+      logger.debug(
+        { email, userId: user._id, authMethod },
+        'created first admin account'
+      )
+
+      res.json({ redir: '/launchpad', email })
+    })
   },
 
-  registerAdmin(req, res, next) {
+  async registerAdmin(req, res) {
     const { email } = req.body
     const { password } = req.body
     if (!email || !password) {
@@ -208,71 +170,70 @@ module.exports = LaunchpadController = {
     }
 
     logger.debug({ email }, 'attempted register first admin user')
-    LaunchpadController._atLeastOneAdminExists(function (err, exists) {
-      if (err != null) {
-        return next(err)
-      }
+    const exists = await LaunchpadController._atLeastOneAdminExists()
 
-      if (exists) {
-        logger.debug(
-          { email: req.body.email },
-          'already have at least one admin user, disallow'
-        )
-        return res.status(403).json({
-          message: { type: 'error', text: 'admin user already exists' },
-        })
-      }
+    if (exists) {
+      logger.debug(
+        { email: req.body.email },
+        'already have at least one admin user, disallow'
+      )
+      return res.status(403).json({
+        message: { type: 'error', text: 'admin user already exists' },
+      })
+    }
 
-      const invalidEmail = AuthenticationManager.validateEmail(email)
-      if (invalidEmail) {
-        return res
-          .status(400)
-          .json({ message: { type: 'error', text: invalidEmail.message } })
-      }
+    const invalidEmail = AuthenticationManager.validateEmail(email)
+    if (invalidEmail) {
+      return res
+        .status(400)
+        .json({ message: { type: 'error', text: invalidEmail.message } })
+    }
 
-      const invalidPassword = AuthenticationManager.validatePassword(
-        password,
-        email
-      )
-      if (invalidPassword) {
-        return res
-          .status(400)
-          .json({ message: { type: 'error', text: invalidPassword.message } })
-      }
+    const invalidPassword = AuthenticationManager.validatePassword(
+      password,
+      email
+    )
+    if (invalidPassword) {
+      return res
+        .status(400)
+        .json({ message: { type: 'error', text: invalidPassword.message } })
+    }
 
-      const body = { email, password }
-      UserRegistrationHandler.registerNewUser(body, function (err, user) {
-        if (err != null) {
-          return next(err)
-        }
+    const body = { email, password }
 
-        logger.debug({ userId: user._id }, 'making user an admin')
-        // Ignore spurious floating promises warning until we promisify
-        // eslint-disable-next-line @typescript-eslint/no-floating-promises
-        User.updateOne(
-          { _id: user._id },
-          {
-            $set: {
-              isAdmin: true,
-              emails: [{ email }],
-            },
-          },
-          function (err) {
-            if (err != null) {
-              OError.tag(err, 'error setting user to admin', {
-                user_id: user._id,
-              })
-              return next(err)
-            }
+    const user = await UserRegistrationHandler.promises.registerNewUser(body)
 
-            logger.debug(
-              { email, userId: user._id },
-              'created first admin account'
-            )
-            res.json({ redir: '/launchpad' })
-          }
-        )
+    logger.debug({ userId: user._id }, 'making user an admin')
+
+    try {
+      await User.updateOne(
+        { _id: user._id },
+        {
+          $set: {
+            isAdmin: true,
+            emails: [{ email }],
+          },
+        }
+      ).exec()
+    } catch (err) {
+      OError.tag(err, 'error setting user to admin', {
+        user_id: user._id,
       })
-    })
+      throw err
+    }
+
+    logger.debug({ email, userId: user._id }, 'created first admin account')
+    res.json({ redir: '/launchpad' })
   },
 }
+
+const LaunchpadController = {
+  launchpadPage: expressify(_LaunchpadController.launchpadPage),
+  registerAdmin: expressify(_LaunchpadController.registerAdmin),
+  registerExternalAuthAdmin: _LaunchpadController.registerExternalAuthAdmin,
+  sendTestEmail: expressify(_LaunchpadController.sendTestEmail),
+  _atLeastOneAdminExists: _LaunchpadController._atLeastOneAdminExists,
+  _getAuthMethod: _LaunchpadController._getAuthMethod,
+}
+
+module.exports = LaunchpadController

+ 207 - 205
services/web/modules/launchpad/test/unit/src/LaunchpadControllerTests.js

@@ -11,9 +11,9 @@
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
 const SandboxedModule = require('sandboxed-module')
-const assert = require('assert')
 const { expect } = require('chai')
 const sinon = require('sinon')
+const MockResponse = require('../../../../../test/unit/src/helpers/MockResponse')
 const modulePath = require('path').join(
   __dirname,
   '../../../app/src/LaunchpadController.js'
@@ -36,10 +36,14 @@ describe('LaunchpadController', function () {
         }),
         '@overleaf/metrics': (this.Metrics = {}),
         '../../../../app/src/Features/User/UserRegistrationHandler':
-          (this.UserRegistrationHandler = {}),
+          (this.UserRegistrationHandler = {
+            promises: {},
+          }),
         '../../../../app/src/Features/Email/EmailHandler': (this.EmailHandler =
-          {}),
-        '../../../../app/src/Features/User/UserGetter': (this.UserGetter = {}),
+          { promises: {} }),
+        '../../../../app/src/Features/User/UserGetter': (this.UserGetter = {
+          promises: {},
+        }),
         '../../../../app/src/models/User': { User: this.User },
         '../../../../app/src/Features/Authentication/AuthenticationController':
           (this.AuthenticationController = {}),
@@ -58,21 +62,12 @@ describe('LaunchpadController', function () {
       session: {},
     }
 
-    this.res = {
-      render: sinon.stub(),
-      redirect: sinon.stub(),
-      json: sinon.stub(),
-      send: sinon.stub(),
-      sendStatus: sinon.stub(),
-      locals: {
-        translate(key) {
-          return key
-        },
+    this.res = new MockResponse()
+    this.res.locals = {
+      translate(key) {
+        return key
       },
     }
-    this.res.status = sinon.stub().returns(this.res)
-
-    return (this.next = sinon.stub())
   })
 
   describe('launchpadPage', function () {
@@ -91,12 +86,13 @@ describe('LaunchpadController', function () {
     describe('when the user is not logged in', function () {
       beforeEach(function () {
         this.SessionManager.getSessionUser = sinon.stub().returns(null)
-        return (this.res.render = sinon.stub())
       })
 
       describe('when there are no admins', function () {
-        beforeEach(function () {
-          this._atLeastOneAdminExists.callsArgWith(0, null, false)
+        beforeEach(function (done) {
+          this._atLeastOneAdminExists.resolves(false)
+          this.res.callback = done
+          this.next = sinon.stub().callsFake(() => done())
           return this.LaunchpadController.launchpadPage(
             this.req,
             this.res,
@@ -120,8 +116,10 @@ describe('LaunchpadController', function () {
       })
 
       describe('when there is at least one admin', function () {
-        beforeEach(function () {
-          this._atLeastOneAdminExists.callsArgWith(0, null, true)
+        beforeEach(function (done) {
+          this._atLeastOneAdminExists.resolves(true)
+          this.res.callback = done
+          this.next = sinon.stub().callsFake(() => done())
           return this.LaunchpadController.launchpadPage(
             this.req,
             this.res,
@@ -149,16 +147,16 @@ describe('LaunchpadController', function () {
           email: 'abcd@example.com',
         }
         this.SessionManager.getSessionUser = sinon.stub().returns(this.user)
-        this._atLeastOneAdminExists.callsArgWith(0, null, true)
-        this.res.render = sinon.stub()
-        return (this.res.redirect = sinon.stub())
+        this._atLeastOneAdminExists.resolves(true)
       })
 
       describe('when the user is an admin', function () {
-        beforeEach(function () {
-          this.UserGetter.getUser = sinon
+        beforeEach(function (done) {
+          this.UserGetter.promises.getUser = sinon
             .stub()
-            .callsArgWith(2, null, { isAdmin: true })
+            .resolves({ isAdmin: true })
+          this.res.callback = done
+          this.next = sinon.stub().callsFake(() => done())
           return this.LaunchpadController.launchpadPage(
             this.req,
             this.res,
@@ -183,10 +181,12 @@ describe('LaunchpadController', function () {
       })
 
       describe('when the user is not an admin', function () {
-        beforeEach(function () {
-          this.UserGetter.getUser = sinon
+        beforeEach(function (done) {
+          this.UserGetter.promises.getUser = sinon
             .stub()
-            .callsArgWith(2, null, { isAdmin: false })
+            .resolves({ isAdmin: false })
+          this.res.callback = done
+          this.next = sinon.stub().callsFake(() => done())
           return this.LaunchpadController.launchpadPage(
             this.req,
             this.res,
@@ -205,75 +205,54 @@ describe('LaunchpadController', function () {
   describe('_atLeastOneAdminExists', function () {
     describe('when there are no admins', function () {
       beforeEach(function () {
-        return (this.UserGetter.getUser = sinon
-          .stub()
-          .callsArgWith(2, null, null))
+        return (this.UserGetter.promises.getUser = sinon.stub().resolves(null))
       })
 
-      it('should callback with false', function (done) {
-        return this.LaunchpadController._atLeastOneAdminExists(
-          (err, exists) => {
-            expect(err).to.equal(null)
-            expect(exists).to.equal(false)
-            return done()
-          }
-        )
+      it('should callback with false', async function () {
+        const exists = await this.LaunchpadController._atLeastOneAdminExists()
+        expect(exists).to.equal(false)
       })
     })
 
     describe('when there are some admins', function () {
       beforeEach(function () {
-        return (this.UserGetter.getUser = sinon
+        return (this.UserGetter.promises.getUser = sinon
           .stub()
-          .callsArgWith(2, null, { _id: 'abcd' }))
+          .resolves({ _id: 'abcd' }))
       })
 
-      it('should callback with true', function (done) {
-        return this.LaunchpadController._atLeastOneAdminExists(
-          (err, exists) => {
-            expect(err).to.equal(null)
-            expect(exists).to.equal(true)
-            return done()
-          }
-        )
+      it('should callback with true', async function () {
+        const exists = await this.LaunchpadController._atLeastOneAdminExists()
+        expect(exists).to.equal(true)
       })
     })
 
     describe('when getUser produces an error', function () {
       beforeEach(function () {
-        return (this.UserGetter.getUser = sinon
+        return (this.UserGetter.promises.getUser = sinon
           .stub()
-          .callsArgWith(2, new Error('woops')))
+          .rejects(new Error('woops')))
       })
 
-      it('should produce an error', function (done) {
-        return this.LaunchpadController._atLeastOneAdminExists(
-          (err, exists) => {
-            expect(err).to.not.equal(null)
-            expect(err).to.be.instanceof(Error)
-            expect(exists).to.equal(undefined)
-            return done()
-          }
-        )
+      it('should produce an error', async function () {
+        await expect(this.LaunchpadController._atLeastOneAdminExists()).rejected
       })
     })
   })
 
   describe('sendTestEmail', function () {
     beforeEach(function () {
-      this.EmailHandler.sendEmail = sinon.stub().callsArgWith(2, null)
+      this.EmailHandler.promises.sendEmail = sinon.stub().resolves()
       this.req.body.email = 'someone@example.com'
-      this.res.sendStatus = sinon.stub()
       return (this.next = sinon.stub())
     })
 
-    it('should produce a 200 response', function () {
+    it('should produce a 200 response', function (done) {
+      this.res.callback = () => {
+        this.res.json.calledWith({ message: 'email_sent' }).should.equal(true)
+        done()
+      }
       this.LaunchpadController.sendTestEmail(this.req, this.res, this.next)
-      this.res.json
-        .calledWith({
-          message: 'email_sent',
-        })
-        .should.equal(true)
     })
 
     it('should not call next with an error', function () {
@@ -283,23 +262,26 @@ describe('LaunchpadController', function () {
 
     it('should have called sendEmail', function () {
       this.LaunchpadController.sendTestEmail(this.req, this.res, this.next)
-      this.EmailHandler.sendEmail.callCount.should.equal(1)
-      return this.EmailHandler.sendEmail
+      this.EmailHandler.promises.sendEmail.callCount.should.equal(1)
+      return this.EmailHandler.promises.sendEmail
         .calledWith('testEmail')
         .should.equal(true)
     })
 
     describe('when sendEmail produces an error', function () {
       beforeEach(function () {
-        return (this.EmailHandler.sendEmail = sinon
+        return (this.EmailHandler.promises.sendEmail = sinon
           .stub()
-          .callsArgWith(2, new Error('woops')))
+          .rejects(new Error('woops')))
       })
 
-      it('should call next with an error', function () {
+      it('should call next with an error', function (done) {
+        this.next = sinon.stub().callsFake(err => {
+          expect(err).to.be.instanceof(Error)
+          this.next.callCount.should.equal(1)
+          done()
+        })
         this.LaunchpadController.sendTestEmail(this.req, this.res, this.next)
-        this.next.callCount.should.equal(1)
-        return expect(this.next.lastCall.args[0]).to.be.instanceof(Error)
       })
     })
 
@@ -333,8 +315,8 @@ describe('LaunchpadController', function () {
     })
 
     describe('when all goes well', function () {
-      beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, null, false)
+      beforeEach(function (done) {
+        this._atLeastOneAdminExists.resolves(false)
         this.email = 'someone@example.com'
         this.password = 'a_really_bad_password'
         this.req.body.email = this.email
@@ -343,15 +325,17 @@ describe('LaunchpadController', function () {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon
+        this.UserRegistrationHandler.promises.registerNewUser = sinon
+          .stub()
+          .resolves(this.user)
+        this.User.updateOne = sinon
           .stub()
-          .callsArgWith(1, null, this.user)
-        this.User.updateOne = sinon.stub().callsArgWith(2, null)
+          .returns({ exec: sinon.stub().resolves() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
         this.AuthenticationManager.validateEmail = sinon.stub().returns(null)
         this.AuthenticationManager.validatePassword = sinon.stub().returns(null)
-        this.res.json = sinon.stub()
-        this.next = sinon.stub()
+        this.next = sinon.stub().callsFake(() => done())
+        this.res.callback = done
         return this.LaunchpadController.registerAdmin(
           this.req,
           this.res,
@@ -369,8 +353,10 @@ describe('LaunchpadController', function () {
       })
 
       it('should have called registerNewUser', function () {
-        this.UserRegistrationHandler.registerNewUser.callCount.should.equal(1)
-        return this.UserRegistrationHandler.registerNewUser
+        this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
+          1
+        )
+        return this.UserRegistrationHandler.promises.registerNewUser
           .calledWith({ email: this.email, password: this.password })
           .should.equal(true)
       })
@@ -393,7 +379,7 @@ describe('LaunchpadController', function () {
 
     describe('when no email is supplied', function () {
       beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, null, false)
+        this._atLeastOneAdminExists.resolves(false)
         this.email = undefined
         this.password = 'a_really_bad_password'
         this.req.body.email = this.email
@@ -402,10 +388,9 @@ describe('LaunchpadController', function () {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon.stub()
-        this.User.updateOne = sinon.stub()
+        this.UserRegistrationHandler.promises.registerNewUser = sinon.stub()
+        this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
-        this.res.sendStatus = sinon.stub()
         this.next = sinon.stub()
         return this.LaunchpadController.registerAdmin(
           this.req,
@@ -424,7 +409,7 @@ describe('LaunchpadController', function () {
       })
 
       it('should not call registerNewUser', function () {
-        return this.UserRegistrationHandler.registerNewUser.callCount.should.equal(
+        return this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
           0
         )
       })
@@ -432,7 +417,7 @@ describe('LaunchpadController', function () {
 
     describe('when no password is supplied', function () {
       beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, null, false)
+        this._atLeastOneAdminExists.resolves(false)
         this.email = 'someone@example.com'
         this.password = undefined
         this.req.body.email = this.email
@@ -441,10 +426,9 @@ describe('LaunchpadController', function () {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon.stub()
-        this.User.updateOne = sinon.stub()
+        this.UserRegistrationHandler.promises.registerNewUser = sinon.stub()
+        this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
-        this.res.sendStatus = sinon.stub()
         this.next = sinon.stub()
         return this.LaunchpadController.registerAdmin(
           this.req,
@@ -463,15 +447,15 @@ describe('LaunchpadController', function () {
       })
 
       it('should not call registerNewUser', function () {
-        return this.UserRegistrationHandler.registerNewUser.callCount.should.equal(
+        return this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
           0
         )
       })
     })
 
     describe('when an invalid email is supplied', function () {
-      beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, null, false)
+      beforeEach(function (done) {
+        this._atLeastOneAdminExists.resolves(false)
         this.email = 'someone@example.com'
         this.password = 'invalid password'
         this.req.body.email = this.email
@@ -480,15 +464,15 @@ describe('LaunchpadController', function () {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon.stub()
-        this.User.updateOne = sinon.stub()
+        this.UserRegistrationHandler.promises.registerNewUser = sinon.stub()
+        this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
         this.AuthenticationManager.validateEmail = sinon
           .stub()
           .returns(new Error('bad email'))
         this.AuthenticationManager.validatePassword = sinon.stub().returns(null)
-        this.res.sendStatus = sinon.stub()
-        this.next = sinon.stub()
+        this.res.callback = done
+        this.next = sinon.stub().callsFake(() => done())
         return this.LaunchpadController.registerAdmin(
           this.req,
           this.res,
@@ -505,15 +489,15 @@ describe('LaunchpadController', function () {
       })
 
       it('should not call registerNewUser', function () {
-        return this.UserRegistrationHandler.registerNewUser.callCount.should.equal(
+        return this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
           0
         )
       })
     })
 
     describe('when an invalid password is supplied', function () {
-      beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, null, false)
+      beforeEach(function (done) {
+        this._atLeastOneAdminExists.resolves(false)
         this.email = 'someone@example.com'
         this.password = 'invalid password'
         this.req.body.email = this.email
@@ -522,15 +506,15 @@ describe('LaunchpadController', function () {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon.stub()
-        this.User.updateOne = sinon.stub()
+        this.UserRegistrationHandler.promises.registerNewUser = sinon.stub()
+        this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
         this.AuthenticationManager.validateEmail = sinon.stub().returns(null)
         this.AuthenticationManager.validatePassword = sinon
           .stub()
           .returns(new Error('bad password'))
-        this.res.sendStatus = sinon.stub()
-        this.next = sinon.stub()
+        this.res.callback = done
+        this.next = sinon.stub().callsFake(() => done())
         return this.LaunchpadController.registerAdmin(
           this.req,
           this.res,
@@ -547,15 +531,15 @@ describe('LaunchpadController', function () {
       })
 
       it('should not call registerNewUser', function () {
-        return this.UserRegistrationHandler.registerNewUser.callCount.should.equal(
+        return this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
           0
         )
       })
     })
 
     describe('when there are already existing admins', function () {
-      beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, null, true)
+      beforeEach(function (done) {
+        this._atLeastOneAdminExists.resolves(true)
         this.email = 'someone@example.com'
         this.password = 'a_really_bad_password'
         this.req.body.email = this.email
@@ -564,13 +548,13 @@ describe('LaunchpadController', function () {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon.stub()
-        this.User.updateOne = sinon.stub()
+        this.UserRegistrationHandler.promises.registerNewUser = sinon.stub()
+        this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
         this.AuthenticationManager.validateEmail = sinon.stub().returns(null)
         this.AuthenticationManager.validatePassword = sinon.stub().returns(null)
-        this.res.sendStatus = sinon.stub()
-        this.next = sinon.stub()
+        this.res.callback = done
+        this.next = sinon.stub().callsFake(() => done())
         return this.LaunchpadController.registerAdmin(
           this.req,
           this.res,
@@ -584,15 +568,15 @@ describe('LaunchpadController', function () {
       })
 
       it('should not call registerNewUser', function () {
-        return this.UserRegistrationHandler.registerNewUser.callCount.should.equal(
+        return this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
           0
         )
       })
     })
 
     describe('when checking admins produces an error', function () {
-      beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, new Error('woops'))
+      beforeEach(function (done) {
+        this._atLeastOneAdminExists.rejects(new Error('woops'))
         this.email = 'someone@example.com'
         this.password = 'a_really_bad_password'
         this.req.body.email = this.email
@@ -601,11 +585,11 @@ describe('LaunchpadController', function () {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon.stub()
-        this.User.updateOne = sinon.stub()
+        this.UserRegistrationHandler.promises.registerNewUser = sinon.stub()
+        this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
-        this.res.sendStatus = sinon.stub()
-        this.next = sinon.stub()
+        this.res.callback = done
+        this.next = sinon.stub().callsFake(() => done())
         return this.LaunchpadController.registerAdmin(
           this.req,
           this.res,
@@ -623,15 +607,15 @@ describe('LaunchpadController', function () {
       })
 
       it('should not call registerNewUser', function () {
-        return this.UserRegistrationHandler.registerNewUser.callCount.should.equal(
+        return this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
           0
         )
       })
     })
 
     describe('when registerNewUser produces an error', function () {
-      beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, null, false)
+      beforeEach(function (done) {
+        this._atLeastOneAdminExists.resolves(false)
         this.email = 'someone@example.com'
         this.password = 'a_really_bad_password'
         this.req.body.email = this.email
@@ -640,15 +624,15 @@ describe('LaunchpadController', function () {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon
+        this.UserRegistrationHandler.promises.registerNewUser = sinon
           .stub()
-          .callsArgWith(1, new Error('woops'))
-        this.User.updateOne = sinon.stub()
+          .rejects(new Error('woops'))
+        this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
         this.AuthenticationManager.validateEmail = sinon.stub().returns(null)
         this.AuthenticationManager.validatePassword = sinon.stub().returns(null)
-        this.res.json = sinon.stub()
-        this.next = sinon.stub()
+        this.res.callback = done
+        this.next = sinon.stub().callsFake(() => done())
         return this.LaunchpadController.registerAdmin(
           this.req,
           this.res,
@@ -666,8 +650,10 @@ describe('LaunchpadController', function () {
       })
 
       it('should have called registerNewUser', function () {
-        this.UserRegistrationHandler.registerNewUser.callCount.should.equal(1)
-        return this.UserRegistrationHandler.registerNewUser
+        this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
+          1
+        )
+        return this.UserRegistrationHandler.promises.registerNewUser
           .calledWith({ email: this.email, password: this.password })
           .should.equal(true)
       })
@@ -678,8 +664,8 @@ describe('LaunchpadController', function () {
     })
 
     describe('when user update produces an error', function () {
-      beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, null, false)
+      beforeEach(function (done) {
+        this._atLeastOneAdminExists.resolves(false)
         this.email = 'someone@example.com'
         this.password = 'a_really_bad_password'
         this.req.body.email = this.email
@@ -688,15 +674,17 @@ describe('LaunchpadController', function () {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon
+        this.UserRegistrationHandler.promises.registerNewUser = sinon
           .stub()
-          .callsArgWith(1, null, this.user)
-        this.User.updateOne = sinon.stub().callsArgWith(2, new Error('woops'))
+          .resolves(this.user)
+        this.User.updateOne = sinon.stub().returns({
+          exec: sinon.stub().rejects(new Error('woops')),
+        })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
         this.AuthenticationManager.validateEmail = sinon.stub().returns(null)
         this.AuthenticationManager.validatePassword = sinon.stub().returns(null)
-        this.res.json = sinon.stub()
-        this.next = sinon.stub()
+        this.res.callback = done
+        this.next = sinon.stub().callsFake(() => done())
         return this.LaunchpadController.registerAdmin(
           this.req,
           this.res,
@@ -714,18 +702,20 @@ describe('LaunchpadController', function () {
       })
 
       it('should have called registerNewUser', function () {
-        this.UserRegistrationHandler.registerNewUser.callCount.should.equal(1)
-        return this.UserRegistrationHandler.registerNewUser
+        this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
+          1
+        )
+        return this.UserRegistrationHandler.promises.registerNewUser
           .calledWith({ email: this.email, password: this.password })
           .should.equal(true)
       })
     })
 
     describe('when overleaf', function () {
-      beforeEach(function () {
+      beforeEach(function (done) {
         this.Settings.overleaf = { one: 1 }
         this.Settings.createV1AccountOnLogin = true
-        this._atLeastOneAdminExists.callsArgWith(0, null, false)
+        this._atLeastOneAdminExists.resolves(false)
         this.email = 'someone@example.com'
         this.password = 'a_really_bad_password'
         this.req.body.email = this.email
@@ -734,18 +724,20 @@ describe('LaunchpadController', function () {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon
+        this.UserRegistrationHandler.promises.registerNewUser = sinon
+          .stub()
+          .resolves(this.user)
+        this.User.updateOne = sinon
           .stub()
-          .callsArgWith(1, null, this.user)
-        this.User.updateOne = sinon.stub().callsArgWith(2, null)
+          .returns({ exec: sinon.stub().resolves() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
         this.AuthenticationManager.validateEmail = sinon.stub().returns(null)
         this.AuthenticationManager.validatePassword = sinon.stub().returns(null)
-        this.UserGetter.getUser = sinon
+        this.UserGetter.promises.getUser = sinon
           .stub()
-          .callsArgWith(1, null, { _id: '1234' })
-        this.res.json = sinon.stub()
-        this.next = sinon.stub()
+          .resolves({ _id: '1234' })
+        this.next = sinon.stub().callsFake(() => done())
+        this.res.callback = done
         return this.LaunchpadController.registerAdmin(
           this.req,
           this.res,
@@ -763,8 +755,10 @@ describe('LaunchpadController', function () {
       })
 
       it('should have called registerNewUser', function () {
-        this.UserRegistrationHandler.registerNewUser.callCount.should.equal(1)
-        return this.UserRegistrationHandler.registerNewUser
+        this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
+          1
+        )
+        return this.UserRegistrationHandler.promises.registerNewUser
           .calledWith({ email: this.email, password: this.password })
           .should.equal(true)
       })
@@ -799,21 +793,23 @@ describe('LaunchpadController', function () {
     })
 
     describe('when all goes well', function () {
-      beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, null, false)
+      beforeEach(function (done) {
+        this._atLeastOneAdminExists.resolves(false)
         this.email = 'someone@example.com'
         this.req.body.email = this.email
         this.user = {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon
+        this.UserRegistrationHandler.promises.registerNewUser = sinon
           .stub()
-          .callsArgWith(1, null, this.user)
-        this.User.updateOne = sinon.stub().callsArgWith(2, null)
+          .resolves(this.user)
+        this.User.updateOne = sinon
+          .stub()
+          .returns({ exec: sinon.stub().resolves() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
-        this.res.json = sinon.stub()
-        this.next = sinon.stub()
+        this.res.callback = done
+        this.next = sinon.stub().callsFake(() => done())
         return this.LaunchpadController.registerExternalAuthAdmin('ldap')(
           this.req,
           this.res,
@@ -831,8 +827,10 @@ describe('LaunchpadController', function () {
       })
 
       it('should have called registerNewUser', function () {
-        this.UserRegistrationHandler.registerNewUser.callCount.should.equal(1)
-        return this.UserRegistrationHandler.registerNewUser
+        this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
+          1
+        )
+        return this.UserRegistrationHandler.promises.registerNewUser
           .calledWith({
             email: this.email,
             password: 'password_here',
@@ -867,17 +865,16 @@ describe('LaunchpadController', function () {
 
     describe('when the authMethod is invalid', function () {
       beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, null, false)
+        this._atLeastOneAdminExists.resolves(false)
         this.email = undefined
         this.req.body.email = this.email
         this.user = {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon.stub()
-        this.User.updateOne = sinon.stub()
+        this.UserRegistrationHandler.promises.registerNewUser = sinon.stub()
+        this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
-        this.res.sendStatus = sinon.stub()
         this.next = sinon.stub()
         return this.LaunchpadController.registerExternalAuthAdmin(
           'NOTAVALIDAUTHMETHOD'
@@ -894,7 +891,7 @@ describe('LaunchpadController', function () {
       })
 
       it('should not call registerNewUser', function () {
-        return this.UserRegistrationHandler.registerNewUser.callCount.should.equal(
+        return this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
           0
         )
       })
@@ -902,17 +899,16 @@ describe('LaunchpadController', function () {
 
     describe('when no email is supplied', function () {
       beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, null, false)
+        this._atLeastOneAdminExists.resolves(false)
         this.email = undefined
         this.req.body.email = this.email
         this.user = {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon.stub()
-        this.User.updateOne = sinon.stub()
+        this.UserRegistrationHandler.promises.registerNewUser = sinon.stub()
+        this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
-        this.res.sendStatus = sinon.stub()
         this.next = sinon.stub()
         return this.LaunchpadController.registerExternalAuthAdmin('ldap')(
           this.req,
@@ -931,26 +927,26 @@ describe('LaunchpadController', function () {
       })
 
       it('should not call registerNewUser', function () {
-        return this.UserRegistrationHandler.registerNewUser.callCount.should.equal(
+        return this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
           0
         )
       })
     })
 
     describe('when there are already existing admins', function () {
-      beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, null, true)
+      beforeEach(function (done) {
+        this._atLeastOneAdminExists.resolves(true)
         this.email = 'someone@example.com'
         this.req.body.email = this.email
         this.user = {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon.stub()
-        this.User.updateOne = sinon.stub()
+        this.UserRegistrationHandler.promises.registerNewUser = sinon.stub()
+        this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
-        this.res.sendStatus = sinon.stub()
-        this.next = sinon.stub()
+        this.res.callback = done
+        this.next = sinon.stub().callsFake(() => done())
         return this.LaunchpadController.registerExternalAuthAdmin('ldap')(
           this.req,
           this.res,
@@ -964,26 +960,26 @@ describe('LaunchpadController', function () {
       })
 
       it('should not call registerNewUser', function () {
-        return this.UserRegistrationHandler.registerNewUser.callCount.should.equal(
+        return this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
           0
         )
       })
     })
 
     describe('when checking admins produces an error', function () {
-      beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, new Error('woops'))
+      beforeEach(function (done) {
+        this._atLeastOneAdminExists.rejects(new Error('woops'))
         this.email = 'someone@example.com'
         this.req.body.email = this.email
         this.user = {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon.stub()
-        this.User.updateOne = sinon.stub()
+        this.UserRegistrationHandler.promises.registerNewUser = sinon.stub()
+        this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
-        this.res.sendStatus = sinon.stub()
-        this.next = sinon.stub()
+        this.res.callback = done
+        this.next = sinon.stub().callsFake(() => done())
         return this.LaunchpadController.registerExternalAuthAdmin('ldap')(
           this.req,
           this.res,
@@ -1001,28 +997,28 @@ describe('LaunchpadController', function () {
       })
 
       it('should not call registerNewUser', function () {
-        return this.UserRegistrationHandler.registerNewUser.callCount.should.equal(
+        return this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
           0
         )
       })
     })
 
     describe('when registerNewUser produces an error', function () {
-      beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, null, false)
+      beforeEach(function (done) {
+        this._atLeastOneAdminExists.resolves(false)
         this.email = 'someone@example.com'
         this.req.body.email = this.email
         this.user = {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon
+        this.UserRegistrationHandler.promises.registerNewUser = sinon
           .stub()
-          .callsArgWith(1, new Error('woops'))
-        this.User.updateOne = sinon.stub()
+          .rejects(new Error('woops'))
+        this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
-        this.res.json = sinon.stub()
-        this.next = sinon.stub()
+        this.res.callback = done
+        this.next = sinon.stub().callsFake(() => done())
         return this.LaunchpadController.registerExternalAuthAdmin('ldap')(
           this.req,
           this.res,
@@ -1040,8 +1036,10 @@ describe('LaunchpadController', function () {
       })
 
       it('should have called registerNewUser', function () {
-        this.UserRegistrationHandler.registerNewUser.callCount.should.equal(1)
-        return this.UserRegistrationHandler.registerNewUser
+        this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
+          1
+        )
+        return this.UserRegistrationHandler.promises.registerNewUser
           .calledWith({
             email: this.email,
             password: 'password_here',
@@ -1057,21 +1055,23 @@ describe('LaunchpadController', function () {
     })
 
     describe('when user update produces an error', function () {
-      beforeEach(function () {
-        this._atLeastOneAdminExists.callsArgWith(0, null, false)
+      beforeEach(function (done) {
+        this._atLeastOneAdminExists.resolves(false)
         this.email = 'someone@example.com'
         this.req.body.email = this.email
         this.user = {
           _id: 'abcdef',
           email: this.email,
         }
-        this.UserRegistrationHandler.registerNewUser = sinon
+        this.UserRegistrationHandler.promises.registerNewUser = sinon
           .stub()
-          .callsArgWith(1, null, this.user)
-        this.User.updateOne = sinon.stub().callsArgWith(2, new Error('woops'))
+          .resolves(this.user)
+        this.User.updateOne = sinon.stub().returns({
+          exec: sinon.stub().rejects(new Error('woops')),
+        })
         this.AuthenticationController.setRedirectInSession = sinon.stub()
-        this.res.json = sinon.stub()
-        this.next = sinon.stub()
+        this.res.callback = done
+        this.next = sinon.stub().callsFake(() => done())
         return this.LaunchpadController.registerExternalAuthAdmin('ldap')(
           this.req,
           this.res,
@@ -1089,8 +1089,10 @@ describe('LaunchpadController', function () {
       })
 
       it('should have called registerNewUser', function () {
-        this.UserRegistrationHandler.registerNewUser.callCount.should.equal(1)
-        return this.UserRegistrationHandler.registerNewUser
+        this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal(
+          1
+        )
+        return this.UserRegistrationHandler.promises.registerNewUser
           .calledWith({
             email: this.email,
             password: 'password_here',