From 45e5808a35f42ffce4a7f5552eb5c946459cd1c6 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween Date: Mon, 7 Oct 2019 08:13:41 -0400 Subject: [PATCH] Merge pull request #2202 from overleaf/em-collab-set-permissions Endpoint for setting a collaborator's permissions GitOrigin-RevId: eb4d4dcc476908f5a42fefd7b81ef6fcc000be5b --- .../Collaborators/CollaboratorsController.js | 25 ++++- .../Collaborators/CollaboratorsHandler.js | 43 +++++++- .../Collaborators/CollaboratorsRouter.js | 32 ++++-- services/web/app/src/infrastructure/Server.js | 6 ++ .../web/app/src/infrastructure/Validation.js | 14 +++ services/web/package-lock.json | 84 +++++++++++++++ services/web/package.json | 3 + services/web/test/acceptance/bootstrap.js | 1 + .../web/test/acceptance/src/SharingTests.js | 102 ++++++++++++++++++ .../web/test/acceptance/src/helpers/User.js | 25 +++++ .../CollaboratorsControllerTests.js | 73 ++++++++++--- .../CollaboratorsHandlerTests.js | 67 +++++++++++- 12 files changed, 444 insertions(+), 31 deletions(-) create mode 100644 services/web/app/src/infrastructure/Validation.js create mode 100644 services/web/test/acceptance/src/SharingTests.js diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsController.js b/services/web/app/src/Features/Collaborators/CollaboratorsController.js index 14a40ea06f..73269e872b 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsController.js +++ b/services/web/app/src/Features/Collaborators/CollaboratorsController.js @@ -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( diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js b/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js index 30fdf3a09a..2007e3273d 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js +++ b/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js @@ -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( diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsRouter.js b/services/web/app/src/Features/Collaborators/CollaboratorsRouter.js index a2176ab743..ee2578c651 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsRouter.js +++ b/services/web/app/src/Features/Collaborators/CollaboratorsRouter.js @@ -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 diff --git a/services/web/app/src/infrastructure/Server.js b/services/web/app/src/infrastructure/Server.js index 0d4de98d99..428d1206f5 100644 --- a/services/web/app/src/infrastructure/Server.js +++ b/services/web/app/src/infrastructure/Server.js @@ -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) } diff --git a/services/web/app/src/infrastructure/Validation.js b/services/web/app/src/infrastructure/Validation.js new file mode 100644 index 0000000000..bc1529b07e --- /dev/null +++ b/services/web/app/src/infrastructure/Validation.js @@ -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 }) +} diff --git a/services/web/package-lock.json b/services/web/package-lock.json index b103d6648a..c38479ee83 100644 --- a/services/web/package-lock.json +++ b/services/web/package-lock.json @@ -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", diff --git a/services/web/package.json b/services/web/package.json index 6c0d12f107..edab97669b 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -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", diff --git a/services/web/test/acceptance/bootstrap.js b/services/web/test/acceptance/bootstrap.js index b2410402a0..1d2042b924 100644 --- a/services/web/test/acceptance/bootstrap.js +++ b/services/web/test/acceptance/bootstrap.js @@ -1,2 +1,3 @@ const chai = require('chai') chai.use(require('chai-as-promised')) +chai.use(require('chaid')) diff --git a/services/web/test/acceptance/src/SharingTests.js b/services/web/test/acceptance/src/SharingTests.js new file mode 100644 index 0000000000..3856cf1f71 --- /dev/null +++ b/services/web/test/acceptance/src/SharingTests.js @@ -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]) + }) + }) +}) diff --git a/services/web/test/acceptance/src/helpers/User.js b/services/web/test/acceptance/src/helpers/User.js index 87a6f5d99e..69ca50be9a 100644 --- a/services/web/test/acceptance/src/helpers/User.js +++ b/services/web/test/acceptance/src/helpers/User.js @@ -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 { diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.js b/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.js index 025d8d817d..16216dd327 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.js +++ b/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.js @@ -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() + } + ) + }) + }) }) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js b/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js index 6246730a14..245036c50f 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js +++ b/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js @@ -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) + }) + }) })