Selaa lähdekoodia

Merge pull request #20034 from overleaf/tm-collab-limit-edit-invites

Enforce collaborator limit when accepting project invites

GitOrigin-RevId: 94f281113fe7c7b6d0a5ef43e11ab579400d9e56
Thomas 2 vuotta sitten
vanhempi
sitoutus
98a914bb94

+ 9 - 2
services/web/app/src/Features/Collaborators/CollaboratorsHandler.js

@@ -101,7 +101,8 @@ async function addUserIdToProject(
   projectId,
   addingUserId,
   userId,
-  privilegeLevel
+  privilegeLevel,
+  { pendingEditor } = {}
 ) {
   const project = await ProjectGetter.promises.getProject(projectId, {
     owner_ref: 1,
@@ -124,7 +125,13 @@ async function addUserIdToProject(
     )
   } else if (privilegeLevel === PrivilegeLevels.READ_ONLY) {
     level = { readOnly_refs: userId }
-    logger.debug({ privileges: 'readOnly', userId, projectId }, 'adding user')
+    if (pendingEditor) {
+      level.pendingEditor_refs = userId
+    }
+    logger.debug(
+      { privileges: 'readOnly', userId, projectId, pendingEditor },
+      'adding user'
+    )
   } else {
     throw new Error(`unknown privilegeLevel: ${privilegeLevel}`)
   }

+ 4 - 3
services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js

@@ -4,6 +4,7 @@ const LimitationsManager = require('../Subscription/LimitationsManager')
 const UserGetter = require('../User/UserGetter')
 const CollaboratorsGetter = require('./CollaboratorsGetter')
 const CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
+const CollaboratorsInviteGetter = require('./CollaboratorsInviteGetter')
 const logger = require('@overleaf/logger')
 const Settings = require('@overleaf/settings')
 const EmailHelper = require('../Helpers/EmailHelper')
@@ -38,7 +39,7 @@ const CollaboratorsInviteController = {
     const projectId = req.params.Project_id
     logger.debug({ projectId }, 'getting all active invites for project')
     const invites =
-      await CollaboratorsInviteHandler.promises.getAllInvites(projectId)
+      await CollaboratorsInviteGetter.promises.getAllInvites(projectId)
     res.json({ invites })
   },
 
@@ -291,7 +292,7 @@ const CollaboratorsInviteController = {
     }
 
     // get the invite
-    const invite = await CollaboratorsInviteHandler.promises.getInviteByToken(
+    const invite = await CollaboratorsInviteGetter.promises.getInviteByToken(
       projectId,
       token
     )
@@ -351,7 +352,7 @@ const CollaboratorsInviteController = {
       'got request to accept invite'
     )
 
-    const invite = await CollaboratorsInviteHandler.promises.getInviteByToken(
+    const invite = await CollaboratorsInviteGetter.promises.getInviteByToken(
       projectId,
       token
     )

+ 55 - 0
services/web/app/src/Features/Collaborators/CollaboratorsInviteGetter.js

@@ -0,0 +1,55 @@
+const logger = require('@overleaf/logger')
+const { ProjectInvite } = require('../../models/ProjectInvite')
+const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
+const CollaboratorsInviteHelper = require('./CollaboratorsInviteHelper')
+
+async function getAllInvites(projectId) {
+  logger.debug({ projectId }, 'fetching invites for project')
+  const invites = await ProjectInvite.find({ projectId })
+    .select('_id email privileges')
+    .exec()
+  logger.debug(
+    { projectId, count: invites.length },
+    'found invites for project'
+  )
+  return invites
+}
+
+async function getInviteCount(projectId) {
+  logger.debug({ projectId }, 'counting invites for project')
+  const count = await ProjectInvite.countDocuments({ projectId }).exec()
+  return count
+}
+
+async function getEditInviteCount(projectId) {
+  logger.debug({ projectId }, 'counting edit invites for project')
+  const count = await ProjectInvite.countDocuments({
+    projectId,
+    privileges: { $ne: PrivilegeLevels.READ_ONLY },
+  }).exec()
+  return count
+}
+
+async function getInviteByToken(projectId, tokenString) {
+  logger.debug({ projectId }, 'fetching invite by token')
+  const invite = await ProjectInvite.findOne({
+    projectId,
+    tokenHmac: CollaboratorsInviteHelper.hashInviteToken(tokenString),
+  }).exec()
+
+  if (invite == null) {
+    logger.err({ projectId }, 'no invite found')
+    return null
+  }
+
+  return invite
+}
+
+module.exports = {
+  promises: {
+    getAllInvites,
+    getInviteCount,
+    getEditInviteCount,
+    getInviteByToken,
+  },
+}

+ 34 - 45
services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.js

@@ -8,36 +8,12 @@ const UserGetter = require('../User/UserGetter')
 const ProjectGetter = require('../Project/ProjectGetter')
 const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
 const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
+const SplitTestHandler = require('../SplitTests/SplitTestHandler')
+const LimitationsManager = require('../Subscription/LimitationsManager')
+const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
 const _ = require('lodash')
 
 const CollaboratorsInviteHandler = {
-  async getAllInvites(projectId) {
-    logger.debug({ projectId }, 'fetching invites for project')
-    const invites = await ProjectInvite.find({ projectId })
-      .select('_id email privileges')
-      .exec()
-    logger.debug(
-      { projectId, count: invites.length },
-      'found invites for project'
-    )
-    return invites
-  },
-
-  async getInviteCount(projectId) {
-    logger.debug({ projectId }, 'counting invites for project')
-    const count = await ProjectInvite.countDocuments({ projectId }).exec()
-    return count
-  },
-
-  async getEditInviteCount(projectId) {
-    logger.debug({ projectId }, 'counting edit invites for project')
-    const count = await ProjectInvite.countDocuments({
-      projectId,
-      privileges: { $ne: PrivilegeLevels.READ_ONLY },
-    }).exec()
-    return count
-  },
-
   async _trySendInviteNotification(projectId, sendingUser, invite) {
     const { email } = invite
     const existingUser = await UserGetter.promises.getUserByAnyEmail(email, {
@@ -152,27 +128,43 @@ const CollaboratorsInviteHandler = {
     )
   },
 
-  async getInviteByToken(projectId, tokenString) {
-    logger.debug({ projectId }, 'fetching invite by token')
-    const invite = await ProjectInvite.findOne({
-      projectId,
-      tokenHmac: CollaboratorsInviteHelper.hashInviteToken(tokenString),
-    }).exec()
-
-    if (invite == null) {
-      logger.err({ projectId }, 'no invite found')
-      return null
+  async acceptInvite(invite, projectId, user) {
+    const project = await ProjectGetter.promises.getProject(projectId, {
+      owner_ref: 1,
+    })
+    const linkSharingEnforcement =
+      await SplitTestHandler.promises.getAssignmentForUser(
+        project.owner_ref,
+        'link-sharing-enforcement'
+      )
+    const pendingEditor =
+      invite.privileges === PrivilegeLevels.READ_AND_WRITE &&
+      linkSharingEnforcement?.variant === 'active' &&
+      !(await LimitationsManager.promises.canAcceptEditCollaboratorInvite(
+        project._id
+      ))
+    if (pendingEditor) {
+      logger.debug(
+        { projectId, userId: user._id },
+        'no collaborator slots available, user added as read only (pending editor)'
+      )
+      await ProjectAuditLogHandler.promises.addEntry(
+        projectId,
+        'editor-moved-to-pending', // controller already logged accept-invite
+        null,
+        null,
+        {
+          userId: user._id.toString(),
+        }
+      )
     }
 
-    return invite
-  },
-
-  async acceptInvite(invite, projectId, user) {
     await CollaboratorsHandler.promises.addUserIdToProject(
       projectId,
       invite.sendingUserId,
       user._id,
-      invite.privileges
+      pendingEditor ? PrivilegeLevels.READ_ONLY : invite.privileges,
+      { pendingEditor }
     )
 
     // Remove invite
@@ -192,12 +184,9 @@ const CollaboratorsInviteHandler = {
 
 module.exports = {
   promises: CollaboratorsInviteHandler,
-  getAllInvites: callbackify(CollaboratorsInviteHandler.getAllInvites),
-  getInviteCount: callbackify(CollaboratorsInviteHandler.getInviteCount),
   inviteToProject: callbackify(CollaboratorsInviteHandler.inviteToProject),
   revokeInvite: callbackify(CollaboratorsInviteHandler.revokeInvite),
   generateNewInvite: callbackify(CollaboratorsInviteHandler.generateNewInvite),
-  getInviteByToken: callbackify(CollaboratorsInviteHandler.getInviteByToken),
   acceptInvite: callbackify(CollaboratorsInviteHandler.acceptInvite),
   _trySendInviteNotification: callbackify(
     CollaboratorsInviteHandler._trySendInviteNotification

+ 2 - 2
services/web/app/src/Features/Editor/EditorHttpController.js

@@ -5,7 +5,7 @@ const AuthorizationManager = require('../Authorization/AuthorizationManager')
 const ProjectEditorHandler = require('../Project/ProjectEditorHandler')
 const Metrics = require('@overleaf/metrics')
 const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
-const CollaboratorsInviteHandler = require('../Collaborators/CollaboratorsInviteHandler')
+const CollaboratorsInviteGetter = require('../Collaborators/CollaboratorsInviteGetter')
 const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
 const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
 const SessionManager = require('../Authentication/SessionManager')
@@ -128,7 +128,7 @@ async function _buildJoinProjectView(req, projectId, userId) {
     return { project: null, privilegeLevel: null, isRestrictedUser: false }
   }
   const invites =
-    await CollaboratorsInviteHandler.promises.getAllInvites(projectId)
+    await CollaboratorsInviteGetter.promises.getAllInvites(projectId)
   const isTokenMember = await CollaboratorsHandler.promises.userIsTokenMember(
     userId,
     projectId

+ 16 - 3
services/web/app/src/Features/Subscription/LimitationsManager.js

@@ -4,7 +4,7 @@ const UserGetter = require('../User/UserGetter')
 const SubscriptionLocator = require('./SubscriptionLocator')
 const Settings = require('@overleaf/settings')
 const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
-const CollaboratorsInvitesHandler = require('../Collaborators/CollaboratorsInviteHandler')
+const CollaboratorsInvitesGetter = require('../Collaborators/CollaboratorsInviteGetter')
 const V1SubscriptionManager = require('./V1SubscriptionManager')
 const { V1ConnectionError } = require('../Errors/Errors')
 const {
@@ -28,6 +28,18 @@ async function allowedNumberOfCollaboratorsForUser(userId) {
   }
 }
 
+async function canAcceptEditCollaboratorInvite(projectId) {
+  const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
+  if (allowedNumber < 0) {
+    return true // -1 means unlimited
+  }
+  const currentEditors =
+    await CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
+      projectId
+    )
+  return currentEditors + 1 <= allowedNumber
+}
+
 async function canAddXCollaborators(projectId, numberOfNewCollaborators) {
   const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
   if (allowedNumber < 0) {
@@ -36,7 +48,7 @@ async function canAddXCollaborators(projectId, numberOfNewCollaborators) {
   const currentNumber =
     await CollaboratorsGetter.promises.getInvitedCollaboratorCount(projectId)
   const inviteCount =
-    await CollaboratorsInvitesHandler.promises.getInviteCount(projectId)
+    await CollaboratorsInvitesGetter.promises.getInviteCount(projectId)
   return currentNumber + inviteCount + numberOfNewCollaborators <= allowedNumber
 }
 
@@ -53,7 +65,7 @@ async function canAddXEditCollaborators(
       projectId
     )
   const editInviteCount =
-    await CollaboratorsInvitesHandler.promises.getEditInviteCount(projectId)
+    await CollaboratorsInvitesGetter.promises.getEditInviteCount(projectId)
   return (
     currentEditors + editInviteCount + numberOfNewEditCollaborators <=
     allowedNumber
@@ -179,6 +191,7 @@ const LimitationsManager = {
   promises: {
     allowedNumberOfCollaboratorsInProject,
     allowedNumberOfCollaboratorsForUser,
+    canAcceptEditCollaboratorInvite,
     canAddXCollaborators,
     canAddXEditCollaborators,
     hasPaidSubscription,

+ 26 - 0
services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js

@@ -246,6 +246,32 @@ describe('CollaboratorsHandler', function () {
           this.userId
         )
       })
+
+      describe('and with pendingEditor flag', function () {
+        it('should add them to the pending editor refs', async function () {
+          this.ProjectMock.expects('updateOne')
+            .withArgs(
+              {
+                _id: this.project._id,
+              },
+              {
+                $addToSet: {
+                  readOnly_refs: this.userId,
+                  pendingEditor_refs: this.userId,
+                },
+              }
+            )
+            .chain('exec')
+            .resolves()
+          await this.CollaboratorsHandler.promises.addUserIdToProject(
+            this.project._id,
+            this.addingUserId,
+            this.userId,
+            'readOnly',
+            { pendingEditor: true }
+          )
+        })
+      })
     })
 
     describe('as readAndWrite', function () {

+ 26 - 20
services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.js

@@ -83,15 +83,20 @@ describe('CollaboratorsInviteController', function () {
 
     this.CollaboratorsInviteHandler = {
       promises: {
-        getAllInvites: sinon.stub(),
         inviteToProject: sinon.stub().resolves(this.inviteReducedData),
-        getInviteByToken: sinon.stub().resolves(this.invite),
         generateNewInvite: sinon.stub().resolves(this.invite),
         revokeInvite: sinon.stub().resolves(this.invite),
         acceptInvite: sinon.stub(),
       },
     }
 
+    this.CollaboratorsInviteGetter = {
+      promises: {
+        getAllInvites: sinon.stub(),
+        getInviteByToken: sinon.stub().resolves(this.invite),
+      },
+    }
+
     this.EditorRealTimeController = {
       emitToRoom: sinon.stub(),
     }
@@ -123,6 +128,7 @@ describe('CollaboratorsInviteController', function () {
         '../User/UserGetter': this.UserGetter,
         './CollaboratorsGetter': this.CollaboratorsGetter,
         './CollaboratorsInviteHandler': this.CollaboratorsInviteHandler,
+        './CollaboratorsInviteGetter': this.CollaboratorsInviteGetter,
         '../Editor/EditorRealTimeController': this.EditorRealTimeController,
         '../Analytics/AnalyticsManager': this.AnalyticsManger,
         '../Authentication/SessionManager': this.SessionManager,
@@ -150,7 +156,7 @@ describe('CollaboratorsInviteController', function () {
 
     describe('when all goes well', function () {
       beforeEach(function (done) {
-        this.CollaboratorsInviteHandler.promises.getAllInvites.resolves(
+        this.CollaboratorsInviteGetter.promises.getAllInvites.resolves(
           this.fakeInvites
         )
         this.res.callback = () => done()
@@ -173,10 +179,10 @@ describe('CollaboratorsInviteController', function () {
       })
 
       it('should have called CollaboratorsInviteHandler.getAllInvites', function () {
-        this.CollaboratorsInviteHandler.promises.getAllInvites.callCount.should.equal(
+        this.CollaboratorsInviteGetter.promises.getAllInvites.callCount.should.equal(
           1
         )
-        this.CollaboratorsInviteHandler.promises.getAllInvites
+        this.CollaboratorsInviteGetter.promises.getAllInvites
           .calledWith(this.projectId)
           .should.equal(true)
       })
@@ -184,7 +190,7 @@ describe('CollaboratorsInviteController', function () {
 
     describe('when CollaboratorsInviteHandler.getAllInvites produces an error', function () {
       beforeEach(function (done) {
-        this.CollaboratorsInviteHandler.promises.getAllInvites.rejects(
+        this.CollaboratorsInviteGetter.promises.getAllInvites.rejects(
           new Error('woops')
         )
         this.next.callsFake(() => done())
@@ -802,7 +808,7 @@ describe('CollaboratorsInviteController', function () {
       this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves(
         false
       )
-      this.CollaboratorsInviteHandler.promises.getInviteByToken.resolves(
+      this.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(
         this.invite
       )
       this.ProjectGetter.promises.getProject.resolves(this.fakeProject)
@@ -838,10 +844,10 @@ describe('CollaboratorsInviteController', function () {
       })
 
       it('should call getInviteByToken', function () {
-        this.CollaboratorsInviteHandler.promises.getInviteByToken.callCount.should.equal(
+        this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal(
           1
         )
-        this.CollaboratorsInviteHandler.promises.getInviteByToken
+        this.CollaboratorsInviteGetter.promises.getInviteByToken
           .calledWith(this.fakeProject._id, this.invite.token)
           .should.equal(true)
       })
@@ -924,7 +930,7 @@ describe('CollaboratorsInviteController', function () {
       })
 
       it('should not call getInviteByToken', function () {
-        this.CollaboratorsInviteHandler.promises.getInviteByToken.callCount.should.equal(
+        this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal(
           0
         )
       })
@@ -966,7 +972,7 @@ describe('CollaboratorsInviteController', function () {
       })
 
       it('should not call getInviteByToken', function () {
-        this.CollaboratorsInviteHandler.promises.getInviteByToken.callCount.should.equal(
+        this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal(
           0
         )
       })
@@ -982,7 +988,7 @@ describe('CollaboratorsInviteController', function () {
 
     describe('when the getInviteByToken produces an error', function () {
       beforeEach(function (done) {
-        this.CollaboratorsInviteHandler.promises.getInviteByToken.rejects(
+        this.CollaboratorsInviteGetter.promises.getInviteByToken.rejects(
           new Error('woops')
         )
         this.next.callsFake(() => done())
@@ -1008,7 +1014,7 @@ describe('CollaboratorsInviteController', function () {
       })
 
       it('should call getInviteByToken', function () {
-        this.CollaboratorsInviteHandler.promises.getInviteByToken.callCount.should.equal(
+        this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal(
           1
         )
         this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject
@@ -1027,7 +1033,7 @@ describe('CollaboratorsInviteController', function () {
 
     describe('when the getInviteByToken does not produce an invite', function () {
       beforeEach(function (done) {
-        this.CollaboratorsInviteHandler.promises.getInviteByToken.resolves(null)
+        this.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(null)
         this.res.callback = () => done()
         this.CollaboratorsInviteController.viewInvite(
           this.req,
@@ -1057,7 +1063,7 @@ describe('CollaboratorsInviteController', function () {
       })
 
       it('should call getInviteByToken', function () {
-        this.CollaboratorsInviteHandler.promises.getInviteByToken.callCount.should.equal(
+        this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal(
           1
         )
         this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject
@@ -1100,7 +1106,7 @@ describe('CollaboratorsInviteController', function () {
       })
 
       it('should call getInviteByToken', function () {
-        this.CollaboratorsInviteHandler.promises.getInviteByToken.callCount.should.equal(
+        this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal(
           1
         )
       })
@@ -1149,7 +1155,7 @@ describe('CollaboratorsInviteController', function () {
       })
 
       it('should call getInviteByToken', function () {
-        this.CollaboratorsInviteHandler.promises.getInviteByToken.callCount.should.equal(
+        this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal(
           1
         )
       })
@@ -1192,7 +1198,7 @@ describe('CollaboratorsInviteController', function () {
       })
 
       it('should call getInviteByToken', function () {
-        this.CollaboratorsInviteHandler.promises.getInviteByToken.callCount.should.equal(
+        this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal(
           1
         )
       })
@@ -1241,7 +1247,7 @@ describe('CollaboratorsInviteController', function () {
       })
 
       it('should call getInviteByToken', function () {
-        this.CollaboratorsInviteHandler.promises.getInviteByToken.callCount.should.equal(
+        this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal(
           1
         )
       })
@@ -1513,7 +1519,7 @@ describe('CollaboratorsInviteController', function () {
 
     describe('when the invite is not found', function () {
       beforeEach(function (done) {
-        this.CollaboratorsInviteHandler.promises.getInviteByToken.resolves(null)
+        this.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(null)
         this.next.callsFake(() => done())
         this.CollaboratorsInviteController.acceptInvite(
           this.req,

+ 232 - 0
services/web/test/unit/src/Collaborators/CollaboratorsInviteGetterTests.js

@@ -0,0 +1,232 @@
+const sinon = require('sinon')
+const { expect } = require('chai')
+const SandboxedModule = require('sandboxed-module')
+const { ObjectId } = require('mongodb')
+const Crypto = require('crypto')
+
+const MODULE_PATH =
+  '../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter.js'
+
+describe('CollaboratorsInviteGetter', function () {
+  beforeEach(function () {
+    this.ProjectInvite = class ProjectInvite {
+      constructor(options) {
+        if (options == null) {
+          options = {}
+        }
+        this._id = new ObjectId()
+        for (const k in options) {
+          const v = options[k]
+          this[k] = v
+        }
+      }
+    }
+    this.ProjectInvite.prototype.save = sinon.stub()
+    this.ProjectInvite.findOne = sinon.stub()
+    this.ProjectInvite.find = sinon.stub()
+    this.ProjectInvite.deleteOne = sinon.stub()
+    this.ProjectInvite.findOneAndDelete = sinon.stub()
+    this.ProjectInvite.countDocuments = sinon.stub()
+
+    this.Crypto = {
+      randomBytes: sinon.stub().callsFake(Crypto.randomBytes),
+    }
+
+    this.CollaboratorsInviteHelper = {
+      generateToken: sinon.stub().returns(this.Crypto.randomBytes(24)),
+      hashInviteToken: sinon.stub().returns(this.tokenHmac),
+    }
+
+    this.CollaboratorsInviteGetter = SandboxedModule.require(MODULE_PATH, {
+      requires: {
+        '../../models/ProjectInvite': { ProjectInvite: this.ProjectInvite },
+        './CollaboratorsInviteHelper': this.CollaboratorsInviteHelper,
+      },
+    })
+
+    this.projectId = new ObjectId()
+    this.sendingUserId = new ObjectId()
+    this.email = 'user@example.com'
+    this.userId = new ObjectId()
+    this.inviteId = new ObjectId()
+    this.token = 'hnhteaosuhtaeosuahs'
+    this.privileges = 'readAndWrite'
+    this.fakeInvite = {
+      _id: this.inviteId,
+      email: this.email,
+      token: this.token,
+      tokenHmac: this.tokenHmac,
+      sendingUserId: this.sendingUserId,
+      projectId: this.projectId,
+      privileges: this.privileges,
+      createdAt: new Date(),
+    }
+  })
+
+  describe('getInviteCount', function () {
+    beforeEach(function () {
+      this.ProjectInvite.countDocuments.returns({
+        exec: sinon.stub().resolves(2),
+      })
+      this.call = async () => {
+        return await this.CollaboratorsInviteGetter.promises.getInviteCount(
+          this.projectId
+        )
+      }
+    })
+
+    it('should produce the count of documents', async function () {
+      const count = await this.call()
+      expect(count).to.equal(2)
+    })
+
+    describe('when model.countDocuments produces an error', function () {
+      beforeEach(function () {
+        this.ProjectInvite.countDocuments.returns({
+          exec: sinon.stub().rejects(new Error('woops')),
+        })
+      })
+
+      it('should produce an error', async function () {
+        await expect(this.call()).to.be.rejectedWith(Error)
+      })
+    })
+  })
+
+  describe('getEditInviteCount', function () {
+    beforeEach(function () {
+      this.ProjectInvite.countDocuments.returns({
+        exec: sinon.stub().resolves(2),
+      })
+      this.call = async () => {
+        return await this.CollaboratorsInviteGetter.promises.getEditInviteCount(
+          this.projectId
+        )
+      }
+    })
+
+    it('should produce the count of documents', async function () {
+      const count = await this.call()
+      expect(this.ProjectInvite.countDocuments).to.be.calledWith({
+        projectId: this.projectId,
+        privileges: { $ne: 'readOnly' },
+      })
+      expect(count).to.equal(2)
+    })
+
+    describe('when model.countDocuments produces an error', function () {
+      beforeEach(function () {
+        this.ProjectInvite.countDocuments.returns({
+          exec: sinon.stub().rejects(new Error('woops')),
+        })
+      })
+
+      it('should produce an error', async function () {
+        await expect(this.call()).to.be.rejectedWith(Error)
+      })
+    })
+  })
+
+  describe('getAllInvites', function () {
+    beforeEach(function () {
+      this.fakeInvites = [
+        { _id: new ObjectId(), one: 1 },
+        { _id: new ObjectId(), two: 2 },
+      ]
+      this.ProjectInvite.find.returns({
+        select: sinon.stub().returnsThis(),
+        exec: sinon.stub().resolves(this.fakeInvites),
+      })
+      this.call = async () => {
+        return await this.CollaboratorsInviteGetter.promises.getAllInvites(
+          this.projectId
+        )
+      }
+    })
+
+    describe('when all goes well', function () {
+      beforeEach(function () {})
+
+      it('should produce a list of invite objects', async function () {
+        const invites = await this.call()
+        expect(invites).to.not.be.oneOf([null, undefined])
+        expect(invites).to.deep.equal(this.fakeInvites)
+      })
+
+      it('should have called ProjectInvite.find', async function () {
+        await this.call()
+        this.ProjectInvite.find.callCount.should.equal(1)
+        this.ProjectInvite.find
+          .calledWith({ projectId: this.projectId })
+          .should.equal(true)
+      })
+    })
+
+    describe('when ProjectInvite.find produces an error', function () {
+      beforeEach(function () {
+        this.ProjectInvite.find.returns({
+          select: sinon.stub().returnsThis(),
+          exec: sinon.stub().rejects(new Error('woops')),
+        })
+      })
+
+      it('should produce an error', async function () {
+        await expect(this.call()).to.be.rejectedWith(Error)
+      })
+    })
+  })
+
+  describe('getInviteByToken', function () {
+    beforeEach(function () {
+      this.ProjectInvite.findOne.returns({
+        exec: sinon.stub().resolves(this.fakeInvite),
+      })
+      this.call = async () => {
+        return await this.CollaboratorsInviteGetter.promises.getInviteByToken(
+          this.projectId,
+          this.token
+        )
+      }
+    })
+
+    describe('when all goes well', function () {
+      it('should produce the invite object', async function () {
+        const invite = await this.call()
+        expect(invite).to.deep.equal(this.fakeInvite)
+      })
+
+      it('should call ProjectInvite.findOne', async function () {
+        await this.call()
+        this.ProjectInvite.findOne.callCount.should.equal(1)
+        this.ProjectInvite.findOne
+          .calledWith({ projectId: this.projectId, tokenHmac: this.tokenHmac })
+          .should.equal(true)
+      })
+    })
+
+    describe('when findOne produces an error', function () {
+      beforeEach(function () {
+        this.ProjectInvite.findOne.returns({
+          exec: sinon.stub().rejects(new Error('woops')),
+        })
+      })
+
+      it('should produce an error', async function () {
+        await expect(this.call()).to.be.rejectedWith(Error)
+      })
+    })
+
+    describe('when findOne does not find an invite', function () {
+      beforeEach(function () {
+        this.ProjectInvite.findOne.returns({
+          exec: sinon.stub().resolves(null),
+        })
+      })
+
+      it('should not produce an invite object', async function () {
+        const invite = await this.call()
+        expect(invite).to.be.oneOf([null, undefined])
+      })
+    })
+  })
+})

+ 80 - 171
services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.js

@@ -39,7 +39,7 @@ describe('CollaboratorsInviteHandler', function () {
       },
     }
     this.UserGetter = { promises: { getUser: sinon.stub() } }
-    this.ProjectGetter = { promises: {} }
+    this.ProjectGetter = { promises: { getProject: sinon.stub().resolves() } }
     this.NotificationsBuilder = { promises: {} }
     this.tokenHmac = 'jkhajkefhaekjfhkfg'
     this.CollaboratorsInviteHelper = {
@@ -47,6 +47,25 @@ describe('CollaboratorsInviteHandler', function () {
       hashInviteToken: sinon.stub().returns(this.tokenHmac),
     }
 
+    this.SplitTestHandler = {
+      promises: {
+        getAssignmentForUser: sinon.stub().resolves(),
+      },
+    }
+
+    this.LimitationsManager = {
+      promises: {
+        canAcceptEditCollaboratorInvite: sinon.stub().resolves(),
+      },
+    }
+
+    this.ProjectAuditLogHandler = {
+      promises: {
+        addEntry: sinon.stub().resolves(),
+      },
+      addEntryInBackground: sinon.stub(),
+    }
+
     this.CollaboratorsInviteHandler = SandboxedModule.require(MODULE_PATH, {
       requires: {
         '@overleaf/settings': this.settings,
@@ -57,7 +76,10 @@ describe('CollaboratorsInviteHandler', function () {
         '../Project/ProjectGetter': this.ProjectGetter,
         '../Notifications/NotificationsBuilder': this.NotificationsBuilder,
         './CollaboratorsInviteHelper': this.CollaboratorsInviteHelper,
-        crypto: this.Crypto,
+        '../SplitTests/SplitTestHandler': this.SplitTestHandler,
+        '../Subscription/LimitationsManager': this.LimitationsManager,
+        '../Project/ProjectAuditLogHandler': this.ProjectAuditLogHandler,
+        crypto: this.CryptogetAssignmentForUser,
       },
     })
 
@@ -88,119 +110,6 @@ describe('CollaboratorsInviteHandler', function () {
     }
   })
 
-  describe('getInviteCount', function () {
-    beforeEach(function () {
-      this.ProjectInvite.countDocuments.returns({
-        exec: sinon.stub().resolves(2),
-      })
-      this.call = async () => {
-        return await this.CollaboratorsInviteHandler.promises.getInviteCount(
-          this.projectId
-        )
-      }
-    })
-
-    it('should produce the count of documents', async function () {
-      const count = await this.call()
-      expect(count).to.equal(2)
-    })
-
-    describe('when model.countDocuments produces an error', function () {
-      beforeEach(function () {
-        this.ProjectInvite.countDocuments.returns({
-          exec: sinon.stub().rejects(new Error('woops')),
-        })
-      })
-
-      it('should produce an error', async function () {
-        await expect(this.call()).to.be.rejectedWith(Error)
-      })
-    })
-  })
-
-  describe('getEditInviteCount', function () {
-    beforeEach(function () {
-      this.ProjectInvite.countDocuments.returns({
-        exec: sinon.stub().resolves(2),
-      })
-      this.call = async () => {
-        return await this.CollaboratorsInviteHandler.promises.getEditInviteCount(
-          this.projectId
-        )
-      }
-    })
-
-    it('should produce the count of documents', async function () {
-      const count = await this.call()
-      expect(this.ProjectInvite.countDocuments).to.be.calledWith({
-        projectId: this.projectId,
-        privileges: { $ne: 'readOnly' },
-      })
-      expect(count).to.equal(2)
-    })
-
-    describe('when model.countDocuments produces an error', function () {
-      beforeEach(function () {
-        this.ProjectInvite.countDocuments.returns({
-          exec: sinon.stub().rejects(new Error('woops')),
-        })
-      })
-
-      it('should produce an error', async function () {
-        await expect(this.call()).to.be.rejectedWith(Error)
-      })
-    })
-  })
-
-  describe('getAllInvites', function () {
-    beforeEach(function () {
-      this.fakeInvites = [
-        { _id: new ObjectId(), one: 1 },
-        { _id: new ObjectId(), two: 2 },
-      ]
-      this.ProjectInvite.find.returns({
-        select: sinon.stub().returnsThis(),
-        exec: sinon.stub().resolves(this.fakeInvites),
-      })
-      this.call = async () => {
-        return await this.CollaboratorsInviteHandler.promises.getAllInvites(
-          this.projectId
-        )
-      }
-    })
-
-    describe('when all goes well', function () {
-      beforeEach(function () {})
-
-      it('should produce a list of invite objects', async function () {
-        const invites = await this.call()
-        expect(invites).to.not.be.oneOf([null, undefined])
-        expect(invites).to.deep.equal(this.fakeInvites)
-      })
-
-      it('should have called ProjectInvite.find', async function () {
-        await this.call()
-        this.ProjectInvite.find.callCount.should.equal(1)
-        this.ProjectInvite.find
-          .calledWith({ projectId: this.projectId })
-          .should.equal(true)
-      })
-    })
-
-    describe('when ProjectInvite.find produces an error', function () {
-      beforeEach(function () {
-        this.ProjectInvite.find.returns({
-          select: sinon.stub().returnsThis(),
-          exec: sinon.stub().rejects(new Error('woops')),
-        })
-      })
-
-      it('should produce an error', async function () {
-        await expect(this.call()).to.be.rejectedWith(Error)
-      })
-    })
-  })
-
   describe('inviteToProject', function () {
     beforeEach(function () {
       this.ProjectInvite.prototype.save.callsFake(async function () {
@@ -484,67 +393,15 @@ describe('CollaboratorsInviteHandler', function () {
     })
   })
 
-  describe('getInviteByToken', function () {
-    beforeEach(function () {
-      this.ProjectInvite.findOne.returns({
-        exec: sinon.stub().resolves(this.fakeInvite),
-      })
-      this.call = async () => {
-        return await this.CollaboratorsInviteHandler.promises.getInviteByToken(
-          this.projectId,
-          this.token
-        )
-      }
-    })
-
-    describe('when all goes well', function () {
-      it('should produce the invite object', async function () {
-        const invite = await this.call()
-        expect(invite).to.deep.equal(this.fakeInvite)
-      })
-
-      it('should call ProjectInvite.findOne', async function () {
-        await this.call()
-        this.ProjectInvite.findOne.callCount.should.equal(1)
-        this.ProjectInvite.findOne
-          .calledWith({ projectId: this.projectId, tokenHmac: this.tokenHmac })
-          .should.equal(true)
-      })
-    })
-
-    describe('when findOne produces an error', function () {
-      beforeEach(function () {
-        this.ProjectInvite.findOne.returns({
-          exec: sinon.stub().rejects(new Error('woops')),
-        })
-      })
-
-      it('should produce an error', async function () {
-        await expect(this.call()).to.be.rejectedWith(Error)
-      })
-    })
-
-    describe('when findOne does not find an invite', function () {
-      beforeEach(function () {
-        this.ProjectInvite.findOne.returns({
-          exec: sinon.stub().resolves(null),
-        })
-      })
-
-      it('should not produce an invite object', async function () {
-        const invite = await this.call()
-        expect(invite).to.be.oneOf([null, undefined])
-      })
-    })
-  })
-
   describe('acceptInvite', function () {
     beforeEach(function () {
       this.fakeProject = {
         _id: this.projectId,
-        collaberator_refs: [],
-        readOnly_refs: [],
+        owner_ref: this.sendingUserId,
       }
+      this.ProjectGetter.promises.getProject = sinon
+        .stub()
+        .resolves(this.fakeProject)
       this.CollaboratorsHandler.promises.addUserIdToProject.resolves()
       this.CollaboratorsInviteHandler.promises._tryCancelInviteNotification =
         sinon.stub().resolves()
@@ -602,6 +459,58 @@ describe('CollaboratorsInviteHandler', function () {
       })
     })
 
+    describe('when link-sharing-enforcement is active', function () {
+      beforeEach(function () {
+        this.SplitTestHandler.promises.getAssignmentForUser.resolves({
+          variant: 'active',
+        })
+      })
+
+      describe('when the project has no more edit collaborator slots', function () {
+        beforeEach(function () {
+          this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves(
+            false
+          )
+        })
+
+        it('should add readAndWrite invitees to the project as readOnly (pendingEditor) users', async function () {
+          await this.call()
+          this.ProjectAuditLogHandler.promises.addEntry.should.have.been.calledWith(
+            this.projectId,
+            'editor-moved-to-pending',
+            null,
+            null,
+            { userId: this.userId.toString() }
+          )
+          this.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith(
+            this.projectId,
+            this.sendingUserId,
+            this.userId,
+            'readOnly',
+            { pendingEditor: true }
+          )
+        })
+      })
+
+      describe('when the project has available edit collaborator slots', function () {
+        beforeEach(function () {
+          this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves(
+            true
+          )
+        })
+
+        it('should add readAndWrite invitees to the project as normal', async function () {
+          await this.call()
+          this.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith(
+            this.projectId,
+            this.sendingUserId,
+            this.userId,
+            this.fakeInvite.privileges
+          )
+        })
+      })
+    })
+
     describe('when addUserIdToProject produces an error', function () {
       beforeEach(function () {
         this.CollaboratorsHandler.promises.addUserIdToProject.callsArgWith(

+ 3 - 3
services/web/test/unit/src/Editor/EditorHttpControllerTests.js

@@ -67,7 +67,7 @@ describe('EditorHttpController', function () {
         userIsTokenMember: sinon.stub().resolves(false),
       },
     }
-    this.CollaboratorsInviteHandler = {
+    this.CollaboratorsInviteGetter = {
       promises: {
         getAllInvites: sinon.stub().resolves([
           {
@@ -147,8 +147,8 @@ describe('EditorHttpController', function () {
         '@overleaf/metrics': this.Metrics,
         '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
         '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler,
-        '../Collaborators/CollaboratorsInviteHandler':
-          this.CollaboratorsInviteHandler,
+        '../Collaborators/CollaboratorsInviteGetter':
+          this.CollaboratorsInviteGetter,
         '../TokenAccess/TokenAccessHandler': this.TokenAccessHandler,
         '../Authentication/SessionManager': this.SessionManager,
         '../../infrastructure/FileWriter': this.FileWriter,

+ 88 - 17
services/web/test/unit/src/Subscription/LimitationsManagerTests.js

@@ -1,6 +1,7 @@
 const SandboxedModule = require('sandboxed-module')
 const assert = require('assert')
 const sinon = require('sinon')
+const { expect } = require('chai')
 const modulePath = require('path').join(
   __dirname,
   '../../../../app/src/Features/Subscription/LimitationsManager'
@@ -60,7 +61,7 @@ describe('LimitationsManager', function () {
       },
     }
 
-    this.CollaboratorsInviteHandler = {
+    this.CollaboratorsInviteGetter = {
       promises: {
         getInviteCount: sinon.stub().resolves(),
         getEditInviteCount: sinon.stub().resolves(),
@@ -74,8 +75,8 @@ describe('LimitationsManager', function () {
         './SubscriptionLocator': this.SubscriptionLocator,
         '@overleaf/settings': (this.Settings = {}),
         '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
-        '../Collaborators/CollaboratorsInviteHandler':
-          this.CollaboratorsInviteHandler,
+        '../Collaborators/CollaboratorsInviteGetter':
+          this.CollaboratorsInviteGetter,
         './V1SubscriptionManager': this.V1SubscriptionManager,
       },
     })
@@ -157,6 +158,76 @@ describe('LimitationsManager', function () {
     })
   })
 
+  describe('canAcceptEditCollaboratorInvite', function () {
+    describe('when the project has fewer collaborators than allowed', function () {
+      beforeEach(function () {
+        this.current_number = 1
+        this.user.features.collaborators = 2
+        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
+          sinon.stub().resolves(this.current_number)
+      })
+
+      it('should return true', async function () {
+        const result =
+          await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
+            this.projectId
+          )
+        expect(result).to.be.true
+      })
+    })
+
+    describe('when accepting the invite would exceed the collaborator limit', function () {
+      beforeEach(function () {
+        this.current_number = 2
+        this.user.features.collaborators = 2
+        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
+          sinon.stub().resolves(this.current_number)
+      })
+
+      it('should return false', async function () {
+        const result =
+          await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
+            this.projectId
+          )
+        expect(result).to.be.false
+      })
+    })
+
+    describe('when the project has more collaborators than allowed', function () {
+      beforeEach(function () {
+        this.current_number = 3
+        this.user.features.collaborators = 2
+        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
+          sinon.stub().resolves(this.current_number)
+      })
+
+      it('should return false', async function () {
+        const result =
+          await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
+            this.projectId
+          )
+        expect(result).to.be.false
+      })
+    })
+
+    describe('when the project has infinite collaborators', function () {
+      beforeEach(function () {
+        this.current_number = 100
+        this.user.features.collaborators = -1
+        this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
+          sinon.stub().resolves(this.current_number)
+      })
+
+      it('should return true', async function () {
+        const result =
+          await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
+            this.projectId
+          )
+        expect(result).to.be.true
+      })
+    })
+  })
+
   describe('canAddXCollaborators', function () {
     describe('when the project has fewer collaborators than allowed', function () {
       beforeEach(function (done) {
@@ -166,7 +237,7 @@ describe('LimitationsManager', function () {
         this.CollaboratorsGetter.promises.getInvitedCollaboratorCount = sinon
           .stub()
           .resolves(this.current_number)
-        this.CollaboratorsInviteHandler.promises.getInviteCount = sinon
+        this.CollaboratorsInviteGetter.promises.getInviteCount = sinon
           .stub()
           .resolves(this.invite_count)
         this.callback = sinon.stub().callsFake(() => done())
@@ -190,7 +261,7 @@ describe('LimitationsManager', function () {
         this.CollaboratorsGetter.promises.getInvitedCollaboratorCount = sinon
           .stub()
           .resolves(this.current_number)
-        this.CollaboratorsInviteHandler.promises.getInviteCount = sinon
+        this.CollaboratorsInviteGetter.promises.getInviteCount = sinon
           .stub()
           .resolves(this.invite_count)
         this.callback = sinon.stub().callsFake(() => done())
@@ -214,7 +285,7 @@ describe('LimitationsManager', function () {
         this.CollaboratorsGetter.promises.getInvitedCollaboratorCount = sinon
           .stub()
           .resolves(this.current_number)
-        this.CollaboratorsInviteHandler.promises.getInviteCount = sinon
+        this.CollaboratorsInviteGetter.promises.getInviteCount = sinon
           .stub()
           .resolves(this.invite_count)
         this.callback = sinon.stub().callsFake(() => done())
@@ -238,7 +309,7 @@ describe('LimitationsManager', function () {
         this.CollaboratorsGetter.promises.getInvitedCollaboratorCount = sinon
           .stub()
           .resolves(this.current_number)
-        this.CollaboratorsInviteHandler.promises.getInviteCount = sinon
+        this.CollaboratorsInviteGetter.promises.getInviteCount = sinon
           .stub()
           .resolves(this.invite_count)
         this.callback = sinon.stub().callsFake(() => done())
@@ -262,7 +333,7 @@ describe('LimitationsManager', function () {
         this.CollaboratorsGetter.promises.getInvitedCollaboratorCount = sinon
           .stub()
           .resolves(this.current_number)
-        this.CollaboratorsInviteHandler.promises.getInviteCount = sinon
+        this.CollaboratorsInviteGetter.promises.getInviteCount = sinon
           .stub()
           .resolves(this.invite_count)
         this.callback = sinon.stub().callsFake(() => done())
@@ -286,7 +357,7 @@ describe('LimitationsManager', function () {
         this.CollaboratorsGetter.promises.getInvitedCollaboratorCount = sinon
           .stub()
           .resolves(this.current_number)
-        this.CollaboratorsInviteHandler.promises.getInviteCount = sinon
+        this.CollaboratorsInviteGetter.promises.getInviteCount = sinon
           .stub()
           .resolves(this.invite_count)
         this.callback = sinon.stub().callsFake(() => done())
@@ -310,7 +381,7 @@ describe('LimitationsManager', function () {
         this.CollaboratorsGetter.promises.getInvitedCollaboratorCount = sinon
           .stub()
           .resolves(this.current_number)
-        this.CollaboratorsInviteHandler.promises.getInviteCount = sinon
+        this.CollaboratorsInviteGetter.promises.getInviteCount = sinon
           .stub()
           .resolves(this.invite_count)
         this.callback = sinon.stub().callsFake(() => done())
@@ -334,7 +405,7 @@ describe('LimitationsManager', function () {
         this.invite_count = 0
         this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
           sinon.stub().resolves(this.current_number)
-        this.CollaboratorsInviteHandler.promises.getEditInviteCount = sinon
+        this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
           .stub()
           .resolves(this.invite_count)
         this.callback = sinon.stub().callsFake(() => done())
@@ -357,7 +428,7 @@ describe('LimitationsManager', function () {
         this.invite_count = 1
         this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
           sinon.stub().resolves(this.current_number)
-        this.CollaboratorsInviteHandler.promises.getEditInviteCount = sinon
+        this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
           .stub()
           .resolves(this.invite_count)
         this.callback = sinon.stub().callsFake(() => done())
@@ -380,7 +451,7 @@ describe('LimitationsManager', function () {
         this.invite_count = 0
         this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
           sinon.stub().resolves(this.current_number)
-        this.CollaboratorsInviteHandler.promises.getEditInviteCount = sinon
+        this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
           .stub()
           .resolves(this.invite_count)
         this.callback = sinon.stub().callsFake(() => done())
@@ -403,7 +474,7 @@ describe('LimitationsManager', function () {
         this.invite_count = 0
         this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
           sinon.stub().resolves(this.current_number)
-        this.CollaboratorsInviteHandler.promises.getEditInviteCount = sinon
+        this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
           .stub()
           .resolves(this.invite_count)
         this.callback = sinon.stub().callsFake(() => done())
@@ -426,7 +497,7 @@ describe('LimitationsManager', function () {
         this.invite_count = 0
         this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
           sinon.stub().resolves(this.current_number)
-        this.CollaboratorsInviteHandler.promises.getEditInviteCount = sinon
+        this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
           .stub()
           .resolves(this.invite_count)
         this.callback = sinon.stub().callsFake(() => done())
@@ -449,7 +520,7 @@ describe('LimitationsManager', function () {
         this.invite_count = 2
         this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
           sinon.stub().resolves(this.current_number)
-        this.CollaboratorsInviteHandler.promises.getEditInviteCount = sinon
+        this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
           .stub()
           .resolves(this.invite_count)
         this.callback = sinon.stub().callsFake(() => done())
@@ -472,7 +543,7 @@ describe('LimitationsManager', function () {
         this.invite_count = 1
         this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
           sinon.stub().resolves(this.current_number)
-        this.CollaboratorsInviteHandler.promises.getEditInviteCount = sinon
+        this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
           .stub()
           .resolves(this.invite_count)
         this.callback = sinon.stub().callsFake(() => done())