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

Merge pull request #17380 from overleaf/dp-mongoose-callback-toke-access-handler

Promisify TokenAccessHandler and TokenAccessHandlerTests

GitOrigin-RevId: 835081e78977456a59b7e16043fd6dcbdbce3ade
David 2 лет назад
Родитель
Сommit
43007539a0

+ 126 - 135
services/web/app/src/Features/TokenAccess/TokenAccessHandler.js

@@ -7,7 +7,7 @@ const Settings = require('@overleaf/settings')
 const logger = require('@overleaf/logger')
 const V1Api = require('../V1/V1Api')
 const crypto = require('crypto')
-const { promisifyAll } = require('@overleaf/promise-utils')
+const { callbackifyAll } = require('@overleaf/promise-utils')
 const Analytics = require('../Analytics/AnalyticsManager')
 
 const READ_AND_WRITE_TOKEN_PATTERN = '([0-9]+[a-z]{6,12})'
@@ -23,27 +23,22 @@ const TokenAccessHandler = {
     Settings.allowAnonymousReadAndWriteSharing === true,
 
   READ_AND_WRITE_TOKEN_PATTERN,
-  READ_AND_WRITE_TOKEN_REGEX: new RegExp(`^${READ_AND_WRITE_TOKEN_PATTERN}$`),
-  READ_AND_WRITE_URL_REGEX: new RegExp(`^/${READ_AND_WRITE_TOKEN_PATTERN}$`),
-
   READ_ONLY_TOKEN_PATTERN,
-  READ_ONLY_TOKEN_REGEX: new RegExp(`^${READ_ONLY_TOKEN_PATTERN}$`),
-  READ_ONLY_URL_REGEX: new RegExp(`^/read/${READ_ONLY_TOKEN_PATTERN}$`),
 
-  makeReadAndWriteTokenUrl(token) {
+  _makeReadAndWriteTokenUrl(token) {
     return `/${token}`
   },
 
-  makeReadOnlyTokenUrl(token) {
+  _makeReadOnlyTokenUrl(token) {
     return `/read/${token}`
   },
 
   makeTokenUrl(token) {
     const tokenType = TokenAccessHandler.getTokenType(token)
     if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) {
-      return TokenAccessHandler.makeReadAndWriteTokenUrl(token)
+      return TokenAccessHandler._makeReadAndWriteTokenUrl(token)
     } else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) {
-      return TokenAccessHandler.makeReadOnlyTokenUrl(token)
+      return TokenAccessHandler._makeReadOnlyTokenUrl(token)
     } else {
       throw new Error('invalid token type')
     }
@@ -85,24 +80,22 @@ const TokenAccessHandler = {
     return project.publicAccesLevel === PublicAccessLevels.TOKEN_BASED
   },
 
-  _projectFindOne(query, callback) {
-    Project.findOne(
-      query,
-      {
-        _id: 1,
-        tokens: 1,
-        publicAccesLevel: 1,
-        owner_ref: 1,
-        name: 1,
-        tokenAccessReadOnly_refs: 1,
-        tokenAccessReadAndWrite_refs: 1,
-      },
-      callback
-    )
+  async _projectFindOne(query) {
+    return await Project.findOne(query, {
+      _id: 1,
+      tokens: 1,
+      publicAccesLevel: 1,
+      owner_ref: 1,
+      name: 1,
+      tokenAccessReadOnly_refs: 1,
+      tokenAccessReadAndWrite_refs: 1,
+    }).exec()
   },
 
-  getProjectByReadOnlyToken(token, callback) {
-    TokenAccessHandler._projectFindOne({ 'tokens.readOnly': token }, callback)
+  async getProjectByReadOnlyToken(token) {
+    return await TokenAccessHandler._projectFindOne({
+      'tokens.readOnly': token,
+    })
   },
 
   _extractNumericPrefix(token) {
@@ -113,92 +106,83 @@ const TokenAccessHandler = {
     return token.match(/^\d+(\w+)/)
   },
 
-  getProjectByReadAndWriteToken(token, callback) {
+  async getProjectByReadAndWriteToken(token) {
     const numericPrefixMatch = TokenAccessHandler._extractNumericPrefix(token)
     if (!numericPrefixMatch) {
-      return callback(null, null)
+      return null
     }
     const numerics = numericPrefixMatch[1]
-    TokenAccessHandler._projectFindOne(
-      {
-        'tokens.readAndWritePrefix': numerics,
-      },
-      function (err, project) {
-        if (err != null) {
-          return callback(err)
-        }
-        if (project == null) {
-          return callback(null, null)
-        }
-        try {
-          if (
-            !crypto.timingSafeEqual(
-              Buffer.from(token),
-              Buffer.from(project.tokens.readAndWrite)
-            )
-          ) {
-            logger.err(
-              { projectId: project._id },
-              'read-and-write token match on numeric section, but not on full token'
-            )
-            callback(null, null)
-          } else {
-            callback(null, project)
-          }
-        } catch (error) {
-          err = error
-          logger.err(
-            { projectId: project._id, cryptoErr: err },
-            'error comparing tokens'
-          )
-          callback(null, null)
-        }
+
+    const project = await TokenAccessHandler._projectFindOne({
+      'tokens.readAndWritePrefix': numerics,
+    })
+
+    if (project == null) {
+      return null
+    }
+
+    try {
+      if (
+        !crypto.timingSafeEqual(
+          Buffer.from(token),
+          Buffer.from(project.tokens.readAndWrite)
+        )
+      ) {
+        logger.err(
+          { projectId: project._id },
+          'read-and-write token match on numeric section, but not on full token'
+        )
+        return null
+      } else {
+        return project
       }
-    )
+    } catch (error) {
+      logger.err({ projectId: project._id, error }, 'error comparing tokens')
+      return null
+    }
   },
 
-  getProjectByToken(tokenType, token, callback) {
+  async getProjectByToken(tokenType, token) {
     if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) {
-      TokenAccessHandler.getProjectByReadOnlyToken(token, callback)
+      return await TokenAccessHandler.getProjectByReadOnlyToken(token)
     } else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) {
-      TokenAccessHandler.getProjectByReadAndWriteToken(token, callback)
-    } else {
-      callback(new Error('invalid token type'))
+      return await TokenAccessHandler.getProjectByReadAndWriteToken(token)
     }
+    throw new Error('invalid token type')
   },
 
-  addReadOnlyUserToProject(userId, projectId, callback) {
+  async addReadOnlyUserToProject(userId, projectId) {
     userId = new ObjectId(userId.toString())
     projectId = new ObjectId(projectId.toString())
     Analytics.recordEventForUser(userId, 'project-joined', {
       mode: 'read-only',
     })
-    Project.updateOne(
+
+    return await Project.updateOne(
       {
         _id: projectId,
       },
       {
         $addToSet: { tokenAccessReadOnly_refs: userId },
-      },
-      callback
-    )
+      }
+    ).exec()
   },
 
-  addReadAndWriteUserToProject(userId, projectId, callback) {
+  async addReadAndWriteUserToProject(userId, projectId) {
     userId = new ObjectId(userId.toString())
     projectId = new ObjectId(projectId.toString())
     Analytics.recordEventForUser(userId, 'project-joined', {
       mode: 'read-write',
     })
-    Project.updateOne(
+
+    return await Project.updateOne(
       {
         _id: projectId,
       },
       {
         $addToSet: { tokenAccessReadAndWrite_refs: userId },
-      },
-      callback
-    )
+      }
+    ).exec()
   },
 
   grantSessionTokenAccess(req, projectId, token) {
@@ -219,65 +203,58 @@ const TokenAccessHandler = {
     return token
   },
 
-  validateTokenForAnonymousAccess(projectId, token, callback) {
+  async validateTokenForAnonymousAccess(projectId, token, callback) {
     if (!token) {
-      return callback(null, false, false)
+      return { isValidReadAndWrite: false, isValidReadOnly: false }
     }
+
     const tokenType = TokenAccessHandler.getTokenType(token)
     if (!tokenType) {
-      return callback(new Error('invalid token type'))
+      throw new Error('invalid token type')
     }
-    TokenAccessHandler.getProjectByToken(tokenType, token, (err, project) => {
-      if (err) {
-        return callback(err)
-      }
-      if (
-        !project ||
-        !TokenAccessHandler.tokenAccessEnabledForProject(project) ||
-        project._id.toString() !== projectId.toString()
-      ) {
-        return callback(null, false, false)
-      }
-      // TODO: think about cleaning up this interface and its usage in AuthorizationManager
-      callback(
-        null,
+
+    const project = await TokenAccessHandler.getProjectByToken(tokenType, token)
+
+    if (
+      !project ||
+      !TokenAccessHandler.tokenAccessEnabledForProject(project) ||
+      project._id.toString() !== projectId.toString()
+    ) {
+      return { isValidReadAndWrite: false, isValidReadOnly: false }
+    }
+
+    // TODO: think about cleaning up this interface and its usage in AuthorizationManager
+    return {
+      isValidReadAndWrite:
         tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE &&
-          TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED,
-        tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY
-      )
-    })
+        TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED,
+      isValidReadOnly: tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY,
+    }
   },
 
-  getV1DocPublishedInfo(token, callback) {
+  async getV1DocPublishedInfo(token) {
     // default to allowing access
     if (!Settings.apis.v1 || !Settings.apis.v1.url) {
-      return callback(null, { allow: true })
+      return { allow: true }
     }
-    V1Api.request(
-      { url: `/api/v1/overleaf/docs/${token}/is_published` },
-      function (err, response, body) {
-        if (err != null) {
-          return callback(err)
-        }
-        callback(null, body)
-      }
-    )
+
+    const { body } = await V1Api.promises.request({
+      url: `/api/v1/overleaf/docs/${token}/is_published`,
+    })
+    return body
   },
 
-  getV1DocInfo(token, v2UserId, callback) {
+  async getV1DocInfo(token, v2UserId) {
     if (!Settings.apis || !Settings.apis.v1) {
-      return callback(null, {
+      return {
         exists: true,
         exported: false,
-      })
+      }
     }
+
     const v1Url = `/api/v1/overleaf/docs/${token}/info`
-    V1Api.request({ url: v1Url }, function (err, response, body) {
-      if (err != null) {
-        return callback(err)
-      }
-      callback(null, body)
-    })
+    const { body } = await V1Api.promises.request({ url: v1Url })
+    return body
   },
 
   createTokenHashPrefix(token) {
@@ -337,19 +314,33 @@ const TokenAccessHandler = {
   },
 }
 
-TokenAccessHandler.promises = promisifyAll(TokenAccessHandler, {
-  without: [
-    'getTokenType',
-    'tokenAccessEnabledForProject',
-    '_extractNumericPrefix',
-    '_extractStringSuffix',
-    '_projectFindOne',
-    'grantSessionTokenAccess',
-    'getRequestToken',
-  ],
-  multiResult: {
-    validateTokenForAnonymousAccess: ['isValidReadAndWrite', 'isValidReadOnly'],
-  },
-})
-
-module.exports = TokenAccessHandler
+module.exports = {
+  ...TokenAccessHandler,
+  ...callbackifyAll(TokenAccessHandler, {
+    multiResult: {
+      validateTokenForAnonymousAccess: [
+        'isValidReadAndWrite',
+        'isValidReadOnly',
+      ],
+    },
+    without: [
+      'makeTokenUrl',
+      'getTokenType',
+      'isReadOnlyToken',
+      'isReadAndWriteToken',
+      'isValidToken',
+      'tokenAccessEnabledForProject',
+      'grantSessionTokenAccess',
+      'getRequestToken',
+      'createTokenHashPrefix',
+      'normalizeTokenHashPrefix',
+      'checkTokenHashPrefix',
+      '_makeReadAndWriteTokenUrl',
+      '_makeReadOnlyTokenUrl',
+      '_projectFindOne',
+      '_extractNumericPrefix',
+      '_extractStringSuffix',
+    ],
+  }),
+  promises: TokenAccessHandler,
+}

+ 260 - 335
services/web/test/unit/src/TokenAccess/TokenAccessHandlerTests.js

@@ -26,7 +26,9 @@ describe('TokenAccessHandler', function () {
         '@overleaf/metrics': (this.Metrics = { inc: sinon.stub() }),
         '@overleaf/settings': (this.settings = {}),
         '../V1/V1Api': (this.V1Api = {
-          request: sinon.stub(),
+          promises: {
+            request: sinon.stub(),
+          },
         }),
         crypto: (this.Crypto = require('crypto')),
         '../Analytics/AnalyticsManager': (this.Analytics = {
@@ -57,19 +59,18 @@ describe('TokenAccessHandler', function () {
   describe('getProjectByReadOnlyToken', function () {
     beforeEach(function () {
       this.token = 'abcdefabcdef'
-      this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project)
+      this.Project.findOne = sinon.stub().returns({
+        exec: sinon.stub().resolves(this.project),
+      })
     })
 
-    it('should get the project', function (done) {
-      this.TokenAccessHandler.getProjectByReadOnlyToken(
-        this.token,
-        (err, project) => {
-          expect(err).to.not.exist
-          expect(project).to.exist
-          expect(this.Project.findOne.callCount).to.equal(1)
-          done()
-        }
-      )
+    it('should get the project', async function () {
+      const project =
+        await this.TokenAccessHandler.promises.getProjectByReadOnlyToken(
+          this.token
+        )
+      expect(project).to.exist
+      expect(this.Project.findOne.callCount).to.equal(1)
     })
   })
 
@@ -81,69 +82,55 @@ describe('TokenAccessHandler', function () {
         readAndWrite: this.token,
         readAndWritePrefix: '1234',
       }
-      this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project)
+      this.Project.findOne = sinon.stub().returns({
+        exec: sinon.stub().resolves(this.project),
+      })
     })
 
     afterEach(function () {
       this.Crypto.timingSafeEqual.restore()
     })
 
-    it('should get the project and do timing-safe comparison', function (done) {
-      this.TokenAccessHandler.getProjectByReadAndWriteToken(
-        this.token,
-        (err, project) => {
-          expect(err).to.not.exist
-          expect(project).to.exist
-          expect(this.Crypto.timingSafeEqual.callCount).to.equal(1)
-          expect(
-            this.Crypto.timingSafeEqual.calledWith(Buffer.from(this.token))
-          ).to.equal(true)
-          expect(this.Project.findOne.callCount).to.equal(1)
-          done()
-        }
-      )
+    it('should get the project and do timing-safe comparison', async function () {
+      const project =
+        await this.TokenAccessHandler.promises.getProjectByReadAndWriteToken(
+          this.token
+        )
+      expect(project).to.exist
+      expect(this.Crypto.timingSafeEqual.callCount).to.equal(1)
+      expect(
+        this.Crypto.timingSafeEqual.calledWith(Buffer.from(this.token))
+      ).to.equal(true)
+      expect(this.Project.findOne.callCount).to.equal(1)
     })
   })
 
   describe('addReadOnlyUserToProject', function () {
     beforeEach(function () {
-      this.Project.updateOne = sinon.stub().callsArgWith(2, null)
+      this.Project.updateOne = sinon.stub().returns({
+        exec: sinon.stub().resolves(null),
+      })
     })
 
-    it('should call Project.updateOne', function (done) {
-      this.TokenAccessHandler.addReadOnlyUserToProject(
+    it('should call Project.updateOne', async function () {
+      await this.TokenAccessHandler.promises.addReadOnlyUserToProject(
         this.userId,
-        this.projectId,
-        err => {
-          expect(err).to.not.exist
-          expect(this.Project.updateOne.callCount).to.equal(1)
-          expect(
-            this.Project.updateOne.calledWith({
-              _id: this.projectId,
-            })
-          ).to.equal(true)
-          expect(
-            this.Project.updateOne.lastCall.args[1].$addToSet
-          ).to.have.keys('tokenAccessReadOnly_refs')
-          sinon.assert.calledWith(
-            this.Analytics.recordEventForUser,
-            this.userId,
-            'project-joined',
-            { mode: 'read-only' }
-          )
-          done()
-        }
+        this.projectId
       )
-    })
-
-    it('should not produce an error', function (done) {
-      this.TokenAccessHandler.addReadOnlyUserToProject(
+      expect(this.Project.updateOne.callCount).to.equal(1)
+      expect(
+        this.Project.updateOne.calledWith({
+          _id: this.projectId,
+        })
+      ).to.equal(true)
+      expect(this.Project.updateOne.lastCall.args[1].$addToSet).to.have.keys(
+        'tokenAccessReadOnly_refs'
+      )
+      sinon.assert.calledWith(
+        this.Analytics.recordEventForUser,
         this.userId,
-        this.projectId,
-        err => {
-          expect(err).to.not.exist
-          done()
-        }
+        'project-joined',
+        { mode: 'read-only' }
       )
     })
 
@@ -151,61 +138,47 @@ describe('TokenAccessHandler', function () {
       beforeEach(function () {
         this.Project.updateOne = sinon
           .stub()
-          .callsArgWith(2, new Error('woops'))
+          .returns({ exec: sinon.stub().rejects(new Error('woops')) })
       })
 
-      it('should produce an error', function (done) {
-        this.TokenAccessHandler.addReadOnlyUserToProject(
-          this.userId,
-          this.projectId,
-          err => {
-            expect(err).to.exist
-            done()
-          }
-        )
+      it('should be rejected', async function () {
+        await expect(
+          this.TokenAccessHandler.promises.addReadOnlyUserToProject(
+            this.userId,
+            this.projectId
+          )
+        ).to.be.rejected
       })
     })
   })
 
   describe('addReadAndWriteUserToProject', function () {
     beforeEach(function () {
-      this.Project.updateOne = sinon.stub().callsArgWith(2, null)
+      this.Project.updateOne = sinon
+        .stub()
+        .returns({ exec: sinon.stub().resolves(null) })
     })
 
-    it('should call Project.updateOne', function (done) {
-      this.TokenAccessHandler.addReadAndWriteUserToProject(
+    it('should call Project.updateOne', async function () {
+      await this.TokenAccessHandler.promises.addReadAndWriteUserToProject(
         this.userId,
-        this.projectId,
-        err => {
-          expect(err).to.not.exist
-          expect(this.Project.updateOne.callCount).to.equal(1)
-          expect(
-            this.Project.updateOne.calledWith({
-              _id: this.projectId,
-            })
-          ).to.equal(true)
-          expect(
-            this.Project.updateOne.lastCall.args[1].$addToSet
-          ).to.have.keys('tokenAccessReadAndWrite_refs')
-          sinon.assert.calledWith(
-            this.Analytics.recordEventForUser,
-            this.userId,
-            'project-joined',
-            { mode: 'read-write' }
-          )
-          done()
-        }
+        this.projectId
       )
-    })
 
-    it('should not produce an error', function (done) {
-      this.TokenAccessHandler.addReadAndWriteUserToProject(
+      expect(this.Project.updateOne.callCount).to.equal(1)
+      expect(
+        this.Project.updateOne.calledWith({
+          _id: this.projectId,
+        })
+      ).to.equal(true)
+      expect(this.Project.updateOne.lastCall.args[1].$addToSet).to.have.keys(
+        'tokenAccessReadAndWrite_refs'
+      )
+      sinon.assert.calledWith(
+        this.Analytics.recordEventForUser,
         this.userId,
-        this.projectId,
-        err => {
-          expect(err).to.not.exist
-          done()
-        }
+        'project-joined',
+        { mode: 'read-write' }
       )
     })
 
@@ -213,18 +186,16 @@ describe('TokenAccessHandler', function () {
       beforeEach(function () {
         this.Project.updateOne = sinon
           .stub()
-          .callsArgWith(2, new Error('woops'))
+          .returns({ exec: sinon.stub().rejects(new Error('woops')) })
       })
 
-      it('should produce an error', function (done) {
-        this.TokenAccessHandler.addReadAndWriteUserToProject(
-          this.userId,
-          this.projectId,
-          err => {
-            expect(err).to.exist
-            done()
-          }
-        )
+      it('should produce an error', async function () {
+        await expect(
+          this.TokenAccessHandler.promises.addReadAndWriteUserToProject(
+            this.userId,
+            this.projectId
+          )
+        ).to.be.rejected
       })
     })
   })
@@ -234,8 +205,8 @@ describe('TokenAccessHandler', function () {
       this.req = { session: {}, headers: {} }
     })
 
-    it('should add the token to the session', function (done) {
-      this.TokenAccessHandler.grantSessionTokenAccess(
+    it('should add the token to the session', function () {
+      this.TokenAccessHandler.promises.grantSessionTokenAccess(
         this.req,
         this.projectId,
         this.token
@@ -243,7 +214,6 @@ describe('TokenAccessHandler', function () {
       expect(
         this.req.session.anonTokenAccess[this.projectId.toString()]
       ).to.equal(this.token)
-      done()
     })
   })
 
@@ -251,185 +221,158 @@ describe('TokenAccessHandler', function () {
     describe('when a read-only project is found', function () {
       beforeEach(function () {
         this.TokenAccessHandler.getTokenType = sinon.stub().returns('readOnly')
-        this.TokenAccessHandler.getProjectByToken = sinon
+        this.TokenAccessHandler.promises.getProjectByToken = sinon
           .stub()
-          .callsArgWith(2, null, this.project)
+          .resolves(this.project)
       })
 
-      it('should try to find projects with both kinds of token', function (done) {
-        this.TokenAccessHandler.validateTokenForAnonymousAccess(
+      it('should try to find projects with both kinds of token', async function () {
+        await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
           this.projectId,
-          this.token,
-          (err, allowed) => {
-            expect(err).to.not.exist
-            expect(
-              this.TokenAccessHandler.getProjectByToken.callCount
-            ).to.equal(1)
-            done()
-          }
+          this.token
         )
+
+        expect(
+          this.TokenAccessHandler.promises.getProjectByToken.callCount
+        ).to.equal(1)
       })
 
-      it('should allow read-only access', function (done) {
-        this.TokenAccessHandler.validateTokenForAnonymousAccess(
-          this.projectId,
-          this.token,
-          (err, rw, ro) => {
-            expect(err).to.not.exist
-            expect(rw).to.equal(false)
-            expect(ro).to.equal(true)
-            done()
-          }
-        )
+      it('should allow read-only access', async function () {
+        const { isValidReadAndWrite, isValidReadOnly } =
+          await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
+            this.projectId,
+            this.token
+          )
+
+        expect(isValidReadAndWrite).to.equal(false)
+        expect(isValidReadOnly).to.equal(true)
       })
     })
 
     describe('when a read-and-write project is found', function () {
       beforeEach(function () {
-        this.TokenAccessHandler.getTokenType = sinon
+        this.TokenAccessHandler.promises.getTokenType = sinon
           .stub()
           .returns('readAndWrite')
-        this.TokenAccessHandler.getProjectByToken = sinon
+        this.TokenAccessHandler.promises.getProjectByToken = sinon
           .stub()
-          .callsArgWith(2, null, this.project)
+          .resolves(this.project)
       })
 
-      describe('when Anonymous token access is not enabled', function (done) {
+      describe('when Anonymous token access is not enabled', function () {
         beforeEach(function () {
           this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false
         })
 
-        it('should try to find projects with both kinds of token', function (done) {
-          this.TokenAccessHandler.validateTokenForAnonymousAccess(
+        it('should try to find projects with both kinds of token', async function () {
+          await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
             this.projectId,
-            this.token,
-            (err, rw, ro) => {
-              expect(err).to.not.exist
-              expect(
-                this.TokenAccessHandler.getProjectByToken.callCount
-              ).to.equal(1)
-              done()
-            }
+            this.token
           )
+
+          expect(
+            this.TokenAccessHandler.promises.getProjectByToken.callCount
+          ).to.equal(1)
         })
 
-        it('should not allow read-and-write access', function (done) {
-          this.TokenAccessHandler.validateTokenForAnonymousAccess(
-            this.projectId,
-            this.token,
-            (err, rw, ro) => {
-              expect(err).to.not.exist
-              expect(rw).to.equal(false)
-              expect(ro).to.equal(false)
-              done()
-            }
-          )
+        it('should not allow read-and-write access', async function () {
+          const { isValidReadAndWrite, isValidReadOnly } =
+            await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
+              this.projectId,
+              this.token
+            )
+
+          expect(isValidReadAndWrite).to.equal(false)
+          expect(isValidReadOnly).to.equal(false)
         })
       })
 
-      describe('when anonymous token access is enabled', function (done) {
+      describe('when anonymous token access is enabled', function () {
         beforeEach(function () {
-          this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true
+          this.TokenAccessHandler.promises.ANONYMOUS_READ_AND_WRITE_ENABLED = true
         })
 
-        it('should try to find projects with both kinds of token', function (done) {
-          this.TokenAccessHandler.validateTokenForAnonymousAccess(
+        it('should try to find projects with both kinds of token', async function () {
+          await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
             this.projectId,
-            this.token,
-            (err, rw, ro) => {
-              expect(err).to.not.exist
-              expect(
-                this.TokenAccessHandler.getProjectByToken.callCount
-              ).to.equal(1)
-              done()
-            }
+            this.token
           )
+
+          expect(
+            this.TokenAccessHandler.promises.getProjectByToken.callCount
+          ).to.equal(1)
         })
 
-        it('should allow read-and-write access', function (done) {
-          this.TokenAccessHandler.validateTokenForAnonymousAccess(
-            this.projectId,
-            this.token,
-            (err, rw, ro) => {
-              expect(err).to.not.exist
-              expect(rw).to.equal(true)
-              expect(ro).to.equal(false)
-              done()
-            }
-          )
+        it('should allow read-and-write access', async function () {
+          const { isValidReadAndWrite, isValidReadOnly } =
+            await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
+              this.projectId,
+              this.token
+            )
+
+          expect(isValidReadAndWrite).to.equal(true)
+          expect(isValidReadOnly).to.equal(false)
         })
       })
     })
 
     describe('when no project is found', function () {
       beforeEach(function () {
-        this.TokenAccessHandler.getProjectByToken = sinon
+        this.TokenAccessHandler.promises.getProjectByToken = sinon
           .stub()
-          .callsArgWith(2, null, null, null)
+          .resolves(null)
       })
 
-      it('should try to find projects with both kinds of token', function (done) {
-        this.TokenAccessHandler.validateTokenForAnonymousAccess(
+      it('should try to find projects with both kinds of token', async function () {
+        await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
           this.projectId,
-          this.token,
-          (err, allowed) => {
-            expect(err).to.not.exist
-            expect(
-              this.TokenAccessHandler.getProjectByToken.callCount
-            ).to.equal(1)
-            done()
-          }
+          this.token
         )
+
+        expect(
+          this.TokenAccessHandler.promises.getProjectByToken.callCount
+        ).to.equal(1)
       })
 
-      it('should not allow any access', function (done) {
-        this.TokenAccessHandler.validateTokenForAnonymousAccess(
-          this.projectId,
-          this.token,
-          (err, rw, ro) => {
-            expect(err).to.not.exist
-            expect(rw).to.equal(false)
-            expect(ro).to.equal(false)
-            done()
-          }
-        )
+      it('should not allow any access', async function () {
+        const { isValidReadAndWrite, isValidReadOnly } =
+          await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
+            this.projectId,
+            this.token
+          )
+
+        expect(isValidReadAndWrite).to.equal(false)
+        expect(isValidReadOnly).to.equal(false)
       })
     })
 
     describe('when findProject produces an error', function () {
       beforeEach(function () {
-        this.TokenAccessHandler.getProjectByToken = sinon
+        this.TokenAccessHandler.promises.getProjectByToken = sinon
           .stub()
-          .callsArgWith(2, new Error('woops'))
+          .rejects(new Error('woops'))
       })
 
-      it('should try to find projects with both kinds of token', function (done) {
-        this.TokenAccessHandler.validateTokenForAnonymousAccess(
-          this.projectId,
-          this.token,
-          (err, allowed) => {
-            expect(err).to.exist
-            expect(allowed).to.not.exist
-            expect(
-              this.TokenAccessHandler.getProjectByToken.callCount
-            ).to.equal(1)
-            done()
-          }
-        )
+      it('should try to find projects with both kinds of token', async function () {
+        await expect(
+          this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
+            this.projectId,
+            this.token
+          )
+        ).to.be.rejected
+
+        expect(
+          this.TokenAccessHandler.promises.getProjectByToken.callCount
+        ).to.equal(1)
       })
 
-      it('should produce an error and not allow access', function (done) {
-        this.TokenAccessHandler.validateTokenForAnonymousAccess(
-          this.projectId,
-          this.token,
-          (err, rw, ro) => {
-            expect(err).to.exist
-            expect(err).to.be.instanceof(Error)
-            expect(rw).to.equal(undefined)
-            expect(ro).to.equal(undefined)
-            done()
-          }
-        )
+      it('should produce an error and not allow access', async function () {
+        await expect(
+          this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
+            this.projectId,
+            this.token
+          )
+        ).to.be.rejected
       })
     })
 
@@ -443,22 +386,20 @@ describe('TokenAccessHandler', function () {
           this.TokenAccessHandler.getTokenType = sinon
             .stub()
             .returns('readAndWrite')
-          this.TokenAccessHandler.getProjectByToken = sinon
+          this.TokenAccessHandler.promises.getProjectByToken = sinon
             .stub()
-            .callsArgWith(2, null, this.project)
+            .resolves(this.project)
         })
 
-        it('should not allow any access', function (done) {
-          this.TokenAccessHandler.validateTokenForAnonymousAccess(
-            this.projectId,
-            this.token,
-            (err, rw, ro) => {
-              expect(err).to.not.exist
-              expect(rw).to.equal(false)
-              expect(ro).to.equal(false)
-              done()
-            }
-          )
+        it('should not allow any access', async function () {
+          const { isValidReadAndWrite, isValidReadOnly } =
+            await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
+              this.projectId,
+              this.token
+            )
+
+          expect(isValidReadAndWrite).to.equal(false)
+          expect(isValidReadOnly).to.equal(false)
         })
       })
 
@@ -467,66 +408,60 @@ describe('TokenAccessHandler', function () {
           this.TokenAccessHandler.getTokenType = sinon
             .stub()
             .returns('readOnly')
-          this.TokenAccessHandler.getProjectByToken = sinon
+          this.TokenAccessHandler.promises.getProjectByToken = sinon
             .stub()
-            .callsArgWith(2, null, this.project)
+            .resolves(this.project)
         })
 
-        it('should not allow any access', function (done) {
-          this.TokenAccessHandler.validateTokenForAnonymousAccess(
-            this.projectId,
-            this.token,
-            (err, rw, ro) => {
-              expect(err).to.not.exist
-              expect(rw).to.equal(false)
-              expect(ro).to.equal(false)
-              done()
-            }
-          )
+        it('should not allow any access', async function () {
+          const { isValidReadAndWrite, isValidReadOnly } =
+            await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
+              this.projectId,
+              this.token
+            )
+
+          expect(isValidReadAndWrite).to.equal(false)
+          expect(isValidReadOnly).to.equal(false)
         })
       })
 
       describe('with nothing', function () {
         beforeEach(function () {
-          this.TokenAccessHandler.getProjectByToken = sinon
+          this.TokenAccessHandler.promises.getProjectByToken = sinon
             .stub()
-            .callsArgWith(1, null, null, null)
+            .resolves(null)
         })
 
-        it('should not allow any access', function (done) {
-          this.TokenAccessHandler.validateTokenForAnonymousAccess(
-            this.projectId,
-            null,
-            (err, rw, ro) => {
-              expect(err).to.not.exist
-              expect(rw).to.equal(false)
-              expect(ro).to.equal(false)
-              done()
-            }
-          )
+        it('should not allow any access', async function () {
+          const { isValidReadAndWrite, isValidReadOnly } =
+            await this.TokenAccessHandler.promises.validateTokenForAnonymousAccess(
+              this.projectId,
+              this.token
+            )
+
+          expect(isValidReadAndWrite).to.equal(false)
+          expect(isValidReadOnly).to.equal(false)
         })
       })
     })
   })
 
   describe('getDocPublishedInfo', function () {
-    beforeEach(function () {
-      this.callback = sinon.stub()
-    })
-
     describe('when v1 api not set', function () {
       beforeEach(function () {
         this.settings.apis = { v1: undefined }
-        this.TokenAccessHandler.getV1DocPublishedInfo(this.token, this.callback)
       })
 
-      it('should not check access and return default info', function () {
-        expect(this.V1Api.request.called).to.equal(false)
-        expect(
-          this.callback.calledWith(null, {
-            allow: true,
-          })
-        ).to.equal(true)
+      it('should not check access and return default info', async function () {
+        const info =
+          await this.TokenAccessHandler.promises.getV1DocPublishedInfo(
+            this.token
+          )
+
+        expect(this.V1Api.promises.request.called).to.equal(false)
+        expect(info).to.deep.equal({
+          allow: true,
+        })
       })
     })
 
@@ -537,63 +472,53 @@ describe('TokenAccessHandler', function () {
 
       describe('on V1Api.request success', function () {
         beforeEach(function () {
-          this.V1Api.request = sinon
+          this.V1Api.promises.request = sinon
             .stub()
-            .callsArgWith(1, null, null, 'mock-data')
-          this.TokenAccessHandler.getV1DocPublishedInfo(
-            this.token,
-            this.callback
-          )
+            .resolves({ body: 'mock-data' })
         })
 
-        it('should return response body', function () {
+        it('should return response body', async function () {
+          const info =
+            await this.TokenAccessHandler.promises.getV1DocPublishedInfo(
+              this.token
+            )
+
           expect(
-            this.V1Api.request.calledWith({
+            this.V1Api.promises.request.calledWith({
               url: `/api/v1/overleaf/docs/${this.token}/is_published`,
             })
           ).to.equal(true)
-          expect(this.callback.calledWith(null, 'mock-data')).to.equal(true)
+          expect(info).to.equal('mock-data')
         })
       })
 
       describe('on V1Api.request error', function () {
         beforeEach(function () {
-          this.V1Api.request = sinon.stub().callsArgWith(1, 'error')
-          this.TokenAccessHandler.getV1DocPublishedInfo(
-            this.token,
-            this.callback
-          )
+          this.V1Api.promises.request = sinon.stub().rejects('error')
         })
 
-        it('should callback with error', function () {
-          expect(this.callback.calledWith('error')).to.equal(true)
+        it('should be rejected', async function () {
+          await expect(
+            this.TokenAccessHandler.promises.getV1DocPublishedInfo(this.token)
+          ).to.be.rejected
         })
       })
     })
   })
 
   describe('getV1DocInfo', function () {
-    beforeEach(function () {
-      this.callback = sinon.stub()
-    })
-
     describe('when v1 api not set', function () {
-      beforeEach(function () {
-        this.TokenAccessHandler.getV1DocInfo(
+      it('should not check access and return default info', async function () {
+        const info = await this.TokenAccessHandler.promises.getV1DocInfo(
           this.token,
-          this.v2UserId,
-          this.callback
+          this.v2UserId
         )
-      })
 
-      it('should not check access and return default info', function () {
-        expect(this.V1Api.request.called).to.equal(false)
-        expect(
-          this.callback.calledWith(null, {
-            exists: true,
-            exported: false,
-          })
-        ).to.equal(true)
+        expect(this.V1Api.promises.request.called).to.equal(false)
+        expect(info).to.deep.equal({
+          exists: true,
+          exported: false,
+        })
       })
     })
 
@@ -604,38 +529,38 @@ describe('TokenAccessHandler', function () {
 
       describe('on V1Api.request success', function () {
         beforeEach(function () {
-          this.V1Api.request = sinon
+          this.V1Api.promises.request = sinon
             .stub()
-            .callsArgWith(1, null, null, 'mock-data')
-          this.TokenAccessHandler.getV1DocInfo(
+            .resolves({ body: 'mock-data' })
+        })
+
+        it('should return response body', async function () {
+          const info = await this.TokenAccessHandler.promises.getV1DocInfo(
             this.token,
-            this.v2UserId,
-            this.callback
+            this.v2UserId
           )
-        })
 
-        it('should return response body', function () {
           expect(
-            this.V1Api.request.calledWith({
+            this.V1Api.promises.request.calledWith({
               url: `/api/v1/overleaf/docs/${this.token}/info`,
             })
           ).to.equal(true)
-          expect(this.callback.calledWith(null, 'mock-data')).to.equal(true)
+          expect(info).to.equal('mock-data')
         })
       })
 
       describe('on V1Api.request error', function () {
         beforeEach(function () {
-          this.V1Api.request = sinon.stub().callsArgWith(1, 'error')
-          this.TokenAccessHandler.getV1DocInfo(
-            this.token,
-            this.v2UserId,
-            this.callback
-          )
+          this.V1Api.promises.request = sinon.stub().rejects('error')
         })
 
-        it('should callback with error', function () {
-          expect(this.callback.calledWith('error')).to.equal(true)
+        it('should be rejected', async function () {
+          await expect(
+            this.TokenAccessHandler.promises.getV1DocInfo(
+              this.token,
+              this.v2UserId
+            )
+          ).to.be.rejected
         })
       })
     })