Skip to content
Snippets Groups Projects
Commit 06e3b140 authored by Sebastian Mihalache's avatar Sebastian Mihalache
Browse files

feat(component-manuscript-manager): create post api

parent 6422be63
No related branches found
No related tags found
1 merge request!8Sprint #10
Showing
with 519 additions and 360 deletions
......@@ -5,4 +5,5 @@ node_modules/
uploads/
.env.*
.env
config/local*.*
\ No newline at end of file
config/local*.*
public/apidoc
\ No newline at end of file
{
"name": "Component Manuscript Manager API documentation",
"version": "0.0.1",
"description": "A list of APIs for the Manuscript Manager Component",
"template": {
"forceLanguage": "en"
}
}
module.exports = {
backend: () => app => {
require('./src/FragmentsRecommendations')(app)
},
}
{
"name": "xpub-faraday-server",
"name": "pubsweet-component-manuscript-manager",
"version": "0.0.1",
"description": "xpub configured for faraday",
"description": "manuscript manager component for pubsweet",
"license": "MIT",
"author": "Collaborative Knowledge Foundation",
"files": [
"src"
],
"main": "index.js",
"scripts": {
"test": "jest"
"test": "jest",
"docs": "./node_modules/.bin/apidoc -e \"(node_modules|public)\" -o public/apidoc",
"open-docs": "open public/apidoc/index.html"
},
"repository": {
"type": "git",
"url": "https://gitlab.coko.foundation/xpub/xpub"
"url": "https://gitlab.coko.foundation/xpub/xpub",
"path": "component-manuscript-manager"
},
"dependencies": {
"body-parser": "^1.17.2",
"config": "^1.26.1",
"moment": "^2.18.1",
"nodemailer": "^4.0.1",
"uuid": "^3.2.1"
"chance": "^1.0.13"
},
"peerDependencies": {
"@pubsweet/logger": "^0.0.1",
"pubsweet": "^1.1.1",
"pubsweet-client": "^1.1.1",
"pubsweet-component-mail-service": "0.0.1",
"pubsweet-server": "^1.0.1"
},
"devDependencies": {
"apidoc": "^0.17.6",
"jest": "^22.1.1",
"supertest": "^3.0.0"
},
"jest": {
"verbose": true,
"testRegex": "/src/.*.test.js$"
},
"publishConfig": {
"access": "public"
}
}
const bodyParser = require('body-parser')
const FragmentsRecommendations = app => {
app.use(bodyParser.json())
const basePath =
'/api/collections/:collectionId/fragments/:fragmentId/recommendations'
const routePath = './routes/fragmentsRecommendations'
const authBearer = app.locals.passport.authenticate('bearer', {
session: false,
})
/**
* @api {post} /api/collections/:collectionId/fragments/:fragmentId/recommendations Create a recommendation on a fragment
* @apiGroup FragmentsRecommendations
* @apiParam {collectionId} collectionId Collection id
* @apiParam {fragmentId} fragmentId Fragment id
* @apiParamExample {json} Body
* {
* "recommendation": "accept", [acceptedValues: accept, revise, etc.],
* "comments":
* [
* {
* "content": "A very nice manuscript",
* "public": true
* "files":
* [
* {
* "id": "111-22-333",
* "name": "file.pdf",
* "size": 104232
* }
* ]
* }
* ],
* "type": "review" [acceptedValues: review, editorRecommendation]
* }
* @apiSuccessExample {json} Success
* HTTP/1.1 200 OK
* {
* "id": "7b2431af-210c-49f9-a69a-e19271066ebd",
* "userId": "4c3f8ee1-785b-4adb-87b4-407a27f652c6",
* "submittedOn": 1525428890167,
* "recommendation": "accept", [acceptedValues: accept, revise, etc.],
* "comments":
* [
* {
* "content": "A very nice manuscript",
* "public": true
* "files":
* [
* {
* "id": "111-22-333",
* "name": "file.pdf",
* "size": 104232
* }
* ]
* }
* ],
* "type": "review" [acceptedValues: review, editorRecommendation]
* }
* @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),
)
}
module.exports = FragmentsRecommendations
const logger = require('@pubsweet/logger')
const checkForUndefinedParams = (...params) => {
if (params.includes(undefined)) {
return false
}
return true
}
const validateEmailAndToken = async (email, token, userModel) => {
try {
const user = await userModel.findByEmail(email)
if (user) {
if (token !== user.passwordResetToken) {
logger.error(
`invite pw reset tokens do not match: REQ ${token} vs. DB ${
user.passwordResetToken
}`,
)
return {
success: false,
status: 400,
message: 'invalid request',
}
}
return { success: true, user }
}
} catch (e) {
if (e.name === 'NotFoundError') {
logger.error('invite pw reset on non-existing user')
return {
success: false,
status: 404,
message: 'user not found',
}
} else if (e.name === 'ValidationError') {
logger.error('invite pw reset validation error')
return {
success: false,
status: 400,
message: e.details[0].message,
}
}
logger.error('internal server error')
return {
success: false,
status: 500,
message: e.details[0].message,
}
}
return {
success: false,
status: 500,
message: 'something went wrong',
}
}
const handleNotFoundError = async (error, item) => {
const response = {
success: false,
status: 500,
message: 'Something went wrong',
}
if (error.name === 'NotFoundError') {
logger.error(`invalid ${item} id`)
response.status = 404
response.message = `${item} not found`
return response
}
logger.error(error)
return response
}
const getBaseUrl = req => `${req.protocol}://${req.get('host')}`
module.exports = {
checkForUndefinedParams,
validateEmailAndToken,
handleNotFoundError,
getBaseUrl,
}
const helpers = require('../../helpers/helpers')
const uuid = require('uuid')
module.exports = models => async (req, res) => {
const { recommendation, comments, type } = req.body
if (!helpers.checkForUndefinedParams(recommendation, comments, type))
return res.status(400).json({ error: 'Parameters are missing.' })
const reqUser = await models.User.find(req.user)
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: `collection ${
collection.id
} does not contain fragment ${fragmentId}.`,
})
fragment = await models.Fragment.find(fragmentId)
} catch (e) {
const notFoundError = await helpers.handleNotFoundError(e, 'item')
return res.status(notFoundError.status).json({
error: notFoundError.message,
})
}
fragment.recommendations = fragment.recommendations || []
const newRecommendation = {
id: uuid.v4(),
userId: reqUser.id,
submittedOn: new Date(),
type,
recommendation,
comments,
}
fragment.recommendations.push(newRecommendation)
await fragment.save()
return res.status(200).json(newRecommendation)
}
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
const authors = require('./authors')
const fragments = require('./fragments')
const users = require('./users')
const collections = require('./collections')
const fragments = require('./fragments')
module.exports = {
authors,
users,
fragments,
collections,
fragments,
}
const Chance = require('chance')
const chance = new Chance()
const fragments = {
fragment: {
id: chance.guid(),
metadata: {
title: chance.sentence(),
abstract: chance.paragraph(),
},
save: jest.fn(),
},
}
module.exports = fragments
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(),
}
const { reviewer } = require('./userData')
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,
invitationToken: 'inv-token-123',
},
}
module.exports = users
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 chance = new Chance()
const reqBody = {
recommendation: 'accept',
comments: [
{
content: chance.paragraph(),
public: chance.bool(),
files: [
{
id: chance.guid(),
name: 'file.pdf',
size: chance.natural(),
},
],
},
],
type: 'review',
}
// const route = {
// path: '/api/collections/:collectionId/invitations',
// }
const path = '../../routes/fragmentsRecommendations/post'
describe('Post collections 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 { reviewer } = testFixtures.users
delete body.comments
const res = await requests.sendRequest({
body,
userId: reviewer.id,
models,
path,
})
expect(res.statusCode).toBe(400)
const data = JSON.parse(res._getData())
expect(data.error).toEqual('Parameters are missing.')
})
it('should return success when the parameters are correct', async () => {
const { reviewer } = testFixtures.users
const { collection } = testFixtures.collections
const { fragment } = testFixtures.fragments
const res = await requests.sendRequest({
body,
userId: reviewer.id,
models,
path,
params: {
collectionId: collection.id,
fragmentId: fragment.id,
},
})
expect(res.statusCode).toBe(200)
const data = JSON.parse(res._getData())
expect(data.userId).toEqual(reviewer.id)
})
})
// 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)),
},
}
UserMock.find = jest.fn(id => findMock(id, 'users', fixtures))
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 }
const httpMocks = require('node-mocks-http')
const sendRequest = async ({
body = {},
userId,
route,
models,
path,
params = {},
query = {},
}) => {
const req = httpMocks.createRequest({
body,
})
req.user = userId
req.route = route
req.params = params
req.query = query
const res = httpMocks.createResponse()
await require(path)(models)(req, res)
return res
}
module.exports = { sendRequest }
/* 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 = '111222'
this.id = uuid.v4()
return Promise.resolve(this)
})
......
## xPub-faraday-server
A server package that adds extra features needed by `xpub-faraday` on top of `pubsweet`
\ No newline at end of file
module.exports = {
backend: () => app => require('./src/AuthorBackend')(app),
}
const bodyParser = require('body-parser')
const uuid = require('uuid')
const AuthorBackend = app => {
const authBearer = app.locals.passport.authenticate('bearer', {
session: false,
})
app.post(
'/api/collections/:collectionId/fragments/:fragmentId/authors',
authBearer,
bodyParser.json(),
async (req, res, next) => {
try {
let fragment = await app.locals.models.Fragment.find(
req.params.fragmentId,
)
const collection = await app.locals.models.Collection.find(
req.params.collectionId,
)
fragment.authors = fragment.authors ? fragment.authors : []
if (fragment.authors.length > 0) {
const emailAuthors = fragment.authors.filter(
author => author.email === req.body.email,
)
if (emailAuthors.length > 0) {
res
.status(400)
.json({ error: 'Author with the same email already exists' })
return
}
const submittingAuthors = fragment.authors.filter(
author =>
author.isSubmitting === true &&
author.isSubmitting === req.body.isSubmitting,
)
if (submittingAuthors.length > 0) {
res
.status(400)
.json({ error: 'There can only be one submitting author' })
return
}
}
req.body.id = uuid.v4()
fragment.authors.push(req.body)
const reqUser = await app.locals.models.User.find(req.user)
if (reqUser.admin === true && req.body.isSubmitting === true) {
try {
// check if author has corresponding user
const user = await app.locals.models.User.findByEmail(
req.body.email,
)
fragment.owners.push(user.id)
collection.owners.push(user.id)
} catch (e) {
if (e.name === 'NotFoundError') {
// create a new User account
const userBody = {
username: `${req.body.firstName}${
req.body.lastName
}${Math.floor(Math.random() * 1000)}`,
email: req.body.email,
password: uuid.v4(),
}
let newUser = new app.locals.models.User(userBody)
newUser = await newUser.save()
fragment.owners.push(newUser.id)
collection.owners.push(newUser.id)
}
}
}
fragment = await fragment.save()
await collection.save()
res.status(200).json(fragment.authors[fragment.authors.length - 1])
} catch (e) {
if (e.name === 'NotFoundError') {
res.status(e.status).json({ error: 'Object not found' })
return
}
if (e.name === 'ValidationError') {
res.status(404).json({ error: e.details[0].message })
return
}
res.status(400).json({ error: 'Something went wrong' })
}
},
)
app.delete(
'/api/fragments/:fragmentId/authors/:authorEmail',
authBearer,
async (req, res, next) => {
const { fragmentId, authorEmail } = req.params
try {
let fragment = await app.locals.models.Fragment.find(fragmentId)
if (fragment.authors === 'undefined') {
res.status(404).json({ error: 'Fragment does not have any authors' })
return
}
if (fragment.authors.length === 0) {
res.status(404).json({ error: 'Fragment does not have any authors' })
return
}
const newAuthors = fragment.authors.filter(
author => author.email !== authorEmail,
)
if (newAuthors.length === fragment.authors.length) {
res.status(404).json({ error: 'Author not found' })
return
}
fragment.authors = newAuthors
fragment = await fragment.save()
res.status(204).json({})
return
} catch (e) {
if (e.name === 'NotFoundError') {
res.status(e.status).json({ error: 'Fragment not found' })
return
}
if (e.name === 'ValidationError') {
res.status(404).json({ error: e.details[0].message })
return
}
res.status(400).json({ error: 'Something went wrong' })
}
},
)
}
module.exports = AuthorBackend
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
process.env.SUPPRESS_NO_CONFIG_WARNING = true
const bodyParser = require('body-parser')
const supertest = require('supertest')
const component = require('..')
const express = require('express')
const fixtures = require('./fixtures/fixtures')
const passport = require('passport')
const BearerStrategy = require('passport-http-bearer').Strategy
const cloneDeep = require('lodash/cloneDeep')
const UserMock = require('./mocks/User')
const Fragment = require('./mocks/Fragment')
function makeApp(collection, fragment, standardUser, existingUser) {
const app = express()
app.use(bodyParser.json())
app.use(passport.initialize())
passport.use(
'bearer',
new BearerStrategy((token, done) =>
done(null, fixtures.users.standardUser, { scope: 'all' }),
),
)
app.locals.passport = passport
app.locals.models = {
Fragment: {
find: jest.fn(
() =>
fragment instanceof Error
? Promise.reject(fragment)
: Promise.resolve(fragment),
),
},
User: {},
Collection: {
find: jest.fn(
() =>
collection instanceof Error
? Promise.reject(collection)
: Promise.resolve(collection),
),
},
}
UserMock.find = jest.fn(
() =>
standardUser instanceof Error
? Promise.reject(standardUser)
: Promise.resolve(standardUser),
)
UserMock.findByEmail = jest.fn(
() =>
existingUser instanceof Error
? Promise.reject(existingUser)
: Promise.resolve(existingUser),
)
app.locals.models.User = UserMock
component.backend()(app)
return supertest(app)
}
const createAuthorUrl = '/api/collections/123/fragments/123/authors'
const getNewFragment = authors => {
const fragProps = {
type: 'fragment',
fragmentType: 'blogpost',
title: 'Just your regular blogpost',
source: '<blog></blog>',
presentation: '<p></p>',
authors,
owners: [],
}
return new Fragment(fragProps)
}
describe('Author Backend API', () => {
let testFixtures = {}
beforeEach(() => (testFixtures = cloneDeep(fixtures)))
it('should return an error if fragment is not found', () => {
const error = new Error()
error.name = 'NotFoundError'
error.status = 404
return makeApp(testFixtures.collections.standardCollection, error)
.post(createAuthorUrl)
.set('Authorization', 'Bearer 123')
.send(testFixtures.authors.standardAuthor)
.expect(404, '{"error":"Object not found"}')
})
it('should return an error if collection is not found', () => {
const error = new Error()
error.name = 'NotFoundError'
error.status = 404
return makeApp(error)
.post(createAuthorUrl)
.set('Authorization', 'Bearer 123')
.send(testFixtures.authors.standardAuthor)
.expect(404, '{"error":"Object not found"}')
})
it('should return an error if an author field is invalid', () => {
const error = new Error()
error.name = 'ValidationError'
error.status = 404
error.details = []
error.details.push({ message: 'firstName is required' })
return makeApp(testFixtures.collections.standardCollection, error)
.post(createAuthorUrl)
.set('Authorization', 'Bearer 123')
.send(testFixtures.authors.invalidAuthor)
.expect(404, '{"error":"firstName is required"}')
})
it('should return an error if an author already exists with the same email', () => {
const fragment = getNewFragment([testFixtures.authors.standardAuthor])
return makeApp(testFixtures.collections.standardCollection, fragment)
.post(createAuthorUrl)
.set('Authorization', 'Bearer 123')
.send(testFixtures.authors.standardAuthor)
.expect(400, '{"error":"Author with the same email already exists"}')
})
it('should return an error if there already is a submitting author', () => {
const fragment = getNewFragment([testFixtures.authors.standardAuthor])
return makeApp(testFixtures.collections.standardCollection, fragment)
.post(createAuthorUrl)
.set('Authorization', 'Bearer 123')
.send(testFixtures.authors.newSubmittingAuthor)
.expect(400, '{"error":"There can only be one submitting author"}')
})
it('should return success when saving a new author', () => {
const fragment = getNewFragment([])
return makeApp(
fixtures.collections.standardCollection,
fragment,
testFixtures.users.standardUser,
)
.post(createAuthorUrl)
.set('Authorization', 'Bearer 123')
.send(testFixtures.authors.newAuthor)
.expect(200)
.then(() => expect(fragment.save).toHaveBeenCalled())
})
it('should return success when the admin adds a submitting author and the author already has a corresponding user account', () => {
const fragment = getNewFragment([])
return makeApp(
testFixtures.collections.standardCollection,
fragment,
testFixtures.users.admin,
testFixtures.users.existingUser,
)
.post(createAuthorUrl)
.set('Authorization', 'Bearer 123')
.send(testFixtures.authors.standardAuthor)
.expect(200)
.then(() => {
expect(fragment.save).toHaveBeenCalled()
expect(
testFixtures.collections.standardCollection.save,
).toHaveBeenCalled()
expect(fragment.owners.length).toBeGreaterThan(0)
expect(
testFixtures.collections.standardCollection.owners.length,
).toBeGreaterThan(1)
expect(fragment.owners[0]).toBe('123987')
expect(testFixtures.collections.standardCollection.owners[1]).toBe(
'123987',
)
})
})
it('should return success when the admin adds a submitting author and creates a corresponding user account', () => {
const error = new Error()
error.name = 'NotFoundError'
error.status = 404
const fragment = getNewFragment([])
return makeApp(
testFixtures.collections.standardCollection,
fragment,
testFixtures.users.admin,
error,
)
.post(createAuthorUrl)
.set('Authorization', 'Bearer 123')
.send(testFixtures.authors.standardAuthor)
.expect(200)
.then(() => {
expect(fragment.save).toHaveBeenCalled()
expect(
testFixtures.collections.standardCollection.save,
).toHaveBeenCalled()
expect(fragment.owners.length).toBeGreaterThan(0)
expect(
testFixtures.collections.standardCollection.owners.length,
).toBeGreaterThan(1)
expect(fragment.owners[0]).toBe('111222')
expect(testFixtures.collections.standardCollection.owners[1]).toBe(
'111222',
)
})
})
})
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment