diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 3407c6874..57bdabfc9 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -18,19 +18,19 @@ import sentry from './sentryMiddleware' export default schema => { const middlewares = { - permissions: permissions, - sentry: sentry, - activityPub: activityPub, - dateTime: dateTime, - validation: validation, - sluggify: sluggify, - excerpt: excerpt, - handleContentData: handleContentData, - xss: xss, - softDelete: softDelete, - user: user, - includedFields: includedFields, - orderBy: orderBy, + permissions, + sentry, + activityPub, + dateTime, + validation, + sluggify, + excerpt, + handleContentData, + xss, + softDelete, + user, + includedFields, + orderBy, email: email({ isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT }), } diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index 6094911b1..134c85c0c 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -2,6 +2,8 @@ import { UserInputError } from 'apollo-server' const COMMENT_MIN_LENGTH = 1 const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!' +const NO_CATEGORIES_ERR_MESSAGE = + 'You cannot save a post without at least one category or more than three' const validateCommentCreation = async (resolve, root, args, context, info) => { const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim() @@ -19,6 +21,7 @@ const validateCommentCreation = async (resolve, root, args, context, info) => { postId, }, ) + session.close() const [post] = postQueryRes.records.map(record => { return record.get('post') }) @@ -43,9 +46,33 @@ const validateUpdateComment = async (resolve, root, args, context, info) => { const validatePost = async (resolve, root, args, context, info) => { const { categoryIds } = args if (!Array.isArray(categoryIds) || !categoryIds.length || categoryIds.length > 3) { - throw new UserInputError( - 'You cannot save a post without at least one category or more than three', - ) + throw new UserInputError(NO_CATEGORIES_ERR_MESSAGE) + } + return resolve(root, args, context, info) +} + +const validateUpdatePost = async (resolve, root, args, context, info) => { + const { id, categoryIds } = args + const session = context.driver.session() + const categoryQueryRes = await session.run( + ` + MATCH (post:Post {id: $id})-[:CATEGORIZED]->(category:Category) + RETURN category`, + { id }, + ) + session.close() + const [category] = categoryQueryRes.records.map(record => { + return record.get('category') + }) + + if (category) { + if (categoryIds && categoryIds.length > 3) { + throw new UserInputError(NO_CATEGORIES_ERR_MESSAGE) + } + } else { + if (!Array.isArray(categoryIds) || !categoryIds.length || categoryIds.length > 3) { + throw new UserInputError(NO_CATEGORIES_ERR_MESSAGE) + } } return resolve(root, args, context, info) } @@ -55,6 +82,6 @@ export default { CreateComment: validateCommentCreation, UpdateComment: validateUpdateComment, CreatePost: validatePost, - UpdatePost: validatePost, + UpdatePost: validateUpdatePost, }, } diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 5bb0c4f81..46d7c414f 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -75,22 +75,28 @@ export default { delete params.categoryIds params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) const session = context.driver.session() - const cypherDeletePreviousRelations = ` - MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category) - DELETE previousRelations - RETURN post, category + + let updatePostCypher = `MATCH (post:Post {id: $params.id}) + SET post = $params ` - await session.run(cypherDeletePreviousRelations, { params }) + if (categoryIds && categoryIds.length) { + const cypherDeletePreviousRelations = ` + MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category) + DELETE previousRelations + RETURN post, category + ` - const updatePostCypher = `MATCH (post:Post {id: $params.id}) - SET post = $params - WITH post + await session.run(cypherDeletePreviousRelations, { params }) + + updatePostCypher += `WITH post UNWIND $categoryIds AS categoryId MATCH (category:Category {id: categoryId}) MERGE (post)-[:CATEGORIZED]->(category) - RETURN post` + ` + } + updatePostCypher += `RETURN post` const updatePostVariables = { categoryIds, params } const transactionRes = await session.run(updatePostCypher, updatePostVariables) diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 44618ecdc..62507af0e 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -13,6 +13,7 @@ let client let userParams let authorParams +const postId = 'p3589' const postTitle = 'I am a title' const postContent = 'Some content' const oldTitle = 'Old title' @@ -96,7 +97,7 @@ beforeEach(async () => { }), ]) createPostVariables = { - id: 'p3589', + id: postId, title: postTitle, content: postContent, categoryIds, @@ -218,7 +219,7 @@ describe('CreatePost', () => { Post: [ { title: postTitle, - id: 'p3589', + id: postId, categories: expect.arrayContaining(categoryIdsArray), }, ], @@ -246,17 +247,16 @@ describe('UpdatePost', () => { await asAuthor.create('User', authorParams) await asAuthor.authenticateAs(authorParams) await asAuthor.create('Post', { - id: 'p1', + id: postId, title: oldTitle, content: oldContent, categoryIds, }) updatePostVariables = { - id: 'p1', + id: postId, title: newTitle, content: newContent, - categoryIds: null, } }) @@ -291,55 +291,96 @@ describe('UpdatePost', () => { }) it('updates a post', async () => { - updatePostVariables.categoryIds = ['cat9'] - const expected = { UpdatePost: { id: 'p1', content: newContent } } + updatePostVariables.categoryIds = ['cat27'] + const expected = { UpdatePost: { id: postId, content: newContent } } await expect(client.request(updatePostMutation, updatePostVariables)).resolves.toEqual( expected, ) }) describe('categories', () => { - beforeEach(async () => { - await client.request(createPostMutation, createPostVariables) - updatePostVariables = { - id: 'p3589', - title: newTitle, - content: newContent, - categoryIds: ['cat27'], - } + it('allows a user to update other attributes without passing in categoryIds explicitly', async () => { + const expected = { UpdatePost: { id: postId, content: newContent } } + await expect(client.request(updatePostMutation, updatePostVariables)).resolves.toEqual( + expected, + ) }) it('allows a user to update the categories of a post', async () => { + updatePostVariables.categoryIds = ['cat27'] await client.request(updatePostMutation, updatePostVariables) const expected = [{ id: 'cat27' }] const postQueryWithCategoriesVariables = { - id: 'p3589', + id: postId, } await expect( client.request(postQueryWithCategories, postQueryWithCategoriesVariables), ).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] }) }) - it('throws an error if categoryIds is not an array', async () => { - updatePostVariables.categoryIds = null - await expect(client.request(updatePostMutation, updatePostVariables)).rejects.toThrow( - postSaveError, - ) - }) - - it('requires at least one category for successful update', async () => { - updatePostVariables.categoryIds = [] - await expect(client.request(updatePostMutation, updatePostVariables)).rejects.toThrow( - postSaveError, - ) - }) - it('allows a maximum of three category for a successful update', async () => { updatePostVariables.categoryIds = ['cat9', 'cat27', 'cat15', 'cat4'] await expect(client.request(updatePostMutation, updatePostVariables)).rejects.toThrow( postSaveError, ) }) + + describe('post created without categories somehow', () => { + let ownerNode, owner, postMutationAction + beforeEach(async () => { + const postSomehowCreated = await instance.create('Post', { + id: 'how-was-this-created', + title: postTitle, + content: postContent, + }) + ownerNode = await instance.create('User', { + id: 'author-of-post-without-category', + name: 'Hacker', + slug: 'hacker', + email: 'hacker@example.org', + password: '1234', + }) + owner = await ownerNode.toJson() + await postSomehowCreated.relateTo(ownerNode, 'author') + postMutationAction = async (user, mutation, variables) => { + const { server } = createServer({ + context: () => { + return { + user, + neode: instance, + driver, + } + }, + }) + const { mutate } = createTestClient(server) + + return mutate({ + mutation, + variables, + }) + } + updatePostVariables.id = 'how-was-this-created' + }) + + it('throws an error if categoryIds is not an array', async () => { + const mustAddCategoryToPost = await postMutationAction( + owner, + updatePostMutation, + updatePostVariables, + ) + expect(mustAddCategoryToPost.errors[0]).toHaveProperty('message', postSaveError) + }) + + it('requires at least one category for successful update', async () => { + updatePostVariables.categoryIds = [] + const mustAddCategoryToPost = await postMutationAction( + owner, + updatePostMutation, + updatePostVariables, + ) + expect(mustAddCategoryToPost.errors[0]).toHaveProperty('message', postSaveError) + }) + }) }) }) }) @@ -355,7 +396,7 @@ describe('DeletePost', () => { ` const variables = { - id: 'p1', + id: postId, } beforeEach(async () => { @@ -363,7 +404,7 @@ describe('DeletePost', () => { await asAuthor.create('User', authorParams) await asAuthor.authenticateAs(authorParams) await asAuthor.create('Post', { - id: 'p1', + id: postId, content: 'To be deleted', categoryIds, }) @@ -396,7 +437,7 @@ describe('DeletePost', () => { }) it('deletes a post', async () => { - const expected = { DeletePost: { id: 'p1', content: 'To be deleted' } } + const expected = { DeletePost: { id: postId, content: 'To be deleted' } } await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) })