Merge pull request #2202 from overleaf/em-collab-set-permissions

Endpoint for setting a collaborator's permissions

GitOrigin-RevId: eb4d4dcc476908f5a42fefd7b81ef6fcc000be5b
This commit is contained in:
Eric Mc Sween 2019-10-07 08:13:41 -04:00 committed by sharelatex
parent 17babc034f
commit 45e5808a35
12 changed files with 444 additions and 31 deletions

View file

@ -1,16 +1,19 @@
const OError = require('@overleaf/o-error')
const HttpErrors = require('@overleaf/o-error/http')
const CollaboratorsHandler = require('./CollaboratorsHandler')
const CollaboratorsGetter = require('./CollaboratorsGetter')
const AuthenticationController = require('../Authentication/AuthenticationController')
const EditorRealTimeController = require('../Editor/EditorRealTimeController')
const TagsHandler = require('../Tags/TagsHandler')
const Errors = require('../Errors/Errors')
const logger = require('logger-sharelatex')
const { expressify } = require('../../util/promises')
module.exports = {
removeUserFromProject: expressify(removeUserFromProject),
removeSelfFromProject: expressify(removeSelfFromProject),
getAllMembers: expressify(getAllMembers)
getAllMembers: expressify(getAllMembers),
setCollaboratorInfo: expressify(setCollaboratorInfo)
}
async function removeUserFromProject(req, res, next) {
@ -45,6 +48,26 @@ async function getAllMembers(req, res, next) {
res.json({ members })
}
async function setCollaboratorInfo(req, res, next) {
const projectId = req.params.Project_id
const userId = req.params.user_id
const { privilegeLevel } = req.body
try {
await CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
projectId,
userId,
privilegeLevel
)
} catch (err) {
if (err instanceof Errors.NotFoundError) {
throw new HttpErrors.NotFoundError({})
} else {
throw err
}
}
res.sendStatus(204)
}
async function _removeUserIdFromProject(projectId, userId) {
await CollaboratorsHandler.promises.removeUserFromProject(projectId, userId)
EditorRealTimeController.emitToRoom(

View file

@ -7,6 +7,7 @@ const ContactManager = require('../Contacts/ContactManager')
const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
const ProjectEntityHandler = require('../Project/ProjectEntityHandler')
const CollaboratorsGetter = require('./CollaboratorsGetter')
const Errors = require('../Errors/Errors')
module.exports = {
removeUserFromProject: callbackify(removeUserFromProject),
@ -17,7 +18,8 @@ module.exports = {
removeUserFromProject,
removeUserFromAllProjects,
addUserIdToProject,
transferProjects
transferProjects,
setCollaboratorPrivilegeLevel
}
}
@ -150,6 +152,45 @@ async function transferProjects(fromUserId, toUserId) {
})
}
async function setCollaboratorPrivilegeLevel(
projectId,
userId,
privilegeLevel
) {
// Make sure we're only updating the project if the user is already a
// collaborator
const query = {
_id: projectId,
$or: [{ collaberator_refs: userId }, { readOnly_refs: userId }]
}
let update
switch (privilegeLevel) {
case PrivilegeLevels.READ_AND_WRITE: {
update = {
$pull: { readOnly_refs: userId },
$addToSet: { collaberator_refs: userId }
}
break
}
case PrivilegeLevels.READ_ONLY: {
update = {
$pull: { collaberator_refs: userId },
$addToSet: { readOnly_refs: userId }
}
break
}
default: {
throw new OError({
message: `unknown privilege level: ${privilegeLevel}`
})
}
}
const mongoResponse = await Project.updateOne(query, update).exec()
if (mongoResponse.n === 0) {
throw new Errors.NotFoundError('project or collaborator not found')
}
}
async function _flushProjects(projectIds) {
for (const projectId of projectIds) {
await ProjectEntityHandler.promises.flushProjectToThirdPartyDataStore(

View file

@ -1,19 +1,11 @@
/* eslint-disable
max-len,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const CollaboratorsController = require('./CollaboratorsController')
const AuthenticationController = require('../Authentication/AuthenticationController')
const AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware')
const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
const CollaboratorsInviteController = require('./CollaboratorsInviteController')
const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware')
const CaptchaMiddleware = require('../Captcha/CaptchaMiddleware')
const { Joi, validate } = require('../../infrastructure/Validation')
module.exports = {
apply(webRouter, apiRouter) {
@ -23,6 +15,24 @@ module.exports = {
CollaboratorsController.removeSelfFromProject
)
webRouter.put(
'/project/:Project_id/users/:user_id',
AuthenticationController.requireLogin(),
validate({
params: Joi.object({
Project_id: Joi.objectId(),
user_id: Joi.objectId()
}),
body: Joi.object({
privilegeLevel: Joi.string()
.valid(PrivilegeLevels.READ_ONLY, PrivilegeLevels.READ_AND_WRITE)
.required()
})
}),
AuthorizationMiddleware.ensureUserCanAdminProject,
CollaboratorsController.setCollaboratorInfo
)
webRouter.delete(
'/project/:Project_id/users/:user_id',
AuthenticationController.requireLogin(),
@ -91,7 +101,7 @@ module.exports = {
CollaboratorsInviteController.viewInvite
)
return webRouter.post(
webRouter.post(
'/project/:Project_id/invite/token/:token/accept',
AuthenticationController.requireLogin(),
CollaboratorsInviteController.acceptInvite

View file

@ -5,6 +5,7 @@ const logger = require('logger-sharelatex')
const metrics = require('metrics-sharelatex')
const crawlerLogger = require('./CrawlerLogger')
const expressLocals = require('./ExpressLocals')
const Validation = require('./Validation')
const Router = require('../router')
const helmet = require('helmet')
const UserSessionsRedis = require('../Features/User/UserSessionsRedis')
@ -242,6 +243,7 @@ const enableApiRouter =
if (enableApiRouter || notDefined(enableApiRouter)) {
logger.info('providing api router')
app.use(privateApiRouter)
app.use(Validation.errorMiddleware)
app.use(HttpErrorController.handleError)
app.use(ErrorController.handleApiError)
}
@ -250,10 +252,14 @@ const enableWebRouter =
Settings.web != null ? Settings.web.enableWebRouter : undefined
if (enableWebRouter || notDefined(enableWebRouter)) {
logger.info('providing web router')
app.use(publicApiRouter) // public API goes with web router for public access
app.use(Validation.errorMiddleware)
app.use(HttpErrorController.handleError)
app.use(ErrorController.handleApiError)
app.use(webRouter)
app.use(Validation.errorMiddleware)
app.use(HttpErrorController.handleError)
app.use(ErrorController.handleError)
}

View file

@ -0,0 +1,14 @@
const { Joi: CelebrateJoi, celebrate, errors } = require('celebrate')
const JoiObjectId = require('joi-mongodb-objectid')
const Joi = CelebrateJoi.extend(JoiObjectId)
const errorMiddleware = errors()
module.exports = { Joi, validate, errorMiddleware }
/**
* Validation middleware
*/
function validate(schema) {
return celebrate(schema, { allowUnknown: true })
}

View file

@ -1864,6 +1864,40 @@
}
}
},
"@hapi/address": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.2.tgz",
"integrity": "sha512-O4QDrx+JoGKZc6aN64L04vqa7e41tIiLU+OvKdcYaEMP97UttL0f9GIi9/0A4WAMx0uBd6SidDIhktZhgOcN8Q=="
},
"@hapi/bourne": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz",
"integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA=="
},
"@hapi/hoek": {
"version": "8.2.5",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.2.5.tgz",
"integrity": "sha512-rmGFzok1zR3xZKd5m3ihWdqafXFxvPHoQ/78+AG5URKbEbJiwBBfRgzbu+07W5f3+07JRshw6QqGbVmCp8ntig=="
},
"@hapi/joi": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz",
"integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==",
"requires": {
"@hapi/address": "2.x.x",
"@hapi/bourne": "1.x.x",
"@hapi/hoek": "8.x.x",
"@hapi/topo": "3.x.x"
}
},
"@hapi/topo": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.4.tgz",
"integrity": "sha512-aVWQTOI9wBD6zawmOr6f+tdEIxQC8JXfQVLTjgGe8YEStAWGn/GNNVTobKJhbWKveQj2RyYF3oYbO9SC8/eOCA==",
"requires": {
"@hapi/hoek": "8.x.x"
}
},
"@overleaf/o-error": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@overleaf/o-error/-/o-error-2.1.0.tgz",
@ -4169,6 +4203,22 @@
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="
},
"celebrate": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/celebrate/-/celebrate-10.0.1.tgz",
"integrity": "sha512-Eke/caDOlcLjwk8WN4+bX9WyiiIgJ+zjbsh368FAtEj74bNC3Y+gVfFF8efmP9iYVnFSkLy31+6bKCQm/Zza+g==",
"requires": {
"@hapi/joi": "15.x.x",
"escape-html": "1.0.3"
},
"dependencies": {
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
}
}
},
"center-align": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
@ -4197,6 +4247,12 @@
"check-error": "^1.0.2"
}
},
"chaid": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/chaid/-/chaid-1.0.2.tgz",
"integrity": "sha1-9Y6UNgUoq9qkas8LOlF0oG4Vd1A=",
"dev": true
},
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
@ -10675,6 +10731,34 @@
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
"integrity": "sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w=="
},
"joi-mongodb-objectid": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/joi-mongodb-objectid/-/joi-mongodb-objectid-0.1.0.tgz",
"integrity": "sha512-5N86VRXOd8TZ2nEvlg/EvIeF/StFrB2VAI9iD46di2B2eR7dU2kLhsYTwiAOQMFozIg64qfWs/eEed52w9YpBw==",
"requires": {
"bson": "^4.0.0"
},
"dependencies": {
"bson": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/bson/-/bson-4.0.2.tgz",
"integrity": "sha512-rBdCxMBCg2aR420e1oKUejjcuPZLTibA7zEhWAlliFWEwzuBCC9Dkp5r7VFFIQB2t1WVsvTbohry575mc7Xw5A==",
"requires": {
"buffer": "^5.1.0",
"long": "^4.0.0"
}
},
"buffer": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz",
"integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==",
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
}
}
}
},
"jquery": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-1.11.1.tgz",

View file

@ -41,6 +41,7 @@
"bcrypt": "^3.0.4",
"body-parser": "^1.13.1",
"bufferedstream": "1.6.0",
"celebrate": "^10.0.1",
"chai-as-promised": "^7.1.1",
"codemirror": "^5.33.0",
"connect-redis": "^3.1.0",
@ -63,6 +64,7 @@
"helmet": "^3.8.1",
"http-proxy": "^1.8.1",
"is-utf8": "^0.2.1",
"joi-mongodb-objectid": "^0.1.0",
"jquery": "^1.11.1",
"json2csv": "^4.3.3",
"jsonwebtoken": "^8.0.1",
@ -125,6 +127,7 @@
"babel-plugin-angularjs-annotate": "^0.10.0",
"chai": "3.5.0",
"chai-as-promised": "^7.1.1",
"chaid": "^1.0.2",
"clean-css-cli": "^4.2.1",
"es6-promise": "^4.0.5",
"eslint": "^4.18.1",

View file

@ -1,2 +1,3 @@
const chai = require('chai')
chai.use(require('chai-as-promised'))
chai.use(require('chaid'))

View file

@ -0,0 +1,102 @@
const { expect } = require('chai')
const User = require('./helpers/User').promises
describe('Sharing', function() {
beforeEach(async function() {
this.ownerSession = new User()
this.collaboratorSession = new User()
this.strangerSession = new User()
await this.ownerSession.login()
await this.collaboratorSession.login()
await this.strangerSession.login()
this.owner = await this.ownerSession.get()
this.collaborator = await this.collaboratorSession.get()
this.stranger = await this.strangerSession.get()
this.projectId = await this.ownerSession.createProject('Test project')
})
describe('with read-only collaborator', function() {
beforeEach(async function() {
await this.ownerSession.addUserToProject(
this.projectId,
this.collaborator,
'readOnly'
)
})
it('sets the privilege level to read-write', async function() {
await this.ownerSession.setCollaboratorInfo(
this.projectId,
this.collaborator._id,
{ privilegeLevel: 'readAndWrite' }
)
const project = await this.ownerSession.getProject(this.projectId)
expect(project.collaberator_refs).to.be.unordered.ids([
this.collaborator._id
])
expect(project.readOnly_refs).to.deep.equal([])
})
it('treats setting the privilege to read-only as a noop', async function() {
await this.ownerSession.setCollaboratorInfo(
this.projectId,
this.collaborator._id,
{ privilegeLevel: 'readOnly' }
)
const project = await this.ownerSession.getProject(this.projectId)
expect(project.collaberator_refs).to.deep.equal([])
expect(project.readOnly_refs).to.be.unordered.ids([this.collaborator._id])
})
it('prevents non-owners to set the privilege level', async function() {
await expect(
this.collaboratorSession.setCollaboratorInfo(
this.projectId,
this.collaborator._id,
{ privilegeLevel: 'readAndWrite' }
)
).to.be.rejectedWith('Unexpected status code: 403')
})
it('validates the privilege level', async function() {
await expect(
this.collaboratorSession.setCollaboratorInfo(
this.projectId,
this.collaborator._id,
{ privilegeLevel: 'superpowers' }
)
).to.be.rejectedWith('Unexpected status code: 400')
})
it('returns 404 if the user is not already a collaborator', async function() {
await expect(
this.ownerSession.setCollaboratorInfo(
this.projectId,
this.stranger._id,
{ privilegeLevel: 'readOnly' }
)
).to.be.rejectedWith('Unexpected status code: 404')
})
})
describe('with read-write collaborator', function() {
beforeEach(async function() {
await this.ownerSession.addUserToProject(
this.projectId,
this.collaborator,
'readAndWrite'
)
})
it('sets the privilege level to read-only', async function() {
await this.ownerSession.setCollaboratorInfo(
this.projectId,
this.collaborator._id,
{ privilegeLevel: 'readOnly' }
)
const project = await this.ownerSession.getProject(this.projectId)
expect(project.collaberator_refs).to.deep.equal([])
expect(project.readOnly_refs).to.be.unordered.ids([this.collaborator._id])
})
})
})

View file

@ -678,6 +678,31 @@ class User {
callback
)
}
setCollaboratorInfo(projectId, userId, info, callback) {
this.getCsrfToken(err => {
if (err != null) {
return callback(err)
}
this.request.put(
{
url: `/project/${projectId.toString()}/users/${userId.toString()}`,
json: info
},
(err, response) => {
if (err != null) {
return callback(err)
}
if (response.statusCode !== 204) {
return callback(
new Error(`Unexpected status code: ${response.statusCode}`)
)
}
callback()
}
)
})
}
}
User.promises = class extends User {

View file

@ -1,6 +1,9 @@
const sinon = require('sinon')
const { expect } = require('chai')
const SandboxedModule = require('sandboxed-module')
const { ObjectId } = require('mongodb')
const HttpErrors = require('@overleaf/o-error/http')
const Errors = require('../../../../app/src/Features/Errors/Errors')
const MockRequest = require('../helpers/MockRequest')
const MockResponse = require('../helpers/MockResponse')
@ -12,13 +15,14 @@ describe('CollaboratorsController', function() {
this.res = new MockResponse()
this.req = new MockRequest()
this.user_id = 'user-id-123'
this.project_id = 'project-id-123'
this.userId = ObjectId()
this.projectId = ObjectId()
this.callback = sinon.stub()
this.CollaboratorsHandler = {
promises: {
removeUserFromProject: sinon.stub().resolves()
removeUserFromProject: sinon.stub().resolves(),
setCollaboratorPrivilegeLevel: sinon.stub().resolves()
}
}
this.CollaboratorsGetter = {
@ -35,7 +39,7 @@ describe('CollaboratorsController', function() {
}
}
this.AuthenticationController = {
getLoggedInUserId: sinon.stub().returns(this.user_id)
getLoggedInUserId: sinon.stub().returns(this.userId)
}
this.logger = {
err: sinon.stub(),
@ -54,6 +58,8 @@ describe('CollaboratorsController', function() {
'../Tags/TagsHandler': this.TagsHandler,
'../Authentication/AuthenticationController': this
.AuthenticationController,
'../Errors/Errors': Errors,
'@overleaf/o-error/http': HttpErrors,
'logger-sharelatex': this.logger
}
})
@ -62,8 +68,8 @@ describe('CollaboratorsController', function() {
describe('removeUserFromProject', function() {
beforeEach(function(done) {
this.req.params = {
Project_id: this.project_id,
user_id: this.user_id
Project_id: this.projectId,
user_id: this.userId
}
this.res.sendStatus = sinon.spy(() => {
done()
@ -74,14 +80,14 @@ describe('CollaboratorsController', function() {
it('should from the user from the project', function() {
expect(
this.CollaboratorsHandler.promises.removeUserFromProject
).to.have.been.calledWith(this.project_id, this.user_id)
).to.have.been.calledWith(this.projectId, this.userId)
})
it('should emit a userRemovedFromProject event to the proejct', function() {
expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith(
this.project_id,
this.projectId,
'userRemovedFromProject',
this.user_id
this.userId
)
})
@ -91,7 +97,7 @@ describe('CollaboratorsController', function() {
it('should have called emitToRoom', function() {
expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith(
this.project_id,
this.projectId,
'project:membership:changed'
)
})
@ -99,7 +105,7 @@ describe('CollaboratorsController', function() {
describe('removeSelfFromProject', function() {
beforeEach(function(done) {
this.req.params = { Project_id: this.project_id }
this.req.params = { Project_id: this.projectId }
this.res.sendStatus = sinon.spy(() => {
done()
})
@ -109,21 +115,21 @@ describe('CollaboratorsController', function() {
it('should remove the logged in user from the project', function() {
expect(
this.CollaboratorsHandler.promises.removeUserFromProject
).to.have.been.calledWith(this.project_id, this.user_id)
).to.have.been.calledWith(this.projectId, this.userId)
})
it('should emit a userRemovedFromProject event to the proejct', function() {
expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith(
this.project_id,
this.projectId,
'userRemovedFromProject',
this.user_id
this.userId
)
})
it('should remove the project from all tags', function() {
expect(
this.TagsHandler.promises.removeProjectFromAllTags
).to.have.been.calledWith(this.user_id, this.project_id)
).to.have.been.calledWith(this.userId, this.projectId)
})
it('should return a success code', function() {
@ -133,7 +139,7 @@ describe('CollaboratorsController', function() {
describe('getAllMembers', function() {
beforeEach(function(done) {
this.req.params = { Project_id: this.project_id }
this.req.params = { Project_id: this.projectId }
this.res.json = sinon.spy(() => {
done()
})
@ -187,4 +193,39 @@ describe('CollaboratorsController', function() {
})
})
})
describe('setCollaboratorInfo', function() {
beforeEach(function() {
this.req.params = {
Project_id: this.projectId,
user_id: this.userId
}
this.req.body = { privilegeLevel: 'readOnly' }
})
it('should set the collaborator privilege level', function(done) {
this.res.sendStatus = status => {
expect(status).to.equal(204)
expect(
this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel
).to.have.been.calledWith(this.projectId, this.userId, 'readOnly')
done()
}
this.CollaboratorsController.setCollaboratorInfo(this.req, this.res)
})
it('should return a 404 when the project or collaborator is not found', function(done) {
this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel.rejects(
new Errors.NotFoundError()
)
this.CollaboratorsController.setCollaboratorInfo(
this.req,
this.res,
err => {
expect(err).to.be.instanceof(HttpErrors.NotFoundError)
done()
}
)
})
})
})

View file

@ -21,8 +21,8 @@ describe('CollaboratorsHandler', function() {
warn: sinon.stub(),
err: sinon.stub()
}
this.userId = 'mock-user-id'
this.addingUserId = 'adding-user-id'
this.userId = ObjectId()
this.addingUserId = ObjectId()
this.project = {
_id: ObjectId()
}
@ -346,4 +346,67 @@ describe('CollaboratorsHandler', function() {
})
})
})
describe('setCollaboratorPrivilegeLevel', function() {
it('sets a collaborator to read-only', async function() {
this.ProjectMock.expects('updateOne')
.withArgs(
{
_id: this.projectId,
$or: [
{ collaberator_refs: this.userId },
{ readOnly_refs: this.userId }
]
},
{
$pull: { collaberator_refs: this.userId },
$addToSet: { readOnly_refs: this.userId }
}
)
.chain('exec')
.resolves({ n: 1 })
await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
this.projectId,
this.userId,
'readOnly'
)
})
it('sets a collaborator to read-write', async function() {
this.ProjectMock.expects('updateOne')
.withArgs(
{
_id: this.projectId,
$or: [
{ collaberator_refs: this.userId },
{ readOnly_refs: this.userId }
]
},
{
$addToSet: { collaberator_refs: this.userId },
$pull: { readOnly_refs: this.userId }
}
)
.chain('exec')
.resolves({ n: 1 })
await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
this.projectId,
this.userId,
'readAndWrite'
)
})
it('throws a NotFoundError if the project or collaborator does not exist', async function() {
this.ProjectMock.expects('updateOne')
.chain('exec')
.resolves({ n: 0 })
await expect(
this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
this.projectId,
this.userId,
'readAndWrite'
)
).to.be.rejectedWith(Errors.NotFoundError)
})
})
})