Merge pull request #19185 from overleaf/tm-validate-can-invite-editor-2

Update inviteToProject to check if editor slots are available

GitOrigin-RevId: bb67ae6329130573ba43e9524a3084bf5551ebde
This commit is contained in:
Thomas 2024-06-28 14:06:52 +02:00 committed by Copybot
parent a047388b08
commit 6a65644778
2 changed files with 254 additions and 4 deletions

View file

@ -15,6 +15,8 @@ const { expressify } = require('@overleaf/promise-utils')
const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
const Errors = require('../Errors/Errors')
const AuthenticationController = require('../Authentication/AuthenticationController')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
// This rate limiter allows a different number of requests depending on the
// number of callaborators a user is allowed. This is implemented by providing
@ -97,10 +99,33 @@ const CollaboratorsInviteController = {
logger.debug({ projectId, email, sendingUserId }, 'inviting to project')
const allowed = await LimitationsManager.promises.canAddXCollaborators(
projectId,
1
)
const project = await ProjectGetter.promises.getProject(projectId, {
owner_ref: 1,
})
const linkSharingChanges =
await SplitTestHandler.promises.getAssignmentForUser(
project.owner_ref,
'link-sharing-warning'
)
let allowed = false
if (linkSharingChanges?.variant === 'active') {
// if link-sharing-warning is active, can always invite read-only collaborators
if (privileges === PrivilegeLevels.READ_ONLY) {
allowed = true
} else {
allowed = await LimitationsManager.promises.canAddXEditCollaborators(
projectId,
1
)
}
} else {
allowed = await LimitationsManager.promises.canAddXCollaborators(
projectId,
1
)
}
if (!allowed) {
logger.debug(
{ projectId, email, sendingUserId },

View file

@ -16,6 +16,10 @@ describe('CollaboratorsInviteController', function () {
this.tokenHmac = 'some-hmac-token'
this.targetEmail = 'user@example.com'
this.privileges = 'readAndWrite'
this.projectOwner = {
_id: 'project-owner-id',
email: 'project-owner@example.com',
}
this.currentUser = {
_id: 'current-user-id',
email: 'current-user@example.com',
@ -30,6 +34,10 @@ describe('CollaboratorsInviteController', function () {
privileges: this.privileges,
createdAt: new Date(),
}
this.project = {
_id: this.projectId,
owner_ref: this.projectOwner._id,
}
this.SessionManager = {
getSessionUser: sinon.stub().returns(this.currentUser),
@ -48,6 +56,7 @@ describe('CollaboratorsInviteController', function () {
promises: {
allowedNumberOfCollaboratorsForUser: sinon.stub(),
canAddXCollaborators: sinon.stub().resolves(true),
canAddXEditCollaborators: sinon.stub().resolves(true),
},
}
@ -98,6 +107,12 @@ describe('CollaboratorsInviteController', function () {
setRedirectInSession: sinon.stub(),
}
this.SplitTestHandler = {
promises: {
getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }),
},
}
this.CollaboratorsInviteController = SandboxedModule.require(MODULE_PATH, {
requires: {
'../Project/ProjectGetter': this.ProjectGetter,
@ -113,6 +128,7 @@ describe('CollaboratorsInviteController', function () {
'../../infrastructure/RateLimiter': this.RateLimiter,
'../Authentication/AuthenticationController':
this.AuthenticationController,
'../SplitTests/SplitTestHandler': this.SplitTestHandler,
},
})
@ -191,6 +207,215 @@ describe('CollaboratorsInviteController', function () {
email: this.targetEmail,
privileges: this.privileges,
}
this.ProjectGetter.promises.getProject.resolves({
owner_ref: this.project.owner_ref,
})
})
describe('when in link-sharing-warning test', function (done) {
beforeEach(function () {
this.SplitTestHandler.promises.getAssignmentForUser.resolves({
variant: 'active',
})
})
describe('when all goes well', function (done) {
beforeEach(function (done) {
this.CollaboratorsInviteController.promises._checkShouldInviteEmail =
sinon.stub().resolves(true)
this.CollaboratorsInviteController.promises._checkRateLimit = sinon
.stub()
.resolves(true)
this.res.callback = () => done()
this.CollaboratorsInviteController.inviteToProject(
this.req,
this.res,
done
)
})
it('should produce json response', function () {
this.res.json.callCount.should.equal(1)
expect(this.res.json.firstCall.args[0]).to.deep.equal({
invite: this.invite,
})
})
it('should have called canAddXEditCollaborators', function () {
this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal(
1
)
this.LimitationsManager.promises.canAddXEditCollaborators
.calledWith(this.projectId)
.should.equal(true)
})
it('should have called _checkShouldInviteEmail', function () {
this.CollaboratorsInviteController.promises._checkShouldInviteEmail.callCount.should.equal(
1
)
this.CollaboratorsInviteController.promises._checkShouldInviteEmail
.calledWith(this.targetEmail)
.should.equal(true)
})
it('should have called inviteToProject', function () {
this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal(
1
)
this.CollaboratorsInviteHandler.promises.inviteToProject
.calledWith(
this.projectId,
this.currentUser,
this.targetEmail,
this.privileges
)
.should.equal(true)
})
it('should have called emitToRoom', function () {
this.EditorRealTimeController.emitToRoom.callCount.should.equal(1)
this.EditorRealTimeController.emitToRoom
.calledWith(this.projectId, 'project:membership:changed')
.should.equal(true)
})
it('adds a project audit log entry', function () {
this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith(
this.projectId,
'send-invite',
this.currentUser._id,
this.req.ip,
{
inviteId: this.invite._id,
privileges: this.privileges,
}
)
})
})
describe('when the user is not allowed to add more edit collaborators', function () {
beforeEach(function () {
this.LimitationsManager.promises.canAddXEditCollaborators.resolves(
false
)
})
describe('readAndWrite collaborator', function () {
beforeEach(function (done) {
this.privileges = 'readAndWrite'
this.CollaboratorsInviteController.promises._checkShouldInviteEmail =
sinon.stub().resolves(true)
this.CollaboratorsInviteController.promises._checkRateLimit = sinon
.stub()
.resolves(true)
this.res.callback = () => done()
this.CollaboratorsInviteController.inviteToProject(
this.req,
this.res,
this.next
)
})
it('should produce json response without an invite', function () {
this.res.json.callCount.should.equal(1)
expect(this.res.json.firstCall.args[0]).to.deep.equal({
invite: null,
})
})
it('should not have called _checkShouldInviteEmail', function () {
this.CollaboratorsInviteController.promises._checkShouldInviteEmail.callCount.should.equal(
0
)
this.CollaboratorsInviteController.promises._checkShouldInviteEmail
.calledWith(this.currentUser, this.targetEmail)
.should.equal(false)
})
it('should not have called inviteToProject', function () {
this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal(
0
)
})
})
describe('readOnly collaborator (always allowed)', function () {
beforeEach(function (done) {
this.req.body = {
email: this.targetEmail,
privileges: (this.privileges = 'readOnly'),
}
this.CollaboratorsInviteController.promises._checkShouldInviteEmail =
sinon.stub().resolves(true)
this.CollaboratorsInviteController.promises._checkRateLimit = sinon
.stub()
.resolves(true)
this.res.callback = () => done()
this.CollaboratorsInviteController.inviteToProject(
this.req,
this.res,
this.next
)
})
it('should produce json response', function () {
this.res.json.callCount.should.equal(1)
expect(this.res.json.firstCall.args[0]).to.deep.equal({
invite: this.invite,
})
})
it('should not have called canAddXEditCollaborators', function () {
this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal(
0
)
})
it('should have called _checkShouldInviteEmail', function () {
this.CollaboratorsInviteController.promises._checkShouldInviteEmail.callCount.should.equal(
1
)
this.CollaboratorsInviteController.promises._checkShouldInviteEmail
.calledWith(this.targetEmail)
.should.equal(true)
})
it('should have called inviteToProject', function () {
this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal(
1
)
this.CollaboratorsInviteHandler.promises.inviteToProject
.calledWith(
this.projectId,
this.currentUser,
this.targetEmail,
this.privileges
)
.should.equal(true)
})
it('should have called emitToRoom', function () {
this.EditorRealTimeController.emitToRoom.callCount.should.equal(1)
this.EditorRealTimeController.emitToRoom
.calledWith(this.projectId, 'project:membership:changed')
.should.equal(true)
})
it('adds a project audit log entry', function () {
this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith(
this.projectId,
'send-invite',
this.currentUser._id,
this.req.ip,
{
inviteId: this.invite._id,
privileges: this.privileges,
}
)
})
})
})
})
describe('when all goes well', function (done) {