diff --git a/services/web/app/src/Features/Authorization/AuthorizationManager.js b/services/web/app/src/Features/Authorization/AuthorizationManager.js index 37fb47ac66..a9b20100a4 100644 --- a/services/web/app/src/Features/Authorization/AuthorizationManager.js +++ b/services/web/app/src/Features/Authorization/AuthorizationManager.js @@ -1,5 +1,5 @@ let AuthorizationManager -const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') const ProjectGetter = require('../Project/ProjectGetter') const { User } = require('../../models/User') const PrivilegeLevels = require('./PrivilegeLevels') @@ -55,7 +55,7 @@ module.exports = AuthorizationManager = { // User is present, get their privilege level from database getPrivilegeLevelForProjectWithUser(userId, projectId, token, callback) { - CollaboratorsHandler.getMemberIdPrivilegeLevel(userId, projectId, function( + CollaboratorsGetter.getMemberIdPrivilegeLevel(userId, projectId, function( error, privilegeLevel ) { diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsController.js b/services/web/app/src/Features/Collaborators/CollaboratorsController.js index f140ac3fd8..14a40ea06f 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsController.js +++ b/services/web/app/src/Features/Collaborators/CollaboratorsController.js @@ -1,87 +1,56 @@ -const async = require('async') +const OError = require('@overleaf/o-error') const CollaboratorsHandler = require('./CollaboratorsHandler') +const CollaboratorsGetter = require('./CollaboratorsGetter') const AuthenticationController = require('../Authentication/AuthenticationController') const EditorRealTimeController = require('../Editor/EditorRealTimeController') const TagsHandler = require('../Tags/TagsHandler') const logger = require('logger-sharelatex') +const { expressify } = require('../../util/promises') -const CollaboratorsController = { - removeUserFromProject(req, res, next) { - const projectId = req.params.Project_id - const userId = req.params.user_id - CollaboratorsController._removeUserIdFromProject( - projectId, - userId, - function(error) { - if (error) { - return next(error) - } - EditorRealTimeController.emitToRoom( - projectId, - 'project:membership:changed', - { members: true } - ) - res.sendStatus(204) - } - ) - }, - - removeSelfFromProject(req, res, next) { - const projectId = req.params.Project_id - const userId = AuthenticationController.getLoggedInUserId(req) - CollaboratorsController._removeUserIdFromProject( - projectId, - userId, - function(error) { - if (error) { - return next(error) - } - res.sendStatus(204) - } - ) - }, - - _removeUserIdFromProject(projectId, userId, callback) { - async.series( - [ - cb => { - CollaboratorsHandler.removeUserFromProject(projectId, userId, cb) - }, - cb => { - EditorRealTimeController.emitToRoom( - projectId, - 'userRemovedFromProject', - userId - ) - cb() - }, - cb => { - TagsHandler.removeProjectFromAllTags(userId, projectId, cb) - } - ], - function(error) { - if (error) { - return callback(error) - } - callback() - } - ) - }, - - getAllMembers(req, res, next) { - const projectId = req.params.Project_id - logger.log({ projectId }, 'getting all active members for project') - CollaboratorsHandler.getAllInvitedMembers(projectId, function( - err, - members - ) { - if (err) { - logger.warn({ projectId }, 'error getting members for project') - return next(err) - } - res.json({ members }) - }) - } +module.exports = { + removeUserFromProject: expressify(removeUserFromProject), + removeSelfFromProject: expressify(removeSelfFromProject), + getAllMembers: expressify(getAllMembers) } -module.exports = CollaboratorsController +async function removeUserFromProject(req, res, next) { + const projectId = req.params.Project_id + const userId = req.params.user_id + await _removeUserIdFromProject(projectId, userId) + EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', { + members: true + }) + res.sendStatus(204) +} + +async function removeSelfFromProject(req, res, next) { + const projectId = req.params.Project_id + const userId = AuthenticationController.getLoggedInUserId(req) + await _removeUserIdFromProject(projectId, userId) + res.sendStatus(204) +} + +async function getAllMembers(req, res, next) { + const projectId = req.params.Project_id + logger.log({ projectId }, 'getting all active members for project') + let members + try { + members = await CollaboratorsGetter.promises.getAllInvitedMembers(projectId) + } catch (err) { + throw new OError({ + message: 'error getting members for project', + info: { projectId } + }).withCause(err) + } + res.json({ members }) +} + +async function _removeUserIdFromProject(projectId, userId) { + await CollaboratorsHandler.promises.removeUserFromProject(projectId, userId) + EditorRealTimeController.emitToRoom( + projectId, + 'userRemovedFromProject', + userId + ) + await TagsHandler.promises.removeProjectFromAllTags(userId, projectId) +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsGetter.js b/services/web/app/src/Features/Collaborators/CollaboratorsGetter.js new file mode 100644 index 0000000000..31a27d9565 --- /dev/null +++ b/services/web/app/src/Features/Collaborators/CollaboratorsGetter.js @@ -0,0 +1,272 @@ +const { callbackify } = require('util') +const pLimit = require('p-limit') +const { ObjectId } = require('mongojs') +const OError = require('@overleaf/o-error') +const { Project } = require('../../models/Project') +const UserGetter = require('../User/UserGetter') +const ProjectGetter = require('../Project/ProjectGetter') +const PublicAccessLevels = require('../Authorization/PublicAccessLevels') +const Errors = require('../Errors/Errors') +const ProjectEditorHandler = require('../Project/ProjectEditorHandler') +const Sources = require('../Authorization/Sources') +const PrivilegeLevels = require('../Authorization/PrivilegeLevels') + +module.exports = { + getMemberIdsWithPrivilegeLevels: callbackify(getMemberIdsWithPrivilegeLevels), + getMemberIds: callbackify(getMemberIds), + getInvitedMemberIds: callbackify(getInvitedMemberIds), + getInvitedMembersWithPrivilegeLevels: callbackify( + getInvitedMembersWithPrivilegeLevels + ), + getInvitedMembersWithPrivilegeLevelsFromFields: callbackify( + getInvitedMembersWithPrivilegeLevelsFromFields + ), + getMemberIdPrivilegeLevel: callbackify(getMemberIdPrivilegeLevel), + getInvitedCollaboratorCount: callbackify(getInvitedCollaboratorCount), + getProjectsUserIsMemberOf: callbackify(getProjectsUserIsMemberOf), + isUserInvitedMemberOfProject: callbackify(isUserInvitedMemberOfProject), + userIsTokenMember: callbackify(userIsTokenMember), + getAllInvitedMembers: callbackify(getAllInvitedMembers), + promises: { + getMemberIdsWithPrivilegeLevels, + getMemberIds, + getInvitedMemberIds, + getInvitedMembersWithPrivilegeLevels, + getInvitedMembersWithPrivilegeLevelsFromFields, + getMemberIdPrivilegeLevel, + getInvitedCollaboratorCount, + getProjectsUserIsMemberOf, + isUserInvitedMemberOfProject, + userIsTokenMember, + getAllInvitedMembers + } +} + +async function getMemberIdsWithPrivilegeLevels(projectId) { + const project = await ProjectGetter.promises.getProject(projectId, { + owner_ref: 1, + collaberator_refs: 1, + readOnly_refs: 1, + tokenAccessReadOnly_refs: 1, + tokenAccessReadAndWrite_refs: 1, + publicAccesLevel: 1 + }) + if (!project) { + throw new Errors.NotFoundError(`no project found with id ${projectId}`) + } + const memberIds = _getMemberIdsWithPrivilegeLevelsFromFields( + project.owner_ref, + project.collaberator_refs, + project.readOnly_refs, + project.tokenAccessReadAndWrite_refs, + project.tokenAccessReadOnly_refs, + project.publicAccesLevel + ) + return memberIds +} + +async function getMemberIds(projectId) { + const members = await getMemberIdsWithPrivilegeLevels(projectId) + return members.map(m => m.id) +} + +async function getInvitedMemberIds(projectId) { + const members = await getMemberIdsWithPrivilegeLevels(projectId) + return members.filter(m => m.source !== Sources.TOKEN).map(m => m.id) +} + +async function getInvitedMembersWithPrivilegeLevels(projectId) { + let members = await getMemberIdsWithPrivilegeLevels(projectId) + members = members.filter(m => m.source !== Sources.TOKEN) + return _loadMembers(members) +} + +async function getInvitedMembersWithPrivilegeLevelsFromFields( + ownerId, + collaboratorIds, + readOnlyIds +) { + let members = _getMemberIdsWithPrivilegeLevelsFromFields( + ownerId, + collaboratorIds, + readOnlyIds, + [], + [], + null + ) + return _loadMembers(members) +} + +async function getMemberIdPrivilegeLevel(userId, projectId) { + // In future if the schema changes and getting all member ids is more expensive (multiple documents) + // then optimise this. + if (userId == null) { + return PrivilegeLevels.NONE + } + const members = await getMemberIdsWithPrivilegeLevels(projectId) + for (const member of members) { + if (member.id === userId.toString()) { + return member.privilegeLevel + } + } + return PrivilegeLevels.NONE +} + +async function getInvitedCollaboratorCount(projectId) { + const count = await _getInvitedMemberCount(projectId) + return count - 1 // Don't count project owner +} + +async function isUserInvitedMemberOfProject(userId, projectId) { + const members = await getMemberIdsWithPrivilegeLevels(projectId) + for (const member of members) { + if ( + member.id.toString() === userId.toString() && + member.source !== Sources.TOKEN + ) { + return true + } + } + return false +} + +async function getProjectsUserIsMemberOf(userId, fields) { + const limit = pLimit(2) + const [ + readAndWrite, + readOnly, + tokenReadAndWrite, + tokenReadOnly + ] = await Promise.all([ + limit(() => Project.find({ collaberator_refs: userId }, fields).exec()), + limit(() => Project.find({ readOnly_refs: userId }, fields).exec()), + limit(() => + Project.find( + { + tokenAccessReadAndWrite_refs: userId, + publicAccesLevel: PublicAccessLevels.TOKEN_BASED + }, + fields + ).exec() + ), + limit(() => + Project.find( + { + tokenAccessReadOnly_refs: userId, + publicAccesLevel: PublicAccessLevels.TOKEN_BASED + }, + fields + ).exec() + ) + ]) + return { readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly } +} + +async function getAllInvitedMembers(projectId) { + try { + const rawMembers = await getInvitedMembersWithPrivilegeLevels(projectId) + const { members } = ProjectEditorHandler.buildOwnerAndMembersViews( + rawMembers + ) + return members + } catch (err) { + throw new OError({ + message: 'error getting members for project', + info: { projectId } + }).withCause(err) + } +} + +async function userIsTokenMember(userId, projectId) { + userId = ObjectId(userId.toString()) + projectId = ObjectId(projectId.toString()) + const project = await Project.findOne( + { + _id: projectId, + $or: [ + { tokenAccessReadOnly_refs: userId }, + { tokenAccessReadAndWrite_refs: userId } + ] + }, + { + _id: 1 + } + ).exec() + return project != null +} + +async function _getInvitedMemberCount(projectId) { + const members = await getMemberIdsWithPrivilegeLevels(projectId) + return members.filter(m => m.source !== Sources.TOKEN).length +} + +function _getMemberIdsWithPrivilegeLevelsFromFields( + ownerId, + collaboratorIds, + readOnlyIds, + tokenAccessIds, + tokenAccessReadOnlyIds, + publicAccessLevel +) { + const members = [] + members.push({ + id: ownerId.toString(), + privilegeLevel: PrivilegeLevels.OWNER, + source: Sources.OWNER + }) + for (const memberId of collaboratorIds || []) { + members.push({ + id: memberId.toString(), + privilegeLevel: PrivilegeLevels.READ_AND_WRITE, + source: Sources.INVITE + }) + } + for (const memberId of readOnlyIds || []) { + members.push({ + id: memberId.toString(), + privilegeLevel: PrivilegeLevels.READ_ONLY, + source: Sources.INVITE + }) + } + if (publicAccessLevel === PublicAccessLevels.TOKEN_BASED) { + for (const memberId of tokenAccessIds || []) { + members.push({ + id: memberId.toString(), + privilegeLevel: PrivilegeLevels.READ_AND_WRITE, + source: Sources.TOKEN + }) + } + for (const memberId of tokenAccessReadOnlyIds || []) { + members.push({ + id: memberId.toString(), + privilegeLevel: PrivilegeLevels.READ_ONLY, + source: Sources.TOKEN + }) + } + } + return members +} + +async function _loadMembers(members) { + const limit = pLimit(3) + const results = await Promise.all( + members.map(member => + limit(async () => { + const user = await UserGetter.promises.getUser(member.id, { + _id: 1, + email: 1, + features: 1, + first_name: 1, + last_name: 1, + signUpDate: 1 + }) + if (user != null) { + return { user, privilegeLevel: member.privilegeLevel } + } else { + return null + } + }) + ) + ) + return results.filter(r => r != null) +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js b/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js index adcaf5e67b..30fdf3a09a 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js +++ b/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js @@ -1,672 +1,159 @@ -/* eslint-disable - camelcase, - handle-callback-err, - max-len, - no-unused-vars, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const UserCreator = require('../User/UserCreator') +const { callbackify } = require('util') +const OError = require('@overleaf/o-error') const { Project } = require('../../models/Project') const ProjectGetter = require('../Project/ProjectGetter') const logger = require('logger-sharelatex') -const UserGetter = require('../User/UserGetter') const ContactManager = require('../Contacts/ContactManager') -const CollaboratorsEmailHandler = require('./CollaboratorsEmailHandler') -const async = require('async') const PrivilegeLevels = require('../Authorization/PrivilegeLevels') -const PublicAccessLevels = require('../Authorization/PublicAccessLevels') -const Errors = require('../Errors/Errors') -const EmailHelper = require('../Helpers/EmailHelper') -const ProjectEditorHandler = require('../Project/ProjectEditorHandler') -const Sources = require('../Authorization/Sources') -const { ObjectId } = require('mongojs') -const { promisifyAll } = require('../../util/promises') +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') +const CollaboratorsGetter = require('./CollaboratorsGetter') -const CollaboratorsHandler = { - getMemberIdsWithPrivilegeLevels(project_id, callback) { - if (callback == null) { - callback = function(error, members) {} - } - const projection = { - owner_ref: 1, - collaberator_refs: 1, - readOnly_refs: 1, - tokenAccessReadOnly_refs: 1, - tokenAccessReadAndWrite_refs: 1, - publicAccesLevel: 1 - } - ProjectGetter.getProject(project_id, projection, (error, project) => { - if (error) { - return callback(error) - } - if (!project) { - return callback( - new Errors.NotFoundError(`no project found with id ${project_id}`) - ) - } - callback( - null, - CollaboratorsHandler.getMemberIdsWithPrivilegeLevelsFromFields( - project.owner_ref, - project.collaberator_refs, - project.readOnly_refs, - project.tokenAccessReadAndWrite_refs, - project.tokenAccessReadOnly_refs, - project.publicAccesLevel - ) - ) - }) - }, - - getMemberIdsWithPrivilegeLevelsFromFields( - ownerId, - collaboratorIds, - readOnlyIds, - tokenAccessIds, - tokenAccessReadOnlyIds, - publicAccessLevel - ) { - let member_id - const members = [] - members.push({ - id: ownerId.toString(), - privilegeLevel: PrivilegeLevels.OWNER, - source: Sources.OWNER - }) - for (member_id of Array.from(collaboratorIds || [])) { - members.push({ - id: member_id.toString(), - privilegeLevel: PrivilegeLevels.READ_AND_WRITE, - source: Sources.INVITE - }) - } - for (member_id of Array.from(readOnlyIds || [])) { - members.push({ - id: member_id.toString(), - privilegeLevel: PrivilegeLevels.READ_ONLY, - source: Sources.INVITE - }) - } - if (publicAccessLevel === PublicAccessLevels.TOKEN_BASED) { - for (member_id of Array.from(tokenAccessIds || [])) { - members.push({ - id: member_id.toString(), - privilegeLevel: PrivilegeLevels.READ_AND_WRITE, - source: Sources.TOKEN - }) - } - for (member_id of Array.from(tokenAccessReadOnlyIds || [])) { - members.push({ - id: member_id.toString(), - privilegeLevel: PrivilegeLevels.READ_ONLY, - source: Sources.TOKEN - }) - } - } - return members - }, - - getMemberIds(project_id, callback) { - if (callback == null) { - callback = function(error, member_ids) {} - } - return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( - project_id, - function(error, members) { - if (error != null) { - return callback(error) - } - return callback(null, members.map(m => m.id)) - } - ) - }, - - getInvitedMemberIds(project_id, callback) { - if (callback == null) { - callback = function(error, member_ids) {} - } - return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( - project_id, - function(error, members) { - if (error != null) { - return callback(error) - } - return callback( - null, - members.filter(m => m.source !== Sources.TOKEN).map(m => m.id) - ) - } - ) - }, - - USER_PROJECTION: { - _id: 1, - email: 1, - features: 1, - first_name: 1, - last_name: 1, - signUpDate: 1 - }, - _loadMembers(members, callback) { - if (callback == null) { - callback = function(error, users) {} - } - const result = [] - return async.mapLimit( - members, - 3, - (member, cb) => - UserGetter.getUser( - member.id, - CollaboratorsHandler.USER_PROJECTION, - function(error, user) { - if (error != null) { - return cb(error) - } - if (user != null) { - result.push({ user, privilegeLevel: member.privilegeLevel }) - } - return cb() - } - ), - function(error) { - if (error != null) { - return callback(error) - } - return callback(null, result) - } - ) - }, - - getMembersWithPrivilegeLevels(project_id, callback) { - if (callback == null) { - callback = function(error, members) {} - } - return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( - project_id, - function(error, members) { - if (members == null) { - members = [] - } - if (error != null) { - return callback(error) - } - return CollaboratorsHandler._loadMembers(members, (error, users) => - callback(error, users) - ) - } - ) - }, - - getInvitedMembersWithPrivilegeLevels(project_id, callback) { - if (callback == null) { - callback = function(error, members) {} - } - return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( - project_id, - function(error, members) { - if (members == null) { - members = [] - } - if (error != null) { - return callback(error) - } - members = members.filter(m => m.source !== Sources.TOKEN) - return CollaboratorsHandler._loadMembers(members, (error, users) => - callback(error, users) - ) - } - ) - }, - - getInvitedMembersWithPrivilegeLevelsFromFields( - ownerId, - collaboratorIds, - readOnlyIds, - callback - ) { - let members = CollaboratorsHandler.getMemberIdsWithPrivilegeLevelsFromFields( - ownerId, - collaboratorIds, - readOnlyIds, - [], - [], - null - ) - return CollaboratorsHandler._loadMembers(members, (error, users) => - callback(error, users) - ) - }, - - getTokenMembersWithPrivilegeLevels(project_id, callback) { - if (callback == null) { - callback = function(error, members) {} - } - return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( - project_id, - function(error, members) { - if (members == null) { - members = [] - } - if (error != null) { - return callback(error) - } - members = members.filter(m => m.source === Sources.TOKEN) - return CollaboratorsHandler._loadMembers(members, (error, users) => - callback(error, users) - ) - } - ) - }, - - getMemberIdPrivilegeLevel(user_id, project_id, callback) { - // In future if the schema changes and getting all member ids is more expensive (multiple documents) - // then optimise this. - if (callback == null) { - callback = function(error, privilegeLevel) {} - } - return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( - project_id, - function(error, members) { - if (members == null) { - members = [] - } - if (error != null) { - return callback(error) - } - for (let member of Array.from(members)) { - if ( - member.id === (user_id != null ? user_id.toString() : undefined) - ) { - return callback(null, member.privilegeLevel) - } - } - return callback(null, PrivilegeLevels.NONE) - } - ) - }, - - getInvitedMemberCount(project_id, callback) { - if (callback == null) { - callback = function(error, count) {} - } - return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( - project_id, - function(error, members) { - if (error != null) { - return callback(error) - } - return callback( - null, - (members || []).filter(m => m.source !== Sources.TOKEN).length - ) - } - ) - }, - - getInvitedCollaboratorCount(project_id, callback) { - if (callback == null) { - callback = function(error, count) {} - } - return CollaboratorsHandler.getInvitedMemberCount(project_id, function( - error, - count - ) { - if (error != null) { - return callback(error) - } - return callback(null, count - 1) - }) - }, // Don't count project owner - - isUserInvitedMemberOfProject(user_id, project_id, callback) { - if (callback == null) { - callback = function(error, isMember, privilegeLevel) {} - } - return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( - project_id, - function(error, members) { - if (members == null) { - members = [] - } - if (error != null) { - return callback(error) - } - for (let member of Array.from(members)) { - if ( - member.id.toString() === user_id.toString() && - member.source !== Sources.TOKEN - ) { - return callback(null, true, member.privilegeLevel) - } - } - return callback(null, false, null) - } - ) - }, - - getProjectsUserIsMemberOf(user_id, fields, callback) { - if (callback == null) { - callback = function(error, results) {} - } - return async.mapLimit( - [ - { collaberator_refs: user_id }, - { readOnly_refs: user_id }, - { - tokenAccessReadAndWrite_refs: user_id, - publicAccesLevel: PublicAccessLevels.TOKEN_BASED - }, - { - tokenAccessReadOnly_refs: user_id, - publicAccesLevel: PublicAccessLevels.TOKEN_BASED - } - ], - 2, - (query, cb) => Project.find(query, fields, cb), - function(error, results) { - if (error != null) { - return callback(error) - } - const projects = { - readAndWrite: results[0], - readOnly: results[1], - tokenReadAndWrite: results[2], - tokenReadOnly: results[3] - } - return callback(null, projects) - } - ) - }, - - removeUserFromProject(project_id, user_id, callback) { - if (callback == null) { - callback = function(error) {} - } - logger.log({ user_id, project_id }, 'removing user') - const conditions = { _id: project_id } - const update = { $pull: {} } - update['$pull'] = { - collaberator_refs: user_id, - readOnly_refs: user_id, - tokenAccessReadOnly_refs: user_id, - tokenAccessReadAndWrite_refs: user_id - } - return Project.update(conditions, update, function(err) { - if (err != null) { - logger.warn({ err }, 'problem removing user from project collaberators') - } - return callback(err) - }) - }, - - removeUserFromAllProjets(user_id, callback) { - if (callback == null) { - callback = function(error) {} - } - return CollaboratorsHandler.getProjectsUserIsMemberOf( - user_id, - { _id: 1 }, - function( - error, - { readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly } - ) { - if (error != null) { - return callback(error) - } - const allProjects = readAndWrite - .concat(readOnly) - .concat(tokenReadAndWrite) - .concat(tokenReadOnly) - const jobs = [] - for (let project of Array.from(allProjects)) { - ;(project => - jobs.push(function(cb) { - if (project == null) { - return cb() - } - return CollaboratorsHandler.removeUserFromProject( - project._id, - user_id, - cb - ) - }))(project) - } - return async.series(jobs, callback) - } - ) - }, - - addUserIdToProject( - project_id, - adding_user_id, - user_id, - privilegeLevel, - callback - ) { - if (callback == null) { - callback = function(error) {} - } - return ProjectGetter.getProject( - project_id, - { collaberator_refs: 1, readOnly_refs: 1 }, - function(error, project) { - let level - if (error != null) { - return callback(error) - } - let existing_users = project.collaberator_refs || [] - existing_users = existing_users.concat(project.readOnly_refs || []) - existing_users = existing_users.map(u => u.toString()) - if (existing_users.indexOf(user_id.toString()) > -1) { - return callback(null) // User already in Project - } - - if (privilegeLevel === PrivilegeLevels.READ_AND_WRITE) { - level = { collaberator_refs: user_id } - logger.log( - { privileges: 'readAndWrite', user_id, project_id }, - 'adding user' - ) - } else if (privilegeLevel === PrivilegeLevels.READ_ONLY) { - level = { readOnly_refs: user_id } - logger.log( - { privileges: 'readOnly', user_id, project_id }, - 'adding user' - ) - } else { - return callback( - new Error(`unknown privilegeLevel: ${privilegeLevel}`) - ) - } - - if (adding_user_id) { - ContactManager.addContact(adding_user_id, user_id) - } - - return Project.update( - { _id: project_id }, - { $addToSet: level }, - function(error) { - if (error != null) { - return callback(error) - } - // Flush to TPDS in background to add files to collaborator's Dropbox - const ProjectEntityHandler = require('../Project/ProjectEntityHandler') - ProjectEntityHandler.flushProjectToThirdPartyDataStore( - project_id, - function(error) { - if (error != null) { - return logger.error( - { err: error, project_id, user_id }, - 'error flushing to TPDS after adding collaborator' - ) - } - } - ) - return callback() - } - ) - } - ) - }, - - getAllInvitedMembers(projectId, callback) { - if (callback == null) { - callback = function(err, members) {} - } - logger.log({ projectId }, 'fetching all members') - return CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels( - projectId, - function(error, rawMembers) { - if (error != null) { - logger.warn({ projectId, error }, 'error getting members for project') - return callback(error) - } - const { - owner, - members - } = ProjectEditorHandler.buildOwnerAndMembersViews(rawMembers) - return callback(null, members) - } - ) - }, - - userIsTokenMember(userId, projectId, callback) { - if (callback == null) { - callback = function(err, isTokenMember) {} - } - userId = ObjectId(userId.toString()) - projectId = ObjectId(projectId.toString()) - return Project.findOne( - { - _id: projectId, - $or: [ - { tokenAccessReadOnly_refs: userId }, - { tokenAccessReadAndWrite_refs: userId } - ] - }, - { - _id: 1 - }, - function(err, project) { - if (err != null) { - return callback(err) - } - return callback(null, project != null) - } - ) - }, - - transferProjects(from_user_id, to_user_id, callback) { - if (callback == null) { - callback = function(err, projects) {} - } - const MEMBER_KEYS = ['collaberator_refs', 'readOnly_refs'] - - // Find all the projects this user is part of so we can flush them to TPDS - let query = { - $or: [{ owner_ref: from_user_id }].concat( - MEMBER_KEYS.map(function(key) { - const q = {} - q[key] = from_user_id - return q - }) - ) // [{ collaberator_refs: from_user_id }, ...] - } - return Project.find(query, { _id: 1 }, function(error, projects) { - if (projects == null) { - projects = [] - } - if (error != null) { - return callback(error) - } - - const project_ids = projects.map(p => p._id) - logger.log( - { project_ids, from_user_id, to_user_id }, - 'transferring projects' - ) - - const update_jobs = [] - update_jobs.push(cb => - Project.update( - { owner_ref: from_user_id }, - { $set: { owner_ref: to_user_id } }, - { multi: true }, - cb - ) - ) - for (let key of Array.from(MEMBER_KEYS)) { - ;(key => - update_jobs.push(function(cb) { - query = {} - const addNewUserUpdate = { $addToSet: {} } - const removeOldUserUpdate = { $pull: {} } - query[key] = from_user_id - removeOldUserUpdate.$pull[key] = from_user_id - addNewUserUpdate.$addToSet[key] = to_user_id - // Mongo won't let us pull and addToSet in the same query, so do it in - // two. Note we need to add first, since the query is based on the old user. - return Project.update( - query, - addNewUserUpdate, - { multi: true }, - function(error) { - if (error != null) { - return cb(error) - } - return Project.update( - query, - removeOldUserUpdate, - { multi: true }, - cb - ) - } - ) - }))(key) - } - - // Flush each project to TPDS to add files to new user's Dropbox - const ProjectEntityHandler = require('../Project/ProjectEntityHandler') - const flush_jobs = [] - for (let project_id of Array.from(project_ids)) { - ;(project_id => - flush_jobs.push(cb => - ProjectEntityHandler.flushProjectToThirdPartyDataStore( - project_id, - cb - ) - ))(project_id) - } - - // Flush in background, no need to block on this - async.series(flush_jobs, function(error) { - if (error != null) { - return logger.err( - { err: error, project_ids, from_user_id, to_user_id }, - 'error flushing tranferred projects to TPDS' - ) - } - }) - - return async.series(update_jobs, function(err) { - logger.log('flushed transferred projects to TPDS') - return callback(err) - }) - }) +module.exports = { + removeUserFromProject: callbackify(removeUserFromProject), + removeUserFromAllProjects: callbackify(removeUserFromAllProjects), + addUserIdToProject: callbackify(addUserIdToProject), + transferProjects: callbackify(transferProjects), + promises: { + removeUserFromProject, + removeUserFromAllProjects, + addUserIdToProject, + transferProjects } } -CollaboratorsHandler.promises = promisifyAll(CollaboratorsHandler, { - without: ['getMemberIdsWithPrivilegeLevelsFromFields'] -}) -module.exports = CollaboratorsHandler +async function removeUserFromProject(projectId, userId) { + try { + await Project.update( + { _id: projectId }, + { + $pull: { + collaberator_refs: userId, + readOnly_refs: userId, + tokenAccessReadOnly_refs: userId, + tokenAccessReadAndWrite_refs: userId + } + } + ).exec() + } catch (err) { + throw new OError({ + message: 'problem removing user from project collaborators', + info: { projectId, userId } + }).withCause(err) + } +} + +async function removeUserFromAllProjects(userId) { + const { + readAndWrite, + readOnly, + tokenReadAndWrite, + tokenReadOnly + } = await CollaboratorsGetter.promises.getProjectsUserIsMemberOf(userId, { + _id: 1 + }) + const allProjects = readAndWrite + .concat(readOnly) + .concat(tokenReadAndWrite) + .concat(tokenReadOnly) + for (const project of allProjects) { + await removeUserFromProject(project._id, userId) + } +} + +async function addUserIdToProject( + projectId, + addingUserId, + userId, + privilegeLevel +) { + const project = await ProjectGetter.promises.getProject(projectId, { + collaberator_refs: 1, + readOnly_refs: 1 + }) + let level + let existingUsers = project.collaberator_refs || [] + existingUsers = existingUsers.concat(project.readOnly_refs || []) + existingUsers = existingUsers.map(u => u.toString()) + if (existingUsers.includes(userId.toString())) { + return // User already in Project + } + if (privilegeLevel === PrivilegeLevels.READ_AND_WRITE) { + level = { collaberator_refs: userId } + logger.log({ privileges: 'readAndWrite', userId, projectId }, 'adding user') + } else if (privilegeLevel === PrivilegeLevels.READ_ONLY) { + level = { readOnly_refs: userId } + logger.log({ privileges: 'readOnly', userId, projectId }, 'adding user') + } else { + throw new Error(`unknown privilegeLevel: ${privilegeLevel}`) + } + + if (addingUserId) { + ContactManager.addContact(addingUserId, userId) + } + + await Project.update({ _id: projectId }, { $addToSet: level }).exec() + + // Flush to TPDS in background to add files to collaborator's Dropbox + ProjectEntityHandler.promises + .flushProjectToThirdPartyDataStore(projectId) + .catch(err => { + logger.error( + { err, projectId, userId }, + 'error flushing to TPDS after adding collaborator' + ) + }) +} + +async function transferProjects(fromUserId, toUserId) { + // Find all the projects this user is part of so we can flush them to TPDS + const projects = await Project.find( + { + $or: [ + { owner_ref: fromUserId }, + { collaberator_refs: fromUserId }, + { readOnly_refs: fromUserId } + ] + }, + { _id: 1 } + ).exec() + const projectIds = projects.map(p => p._id) + logger.log({ projectIds, fromUserId, toUserId }, 'transferring projects') + + await Project.update( + { owner_ref: fromUserId }, + { $set: { owner_ref: toUserId } }, + { multi: true } + ).exec() + await Project.update( + { collaberator_refs: fromUserId }, + { + $addToSet: { collaberator_refs: toUserId }, + $pull: { collaberator_refs: fromUserId } + }, + { multi: true } + ).exec() + await Project.update( + { readOnly_refs: fromUserId }, + { + $addToSet: { readOnly_refs: toUserId }, + $pull: { readOnly_refs: fromUserId } + }, + { multi: true } + ).exec() + + // Flush in background, no need to block on this + _flushProjects(projectIds).catch(err => { + logger.err( + { err, projectIds, fromUserId, toUserId }, + 'error flushing tranferred projects to TPDS' + ) + }) +} + +async function _flushProjects(projectIds) { + for (const projectId of projectIds) { + await ProjectEntityHandler.promises.flushProjectToThirdPartyDataStore( + projectId + ) + } +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js b/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js index 933e514148..f859fc16e1 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js +++ b/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js @@ -16,7 +16,7 @@ let CollaboratorsInviteController const ProjectGetter = require('../Project/ProjectGetter') const LimitationsManager = require('../Subscription/LimitationsManager') const UserGetter = require('../User/UserGetter') -const CollaboratorsHandler = require('./CollaboratorsHandler') +const CollaboratorsGetter = require('./CollaboratorsGetter') const CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') const logger = require('logger-sharelatex') const Settings = require('settings-sharelatex') @@ -257,10 +257,10 @@ module.exports = CollaboratorsInviteController = { } // check if the user is already a member of the project const currentUser = AuthenticationController.getSessionUser(req) - return CollaboratorsHandler.isUserInvitedMemberOfProject( + return CollaboratorsGetter.isUserInvitedMemberOfProject( currentUser._id, projectId, - function(err, isMember, _privilegeLevel) { + function(err, isMember) { if (err != null) { logger.warn( { err, projectId }, diff --git a/services/web/app/src/Features/Editor/EditorHttpController.js b/services/web/app/src/Features/Editor/EditorHttpController.js index 2f3a29866c..78e830c440 100644 --- a/services/web/app/src/Features/Editor/EditorHttpController.js +++ b/services/web/app/src/Features/Editor/EditorHttpController.js @@ -23,7 +23,7 @@ const UserGetter = require('../User/UserGetter') const AuthorizationManager = require('../Authorization/AuthorizationManager') const ProjectEditorHandler = require('../Project/ProjectEditorHandler') const Metrics = require('metrics-sharelatex') -const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') const CollaboratorsInviteHandler = require('../Collaborators/CollaboratorsInviteHandler') const PrivilegeLevels = require('../Authorization/PrivilegeLevels') const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') @@ -76,7 +76,7 @@ module.exports = EditorHttpController = { if (project == null) { return callback(new Errors.NotFoundError('project not found')) } - return CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels( + return CollaboratorsGetter.getInvitedMembersWithPrivilegeLevels( project_id, function(error, members) { if (error != null) { diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index c7eca1065b..e8998f3d39 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -28,7 +28,7 @@ const AuthenticationController = require('../Authentication/AuthenticationContro const PackageVersions = require('../../infrastructure/PackageVersions') const Sources = require('../Authorization/Sources') const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') -const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') const Modules = require('../../infrastructure/Modules') const ProjectEntityHandler = require('./ProjectEntityHandler') const UserGetter = require('../User/UserGetter') @@ -616,7 +616,7 @@ const ProjectController = { if (userId == null) { return cb() } - CollaboratorsHandler.userIsTokenMember(userId, projectId, cb) + CollaboratorsGetter.userIsTokenMember(userId, projectId, cb) }, brandVariation: [ 'project', diff --git a/services/web/app/src/Features/Project/ProjectDeleter.js b/services/web/app/src/Features/Project/ProjectDeleter.js index d9bc87b1d6..8293f49b9f 100644 --- a/services/web/app/src/Features/Project/ProjectDeleter.js +++ b/services/web/app/src/Features/Project/ProjectDeleter.js @@ -23,6 +23,7 @@ const async = require('async') const ProjectHelper = require('./ProjectHelper') const ProjectDetailsHandler = require('./ProjectDetailsHandler') const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') const DocstoreManager = require('../Docstore/DocstoreManager') const moment = require('moment') @@ -73,7 +74,7 @@ const ProjectDeleter = { if (err != null) { return callback(err) } - return CollaboratorsHandler.removeUserFromAllProjets( + return CollaboratorsHandler.removeUserFromAllProjects( user_id, callback ) @@ -249,8 +250,7 @@ async function deleteProject(project_id, options = {}) { ) await flushProjectToMongoAndDelete(project_id) - const getMemberIds = promisify(CollaboratorsHandler.getMemberIds) - let member_ids = await getMemberIds(project_id) + let member_ids = await CollaboratorsGetter.promises.getMemberIds(project_id) // fire these jobs in the background Array.from(member_ids).forEach(member_id => diff --git a/services/web/app/src/Features/Project/ProjectGetter.js b/services/web/app/src/Features/Project/ProjectGetter.js index 18b4752383..6ba34b28e3 100644 --- a/services/web/app/src/Features/Project/ProjectGetter.js +++ b/services/web/app/src/Features/Project/ProjectGetter.js @@ -181,7 +181,7 @@ const ProjectGetter = { } } } - const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') + const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') return Project.find({ owner_ref: user_id }, fields, function( error, ownedProjects @@ -189,7 +189,7 @@ const ProjectGetter = { if (error != null) { return callback(error) } - return CollaboratorsHandler.getProjectsUserIsMemberOf( + return CollaboratorsGetter.getProjectsUserIsMemberOf( user_id, fields, function(error, projects) { diff --git a/services/web/app/src/Features/Subscription/LimitationsManager.js b/services/web/app/src/Features/Subscription/LimitationsManager.js index 074d5ce671..b5cdffd5ce 100644 --- a/services/web/app/src/Features/Subscription/LimitationsManager.js +++ b/services/web/app/src/Features/Subscription/LimitationsManager.js @@ -18,7 +18,7 @@ const ProjectGetter = require('../Project/ProjectGetter') const UserGetter = require('../User/UserGetter') const SubscriptionLocator = require('./SubscriptionLocator') const Settings = require('settings-sharelatex') -const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') const CollaboratorsInvitesHandler = require('../Collaborators/CollaboratorsInviteHandler') const V1SubscriptionManager = require('./V1SubscriptionManager') @@ -62,7 +62,7 @@ module.exports = LimitationsManager = { if (error != null) { return callback(error) } - return CollaboratorsHandler.getInvitedCollaboratorCount( + return CollaboratorsGetter.getInvitedCollaboratorCount( project_id, (error, current_number) => { if (error != null) { diff --git a/services/web/app/src/Features/Tags/TagsHandler.js b/services/web/app/src/Features/Tags/TagsHandler.js index fa049823e4..f2b48188e4 100644 --- a/services/web/app/src/Features/Tags/TagsHandler.js +++ b/services/web/app/src/Features/Tags/TagsHandler.js @@ -12,13 +12,13 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let TagsHandler const settings = require('settings-sharelatex') const request = require('request') const logger = require('logger-sharelatex') +const { promisifyAll } = require('../../util/promises') const TIMEOUT = 1000 -module.exports = TagsHandler = { +const TagsHandler = { getAllTags(user_id, callback) { this._requestTags(user_id, (err, allTags) => { if (allTags == null) { @@ -205,3 +205,6 @@ module.exports = TagsHandler = { ) } } + +TagsHandler.promises = promisifyAll(TagsHandler) +module.exports = TagsHandler diff --git a/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js b/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js index eec83e1f41..0cbf47c0a1 100644 --- a/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js +++ b/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js @@ -21,7 +21,7 @@ const ProjectGetter = require('../Project/ProjectGetter') const keys = require('../../infrastructure/Keys') const metrics = require('metrics-sharelatex') const request = require('request') -const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') const { promisifyAll } = require('../../util/promises') const buildPath = function(user_id, project_name, filePath) { @@ -298,7 +298,7 @@ var getProjectsUsersIds = function(project_id, callback) { if (err != null) { return callback(err) } - return CollaboratorsHandler.getInvitedMemberIds(project_id, function( + return CollaboratorsGetter.getInvitedMemberIds(project_id, function( err, member_ids ) { diff --git a/services/web/app/src/Features/TokenAccess/TokenAccessHandler.js b/services/web/app/src/Features/TokenAccess/TokenAccessHandler.js index d39faa63ca..d4a9b1bc55 100644 --- a/services/web/app/src/Features/TokenAccess/TokenAccessHandler.js +++ b/services/web/app/src/Features/TokenAccess/TokenAccessHandler.js @@ -1,5 +1,5 @@ const { Project } = require('../../models/Project') -const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') const PublicAccessLevels = require('../Authorization/PublicAccessLevels') const PrivilegeLevels = require('../Authorization/PrivilegeLevels') const UserGetter = require('../User/UserGetter') @@ -133,7 +133,7 @@ const TokenAccessHandler = { }, _userIsMember(userId, projectId, callback) { - CollaboratorsHandler.isUserInvitedMemberOfProject( + CollaboratorsGetter.isUserInvitedMemberOfProject( userId, projectId, callback diff --git a/services/web/app/src/Features/UserMembership/UserMembershipMiddleware.js b/services/web/app/src/Features/UserMembership/UserMembershipMiddleware.js index 1f1ea67b4a..a078fc31e5 100644 --- a/services/web/app/src/Features/UserMembership/UserMembershipMiddleware.js +++ b/services/web/app/src/Features/UserMembership/UserMembershipMiddleware.js @@ -1,4 +1,4 @@ -const expressify = require('../../util/expressify') +const { expressify } = require('../../util/promises') const async = require('async') const UserMembershipAuthorization = require('./UserMembershipAuthorization') const AuthenticationController = require('../Authentication/AuthenticationController') diff --git a/services/web/app/src/util/expressify.js b/services/web/app/src/util/expressify.js deleted file mode 100644 index 9ced59a66a..0000000000 --- a/services/web/app/src/util/expressify.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = function expressify(fn) { - return (req, res, next) => { - fn(req, res, next).catch(next) - } -} diff --git a/services/web/app/src/util/promises.js b/services/web/app/src/util/promises.js index fa4379a6d1..967b5bd9b0 100644 --- a/services/web/app/src/util/promises.js +++ b/services/web/app/src/util/promises.js @@ -1,6 +1,9 @@ const { promisify } = require('util') -module.exports = { promisifyAll } +module.exports = { + promisifyAll, + expressify +} /** * Promisify all functions in a module. @@ -29,3 +32,14 @@ function promisifyAll(module, opts = {}) { } return promises } + +/** + * Transform an async function into an Express middleware + * + * Any error will be passed to the error middlewares via `next()` + */ +function expressify(fn) { + return (req, res, next) => { + fn(req, res, next).catch(next) + } +} diff --git a/services/web/package-lock.json b/services/web/package-lock.json index a03ae28956..b103d6648a 100644 --- a/services/web/package-lock.json +++ b/services/web/package-lock.json @@ -15069,12 +15069,11 @@ "dev": true }, "p-limit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", - "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", - "dev": true, + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", "requires": { - "p-try": "^1.0.0" + "p-try": "^2.0.0" } }, "p-locate": { @@ -15084,6 +15083,23 @@ "dev": true, "requires": { "p-limit": "^1.1.0" + }, + "dependencies": { + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + } } }, "p-map": { @@ -15130,10 +15146,9 @@ } }, "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "pac-proxy-agent": { "version": "2.0.2", diff --git a/services/web/package.json b/services/web/package.json index 9ab8358861..6c0d12f107 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -86,6 +86,7 @@ "nvd3": "^1.8.6", "oauth2-server": "^3.0.1", "optimist": "0.6.1", + "p-limit": "^2.2.1", "passport": "^0.3.2", "passport-google-oauth20": "^1.0.0", "passport-ldapauth": "^0.6.0", diff --git a/services/web/test/unit/bootstrap.js b/services/web/test/unit/bootstrap.js index 61317b2bad..ef5fb6f278 100644 --- a/services/web/test/unit/bootstrap.js +++ b/services/web/test/unit/bootstrap.js @@ -8,6 +8,9 @@ chai.use(require('sinon-chai')) // Load promise support for chai chai.use(require('chai-as-promised')) +// Do not truncate assertion errors +chai.config.truncateThreshold = 0 + // add support for promises in sinon require('sinon-as-promised') // add support for mongoose in sinon diff --git a/services/web/test/unit/src/Authorization/AuthorizationManagerTests.js b/services/web/test/unit/src/Authorization/AuthorizationManagerTests.js index a42d6b7970..8c197c20df 100644 --- a/services/web/test/unit/src/Authorization/AuthorizationManagerTests.js +++ b/services/web/test/unit/src/Authorization/AuthorizationManagerTests.js @@ -27,7 +27,7 @@ describe('AuthorizationManager', function() { console: console }, requires: { - '../Collaborators/CollaboratorsHandler': (this.CollaboratorsHandler = {}), + '../Collaborators/CollaboratorsGetter': (this.CollaboratorsGetter = {}), '../Project/ProjectGetter': (this.ProjectGetter = {}), '../../models/User': { User: (this.User = {}) @@ -49,7 +49,7 @@ describe('AuthorizationManager', function() { beforeEach(function() { this.ProjectGetter.getProject = sinon.stub() this.AuthorizationManager.isUserSiteAdmin = sinon.stub() - return (this.CollaboratorsHandler.getMemberIdPrivilegeLevel = sinon.stub()) + return (this.CollaboratorsGetter.getMemberIdPrivilegeLevel = sinon.stub()) }) describe('with a token-based project', function() { @@ -64,7 +64,7 @@ describe('AuthorizationManager', function() { this.AuthorizationManager.isUserSiteAdmin .withArgs(this.user_id) .yields(null, false) - this.CollaboratorsHandler.getMemberIdPrivilegeLevel + this.CollaboratorsGetter.getMemberIdPrivilegeLevel .withArgs(this.user_id, this.project_id) .yields(null, 'readOnly') return this.AuthorizationManager.getPrivilegeLevelForProject( @@ -87,7 +87,7 @@ describe('AuthorizationManager', function() { this.AuthorizationManager.isUserSiteAdmin .withArgs(this.user_id) .yields(null, false) - this.CollaboratorsHandler.getMemberIdPrivilegeLevel + this.CollaboratorsGetter.getMemberIdPrivilegeLevel .withArgs(this.user_id, this.project_id) .yields(null, false) return this.AuthorizationManager.getPrivilegeLevelForProject( @@ -110,7 +110,7 @@ describe('AuthorizationManager', function() { this.AuthorizationManager.isUserSiteAdmin .withArgs(this.user_id) .yields(null, true) - this.CollaboratorsHandler.getMemberIdPrivilegeLevel + this.CollaboratorsGetter.getMemberIdPrivilegeLevel .withArgs(this.user_id, this.project_id) .yields(null, false) return this.AuthorizationManager.getPrivilegeLevelForProject( @@ -143,8 +143,8 @@ describe('AuthorizationManager', function() { ) }) - it('should not call CollaboratorsHandler.getMemberIdPrivilegeLevel', function() { - return this.CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal( + it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function() { + return this.CollaboratorsGetter.getMemberIdPrivilegeLevel.called.should.equal( false ) }) @@ -184,8 +184,8 @@ describe('AuthorizationManager', function() { ) }) - it('should not call CollaboratorsHandler.getMemberIdPrivilegeLevel', function() { - return this.CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal( + it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function() { + return this.CollaboratorsGetter.getMemberIdPrivilegeLevel.called.should.equal( false ) }) @@ -224,8 +224,8 @@ describe('AuthorizationManager', function() { ) }) - it('should not call CollaboratorsHandler.getMemberIdPrivilegeLevel', function() { - return this.CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal( + it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function() { + return this.CollaboratorsGetter.getMemberIdPrivilegeLevel.called.should.equal( false ) }) @@ -264,8 +264,8 @@ describe('AuthorizationManager', function() { ) }) - it('should not call CollaboratorsHandler.getMemberIdPrivilegeLevel', function() { - return this.CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal( + it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function() { + return this.CollaboratorsGetter.getMemberIdPrivilegeLevel.called.should.equal( false ) }) @@ -303,7 +303,7 @@ describe('AuthorizationManager', function() { this.AuthorizationManager.isUserSiteAdmin .withArgs(this.user_id) .yields(null, false) - this.CollaboratorsHandler.getMemberIdPrivilegeLevel + this.CollaboratorsGetter.getMemberIdPrivilegeLevel .withArgs(this.user_id, this.project_id) .yields(null, 'readOnly') return this.AuthorizationManager.getPrivilegeLevelForProject( @@ -326,7 +326,7 @@ describe('AuthorizationManager', function() { this.AuthorizationManager.isUserSiteAdmin .withArgs(this.user_id) .yields(null, false) - this.CollaboratorsHandler.getMemberIdPrivilegeLevel + this.CollaboratorsGetter.getMemberIdPrivilegeLevel .withArgs(this.user_id, this.project_id) .yields(null, false) return this.AuthorizationManager.getPrivilegeLevelForProject( @@ -349,7 +349,7 @@ describe('AuthorizationManager', function() { this.AuthorizationManager.isUserSiteAdmin .withArgs(this.user_id) .yields(null, true) - this.CollaboratorsHandler.getMemberIdPrivilegeLevel + this.CollaboratorsGetter.getMemberIdPrivilegeLevel .withArgs(this.user_id, this.project_id) .yields(null, false) return this.AuthorizationManager.getPrivilegeLevelForProject( @@ -377,8 +377,8 @@ describe('AuthorizationManager', function() { ) }) - it('should not call CollaboratorsHandler.getMemberIdPrivilegeLevel', function() { - return this.CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal( + it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function() { + return this.CollaboratorsGetter.getMemberIdPrivilegeLevel.called.should.equal( false ) }) @@ -409,7 +409,7 @@ describe('AuthorizationManager', function() { this.AuthorizationManager.isUserSiteAdmin .withArgs(this.user_id) .yields(null, false) - this.CollaboratorsHandler.getMemberIdPrivilegeLevel + this.CollaboratorsGetter.getMemberIdPrivilegeLevel .withArgs(this.user_id, this.project_id) .yields(null, 'readOnly') return this.AuthorizationManager.getPrivilegeLevelForProject( @@ -432,7 +432,7 @@ describe('AuthorizationManager', function() { this.AuthorizationManager.isUserSiteAdmin .withArgs(this.user_id) .yields(null, false) - this.CollaboratorsHandler.getMemberIdPrivilegeLevel + this.CollaboratorsGetter.getMemberIdPrivilegeLevel .withArgs(this.user_id, this.project_id) .yields(null, false) return this.AuthorizationManager.getPrivilegeLevelForProject( @@ -455,7 +455,7 @@ describe('AuthorizationManager', function() { this.AuthorizationManager.isUserSiteAdmin .withArgs(this.user_id) .yields(null, true) - this.CollaboratorsHandler.getMemberIdPrivilegeLevel + this.CollaboratorsGetter.getMemberIdPrivilegeLevel .withArgs(this.user_id, this.project_id) .yields(null, false) return this.AuthorizationManager.getPrivilegeLevelForProject( @@ -483,8 +483,8 @@ describe('AuthorizationManager', function() { ) }) - it('should not call CollaboratorsHandler.getMemberIdPrivilegeLevel', function() { - return this.CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal( + it('should not call CollaboratorsGetter.getMemberIdPrivilegeLevel', function() { + return this.CollaboratorsGetter.getMemberIdPrivilegeLevel.called.should.equal( false ) }) @@ -525,7 +525,7 @@ describe('AuthorizationManager', function() { this.AuthorizationManager.isUserSiteAdmin .withArgs(this.user_id) .yields(null, false) - return this.CollaboratorsHandler.getMemberIdPrivilegeLevel + return this.CollaboratorsGetter.getMemberIdPrivilegeLevel .withArgs(this.user_id, this.project_id) .yields(null, 'readOnly') }) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.js b/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.js index e98d863678..025d8d817d 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.js +++ b/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.js @@ -1,177 +1,174 @@ -/* eslint-disable - max-len, - no-return-assign, - no-unused-vars, -*/ -// 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 sinon = require('sinon') -const chai = require('chai') -const should = chai.should() -const { expect } = chai -const modulePath = - '../../../../app/src/Features/Collaborators/CollaboratorsController.js' +const { expect } = require('chai') const SandboxedModule = require('sandboxed-module') -const events = require('events') const MockRequest = require('../helpers/MockRequest') const MockResponse = require('../helpers/MockResponse') -const { ObjectId } = require('mongojs') + +const MODULE_PATH = + '../../../../app/src/Features/Collaborators/CollaboratorsController.js' describe('CollaboratorsController', function() { beforeEach(function() { - this.CollaboratorsController = SandboxedModule.require(modulePath, { + this.res = new MockResponse() + this.req = new MockRequest() + + this.user_id = 'user-id-123' + this.project_id = 'project-id-123' + this.callback = sinon.stub() + + this.CollaboratorsHandler = { + promises: { + removeUserFromProject: sinon.stub().resolves() + } + } + this.CollaboratorsGetter = { + promises: { + getAllInvitedMembers: sinon.stub() + } + } + this.EditorRealTimeController = { + emitToRoom: sinon.stub() + } + this.TagsHandler = { + promises: { + removeProjectFromAllTags: sinon.stub().resolves() + } + } + this.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(this.user_id) + } + this.logger = { + err: sinon.stub(), + warn: sinon.stub(), + log: sinon.stub() + } + + this.CollaboratorsController = SandboxedModule.require(MODULE_PATH, { globals: { console: console }, requires: { - './CollaboratorsHandler': (this.CollaboratorsHandler = {}), - '../Editor/EditorRealTimeController': (this.EditorRealTimeController = {}), - '../Tags/TagsHandler': (this.TagsHandler = {}), - '../Authentication/AuthenticationController': (this.AuthenticationController = {}), - 'logger-sharelatex': (this.logger = { - err: sinon.stub(), - warn: sinon.stub(), - log: sinon.stub() - }) + './CollaboratorsHandler': this.CollaboratorsHandler, + './CollaboratorsGetter': this.CollaboratorsGetter, + '../Editor/EditorRealTimeController': this.EditorRealTimeController, + '../Tags/TagsHandler': this.TagsHandler, + '../Authentication/AuthenticationController': this + .AuthenticationController, + 'logger-sharelatex': this.logger } }) - this.res = new MockResponse() - this.req = new MockRequest() - - this.project_id = 'project-id-123' - return (this.callback = sinon.stub()) }) describe('removeUserFromProject', function() { - beforeEach(function() { + beforeEach(function(done) { this.req.params = { - Project_id: (this.project_id = 'project-id-123'), - user_id: (this.user_id = 'user-id-123') + Project_id: this.project_id, + user_id: this.user_id } - this.res.sendStatus = sinon.stub() - this.CollaboratorsHandler.removeUserFromProject = sinon.stub().callsArg(2) - this.EditorRealTimeController.emitToRoom = sinon.stub() - this.TagsHandler.removeProjectFromAllTags = sinon.stub().callsArg(2) - return this.CollaboratorsController.removeUserFromProject( - this.req, - this.res - ) + this.res.sendStatus = sinon.spy(() => { + done() + }) + this.CollaboratorsController.removeUserFromProject(this.req, this.res) }) it('should from the user from the project', function() { - return this.CollaboratorsHandler.removeUserFromProject - .calledWith(this.project_id, this.user_id) - .should.equal(true) + expect( + this.CollaboratorsHandler.promises.removeUserFromProject + ).to.have.been.calledWith(this.project_id, this.user_id) }) it('should emit a userRemovedFromProject event to the proejct', function() { - return this.EditorRealTimeController.emitToRoom - .calledWith(this.project_id, 'userRemovedFromProject', this.user_id) - .should.equal(true) + expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + this.project_id, + 'userRemovedFromProject', + this.user_id + ) }) it('should send the back a success response', function() { - return this.res.sendStatus.calledWith(204).should.equal(true) + this.res.sendStatus.calledWith(204).should.equal(true) }) it('should have called emitToRoom', function() { - return this.EditorRealTimeController.emitToRoom - .calledWith(this.project_id, 'project:membership:changed') - .should.equal(true) + expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + this.project_id, + 'project:membership:changed' + ) }) }) describe('removeSelfFromProject', function() { - beforeEach(function() { - this.user_id = 'user-id-123' - this.AuthenticationController.getLoggedInUserId = sinon - .stub() - .returns(this.user_id) + beforeEach(function(done) { this.req.params = { Project_id: this.project_id } - this.res.sendStatus = sinon.stub() - this.CollaboratorsHandler.removeUserFromProject = sinon.stub().callsArg(2) - this.EditorRealTimeController.emitToRoom = sinon.stub() - this.TagsHandler.removeProjectFromAllTags = sinon.stub().callsArg(2) - return this.CollaboratorsController.removeSelfFromProject( - this.req, - this.res - ) + this.res.sendStatus = sinon.spy(() => { + done() + }) + this.CollaboratorsController.removeSelfFromProject(this.req, this.res) }) it('should remove the logged in user from the project', function() { - return this.CollaboratorsHandler.removeUserFromProject - .calledWith(this.project_id, this.user_id) - .should.equal(true) + expect( + this.CollaboratorsHandler.promises.removeUserFromProject + ).to.have.been.calledWith(this.project_id, this.user_id) }) it('should emit a userRemovedFromProject event to the proejct', function() { - return this.EditorRealTimeController.emitToRoom - .calledWith(this.project_id, 'userRemovedFromProject', this.user_id) - .should.equal(true) - }) - - it('should remove the project from all tags', function() { - sinon.assert.calledWith( - this.TagsHandler.removeProjectFromAllTags, - this.user_id, - this.project_id + expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + this.project_id, + 'userRemovedFromProject', + this.user_id ) }) + it('should remove the project from all tags', function() { + expect( + this.TagsHandler.promises.removeProjectFromAllTags + ).to.have.been.calledWith(this.user_id, this.project_id) + }) + it('should return a success code', function() { - return this.res.sendStatus.calledWith(204).should.equal(true) + this.res.sendStatus.calledWith(204).should.equal(true) }) }) describe('getAllMembers', function() { - beforeEach(function() { - this.AuthenticationController.getLoggedInUserId = sinon - .stub() - .returns((this.user_id = 'user-id-123')) + beforeEach(function(done) { this.req.params = { Project_id: this.project_id } - this.res.json = sinon.stub() + this.res.json = sinon.spy(() => { + done() + }) this.next = sinon.stub() this.members = [{ a: 1 }] - this.CollaboratorsHandler.getAllInvitedMembers = sinon - .stub() - .callsArgWith(1, null, this.members) - return this.CollaboratorsController.getAllMembers( - this.req, - this.res, - this.next + this.CollaboratorsGetter.promises.getAllInvitedMembers.resolves( + this.members ) + this.CollaboratorsController.getAllMembers(this.req, this.res, this.next) }) it('should not produce an error', function() { - return this.next.callCount.should.equal(0) + this.next.callCount.should.equal(0) }) it('should produce a json response', function() { this.res.json.callCount.should.equal(1) - return this.res.json - .calledWith({ members: this.members }) - .should.equal(true) + this.res.json.calledWith({ members: this.members }).should.equal(true) }) - it('should call CollaboratorsHandler.getAllMembers', function() { - return this.CollaboratorsHandler.getAllInvitedMembers.callCount.should.equal( - 1 - ) + it('should call CollaboratorsGetter.getAllInvitedMembers', function() { + expect(this.CollaboratorsGetter.promises.getAllInvitedMembers).to.have + .been.calledOnce }) - describe('when CollaboratorsHandler.getAllInvitedMembers produces an error', function() { - beforeEach(function() { + describe('when CollaboratorsGetter.getAllInvitedMembers produces an error', function() { + beforeEach(function(done) { this.res.json = sinon.stub() - this.next = sinon.stub() - this.CollaboratorsHandler.getAllInvitedMembers = sinon - .stub() - .callsArgWith(1, new Error('woops')) - return this.CollaboratorsController.getAllMembers( + this.next = sinon.spy(() => { + done() + }) + this.CollaboratorsGetter.promises.getAllInvitedMembers.rejects( + new Error('woops') + ) + this.CollaboratorsController.getAllMembers( this.req, this.res, this.next @@ -179,12 +176,14 @@ describe('CollaboratorsController', function() { }) it('should produce an error', function() { - this.next.callCount.should.equal(1) - return this.next.firstCall.args[0].should.be.instanceof(Error) + expect(this.next).to.have.been.calledOnce + expect(this.next).to.have.been.calledWithMatch( + sinon.match.instanceOf(Error) + ) }) it('should not produce a json response', function() { - return this.res.json.callCount.should.equal(0) + this.res.json.callCount.should.equal(0) }) }) }) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsGetterTests.js b/services/web/test/unit/src/Collaborators/CollaboratorsGetterTests.js new file mode 100644 index 0000000000..8c68272b51 --- /dev/null +++ b/services/web/test/unit/src/Collaborators/CollaboratorsGetterTests.js @@ -0,0 +1,358 @@ +const Path = require('path') +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +const { expect } = require('chai') +const { ObjectId } = require('mongojs') +const { Project } = require('../helpers/models/Project') +const Errors = require('../../../../app/src/Features/Errors/Errors') + +const MODULE_PATH = Path.join( + __dirname, + '../../../../app/src/Features/Collaborators/CollaboratorsGetter' +) + +describe('CollaboratorsGetter', function() { + beforeEach(function() { + this.userId = 'mock-user-id' + this.ownerRef = ObjectId() + this.readOnlyRef1 = ObjectId() + this.readOnlyRef2 = ObjectId() + this.readWriteRef1 = ObjectId() + this.readWriteRef2 = ObjectId() + this.readOnlyTokenRef = ObjectId() + this.readWriteTokenRef = ObjectId() + this.nonMemberRef = ObjectId() + this.project = { + _id: ObjectId(), + owner_ref: [this.ownerRef], + readOnly_refs: [this.readOnlyRef1, this.readOnlyRef2], + collaberator_refs: [this.readWriteRef1, this.readWriteRef2], + tokenAccessReadAndWrite_refs: [this.readWriteTokenRef], + tokenAccessReadOnly_refs: [this.readOnlyTokenRef], + publicAccesLevel: 'tokenBased' + } + + this.UserGetter = { + promises: { + getUser: sinon.stub().resolves(null) + } + } + this.ProjectMock = sinon.mock(Project) + this.ProjectGetter = { + promises: { + getProject: sinon.stub().resolves(this.project) + } + } + this.ProjectEditorHandler = { + buildOwnerAndMembersViews: sinon.stub() + } + this.CollaboratorsGetter = SandboxedModule.require(MODULE_PATH, { + globals: { + console: console + }, + requires: { + '../User/UserGetter': this.UserGetter, + '../../models/Project': { Project }, + '../Project/ProjectGetter': this.ProjectGetter, + '../Errors/Errors': Errors, + '../Project/ProjectEditorHandler': this.ProjectEditorHandler + } + }) + }) + + afterEach(function() { + this.ProjectMock.verify() + }) + + describe('getMemberIdsWithPrivilegeLevels', function() { + describe('with project', function() { + it('should return an array of member ids with their privilege levels', async function() { + const result = await this.CollaboratorsGetter.promises.getMemberIdsWithPrivilegeLevels( + this.project._id + ) + expect(result).to.have.deep.members([ + { + id: this.ownerRef.toString(), + privilegeLevel: 'owner', + source: 'owner' + }, + { + id: this.readWriteRef1.toString(), + privilegeLevel: 'readAndWrite', + source: 'invite' + }, + { + id: this.readWriteRef2.toString(), + privilegeLevel: 'readAndWrite', + source: 'invite' + }, + { + id: this.readOnlyRef1.toString(), + privilegeLevel: 'readOnly', + source: 'invite' + }, + { + id: this.readOnlyRef2.toString(), + privilegeLevel: 'readOnly', + source: 'invite' + }, + { + id: this.readOnlyTokenRef.toString(), + privilegeLevel: 'readOnly', + source: 'token' + }, + { + id: this.readWriteTokenRef.toString(), + privilegeLevel: 'readAndWrite', + source: 'token' + } + ]) + }) + }) + + describe('with a missing project', function() { + beforeEach(function() { + this.ProjectGetter.promises.getProject.resolves(null) + }) + + it('should return a NotFoundError', async function() { + await expect( + this.CollaboratorsGetter.promises.getMemberIdsWithPrivilegeLevels( + this.project._id + ) + ).to.be.rejectedWith(Errors.NotFoundError) + }) + }) + }) + + describe('getMemberIds', function() { + it('should return the ids', async function() { + const memberIds = await this.CollaboratorsGetter.promises.getMemberIds( + this.project._id + ) + expect(memberIds).to.have.members([ + this.ownerRef.toString(), + this.readOnlyRef1.toString(), + this.readOnlyRef2.toString(), + this.readWriteRef1.toString(), + this.readWriteRef2.toString(), + this.readWriteTokenRef.toString(), + this.readOnlyTokenRef.toString() + ]) + }) + }) + + describe('getInvitedMemberIds', function() { + it('should return the invited ids', async function() { + const memberIds = await this.CollaboratorsGetter.promises.getInvitedMemberIds( + this.project._id + ) + expect(memberIds).to.have.members([ + this.ownerRef.toString(), + this.readOnlyRef1.toString(), + this.readOnlyRef2.toString(), + this.readWriteRef1.toString(), + this.readWriteRef2.toString() + ]) + }) + }) + + describe('getInvitedMembersWithPrivilegeLevels', function() { + beforeEach(function() { + this.UserGetter.promises.getUser + .withArgs(this.readOnlyRef1.toString()) + .resolves({ _id: this.readOnlyRef1 }) + this.UserGetter.promises.getUser + .withArgs(this.readOnlyTokenRef.toString()) + .resolves({ _id: this.readOnlyTokenRef }) + this.UserGetter.promises.getUser + .withArgs(this.readWriteRef2.toString()) + .resolves({ _id: this.readWriteRef2 }) + this.UserGetter.promises.getUser + .withArgs(this.readWriteTokenRef.toString()) + .resolves({ _id: this.readWriteTokenRef }) + }) + + it('should return an array of invited members with their privilege levels', async function() { + const result = await this.CollaboratorsGetter.promises.getInvitedMembersWithPrivilegeLevels( + this.project._id + ) + expect(result).to.have.deep.members([ + { user: { _id: this.readOnlyRef1 }, privilegeLevel: 'readOnly' }, + { user: { _id: this.readWriteRef2 }, privilegeLevel: 'readAndWrite' } + ]) + }) + }) + + describe('getMemberIdPrivilegeLevel', function() { + it('should return the privilege level if it exists', async function() { + const level = await this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel( + this.readOnlyRef1, + this.project._id + ) + expect(level).to.equal('readOnly') + }) + + it('should return false if the member has no privilege level', async function() { + const level = await this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel( + this.nonMemberRef, + this.project._id + ) + expect(level).to.equal(false) + }) + }) + + describe('isUserInvitedMemberOfProject', function() { + describe('when user is a member of the project', function() { + it('should return true and the privilegeLevel', async function() { + const isMember = await this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject( + this.readOnlyRef1 + ) + expect(isMember).to.equal(true) + }) + }) + + describe('when user is not a member of the project', function() { + it('should return false', async function() { + const isMember = await this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject( + this.nonMemberRef + ) + expect(isMember).to.equal(false) + }) + }) + }) + + describe('getProjectsUserIsMemberOf', function() { + beforeEach(function() { + this.fields = 'mock fields' + this.ProjectMock.expects('find') + .withArgs({ collaberator_refs: this.userId }, this.fields) + .chain('exec') + .resolves(['mock-read-write-project-1', 'mock-read-write-project-2']) + + this.ProjectMock.expects('find') + .withArgs({ readOnly_refs: this.userId }, this.fields) + .chain('exec') + .resolves(['mock-read-only-project-1', 'mock-read-only-project-2']) + this.ProjectMock.expects('find') + .withArgs( + { + tokenAccessReadAndWrite_refs: this.userId, + publicAccesLevel: 'tokenBased' + }, + this.fields + ) + .chain('exec') + .resolves([ + 'mock-token-read-write-project-1', + 'mock-token-read-write-project-2' + ]) + this.ProjectMock.expects('find') + .withArgs( + { + tokenAccessReadOnly_refs: this.userId, + publicAccesLevel: 'tokenBased' + }, + this.fields + ) + .chain('exec') + .resolves([ + 'mock-token-read-only-project-1', + 'mock-token-read-only-project-2' + ]) + }) + + it('should call the callback with the projects', async function() { + const projects = await this.CollaboratorsGetter.promises.getProjectsUserIsMemberOf( + this.userId, + this.fields + ) + expect(projects).to.deep.equal({ + readAndWrite: [ + 'mock-read-write-project-1', + 'mock-read-write-project-2' + ], + readOnly: ['mock-read-only-project-1', 'mock-read-only-project-2'], + tokenReadAndWrite: [ + 'mock-token-read-write-project-1', + 'mock-token-read-write-project-2' + ], + tokenReadOnly: [ + 'mock-token-read-only-project-1', + 'mock-token-read-only-project-2' + ] + }) + }) + }) + + describe('getAllInvitedMembers', function() { + beforeEach(async function() { + this.owningUser = { + _id: this.ownerRef, + email: 'owner@example.com', + features: { a: 1 } + } + this.readWriteUser = { + _id: this.readWriteRef1, + email: 'readwrite@example.com' + } + this.members = [ + { user: this.owningUser, privilegeLevel: 'owner' }, + { user: this.readWriteUser, privilegeLevel: 'readAndWrite' } + ] + this.views = { + owner: this.owningUser, + ownerFeatures: this.owningUser.features, + members: [ + { _id: this.readWriteUser._id, email: this.readWriteUser.email } + ] + } + this.UserGetter.promises.getUser + .withArgs(this.owningUser._id.toString()) + .resolves(this.owningUser) + this.UserGetter.promises.getUser + .withArgs(this.readWriteUser._id.toString()) + .resolves(this.readWriteUser) + this.ProjectEditorHandler.buildOwnerAndMembersViews.returns(this.views) + this.result = await this.CollaboratorsGetter.promises.getAllInvitedMembers( + this.project._id + ) + }) + + it('should produce a list of members', function() { + expect(this.result).to.deep.equal(this.views.members) + }) + + it('should call ProjectEditorHandler.buildOwnerAndMembersViews', function() { + expect(this.ProjectEditorHandler.buildOwnerAndMembersViews).to.have.been + .calledOnce + expect( + this.ProjectEditorHandler.buildOwnerAndMembersViews + ).to.have.been.calledWith(this.members) + }) + }) + + describe('userIsTokenMember', function() { + it('should return true when the project is found', async function() { + this.ProjectMock.expects('findOne') + .chain('exec') + .resolves(this.project) + const isMember = await this.CollaboratorsGetter.promises.userIsTokenMember( + this.userId, + this.project._id + ) + expect(isMember).to.be.true + }) + + it('should return false when the project is not found', async function() { + this.ProjectMock.expects('findOne') + .chain('exec') + .resolves(null) + const isMember = await this.CollaboratorsGetter.promises.userIsTokenMember( + this.userId, + this.project._id + ) + expect(isMember).to.be.false + }) + }) +}) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js b/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js index 7f83e9e310..6246730a14 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js +++ b/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js @@ -1,712 +1,234 @@ -/* eslint-disable - camelcase, - handle-callback-err, - max-len, - no-return-assign, - no-unused-vars, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const should = require('chai').should() +const { promisify } = require('util') const SandboxedModule = require('sandboxed-module') -const assert = require('assert') const path = require('path') const sinon = require('sinon') -const modulePath = path.join( +const { expect } = require('chai') +const Errors = require('../../../../app/src/Features/Errors/Errors') +const { Project } = require('../helpers/models/Project') +const { ObjectId } = require('mongojs') + +const MODULE_PATH = path.join( __dirname, '../../../../app/src/Features/Collaborators/CollaboratorsHandler' ) -const { expect } = require('chai') -const Errors = require('../../../../app/src/Features/Errors/Errors.js') -const { ObjectId } = require('mongojs') + +const sleep = promisify(setTimeout) describe('CollaboratorsHandler', function() { beforeEach(function() { - this.CollaboratorHandler = SandboxedModule.require(modulePath, { + this.logger = { + log: sinon.stub(), + warn: sinon.stub(), + err: sinon.stub() + } + this.userId = 'mock-user-id' + this.addingUserId = 'adding-user-id' + this.project = { + _id: ObjectId() + } + + this.UserGetter = { + promises: { + getUser: sinon.stub().resolves(null) + } + } + this.ContactManager = { + addContact: sinon.stub() + } + this.ProjectMock = sinon.mock(Project) + this.ProjectEntityHandler = { + promises: { + flushProjectToThirdPartyDataStore: sinon.stub().resolves() + } + } + this.ProjectGetter = { + promises: { + getProject: sinon.stub().resolves(this.project) + } + } + this.CollaboratorsGetter = { + promises: { + getProjectsUserIsMemberOf: sinon.stub() + } + } + this.CollaboratorsHandler = SandboxedModule.require(MODULE_PATH, { globals: { console: console }, requires: { - 'logger-sharelatex': (this.logger = { - log: sinon.stub(), - warn: sinon.stub(), - err: sinon.stub() - }), - '../User/UserCreator': (this.UserCreator = {}), - '../User/UserGetter': (this.UserGetter = {}), - '../Contacts/ContactManager': (this.ContactManager = {}), - '../../models/Project': { - Project: (this.Project = {}) - }, - '../Project/ProjectEntityHandler': (this.ProjectEntityHandler = {}), - '../Project/ProjectGetter': (this.ProjectGetter = {}), - './CollaboratorsEmailHandler': (this.CollaboratorsEmailHandler = {}), + 'logger-sharelatex': this.logger, + '../User/UserGetter': this.UserGetter, + '../Contacts/ContactManager': this.ContactManager, + '../../models/Project': { Project }, + '../Project/ProjectEntityHandler': this.ProjectEntityHandler, + '../Project/ProjectGetter': this.ProjectGetter, '../Errors/Errors': Errors, - '../Project/ProjectEditorHandler': (this.ProjectEditorHandler = {}) + './CollaboratorsGetter': this.CollaboratorsGetter } }) - - this.project_id = 'mock-project-id' - this.user_id = 'mock-user-id' - this.adding_user_id = 'adding-user-id' - this.email = 'joe@sharelatex.com' - return (this.callback = sinon.stub()) }) - describe('getMemberIdsWithPrivilegeLevels', function() { - describe('with project', function() { - beforeEach(function() { - this.ProjectGetter.getProject = sinon.stub() - this.ProjectGetter.getProject - .withArgs(this.project_id, { - owner_ref: 1, - collaberator_refs: 1, - readOnly_refs: 1, - tokenAccessReadOnly_refs: 1, - tokenAccessReadAndWrite_refs: 1, - publicAccesLevel: 1 - }) - .yields( - null, - (this.project = { - owner_ref: ['owner-ref'], - readOnly_refs: ['read-only-ref-1', 'read-only-ref-2'], - collaberator_refs: ['read-write-ref-1', 'read-write-ref-2'] - }) - ) - return this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels( - this.project_id, - this.callback - ) - }) - - it('should return an array of member ids with their privilege levels', function() { - return this.callback - .calledWith(null, [ - { id: 'owner-ref', privilegeLevel: 'owner', source: 'owner' }, - { - id: 'read-write-ref-1', - privilegeLevel: 'readAndWrite', - source: 'invite' - }, - { - id: 'read-write-ref-2', - privilegeLevel: 'readAndWrite', - source: 'invite' - }, - { - id: 'read-only-ref-1', - privilegeLevel: 'readOnly', - source: 'invite' - }, - { - id: 'read-only-ref-2', - privilegeLevel: 'readOnly', - source: 'invite' - } - ]) - .should.equal(true) - }) - }) - - describe('with a missing project', function() { - beforeEach(function() { - return (this.ProjectGetter.getProject = sinon.stub().yields(null, null)) - }) - - it('should return a NotFoundError', function(done) { - return this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels( - this.project_id, - error => { - error.should.be.instanceof(Errors.NotFoundError) - return done() - } - ) - }) - }) - }) - - describe('getMemberIds', function() { - beforeEach(function() { - this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() - this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels - .withArgs(this.project_id) - .yields(null, [ - { id: 'member-id-1', source: 'invite' }, - { id: 'member-id-2', source: 'token' } - ]) - return this.CollaboratorHandler.getMemberIds( - this.project_id, - this.callback - ) - }) - - it('should return the ids', function() { - return this.callback - .calledWith(null, ['member-id-1', 'member-id-2']) - .should.equal(true) - }) - }) - - describe('getInvitedMemberIds', function() { - beforeEach(function() { - this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() - this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels - .withArgs(this.project_id) - .yields(null, [ - { id: 'member-id-1', source: 'invite' }, - { id: 'member-id-2', source: 'token' } - ]) - return this.CollaboratorHandler.getInvitedMemberIds( - this.project_id, - this.callback - ) - }) - - it('should return the invited ids', function() { - return this.callback.calledWith(null, ['member-id-1']).should.equal(true) - }) - }) - - describe('getMembersWithPrivilegeLevels', function() { - beforeEach(function() { - this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() - this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels - .withArgs(this.project_id) - .yields(null, [ - { - id: 'read-only-ref-1', - privilegeLevel: 'readOnly', - source: 'token' - }, - { - id: 'read-only-ref-2', - privilegeLevel: 'readOnly', - source: 'invite' - }, - { - id: 'read-write-ref-1', - privilegeLevel: 'readAndWrite', - source: 'token' - }, - { - id: 'read-write-ref-2', - privilegeLevel: 'readAndWrite', - source: 'invite' - }, - { - id: 'doesnt-exist', - privilegeLevel: 'readAndWrite', - source: 'invite' - } - ]) - this.UserGetter.getUser = sinon.stub() - this.UserGetter.getUser - .withArgs('read-only-ref-1') - .yields(null, { _id: 'read-only-ref-1' }) - this.UserGetter.getUser - .withArgs('read-only-ref-2') - .yields(null, { _id: 'read-only-ref-2' }) - this.UserGetter.getUser - .withArgs('read-write-ref-1') - .yields(null, { _id: 'read-write-ref-1' }) - this.UserGetter.getUser - .withArgs('read-write-ref-2') - .yields(null, { _id: 'read-write-ref-2' }) - this.UserGetter.getUser.withArgs('doesnt-exist').yields(null, null) - return this.CollaboratorHandler.getMembersWithPrivilegeLevels( - this.project_id, - this.callback - ) - }) - - it('should return an array of members with their privilege levels', function() { - return this.callback - .calledWith(null, [ - { user: { _id: 'read-only-ref-1' }, privilegeLevel: 'readOnly' }, - { user: { _id: 'read-only-ref-2' }, privilegeLevel: 'readOnly' }, - { user: { _id: 'read-write-ref-1' }, privilegeLevel: 'readAndWrite' }, - { user: { _id: 'read-write-ref-2' }, privilegeLevel: 'readAndWrite' } - ]) - .should.equal(true) - }) - }) - - describe('getInvitedMembersWithPrivilegeLevels', function() { - beforeEach(function() { - this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() - this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels - .withArgs(this.project_id) - .yields(null, [ - { - id: 'read-only-ref-1', - privilegeLevel: 'readOnly', - source: 'token' - }, - { - id: 'read-only-ref-2', - privilegeLevel: 'readOnly', - source: 'invite' - }, - { - id: 'read-write-ref-1', - privilegeLevel: 'readAndWrite', - source: 'token' - }, - { - id: 'read-write-ref-2', - privilegeLevel: 'readAndWrite', - source: 'invite' - }, - { - id: 'doesnt-exist', - privilegeLevel: 'readAndWrite', - source: 'invite' - } - ]) - this.UserGetter.getUser = sinon.stub() - this.UserGetter.getUser - .withArgs('read-only-ref-1') - .yields(null, { _id: 'read-only-ref-1' }) - this.UserGetter.getUser - .withArgs('read-only-ref-2') - .yields(null, { _id: 'read-only-ref-2' }) - this.UserGetter.getUser - .withArgs('read-write-ref-1') - .yields(null, { _id: 'read-write-ref-1' }) - this.UserGetter.getUser - .withArgs('read-write-ref-2') - .yields(null, { _id: 'read-write-ref-2' }) - this.UserGetter.getUser.withArgs('doesnt-exist').yields(null, null) - return this.CollaboratorHandler.getInvitedMembersWithPrivilegeLevels( - this.project_id, - this.callback - ) - }) - - it('should return an array of invited members with their privilege levels', function() { - return this.callback - .calledWith(null, [ - { user: { _id: 'read-only-ref-2' }, privilegeLevel: 'readOnly' }, - { user: { _id: 'read-write-ref-2' }, privilegeLevel: 'readAndWrite' } - ]) - .should.equal(true) - }) - }) - - describe('getTokenMembersWithPrivilegeLevels', function() { - beforeEach(function() { - this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() - this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels - .withArgs(this.project_id) - .yields(null, [ - { - id: 'read-only-ref-1', - privilegeLevel: 'readOnly', - source: 'token' - }, - { - id: 'read-only-ref-2', - privilegeLevel: 'readOnly', - source: 'invite' - }, - { - id: 'read-write-ref-1', - privilegeLevel: 'readAndWrite', - source: 'token' - }, - { - id: 'read-write-ref-2', - privilegeLevel: 'readAndWrite', - source: 'invite' - }, - { - id: 'doesnt-exist', - privilegeLevel: 'readAndWrite', - source: 'invite' - } - ]) - this.UserGetter.getUser = sinon.stub() - this.UserGetter.getUser - .withArgs('read-only-ref-1') - .yields(null, { _id: 'read-only-ref-1' }) - this.UserGetter.getUser - .withArgs('read-only-ref-2') - .yields(null, { _id: 'read-only-ref-2' }) - this.UserGetter.getUser - .withArgs('read-write-ref-1') - .yields(null, { _id: 'read-write-ref-1' }) - this.UserGetter.getUser - .withArgs('read-write-ref-2') - .yields(null, { _id: 'read-write-ref-2' }) - this.UserGetter.getUser.withArgs('doesnt-exist').yields(null, null) - return this.CollaboratorHandler.getTokenMembersWithPrivilegeLevels( - this.project_id, - this.callback - ) - }) - - it('should return an array of token members with their privilege levels', function() { - return this.callback - .calledWith(null, [ - { user: { _id: 'read-only-ref-1' }, privilegeLevel: 'readOnly' }, - { user: { _id: 'read-write-ref-1' }, privilegeLevel: 'readAndWrite' } - ]) - .should.equal(true) - }) - }) - - describe('getMemberIdPrivilegeLevel', function() { - beforeEach(function() { - this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() - return this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels - .withArgs(this.project_id) - .yields(null, [ - { id: 'member-id-1', privilegeLevel: 'readAndWrite' }, - { id: 'member-id-2', privilegeLevel: 'readOnly' } - ]) - }) - - it('should return the privilege level if it exists', function(done) { - return this.CollaboratorHandler.getMemberIdPrivilegeLevel( - 'member-id-2', - this.project_id, - (error, level) => { - expect(level).to.equal('readOnly') - return done() - } - ) - }) - - it('should return false if the member has no privilege level', function(done) { - return this.CollaboratorHandler.getMemberIdPrivilegeLevel( - 'member-id-3', - this.project_id, - (error, level) => { - expect(level).to.equal(false) - return done() - } - ) - }) - }) - - describe('isUserInvitedMemberOfProject', function() { - beforeEach(function() { - return (this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub()) - }) - - describe('when user is a member of the project', function() { - beforeEach(function() { - this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels - .withArgs(this.project_id) - .yields(null, [ - { - id: 'not-the-user', - privilegeLevel: 'readOnly', - source: 'invite' - }, - { - id: this.user_id, - privilegeLevel: 'readAndWrite', - source: 'invite' - } - ]) - return this.CollaboratorHandler.isUserInvitedMemberOfProject( - this.user_id, - this.project_id, - this.callback - ) - }) - - it('should return true and the privilegeLevel', function() { - return this.callback - .calledWith(null, true, 'readAndWrite') - .should.equal(true) - }) - }) - - describe('when user is not a member of the project', function() { - beforeEach(function() { - this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels - .withArgs(this.project_id) - .yields(null, [{ id: 'not-the-user', privilegeLevel: 'readOnly' }]) - return this.CollaboratorHandler.isUserInvitedMemberOfProject( - this.user_id, - this.project_id, - this.callback - ) - }) - - it('should return false', function() { - return this.callback.calledWith(null, false, null).should.equal(true) - }) - }) - }) - - describe('getProjectsUserIsMemberOf', function() { - beforeEach(function() { - this.fields = 'mock fields' - this.Project.find = sinon.stub() - this.Project.find - .withArgs({ collaberator_refs: this.user_id }, this.fields) - .yields(null, [ - 'mock-read-write-project-1', - 'mock-read-write-project-2' - ]) - this.Project.find - .withArgs({ readOnly_refs: this.user_id }, this.fields) - .yields(null, ['mock-read-only-project-1', 'mock-read-only-project-2']) - this.Project.find - .withArgs( - { - tokenAccessReadAndWrite_refs: this.user_id, - publicAccesLevel: 'tokenBased' - }, - this.fields - ) - .yields(null, [ - 'mock-token-read-write-project-1', - 'mock-token-read-write-project-2' - ]) - this.Project.find - .withArgs( - { - tokenAccessReadOnly_refs: this.user_id, - publicAccesLevel: 'tokenBased' - }, - this.fields - ) - .yields(null, [ - 'mock-token-read-only-project-1', - 'mock-token-read-only-project-2' - ]) - return this.CollaboratorHandler.getProjectsUserIsMemberOf( - this.user_id, - this.fields, - this.callback - ) - }) - - it('should call the callback with the projects', function() { - return this.callback - .calledWith(null, { - readAndWrite: [ - 'mock-read-write-project-1', - 'mock-read-write-project-2' - ], - readOnly: ['mock-read-only-project-1', 'mock-read-only-project-2'], - tokenReadAndWrite: [ - 'mock-token-read-write-project-1', - 'mock-token-read-write-project-2' - ], - tokenReadOnly: [ - 'mock-token-read-only-project-1', - 'mock-token-read-only-project-2' - ] - }) - .should.equal(true) - }) + afterEach(function() { + this.ProjectMock.verify() }) describe('removeUserFromProject', function() { - beforeEach(function() { - this.Project.update = sinon.stub().callsArg(2) - return this.CollaboratorHandler.removeUserFromProject( - this.project_id, - this.user_id, - this.callback - ) - }) + beforeEach(function() {}) - it('should remove the user from mongo', function() { - return this.Project.update - .calledWith( + it('should remove the user from mongo', async function() { + this.ProjectMock.expects('update') + .withArgs( { - _id: this.project_id + _id: this.project._id }, { $pull: { - collaberator_refs: this.user_id, - readOnly_refs: this.user_id, - tokenAccessReadOnly_refs: this.user_id, - tokenAccessReadAndWrite_refs: this.user_id + collaberator_refs: this.userId, + readOnly_refs: this.userId, + tokenAccessReadOnly_refs: this.userId, + tokenAccessReadAndWrite_refs: this.userId } } ) - .should.equal(true) + .chain('exec') + .resolves() + await this.CollaboratorsHandler.promises.removeUserFromProject( + this.project._id, + this.userId + ) }) }) - describe('addUserToProject', function() { - beforeEach(function() { - this.Project.update = sinon.stub().callsArg(2) - this.ProjectGetter.getProject = sinon - .stub() - .callsArgWith(2, null, (this.project = {})) - this.ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon - .stub() - .callsArg(1) - this.CollaboratorHandler.addEmailToProject = sinon - .stub() - .callsArgWith(4, null, this.user_id) - return (this.ContactManager.addContact = sinon.stub()) - }) - + describe('addUserIdToProject', function() { describe('as readOnly', function() { - beforeEach(function() { - return this.CollaboratorHandler.addUserIdToProject( - this.project_id, - this.adding_user_id, - this.user_id, - 'readOnly', - this.callback + beforeEach(async function() { + this.ProjectMock.expects('update') + .withArgs( + { + _id: this.project._id + }, + { + $addToSet: { readOnly_refs: this.userId } + } + ) + .chain('exec') + .resolves() + await this.CollaboratorsHandler.promises.addUserIdToProject( + this.project._id, + this.addingUserId, + this.userId, + 'readOnly' ) }) - it('should add the user to the readOnly_refs', function() { - return this.Project.update - .calledWith( - { - _id: this.project_id - }, - { - $addToSet: { readOnly_refs: this.user_id } - } - ) - .should.equal(true) - }) - it('should flush the project to the TPDS', function() { - return this.ProjectEntityHandler.flushProjectToThirdPartyDataStore - .calledWith(this.project_id) - .should.equal(true) + expect( + this.ProjectEntityHandler.promises.flushProjectToThirdPartyDataStore + ).to.have.been.calledWith(this.project._id) }) it('should add the user as a contact for the adding user', function() { - return this.ContactManager.addContact - .calledWith(this.adding_user_id, this.user_id) - .should.equal(true) + expect(this.ContactManager.addContact).to.have.been.calledWith( + this.addingUserId, + this.userId + ) }) }) describe('as readAndWrite', function() { - beforeEach(function() { - return this.CollaboratorHandler.addUserIdToProject( - this.project_id, - this.adding_user_id, - this.user_id, - 'readAndWrite', - this.callback + beforeEach(async function() { + this.ProjectMock.expects('update') + .withArgs( + { + _id: this.project._id + }, + { + $addToSet: { collaberator_refs: this.userId } + } + ) + .chain('exec') + .resolves() + await this.CollaboratorsHandler.promises.addUserIdToProject( + this.project._id, + this.addingUserId, + this.userId, + 'readAndWrite' ) }) - it('should add the user to the collaberator_refs', function() { - return this.Project.update - .calledWith( - { - _id: this.project_id - }, - { - $addToSet: { collaberator_refs: this.user_id } - } - ) - .should.equal(true) - }) - it('should flush the project to the TPDS', function() { - return this.ProjectEntityHandler.flushProjectToThirdPartyDataStore - .calledWith(this.project_id) - .should.equal(true) + expect( + this.ProjectEntityHandler.promises.flushProjectToThirdPartyDataStore + ).to.have.been.calledWith(this.project._id) }) }) describe('with invalid privilegeLevel', function() { - beforeEach(function() { - return this.CollaboratorHandler.addUserIdToProject( - this.project_id, - this.adding_user_id, - this.user_id, - 'notValid', - this.callback - ) - }) - - it('should call the callback with an error', function() { - return this.callback.calledWith(new Error()).should.equal(true) + it('should call the callback with an error', async function() { + await expect( + this.CollaboratorsHandler.promises.addUserIdToProject( + this.project._id, + this.addingUserId, + this.userId, + 'notValid' + ) + ).to.be.rejected }) }) describe('when user already exists as a collaborator', function() { beforeEach(function() { - this.project.collaberator_refs = [this.user_id] - return this.CollaboratorHandler.addUserIdToProject( - this.project_id, - this.adding_user_id, - this.user_id, - 'readAndWrite', - this.callback - ) + this.project.collaberator_refs = [this.userId] }) - it('should not add the user again', function() { - return this.Project.update.called.should.equal(false) + it('should not add the user again', async function() { + await this.CollaboratorsHandler.promises.addUserIdToProject( + this.project._id, + this.addingUserId, + this.userId, + 'readAndWrite' + ) + // Project.update() should not be called. If it is, it will fail because + // the mock is not set up. }) }) - describe('with null adding_user_id', function() { + describe('with null addingUserId', function() { beforeEach(function() { - return this.CollaboratorHandler.addUserIdToProject( - this.project_id, + this.CollaboratorsHandler.promises.addUserIdToProject( + this.project._id, null, - this.user_id, + this.userId, 'readAndWrite', this.callback ) }) it('should not add the adding user as a contact', function() { - return this.ContactManager.addContact.called.should.equal(false) + expect(this.ContactManager.addContact).not.to.have.been.called }) }) }) describe('removeUserFromAllProjects', function() { - beforeEach(function(done) { - this.CollaboratorHandler.getProjectsUserIsMemberOf = sinon.stub() - this.CollaboratorHandler.getProjectsUserIsMemberOf - .withArgs(this.user_id, { _id: 1 }) - .yields(null, { + it('should remove the user from each project', async function() { + this.CollaboratorsGetter.promises.getProjectsUserIsMemberOf + .withArgs(this.userId, { _id: 1 }) + .resolves({ readAndWrite: [ { _id: 'read-and-write-0' }, - { _id: 'read-and-write-1' }, - null + { _id: 'read-and-write-1' } ], - readOnly: [{ _id: 'read-only-0' }, { _id: 'read-only-1' }, null], + readOnly: [{ _id: 'read-only-0' }, { _id: 'read-only-1' }], tokenReadAndWrite: [ { _id: 'token-read-and-write-0' }, - { _id: 'token-read-and-write-1' }, - null + { _id: 'token-read-and-write-1' } ], tokenReadOnly: [ { _id: 'token-read-only-0' }, - { _id: 'token-read-only-1' }, - null + { _id: 'token-read-only-1' } ] }) - this.CollaboratorHandler.removeUserFromProject = sinon.stub().yields() - return this.CollaboratorHandler.removeUserFromAllProjets( - this.user_id, - done - ) - }) - - it('should remove the user from each project', function() { const expectedProjects = [ 'read-and-write-0', 'read-and-write-1', @@ -717,323 +239,110 @@ describe('CollaboratorsHandler', function() { 'token-read-only-0', 'token-read-only-1' ] - return Array.from(expectedProjects).map(project_id => - this.CollaboratorHandler.removeUserFromProject - .calledWith(project_id, this.user_id) - .should.equal(true) - ) - }) - }) - - describe('getAllInvitedMembers', function() { - beforeEach(function() { - this.owning_user = { - _id: 'owner-id', - email: 'owner@example.com', - features: { a: 1 } - } - this.readwrite_user = { - _id: 'readwrite-id', - email: 'readwrite@example.com' - } - this.members = [ - { user: this.owning_user, privilegeLevel: 'owner' }, - { user: this.readwrite_user, privilegeLevel: 'readAndWrite' } - ] - this.CollaboratorHandler.getInvitedMembersWithPrivilegeLevels = sinon - .stub() - .callsArgWith(1, null, this.members) - this.ProjectEditorHandler.buildOwnerAndMembersViews = sinon - .stub() - .returns( - (this.views = { - owner: this.owning_user, - ownerFeatures: this.owning_user.features, - members: [ - { _id: this.readwrite_user._id, email: this.readwrite_user.email } - ] - }) - ) - this.callback = sinon.stub() - return this.CollaboratorHandler.getAllInvitedMembers( - this.project_id, - this.callback - ) - }) - - it('should not produce an error', function() { - this.callback.callCount.should.equal(1) - return expect(this.callback.firstCall.args[0]).to.equal(null) - }) - - it('should produce a list of members', function() { - this.callback.callCount.should.equal(1) - return expect(this.callback.firstCall.args[1]).to.deep.equal( - this.views.members - ) - }) - - it('should call getMembersWithPrivileges', function() { - this.CollaboratorHandler.getInvitedMembersWithPrivilegeLevels.callCount.should.equal( - 1 - ) - return this.CollaboratorHandler.getInvitedMembersWithPrivilegeLevels.firstCall.args[0].should.equal( - this.project_id - ) - }) - - it('should call ProjectEditorHandler.buildOwnerAndMembersViews', function() { - this.ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal( - 1 - ) - return this.ProjectEditorHandler.buildOwnerAndMembersViews.firstCall.args[0].should.equal( - this.members - ) - }) - - describe('when getMembersWithPrivileges produces an error', function() { - beforeEach(function() { - this.CollaboratorHandler.getInvitedMembersWithPrivilegeLevels = sinon - .stub() - .callsArgWith(1, new Error('woops')) - this.ProjectEditorHandler.buildOwnerAndMembersViews = sinon - .stub() - .returns( - (this.views = { - owner: this.owning_user, - ownerFeatures: this.owning_user.features, - members: [ - { - _id: this.readwrite_user._id, - email: this.readwrite_user.email - } - ] - }) + for (const projectId of expectedProjects) { + this.ProjectMock.expects('update') + .withArgs( + { + _id: projectId + }, + { + $pull: { + collaberator_refs: this.userId, + readOnly_refs: this.userId, + tokenAccessReadOnly_refs: this.userId, + tokenAccessReadAndWrite_refs: this.userId + } + } ) - this.callback = sinon.stub() - return this.CollaboratorHandler.getAllInvitedMembers( - this.project_id, - this.callback - ) - }) - - it('should produce an error', function() { - this.callback.callCount.should.equal(1) - expect(this.callback.firstCall.args[0]).to.not.equal(null) - return expect(this.callback.firstCall.args[0]).to.be.instanceof(Error) - }) - - it('should call getMembersWithPrivileges', function() { - this.CollaboratorHandler.getInvitedMembersWithPrivilegeLevels.callCount.should.equal( - 1 - ) - return this.CollaboratorHandler.getInvitedMembersWithPrivilegeLevels.firstCall.args[0].should.equal( - this.project_id - ) - }) - - it('should not call ProjectEditorHandler.buildOwnerAndMembersViews', function() { - return this.ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal( - 0 - ) - }) - }) - }) - - describe('userIsTokenMember', function() { - beforeEach(function() { - this.user_id = ObjectId() - this.project_id = ObjectId() - this.project = { _id: this.project_id } - return (this.Project.findOne = sinon - .stub() - .callsArgWith(2, null, this.project)) - }) - - it('should check the database', function(done) { - return this.CollaboratorHandler.userIsTokenMember( - this.user_id, - this.project_id, - (err, isTokenMember) => { - this.Project.findOne.callCount.should.equal(1) - return done() - } - ) - }) - - it('should return true when the project is found', function(done) { - return this.CollaboratorHandler.userIsTokenMember( - this.user_id, - this.project_id, - (err, isTokenMember) => { - expect(err).to.not.exist - expect(isTokenMember).to.equal(true) - return done() - } - ) - }) - - it('should return false when the project is not found', function(done) { - this.project = null - this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project) - return this.CollaboratorHandler.userIsTokenMember( - this.user_id, - this.project_id, - (err, isTokenMember) => { - expect(err).to.not.exist - expect(isTokenMember).to.equal(false) - return done() - } + .chain('exec') + .resolves() + } + await this.CollaboratorsHandler.promises.removeUserFromAllProjects( + this.userId ) }) }) describe('transferProjects', function() { beforeEach(function() { - this.from_user_id = 'from-user-id' - this.to_user_id = 'to-user-id' + this.fromUserId = ObjectId() + this.toUserId = ObjectId() this.projects = [ { - _id: 'project-id-1' + _id: ObjectId() }, { - _id: 'project-id-2' + _id: ObjectId() } ] - this.Project.find = sinon.stub().yields(null, this.projects) - this.Project.update = sinon.stub().yields() - return (this.ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon - .stub() - .yields()) + this.ProjectMock.expects('find') + .withArgs({ + $or: [ + { owner_ref: this.fromUserId }, + { collaberator_refs: this.fromUserId }, + { readOnly_refs: this.fromUserId } + ] + }) + .chain('exec') + .resolves(this.projects) + this.ProjectMock.expects('update') + .withArgs( + { owner_ref: this.fromUserId }, + { $set: { owner_ref: this.toUserId } }, + { multi: true } + ) + .chain('exec') + .resolves() + this.ProjectMock.expects('update') + .withArgs( + { collaberator_refs: this.fromUserId }, + { + $addToSet: { collaberator_refs: this.toUserId }, + $pull: { collaberator_refs: this.fromUserId } + }, + { multi: true } + ) + .chain('exec') + .resolves() + this.ProjectMock.expects('update') + .withArgs( + { readOnly_refs: this.fromUserId }, + { + $addToSet: { readOnly_refs: this.toUserId }, + $pull: { readOnly_refs: this.fromUserId } + }, + { multi: true } + ) + .chain('exec') + .resolves() }) describe('successfully', function() { - beforeEach(function() { - return this.CollaboratorHandler.transferProjects( - this.from_user_id, - this.to_user_id, - this.callback + it('should flush each project to the TPDS', async function() { + await this.CollaboratorsHandler.promises.transferProjects( + this.fromUserId, + this.toUserId ) - }) - - it('should look up the affected projects', function() { - return this.Project.find - .calledWith({ - $or: [ - { owner_ref: this.from_user_id }, - { collaberator_refs: this.from_user_id }, - { readOnly_refs: this.from_user_id } - ] - }) - .should.equal(true) - }) - - it('should transfer owned projects', function() { - return this.Project.update - .calledWith( - { - owner_ref: this.from_user_id - }, - { - $set: { owner_ref: this.to_user_id } - }, - { - multi: true - } - ) - .should.equal(true) - }) - - it('should transfer collaborator projects', function() { - this.Project.update - .calledWith( - { - collaberator_refs: this.from_user_id - }, - { - $addToSet: { collaberator_refs: this.to_user_id } - }, - { - multi: true - } - ) - .should.equal(true) - return this.Project.update - .calledWith( - { - collaberator_refs: this.from_user_id - }, - { - $pull: { collaberator_refs: this.from_user_id } - }, - { - multi: true - } - ) - .should.equal(true) - }) - - it('should transfer read only collaborator projects', function() { - this.Project.update - .calledWith( - { - readOnly_refs: this.from_user_id - }, - { - $addToSet: { readOnly_refs: this.to_user_id } - }, - { - multi: true - } - ) - .should.equal(true) - return this.Project.update - .calledWith( - { - readOnly_refs: this.from_user_id - }, - { - $pull: { readOnly_refs: this.from_user_id } - }, - { - multi: true - } - ) - .should.equal(true) - }) - - it('should flush each project to the TPDS', function() { - return Array.from(this.projects).map(project => - this.ProjectEntityHandler.flushProjectToThirdPartyDataStore - .calledWith(project._id) - .should.equal(true) - ) - }) - - it('should call the callback', function() { - return this.callback.called.should.equal(true) + await sleep(100) // let the background tasks run + for (const project of this.projects) { + expect( + this.ProjectEntityHandler.promises.flushProjectToThirdPartyDataStore + ).to.have.been.calledWith(project._id) + } }) }) describe('when flushing to TPDS fails', function() { - beforeEach(function() { - this.ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon - .stub() - .yields(new Error('oops')) - return this.CollaboratorHandler.transferProjects( - this.from_user_id, - this.to_user_id, - this.callback + it('should log an error but not fail', async function() { + this.ProjectEntityHandler.promises.flushProjectToThirdPartyDataStore.rejects( + new Error('oops') ) - }) - - it('should log an error', function() { - return this.logger.err.called.should.equal(true) - }) - - it('should not return an error since it happens in the background', function() { - this.callback.called.should.equal(true) - return this.callback.calledWith(new Error('oops')).should.equal(false) + await this.CollaboratorsHandler.promises.transferProjects( + this.fromUserId, + this.toUserId + ) + await sleep(100) // let the background tasks run + expect(this.logger.err).to.have.been.called }) }) }) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.js b/services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.js index bf892f6c33..db6530ae1f 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.js +++ b/services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.js @@ -51,7 +51,7 @@ describe('CollaboratorsInviteController', function() { '../Project/ProjectGetter': (this.ProjectGetter = {}), '../Subscription/LimitationsManager': this.LimitationsManager, '../User/UserGetter': this.UserGetter, - './CollaboratorsHandler': (this.CollaboratorsHandler = {}), + './CollaboratorsGetter': (this.CollaboratorsGetter = {}), './CollaboratorsInviteHandler': (this.CollaboratorsInviteHandler = {}), 'logger-sharelatex': (this.logger = { err: sinon.stub(), @@ -571,9 +571,9 @@ describe('CollaboratorsInviteController', function() { email: 'john@example.com' } - this.CollaboratorsHandler.isUserInvitedMemberOfProject = sinon + this.CollaboratorsGetter.isUserInvitedMemberOfProject = sinon .stub() - .callsArgWith(2, null, false, null) + .callsArgWith(2, null, false) this.CollaboratorsInviteHandler.getInviteByToken = sinon .stub() .callsArgWith(2, null, this.invite) @@ -606,11 +606,11 @@ describe('CollaboratorsInviteController', function() { return this.next.callCount.should.equal(0) }) - it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { - this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function() { + this.CollaboratorsGetter.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - return this.CollaboratorsHandler.isUserInvitedMemberOfProject + return this.CollaboratorsGetter.isUserInvitedMemberOfProject .calledWith(this.current_user_id, this.project_id) .should.equal(true) }) @@ -641,9 +641,9 @@ describe('CollaboratorsInviteController', function() { describe('when user is already a member of the project', function() { beforeEach(function() { - this.CollaboratorsHandler.isUserInvitedMemberOfProject = sinon + this.CollaboratorsGetter.isUserInvitedMemberOfProject = sinon .stub() - .callsArgWith(2, null, true, null) + .callsArgWith(2, null, true) return this.CollaboratorsInviteController.viewInvite( this.req, this.res, @@ -662,11 +662,11 @@ describe('CollaboratorsInviteController', function() { return this.next.callCount.should.equal(0) }) - it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { - this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function() { + this.CollaboratorsGetter.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - return this.CollaboratorsHandler.isUserInvitedMemberOfProject + return this.CollaboratorsGetter.isUserInvitedMemberOfProject .calledWith(this.current_user_id, this.project_id) .should.equal(true) }) @@ -688,7 +688,7 @@ describe('CollaboratorsInviteController', function() { describe('when isUserInvitedMemberOfProject produces an error', function() { beforeEach(function() { - this.CollaboratorsHandler.isUserInvitedMemberOfProject = sinon + this.CollaboratorsGetter.isUserInvitedMemberOfProject = sinon .stub() .callsArgWith(2, new Error('woops')) return this.CollaboratorsInviteController.viewInvite( @@ -703,11 +703,11 @@ describe('CollaboratorsInviteController', function() { return expect(this.next.firstCall.args[0]).to.be.instanceof(Error) }) - it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { - this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function() { + this.CollaboratorsGetter.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - return this.CollaboratorsHandler.isUserInvitedMemberOfProject + return this.CollaboratorsGetter.isUserInvitedMemberOfProject .calledWith(this.current_user_id, this.project_id) .should.equal(true) }) @@ -746,11 +746,11 @@ describe('CollaboratorsInviteController', function() { return this.next.calledWith(this.err).should.equal(true) }) - it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { - this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function() { + this.CollaboratorsGetter.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - return this.CollaboratorsHandler.isUserInvitedMemberOfProject + return this.CollaboratorsGetter.isUserInvitedMemberOfProject .calledWith(this.current_user_id, this.project_id) .should.equal(true) }) @@ -759,7 +759,7 @@ describe('CollaboratorsInviteController', function() { this.CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal( 1 ) - return this.CollaboratorsHandler.isUserInvitedMemberOfProject + return this.CollaboratorsGetter.isUserInvitedMemberOfProject .calledWith(this.current_user_id, this.project_id) .should.equal(true) }) @@ -798,11 +798,11 @@ describe('CollaboratorsInviteController', function() { return this.next.callCount.should.equal(0) }) - it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { - this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function() { + this.CollaboratorsGetter.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - return this.CollaboratorsHandler.isUserInvitedMemberOfProject + return this.CollaboratorsGetter.isUserInvitedMemberOfProject .calledWith(this.current_user_id, this.project_id) .should.equal(true) }) @@ -811,7 +811,7 @@ describe('CollaboratorsInviteController', function() { this.CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal( 1 ) - return this.CollaboratorsHandler.isUserInvitedMemberOfProject + return this.CollaboratorsGetter.isUserInvitedMemberOfProject .calledWith(this.current_user_id, this.project_id) .should.equal(true) }) @@ -840,11 +840,11 @@ describe('CollaboratorsInviteController', function() { return expect(this.next.firstCall.args[0]).to.be.instanceof(Error) }) - it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { - this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function() { + this.CollaboratorsGetter.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - return this.CollaboratorsHandler.isUserInvitedMemberOfProject + return this.CollaboratorsGetter.isUserInvitedMemberOfProject .calledWith(this.current_user_id, this.project_id) .should.equal(true) }) @@ -888,11 +888,11 @@ describe('CollaboratorsInviteController', function() { return this.next.callCount.should.equal(0) }) - it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { - this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function() { + this.CollaboratorsGetter.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - return this.CollaboratorsHandler.isUserInvitedMemberOfProject + return this.CollaboratorsGetter.isUserInvitedMemberOfProject .calledWith(this.current_user_id, this.project_id) .should.equal(true) }) @@ -930,11 +930,11 @@ describe('CollaboratorsInviteController', function() { return expect(this.next.firstCall.args[0]).to.be.instanceof(Error) }) - it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { - this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function() { + this.CollaboratorsGetter.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - return this.CollaboratorsHandler.isUserInvitedMemberOfProject + return this.CollaboratorsGetter.isUserInvitedMemberOfProject .calledWith(this.current_user_id, this.project_id) .should.equal(true) }) @@ -978,11 +978,11 @@ describe('CollaboratorsInviteController', function() { return this.next.callCount.should.equal(0) }) - it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { - this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function() { + this.CollaboratorsGetter.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - return this.CollaboratorsHandler.isUserInvitedMemberOfProject + return this.CollaboratorsGetter.isUserInvitedMemberOfProject .calledWith(this.current_user_id, this.project_id) .should.equal(true) }) diff --git a/services/web/test/unit/src/Editor/EditorHttpControllerTests.js b/services/web/test/unit/src/Editor/EditorHttpControllerTests.js index 475e0062d5..3bf90f2bf7 100644 --- a/services/web/test/unit/src/Editor/EditorHttpControllerTests.js +++ b/services/web/test/unit/src/Editor/EditorHttpControllerTests.js @@ -38,7 +38,7 @@ describe('EditorHttpController', function() { }), './EditorController': (this.EditorController = {}), 'metrics-sharelatex': (this.Metrics = { inc: sinon.stub() }), - '../Collaborators/CollaboratorsHandler': (this.CollaboratorsHandler = {}), + '../Collaborators/CollaboratorsGetter': (this.CollaboratorsGetter = {}), '../Collaborators/CollaboratorsInviteHandler': (this.CollaboratorsInviteHandler = {}), '../TokenAccess/TokenAccessHandler': (this.TokenAccessHandler = {}), '../Authentication/AuthenticationController': (this.AuthenticationController = {}), @@ -177,12 +177,9 @@ describe('EditorHttpController', function() { this.ProjectGetter.getProjectWithoutDocLines = sinon .stub() .callsArgWith(1, null, this.project) - this.CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels = sinon + this.CollaboratorsGetter.getInvitedMembersWithPrivilegeLevels = sinon .stub() .callsArgWith(1, null, this.members) - this.CollaboratorsHandler.getTokenMembersWithPrivilegeLevels = sinon - .stub() - .callsArgWith(1, null, this.tokenMembers) this.CollaboratorsInviteHandler.getAllInvites = sinon .stub() .callsArgWith(1, null, this.invites) @@ -229,7 +226,7 @@ describe('EditorHttpController', function() { }) it('should get the list of users in the project', function() { - return this.CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels + return this.CollaboratorsGetter.getInvitedMembersWithPrivilegeLevels .calledWith(this.project_id) .should.equal(true) }) diff --git a/services/web/test/unit/src/Project/ProjectControllerTests.js b/services/web/test/unit/src/Project/ProjectControllerTests.js index f08669935c..1e54b65128 100644 --- a/services/web/test/unit/src/Project/ProjectControllerTests.js +++ b/services/web/test/unit/src/Project/ProjectControllerTests.js @@ -87,7 +87,7 @@ describe('ProjectController', function() { getRequestToken: sinon.stub().returns(this.token), protectTokens: sinon.stub() } - this.CollaboratorsHandler = { + this.CollaboratorsGetter = { userIsTokenMember: sinon.stub().callsArgWith(2, null, false) } this.ProjectEntityHandler = {} @@ -152,7 +152,7 @@ describe('ProjectController', function() { .AuthenticationController, '../Analytics/AnalyticsManager': this.AnalyticsManager, '../TokenAccess/TokenAccessHandler': this.TokenAccessHandler, - '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler, + '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter, '../../infrastructure/Modules': this.Modules, './ProjectEntityHandler': this.ProjectEntityHandler, '../Errors/Errors': Errors, @@ -805,7 +805,7 @@ describe('ProjectController', function() { }) it('should set isRestrictedTokenMember to true when the user is accessing project via read-only token', function(done) { - this.CollaboratorsHandler.userIsTokenMember.callsArgWith(2, null, true) + this.CollaboratorsGetter.userIsTokenMember.callsArgWith(2, null, true) this.AuthorizationManager.getPrivilegeLevelForProject.callsArgWith( 3, null, @@ -820,7 +820,7 @@ describe('ProjectController', function() { }) it('should set isRestrictedTokenMember to true when anonymous read-only token access', function(done) { - this.CollaboratorsHandler.userIsTokenMember.callsArgWith(2, null, null) + this.CollaboratorsGetter.userIsTokenMember.callsArgWith(2, null, null) this.AuthenticationController.isUserLoggedIn = sinon.stub().returns(false) this.AuthorizationManager.getPrivilegeLevelForProject.callsArgWith( 3, diff --git a/services/web/test/unit/src/Project/ProjectDeleterTests.js b/services/web/test/unit/src/Project/ProjectDeleterTests.js index 7ebc9afeb0..cc65c1479b 100644 --- a/services/web/test/unit/src/Project/ProjectDeleterTests.js +++ b/services/web/test/unit/src/Project/ProjectDeleterTests.js @@ -104,11 +104,15 @@ describe('ProjectDeleter', function() { removeProjectFromAllTags: sinon.stub().callsArgWith(2) } this.CollaboratorsHandler = { - removeUserFromAllProjets: sinon.stub().yields(), - getMemberIds: sinon - .stub() - .withArgs(this.project_id) - .yields(null, ['member-id-1', 'member-id-2']) + removeUserFromAllProjects: sinon.stub().yields() + } + this.CollaboratorsGetter = { + promises: { + getMemberIds: sinon + .stub() + .withArgs(this.project_id) + .resolves(['member-id-1', 'member-id-2']) + } } this.logger = { @@ -151,6 +155,7 @@ describe('ProjectDeleter', function() { '../Tags/TagsHandler': this.TagsHandler, '../FileStore/FileStoreHandler': (this.FileStoreHandler = {}), '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler, + '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter, '../Docstore/DocstoreManager': this.DocstoreManager, './ProjectDetailsHandler': this.ProjectDetailsHandler, '../../infrastructure/mongojs': { db: this.db }, @@ -239,11 +244,11 @@ describe('ProjectDeleter', function() { it('should remove all the projects the user is a collaborator of', function(done) { this.ProjectDeleter.deleteUsersProjects(this.user._id, () => { sinon.assert.calledWith( - this.CollaboratorsHandler.removeUserFromAllProjets, + this.CollaboratorsHandler.removeUserFromAllProjects, this.user._id ) sinon.assert.calledOnce( - this.CollaboratorsHandler.removeUserFromAllProjets + this.CollaboratorsHandler.removeUserFromAllProjects ) done() }) diff --git a/services/web/test/unit/src/Project/ProjectGetterTests.js b/services/web/test/unit/src/Project/ProjectGetterTests.js index b035a0ee0c..43aa638559 100644 --- a/services/web/test/unit/src/Project/ProjectGetterTests.js +++ b/services/web/test/unit/src/Project/ProjectGetterTests.js @@ -48,7 +48,7 @@ describe('ProjectGetter', function() { '../../models/DeletedProject': { DeletedProject: this.DeletedProject }, - '../Collaborators/CollaboratorsHandler': (this.CollaboratorsHandler = {}), + '../Collaborators/CollaboratorsGetter': (this.CollaboratorsGetter = {}), '../../infrastructure/LockManager': (this.LockManager = { runWithLock: sinon.spy((namespace, id, runner, callback) => runner(callback) @@ -314,8 +314,8 @@ describe('ProjectGetter', function() { this.Project.find .withArgs({ owner_ref: this.user_id }, this.fields) .yields(null, ['mock-owned-projects']) - this.CollaboratorsHandler.getProjectsUserIsMemberOf = sinon.stub() - this.CollaboratorsHandler.getProjectsUserIsMemberOf + this.CollaboratorsGetter.getProjectsUserIsMemberOf = sinon.stub() + this.CollaboratorsGetter.getProjectsUserIsMemberOf .withArgs(this.user_id, this.fields) .yields(null, { readAndWrite: ['mock-rw-projects'], diff --git a/services/web/test/unit/src/Subscription/LimitationsManagerTests.js b/services/web/test/unit/src/Subscription/LimitationsManagerTests.js index 1df4caa3d0..ec025e83c4 100644 --- a/services/web/test/unit/src/Subscription/LimitationsManagerTests.js +++ b/services/web/test/unit/src/Subscription/LimitationsManagerTests.js @@ -58,7 +58,7 @@ describe('LimitationsManager', function() { '../User/UserGetter': this.UserGetter, './SubscriptionLocator': this.SubscriptionLocator, 'settings-sharelatex': (this.Settings = {}), - '../Collaborators/CollaboratorsHandler': (this.CollaboratorsHandler = {}), + '../Collaborators/CollaboratorsGetter': (this.CollaboratorsGetter = {}), '../Collaborators/CollaboratorsInviteHandler': (this.CollaboratorsInviteHandler = {}), './V1SubscriptionManager': (this.V1SubscriptionManager = {}), 'logger-sharelatex': { @@ -150,7 +150,7 @@ describe('LimitationsManager', function() { this.current_number = 1 this.allowed_number = 2 this.invite_count = 0 - this.CollaboratorsHandler.getInvitedCollaboratorCount = ( + this.CollaboratorsGetter.getInvitedCollaboratorCount = ( project_id, callback ) => callback(null, this.current_number) @@ -183,7 +183,7 @@ describe('LimitationsManager', function() { this.current_number = 1 this.allowed_number = 4 this.invite_count = 1 - this.CollaboratorsHandler.getInvitedCollaboratorCount = ( + this.CollaboratorsGetter.getInvitedCollaboratorCount = ( project_id, callback ) => callback(null, this.current_number) @@ -216,7 +216,7 @@ describe('LimitationsManager', function() { this.current_number = 1 this.allowed_number = 2 this.invite_count = 0 - this.CollaboratorsHandler.getInvitedCollaboratorCount = ( + this.CollaboratorsGetter.getInvitedCollaboratorCount = ( project_id, callback ) => callback(null, this.current_number) @@ -249,7 +249,7 @@ describe('LimitationsManager', function() { this.current_number = 3 this.allowed_number = 2 this.invite_count = 0 - this.CollaboratorsHandler.getInvitedCollaboratorCount = ( + this.CollaboratorsGetter.getInvitedCollaboratorCount = ( project_id, callback ) => callback(null, this.current_number) @@ -282,7 +282,7 @@ describe('LimitationsManager', function() { this.current_number = 100 this.allowed_number = -1 this.invite_count = 0 - this.CollaboratorsHandler.getInvitedCollaboratorCount = ( + this.CollaboratorsGetter.getInvitedCollaboratorCount = ( project_id, callback ) => callback(null, this.current_number) @@ -315,7 +315,7 @@ describe('LimitationsManager', function() { this.current_number = 0 this.allowed_number = 2 this.invite_count = 2 - this.CollaboratorsHandler.getInvitedCollaboratorCount = ( + this.CollaboratorsGetter.getInvitedCollaboratorCount = ( project_id, callback ) => callback(null, this.current_number) @@ -348,7 +348,7 @@ describe('LimitationsManager', function() { this.current_number = 1 this.allowed_number = 2 this.invite_count = 1 - this.CollaboratorsHandler.getInvitedCollaboratorCount = ( + this.CollaboratorsGetter.getInvitedCollaboratorCount = ( project_id, callback ) => callback(null, this.current_number) diff --git a/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateSenderTests.js b/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateSenderTests.js index 576ea535fa..e59fa95721 100644 --- a/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateSenderTests.js +++ b/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateSenderTests.js @@ -39,7 +39,7 @@ describe('TpdsUpdateSender', function() { this.requestQueuer = function(queue, meth, opts, callback) {} const project = { owner_ref: user_id } const member_ids = [collaberator_ref_1, read_only_ref_1, user_id] - this.CollaboratorsHandler = { + this.CollaboratorsGetter = { getInvitedMemberIds: sinon.stub().yields(null, member_ids) } this.ProjectGetter = { @@ -69,7 +69,7 @@ describe('TpdsUpdateSender', function() { 'logger-sharelatex': { log() {} }, '../Project/ProjectGetter': this.ProjectGetter, request: this.request, - '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler, + '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter, 'metrics-sharelatex': { inc() {} } diff --git a/services/web/test/unit/src/TokenAccess/TokenAccessHandlerTests.js b/services/web/test/unit/src/TokenAccess/TokenAccessHandlerTests.js index 60463bcf0c..e44cc66f34 100644 --- a/services/web/test/unit/src/TokenAccess/TokenAccessHandlerTests.js +++ b/services/web/test/unit/src/TokenAccess/TokenAccessHandlerTests.js @@ -40,7 +40,7 @@ describe('TokenAccessHandler', function() { requires: { '../../models/Project': { Project: (this.Project = {}) }, 'settings-sharelatex': (this.settings = {}), - '../Collaborators/CollaboratorsHandler': (this.CollaboratorsHandler = {}), + '../Collaborators/CollaboratorsGetter': (this.CollaboratorsGetter = {}), '../User/UserGetter': (this.UserGetter = {}), '../V1/V1Api': (this.V1Api = { request: sinon.stub() @@ -301,7 +301,7 @@ describe('TokenAccessHandler', function() { describe('when user does have higher access', function() { beforeEach(function() { this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project) - return (this.CollaboratorsHandler.isUserInvitedMemberOfProject = sinon + return (this.CollaboratorsGetter.isUserInvitedMemberOfProject = sinon .stub() .callsArgWith(2, null, true)) }) @@ -328,10 +328,10 @@ describe('TokenAccessHandler', function() { this.userId, (err, project) => { expect( - this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount + this.CollaboratorsGetter.isUserInvitedMemberOfProject.callCount ).to.equal(1) expect( - this.CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith( + this.CollaboratorsGetter.isUserInvitedMemberOfProject.calledWith( this.userId, this.project._id ) @@ -358,7 +358,7 @@ describe('TokenAccessHandler', function() { describe('when user does not have higher access', function() { beforeEach(function() { this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project) - return (this.CollaboratorsHandler.isUserInvitedMemberOfProject = sinon + return (this.CollaboratorsGetter.isUserInvitedMemberOfProject = sinon .stub() .callsArgWith(2, null, false)) }) @@ -385,10 +385,10 @@ describe('TokenAccessHandler', function() { this.userId, (err, project) => { expect( - this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount + this.CollaboratorsGetter.isUserInvitedMemberOfProject.callCount ).to.equal(1) expect( - this.CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith( + this.CollaboratorsGetter.isUserInvitedMemberOfProject.calledWith( this.userId, this.project._id ) @@ -435,7 +435,7 @@ describe('TokenAccessHandler', function() { describe('when isUserInvitedMemberOfProject produces an error', function() { beforeEach(function() { this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project) - return (this.CollaboratorsHandler.isUserInvitedMemberOfProject = sinon + return (this.CollaboratorsGetter.isUserInvitedMemberOfProject = sinon .stub() .callsArgWith(2, new Error('woops'))) })