diff --git a/packages/component-fixture-manager/.gitignore b/packages/component-fixture-manager/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3614a810088d89d9ccaa28d82401545634874a18 --- /dev/null +++ b/packages/component-fixture-manager/.gitignore @@ -0,0 +1,8 @@ +_build/ +api/ +logs/ +node_modules/ +uploads/ +.env.* +.env +config/local*.* \ No newline at end of file diff --git a/packages/component-fixture-manager/README.md b/packages/component-fixture-manager/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a0ac7afbf2c8255c8a94f4f2ff525dd245cc3c93 --- /dev/null +++ b/packages/component-fixture-manager/README.md @@ -0,0 +1,2 @@ +# Helper Service + diff --git a/packages/component-fixture-manager/index.js b/packages/component-fixture-manager/index.js new file mode 100644 index 0000000000000000000000000000000000000000..dd95e77d96237d61fc1a8f03d5f2d7d2f80a7e0f --- /dev/null +++ b/packages/component-fixture-manager/index.js @@ -0,0 +1 @@ +module.exports = require('./src/Fixture') diff --git a/packages/component-fixture-manager/package.json b/packages/component-fixture-manager/package.json new file mode 100644 index 0000000000000000000000000000000000000000..fe566ea163b877842eba597a058293b0a0af7fbe --- /dev/null +++ b/packages/component-fixture-manager/package.json @@ -0,0 +1,25 @@ +{ + "name": "pubsweet-component-fixture-service", + "version": "0.0.1", + "description": "fixture service component for pubsweet", + "license": "MIT", + "author": "Collaborative Knowledge Foundation", + "files": [ + "src" + ], + "main": "index.js", + "repository": { + "type": "git", + "url": "https://gitlab.coko.foundation/xpub/xpub-faraday", + "path": "component-fixture-service" + }, + "dependencies": { + }, + "peerDependencies": { + "@pubsweet/logger": "^0.0.1", + "pubsweet-server": "^1.0.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/component-fixture-manager/src/Fixture.js b/packages/component-fixture-manager/src/Fixture.js new file mode 100644 index 0000000000000000000000000000000000000000..c2c53d05d677167696b1dad66e7b1f294a23ae10 --- /dev/null +++ b/packages/component-fixture-manager/src/Fixture.js @@ -0,0 +1,7 @@ +const fixtures = require('./fixtures/fixtures') +const Model = require('./helpers/Model') + +module.exports = { + fixtures, + Model, +} diff --git a/packages/component-fixture-manager/src/fixtures/collectionIDs.js b/packages/component-fixture-manager/src/fixtures/collectionIDs.js new file mode 100644 index 0000000000000000000000000000000000000000..0633f816e39359721e73dd32252279f05bcdff5e --- /dev/null +++ b/packages/component-fixture-manager/src/fixtures/collectionIDs.js @@ -0,0 +1,8 @@ +const Chance = require('chance') + +const chance = new Chance() +const collId = chance.guid() + +module.exports = { + standardCollID: collId, +} diff --git a/packages/component-manuscript-manager/src/tests/fixtures/collections.js b/packages/component-fixture-manager/src/fixtures/collections.js similarity index 65% rename from packages/component-manuscript-manager/src/tests/fixtures/collections.js rename to packages/component-fixture-manager/src/fixtures/collections.js index bef6132199981a79b87ea86440146eaab5891883..ead1ed38cabd8e5eaa615a1fa7a9e5b84d6d3052 100644 --- a/packages/component-manuscript-manager/src/tests/fixtures/collections.js +++ b/packages/component-fixture-manager/src/fixtures/collections.js @@ -1,29 +1,17 @@ const Chance = require('chance') -const { - user, - handlingEditor, - author, - reviewer, - answerReviewer, -} = require('./userData') +const { user, handlingEditor, answerHE } = require('./userData') const { fragment } = require('./fragments') +const { standardCollID } = require('./collectionIDs') const chance = new Chance() const collections = { collection: { - id: chance.guid(), + id: standardCollID, title: chance.sentence(), type: 'collection', fragments: [fragment.id], owners: [user.id], save: jest.fn(), - authors: [ - { - userId: author.id, - isSubmitting: true, - isCorresponding: false, - }, - ], invitations: [ { id: chance.guid(), @@ -36,19 +24,10 @@ const collections = { }, { id: chance.guid(), - role: 'reviewer', - hasAnswer: false, - isAccepted: false, - userId: reviewer.id, - invitedOn: chance.timestamp(), - respondedOn: null, - }, - { - id: chance.guid(), - role: 'reviewer', + role: 'handlingEditor', hasAnswer: true, isAccepted: false, - userId: answerReviewer.id, + userId: answerHE.id, invitedOn: chance.timestamp(), respondedOn: chance.timestamp(), }, diff --git a/packages/component-invite/src/tests/fixtures/fixtures.js b/packages/component-fixture-manager/src/fixtures/fixtures.js similarity index 100% rename from packages/component-invite/src/tests/fixtures/fixtures.js rename to packages/component-fixture-manager/src/fixtures/fixtures.js diff --git a/packages/component-fixture-manager/src/fixtures/fragments.js b/packages/component-fixture-manager/src/fixtures/fragments.js new file mode 100644 index 0000000000000000000000000000000000000000..ab41ab42856c37cf396c947897df22c0649e73ce --- /dev/null +++ b/packages/component-fixture-manager/src/fixtures/fragments.js @@ -0,0 +1,73 @@ +const Chance = require('chance') +const { + submittingAuthor, + reviewer, + answerReviewer, + recReviewer, +} = require('./userData') +const { standardCollID } = require('./collectionIDs') + +const chance = new Chance() +const fragments = { + fragment: { + id: chance.guid(), + collectionId: standardCollID, + metadata: { + title: chance.sentence(), + abstract: chance.paragraph(), + }, + recommendations: [ + { + recommendation: 'publish', + 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(), + }, + ], + authors: [ + { + userId: submittingAuthor.id, + isSubmitting: true, + isCorresponding: false, + }, + ], + invitations: [ + { + id: chance.guid(), + role: 'reviewer', + hasAnswer: false, + isAccepted: false, + userId: reviewer.id, + invitedOn: chance.timestamp(), + respondedOn: null, + }, + { + id: chance.guid(), + role: 'reviewer', + hasAnswer: true, + isAccepted: false, + userId: answerReviewer.id, + invitedOn: chance.timestamp(), + respondedOn: chance.timestamp(), + }, + ], + save: jest.fn(() => fragments.fragment), + }, +} + +module.exports = fragments diff --git a/packages/component-user-manager/src/tests/fixtures/teamIDs.js b/packages/component-fixture-manager/src/fixtures/teamIDs.js similarity index 79% rename from packages/component-user-manager/src/tests/fixtures/teamIDs.js rename to packages/component-fixture-manager/src/fixtures/teamIDs.js index f8cfc51464701c2ab93b312dd73ed3ea5e2f7ea3..83ce3a556417bc4650efdc9ff478b8bad64a9f13 100644 --- a/packages/component-user-manager/src/tests/fixtures/teamIDs.js +++ b/packages/component-fixture-manager/src/fixtures/teamIDs.js @@ -2,9 +2,11 @@ const Chance = require('chance') const chance = new Chance() const heID = chance.guid() +const revId = chance.guid() const authorID = chance.guid() module.exports = { heTeamID: heID, + revTeamID: revId, authorTeamID: authorID, } diff --git a/packages/component-invite/src/tests/fixtures/teams.js b/packages/component-fixture-manager/src/fixtures/teams.js similarity index 59% rename from packages/component-invite/src/tests/fixtures/teams.js rename to packages/component-fixture-manager/src/fixtures/teams.js index 7e4611da3489cceb1308e9ffaf072a85866f4ca2..84d784b4b4be4c38a4757fccada612705bc8b1b1 100644 --- a/packages/component-invite/src/tests/fixtures/teams.js +++ b/packages/component-fixture-manager/src/fixtures/teams.js @@ -1,8 +1,12 @@ const users = require('./users') const collections = require('./collections') -const { heTeamID, revTeamID } = require('./teamIDs') +const fragments = require('./fragments') + +const { heTeamID, revTeamID, authorTeamID } = require('./teamIDs') +const { submittingAuthor } = require('./userData') const { collection } = collections +const { fragment } = fragments const { handlingEditor, reviewer } = users const teams = { heTeam: { @@ -29,13 +33,29 @@ const teams = { group: 'reviewer', name: 'reviewer', object: { - type: 'collection', - id: collection.id, + type: 'fragment', + id: fragment.id, }, members: [reviewer.id], save: jest.fn(() => teams.revTeam), updateProperties: jest.fn(() => teams.revTeam), id: revTeamID, }, + authorTeam: { + teamType: { + name: 'author', + permissions: 'author', + }, + group: 'author', + name: 'author', + object: { + type: 'fragment', + id: fragment.id, + }, + members: [submittingAuthor.id], + save: jest.fn(() => teams.authorTeam), + updateProperties: jest.fn(() => teams.authorTeam), + id: authorTeamID, + }, } module.exports = teams diff --git a/packages/component-manuscript-manager/src/tests/fixtures/userData.js b/packages/component-fixture-manager/src/fixtures/userData.js similarity index 86% rename from packages/component-manuscript-manager/src/tests/fixtures/userData.js rename to packages/component-fixture-manager/src/fixtures/userData.js index 4e3b994a70b782f25ec85e0f41410824dd4307c4..9920552365178966754bf7681df61eaa7a15878c 100644 --- a/packages/component-manuscript-manager/src/tests/fixtures/userData.js +++ b/packages/component-fixture-manager/src/fixtures/userData.js @@ -15,5 +15,7 @@ module.exports = { author: generateUserData(), reviewer: generateUserData(), answerReviewer: generateUserData(), + submittingAuthor: generateUserData(), recReviewer: generateUserData(), + answerHE: generateUserData(), } diff --git a/packages/component-invite/src/tests/fixtures/users.js b/packages/component-fixture-manager/src/fixtures/users.js similarity index 65% rename from packages/component-invite/src/tests/fixtures/users.js rename to packages/component-fixture-manager/src/fixtures/users.js index c5f0a5e41f8bca54d62c6534ef263a6de77c99bc..29c50f9327b03f6551aca7cbcf48526446726a0f 100644 --- a/packages/component-invite/src/tests/fixtures/users.js +++ b/packages/component-fixture-manager/src/fixtures/users.js @@ -1,4 +1,4 @@ -const { heTeamID, revTeamID } = require('./teamIDs') +const { heTeamID, revTeamID, authorTeamID } = require('./teamIDs') const { handlingEditor, user, @@ -6,6 +6,9 @@ const { author, reviewer, answerReviewer, + submittingAuthor, + recReviewer, + answerHE, } = require('./userData') const Chance = require('chance') @@ -51,6 +54,21 @@ const users = { handlingEditor: true, title: 'Mr', }, + answerHE: { + type: 'user', + username: chance.word(), + email: answerHE.email, + password: 'password', + admin: false, + id: answerHE.id, + firstName: answerHE.firstName, + lastName: answerHE.lastName, + teams: [heTeamID], + save: jest.fn(() => users.answerHE), + editorInChief: false, + handlingEditor: true, + title: 'Mr', + }, user: { type: 'user', username: chance.word(), @@ -65,6 +83,8 @@ const users = { title: 'Mr', save: jest.fn(() => users.user), isConfirmed: false, + updateProperties: jest.fn(() => users.user), + teams: [], }, author: { type: 'user', @@ -79,6 +99,8 @@ const users = { title: 'Mr', save: jest.fn(() => users.author), isConfirmed: true, + passwordResetToken: chance.hash(), + teams: [authorTeamID], }, reviewer: { type: 'user', @@ -112,6 +134,36 @@ const users = { teams: [revTeamID], invitationToken: 'inv-token-123', }, + submittingAuthor: { + type: 'user', + username: 'sauthor', + email: submittingAuthor.email, + password: 'password', + admin: false, + id: submittingAuthor.id, + passwordResetToken: chance.hash(), + firstName: submittingAuthor.firstName, + lastName: submittingAuthor.lastName, + affiliation: chance.company(), + title: 'Mr', + save: jest.fn(() => users.submittingAuthor), + isConfirmed: false, + }, + 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], + }, } module.exports = users diff --git a/packages/component-invite/src/tests/helpers/Model.js b/packages/component-fixture-manager/src/helpers/Model.js similarity index 95% rename from packages/component-invite/src/tests/helpers/Model.js rename to packages/component-fixture-manager/src/helpers/Model.js index df543155a679c4c7e2bb91a8304835c458e38d44..04bdb01f94441d836aa475fc702d818823405fcd 100644 --- a/packages/component-invite/src/tests/helpers/Model.js +++ b/packages/component-fixture-manager/src/helpers/Model.js @@ -1,5 +1,3 @@ -// const fixtures = require('../fixtures/fixtures') - const UserMock = require('../mocks/User') const TeamMock = require('../mocks/Team') @@ -24,6 +22,9 @@ const build = fixtures => { UserMock.findOneByField = jest.fn((field, value) => findOneByFieldMock(field, value, 'users', fixtures), ) + UserMock.updateProperties = jest.fn(user => + updatePropertiesMock(user, 'users'), + ) TeamMock.find = jest.fn(id => findMock(id, 'teams', fixtures)) TeamMock.updateProperties = jest.fn(team => updatePropertiesMock(team, 'teams', fixtures), diff --git a/packages/component-invite/src/tests/mocks/Team.js b/packages/component-fixture-manager/src/mocks/Team.js similarity index 100% rename from packages/component-invite/src/tests/mocks/Team.js rename to packages/component-fixture-manager/src/mocks/Team.js diff --git a/packages/component-invite/src/tests/mocks/User.js b/packages/component-fixture-manager/src/mocks/User.js similarity index 100% rename from packages/component-invite/src/tests/mocks/User.js rename to packages/component-fixture-manager/src/mocks/User.js diff --git a/packages/component-helper-service/src/services/Collection.js b/packages/component-helper-service/src/services/Collection.js index 6e502735b7ae9ce777bbda52be940b4e06abf953..c0fb4fddd1d9e8a79d587f9e53a3c623837f5c2a 100644 --- a/packages/component-helper-service/src/services/Collection.js +++ b/packages/component-helper-service/src/services/Collection.js @@ -40,38 +40,6 @@ class Collection { await this.collection.save() } - async getAuthorData({ UserModel }) { - const { collection: { authors } } = this - const submittingAuthorData = authors.find( - author => author.isSubmitting === true, - ) - const submittingAuthor = await UserModel.find(submittingAuthorData.userId) - const authorsPromises = authors.map(async author => { - const user = await UserModel.find(author.userId) - return `${user.firstName} ${user.lastName}` - }) - const authorsList = await Promise.all(authorsPromises) - - return { - authorsList, - submittingAuthor, - } - } - - getReviewerInvitations({ agree = true }) { - const { collection: { invitations } } = this - return agree - ? invitations.filter( - inv => - inv.role === 'reviewer' && - inv.hasAnswer === true && - inv.isAccepted === true, - ) - : invitations.filter( - inv => inv.role === 'reviewer' && inv.hasAnswer === false, - ) - } - async addHandlingEditor({ user, invitation }) { this.collection.handlingEditor = { id: user.id, diff --git a/packages/component-helper-service/src/services/Email.js b/packages/component-helper-service/src/services/Email.js index 49a106d8d72305dcf029accd2bd0a094a52cab45..c375875f8157884196f93b2b4e5abf75cd730c03 100644 --- a/packages/component-helper-service/src/services/Email.js +++ b/packages/component-helper-service/src/services/Email.js @@ -1,4 +1,4 @@ -const Collection = require('./Collection') +const Fragment = require('./Fragment') const User = require('./User') const get = require('lodash/get') const config = require('config') @@ -29,15 +29,17 @@ class Email { recommendation = {}, isSubmitted = false, agree = false, + FragmentModel, }) { const { UserModel, collection, - parsedFragment: { recommendations, title, type }, + parsedFragment: { recommendations, title, type, id }, authors: { submittingAuthor: { firstName = '', lastName = '' } }, } = this - const collectionHelper = new Collection({ collection }) - const reviewerInvitations = collectionHelper.getReviewerInvitations({ + const fragment = await FragmentModel.find(id) + const fragmentHelper = new Fragment({ fragment }) + const reviewerInvitations = fragmentHelper.getReviewerInvitations({ agree, }) @@ -98,7 +100,11 @@ class Email { ) } - async setupAuthorsEmail({ requestToRevision = false, publish = false }) { + async setupAuthorsEmail({ + requestToRevision = false, + publish = false, + FragmentModel, + }) { const { baseUrl, collection, @@ -123,7 +129,8 @@ class Email { }, ] } else { - toAuthors = collection.authors.map(author => ({ + const fragment = await FragmentModel.find(id) + toAuthors = fragment.authors.map(author => ({ email: author.email, name: `${author.firstName} ${author.lastName}`, })) diff --git a/packages/component-helper-service/src/services/Fragment.js b/packages/component-helper-service/src/services/Fragment.js index d4be48eec23c41fd8e08e2d5695a34a705454fb1..80b38b4b6caafc3a5f96ac36253fa9f44cfd239b 100644 --- a/packages/component-helper-service/src/services/Fragment.js +++ b/packages/component-helper-service/src/services/Fragment.js @@ -21,6 +21,57 @@ class Fragment { heRecommendation, } } + + async addAuthor({ user, isSubmitting, isCorresponding }) { + const { fragment } = this + fragment.authors = fragment.authors || [] + const author = { + id: user.id, + firstName: user.firstName || '', + lastName: user.lastName || '', + email: user.email, + title: user.title || '', + affiliation: user.affiliation || '', + isSubmitting, + isCorresponding, + } + fragment.authors.push(author) + await fragment.save() + + return author + } + + async getAuthorData({ UserModel }) { + const { fragment: { authors } } = this + const submittingAuthorData = authors.find( + author => author.isSubmitting === true, + ) + const submittingAuthor = await UserModel.find(submittingAuthorData.userId) + const authorsPromises = authors.map(async author => { + const user = await UserModel.find(author.userId) + return `${user.firstName} ${user.lastName}` + }) + const authorsList = await Promise.all(authorsPromises) + + return { + authorsList, + submittingAuthor, + } + } + + getReviewerInvitations({ agree = true }) { + const { fragment: { invitations } } = this + return agree + ? invitations.filter( + inv => + inv.role === 'reviewer' && + inv.hasAnswer === true && + inv.isAccepted === true, + ) + : invitations.filter( + inv => inv.role === 'reviewer' && inv.hasAnswer === false, + ) + } } module.exports = Fragment diff --git a/packages/component-helper-service/src/services/Invitation.js b/packages/component-helper-service/src/services/Invitation.js index ac595dabf0ee0f4880a343f29c8c4376c171a1e6..ce06b5ad4d8617a2c97506bc5759cdbb10200906 100644 --- a/packages/component-helper-service/src/services/Invitation.js +++ b/packages/component-helper-service/src/services/Invitation.js @@ -33,7 +33,7 @@ class Invitation { return { invitedOn, respondedOn, status, id } } - async setupInvitation({ collection }) { + async createInvitation({ parentObject }) { const { userId, role } = this const invitation = { role, @@ -44,9 +44,10 @@ class Invitation { userId, respondedOn: null, } - collection.invitations = collection.invitations || [] - collection.invitations.push(invitation) - collection = await collection.save() + parentObject.invitations = parentObject.invitations || [] + parentObject.invitations.push(invitation) + await parentObject.save() + return invitation } @@ -56,6 +57,22 @@ class Invitation { invitation.userId === this.userId && invitation.role === this.role, ) } + + validateInvitation({ invitation }) { + if (invitation === undefined) + return { status: 404, error: 'Invitation not found.' } + + if (invitation.hasAnswer) + return { status: 400, error: 'Invitation has already been answered.' } + + if (invitation.userId !== this.userId) + return { + status: 403, + error: 'User is not allowed to modify this invitation.', + } + + return { error: null } + } } module.exports = Invitation diff --git a/packages/component-helper-service/src/services/Team.js b/packages/component-helper-service/src/services/Team.js index c53eec64bdcdb7fa2b3b08a1749541ca360dd8bc..9d8d6148a8159b24180e22164247fcf3874aa2fa 100644 --- a/packages/component-helper-service/src/services/Team.js +++ b/packages/component-helper-service/src/services/Team.js @@ -99,6 +99,7 @@ class Team { const objectId = objectType === 'collection' ? collectionId : fragmentId const teams = await TeamModel.all() + const members = get( teams.find( team => @@ -124,7 +125,7 @@ class Team { ) } - async updateHETeam({ collection, role, user }) { + async deleteHandlingEditor({ collection, role, user }) { const team = await this.getTeam({ role, objectType: 'collection', diff --git a/packages/component-helper-service/src/services/authsome.js b/packages/component-helper-service/src/services/authsome.js index 212cee2a3ea23a424b1f77dde2f7dd4cf888b2a0..b2215bb20f4d5d50cd0f75fff2607ba79f514443 100644 --- a/packages/component-helper-service/src/services/authsome.js +++ b/packages/component-helper-service/src/services/authsome.js @@ -12,6 +12,7 @@ const getAuthsome = models => models: { Collection: { find: id => models.Collection.find(id), + all: () => models.Collection.all(), }, Fragment: { find: id => models.Fragment.find(id), diff --git a/packages/component-invite/config/authsome-helpers.js b/packages/component-invite/config/authsome-helpers.js index b5f42492b077cee12d5a09d97404fb559e601f5e..164a9ce8ae550d9cd1709543ceb852169220cdc6 100644 --- a/packages/component-invite/config/authsome-helpers.js +++ b/packages/component-invite/config/authsome-helpers.js @@ -14,9 +14,9 @@ const parseAuthorsData = (coll, matchingCollPerm) => { 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 + // coll.visibleStatus = statuses[status].public + if (publicStatusesPermissions.includes(matchingCollPerm.permission)) { + coll.visibleStatus = statuses[status].public } } @@ -44,7 +44,10 @@ const filterObjectData = ( rec => rec.userId === user.id, ) } - + parseAuthorsData(object, matchingCollPerm) + if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) { + return filterRefusedInvitations(object, user) + } return object } const matchingCollPerm = collectionsPermissions.find( @@ -52,22 +55,18 @@ const filterObjectData = ( ) 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 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 + if (!permissions.includes(team.teamType.permissions)) { + return null } - return null + return team }), ) diff --git a/packages/component-invite/config/authsome-mode.js b/packages/component-invite/config/authsome-mode.js index 762998f83e80d5e678595bb8bf7e57072607adbe..8a92a1301df7ea1bed2dc105beb44834cdb9b16f 100644 --- a/packages/component-invite/config/authsome-mode.js +++ b/packages/component-invite/config/authsome-mode.js @@ -4,6 +4,7 @@ const omit = require('lodash/omit') const helpers = require('./authsome-helpers') async function teamPermissions(user, operation, object, context) { + const { models } = context const permissions = ['handlingEditor', 'author', 'reviewer'] const teams = await helpers.getTeamsByPermissions( user.teams, @@ -11,22 +12,38 @@ async function teamPermissions(user, operation, object, context) { context.models.Team, ) - const collectionsPermissions = await Promise.all( + let collectionsPermissions = await Promise.all( teams.map(async team => { - const collection = await context.models.Collection.find(team.object.id) + let collection + if (team.object.type === 'collection') { + collection = await models.Collection.find(team.object.id) + } else if (team.object.type === 'fragment') { + const fragment = await models.Fragment.find(team.object.id) + collection = await models.Collection.find(fragment.collectionId) + } + if ( + collection.status === 'rejected' && + team.teamType.permissions === 'reviewer' + ) + return null 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 + if (objectType === 'fragment') { + if (collection.fragments.includes(object.id)) + collPerm.fragmentId = object.id + else return null + } + if (objectType === 'collection') + if (object.id !== collection.id) return null return collPerm }), ) - - if (collectionsPermissions.length === 0) return {} + collectionsPermissions = collectionsPermissions.filter(cp => cp !== null) + if (collectionsPermissions.length === 0) return false return { filter: filterParam => { @@ -109,11 +126,16 @@ async function authenticatedUser(user, operation, object, context) { return true } - // Allow the authenticated user to GET collections they own - if (operation === 'GET' && object === '/collections/') { - return { - filter: collection => collection.owners.includes(user.id), + // allow authenticate owners full pass for a collection + if (get(object, 'type') === 'collection') { + if (operation === 'PATCH') { + return { + filter: collection => omit(collection, 'filtered'), + } } + if (object.owners.includes(user.id)) return true + const owner = object.owners.find(own => own.id === user.id) + if (owner !== undefined) return true } // Allow owners of a collection to GET its teams, e.g. @@ -170,32 +192,36 @@ async function authenticatedUser(user, operation, object, context) { return false } - // only allow a reviewer to submit and to modify a recommendation + // only allow a reviewer and an HE 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 authsomeObject = get(object, 'authsomeObject') + const teams = await helpers.getTeamsByPermissions( user.teams, - ['reviewer'], + ['reviewer', 'handlingEditor'], context.models.Team, ) + if (teams.length === 0) return false - const matchingTeam = teams.find(team => team.object.id === collection.id) + const matchingTeam = teams.find( + team => team.object.id === authsomeObject.id, + ) + if (matchingTeam) return true return false } - if (user.teams.length !== 0 && operation === 'GET') { + if (user.teams.length !== 0 && ['GET'].includes(operation)) { const permissions = await teamPermissions(user, operation, object, context) if (permissions) { return permissions } + + return false } if (get(object, 'type') === 'fragment') { @@ -206,19 +232,6 @@ async function authenticatedUser(user, operation, object, context) { } } - 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)) { @@ -240,7 +253,7 @@ const authsomeMode = async (userId, operation, object, context) => { 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 && (user.admin || user.editorInChief)) return true if (user) { return authenticatedUser(user, operation, object, context) diff --git a/packages/component-invite/index.js b/packages/component-invite/index.js index 0e21fe5223f0a0a71bebd53d509b0bef3a5e3e02..f9bc6f5a6ef371301ad40dc80c6f06c59b67577f 100644 --- a/packages/component-invite/index.js +++ b/packages/component-invite/index.js @@ -1,5 +1,6 @@ module.exports = { backend: () => app => { require('./src/CollectionsInvitations')(app) + require('./src/FragmentsInvitations')(app) }, } diff --git a/packages/component-invite/src/CollectionsInvitations.js b/packages/component-invite/src/CollectionsInvitations.js index ba6fa2d852bf1a14b39b182297eff50f2c8661b7..117d14f742928f49b3f3704f051705057615a3ba 100644 --- a/packages/component-invite/src/CollectionsInvitations.js +++ b/packages/component-invite/src/CollectionsInvitations.js @@ -14,13 +14,13 @@ const CollectionsInvitations = app => { * @apiParamExample {json} Body * { * "email": "email@example.com", - * "role": "handlingEditor", [acceptedValues: handlingEditor, reviewer] + * "role": "handlingEditor", [acceptedValues: handlingEditor] * } * @apiSuccessExample {json} Success * HTTP/1.1 200 OK * { * "id": "7b2431af-210c-49f9-a69a-e19271066ebd", - * "role": "reviewer", + * "role": "handlingEditor", * "userId": "4c3f8ee1-785b-4adb-87b4-407a27f652c6", * "hasAnswer": false, * "invitedOn": 1525428890167, @@ -43,7 +43,7 @@ const CollectionsInvitations = app => { * @apiGroup CollectionsInvitations * @apiParam {id} collectionId Collection id * @apiParam {id} [invitationId] Invitation id - * @apiParam {String} role The role to search for: handlingEditor, reviewer, author + * @apiParam {String} role The role to search for: handlingEditor * @apiSuccessExample {json} Success * HTTP/1.1 200 OK * [{ @@ -95,7 +95,7 @@ const CollectionsInvitations = app => { * HTTP/1.1 200 OK * { * "id": "7b2431af-210c-49f9-a69a-e19271066ebd", - * "role": "reviewer", + * "role": "handlingEditor", * "userId": "4c3f8ee1-785b-4adb-87b4-407a27f652c6", * "hasAnswer": true, * "invitedOn": 1525428890167, @@ -113,28 +113,6 @@ const CollectionsInvitations = app => { authBearer, require(`${routePath}/patch`)(app.locals.models), ) - /** - * @api {patch} /api/collections/:collectionId/invitations/:invitationId/decline Decline an invitation as a reviewer - * @apiGroup CollectionsInvitations - * @apiParam {collectionId} collectionId Collection id - * @apiParam {invitationId} invitationId Invitation id - * @apiParamExample {json} Body - * { - * "invitationToken": "f2d814f0-67a5-4590-ba4f-6a83565feb4f", - * } - * @apiSuccessExample {json} Success - * HTTP/1.1 200 OK - * {} - * @apiErrorExample {json} Update invitations errors - * HTTP/1.1 403 Forbidden - * HTTP/1.1 400 Bad Request - * HTTP/1.1 404 Not Found - * HTTP/1.1 500 Internal Server Error - */ - app.patch( - `${basePath}/:invitationId/decline`, - require(`${routePath}/decline`)(app.locals.models), - ) } module.exports = CollectionsInvitations diff --git a/packages/component-invite/src/FragmentsInvitations.js b/packages/component-invite/src/FragmentsInvitations.js new file mode 100644 index 0000000000000000000000000000000000000000..7a4d2507424cbeb5a5a0c0357779508fed009a8d --- /dev/null +++ b/packages/component-invite/src/FragmentsInvitations.js @@ -0,0 +1,145 @@ +const bodyParser = require('body-parser') + +const FragmentsInvitations = app => { + app.use(bodyParser.json()) + const basePath = + '/api/collections/:collectionId/fragments/:fragmentId/invitations' + const routePath = './routes/fragmentsInvitations' + const authBearer = app.locals.passport.authenticate('bearer', { + session: false, + }) + /** + * @api {post} /api/collections/:collectionId/fragments/:fragmentId/invitations Invite a user to a fragment + * @apiGroup FragmentsInvitations + * @apiParam {collectionId} collectionId Collection id + * @apiParam {fragmentId} fragmentId Fragment id + * @apiParamExample {json} Body + * { + * "email": "email@example.com", + * "role": "reviewer", [acceptedValues: reviewer] + * } + * @apiSuccessExample {json} Success + * HTTP/1.1 200 OK + * { + * "id": "7b2431af-210c-49f9-a69a-e19271066ebd", + * "role": "reviewer", + * "userId": "4c3f8ee1-785b-4adb-87b4-407a27f652c6", + * "hasAnswer": false, + * "invitedOn": 1525428890167, + * "isAccepted": false, + * "respondedOn": null + * } + * @apiErrorExample {json} Invite user errors + * HTTP/1.1 403 Forbidden + * HTTP/1.1 400 Bad Request + * HTTP/1.1 404 Not Found + * HTTP/1.1 500 Internal Server Error + */ + app.post( + basePath, + authBearer, + require(`${routePath}/post`)(app.locals.models), + ) + /** + * @api {get} /api/collections/:collectionId/fragments/:fragmentId/invitations/[:invitationId]?role=:role List fragment invitations + * @apiGroup FragmentsInvitations + * @apiParam {id} collectionId Collection id + * @apiParam {id} fragmentId Fragment id + * @apiParam {id} [invitationId] Invitation id + * @apiParam {String} role The role to search for: reviewer + * @apiSuccessExample {json} Success + * HTTP/1.1 200 OK + * [{ + * "name": "John Smith", + * "invitedOn": 1525428890167, + * "respondedOn": 1525428890299, + * "email": "email@example.com", + * "status": "pending", + * "invitationId": "1990881" + * }] + * @apiErrorExample {json} List errors + * HTTP/1.1 403 Forbidden + * HTTP/1.1 400 Bad Request + * HTTP/1.1 404 Not Found + */ + app.get( + `${basePath}/:invitationId?`, + authBearer, + require(`${routePath}/get`)(app.locals.models), + ) + /** + * @api {delete} /api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId Delete invitation + * @apiGroup FragmentsInvitations + * @apiParam {collectionId} collectionId Collection id + * @apiParam {fragmentId} fragmentId Fragment id + * @apiParam {invitationId} invitationId Invitation id + * @apiSuccessExample {json} Success + * HTTP/1.1 204 No Content + * @apiErrorExample {json} Delete errors + * HTTP/1.1 403 Forbidden + * HTTP/1.1 404 Not Found + * HTTP/1.1 500 Internal Server Error + */ + app.delete( + `${basePath}/:invitationId`, + authBearer, + require(`${routePath}/delete`)(app.locals.models), + ) + /** + * @api {patch} /api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId Update an invitation + * @apiGroup FragmentsInvitations + * @apiParam {collectionId} collectionId Collection id + * @apiParam {invitationId} invitationId Invitation id + * @apiParam {fragmentId} fragmentId Fragment id + * @apiParamExample {json} Body + * { + * "isAccepted": false, + * "reason": "I am not ready" [optional] + * } + * @apiSuccessExample {json} Success + * HTTP/1.1 200 OK + * { + * "id": "7b2431af-210c-49f9-a69a-e19271066ebd", + * "role": "reviewer", + * "userId": "4c3f8ee1-785b-4adb-87b4-407a27f652c6", + * "hasAnswer": true, + * "invitedOn": 1525428890167, + * "isAccepted": false, + * "respondedOn": 1525428890299 + * } + * @apiErrorExample {json} Update invitations errors + * HTTP/1.1 403 Forbidden + * HTTP/1.1 400 Bad Request + * HTTP/1.1 404 Not Found + * HTTP/1.1 500 Internal Server Error + */ + app.patch( + `${basePath}/:invitationId`, + authBearer, + require(`${routePath}/patch`)(app.locals.models), + ) + /** + * @api {patch} /api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId/decline Decline an invitation as a reviewer + * @apiGroup FragmentsInvitations + * @apiParam {collectionId} collectionId Collection id + * @apiParam {invitationId} invitationId Invitation id + * @apiParamExample {json} Body + * { + * "invitationToken": "f2d814f0-67a5-4590-ba4f-6a83565feb4f", + * } + * @apiSuccessExample {json} Success + * HTTP/1.1 200 OK + * {} + * @apiErrorExample {json} Update invitations errors + * HTTP/1.1 403 Forbidden + * HTTP/1.1 400 Bad Request + * HTTP/1.1 404 Not Found + * HTTP/1.1 500 Internal Server Error + */ + app.patch( + `${basePath}/:invitationId/decline`, + require(`${routePath}/decline`)(app.locals.models), + ) +} + +module.exports = FragmentsInvitations diff --git a/packages/component-invite/src/routes/collectionsInvitations/delete.js b/packages/component-invite/src/routes/collectionsInvitations/delete.js index e3e69c0ecc26e772b0428004df9fa9134010d141..b7c77e4f41780107d756bb3637ed55b45ac0eff9 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/delete.js +++ b/packages/component-invite/src/routes/collectionsInvitations/delete.js @@ -1,14 +1,9 @@ const config = require('config') -const logger = require('@pubsweet/logger') -const last = require('lodash/last') const mailService = require('pubsweet-component-mail-service') const { services, Team, - Email, - Fragment, - Collection, authsome: authsomeHelper, } = require('pubsweet-component-helper-service') @@ -19,7 +14,7 @@ module.exports = models => async (req, res) => { try { const collection = await models.Collection.find(collectionId) - const collectionHelper = new Collection({ collection }) + const authsome = authsomeHelper.getAuthsome(models) const target = { collection, @@ -30,79 +25,48 @@ module.exports = models => async (req, res) => { return res.status(403).json({ error: 'Unauthorized.', }) - const invitation = await collection.invitations.find( + + collection.invitations = collection.invitations || [] + const invitation = collection.invitations.find( invitation => invitation.id === invitationId, ) - if (invitation === undefined) { - res.status(404).json({ + if (!invitation) + return res.status(404).json({ error: `Invitation ${invitationId} not found`, }) - return - } - const team = await teamHelper.getTeamByGroupAndCollection({ + const team = await teamHelper.getTeam({ role: invitation.role, + objectType: 'collection', }) collection.invitations = collection.invitations.filter( inv => inv.id !== invitation.id, ) - if (invitation.role === 'handlingEditor') { - collection.status = 'submitted' - collection.visibleStatus = statuses[collection.status].private - delete collection.handlingEditor - } else if (invitation.role === 'reviewer') { - await collectionHelper.updateStatusByNumberOfReviewers() - } + + collection.status = 'submitted' + collection.visibleStatus = statuses[collection.status].private + delete collection.handlingEditor await collection.save() + await teamHelper.removeTeamMember({ teamId: team.id, userId: invitation.userId, }) + const UserModel = models.User const user = await UserModel.find(invitation.userId) user.teams = user.teams.filter(userTeamId => team.id !== userTeamId) await user.save() - try { - if (invitation.role === 'handlingEditor') { - await mailService.sendSimpleEmail({ - toEmail: user.email, - emailType: 'revoke-handling-editor', - }) - } else if (invitation.role === 'reviewer') { - const collectionHelper = new Collection({ collection }) - const fragment = await models.Fragment.find(last(collection.fragments)) - const fragmentHelper = new Fragment({ fragment }) - const parsedFragment = await fragmentHelper.getFragmentData({ - handlingEditor: collection.handlingEditor, - }) - const baseUrl = services.getBaseUrl(req) - const { - authorsList: authors, - submittingAuthor, - } = await collectionHelper.getAuthorData({ UserModel }) - const emailHelper = new Email({ - UserModel, - collection, - parsedFragment, - baseUrl, - authors, - }) - emailHelper.setupReviewerUnassignEmail({ - user, - authorName: `${submittingAuthor.firstName} ${ - submittingAuthor.lastName - }`, - }) - } - return res.status(200).json({}) - } catch (e) { - logger.error(e.message) - return res.status(500).json({ error: 'Email could not be sent.' }) - } + mailService.sendSimpleEmail({ + toEmail: user.email, + emailType: 'revoke-handling-editor', + }) + + return res.status(200).json({}) } catch (e) { - const notFoundError = await services.handleNotFoundError(e, 'collection') + const notFoundError = await services.handleNotFoundError(e, 'Collection') return res.status(notFoundError.status).json({ error: notFoundError.message, }) diff --git a/packages/component-invite/src/routes/collectionsInvitations/get.js b/packages/component-invite/src/routes/collectionsInvitations/get.js index 18e1e0eb7a8a8c08a0fb5f3c34e220c91c1678c7..db8832dd49cd77e01908d23915ce26e6cc8bfd78 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/get.js +++ b/packages/component-invite/src/routes/collectionsInvitations/get.js @@ -37,16 +37,18 @@ module.exports = models => async (req, res) => { error: 'Unauthorized.', }) - const members = await teamHelper.getTeamMembersByCollection({ + const members = await teamHelper.getTeamMembers({ role, + objectType: 'collection', }) - if (members === undefined) return res.status(200).json([]) + if (!members) return res.status(200).json([]) // TO DO: handle case for when the invitationID is provided + const invitationHelper = new Invitation({ role }) + const membersData = members.map(async member => { const user = await models.User.find(member) - const invitationHelper = new Invitation({ userId: user.id, role }) - + invitationHelper.userId = user.id const { invitedOn, respondedOn, diff --git a/packages/component-invite/src/routes/collectionsInvitations/patch.js b/packages/component-invite/src/routes/collectionsInvitations/patch.js index 69d6302f6184d69ead39341d5867748bec23477d..d28609053609980f7256195dda6e4c98bdc9f78e 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/patch.js +++ b/packages/component-invite/src/routes/collectionsInvitations/patch.js @@ -1,144 +1,89 @@ -const logger = require('@pubsweet/logger') const mailService = require('pubsweet-component-mail-service') -const last = require('lodash/last') const { - Email, - services, - Fragment, - Collection, Team, User, + services, + Collection, + Invitation, } = require('pubsweet-component-helper-service') module.exports = models => async (req, res) => { const { collectionId, invitationId } = req.params const { isAccepted, reason } = req.body - if (!services.checkForUndefinedParams(isAccepted)) { - res.status(400).json({ error: 'Missing parameters.' }) - logger.error('some parameters are missing') - return - } const UserModel = models.User const user = await UserModel.find(req.user) + try { const collection = await models.Collection.find(collectionId) - const invitation = await collection.invitations.find( + collection.invitations = collection.invitations || [] + const invitation = collection.invitations.find( invitation => invitation.id === invitationId, ) - if (invitation === undefined) - return res.status(404).json({ - error: 'Invitation not found.', - }) - if (invitation.hasAnswer) - return res - .status(400) - .json({ error: 'Invitation has already been answered.' }) - if (invitation.userId !== user.id) - return res.status(403).json({ - error: `User is not allowed to modify this invitation.`, + + const invitationHelper = new Invitation({ + userId: user.id, + role: 'handlingEditor', + }) + + const invitationValidation = invitationHelper.validateInvitation({ + invitation, + }) + if (invitationValidation.error) + return res.status(invitationValidation.status).json({ + error: invitationValidation.error, }) const collectionHelper = new Collection({ collection }) - const fragment = await models.Fragment.find(last(collection.fragments)) - const fragmentHelper = new Fragment({ fragment }) - const parsedFragment = await fragmentHelper.getFragmentData({ - handlingEditor: collection.handlingEditor, - }) const baseUrl = services.getBaseUrl(req) - const { - authorsList: authors, - submittingAuthor, - } = await collectionHelper.getAuthorData({ UserModel }) - const emailHelper = new Email({ - UserModel, - collection, - parsedFragment, - baseUrl, - authors, - }) + const teamHelper = new Team({ TeamModel: models.Team, collectionId }) const userHelper = new User({ UserModel }) - if (invitation.role === 'handlingEditor') - await collectionHelper.updateHandlingEditor({ isAccepted }) + await collectionHelper.updateHandlingEditor({ isAccepted }) invitation.respondedOn = Date.now() invitation.hasAnswer = true const eic = await userHelper.getEditorInChief() const toEmail = eic.email - if (isAccepted === true) { - invitation.isAccepted = true - if ( - invitation.role === 'reviewer' && - collection.status === 'reviewersInvited' - ) - await collectionHelper.updateStatus({ newStatus: 'underReview' }) - await collection.save() - try { - if (invitation.role === 'handlingEditor') - await mailService.sendSimpleEmail({ - toEmail, - user, - emailType: 'handling-editor-agreed', - dashboardUrl: baseUrl, - meta: { - collectionId: collection.customId, - }, - }) - if (invitation.role === 'reviewer') - emailHelper.setupReviewerDecisionEmail({ - agree: true, - timestamp: invitation.respondedOn, - user, - authorName: `${submittingAuthor.firstName} ${ - submittingAuthor.lastName - }`, - }) - return res.status(200).json(invitation) - } catch (e) { - logger.error(e) - return res.status(500).json({ error: 'Email could not be sent.' }) - } - } else { - invitation.isAccepted = false - - if (invitation.role === 'handlingEditor') - await teamHelper.updateHETeam({ - collection, - role: invitation.role, - user, - }) - if (reason !== undefined) { - invitation.reason = reason - } + if (isAccepted) { + invitation.isAccepted = true await collection.save() - try { - if (invitation.role === 'handlingEditor') { - await mailService.sendSimpleEmail({ - toEmail, - user, - emailType: 'handling-editor-declined', - meta: { - reason, - collectionId: collection.customId, - }, - }) - } else if (invitation.role === 'reviewer') { - collectionHelper.updateStatusByNumberOfReviewers() - emailHelper.setupReviewerDecisionEmail({ - agree: false, - user, - }) - } - } catch (e) { - logger.error(e) - return res.status(500).json({ error: 'Email could not be sent.' }) - } + mailService.sendSimpleEmail({ + toEmail, + user, + emailType: 'handling-editor-agreed', + dashboardUrl: baseUrl, + meta: { + collectionId: collection.customId, + }, + }) + + return res.status(200).json(invitation) } - user.save() + + await teamHelper.deleteHandlingEditor({ + collection, + role: invitation.role, + user, + }) + + invitation.isAccepted = false + if (reason) invitation.reason = reason + await collection.save() + + mailService.sendSimpleEmail({ + toEmail, + user, + emailType: 'handling-editor-declined', + meta: { + reason, + collectionId: collection.customId, + }, + }) + return res.status(200).json(invitation) } catch (e) { const notFoundError = await services.handleNotFoundError(e, 'collection') diff --git a/packages/component-invite/src/routes/collectionsInvitations/post.js b/packages/component-invite/src/routes/collectionsInvitations/post.js index 3bafaf954c6276637c2195b93ff0651ab53cdb05..3e20a3c89450bce8077a350249b150ac975dcab4 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/post.js +++ b/packages/component-invite/src/routes/collectionsInvitations/post.js @@ -1,49 +1,38 @@ -const config = require('config') -const get = require('lodash/get') -const last = require('lodash/last') const logger = require('@pubsweet/logger') const mailService = require('pubsweet-component-mail-service') const { - Email, services, authsome: authsomeHelper, - Fragment, Collection, Team, Invitation, - User, } = require('pubsweet-component-helper-service') -const configRoles = config.get('roles') - module.exports = models => async (req, res) => { const { email, role } = req.body - if (!services.checkForUndefinedParams(email, role)) { - res.status(400).json({ error: 'Email and role are required' }) - logger.error('User ID and role are missing') - return - } + if (!services.checkForUndefinedParams(email, role)) + return res.status(400).json({ error: 'Email and role are required' }) + + if (role !== 'handlingEditor') + return res.status(400).json({ + error: `Role ${role} is invalid. Only handlingEditor is allowed.`, + }) - if (!configRoles.collection.includes(role)) { - res.status(400).json({ error: `Role ${role} is invalid` }) - logger.error(`invitation attempted on invalid role ${role}`) - return - } const UserModel = models.User const reqUser = await UserModel.find(req.user) if (email === reqUser.email && !reqUser.admin) { logger.error(`${reqUser.email} tried to invite his own email`) - return res.status(400).json({ error: 'Cannot invite yourself' }) + return res.status(400).json({ error: 'Cannot invite yourself.' }) } - const collectionId = get(req, 'params.collectionId') + const { collectionId } = req.params let collection try { collection = await models.Collection.find(collectionId) } catch (e) { - const notFoundError = await services.handleNotFoundError(e, 'collection') + const notFoundError = await services.handleNotFoundError(e, 'Collection') return res.status(notFoundError.status).json({ error: notFoundError.message, }) @@ -61,115 +50,49 @@ module.exports = models => async (req, res) => { }) const collectionHelper = new Collection({ collection }) - const fragment = await models.Fragment.find(last(collection.fragments)) - const fragmentHelper = new Fragment({ fragment }) - const handlingEditor = collection.handlingEditor || {} - const parsedFragment = await fragmentHelper.getFragmentData({ - handlingEditor, - }) const baseUrl = services.getBaseUrl(req) - const { - authorsList: authors, - submittingAuthor, - } = await collectionHelper.getAuthorData({ UserModel }) - const emailHelper = new Email({ - UserModel, - collection, - parsedFragment, - baseUrl, - authors, + + const teamHelper = new Team({ + TeamModel: models.Team, + collectionId, }) - const teamHelper = new Team({ TeamModel: models.Team, collectionId }) const invitationHelper = new Invitation({ role }) try { const user = await UserModel.findByEmail(email) - await teamHelper.setupManuscriptTeam({ user, role }) + await teamHelper.setupTeam({ user, role, objectType: 'collection' }) invitationHelper.userId = user.id let invitation = invitationHelper.getInvitation({ invitations: collection.invitations, }) - let resend = false - if (invitation !== undefined) { + if (invitation) { if (invitation.hasAnswer) return res .status(400) .json({ error: 'User has already replied to a previous invitation.' }) invitation.invitedOn = Date.now() await collection.save() - resend = true } else { - invitation = await invitationHelper.setupInvitation({ - collection, + invitation = await invitationHelper.createInvitation({ + parentObject: collection, }) } - try { - if (role === 'reviewer') { - if (collection.status === 'heAssigned') - await collectionHelper.updateStatus({ newStatus: 'reviewersInvited' }) + invitation.invitedOn = Date.now() + await collection.save() + await collectionHelper.addHandlingEditor({ user, invitation }) - await emailHelper.setupReviewerInvitationEmail({ - user, - invitationId: invitation.id, - timestamp: invitation.invitedOn, - resend, - authorName: `${submittingAuthor.firstName} ${ - submittingAuthor.lastName - }`, - }) - } + mailService.sendSimpleEmail({ + toEmail: user.email, + user, + emailType: 'assign-handling-editor', + dashboardUrl: baseUrl, + }) - if (role === 'handlingEditor') { - invitation.invitedOn = Date.now() - await collection.save() - await collectionHelper.addHandlingEditor({ user, invitation }) - mailService.sendSimpleEmail({ - toEmail: user.email, - user, - emailType: 'assign-handling-editor', - dashboardUrl: baseUrl, - }) - } - return res.status(200).json(invitation) - } catch (e) { - logger.error(e) - return res.status(500).json({ error: 'Email could not be sent.' }) - } + return res.status(200).json(invitation) } catch (e) { - const userHelper = new User({ UserModel }) - if (role === 'reviewer') { - const newUser = await userHelper.setupNewUser({ - url: baseUrl, - role, - invitationType: 'invite', - body: req.body, - }) - if (newUser.error !== undefined) { - return res.status(newUser.status).json({ - error: newUser.message, - }) - } - if (collection.status === 'heAssigned') - await collectionHelper.updateStatus({ newStatus: 'reviewersInvited' }) - await teamHelper.setupManuscriptTeam({ user: newUser, role }) - invitationHelper.userId = newUser.id - const invitation = await invitationHelper.setupInvitation({ - collection, - }) - - await emailHelper.setupReviewerInvitationEmail({ - user: newUser, - invitationId: invitation.id, - timestamp: invitation.invitedOn, - authorName: `${submittingAuthor.firstName} ${ - submittingAuthor.lastName - }`, - }) - return res.status(200).json(invitation) - } - const notFoundError = await services.handleNotFoundError(e, 'user') + const notFoundError = await services.handleNotFoundError(e, 'User') return res.status(notFoundError.status).json({ error: notFoundError.message, }) diff --git a/packages/component-invite/src/routes/collectionsInvitations/decline.js b/packages/component-invite/src/routes/fragmentsInvitations/decline.js similarity index 64% rename from packages/component-invite/src/routes/collectionsInvitations/decline.js rename to packages/component-invite/src/routes/fragmentsInvitations/decline.js index b049a4a0aeb43f096b0ec673b338d91202440cc4..e867503771902281e8686ff0330e2508ca693dfa 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/decline.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/decline.js @@ -1,14 +1,12 @@ -const last = require('lodash/last') - const { services, Email, Fragment, - Collection, + Invitation, } = require('pubsweet-component-helper-service') module.exports = models => async (req, res) => { - const { collectionId, invitationId } = req.params + const { collectionId, invitationId, fragmentId } = req.params const { invitationToken } = req.body if (!services.checkForUndefinedParams(invitationToken)) @@ -21,30 +19,34 @@ module.exports = models => async (req, res) => { invitationToken, ) const collection = await models.Collection.find(collectionId) - const invitation = await collection.invitations.find( + if (!collection.fragments.includes(fragmentId)) + return res.status(400).json({ + error: `Fragment ${fragmentId} does not match collection ${collectionId}`, + }) + const fragment = await models.Fragment.find(fragmentId) + fragment.invitations = fragment.invitations || [] + const invitation = await fragment.invitations.find( invitation => invitation.id === invitationId, ) - if (invitation === undefined) - return res.status(404).json({ - error: `Invitation ${invitationId} not found`, - }) - if (invitation.hasAnswer) - return res - .status(400) - .json({ error: `Invitation has already been answered.` }) - if (invitation.userId !== user.id) - return res.status(403).json({ - error: `User ${user.email} is not allowed to modify invitation ${ - invitation.id - }`, + + const invitationHelper = new Invitation({ + userId: user.id, + role: 'reviewer', + }) + + const invitationValidation = invitationHelper.validateInvitation({ + invitation, + }) + if (invitationValidation.error) + return res.status(invitationValidation.status).json({ + error: invitationValidation.error, }) invitation.respondedOn = Date.now() invitation.hasAnswer = true invitation.isAccepted = false - await collection.save() - const collectionHelper = new Collection({ collection }) - const fragment = await models.Fragment.find(last(collection.fragments)) + await fragment.save() + const fragmentHelper = new Fragment({ fragment }) const parsedFragment = await fragmentHelper.getFragmentData({ handlingEditor: collection.handlingEditor, @@ -53,7 +55,7 @@ module.exports = models => async (req, res) => { const { authorsList: authors, submittingAuthor, - } = await collectionHelper.getAuthorData({ UserModel }) + } = await fragmentHelper.getAuthorData({ UserModel }) const emailHelper = new Email({ UserModel, collection, diff --git a/packages/component-invite/src/routes/fragmentsInvitations/delete.js b/packages/component-invite/src/routes/fragmentsInvitations/delete.js new file mode 100644 index 0000000000000000000000000000000000000000..b4da00bc268d61d124f9ef248f4ad3af04f91ecd --- /dev/null +++ b/packages/component-invite/src/routes/fragmentsInvitations/delete.js @@ -0,0 +1,97 @@ +const { + services, + Team, + Email, + Fragment, + Collection, + authsome: authsomeHelper, +} = require('pubsweet-component-helper-service') + +module.exports = models => async (req, res) => { + const { collectionId, invitationId, fragmentId } = req.params + const teamHelper = new Team({ + TeamModel: models.Team, + collectionId, + fragmentId, + }) + + try { + const collection = await models.Collection.find(collectionId) + if (!collection.fragments.includes(fragmentId)) + return res.status(400).json({ + error: `Fragment ${fragmentId} does not match collection ${collectionId}.`, + }) + const fragment = await models.Fragment.find(fragmentId) + + const collectionHelper = new Collection({ collection }) + + const authsome = authsomeHelper.getAuthsome(models) + const target = { + collection, + path: req.route.path, + } + const canDelete = await authsome.can(req.user, 'DELETE', target) + if (!canDelete) + return res.status(403).json({ + error: 'Unauthorized.', + }) + fragment.invitations = fragment.invitations || [] + const invitation = await fragment.invitations.find( + invitation => invitation.id === invitationId, + ) + if (!invitation) + return res.status(404).json({ + error: `Invitation ${invitationId} not found`, + }) + + const team = await teamHelper.getTeam({ + role: invitation.role, + objectType: 'fragment', + }) + + fragment.invitations = fragment.invitations.filter( + inv => inv.id !== invitation.id, + ) + + await collectionHelper.updateStatusByNumberOfReviewers() + + await teamHelper.removeTeamMember({ + teamId: team.id, + userId: invitation.userId, + }) + + const UserModel = models.User + const user = await UserModel.find(invitation.userId) + user.teams = user.teams.filter(userTeamId => team.id !== userTeamId) + await user.save() + + const fragmentHelper = new Fragment({ fragment }) + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor: collection.handlingEditor, + }) + const baseUrl = services.getBaseUrl(req) + const { + authorsList: authors, + submittingAuthor, + } = await fragmentHelper.getAuthorData({ UserModel }) + const emailHelper = new Email({ + UserModel, + collection, + parsedFragment, + baseUrl, + authors, + }) + + emailHelper.setupReviewerUnassignEmail({ + user, + authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`, + }) + + return res.status(200).json({}) + } catch (e) { + const notFoundError = await services.handleNotFoundError(e, 'collection') + return res.status(notFoundError.status).json({ + error: notFoundError.message, + }) + } +} diff --git a/packages/component-invite/src/routes/fragmentsInvitations/get.js b/packages/component-invite/src/routes/fragmentsInvitations/get.js new file mode 100644 index 0000000000000000000000000000000000000000..2bb8c540e6576951ba8773108036dda02e8f2fee --- /dev/null +++ b/packages/component-invite/src/routes/fragmentsInvitations/get.js @@ -0,0 +1,91 @@ +const config = require('config') +const { + services, + Team, + Invitation, + authsome: authsomeHelper, +} = require('pubsweet-component-helper-service') + +const configRoles = config.get('roles') + +module.exports = models => async (req, res) => { + const { role } = req.query + if (!services.checkForUndefinedParams(role)) { + res.status(400).json({ error: 'Role is required' }) + return + } + + if (!configRoles.collection.includes(role)) { + res.status(400).json({ error: `Role ${role} is invalid` }) + return + } + + const { collectionId, fragmentId } = req.params + const teamHelper = new Team({ + TeamModel: models.Team, + collectionId, + fragmentId, + }) + + try { + const collection = await models.Collection.find(collectionId) + if (!collection.fragments.includes(fragmentId)) + return res.status(400).json({ + error: `Fragment ${fragmentId} does not match collection ${collectionId}.`, + }) + const fragment = await models.Fragment.find(fragmentId) + + const authsome = authsomeHelper.getAuthsome(models) + const target = { + fragment, + path: req.route.path, + } + const canGet = await authsome.can(req.user, 'GET', target) + + if (!canGet) + return res.status(403).json({ + error: 'Unauthorized.', + }) + + const members = await teamHelper.getTeamMembers({ + role, + objectType: 'fragment', + }) + + if (!members) return res.status(200).json([]) + + // TO DO: handle case for when the invitationID is provided + const invitationHelper = new Invitation({ role }) + + const membersData = members.map(async member => { + const user = await models.User.find(member) + invitationHelper.userId = user.id + const { + invitedOn, + respondedOn, + status, + id, + } = invitationHelper.getInvitationsData({ + invitations: fragment.invitations, + }) + + return { + name: `${user.firstName} ${user.lastName}`, + invitedOn, + respondedOn, + email: user.email, + status, + userId: user.id, + invitationId: id, + } + }) + + const resBody = await Promise.all(membersData) + res.status(200).json(resBody) + } catch (e) { + const notFoundError = await services.handleNotFoundError(e, 'Item') + return res.status(notFoundError.status).json({ + error: notFoundError.message, + }) + } +} diff --git a/packages/component-invite/src/routes/fragmentsInvitations/patch.js b/packages/component-invite/src/routes/fragmentsInvitations/patch.js new file mode 100644 index 0000000000000000000000000000000000000000..7ecb11d6b286fd76b69d892e5222c71d424296c3 --- /dev/null +++ b/packages/component-invite/src/routes/fragmentsInvitations/patch.js @@ -0,0 +1,94 @@ +const { + Email, + services, + Fragment, + Collection, + Invitation, +} = require('pubsweet-component-helper-service') + +module.exports = models => async (req, res) => { + const { collectionId, invitationId, fragmentId } = req.params + const { isAccepted, reason } = req.body + + const UserModel = models.User + const user = await UserModel.find(req.user) + try { + const collection = await models.Collection.find(collectionId) + if (!collection.fragments.includes(fragmentId)) + return res.status(400).json({ + error: `Fragment ${fragmentId} does not match collection ${collectionId}`, + }) + const fragment = await models.Fragment.find(fragmentId) + fragment.invitations = fragment.invitations || [] + const invitation = await fragment.invitations.find( + invitation => invitation.id === invitationId, + ) + + const invitationHelper = new Invitation({ + userId: user.id, + role: 'reviewer', + }) + const invitationValidation = invitationHelper.validateInvitation({ + invitation, + }) + if (invitationValidation.error) + return res.status(invitationValidation.status).json({ + error: invitationValidation.error, + }) + + const collectionHelper = new Collection({ collection }) + const fragmentHelper = new Fragment({ fragment }) + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor: collection.handlingEditor, + }) + const baseUrl = services.getBaseUrl(req) + const { + authorsList: authors, + submittingAuthor, + } = await fragmentHelper.getAuthorData({ UserModel }) + const emailHelper = new Email({ + UserModel, + collection, + parsedFragment, + baseUrl, + authors, + }) + + invitation.respondedOn = Date.now() + invitation.hasAnswer = true + if (isAccepted) { + invitation.isAccepted = true + if (collection.status === 'reviewersInvited') + await collectionHelper.updateStatus({ newStatus: 'underReview' }) + await fragment.save() + + emailHelper.setupReviewerDecisionEmail({ + agree: true, + timestamp: invitation.respondedOn, + user, + authorName: `${submittingAuthor.firstName} ${ + submittingAuthor.lastName + }`, + }) + + return res.status(200).json(invitation) + } + + invitation.isAccepted = false + if (reason) invitation.reason = reason + await fragment.save() + + collectionHelper.updateStatusByNumberOfReviewers() + emailHelper.setupReviewerDecisionEmail({ + agree: false, + user, + }) + + return res.status(200).json(invitation) + } catch (e) { + const notFoundError = await services.handleNotFoundError(e, 'Item') + return res.status(notFoundError.status).json({ + error: notFoundError.message, + }) + } +} diff --git a/packages/component-invite/src/routes/fragmentsInvitations/post.js b/packages/component-invite/src/routes/fragmentsInvitations/post.js new file mode 100644 index 0000000000000000000000000000000000000000..38991ec08d033de565d5685d7ab6077b978d8504 --- /dev/null +++ b/packages/component-invite/src/routes/fragmentsInvitations/post.js @@ -0,0 +1,159 @@ +const logger = require('@pubsweet/logger') +const { + Email, + services, + authsome: authsomeHelper, + Fragment, + Collection, + Team, + Invitation, + User, +} = require('pubsweet-component-helper-service') + +module.exports = models => async (req, res) => { + const { email, role } = req.body + + if (!services.checkForUndefinedParams(email, role)) { + res.status(400).json({ error: 'Email and role are required.' }) + logger.error('User ID and role are missing') + return + } + + if (role !== 'reviewer') + return res + .status(400) + .json({ error: `Role ${role} is invalid. Only reviewer is accepted.` }) + + const UserModel = models.User + const reqUser = await UserModel.find(req.user) + + if (email === reqUser.email && !reqUser.admin) + return res.status(400).json({ error: 'Cannot invite yourself.' }) + + const { collectionId, fragmentId } = req.params + let collection, fragment + + try { + collection = await models.Collection.find(collectionId) + if (!collection.fragments.includes(fragmentId)) + return res.status(400).json({ + error: `Fragment ${fragmentId} does not match collection ${collectionId}.`, + }) + fragment = await models.Fragment.find(fragmentId) + } catch (e) { + const notFoundError = await services.handleNotFoundError(e, 'item') + return res.status(notFoundError.status).json({ + error: notFoundError.message, + }) + } + + const authsome = authsomeHelper.getAuthsome(models) + const target = { + collection, + path: req.route.path, + } + const canPost = await authsome.can(req.user, 'POST', target) + if (!canPost) + return res.status(403).json({ + error: 'Unauthorized.', + }) + + const collectionHelper = new Collection({ collection }) + const fragmentHelper = new Fragment({ fragment }) + const handlingEditor = collection.handlingEditor || {} + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor, + }) + const baseUrl = services.getBaseUrl(req) + const { + authorsList: authors, + submittingAuthor, + } = await fragmentHelper.getAuthorData({ UserModel }) + const emailHelper = new Email({ + UserModel, + collection, + parsedFragment, + baseUrl, + authors, + }) + const teamHelper = new Team({ + TeamModel: models.Team, + collectionId, + fragmentId, + }) + const invitationHelper = new Invitation({ role }) + + try { + const user = await UserModel.findByEmail(email) + await teamHelper.setupTeam({ user, role, objectType: 'fragment' }) + invitationHelper.userId = user.id + + let invitation = invitationHelper.getInvitation({ + invitations: fragment.invitations, + }) + let resend = false + + if (invitation) { + if (invitation.hasAnswer) + return res + .status(400) + .json({ error: 'User has already replied to a previous invitation.' }) + + invitation.invitedOn = Date.now() + await fragment.save() + resend = true + } else { + invitation = await invitationHelper.createInvitation({ + parentObject: fragment, + }) + } + + if (collection.status === 'heAssigned') + await collectionHelper.updateStatus({ newStatus: 'reviewersInvited' }) + + emailHelper.setupReviewerInvitationEmail({ + user, + invitationId: invitation.id, + timestamp: invitation.invitedOn, + resend, + authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`, + }) + + return res.status(200).json(invitation) + } catch (e) { + const userHelper = new User({ UserModel }) + + const newUser = await userHelper.setupNewUser({ + url: baseUrl, + role, + invitationType: 'invite', + body: req.body, + }) + + if (newUser.error !== undefined) { + return res.status(newUser.status).json({ + error: newUser.message, + }) + } + + if (collection.status === 'heAssigned') + await collectionHelper.updateStatus({ newStatus: 'reviewersInvited' }) + + await teamHelper.setupTeam({ user: newUser, role, objectType: 'fragment' }) + + invitationHelper.userId = newUser.id + + const invitation = await invitationHelper.createInvitation({ + parentObject: fragment, + }) + + emailHelper.setupReviewerInvitationEmail({ + user: newUser, + invitationId: invitation.id, + timestamp: invitation.invitedOn, + authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`, + }) + + return res.status(200).json(invitation) + } +} diff --git a/packages/component-invite/src/tests/collectionsInvitations/delete.test.js b/packages/component-invite/src/tests/collectionsInvitations/delete.test.js index 4e6594490c6661e1db150e367b42f348997436bb..7fdef707ccadb721ce6099a69001da73ec29c552 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/delete.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/delete.test.js @@ -2,15 +2,15 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true const cloneDeep = require('lodash/cloneDeep') -const Model = require('./../helpers/Model') -const fixtures = require('./../fixtures/fixtures') -const requests = require('./../helpers/requests') +const fixturesService = require('pubsweet-component-fixture-service') +const requests = require('../requests') +const { Model, fixtures } = fixturesService jest.mock('pubsweet-component-mail-service', () => ({ sendSimpleEmail: jest.fn(), })) -const path = '../../routes/collectionsInvitations/delete' +const path = '../routes/collectionsInvitations/delete' const route = { path: '/api/collections/:collectionId/invitations/:invitationId', } @@ -35,7 +35,7 @@ describe('Delete Collections Invitations route handler', () => { }) expect(res.statusCode).toBe(404) const data = JSON.parse(res._getData()) - expect(data.error).toEqual('collection not found') + expect(data.error).toEqual('Collection not found') }) it('should return an error when the invitation does not exist', async () => { const { editorInChief } = testFixtures.users diff --git a/packages/component-invite/src/tests/collectionsInvitations/get.test.js b/packages/component-invite/src/tests/collectionsInvitations/get.test.js index 7cf8a64115d3cfd58db7db627638ee37d296f43d..d630fa94cd909f282e74d0dd442dcd13b46f587d 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/get.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/get.test.js @@ -1,17 +1,17 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true -const fixtures = require('./../fixtures/fixtures') -const Model = require('./../helpers/Model') const cloneDeep = require('lodash/cloneDeep') -const requests = require('./../helpers/requests') +const fixturesService = require('pubsweet-component-fixture-service') +const requests = require('../requests') +const { Model, fixtures } = fixturesService jest.mock('pubsweet-component-mail-service', () => ({ sendSimpleEmail: jest.fn(), sendNotificationEmail: jest.fn(), sendReviewerInvitationEmail: jest.fn(), })) -const path = '../../routes/collectionsInvitations/get' +const path = '../routes/collectionsInvitations/get' const route = { path: '/api/collections/:collectionId/invitations/:invitationId?', } @@ -124,7 +124,7 @@ describe('Get collection invitations route handler', () => { models, path, query: { - role: 'reviewer', + role: 'handlingEditor', }, params: { collectionId: collection.id, diff --git a/packages/component-invite/src/tests/collectionsInvitations/patch.test.js b/packages/component-invite/src/tests/collectionsInvitations/patch.test.js index 129b791c24ae9fdaf09f3f4bcc8573e1abff2985..7ed11f1546f5b45726c79c3ce683c0eeee0f776e 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/patch.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/patch.test.js @@ -2,10 +2,10 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true const httpMocks = require('node-mocks-http') -const fixtures = require('./../fixtures/fixtures') -const Model = require('./../helpers/Model') const cloneDeep = require('lodash/cloneDeep') +const fixturesService = require('pubsweet-component-fixture-service') +const { Model, fixtures } = fixturesService jest.mock('pubsweet-component-mail-service', () => ({ sendSimpleEmail: jest.fn(), sendNotificationEmail: jest.fn(), @@ -41,22 +41,6 @@ describe('Patch collections invitations route handler', () => { await require(patchPath)(models)(req, res) expect(res.statusCode).toBe(200) }) - it('should return success when the reviewer agrees work on a collection', async () => { - const { reviewer } = testFixtures.users - const { collection } = testFixtures.collections - const req = httpMocks.createRequest({ - body, - }) - req.user = reviewer.id - req.params.collectionId = collection.id - const reviewerInv = collection.invitations.find( - inv => inv.role === 'reviewer' && inv.hasAnswer === false, - ) - req.params.invitationId = reviewerInv.id - const res = httpMocks.createResponse() - await require(patchPath)(models)(req, res) - expect(res.statusCode).toBe(200) - }) it('should return success when the handling editor declines work on a collection', async () => { const { handlingEditor } = testFixtures.users const { collection } = testFixtures.collections @@ -75,41 +59,6 @@ describe('Patch collections invitations route handler', () => { expect(res.statusCode).toBe(200) }) - it('should return success when the reviewer declines work on a collection', async () => { - const { reviewer } = testFixtures.users - const { collection } = testFixtures.collections - body.isAccepted = false - const req = httpMocks.createRequest({ - body, - }) - req.user = reviewer.id - req.params.collectionId = collection.id - const inv = collection.invitations.find( - inv => inv.role === 'reviewer' && inv.hasAnswer === false, - ) - req.params.invitationId = inv.id - const res = httpMocks.createResponse() - await require(patchPath)(models)(req, res) - - expect(res.statusCode).toBe(200) - }) - it('should return an error params are missing', async () => { - const { handlingEditor } = testFixtures.users - const { collection } = testFixtures.collections - delete body.isAccepted - const req = httpMocks.createRequest({ - body, - }) - req.user = handlingEditor.id - req.params.collectionId = collection.id - req.params.invitationId = collection.invitations[0].id - const res = httpMocks.createResponse() - await require(patchPath)(models)(req, res) - - expect(res.statusCode).toBe(400) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual('Missing parameters.') - }) it('should return an error if the collection does not exists', async () => { const { handlingEditor } = testFixtures.users const req = httpMocks.createRequest({ diff --git a/packages/component-invite/src/tests/collectionsInvitations/post.test.js b/packages/component-invite/src/tests/collectionsInvitations/post.test.js index 53eda4a07751aebb1120d5c00eadec0a60a87ae3..0608aced450fbf604c14fb2af509309262808ddc 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/post.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/post.test.js @@ -1,15 +1,12 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true -const random = require('lodash/random') -const fixtures = require('./../fixtures/fixtures') const Chance = require('chance') -const Model = require('./../helpers/Model') -const config = require('config') const cloneDeep = require('lodash/cloneDeep') -const requests = require('./../helpers/requests') +const fixturesService = require('pubsweet-component-fixture-service') +const requests = require('../requests') -const configRoles = config.get('roles') +const { Model, fixtures } = fixturesService jest.mock('pubsweet-component-mail-service', () => ({ sendSimpleEmail: jest.fn(), @@ -17,10 +14,9 @@ jest.mock('pubsweet-component-mail-service', () => ({ sendReviewerInvitationEmail: jest.fn(), })) const chance = new Chance() -const roles = configRoles.collection const reqBody = { email: chance.email(), - role: roles[random(0, roles.length - 1)], + role: 'handlingEditor', firstName: chance.first(), lastName: chance.last(), title: 'Mr', @@ -31,7 +27,7 @@ const route = { path: '/api/collections/:collectionId/invitations', } -const path = '../../routes/collectionsInvitations/post' +const path = '../routes/collectionsInvitations/post' describe('Post collections invitations route handler', () => { let testFixtures = {} let body = {} @@ -79,31 +75,8 @@ describe('Post collections invitations route handler', () => { const data = JSON.parse(res._getData()) expect(data.role).toEqual(body.role) }) - it('should return success when the a reviewer is invited', async () => { - const { user, editorInChief } = testFixtures.users - const { collection } = testFixtures.collections - body = { - email: user.email, - role: 'reviewer', - } - const res = await requests.sendRequest({ - body, - userId: editorInChief.id, - route, - models, - path, - params: { - collectionId: collection.id, - }, - }) - - expect(res.statusCode).toBe(200) - const data = JSON.parse(res._getData()) - expect(data.role).toEqual(body.role) - }) it('should return an error when inviting his self', async () => { const { editorInChief } = testFixtures.users - body.role = roles[random(0, roles.length - 1)] body.email = editorInChief.email const res = await requests.sendRequest({ body, @@ -114,7 +87,7 @@ describe('Post collections invitations route handler', () => { }) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) - expect(data.error).toEqual('Cannot invite yourself') + expect(data.error).toEqual('Cannot invite yourself.') }) it('should return an error when the role is invalid', async () => { const { editorInChief } = testFixtures.users @@ -127,7 +100,9 @@ describe('Post collections invitations route handler', () => { path, }) const data = JSON.parse(res._getData()) - expect(data.error).toEqual(`Role ${body.role} is invalid`) + expect(data.error).toEqual( + `Role ${body.role} is invalid. Only handlingEditor is allowed.`, + ) }) it('should return success when the EiC resends an invitation to a handlingEditor with a collection', async () => { const { handlingEditor, editorInChief } = testFixtures.users @@ -152,15 +127,15 @@ describe('Post collections invitations route handler', () => { expect(data.role).toEqual(body.role) }) it('should return an error when the invitation is already answered', async () => { - const { answerReviewer, handlingEditor } = testFixtures.users + const { answerHE, editorInChief } = testFixtures.users const { collection } = testFixtures.collections body = { - email: answerReviewer.email, - role: 'reviewer', + email: answerHE.email, + role: 'handlingEditor', } const res = await requests.sendRequest({ body, - userId: handlingEditor.id, + userId: editorInChief.id, route, models, path, diff --git a/packages/component-invite/src/tests/fixtures/collections.js b/packages/component-invite/src/tests/fixtures/collections.js deleted file mode 100644 index bef6132199981a79b87ea86440146eaab5891883..0000000000000000000000000000000000000000 --- a/packages/component-invite/src/tests/fixtures/collections.js +++ /dev/null @@ -1,68 +0,0 @@ -const Chance = require('chance') -const { - user, - handlingEditor, - author, - reviewer, - answerReviewer, -} = require('./userData') -const { fragment } = require('./fragments') - -const chance = new Chance() -const collections = { - collection: { - id: chance.guid(), - title: chance.sentence(), - type: 'collection', - fragments: [fragment.id], - owners: [user.id], - save: jest.fn(), - authors: [ - { - userId: author.id, - isSubmitting: true, - isCorresponding: false, - }, - ], - invitations: [ - { - id: chance.guid(), - role: 'handlingEditor', - hasAnswer: false, - isAccepted: false, - userId: handlingEditor.id, - invitedOn: chance.timestamp(), - respondedOn: null, - }, - { - id: chance.guid(), - role: 'reviewer', - hasAnswer: false, - isAccepted: false, - userId: reviewer.id, - invitedOn: chance.timestamp(), - respondedOn: null, - }, - { - id: chance.guid(), - role: 'reviewer', - hasAnswer: true, - isAccepted: false, - userId: answerReviewer.id, - invitedOn: chance.timestamp(), - respondedOn: chance.timestamp(), - }, - ], - handlingEditor: { - id: handlingEditor.id, - hasAnswer: false, - isAccepted: false, - email: handlingEditor.email, - invitedOn: chance.timestamp(), - respondedOn: null, - name: `${handlingEditor.firstName} ${handlingEditor.lastName}`, - }, - }, -} - -module.exports = collections diff --git a/packages/component-invite/src/tests/fixtures/fragments.js b/packages/component-invite/src/tests/fixtures/fragments.js deleted file mode 100644 index 08d0eedf3a531c317cbfb79e58a6d1b019413564..0000000000000000000000000000000000000000 --- a/packages/component-invite/src/tests/fixtures/fragments.js +++ /dev/null @@ -1,15 +0,0 @@ -const Chance = require('chance') - -const chance = new Chance() -const fragments = { - fragment: { - id: chance.guid(), - metadata: { - title: chance.sentence(), - abstract: chance.paragraph(), - }, - recommendations: [], - }, -} - -module.exports = fragments diff --git a/packages/component-invite/src/tests/fixtures/teamIDs.js b/packages/component-invite/src/tests/fixtures/teamIDs.js deleted file mode 100644 index 607fd6661b1e7c9848bf13257a0d198f230fab00..0000000000000000000000000000000000000000 --- a/packages/component-invite/src/tests/fixtures/teamIDs.js +++ /dev/null @@ -1,10 +0,0 @@ -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-invite/src/tests/fixtures/userData.js b/packages/component-invite/src/tests/fixtures/userData.js deleted file mode 100644 index 546e2d867998c6de1cab8e10a4df20fb74d5288d..0000000000000000000000000000000000000000 --- a/packages/component-invite/src/tests/fixtures/userData.js +++ /dev/null @@ -1,18 +0,0 @@ -const Chance = require('chance') - -const chance = new Chance() -const generateUserData = () => ({ - id: chance.guid(), - email: chance.email(), - firstName: chance.first(), - lastName: chance.last(), -}) - -module.exports = { - handlingEditor: generateUserData(), - user: generateUserData(), - admin: generateUserData(), - author: generateUserData(), - reviewer: generateUserData(), - answerReviewer: generateUserData(), -} diff --git a/packages/component-invite/src/tests/collectionsInvitations/decline.test.js b/packages/component-invite/src/tests/fragmentsInvitations/decline.test.js similarity index 73% rename from packages/component-invite/src/tests/collectionsInvitations/decline.test.js rename to packages/component-invite/src/tests/fragmentsInvitations/decline.test.js index 562c3d92981611c95a57664e281fe482a0b04fc3..ece17117d96e22a7c4c7cf2288d537c18b4bf788 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/decline.test.js +++ b/packages/component-invite/src/tests/fragmentsInvitations/decline.test.js @@ -2,8 +2,9 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true const httpMocks = require('node-mocks-http') -const fixtures = require('./../fixtures/fixtures') -const Model = require('./../helpers/Model') +const fixturesService = require('pubsweet-component-fixture-service') + +const { Model, fixtures } = fixturesService const cloneDeep = require('lodash/cloneDeep') jest.mock('pubsweet-component-mail-service', () => ({ @@ -13,8 +14,8 @@ jest.mock('pubsweet-component-mail-service', () => ({ const reqBody = { invitationToken: 'inv-token-123', } -const patchPath = '../../routes/collectionsInvitations/decline' -describe('Patch collections invitations route handler', () => { +const patchPath = '../../routes/fragmentsInvitations/decline' +describe('Decline fragments invitations route handler', () => { let testFixtures = {} let body = {} let models @@ -26,12 +27,16 @@ describe('Patch collections invitations route handler', () => { it('should return success when the reviewer declines work on a collection', async () => { const { reviewer } = testFixtures.users const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + const req = httpMocks.createRequest({ body, }) req.user = reviewer.id req.params.collectionId = collection.id - const inv = collection.invitations.find( + req.params.fragmentId = fragment.id + + const inv = fragment.invitations.find( inv => inv.role === 'reviewer' && inv.hasAnswer === false, ) req.params.invitationId = inv.id @@ -42,13 +47,14 @@ describe('Patch collections invitations route handler', () => { it('should return an error params are missing', async () => { const { reviewer } = testFixtures.users const { collection } = testFixtures.collections + delete body.invitationToken const req = httpMocks.createRequest({ body, }) req.user = reviewer.id req.params.collectionId = collection.id - req.params.invitationId = collection.invitations[0].id + const res = httpMocks.createResponse() await require(patchPath)(models)(req, res) @@ -70,32 +76,59 @@ describe('Patch collections invitations route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('item not found') }) + it('should return an error if the fragment does not exists', async () => { + const { reviewer } = testFixtures.users + const { collection } = testFixtures.collections + + const req = httpMocks.createRequest({ + body, + }) + req.user = reviewer.id + req.params.collectionId = collection.id + req.params.fragmentId = 'invalid-id' + + const res = httpMocks.createResponse() + await require(patchPath)(models)(req, res) + + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual( + `Fragment invalid-id does not match collection ${collection.id}`, + ) + }) it('should return an error when the invitation does not exist', async () => { const { user } = testFixtures.users const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments const req = httpMocks.createRequest({ body, }) req.user = user.id req.params.collectionId = collection.id + req.params.fragmentId = fragment.id + req.params.invitationId = 'invalid-id' const res = httpMocks.createResponse() await require(patchPath)(models)(req, res) expect(res.statusCode).toBe(404) const data = JSON.parse(res._getData()) - expect(data.error).toEqual('Invitation invalid-id not found') + expect(data.error).toEqual('Invitation not found.') }) it('should return an error when the token is invalid', async () => { const { reviewer } = testFixtures.users const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + body.invitationToken = 'invalid-token' const req = httpMocks.createRequest({ body, }) req.user = reviewer.id req.params.collectionId = collection.id - const inv = collection.invitations.find( + req.params.fragmentId = fragment.id + + const inv = fragment.invitations.find( inv => inv.role === 'reviewer' && inv.hasAnswer === false, ) req.params.invitationId = inv.id @@ -108,12 +141,16 @@ describe('Patch collections invitations route handler', () => { it('should return an error when the invitation is already answered', async () => { const { reviewer } = testFixtures.users const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + const req = httpMocks.createRequest({ body, }) req.user = reviewer.id req.params.collectionId = collection.id - const inv = collection.invitations.find(inv => inv.hasAnswer) + req.params.fragmentId = fragment.id + + const inv = fragment.invitations.find(inv => inv.hasAnswer) req.params.invitationId = inv.id const res = httpMocks.createResponse() await require(patchPath)(models)(req, res) diff --git a/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js b/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js new file mode 100644 index 0000000000000000000000000000000000000000..8396173f8262ce7698b2a1ddbed0c17b6fcb7b60 --- /dev/null +++ b/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js @@ -0,0 +1,117 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +process.env.SUPPRESS_NO_CONFIG_WARNING = true + +const cloneDeep = require('lodash/cloneDeep') +const fixturesService = require('pubsweet-component-fixture-service') +const requests = require('../requests') + +const { Model, fixtures } = fixturesService +jest.mock('pubsweet-component-mail-service', () => ({ + sendSimpleEmail: jest.fn(), +})) + +const path = '../routes/fragmentsInvitations/delete' +const route = { + path: + '/api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId', +} + +describe('Delete Fragments Invitations route handler', () => { + let testFixtures = {} + let models + beforeEach(() => { + testFixtures = cloneDeep(fixtures) + models = Model.build(testFixtures) + }) + it('should return an error when the collection does not exist', async () => { + const { editorInChief } = testFixtures.users + const res = await requests.sendRequest({ + userId: editorInChief.id, + route, + models, + path, + params: { + collectionId: 'invalid-id', + }, + }) + expect(res.statusCode).toBe(404) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('collection not found') + }) + it('should return an error when the fragment does not exist', async () => { + const { editorInChief } = testFixtures.users + const { collection } = testFixtures.collections + + const res = await requests.sendRequest({ + userId: editorInChief.id, + route, + models, + path, + params: { + collectionId: collection.id, + fragmentId: 'invalid-id', + }, + }) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual( + `Fragment invalid-id does not match collection ${collection.id}.`, + ) + }) + it('should return an error when the invitation does not exist', async () => { + const { editorInChief } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + const res = await requests.sendRequest({ + userId: editorInChief.id, + route, + models, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + invitationId: 'invalid-id', + }, + }) + expect(res.statusCode).toBe(404) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Invitation invalid-id not found') + }) + it('should return success when the collection and invitation exist', async () => { + const { editorInChief } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + const res = await requests.sendRequest({ + userId: editorInChief.id, + route, + models, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + invitationId: fragment.invitations[0].id, + }, + }) + expect(res.statusCode).toBe(200) + }) + it('should return an error when the user does not have invitation rights', async () => { + const { user } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + const res = await requests.sendRequest({ + userId: user.id, + route, + models, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + invitationId: collection.invitations[0].id, + }, + }) + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Unauthorized.') + }) +}) diff --git a/packages/component-invite/src/tests/fragmentsInvitations/get.test.js b/packages/component-invite/src/tests/fragmentsInvitations/get.test.js new file mode 100644 index 0000000000000000000000000000000000000000..043e162fac230e383f769e197ec47ca0ba8debf9 --- /dev/null +++ b/packages/component-invite/src/tests/fragmentsInvitations/get.test.js @@ -0,0 +1,145 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +process.env.SUPPRESS_NO_CONFIG_WARNING = true + +const cloneDeep = require('lodash/cloneDeep') +const fixturesService = require('pubsweet-component-fixture-service') +const requests = require('../requests') + +const { Model, fixtures } = fixturesService +jest.mock('pubsweet-component-mail-service', () => ({ + sendSimpleEmail: jest.fn(), + sendNotificationEmail: jest.fn(), + sendReviewerInvitationEmail: jest.fn(), +})) +const path = '../routes/fragmentsInvitations/get' +const route = { + path: + '/api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId?', +} +describe('Get fragment invitations route handler', () => { + let testFixtures = {} + let models + beforeEach(() => { + testFixtures = cloneDeep(fixtures) + models = Model.build(testFixtures) + }) + it('should return success when the request data is correct', async () => { + const { handlingEditor } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + const res = await requests.sendRequest({ + userId: handlingEditor.id, + route, + models, + path, + query: { + role: 'reviewer', + }, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + expect(data.length).toBeGreaterThan(0) + }) + it('should return an error when parameters are missing', async () => { + const { handlingEditor } = testFixtures.users + const res = await requests.sendRequest({ + userId: handlingEditor.id, + route, + models, + path, + }) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Role is required') + }) + it('should return an error when the collection does not exist', async () => { + const { handlingEditor } = testFixtures.users + const res = await requests.sendRequest({ + userId: handlingEditor.id, + route, + models, + path, + query: { + role: 'reviewer', + }, + params: { + collectionId: 'invalid-id', + }, + }) + expect(res.statusCode).toBe(404) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Item not found') + }) + it('should return an error when the fragment does not exist', async () => { + const { handlingEditor } = testFixtures.users + const { collection } = testFixtures.collections + const res = await requests.sendRequest({ + userId: handlingEditor.id, + route, + models, + path, + query: { + role: 'reviewer', + }, + params: { + collectionId: collection.id, + fragmentId: 'invalid-id', + }, + }) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual( + `Fragment invalid-id does not match collection ${collection.id}.`, + ) + }) + it('should return an error when the role is invalid', async () => { + const { handlingEditor } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + const res = await requests.sendRequest({ + userId: handlingEditor.id, + route, + models, + path, + query: { + role: 'invalidRole', + }, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual(`Role invalidRole is invalid`) + }) + it('should return an error when a user does not have invitation rights', async () => { + const { user } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + const res = await requests.sendRequest({ + userId: user.id, + route, + models, + path, + query: { + role: 'reviewer', + }, + 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-invite/src/tests/fragmentsInvitations/patch.test.js b/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js new file mode 100644 index 0000000000000000000000000000000000000000..93006adf157c73b8eff63846e335c3bdb8d46a23 --- /dev/null +++ b/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js @@ -0,0 +1,160 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +process.env.SUPPRESS_NO_CONFIG_WARNING = true + +const httpMocks = require('node-mocks-http') +const cloneDeep = require('lodash/cloneDeep') +const fixturesService = require('pubsweet-component-fixture-service') + +const { Model, fixtures } = fixturesService +jest.mock('pubsweet-component-mail-service', () => ({ + sendSimpleEmail: jest.fn(), + sendNotificationEmail: jest.fn(), + sendReviewerInvitationEmail: jest.fn(), +})) + +const reqBody = { + isAccepted: true, +} +const patchPath = '../../routes/fragmentsInvitations/patch' +describe('Patch fragments invitations route handler', () => { + let testFixtures = {} + let body = {} + let models + beforeEach(() => { + testFixtures = cloneDeep(fixtures) + body = cloneDeep(reqBody) + models = Model.build(testFixtures) + }) + it('should return success when the reviewer agrees work on a collection', async () => { + const { reviewer } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + const req = httpMocks.createRequest({ + body, + }) + req.user = reviewer.id + req.params.collectionId = collection.id + req.params.fragmentId = fragment.id + const reviewerInv = fragment.invitations.find( + inv => inv.role === 'reviewer' && inv.hasAnswer === false, + ) + req.params.invitationId = reviewerInv.id + const res = httpMocks.createResponse() + await require(patchPath)(models)(req, res) + expect(res.statusCode).toBe(200) + }) + it('should return success when the reviewer declines work on a collection', async () => { + const { reviewer } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + body.isAccepted = false + const req = httpMocks.createRequest({ + body, + }) + req.user = reviewer.id + req.params.collectionId = collection.id + req.params.fragmentId = fragment.id + + const inv = fragment.invitations.find( + inv => inv.role === 'reviewer' && inv.hasAnswer === false, + ) + req.params.invitationId = inv.id + const res = httpMocks.createResponse() + await require(patchPath)(models)(req, res) + + expect(res.statusCode).toBe(200) + }) + it('should return an error if the collection does not exists', async () => { + const { handlingEditor } = testFixtures.users + const req = httpMocks.createRequest({ + body, + }) + req.user = handlingEditor.id + req.params.collectionId = 'invalid-id' + const res = httpMocks.createResponse() + await require(patchPath)(models)(req, res) + + expect(res.statusCode).toBe(404) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Item not found') + }) + it('should return an error when the invitation does not exist', async () => { + const { user } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + const req = httpMocks.createRequest({ + body, + }) + req.user = user.id + req.params.collectionId = collection.id + req.params.fragmentId = fragment.id + req.params.invitationId = 'invalid-id' + const res = httpMocks.createResponse() + await require(patchPath)(models)(req, res) + + expect(res.statusCode).toBe(404) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Invitation not found.') + }) + it('should return an error when the fragment does not exist', async () => { + const { user } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + const req = httpMocks.createRequest({ + body, + }) + req.user = user.id + req.params.collectionId = collection.id + req.params.fragmentId = fragment.id + req.params.invitationId = 'invalid-id' + const res = httpMocks.createResponse() + await require(patchPath)(models)(req, res) + + expect(res.statusCode).toBe(404) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Invitation not found.') + }) + it("should return an error when a user tries to patch another user's invitation", async () => { + const { handlingEditor } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + const req = httpMocks.createRequest({ + body, + }) + req.user = handlingEditor.id + req.params.collectionId = collection.id + req.params.fragmentId = fragment.id + const inv = fragment.invitations.find( + inv => inv.role === 'reviewer' && inv.hasAnswer === false, + ) + req.params.invitationId = inv.id + const res = httpMocks.createResponse() + await require(patchPath)(models)(req, res) + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual(`User is not allowed to modify this invitation.`) + }) + it('should return an error when the invitation is already answered', async () => { + const { handlingEditor } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + const req = httpMocks.createRequest({ + body, + }) + req.user = handlingEditor.id + req.params.collectionId = collection.id + req.params.fragmentId = fragment.id + + const inv = fragment.invitations.find(inv => inv.hasAnswer) + req.params.invitationId = inv.id + const res = httpMocks.createResponse() + await require(patchPath)(models)(req, res) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual(`Invitation has already been answered.`) + }) +}) diff --git a/packages/component-invite/src/tests/fragmentsInvitations/post.test.js b/packages/component-invite/src/tests/fragmentsInvitations/post.test.js new file mode 100644 index 0000000000000000000000000000000000000000..21da70379fe9ba9dc1e06df3802f6a331000a6fb --- /dev/null +++ b/packages/component-invite/src/tests/fragmentsInvitations/post.test.js @@ -0,0 +1,155 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +process.env.SUPPRESS_NO_CONFIG_WARNING = true + +const Chance = require('chance') +const cloneDeep = require('lodash/cloneDeep') +const fixturesService = require('pubsweet-component-fixture-service') +const requests = require('../requests') + +const { Model, fixtures } = fixturesService + +jest.mock('pubsweet-component-mail-service', () => ({ + sendSimpleEmail: jest.fn(), + sendNotificationEmail: jest.fn(), + sendReviewerInvitationEmail: jest.fn(), +})) +const chance = new Chance() +const reqBody = { + email: chance.email(), + role: 'reviewer', + firstName: chance.first(), + lastName: chance.last(), + title: 'Mr', + affiliation: chance.company(), + admin: false, +} +const route = { + path: '/api/collections/:collectionId/fragments/:fragmentId/invitations', +} + +const path = '../routes/fragmentsInvitations/post' +describe('Post fragments invitations route handler', () => { + let testFixtures = {} + let body = {} + let models + beforeEach(() => { + testFixtures = cloneDeep(fixtures) + body = cloneDeep(reqBody) + models = Model.build(testFixtures) + }) + it('should return an error params are missing', async () => { + const { admin } = testFixtures.users + delete body.email + const res = await requests.sendRequest({ + body, + userId: admin.id, + route, + models, + path, + }) + + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Email and role are required.') + }) + it('should return success when the a reviewer is invited', async () => { + const { user, editorInChief } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + body = { + email: user.email, + role: 'reviewer', + } + const res = await requests.sendRequest({ + body, + userId: editorInChief.id, + route, + models, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + expect(data.role).toEqual(body.role) + }) + it('should return an error when inviting his self', async () => { + const { editorInChief } = testFixtures.users + body.email = editorInChief.email + const res = await requests.sendRequest({ + body, + userId: editorInChief.id, + route, + models, + path, + }) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Cannot invite yourself.') + }) + it('should return an error when the role is invalid', async () => { + const { editorInChief } = testFixtures.users + body.role = 'someRandomRole' + const res = await requests.sendRequest({ + body, + userId: editorInChief.id, + route, + models, + path, + }) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual( + `Role ${body.role} is invalid. Only reviewer is accepted.`, + ) + }) + it('should return an error when the invitation is already answered', async () => { + const { answerReviewer, handlingEditor } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + body = { + email: answerReviewer.email, + role: 'reviewer', + } + const res = await requests.sendRequest({ + body, + userId: handlingEditor.id, + route, + models, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual( + `User has already replied to a previous invitation.`, + ) + }) + it('should return an error when the user does not have invitation rights', async () => { + const { author } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + const res = await requests.sendRequest({ + body, + userId: author.id, + route, + models, + 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-invite/src/tests/helpers/requests.js b/packages/component-invite/src/tests/requests.js similarity index 100% rename from packages/component-invite/src/tests/helpers/requests.js rename to packages/component-invite/src/tests/requests.js diff --git a/packages/component-manuscript-manager/config/authsome-helpers.js b/packages/component-manuscript-manager/config/authsome-helpers.js index 1add8d99b7fb8eca56a2abb7bf43088d599e5efc..164a9ce8ae550d9cd1709543ceb852169220cdc6 100644 --- a/packages/component-manuscript-manager/config/authsome-helpers.js +++ b/packages/component-manuscript-manager/config/authsome-helpers.js @@ -44,7 +44,10 @@ const filterObjectData = ( rec => rec.userId === user.id, ) } - + parseAuthorsData(object, matchingCollPerm) + if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) { + return filterRefusedInvitations(object, user) + } return object } const matchingCollPerm = collectionsPermissions.find( @@ -52,10 +55,6 @@ const filterObjectData = ( ) if (matchingCollPerm === undefined) return null setPublicStatuses(object, matchingCollPerm) - parseAuthorsData(object, matchingCollPerm) - if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) { - return filterRefusedInvitations(object, user) - } return object } @@ -64,10 +63,10 @@ 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 + if (!permissions.includes(team.teamType.permissions)) { + return null } - return null + return team }), ) diff --git a/packages/component-manuscript-manager/config/authsome-mode.js b/packages/component-manuscript-manager/config/authsome-mode.js index 667879b274b86a59a28515ed3d595d97e6ad2808..8a92a1301df7ea1bed2dc105beb44834cdb9b16f 100644 --- a/packages/component-manuscript-manager/config/authsome-mode.js +++ b/packages/component-manuscript-manager/config/authsome-mode.js @@ -4,6 +4,7 @@ const omit = require('lodash/omit') const helpers = require('./authsome-helpers') async function teamPermissions(user, operation, object, context) { + const { models } = context const permissions = ['handlingEditor', 'author', 'reviewer'] const teams = await helpers.getTeamsByPermissions( user.teams, @@ -11,22 +12,38 @@ async function teamPermissions(user, operation, object, context) { context.models.Team, ) - const collectionsPermissions = await Promise.all( + let collectionsPermissions = await Promise.all( teams.map(async team => { - const collection = await context.models.Collection.find(team.object.id) + let collection + if (team.object.type === 'collection') { + collection = await models.Collection.find(team.object.id) + } else if (team.object.type === 'fragment') { + const fragment = await models.Fragment.find(team.object.id) + collection = await models.Collection.find(fragment.collectionId) + } + if ( + collection.status === 'rejected' && + team.teamType.permissions === 'reviewer' + ) + return null 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 + if (objectType === 'fragment') { + if (collection.fragments.includes(object.id)) + collPerm.fragmentId = object.id + else return null + } + if (objectType === 'collection') + if (object.id !== collection.id) return null return collPerm }), ) - - if (collectionsPermissions.length === 0) return {} + collectionsPermissions = collectionsPermissions.filter(cp => cp !== null) + if (collectionsPermissions.length === 0) return false return { filter: filterParam => { @@ -109,11 +126,16 @@ async function authenticatedUser(user, operation, object, context) { return true } - // Allow the authenticated user to GET collections they own - if (operation === 'GET' && object === '/collections/') { - return { - filter: collection => collection.owners.includes(user.id), + // allow authenticate owners full pass for a collection + if (get(object, 'type') === 'collection') { + if (operation === 'PATCH') { + return { + filter: collection => omit(collection, 'filtered'), + } } + if (object.owners.includes(user.id)) return true + const owner = object.owners.find(own => own.id === user.id) + if (owner !== undefined) return true } // Allow owners of a collection to GET its teams, e.g. @@ -173,29 +195,33 @@ async function authenticatedUser(user, operation, object, context) { // only allow a reviewer and an HE 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 authsomeObject = get(object, 'authsomeObject') + const teams = await helpers.getTeamsByPermissions( user.teams, ['reviewer', 'handlingEditor'], context.models.Team, ) + if (teams.length === 0) return false - const matchingTeam = teams.find(team => team.object.id === collection.id) + const matchingTeam = teams.find( + team => team.object.id === authsomeObject.id, + ) + if (matchingTeam) return true return false } - if (user.teams.length !== 0 && operation === 'GET') { + if (user.teams.length !== 0 && ['GET'].includes(operation)) { const permissions = await teamPermissions(user, operation, object, context) if (permissions) { return permissions } + + return false } if (get(object, 'type') === 'fragment') { @@ -206,19 +232,6 @@ async function authenticatedUser(user, operation, object, context) { } } - 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)) { @@ -240,7 +253,7 @@ const authsomeMode = async (userId, operation, object, context) => { 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 && (user.admin || user.editorInChief)) return true if (user) { return authenticatedUser(user, operation, object, context) diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js index fc771b67cbee2448a50bbad958be986c0c7b96d7..cda67212c7da2f4733b359596820d85974ef9e68 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js @@ -5,37 +5,16 @@ const { Fragment, Collection, } = require('pubsweet-component-helper-service') -const logger = require('@pubsweet/logger') module.exports = models => async (req, res) => { const { collectionId, fragmentId, recommendationId } = req.params let collection, fragment try { collection = await models.Collection.find(collectionId) - if (!collection.fragments.includes(fragmentId)) { - logger.error( - `Collection ${collectionId} does not contain fragment ${fragmentId}`, - ) + if (!collection.fragments.includes(fragmentId)) return res.status(400).json({ error: `Collection and fragment do not match.`, }) - } - const authsome = authsomeHelper.getAuthsome(models) - const target = { - collection, - path: req.route.path, - } - const UserModel = models.User - const user = await UserModel.find(req.user) - const canPatch = await authsome.can(req.user, 'PATCH', target) - if (!canPatch) { - logger.error( - `User ${req.user} is not allowed to access Collection ${collectionId}`, - ) - return res.status(403).json({ - error: 'Unauthorized.', - }) - } fragment = await models.Fragment.find(fragmentId) const recommendation = fragment.recommendations.find( @@ -43,10 +22,30 @@ module.exports = models => async (req, res) => { ) if (!recommendation) return res.status(404).json({ error: 'Recommendation not found.' }) + if (recommendation.userId !== req.user) return res.status(403).json({ error: 'Unauthorized.', }) + + const authsome = authsomeHelper.getAuthsome(models) + const authsomeObject = + recommendation.recommendationType === 'editorRecommendation' + ? collection + : fragment + const target = { + authsomeObject, + path: req.route.path, + } + const canPatch = await authsome.can(req.user, 'PATCH', target) + if (!canPatch) + return res.status(403).json({ + error: 'Unauthorized.', + }) + + const UserModel = models.User + const user = await UserModel.find(req.user) + Object.assign(recommendation, req.body) recommendation.updatedOn = Date.now() if (req.body.submittedOn) { @@ -56,7 +55,7 @@ module.exports = models => async (req, res) => { }) const baseUrl = services.getBaseUrl(req) const collectionHelper = new Collection({ collection }) - const authors = await collectionHelper.getAuthorData({ + const authors = await fragmentHelper.getAuthorData({ UserModel, }) const email = new Email({ diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js index d5a4a500385cc43e771d01758a78137236320ad7..68a3e796fc609f546537e99901559e80fc465e3c 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js @@ -32,8 +32,10 @@ module.exports = models => async (req, res) => { }) } const authsome = authsomeHelper.getAuthsome(models) + const authsomeObject = + recommendationType === 'editorRecommendation' ? collection : fragment const target = { - collection, + authsomeObject, path: req.route.path, } const canPost = await authsome.can(req.user, 'POST', target) @@ -59,7 +61,7 @@ module.exports = models => async (req, res) => { handlingEditor: collection.handlingEditor, }) const baseUrl = services.getBaseUrl(req) - const authors = await collectionHelper.getAuthorData({ UserModel }) + const authors = await fragmentHelper.getAuthorData({ UserModel }) const email = new Email({ UserModel, collection, @@ -67,7 +69,7 @@ module.exports = models => async (req, res) => { baseUrl, authors, }) - + const FragmentModel = models.Fragment if (reqUser.editorInChief || reqUser.admin) { if (recommendation === 'return-to-handling-editor') collectionHelper.updateStatus({ newStatus: 'reviewCompleted' }) @@ -78,6 +80,7 @@ module.exports = models => async (req, res) => { email.setupAuthorsEmail({ requestToRevision: false, publish: recommendation === 'publish', + FragmentModel, }) email.setupHandlingEditorEmail({ publish: recommendation === 'publish', @@ -87,6 +90,7 @@ module.exports = models => async (req, res) => { recommendation, isSubmitted: true, agree: true, + FragmentModel, }) } } else if (recommendationType === 'editorRecommendation') { @@ -94,8 +98,9 @@ module.exports = models => async (req, res) => { email.setupReviewersEmail({ recommendation, agree: true, + FragmentModel, }) - email.setupReviewersEmail({ agree: false }) + email.setupReviewersEmail({ agree: false, FragmentModel: models.Fragment }) email.setupEiCEmail({ recommendation, comments: newRecommendation.comments, diff --git a/packages/component-manuscript-manager/src/tests/fixtures/fixtures.js b/packages/component-manuscript-manager/src/tests/fixtures/fixtures.js deleted file mode 100644 index cbb5c85cbe056c7dc633db935bf5707764ac40bd..0000000000000000000000000000000000000000 --- a/packages/component-manuscript-manager/src/tests/fixtures/fixtures.js +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 0bc2a06b2cacf0a2a2dcb83ee5202cf9059f3618..0000000000000000000000000000000000000000 --- a/packages/component-manuscript-manager/src/tests/fixtures/fragments.js +++ /dev/null @@ -1,39 +0,0 @@ -const Chance = require('chance') -const { recReviewer } = require('./userData') - -const chance = new Chance() -const fragments = { - fragment: { - id: chance.guid(), - metadata: { - title: chance.sentence(), - 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(), - }, - ], - }, -} - -module.exports = fragments diff --git a/packages/component-manuscript-manager/src/tests/fixtures/teamIDs.js b/packages/component-manuscript-manager/src/tests/fixtures/teamIDs.js deleted file mode 100644 index 607fd6661b1e7c9848bf13257a0d198f230fab00..0000000000000000000000000000000000000000 --- a/packages/component-manuscript-manager/src/tests/fixtures/teamIDs.js +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 1c87e804343747e1dfa7132259bc044c9f39081e..0000000000000000000000000000000000000000 --- a/packages/component-manuscript-manager/src/tests/fixtures/teams.js +++ /dev/null @@ -1,41 +0,0 @@ -const users = require('./users') -const collections = require('./collections') -const { revTeamID, heTeamID } = require('./teamIDs') - -const { collection } = collections -const { reviewer, handlingEditor } = 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, - }, - heTeam: { - teamType: { - name: 'handlingEditor', - permissions: 'handlingEditor', - }, - group: 'handlingEditor', - name: 'HandlingEditor', - object: { - type: 'collection', - id: collection.id, - }, - members: [handlingEditor.id], - save: jest.fn(() => teams.heTeam), - updateProperties: jest.fn(() => teams.heTeam), - id: heTeamID, - }, -} -module.exports = teams diff --git a/packages/component-manuscript-manager/src/tests/fixtures/users.js b/packages/component-manuscript-manager/src/tests/fixtures/users.js deleted file mode 100644 index 573c25543f255e06ffe659e1dcc5bd3feb2d38cb..0000000000000000000000000000000000000000 --- a/packages/component-manuscript-manager/src/tests/fixtures/users.js +++ /dev/null @@ -1,84 +0,0 @@ -const { reviewer, author, recReviewer, handlingEditor } = require('./userData') -const { revTeamID, heTeamID } = require('./teamIDs') - -const Chance = require('chance') - -const chance = new Chance() -const users = { - reviewer: { - type: 'user', - username: chance.word(), - email: reviewer.email, - password: 'password', - admin: false, - id: reviewer.id, - firstName: reviewer.firstName, - lastName: reviewer.lastName, - affiliation: chance.company(), - title: 'Mr', - save: jest.fn(() => users.reviewer), - isConfirmed: true, - 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], - }, - handlingEditor: { - type: 'user', - username: chance.word(), - email: handlingEditor.email, - password: 'password', - admin: false, - id: handlingEditor.id, - firstName: handlingEditor.firstName, - lastName: handlingEditor.lastName, - teams: [heTeamID], - save: jest.fn(() => users.handlingEditor), - editorInChief: false, - handlingEditor: true, - title: 'Mr', - }, - editorInChief: { - type: 'user', - username: chance.word(), - email: chance.email(), - password: 'password', - admin: false, - id: chance.guid(), - firstName: chance.first(), - lastName: chance.last(), - affiliation: chance.company(), - title: 'Mr', - save: jest.fn(() => users.editorInChief), - isConfirmed: false, - editorInChief: true, - }, -} - -module.exports = users 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 0e3c8013b981d741859bcf134f48db99e59f7e33..31755149c0d5f99a086d4360f32214953eac2765 100644 --- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js +++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js @@ -1,12 +1,12 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true -const fixtures = require('./../fixtures/fixtures') const Chance = require('chance') -const Model = require('./../helpers/Model') const cloneDeep = require('lodash/cloneDeep') -const requests = require('./../helpers/requests') +const fixturesService = require('pubsweet-component-fixture-service') +const requests = require('../requests') +const { Model, fixtures } = fixturesService jest.mock('pubsweet-component-mail-service', () => ({ sendNotificationEmail: jest.fn(), })) @@ -29,7 +29,7 @@ const reqBody = { recommendationType: 'review', } -const path = '../../routes/fragmentsRecommendations/patch' +const path = '../routes/fragmentsRecommendations/patch' const route = { path: '/api/collections/:collectionId/fragments/:fragmentId/recommendations/:recommendationId', @@ -132,19 +132,20 @@ describe('Patch fragments recommendations route handler', () => { 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 { user } = testFixtures.users const { collection } = testFixtures.collections const { fragment } = testFixtures.fragments const res = await requests.sendRequest({ body, - userId: author.id, + userId: user.id, models, route, path, params: { collectionId: collection.id, fragmentId: fragment.id, + recommendationId: fragment.recommendations[0].id, }, }) 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 364448eb2425e07b4f0df6ebcb9de904201d0489..30f7ee90ba001ce677c709bb86e2349cc0ba3e3c 100644 --- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js +++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js @@ -1,12 +1,12 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true -const fixtures = require('./../fixtures/fixtures') const Chance = require('chance') -const Model = require('./../helpers/Model') const cloneDeep = require('lodash/cloneDeep') -const requests = require('./../helpers/requests') +const fixturesService = require('pubsweet-component-fixture-service') +const requests = require('../requests') +const { Model, fixtures } = fixturesService const chance = new Chance() jest.mock('pubsweet-component-mail-service', () => ({ sendNotificationEmail: jest.fn(), @@ -29,7 +29,7 @@ const reqBody = { recommendationType: 'review', } -const path = '../../routes/fragmentsRecommendations/post' +const path = '../routes/fragmentsRecommendations/post' const route = { path: '/api/collections/:collectionId/fragments/:fragmentId/recommendations', } diff --git a/packages/component-manuscript-manager/src/tests/helpers/Model.js b/packages/component-manuscript-manager/src/tests/helpers/Model.js deleted file mode 100644 index 3e5a7364b9a7e907530d21ae9575644a69294dbc..0000000000000000000000000000000000000000 --- a/packages/component-manuscript-manager/src/tests/helpers/Model.js +++ /dev/null @@ -1,35 +0,0 @@ -// const fixtures = require('../fixtures/fixtures') - -const UserMock = require('../mocks/User') - -const notFoundError = new Error() -notFoundError.name = 'NotFoundError' -notFoundError.status = 404 - -const build = fixtures => { - const models = { - User: {}, - Collection: { - find: jest.fn(id => findMock(id, 'collections', 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)) - UserMock.all = jest.fn(() => Object.values(fixtures.users)) - models.User = UserMock - return models -} - -const findMock = (id, type, fixtures) => { - const foundObj = Object.values(fixtures[type]).find( - fixtureObj => fixtureObj.id === id, - ) - if (foundObj === undefined) return Promise.reject(notFoundError) - return Promise.resolve(foundObj) -} -module.exports = { build } diff --git a/packages/component-manuscript-manager/src/tests/mocks/User.js b/packages/component-manuscript-manager/src/tests/mocks/User.js deleted file mode 100644 index b337c5f31ce5d71eaadd61ccb95fb1f83eef7d83..0000000000000000000000000000000000000000 --- a/packages/component-manuscript-manager/src/tests/mocks/User.js +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-disable func-names-any */ -const uuid = require('uuid') - -function User(properties) { - this.type = 'user' - this.email = properties.email - this.username = properties.username - this.password = properties.password - this.roles = properties.roles - this.title = properties.title - this.affiliation = properties.affiliation - this.firstName = properties.firstName - this.lastName = properties.lastName - this.admin = properties.admin -} - -User.prototype.save = jest.fn(function saveUser() { - this.id = uuid.v4() - return Promise.resolve(this) -}) - -module.exports = User diff --git a/packages/component-manuscript-manager/src/tests/helpers/requests.js b/packages/component-manuscript-manager/src/tests/requests.js similarity index 100% rename from packages/component-manuscript-manager/src/tests/helpers/requests.js rename to packages/component-manuscript-manager/src/tests/requests.js diff --git a/packages/component-user-manager/config/authsome-helpers.js b/packages/component-user-manager/config/authsome-helpers.js index 1b7642bfca5445c4c1651d8cbfcca8aad45ef8c5..164a9ce8ae550d9cd1709543ceb852169220cdc6 100644 --- a/packages/component-user-manager/config/authsome-helpers.js +++ b/packages/component-user-manager/config/authsome-helpers.js @@ -44,7 +44,10 @@ const filterObjectData = ( rec => rec.userId === user.id, ) } - + parseAuthorsData(object, matchingCollPerm) + if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) { + return filterRefusedInvitations(object, user) + } return object } const matchingCollPerm = collectionsPermissions.find( @@ -52,10 +55,6 @@ const filterObjectData = ( ) if (matchingCollPerm === undefined) return null setPublicStatuses(object, matchingCollPerm) - parseAuthorsData(object, matchingCollPerm) - if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) { - return filterRefusedInvitations(object, user) - } return object } diff --git a/packages/component-user-manager/config/authsome-mode.js b/packages/component-user-manager/config/authsome-mode.js index 948dd93e25476745ef93e4db143ff99f79ff8cec..8a92a1301df7ea1bed2dc105beb44834cdb9b16f 100644 --- a/packages/component-user-manager/config/authsome-mode.js +++ b/packages/component-user-manager/config/authsome-mode.js @@ -4,6 +4,7 @@ const omit = require('lodash/omit') const helpers = require('./authsome-helpers') async function teamPermissions(user, operation, object, context) { + const { models } = context const permissions = ['handlingEditor', 'author', 'reviewer'] const teams = await helpers.getTeamsByPermissions( user.teams, @@ -13,7 +14,13 @@ async function teamPermissions(user, operation, object, context) { let collectionsPermissions = await Promise.all( teams.map(async team => { - const collection = await context.models.Collection.find(team.object.id) + let collection + if (team.object.type === 'collection') { + collection = await models.Collection.find(team.object.id) + } else if (team.object.type === 'fragment') { + const fragment = await models.Fragment.find(team.object.id) + collection = await models.Collection.find(fragment.collectionId) + } if ( collection.status === 'rejected' && team.teamType.permissions === 'reviewer' @@ -188,19 +195,21 @@ async function authenticatedUser(user, operation, object, context) { // only allow a reviewer and an HE 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 authsomeObject = get(object, 'authsomeObject') + const teams = await helpers.getTeamsByPermissions( user.teams, ['reviewer', 'handlingEditor'], context.models.Team, ) + if (teams.length === 0) return false - const matchingTeam = teams.find(team => team.object.id === collection.id) + const matchingTeam = teams.find( + team => team.object.id === authsomeObject.id, + ) + if (matchingTeam) return true return false } diff --git a/packages/component-user-manager/src/tests/fixtures/collections.js b/packages/component-user-manager/src/tests/fixtures/collections.js deleted file mode 100644 index 6c4e02668459f9d42c48b58151ccb87f190dcc77..0000000000000000000000000000000000000000 --- a/packages/component-user-manager/src/tests/fixtures/collections.js +++ /dev/null @@ -1,32 +0,0 @@ -const Chance = require('chance') -const { submittingAuthor } = require('./userData') -const { fragment } = require('./fragments') - -const chance = new Chance() -const collections = { - standardCollection: { - id: chance.guid(), - title: chance.sentence(), - type: 'collection', - fragments: [fragment.id], - owners: [submittingAuthor.id], - authors: [ - { - userId: submittingAuthor.id, - isSubmitting: true, - isCorresponding: false, - }, - ], - save: jest.fn(() => collections.standardCollection), - }, - authorsCollection: { - id: chance.guid(), - title: chance.sentence(), - type: 'collection', - fragments: [], - owners: [submittingAuthor.id], - save: jest.fn(), - }, -} - -module.exports = collections diff --git a/packages/component-user-manager/src/tests/fixtures/fixtures.js b/packages/component-user-manager/src/tests/fixtures/fixtures.js deleted file mode 100644 index 0ea29e85ac3a373a20a0e82377e0b6f3dfeef51e..0000000000000000000000000000000000000000 --- a/packages/component-user-manager/src/tests/fixtures/fixtures.js +++ /dev/null @@ -1,9 +0,0 @@ -const users = require('./users') -const collections = require('./collections') -const teams = require('./teams') - -module.exports = { - users, - collections, - teams, -} diff --git a/packages/component-user-manager/src/tests/fixtures/fragments.js b/packages/component-user-manager/src/tests/fixtures/fragments.js deleted file mode 100644 index 08d0eedf3a531c317cbfb79e58a6d1b019413564..0000000000000000000000000000000000000000 --- a/packages/component-user-manager/src/tests/fixtures/fragments.js +++ /dev/null @@ -1,15 +0,0 @@ -const Chance = require('chance') - -const chance = new Chance() -const fragments = { - fragment: { - id: chance.guid(), - metadata: { - title: chance.sentence(), - abstract: chance.paragraph(), - }, - recommendations: [], - }, -} - -module.exports = fragments diff --git a/packages/component-user-manager/src/tests/fixtures/teams.js b/packages/component-user-manager/src/tests/fixtures/teams.js deleted file mode 100644 index 410b999e3f98f3b196eb28feb5512c744e5b61ea..0000000000000000000000000000000000000000 --- a/packages/component-user-manager/src/tests/fixtures/teams.js +++ /dev/null @@ -1,25 +0,0 @@ -const users = require('./users') -const collections = require('./collections') -const { authorTeamID } = require('./teamIDs') - -const { standardCollection } = collections -const { submittingAuthor } = users -const teams = { - authorTeam: { - teamType: { - name: 'author', - permissions: 'author', - }, - group: 'author', - name: 'author', - object: { - type: 'collection', - id: standardCollection.id, - }, - members: [submittingAuthor.id], - save: jest.fn(() => teams.authorTeam), - updateProperties: jest.fn(() => teams.authorTeam), - id: authorTeamID, - }, -} -module.exports = teams diff --git a/packages/component-user-manager/src/tests/fixtures/userData.js b/packages/component-user-manager/src/tests/fixtures/userData.js deleted file mode 100644 index 1a962c33dc4915763c259edae6deadfab9911f98..0000000000000000000000000000000000000000 --- a/packages/component-user-manager/src/tests/fixtures/userData.js +++ /dev/null @@ -1,22 +0,0 @@ -const Chance = require('chance') - -const chance = new Chance() - -module.exports = { - author: { - id: chance.guid(), - email: chance.email(), - firstName: chance.first(), - lastName: chance.last(), - }, - submittingAuthor: { - id: chance.guid(), - email: chance.email(), - firstName: chance.first(), - lastName: chance.last(), - }, - admin: { - id: chance.guid(), - email: chance.email(), - }, -} diff --git a/packages/component-user-manager/src/tests/fixtures/users.js b/packages/component-user-manager/src/tests/fixtures/users.js deleted file mode 100644 index 654032921308157e12248205373970f595d0d497..0000000000000000000000000000000000000000 --- a/packages/component-user-manager/src/tests/fixtures/users.js +++ /dev/null @@ -1,49 +0,0 @@ -const { authorTeamID } = require('./teamIDs') -const { author, submittingAuthor, admin } = require('./userData') -const Chance = require('chance') - -const chance = new Chance() -const users = { - admin: { - type: 'user', - username: 'admin', - email: admin.email, - password: 'password', - admin: true, - id: admin.id, - }, - author: { - type: 'user', - username: 'author', - email: author.email, - password: 'password', - admin: false, - id: author.id, - passwordResetToken: chance.hash(), - firstName: author.firstName, - lastName: author.lastName, - affiliation: 'MIT', - title: 'Mr', - save: jest.fn(() => users.author), - isConfirmed: false, - teams: [authorTeamID], - updateProperties: jest.fn(() => users.author), - }, - submittingAuthor: { - type: 'user', - username: 'sauthor', - email: submittingAuthor.email, - password: 'password', - admin: false, - id: submittingAuthor.id, - passwordResetToken: chance.hash(), - firstName: submittingAuthor.firstName, - lastName: submittingAuthor.lastName, - affiliation: chance.company(), - title: 'Mr', - save: jest.fn(() => users.submittingAuthor), - isConfirmed: false, - }, -} - -module.exports = users diff --git a/packages/component-user-manager/src/tests/fragmentsUsers/delete.test.js b/packages/component-user-manager/src/tests/fragmentsUsers/delete.test.js index 7242ac5c76c8b75ce0e265ca9ef98981490fe1a0..5bea3f85277e6cadf09fd9ff8b31ae5bbb15df7f 100644 --- a/packages/component-user-manager/src/tests/fragmentsUsers/delete.test.js +++ b/packages/component-user-manager/src/tests/fragmentsUsers/delete.test.js @@ -1,29 +1,37 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true +const cloneDeep = require('lodash/cloneDeep') const httpMocks = require('node-mocks-http') -const fixtures = require('./../fixtures/fixtures') -const Model = require('./../helpers/Model') +const fixturesService = require('pubsweet-component-fixture-service') -const models = Model.build() +const { Model, fixtures } = fixturesService const { author, submittingAuthor } = fixtures.users -const { standardCollection } = fixtures.collections -const { authorTeam } = fixtures.teams -const deletePath = '../../routes/collectionsUsers/delete' - -describe('Delete collections users route handler', () => { +const { collection } = fixtures.collections +const deletePath = '../../routes/fragmentsUsers/delete' +jest.mock('pubsweet-component-mail-service', () => ({ + sendSimpleEmail: jest.fn(), + sendNotificationEmail: jest.fn(), +})) +describe('Delete fragments users route handler', () => { + let testFixtures = {} + let models + beforeEach(() => { + testFixtures = cloneDeep(fixtures) + models = Model.build(testFixtures) + }) it('should return success when an author is deleted', async () => { const req = httpMocks.createRequest({}) req.user = submittingAuthor.id - req.params.collectionId = standardCollection.id + req.params.collectionId = collection.id + const [fragmentId] = collection.fragments + req.params.fragmentId = fragmentId req.params.userId = author.id const res = httpMocks.createResponse() await require(deletePath)(models)(req, res) expect(res.statusCode).toBe(200) - expect(authorTeam.members).not.toContain(author.id) - expect(author.teams).not.toContain(authorTeam.id) }) it('should return an error when the collection does not exist', async () => { const req = httpMocks.createRequest({}) @@ -40,7 +48,9 @@ describe('Delete collections users route handler', () => { it('should return an error when the user does not exist', async () => { const req = httpMocks.createRequest({}) req.user = submittingAuthor.id - req.params.collectionId = standardCollection.id + req.params.collectionId = collection.id + const [fragmentId] = collection.fragments + req.params.fragmentId = fragmentId req.params.userId = 'invalid-id' const res = httpMocks.createResponse() await require(deletePath)(models)(req, res) @@ -49,4 +59,19 @@ describe('Delete collections users route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('item not found') }) + it('should return an error when the fragment does not exist', async () => { + const req = httpMocks.createRequest() + req.user = submittingAuthor.id + req.params.collectionId = collection.id + req.params.fragmentId = 'invalid-fragment-id' + req.params.userId = author.id + const res = httpMocks.createResponse() + await require(deletePath)(models)(req, res) + + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual( + `Fragment invalid-fragment-id does not match collection ${collection.id}`, + ) + }) }) diff --git a/packages/component-user-manager/src/tests/fragmentsUsers/get.test.js b/packages/component-user-manager/src/tests/fragmentsUsers/get.test.js index b5975b4ed8d941cd760c22b4df9ec729c41e8ab7..66b2292a09acfe684c44feacc35933420ce21a65 100644 --- a/packages/component-user-manager/src/tests/fragmentsUsers/get.test.js +++ b/packages/component-user-manager/src/tests/fragmentsUsers/get.test.js @@ -1,21 +1,34 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true +const cloneDeep = require('lodash/cloneDeep') const httpMocks = require('node-mocks-http') -const fixtures = require('./../fixtures/fixtures') -const Model = require('./../helpers/Model') +const fixturesService = require('pubsweet-component-fixture-service') -const { standardCollection } = fixtures.collections +const { Model, fixtures } = fixturesService + +const { collection } = fixtures.collections const { submittingAuthor } = fixtures.users -const getPath = '../../routes/collectionsUsers/get' -describe('Get collections users route handler', () => { +jest.mock('pubsweet-component-mail-service', () => ({ + sendSimpleEmail: jest.fn(), + sendNotificationEmail: jest.fn(), +})) +const getPath = '../../routes/fragmentsUsers/get' +describe('Get fragments users route handler', () => { + let testFixtures = {} + let models + beforeEach(() => { + testFixtures = cloneDeep(fixtures) + models = Model.build(testFixtures) + }) it('should return success when the request data is correct', async () => { const req = httpMocks.createRequest() - req.params.collectionId = standardCollection.id + req.params.collectionId = collection.id + const [fragmentId] = collection.fragments + req.params.fragmentId = fragmentId req.user = submittingAuthor.id const res = httpMocks.createResponse() - const models = Model.build() await require(getPath)(models)(req, res) expect(res.statusCode).toBe(200) @@ -29,10 +42,23 @@ describe('Get collections users route handler', () => { req.params.collectionId = 'invalid-id' req.user = submittingAuthor.id const res = httpMocks.createResponse() - const models = Model.build() await require(getPath)(models)(req, res) expect(res.statusCode).toBe(404) const data = JSON.parse(res._getData()) - expect(data.error).toEqual('collection not found') + expect(data.error).toEqual('item not found') + }) + it('should return an error when the fragment does not exist', async () => { + const req = httpMocks.createRequest() + req.user = submittingAuthor.id + req.params.collectionId = collection.id + req.params.fragmentId = 'invalid-fragment-id' + const res = httpMocks.createResponse() + await require(getPath)(models)(req, res) + + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual( + `Fragment invalid-fragment-id does not match collection ${collection.id}`, + ) }) }) diff --git a/packages/component-user-manager/src/tests/fragmentsUsers/patch.test.js b/packages/component-user-manager/src/tests/fragmentsUsers/patch.test.js deleted file mode 100644 index 03c35f392d843bcf12c2b4859c656017a311c0a7..0000000000000000000000000000000000000000 --- a/packages/component-user-manager/src/tests/fragmentsUsers/patch.test.js +++ /dev/null @@ -1,119 +0,0 @@ -process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' -process.env.SUPPRESS_NO_CONFIG_WARNING = true - -const httpMocks = require('node-mocks-http') -const fixtures = require('./../fixtures/fixtures') -const Model = require('./../helpers/Model') -const Chance = require('chance') - -const chance = new Chance() - -const models = Model.build() -jest.mock('pubsweet-component-mail-service', () => ({ - sendSimpleEmail: jest.fn(), - sendNotificationEmail: jest.fn(), -})) - -const { author, submittingAuthor } = fixtures.users -const { standardCollection, authorsCollection } = fixtures.collections -const body = { - isSubmitting: false, - isCorresponding: true, - firstName: chance.first(), - lastName: chance.last(), - affiliation: chance.company(), -} -const patchPath = '../../routes/collectionsUsers/patch' -describe('Patch collections users route handler', () => { - it('should return success when the request data is correct', async () => { - const req = httpMocks.createRequest({ - body, - }) - req.user = submittingAuthor.id - req.params.collectionId = standardCollection.id - req.params.userId = submittingAuthor.id - const res = httpMocks.createResponse() - await require(patchPath)(models)(req, res) - const data = JSON.parse(res._getData()) - expect(res.statusCode).toBe(200) - const matchingAuthor = data.authors.find( - author => author.userId === submittingAuthor.id, - ) - expect(matchingAuthor.isSubmitting).toBe(body.isSubmitting) - expect(matchingAuthor.isCorresponding).toBe(body.isCorresponding) - expect(submittingAuthor.firstName).toBe(body.firstName) - expect(submittingAuthor.lastName).toBe(body.lastName) - }) - it('should return an error when the params are missing', async () => { - delete body.isSubmitting - const req = httpMocks.createRequest({ - body, - }) - req.user = submittingAuthor.id - req.params.collectionId = standardCollection.id - req.params.userId = submittingAuthor.id - const res = httpMocks.createResponse() - await require(patchPath)(models)(req, res) - - expect(res.statusCode).toBe(400) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual('Missing parameters') - body.isSubmitting = false - }) - it('should return an error if the collection does not exists', async () => { - const req = httpMocks.createRequest({ - body, - }) - req.user = submittingAuthor.id - req.params.collectionId = 'invalid-id' - req.params.userId = submittingAuthor.id - const res = httpMocks.createResponse() - await require(patchPath)(models)(req, res) - - expect(res.statusCode).toBe(404) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual('item not found') - }) - it('should return an error when the user does not exist', async () => { - const req = httpMocks.createRequest({ - body, - }) - req.user = author.id - req.params.collectionId = standardCollection.id - req.params.userId = 'invalid-id' - const res = httpMocks.createResponse() - await require(patchPath)(models)(req, res) - - expect(res.statusCode).toBe(404) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual('item not found') - }) - it('should return an error when the collection does not have authors', async () => { - const req = httpMocks.createRequest({ - body, - }) - req.user = submittingAuthor.id - req.params.collectionId = authorsCollection.id - req.params.userId = submittingAuthor.id - const res = httpMocks.createResponse() - await require(patchPath)(models)(req, res) - - expect(res.statusCode).toBe(400) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual('Collection does not have any authors') - }) - it('should return an error when the collection and the user do not match', async () => { - const req = httpMocks.createRequest({ - body, - }) - req.user = submittingAuthor.id - req.params.collectionId = standardCollection.id - req.params.userId = author.id - const res = httpMocks.createResponse() - await require(patchPath)(models)(req, res) - - expect(res.statusCode).toBe(400) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual('Collection and user do not match') - }) -}) diff --git a/packages/component-user-manager/src/tests/fragmentsUsers/post.test.js b/packages/component-user-manager/src/tests/fragmentsUsers/post.test.js index b826f58d2a6fd26c78cac03392dc301e05ee8fca..42ceb286f20f187bde72a6e523527f8d42b85ae9 100644 --- a/packages/component-user-manager/src/tests/fragmentsUsers/post.test.js +++ b/packages/component-user-manager/src/tests/fragmentsUsers/post.test.js @@ -2,10 +2,11 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true const httpMocks = require('node-mocks-http') -const fixtures = require('./../fixtures/fixtures') const Chance = require('chance') -const Model = require('./../helpers/Model') const cloneDeep = require('lodash/cloneDeep') +const fixturesService = require('pubsweet-component-fixture-service') + +const { Model, fixtures } = fixturesService jest.mock('pubsweet-component-mail-service', () => ({ sendSimpleEmail: jest.fn(), @@ -14,7 +15,7 @@ jest.mock('pubsweet-component-mail-service', () => ({ const chance = new Chance() const { author, submittingAuthor } = fixtures.users -const { standardCollection } = fixtures.collections +const { collection } = fixtures.collections const postPath = '../../routes/fragmentsUsers/post' const reqBody = { email: chance.email(), @@ -36,8 +37,8 @@ describe('Post fragments users route handler', () => { body, }) req.user = submittingAuthor.id - req.params.collectionId = standardCollection.id - const [fragmentId] = standardCollection.fragments + req.params.collectionId = collection.id + const [fragmentId] = collection.fragments req.params.fragmentId = fragmentId const res = httpMocks.createResponse() await require(postPath)(models)(req, res) @@ -53,8 +54,8 @@ describe('Post fragments users route handler', () => { body, }) req.user = submittingAuthor.id - req.params.collectionId = standardCollection.id - const [fragmentId] = standardCollection.fragments + req.params.collectionId = collection.id + const [fragmentId] = collection.fragments req.params.fragmentId = fragmentId const res = httpMocks.createResponse() await require(postPath)(models)(req, res) @@ -69,8 +70,8 @@ describe('Post fragments users route handler', () => { body, }) req.user = submittingAuthor.id - req.params.collectionId = standardCollection.id - const [fragmentId] = standardCollection.fragments + req.params.collectionId = collection.id + const [fragmentId] = collection.fragments req.params.fragmentId = fragmentId const res = httpMocks.createResponse() await require(postPath)(models)(req, res) @@ -87,8 +88,8 @@ describe('Post fragments users route handler', () => { body, }) req.user = submittingAuthor.id - req.params.collectionId = standardCollection.id - const [fragmentId] = standardCollection.fragments + req.params.collectionId = collection.id + const [fragmentId] = collection.fragments req.params.fragmentId = fragmentId const res = httpMocks.createResponse() await require(postPath)(models)(req, res) @@ -104,8 +105,8 @@ describe('Post fragments users route handler', () => { body, }) req.user = submittingAuthor.id - req.params.collectionId = standardCollection.id - const [fragmentId] = standardCollection.fragments + req.params.collectionId = collection.id + const [fragmentId] = collection.fragments req.params.fragmentId = fragmentId const res = httpMocks.createResponse() await require(postPath)(models)(req, res) @@ -121,8 +122,8 @@ describe('Post fragments users route handler', () => { body, }) req.user = submittingAuthor.id - req.params.collectionId = standardCollection.id - const [fragmentId] = standardCollection.fragments + req.params.collectionId = collection.id + const [fragmentId] = collection.fragments req.params.fragmentId = fragmentId const res = httpMocks.createResponse() await require(postPath)(models)(req, res) @@ -131,4 +132,21 @@ describe('Post fragments users route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('invalid-role is not defined') }) + it('should return an error when the fragment does not exist', async () => { + const req = httpMocks.createRequest({ + body, + }) + req.user = submittingAuthor.id + req.params.collectionId = collection.id + req.params.fragmentId = 'invalid-fragment-id' + req.params.userId = author.id + const res = httpMocks.createResponse() + await require(postPath)(models)(req, res) + + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual( + `Fragment invalid-fragment-id does not match collection ${collection.id}`, + ) + }) }) diff --git a/packages/component-user-manager/src/tests/helpers/Model.js b/packages/component-user-manager/src/tests/helpers/Model.js deleted file mode 100644 index e5f31fa11df30b229f84e129d41c33062f7b6d1d..0000000000000000000000000000000000000000 --- a/packages/component-user-manager/src/tests/helpers/Model.js +++ /dev/null @@ -1,61 +0,0 @@ -const fixtures = require('../fixtures/fixtures') - -const UserMock = require('../mocks/User') -const TeamMock = require('../mocks/Team') - -const notFoundError = new Error() -notFoundError.name = 'NotFoundError' -notFoundError.status = 404 - -const build = () => { - const models = { - User: {}, - Collection: { - find: jest.fn(id => findMock(id, 'collections')), - }, - Team: {}, - } - UserMock.find = jest.fn(id => findMock(id, 'users')) - UserMock.findByEmail = jest.fn(email => findByEmailMock(email)) - UserMock.all = jest.fn(() => Object.values(fixtures.users)) - UserMock.updateProperties = jest.fn(user => - updatePropertiesMock(user, 'users'), - ) - TeamMock.find = jest.fn(id => findMock(id, 'teams')) - TeamMock.updateProperties = jest.fn(team => - updatePropertiesMock(team, 'teams'), - ) - TeamMock.all = jest.fn(() => Object.values(fixtures.teams)) - - models.User = UserMock - models.Team = TeamMock - return models -} - -const findMock = (id, type) => { - const foundObj = Object.values(fixtures[type]).find( - fixtureObj => fixtureObj.id === id, - ) - - if (foundObj === undefined) return Promise.reject(notFoundError) - return Promise.resolve(foundObj) -} - -const findByEmailMock = email => { - const foundUser = Object.values(fixtures.users).find( - fixtureUser => fixtureUser.email === email, - ) - - if (foundUser === undefined) return Promise.reject(notFoundError) - return Promise.resolve(foundUser) -} - -const updatePropertiesMock = (obj, type) => { - const foundObj = Object.values(fixtures[type]).find( - fixtureObj => fixtureObj === obj, - ) - - if (foundObj === undefined) return Promise.reject(notFoundError) - return Promise.resolve(foundObj) -} -module.exports = { build } diff --git a/packages/component-user-manager/src/tests/mocks/Team.js b/packages/component-user-manager/src/tests/mocks/Team.js deleted file mode 100644 index f84ef4dcf8ffa0e5056c8f0eb86573f624d74c06..0000000000000000000000000000000000000000 --- a/packages/component-user-manager/src/tests/mocks/Team.js +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable func-names-any */ - -function Team(properties) { - this.teamType = properties.teamType - this.group = properties.group - this.name = properties.name - this.object = properties.object - this.members = properties.members -} - -Team.prototype.save = jest.fn(function saveTeam() { - this.id = '111222' - return Promise.resolve(this) -}) - -module.exports = Team diff --git a/packages/component-user-manager/src/tests/mocks/User.js b/packages/component-user-manager/src/tests/mocks/User.js deleted file mode 100644 index 9a7459fc5578d3aa1d5dcec8562fa8f17225b1eb..0000000000000000000000000000000000000000 --- a/packages/component-user-manager/src/tests/mocks/User.js +++ /dev/null @@ -1,21 +0,0 @@ -/* eslint-disable func-names-any */ - -function User(properties) { - this.type = 'user' - this.email = properties.email - this.username = properties.username - this.password = properties.password - this.roles = properties.roles - this.title = properties.title - this.affiliation = properties.affiliation - this.firstName = properties.firstName - this.lastName = properties.lastName - this.admin = properties.admin -} - -User.prototype.save = jest.fn(function saveUser() { - this.id = '111222' - return Promise.resolve(this) -}) - -module.exports = User diff --git a/packages/component-user-manager/src/tests/users/resetPassword.test.js b/packages/component-user-manager/src/tests/users/resetPassword.test.js index f2a7522d2874cd428b57896a853943fe8dcb6d9b..12d6e07bc25a0e4a03737d08f09519c1a443d659 100644 --- a/packages/component-user-manager/src/tests/users/resetPassword.test.js +++ b/packages/component-user-manager/src/tests/users/resetPassword.test.js @@ -1,25 +1,27 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true -const httpMocks = require('node-mocks-http') -const fixtures = require('./../fixtures/fixtures') const Chance = require('chance') -const Model = require('./../helpers/Model') -const clone = require('lodash/cloneDeep') +const httpMocks = require('node-mocks-http') +const cloneDeep = require('lodash/cloneDeep') +const fixturesService = require('pubsweet-component-fixture-service') +const { Model, fixtures } = fixturesService const chance = new Chance() -const { author } = fixtures.users -const clonedAuthor = clone(author) - -const body = { - email: clonedAuthor.email, - firstName: clonedAuthor.firstName, - lastName: clonedAuthor.lastName, - title: clonedAuthor.title, - affiliation: clonedAuthor.affiliation, +const { user, author } = fixtures.users +jest.mock('pubsweet-component-mail-service', () => ({ + sendSimpleEmail: jest.fn(), + sendNotificationEmail: jest.fn(), +})) +const reqBody = { + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + title: user.title, + affiliation: user.affiliation, password: 'password', - token: clonedAuthor.passwordResetToken, + token: user.passwordResetToken, isConfirmed: false, } @@ -28,69 +30,67 @@ notFoundError.name = 'NotFoundError' notFoundError.status = 404 const resetPasswordPath = '../../routes/users/resetPassword' describe('Users password reset route handler', () => { + let testFixtures = {} + let body = {} + let models + beforeEach(() => { + testFixtures = cloneDeep(fixtures) + body = cloneDeep(reqBody) + models = Model.build(testFixtures) + }) it('should return an error when some parameters are missing', async () => { delete body.email const req = httpMocks.createRequest({ body }) const res = httpMocks.createResponse() - const models = Model.build() await require(resetPasswordPath)(models)(req, res) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual('missing required params') - body.email = author.email }) it('should return an error when the password is too small', async () => { body.password = 'small' const req = httpMocks.createRequest({ body }) const res = httpMocks.createResponse() - const models = Model.build() await require(resetPasswordPath)(models)(req, res) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual( 'password needs to be at least 7 characters long', ) - body.password = 'password' }) it('should return an error when user is not found', async () => { body.email = chance.email() const req = httpMocks.createRequest({ body }) const res = httpMocks.createResponse() - const models = Model.build() await require(resetPasswordPath)(models)(req, res) expect(res.statusCode).toBe(404) const data = JSON.parse(res._getData()) expect(data.error).toEqual('user not found') - body.email = author.email }) it('should return an error when the tokens do not match', async () => { body.token = chance.hash() const req = httpMocks.createRequest({ body }) const res = httpMocks.createResponse() - const models = Model.build() await require(resetPasswordPath)(models)(req, res) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual('invalid request') - body.token = author.passwordResetToken }) it('should return an error when the user is already confirmed', async () => { - author.isConfirmed = true + body.email = author.email + body.token = author.passwordResetToken const req = httpMocks.createRequest({ body }) const res = httpMocks.createResponse() - const models = Model.build() await require(resetPasswordPath)(models)(req, res) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual('User is already confirmed') - author.isConfirmed = false }) it('should return success when the body is correct', async () => { const req = httpMocks.createRequest({ body }) const res = httpMocks.createResponse() - const models = Model.build() await require(resetPasswordPath)(models)(req, res) expect(res.statusCode).toBe(200) diff --git a/packages/xpub-faraday/config/authsome-helpers.js b/packages/xpub-faraday/config/authsome-helpers.js index 1b7642bfca5445c4c1651d8cbfcca8aad45ef8c5..164a9ce8ae550d9cd1709543ceb852169220cdc6 100644 --- a/packages/xpub-faraday/config/authsome-helpers.js +++ b/packages/xpub-faraday/config/authsome-helpers.js @@ -44,7 +44,10 @@ const filterObjectData = ( rec => rec.userId === user.id, ) } - + parseAuthorsData(object, matchingCollPerm) + if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) { + return filterRefusedInvitations(object, user) + } return object } const matchingCollPerm = collectionsPermissions.find( @@ -52,10 +55,6 @@ const filterObjectData = ( ) if (matchingCollPerm === undefined) return null setPublicStatuses(object, matchingCollPerm) - parseAuthorsData(object, matchingCollPerm) - if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) { - return filterRefusedInvitations(object, user) - } return object } diff --git a/packages/xpub-faraday/config/authsome-mode.js b/packages/xpub-faraday/config/authsome-mode.js index 948dd93e25476745ef93e4db143ff99f79ff8cec..8a92a1301df7ea1bed2dc105beb44834cdb9b16f 100644 --- a/packages/xpub-faraday/config/authsome-mode.js +++ b/packages/xpub-faraday/config/authsome-mode.js @@ -4,6 +4,7 @@ const omit = require('lodash/omit') const helpers = require('./authsome-helpers') async function teamPermissions(user, operation, object, context) { + const { models } = context const permissions = ['handlingEditor', 'author', 'reviewer'] const teams = await helpers.getTeamsByPermissions( user.teams, @@ -13,7 +14,13 @@ async function teamPermissions(user, operation, object, context) { let collectionsPermissions = await Promise.all( teams.map(async team => { - const collection = await context.models.Collection.find(team.object.id) + let collection + if (team.object.type === 'collection') { + collection = await models.Collection.find(team.object.id) + } else if (team.object.type === 'fragment') { + const fragment = await models.Fragment.find(team.object.id) + collection = await models.Collection.find(fragment.collectionId) + } if ( collection.status === 'rejected' && team.teamType.permissions === 'reviewer' @@ -188,19 +195,21 @@ async function authenticatedUser(user, operation, object, context) { // only allow a reviewer and an HE 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 authsomeObject = get(object, 'authsomeObject') + const teams = await helpers.getTeamsByPermissions( user.teams, ['reviewer', 'handlingEditor'], context.models.Team, ) + if (teams.length === 0) return false - const matchingTeam = teams.find(team => team.object.id === collection.id) + const matchingTeam = teams.find( + team => team.object.id === authsomeObject.id, + ) + if (matchingTeam) return true return false } diff --git a/packages/xpub-faraday/config/validations.js b/packages/xpub-faraday/config/validations.js index 2b2c1062dbf806562db74079846592af8cd5e634..b8d8d607061cd81a82c828686002baac69a89312 100644 --- a/packages/xpub-faraday/config/validations.js +++ b/packages/xpub-faraday/config/validations.js @@ -7,7 +7,6 @@ module.exports = { created: Joi.date(), title: Joi.string(), status: Joi.string(), - reviewers: Joi.array(), customId: Joi.string(), invitations: Joi.array(), handlingEditor: Joi.object(), @@ -74,6 +73,7 @@ module.exports = { lock: Joi.object(), decision: Joi.object(), authors: Joi.array(), + invitations: Joi.array(), recommendations: Joi.array().items( Joi.object({ id: Joi.string().required(),