diff --git a/packages/component-invite/config/authsome-helpers.js b/packages/component-invite/config/authsome-helpers.js index e30375101e82bc13c9d464ec8c4be74686c6c7d9..b5f42492b077cee12d5a09d97404fb559e601f5e 100644 --- a/packages/component-invite/config/authsome-helpers.js +++ b/packages/component-invite/config/authsome-helpers.js @@ -6,23 +6,78 @@ const statuses = config.get('statuses') const publicStatusesPermissions = ['author', 'reviewer'] -module.exports = { - parseAuthorsData: (coll, matchingCollPerm) => { +const parseAuthorsData = (coll, matchingCollPerm) => { + if (['reviewer'].includes(matchingCollPerm.permission)) { + coll.authors = coll.authors.map(a => omit(a, ['email'])) + } +} + +const setPublicStatuses = (coll, matchingCollPerm) => { + const status = get(coll, 'status') || 'draft' + coll.visibleStatus = statuses[status].public + if (!publicStatusesPermissions.includes(matchingCollPerm.permission)) { + coll.visibleStatus = statuses[coll.status].private + } +} + +const filterRefusedInvitations = (coll, user) => { + const matchingInv = coll.invitations.find(inv => inv.userId === user.id) + if (matchingInv === undefined) return null + if (matchingInv.hasAnswer === true && !matchingInv.isAccepted) return null + return coll +} + +const filterObjectData = ( + collectionsPermissions = [], + object = {}, + user = {}, +) => { + if (object.type === 'fragment') { + const matchingCollPerm = collectionsPermissions.find( + collPerm => object.id === collPerm.fragmentId, + ) + if (matchingCollPerm === undefined) return null if (['reviewer'].includes(matchingCollPerm.permission)) { - coll.authors = coll.authors.map(a => omit(a, ['email'])) - } - }, - setPublicStatuses: (coll, matchingCollPerm) => { - const status = get(coll, 'status') || 'draft' - coll.visibleStatus = statuses[status].public - if (!publicStatusesPermissions.includes(matchingCollPerm.permission)) { - coll.visibleStatus = statuses[coll.status].private + object.files = omit(object.files, ['coverLetter']) + if (object.recommendations) + object.recommendations = object.recommendations.filter( + rec => rec.userId === user.id, + ) } - }, - filterRefusedInvitations: (coll, user) => { - const matchingInv = coll.invitations.find(inv => inv.userId === user.id) - if (matchingInv === undefined) return null - if (matchingInv.hasAnswer === true && !matchingInv.isAccepted) return null - return coll - }, + + return object + } + const matchingCollPerm = collectionsPermissions.find( + collPerm => object.id === collPerm.id, + ) + if (matchingCollPerm === undefined) return null + setPublicStatuses(object, matchingCollPerm) + parseAuthorsData(object, matchingCollPerm) + if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) { + return filterRefusedInvitations(object, user) + } + + return object +} + +const getTeamsByPermissions = async (teamIds, permissions, TeamModel) => { + const teams = await Promise.all( + teamIds.map(async teamId => { + const team = await TeamModel.find(teamId) + if (permissions.includes(team.teamType.permissions)) { + return team + } + return null + }), + ) + + return teams.filter(Boolean) +} + +module.exports = { + parseAuthorsData, + setPublicStatuses, + filterRefusedInvitations, + filterObjectData, + getTeamsByPermissions, } diff --git a/packages/component-invite/config/authsome-mode.js b/packages/component-invite/config/authsome-mode.js index 2e3d8ea7faf8d5e86169de321d922bbc25c9a56d..3498bc042c01679ecc1e75f8a783628e63cd153c 100644 --- a/packages/component-invite/config/authsome-mode.js +++ b/packages/component-invite/config/authsome-mode.js @@ -5,52 +5,47 @@ const helpers = require('./authsome-helpers') async function teamPermissions(user, operation, object, context) { const permissions = ['handlingEditor', 'author', 'reviewer'] - const teams = await Promise.all( - user.teams.map(async teamId => { - const team = await context.models.Team.find(teamId) - if (permissions.includes(team.teamType.permissions)) { - return team + const teams = await helpers.getTeamsByPermissions( + user.teams, + permissions, + context.models.Team, + ) + + const collectionsPermissions = await Promise.all( + teams.map(async team => { + const collection = await context.models.Collection.find(team.object.id) + const collPerm = { + id: collection.id, + permission: team.teamType.permissions, } - return null + const objectType = get(object, 'type') + if (objectType === 'fragment' && collection.fragments.includes(object.id)) + collPerm.fragmentId = object.id + + return collPerm }), ) - const collectionsPermissions = teams.filter(Boolean).map(team => ({ - id: team.object.id, - permission: team.teamType.permissions, - })) + if (collectionsPermissions.length === 0) return {} - if (collectionsPermissions.length > 0) { - return { - filter: filterParam => { - if (!filterParam.length) return filterParam - - const collections = filterParam - .map(coll => { - const matchingCollPerm = collectionsPermissions.find( - collPerm => coll.id === collPerm.id, - ) - if (matchingCollPerm === undefined) { - return null - } - helpers.setPublicStatuses(coll, matchingCollPerm) - helpers.parseAuthorsData(coll, matchingCollPerm) - if ( - ['reviewer', 'handlingEditor'].includes( - matchingCollPerm.permission, - ) - ) { - return helpers.filterRefusedInvitations(coll, user) - } - return coll - }) - .filter(Boolean) - return collections - }, - } - } + return { + filter: filterParam => { + if (!filterParam.length) { + return helpers.filterObjectData( + collectionsPermissions, + filterParam, + user, + ) + } - return {} + const collections = filterParam + .map(coll => + helpers.filterObjectData(collectionsPermissions, coll, user), + ) + .filter(Boolean) + return collections + }, + } } function unauthenticatedUser(operation, object) { @@ -153,7 +148,7 @@ async function authenticatedUser(user, operation, object, context) { } } - // only allow the HE to create, delete an invitation, or get inv details` + // only allow the HE to create, delete an invitation, or get invitation details if ( ['POST', 'GET', 'DELETE'].includes(operation) && get(object.collection, 'type') === 'collection' && @@ -162,9 +157,29 @@ async function authenticatedUser(user, operation, object, context) { const collection = await context.models.Collection.find( get(object.collection, 'id'), ) - if (collection.handlingEditor.id === user.id) { - return true - } + const handlingEditor = get(collection, 'handlingEditor') + if (!handlingEditor) return false + if (handlingEditor.id === user.id) return true + return false + } + + // only allow a reviewer to submit and to modify a recommendation + if ( + ['POST', 'PATCH'].includes(operation) && + get(object.collection, 'type') === 'collection' && + object.path.includes('recommendations') + ) { + const collection = await context.models.Collection.find( + get(object.collection, 'id'), + ) + const teams = await helpers.getTeamsByPermissions( + user.teams, + ['reviewer'], + context.models.Team, + ) + if (teams.length === 0) return false + const matchingTeam = teams.find(team => team.object.id === collection.id) + if (matchingTeam) return true return false } diff --git a/packages/component-manuscript-manager/config/authsome-helpers.js b/packages/component-manuscript-manager/config/authsome-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..30c00fdbe00f4f00c34324a81d363191da6d12a5 --- /dev/null +++ b/packages/component-manuscript-manager/config/authsome-helpers.js @@ -0,0 +1,83 @@ +const omit = require('lodash/omit') +const config = require('config') +const get = require('lodash/get') + +const statuses = config.get('statuses') + +const publicStatusesPermissions = ['author', 'reviewer'] + +const parseAuthorsData = (coll, matchingCollPerm) => { + if (['reviewer'].includes(matchingCollPerm.permission)) { + coll.authors = coll.authors.map(a => omit(a, ['email'])) + } +} + +const setPublicStatuses = (coll, matchingCollPerm) => { + const status = get(coll, 'status') || 'draft' + coll.visibleStatus = statuses[status].public + if (!publicStatusesPermissions.includes(matchingCollPerm.permission)) { + coll.visibleStatus = statuses[coll.status].private + } +} + +const filterRefusedInvitations = (coll, user) => { + const matchingInv = coll.invitations.find(inv => inv.userId === user.id) + if (matchingInv === undefined) return null + if (matchingInv.hasAnswer === true && !matchingInv.isAccepted) return null + return coll +} + +const filterObjectData = ( + collectionsPermissions = [], + object = {}, + user = {}, +) => { + if (object.type === 'fragment') { + const matchingCollPerm = collectionsPermissions.find( + collPerm => object.id === collPerm.fragmentId, + ) + if (matchingCollPerm === undefined) return null + if (['reviewer'].includes(matchingCollPerm.permission)) { + object.files = omit(object.files, ['coverLetter']) + if (object.recommendations) + object.recommendations = object.recommendations.filter( + rec => rec.userId === user.id, + ) + } + + return object + } + const matchingCollPerm = collectionsPermissions.find( + collPerm => object.id === collPerm.id, + ) + if (matchingCollPerm === undefined) return null + setPublicStatuses(object, matchingCollPerm) + parseAuthorsData(object, matchingCollPerm) + if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) { + return filterRefusedInvitations(object, user) + } + + return object +} + +const getTeamsByPermissions = async (teamIds = [], permissions, TeamModel) => { + const teams = await Promise.all( + teamIds.map(async teamId => { + const team = await TeamModel.find(teamId) + if (permissions.includes(team.teamType.permissions)) { + return team + } + return null + }), + ) + + return teams.filter(Boolean) +} + +module.exports = { + parseAuthorsData, + setPublicStatuses, + filterRefusedInvitations, + filterObjectData, + getTeamsByPermissions, +} diff --git a/packages/component-manuscript-manager/config/authsome-mode.js b/packages/component-manuscript-manager/config/authsome-mode.js new file mode 100644 index 0000000000000000000000000000000000000000..3498bc042c01679ecc1e75f8a783628e63cd153c --- /dev/null +++ b/packages/component-manuscript-manager/config/authsome-mode.js @@ -0,0 +1,245 @@ +const get = require('lodash/get') +const pickBy = require('lodash/pickBy') +const omit = require('lodash/omit') +const helpers = require('./authsome-helpers') + +async function teamPermissions(user, operation, object, context) { + const permissions = ['handlingEditor', 'author', 'reviewer'] + const teams = await helpers.getTeamsByPermissions( + user.teams, + permissions, + context.models.Team, + ) + + const collectionsPermissions = await Promise.all( + teams.map(async team => { + const collection = await context.models.Collection.find(team.object.id) + const collPerm = { + id: collection.id, + permission: team.teamType.permissions, + } + const objectType = get(object, 'type') + if (objectType === 'fragment' && collection.fragments.includes(object.id)) + collPerm.fragmentId = object.id + + return collPerm + }), + ) + + if (collectionsPermissions.length === 0) return {} + + return { + filter: filterParam => { + if (!filterParam.length) { + return helpers.filterObjectData( + collectionsPermissions, + filterParam, + user, + ) + } + + const collections = filterParam + .map(coll => + helpers.filterObjectData(collectionsPermissions, coll, user), + ) + .filter(Boolean) + return collections + }, + } +} + +function unauthenticatedUser(operation, object) { + // Public/unauthenticated users can GET /collections, filtered by 'published' + if (operation === 'GET' && object && object.path === '/collections') { + return { + filter: collections => + collections.filter(collection => collection.published), + } + } + + // Public/unauthenticated users can GET /collections/:id/fragments, filtered by 'published' + if ( + operation === 'GET' && + object && + object.path === '/collections/:id/fragments' + ) { + return { + filter: fragments => fragments.filter(fragment => fragment.published), + } + } + + // and filtered individual collection's properties: id, title, source, content, owners + if (operation === 'GET' && object && object.type === 'collection') { + if (object.published) { + return { + filter: collection => + pickBy(collection, (_, key) => + ['id', 'title', 'owners'].includes(key), + ), + } + } + } + + if (operation === 'GET' && object && object.type === 'fragment') { + if (object.published) { + return { + filter: fragment => + pickBy(fragment, (_, key) => + ['id', 'title', 'source', 'presentation', 'owners'].includes(key), + ), + } + } + } + + return false +} + +async function authenticatedUser(user, operation, object, context) { + // Allow the authenticated user to POST a collection (but not with a 'filtered' property) + if (operation === 'POST' && object.path === '/collections') { + return { + filter: collection => omit(collection, 'filtered'), + } + } + + // Allow the authenticated user to GET collections they own + if (operation === 'GET' && object === '/collections/') { + return { + filter: collection => collection.owners.includes(user.id), + } + } + + // Allow owners of a collection to GET its teams, e.g. + // GET /api/collections/1/teams + if (operation === 'GET' && get(object, 'path') === '/teams') { + const collectionId = get(object, 'params.collectionId') + if (collectionId) { + const collection = await context.models.Collection.find(collectionId) + if (collection.owners.includes(user.id)) { + return true + } + } + } + + if ( + operation === 'GET' && + get(object, 'type') === 'team' && + get(object, 'object.type') === 'collection' + ) { + const collection = await context.models.Collection.find( + get(object, 'object.id'), + ) + if (collection.owners.includes(user.id)) { + return true + } + } + + // Advanced example + // Allow authenticated users to create a team based around a collection + // if they are one of the owners of this collection + if (['POST', 'PATCH'].includes(operation) && get(object, 'type') === 'team') { + if (get(object, 'object.type') === 'collection') { + const collection = await context.models.Collection.find( + get(object, 'object.id'), + ) + if (collection.owners.includes(user.id)) { + return true + } + } + } + + // only allow the HE to create, delete an invitation, or get invitation details + if ( + ['POST', 'GET', 'DELETE'].includes(operation) && + get(object.collection, 'type') === 'collection' && + object.path.includes('invitations') + ) { + const collection = await context.models.Collection.find( + get(object.collection, 'id'), + ) + const handlingEditor = get(collection, 'handlingEditor') + if (!handlingEditor) return false + if (handlingEditor.id === user.id) return true + return false + } + + // only allow a reviewer to submit and to modify a recommendation + if ( + ['POST', 'PATCH'].includes(operation) && + get(object.collection, 'type') === 'collection' && + object.path.includes('recommendations') + ) { + const collection = await context.models.Collection.find( + get(object.collection, 'id'), + ) + const teams = await helpers.getTeamsByPermissions( + user.teams, + ['reviewer'], + context.models.Team, + ) + if (teams.length === 0) return false + const matchingTeam = teams.find(team => team.object.id === collection.id) + if (matchingTeam) return true + return false + } + + if (user.teams.length !== 0) { + const permissions = await teamPermissions(user, operation, object, context) + + if (permissions) { + return permissions + } + } + + if (get(object, 'type') === 'fragment') { + const fragment = object + + if (fragment.owners.includes(user.id)) { + return true + } + } + + if (get(object, 'type') === 'collection') { + if (['GET', 'DELETE'].includes(operation)) { + return true + } + + // Only allow filtered updating (mirroring filtered creation) for non-admin users) + if (operation === 'PATCH') { + return { + filter: collection => omit(collection, 'filtered'), + } + } + } + + // A user can GET, DELETE and PATCH itself + if (get(object, 'type') === 'user' && get(object, 'id') === user.id) { + if (['GET', 'DELETE', 'PATCH'].includes(operation)) { + return true + } + } + // If no individual permissions exist (above), fallback to unauthenticated + // user's permission + return unauthenticatedUser(operation, object) +} + +const authsomeMode = async (userId, operation, object, context) => { + if (!userId) { + return unauthenticatedUser(operation, object) + } + + // It's up to us to retrieve the relevant models for our + // authorization/authsome mode, e.g. + const user = await context.models.User.find(userId) + + // Admins and editor in chiefs can do anything + if (user && (user.admin === true || user.editorInChief === true)) return true + + if (user) { + return authenticatedUser(user, operation, object, context) + } + + return false +} + +module.exports = authsomeMode diff --git a/packages/component-manuscript-manager/config/default.js b/packages/component-manuscript-manager/config/default.js new file mode 100644 index 0000000000000000000000000000000000000000..7afbb2f22c26d8ae4d9f314989bc4a02189360f7 --- /dev/null +++ b/packages/component-manuscript-manager/config/default.js @@ -0,0 +1,58 @@ +const path = require('path') + +module.exports = { + authsome: { + mode: path.resolve(__dirname, 'authsome-mode.js'), + teams: { + handlingEditor: { + name: 'Handling Editors', + }, + reviewer: { + name: 'Reviewer', + }, + }, + }, + mailer: { + from: 'test@example.com', + }, + 'invite-reset-password': { + url: + process.env.PUBSWEET_INVITE_PASSWORD_RESET_URL || + 'http://localhost:3000/invite', + }, + roles: { + global: ['admin', 'editorInChief', 'author', 'handlingEditor'], + collection: ['handlingEditor', 'reviewer', 'author'], + inviteRights: { + admin: ['admin', 'editorInChief', 'author'], + editorInChief: ['handlingEditor'], + handlingEditor: ['reviewer'], + }, + }, + statuses: { + draft: { + public: 'Draft', + private: 'Draft', + }, + submitted: { + public: 'Submitted', + private: 'Submitted', + }, + heInvited: { + public: 'Submitted', + private: 'HE Invited', + }, + heAssigned: { + public: 'HE Assigned', + private: 'HE Assigned', + }, + reviewersInvited: { + public: 'Reviewers Invited', + private: 'Reviewers Invited', + }, + underReview: { + public: 'Under Review', + private: 'Under Review', + }, + }, +} diff --git a/packages/component-manuscript-manager/config/test.js b/packages/component-manuscript-manager/config/test.js new file mode 100644 index 0000000000000000000000000000000000000000..0eb54780a931f66f1b611d9a4d51552908ece2c3 --- /dev/null +++ b/packages/component-manuscript-manager/config/test.js @@ -0,0 +1,59 @@ +const path = require('path') + +module.exports = { + authsome: { + mode: path.resolve(__dirname, 'authsome-mode.js'), + teams: { + handlingEditor: { + name: 'Handling Editors', + }, + reviewer: { + name: 'Reviewer', + }, + }, + }, + mailer: { + from: 'test@example.com', + }, + 'invite-reset-password': { + url: + process.env.PUBSWEET_INVITE_PASSWORD_RESET_URL || + 'http://localhost:3000/invite', + }, + roles: { + global: ['admin', 'editorInChief', 'author', 'handlingEditor'], + collection: ['handlingEditor', 'reviewer', 'author'], + inviteRights: { + admin: ['admin', 'editorInChief', 'author', 'handlingEditor', 'author'], + editorInChief: ['handlingEditor'], + handlingEditor: ['reviewer'], + author: ['author'], + }, + }, + statuses: { + draft: { + public: 'Draft', + private: 'Draft', + }, + submitted: { + public: 'Submitted', + private: 'Submitted', + }, + heInvited: { + public: 'Submitted', + private: 'HE Invited', + }, + heAssigned: { + public: 'HE Assigned', + private: 'HE Assigned', + }, + reviewersInvited: { + public: 'Reviewers Invited', + private: 'Reviewers Invited', + }, + underReview: { + public: 'Under Review', + private: 'Under Review', + }, + }, +} diff --git a/packages/component-manuscript-manager/src/helpers/Collection.js b/packages/component-manuscript-manager/src/helpers/Collection.js new file mode 100644 index 0000000000000000000000000000000000000000..4949969b040748ff54163782eda0155acd6ec789 --- /dev/null +++ b/packages/component-manuscript-manager/src/helpers/Collection.js @@ -0,0 +1,13 @@ +const config = require('config') + +const statuses = config.get('statuses') + +const updateStatus = async (collection, newStatus) => { + collection.status = newStatus + collection.visibleStatus = statuses[collection.status].private + await collection.save() +} + +module.exports = { + updateStatus, +} diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js index 2e4ee8ae1fd06f711b803bd0af93dcbad6aa6f71..9c1b974768e0c4e9a009cce1f858dc9f1ea02864 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js @@ -1,5 +1,6 @@ const helpers = require('../../helpers/helpers') const authsomeHelper = require('../../helpers/authsome') +const collectionHelper = require('../../helpers/Collection') module.exports = models => async (req, res) => { const { collectionId, fragmentId, recommendationId } = req.params @@ -15,8 +16,8 @@ module.exports = models => async (req, res) => { collection, path: req.route.path, } - const canPost = await authsome.can(req.user, 'PATCH', target) - if (!canPost) + const canPatch = await authsome.can(req.user, 'PATCH', target) + if (!canPatch) return res.status(403).json({ error: 'Unauthorized.', }) @@ -32,6 +33,8 @@ module.exports = models => async (req, res) => { }) Object.assign(recommendation, req.body) recommendation.updatedOn = Date.now() + if (req.body.submittedOn !== undefined) + await collectionHelper.updateStatus(collection, 'reviewCompleted') await fragment.save() return res.status(200).json(recommendation) } catch (e) { diff --git a/packages/component-manuscript-manager/src/tests/fixtures/fixtures.js b/packages/component-manuscript-manager/src/tests/fixtures/fixtures.js index 4ca540342b93acc22bf7903e6f5f8251481bc6ca..cbb5c85cbe056c7dc633db935bf5707764ac40bd 100644 --- a/packages/component-manuscript-manager/src/tests/fixtures/fixtures.js +++ b/packages/component-manuscript-manager/src/tests/fixtures/fixtures.js @@ -1,9 +1,11 @@ const users = require('./users') const collections = require('./collections') const fragments = require('./fragments') +const teams = require('./teams') module.exports = { users, collections, fragments, + teams, } diff --git a/packages/component-manuscript-manager/src/tests/fixtures/fragments.js b/packages/component-manuscript-manager/src/tests/fixtures/fragments.js index c64478c3390cb63db38602cced003cf97087a32f..0bc2a06b2cacf0a2a2dcb83ee5202cf9059f3618 100644 --- a/packages/component-manuscript-manager/src/tests/fixtures/fragments.js +++ b/packages/component-manuscript-manager/src/tests/fixtures/fragments.js @@ -1,4 +1,5 @@ const Chance = require('chance') +const { recReviewer } = require('./userData') const chance = new Chance() const fragments = { @@ -9,6 +10,29 @@ const fragments = { abstract: chance.paragraph(), }, save: jest.fn(), + recommendations: [ + { + recommendation: 'accept', + recommendationType: 'review', + comments: [ + { + content: chance.paragraph(), + public: chance.bool(), + files: [ + { + id: chance.guid(), + name: 'file.pdf', + size: chance.natural(), + }, + ], + }, + ], + id: chance.guid(), + userId: recReviewer.id, + createdOn: chance.timestamp(), + updatedOn: chance.timestamp(), + }, + ], }, } diff --git a/packages/component-manuscript-manager/src/tests/fixtures/teamIDs.js b/packages/component-manuscript-manager/src/tests/fixtures/teamIDs.js new file mode 100644 index 0000000000000000000000000000000000000000..607fd6661b1e7c9848bf13257a0d198f230fab00 --- /dev/null +++ b/packages/component-manuscript-manager/src/tests/fixtures/teamIDs.js @@ -0,0 +1,10 @@ +const Chance = require('chance') + +const chance = new Chance() +const heID = chance.guid() +const revId = chance.guid() + +module.exports = { + heTeamID: heID, + revTeamID: revId, +} diff --git a/packages/component-manuscript-manager/src/tests/fixtures/teams.js b/packages/component-manuscript-manager/src/tests/fixtures/teams.js new file mode 100644 index 0000000000000000000000000000000000000000..fb2a14eb33ff92d6e39b4763a7ebe89f46028d5c --- /dev/null +++ b/packages/component-manuscript-manager/src/tests/fixtures/teams.js @@ -0,0 +1,25 @@ +const users = require('./users') +const collections = require('./collections') +const { revTeamID } = require('./teamIDs') + +const { collection } = collections +const { reviewer } = users +const teams = { + revTeam: { + teamType: { + name: 'reviewer', + permissions: 'reviewer', + }, + group: 'reviewer', + name: 'reviewer', + object: { + type: 'collection', + id: collection.id, + }, + members: [reviewer.id], + save: jest.fn(() => teams.revTeam), + updateProperties: jest.fn(() => teams.revTeam), + id: revTeamID, + }, +} +module.exports = teams diff --git a/packages/component-manuscript-manager/src/tests/fixtures/userData.js b/packages/component-manuscript-manager/src/tests/fixtures/userData.js index 546e2d867998c6de1cab8e10a4df20fb74d5288d..4e3b994a70b782f25ec85e0f41410824dd4307c4 100644 --- a/packages/component-manuscript-manager/src/tests/fixtures/userData.js +++ b/packages/component-manuscript-manager/src/tests/fixtures/userData.js @@ -15,4 +15,5 @@ module.exports = { author: generateUserData(), reviewer: generateUserData(), answerReviewer: generateUserData(), + recReviewer: generateUserData(), } diff --git a/packages/component-manuscript-manager/src/tests/fixtures/users.js b/packages/component-manuscript-manager/src/tests/fixtures/users.js index f3ede318fb34ae24db90440959bd3822f186fbf6..8ac951e6a00bfa0a4be5d6584a0eca881560cda3 100644 --- a/packages/component-manuscript-manager/src/tests/fixtures/users.js +++ b/packages/component-manuscript-manager/src/tests/fixtures/users.js @@ -1,4 +1,6 @@ -const { reviewer } = require('./userData') +const { reviewer, author, recReviewer } = require('./userData') +const { revTeamID } = require('./teamIDs') + const Chance = require('chance') const chance = new Chance() @@ -16,7 +18,36 @@ const users = { title: 'Mr', save: jest.fn(() => users.reviewer), isConfirmed: true, - invitationToken: 'inv-token-123', + teams: [revTeamID], + }, + author: { + type: 'user', + username: chance.word(), + email: author.email, + password: 'password', + admin: false, + id: author.id, + firstName: author.firstName, + lastName: author.lastName, + affiliation: chance.company(), + title: 'Mr', + save: jest.fn(() => users.author), + isConfirmed: true, + }, + recReviewer: { + type: 'user', + username: chance.word(), + email: recReviewer.email, + password: 'password', + admin: false, + id: recReviewer.id, + firstName: recReviewer.firstName, + lastName: recReviewer.lastName, + affiliation: chance.company(), + title: 'Mr', + save: jest.fn(() => users.recReviewer), + isConfirmed: true, + teams: [revTeamID], }, } diff --git a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js index 840d74cc57229a4e604ecf3d10e1f6c9f5c30470..773188df8be57312ea9ab5e83b3c724db224065c 100644 --- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js +++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js @@ -27,7 +27,11 @@ const reqBody = { } const path = '../../routes/fragmentsRecommendations/patch' -describe('Patch collections invitations route handler', () => { +const route = { + path: + '/api/collections/:collectionId/fragments/:fragmentId/recommendations/:recommendationId', +} +describe('Patch fragments recommendations route handler', () => { let testFixtures = {} let body = {} let models @@ -36,30 +40,16 @@ describe('Patch collections invitations route handler', () => { body = cloneDeep(reqBody) models = Model.build(testFixtures) }) - it('should return an error when params are missing', async () => { - const { reviewer } = testFixtures.users - delete body.comments - const res = await requests.sendRequest({ - body, - userId: reviewer.id, - models, - path, - }) - - expect(res.statusCode).toBe(400) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual('Parameters are missing.') - }) it('should return success when the parameters are correct', async () => { - const { reviewer } = testFixtures.users + const { recReviewer } = testFixtures.users const { collection } = testFixtures.collections const { fragment } = testFixtures.fragments const recommendation = fragment.recommendations[0] - const res = await requests.sendRequest({ body, - userId: reviewer.id, + userId: recReviewer.id, models, + route, path, params: { collectionId: collection.id, @@ -70,7 +60,7 @@ describe('Patch collections invitations route handler', () => { expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) - expect(data.userId).toEqual(reviewer.id) + expect(data.userId).toEqual(recReviewer.id) }) it('should return an error when the fragmentId does not match the collectionId', async () => { const { reviewer } = testFixtures.users @@ -83,6 +73,7 @@ describe('Patch collections invitations route handler', () => { body, userId: reviewer.id, models, + route, path, params: { collectionId: collection.id, @@ -103,6 +94,7 @@ describe('Patch collections invitations route handler', () => { body, userId: reviewer.id, models, + route, path, params: { collectionId: 'invalid-id', @@ -117,12 +109,13 @@ describe('Patch collections invitations route handler', () => { }) it('should return an error when the recommendation does not exist', async () => { const { reviewer } = testFixtures.users - const { collection } = testFixtures.collection + const { collection } = testFixtures.collections const { fragment } = testFixtures.fragments const res = await requests.sendRequest({ body, userId: reviewer.id, models, + route, path, params: { collectionId: collection.id, @@ -133,6 +126,27 @@ describe('Patch collections invitations route handler', () => { expect(res.statusCode).toBe(404) const data = JSON.parse(res._getData()) - expect(data.error).toEqual('Item not found') + expect(data.error).toEqual('Recommendation not found.') + }) + it('should return an error when the request user is not a reviewer', async () => { + const { author } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + const res = await requests.sendRequest({ + body, + userId: author.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Unauthorized.') }) }) diff --git a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js index 1bc3506019b290db9b0dea571f22a454e18002f2..ca11f8fa76ea5cd757abfd6710fc2482f9d33981 100644 --- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js +++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js @@ -27,7 +27,10 @@ const reqBody = { } const path = '../../routes/fragmentsRecommendations/post' -describe('Post collections invitations route handler', () => { +const route = { + path: '/api/collections/:collectionId/fragments/:fragmentId/recommendations', +} +describe('Post fragments recommendations route handler', () => { let testFixtures = {} let body = {} let models @@ -38,17 +41,18 @@ describe('Post collections invitations route handler', () => { }) it('should return an error when params are missing', async () => { const { reviewer } = testFixtures.users - delete body.comments + delete body.recommendationType const res = await requests.sendRequest({ body, userId: reviewer.id, + route, models, path, }) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) - expect(data.error).toEqual('Parameters are missing.') + expect(data.error).toEqual('Recommendation type is required.') }) it('should return success when the parameters are correct', async () => { const { reviewer } = testFixtures.users @@ -59,6 +63,7 @@ describe('Post collections invitations route handler', () => { body, userId: reviewer.id, models, + route, path, params: { collectionId: collection.id, @@ -79,6 +84,7 @@ describe('Post collections invitations route handler', () => { body, userId: reviewer.id, models, + route, path, params: { collectionId: collection.id, @@ -97,6 +103,7 @@ describe('Post collections invitations route handler', () => { body, userId: reviewer.id, models, + route, path, params: { collectionId: 'invalid-id', @@ -108,4 +115,25 @@ describe('Post collections invitations route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('Item not found') }) + it('should return an error when the request user is not a reviewer', async () => { + const { author } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + const res = await requests.sendRequest({ + body, + userId: author.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Unauthorized.') + }) }) diff --git a/packages/component-manuscript-manager/src/tests/helpers/Model.js b/packages/component-manuscript-manager/src/tests/helpers/Model.js index ce46f395b170438c22d8e7c6be2a8e0fd7fabba7..3d6ae0871452e134c63f65913af6b9de4b210f8a 100644 --- a/packages/component-manuscript-manager/src/tests/helpers/Model.js +++ b/packages/component-manuscript-manager/src/tests/helpers/Model.js @@ -15,6 +15,9 @@ const build = fixtures => { Fragment: { find: jest.fn(id => findMock(id, 'fragments', fixtures)), }, + Team: { + find: jest.fn(id => findMock(id, 'teams', fixtures)), + }, } UserMock.find = jest.fn(id => findMock(id, 'users', fixtures)) models.User = UserMock diff --git a/packages/xpub-faraday/config/authsome-helpers.js b/packages/xpub-faraday/config/authsome-helpers.js index b5f42492b077cee12d5a09d97404fb559e601f5e..30c00fdbe00f4f00c34324a81d363191da6d12a5 100644 --- a/packages/xpub-faraday/config/authsome-helpers.js +++ b/packages/xpub-faraday/config/authsome-helpers.js @@ -60,7 +60,7 @@ const filterObjectData = ( return object } -const getTeamsByPermissions = async (teamIds, permissions, TeamModel) => { +const getTeamsByPermissions = async (teamIds = [], permissions, TeamModel) => { const teams = await Promise.all( teamIds.map(async teamId => { const team = await TeamModel.find(teamId) diff --git a/packages/xpub-faraday/config/default.js b/packages/xpub-faraday/config/default.js index a773b41f2bd0746604c0ca261d1b0aa01fe5267d..4edb74bf17a47ed1c72ed63d7420dbe2276aed9f 100644 --- a/packages/xpub-faraday/config/default.js +++ b/packages/xpub-faraday/config/default.js @@ -114,5 +114,9 @@ module.exports = { public: 'Under Review', private: 'Under Review', }, + reviewCompleted: { + public: 'Under Review', + private: 'Review Completed', + }, }, }