diff --git a/backend/src/resolvers/comments.js b/backend/src/resolvers/comments.js index 97b99f4ab..d4775b235 100644 --- a/backend/src/resolvers/comments.js +++ b/backend/src/resolvers/comments.js @@ -2,44 +2,47 @@ import { neo4jgraphql } from 'neo4j-graphql-js' import { UserInputError } from 'apollo-server' const COMMENT_MIN_LENGTH = 1 +const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!' + export default { - Query: { - CommentByPost: async (object, params, context, resolveInfo) => { - const { postId } = params - - const session = context.driver.session() - const transactionRes = await session.run(` - MATCH (comment:Comment)-[:COMMENTS]->(post:Post {id: $postId}) - RETURN comment {.id, .contentExcerpt, .createdAt} ORDER BY comment.createdAt ASC`, { - postId - }) - - session.close() - let comments = [] - transactionRes.records.map(record => { - comments.push(record.get('comment')) - }) - - return comments - } - }, Mutation: { CreateComment: async (object, params, context, resolveInfo) => { const content = params.content.replace(/<(?:.|\n)*?>/gm, '').trim() + const { postId } = params + // Adding relationship from comment to post by passing in the postId, + // but we do not want to create the comment with postId as an attribute + // because we use relationships for this. So, we are deleting it from params + // before comment creation. + delete params.postId if (!params.content || content.length < COMMENT_MIN_LENGTH) { throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`) } - const { postId } = params - delete params.postId - const comment = await neo4jgraphql(object, params, context, resolveInfo, false) + if (!postId.trim()) { + throw new UserInputError(NO_POST_ERR_MESSAGE) + } const session = context.driver.session() + const postQueryRes = await session.run(` + MATCH (post:Post {id: $postId}) + RETURN post`, { + postId + } + ) + const [post] = postQueryRes.records.map(record => { + return record.get('post') + }) + + if (!post) { + throw new UserInputError(NO_POST_ERR_MESSAGE) + } + const comment = await neo4jgraphql(object, params, context, resolveInfo, false) await session.run(` - MATCH (post:Post {id: $postId}), (comment:Comment {id: $commentId}) - MERGE (post)<-[:COMMENTS]-(comment) - RETURN comment {.id, .content}`, { + MATCH (post:Post {id: $postId}), (comment:Comment {id: $commentId}), (author:User {id: $userId}) + MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author) + RETURN post`, { + userId: context.user.id, postId, commentId: comment.id } diff --git a/backend/src/resolvers/comments.spec.js b/backend/src/resolvers/comments.spec.js index 34a18d807..87a0df270 100644 --- a/backend/src/resolvers/comments.spec.js +++ b/backend/src/resolvers/comments.spec.js @@ -4,7 +4,10 @@ import { host, login } from '../jest/helpers' const factory = Factory() let client -let variables +let createCommentVariables +let createPostVariables +let createCommentVariablesSansPostId +let createCommentVariablesWithNonExistentPost beforeEach(async () => { await factory.create('User', { @@ -18,22 +21,36 @@ afterEach(async () => { }) describe('CreateComment', () => { - const mutation = ` - mutation($postId: ID, $content: String!) { - CreateComment(postId: $postId, content: $content) { - id - content + const createCommentMutation = ` + mutation($postId: ID, $content: String!) { + CreateComment(postId: $postId, content: $content) { + id + content + } + } + ` + const createPostMutation = ` + mutation($id: ID!, $title: String!, $content: String!) { + CreatePost(id: $id, title: $title, content: $content) { + id + } + } + ` + const commentQueryForPostId = ` + query($content: String) { + Comment(content: $content) { + postId } } ` describe('unauthenticated', () => { it('throws authorization error', async () => { - variables = { + createCommentVariables = { postId: 'p1', content: 'I\'m not authorised to comment' } client = new GraphQLClient(host) - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + await expect(client.request(createCommentMutation, createCommentVariables)).rejects.toThrow('Not Authorised') }) }) @@ -42,40 +59,120 @@ describe('CreateComment', () => { beforeEach(async () => { headers = await login({ email: 'test@example.org', password: '1234' }) client = new GraphQLClient(host, { headers }) - }) - - it('creates a comment', async () => { - variables = { + createCommentVariables = { postId: 'p1', content: 'I\'m authorised to comment' } + createPostVariables = { + id: 'p1', + title: 'post to comment on', + content: 'please comment on me' + } + await client.request(createPostMutation, createPostVariables) + }) + + it('creates a comment', async () => { const expected = { CreateComment: { content: 'I\'m authorised to comment' } } - await expect(client.request(mutation, variables)).resolves.toMatchObject(expected) + await expect(client.request(createCommentMutation, createCommentVariables)).resolves.toMatchObject(expected) }) - it('throw an error if an empty string is sent as content', async () => { - variables = { + it('assigns the authenticated user as author', async () => { + await client.request(createCommentMutation, createCommentVariables) + + const { User } = await client.request(`{ + User(email: "test@example.org") { + comments { + content + } + } + }`) + + expect(User).toEqual([ { comments: [ { content: 'I\'m authorised to comment' } ] } ]) + }) + + it('throw an error if an empty string is sent from the editor as content', async () => { + createCommentVariables = { postId: 'p1', content: '
' } - await expect(client.request(mutation, variables)) + await expect(client.request(createCommentMutation, createCommentVariables)) .rejects.toThrow('Comment must be at least 1 character long!') }) - it('throws an error if a comment does not contain a single character', async () => { - variables = { + it('throws an error if a comment sent from the editor does not contain a single character', async () => { + createCommentVariables = { postId: 'p1', content: '' } - await expect(client.request(mutation, variables)) + await expect(client.request(createCommentMutation, createCommentVariables)) .rejects.toThrow('Comment must be at least 1 character long!') }) + + it('throws an error if postId is sent as an empty string', async () => { + createCommentVariables = { + postId: 'p1', + content: '' + } + + await expect(client.request(createCommentMutation, createCommentVariables)) + .rejects.toThrow('Comment must be at least 1 character long!') + }) + + it('throws an error if content is sent as an string of empty characters', async () => { + createCommentVariables = { + postId: 'p1', + content: ' ' + } + + await expect(client.request(createCommentMutation, createCommentVariables)) + .rejects.toThrow('Comment must be at least 1 character long!') + }) + + it('throws an error if postId is sent as an empty string', async () => { + createCommentVariablesSansPostId = { + postId: '', + content: 'this comment should not be created' + } + + await expect(client.request(createCommentMutation, createCommentVariablesSansPostId)) + .rejects.toThrow('Comment cannot be created without a post!') + }) + + it('throws an error if postId is sent as an string of empty characters', async () => { + createCommentVariablesSansPostId = { + postId: ' ', + content: 'this comment should not be created' + } + + await expect(client.request(createCommentMutation, createCommentVariablesSansPostId)) + .rejects.toThrow('Comment cannot be created without a post!') + }) + + it('throws an error if the post does not exist in the database', async () => { + createCommentVariablesWithNonExistentPost = { + postId: 'p2', + content: 'comment should not be created cause the post doesn\'t exist' + } + + await expect(client.request(createCommentMutation, createCommentVariablesWithNonExistentPost)) + .rejects.toThrow('Comment cannot be created without a post!') + }) + + it('does not create the comment with the postId as an attribute', async () => { + const commentQueryVariablesByContent = { + content: 'I\'m authorised to comment' + } + + await client.request(createCommentMutation, createCommentVariables) + const { Comment } = await client.request(commentQueryForPostId, commentQueryVariablesByContent) + expect(Comment).toEqual([{ postId: null }]) + }) }) }) diff --git a/backend/src/resolvers/moderation.spec.js b/backend/src/resolvers/moderation.spec.js index f8aa6e10b..28f4dc322 100644 --- a/backend/src/resolvers/moderation.spec.js +++ b/backend/src/resolvers/moderation.spec.js @@ -16,6 +16,9 @@ const setupAuthenticateClient = (params) => { let createResource let authenticateClient +let createPostVariables +let createCommentVariables + beforeEach(() => { createResource = () => {} authenticateClient = () => { @@ -103,18 +106,21 @@ describe('disable', () => { variables = { id: 'c47' } - + createPostVariables = { + id: 'p3', + title: 'post to comment on', + content: 'please comment on me' + } + createCommentVariables = { + id: 'c47', + postId: 'p3', + content: 'this comment was created for this post' + } createResource = async () => { await factory.create('User', { id: 'u45', email: 'commenter@example.org', password: '1234' }) - await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' }) - await Promise.all([ - factory.create('Post', { id: 'p3' }), - factory.create('Comment', { id: 'c47', postId: 'p3', content: 'this comment was created for this post' }) - ]) - - await Promise.all([ - factory.relate('Comment', 'Author', { from: 'u45', to: 'c47' }) - ]) + const asAuthenticatedUser = await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' }) + await asAuthenticatedUser.create('Post', createPostVariables) + await asAuthenticatedUser.create('Comment', createCommentVariables) } }) @@ -277,17 +283,21 @@ describe('enable', () => { variables = { id: 'c456' } - + createPostVariables = { + id: 'p9', + title: 'post to comment on', + content: 'please comment on me' + } + createCommentVariables = { + id: 'c456', + postId: 'p9', + content: 'this comment was created for this post' + } createResource = async () => { await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' }) - await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) - await Promise.all([ - factory.create('Post', { id: 'p9' }), - factory.create('Comment', { id: 'c456' }) - ]) - await Promise.all([ - factory.relate('Comment', 'Author', { from: 'u123', to: 'c456' }) - ]) + const asAuthenticatedUser = await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) + await asAuthenticatedUser.create('Post', createPostVariables) + await asAuthenticatedUser.create('Comment', createCommentVariables) const disableMutation = ` mutation { diff --git a/backend/src/resolvers/reports.spec.js b/backend/src/resolvers/reports.spec.js index ae8894572..9bd1fe753 100644 --- a/backend/src/resolvers/reports.spec.js +++ b/backend/src/resolvers/reports.spec.js @@ -9,6 +9,7 @@ describe('report', () => { let headers let returnedObject let variables + let createPostVariables beforeEach(async () => { returnedObject = '{ description }' @@ -128,8 +129,14 @@ describe('report', () => { describe('reported resource is a comment', () => { beforeEach(async () => { - await factory.authenticateAs({ email: 'test@example.org', password: '1234' }) - await factory.create('Comment', { id: 'c34', content: 'Robert getting tired.' }) + createPostVariables = { + id: 'p1', + title: 'post to comment on', + content: 'please comment on me' + } + const asAuthenticatedUser = await factory.authenticateAs({ email: 'test@example.org', password: '1234' }) + await asAuthenticatedUser.create('Post', createPostVariables) + await asAuthenticatedUser.create('Comment', { postId: 'p1', id: 'c34', content: 'Robert getting tired.' }) variables = { id: 'c34' } }) diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index f17c20315..8694a7948 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -189,33 +189,18 @@ import Factory from './factories' ]) await Promise.all([ - f.create('Comment', { id: 'c1', postId: 'p1' }), - f.create('Comment', { id: 'c2', postId: 'p1' }), - f.create('Comment', { id: 'c3', postId: 'p3' }), - f.create('Comment', { id: 'c4', postId: 'p2' }), - f.create('Comment', { id: 'c5', postId: 'p3' }), - f.create('Comment', { id: 'c6', postId: 'p4' }), - f.create('Comment', { id: 'c7', postId: 'p2' }), - f.create('Comment', { id: 'c8', postId: 'p15' }), - f.create('Comment', { id: 'c9', postId: 'p15' }), - f.create('Comment', { id: 'c10', postId: 'p15' }), - f.create('Comment', { id: 'c11', postId: 'p15' }), - f.create('Comment', { id: 'c12', postId: 'p15' }) - ]) - - await Promise.all([ - f.relate('Comment', 'Author', { from: 'u3', to: 'c1' }), - f.relate('Comment', 'Author', { from: 'u1', to: 'c2' }), - f.relate('Comment', 'Author', { from: 'u1', to: 'c3' }), - f.relate('Comment', 'Author', { from: 'u4', to: 'c4' }), - f.relate('Comment', 'Author', { from: 'u4', to: 'c5' }), - f.relate('Comment', 'Author', { from: 'u3', to: 'c6' }), - f.relate('Comment', 'Author', { from: 'u2', to: 'c7' }), - f.relate('Comment', 'Author', { from: 'u5', to: 'c8' }), - f.relate('Comment', 'Author', { from: 'u6', to: 'c9' }), - f.relate('Comment', 'Author', { from: 'u7', to: 'c10' }), - f.relate('Comment', 'Author', { from: 'u5', to: 'c11' }), - f.relate('Comment', 'Author', { from: 'u6', to: 'c12' }) + asUser.create('Comment', { id: 'c1', postId: 'p1' }), + asTick.create('Comment', { id: 'c2', postId: 'p1' }), + asTrack.create('Comment', { id: 'c3', postId: 'p3' }), + asTrick.create('Comment', { id: 'c4', postId: 'p2' }), + asModerator.create('Comment', { id: 'c5', postId: 'p3' }), + asAdmin.create('Comment', { id: 'c6', postId: 'p4' }), + asUser.create('Comment', { id: 'c7', postId: 'p2' }), + asTick.create('Comment', { id: 'c8', postId: 'p15' }), + asTrick.create('Comment', { id: 'c9', postId: 'p15' }), + asTrack.create('Comment', { id: 'c10', postId: 'p15' }), + asUser.create('Comment', { id: 'c11', postId: 'p15' }), + asUser.create('Comment', { id: 'c12', postId: 'p15' }) ]) const disableMutation = 'mutation($id: ID!) { disable(id: $id) }' diff --git a/cypress/integration/common/post.js b/cypress/integration/common/post.js index 85a9f3339..f6a1bbedd 100644 --- a/cypress/integration/common/post.js +++ b/cypress/integration/common/post.js @@ -1,5 +1,7 @@ import { When, Then } from 'cypress-cucumber-preprocessor/steps' +const narratorAvatar = 'https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg' + Then('I click on the {string} button', text => { cy.get('button').contains(text).click() }) @@ -12,6 +14,9 @@ Then('my comment should be successfully created', () => { Then('I should see my comment', () => { cy.get('div.comment p') .should('contain', 'Human Connection rocks') + .get('.ds-avatar img') + .should('have.attr', 'src') + .and('contain', narratorAvatar) }) Then('the editor should be cleared', () => { diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 85a660f43..0be3f882f 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -11,6 +11,7 @@ let loginCredentials = { } const narratorParams = { name: 'Peter Pan', + avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg', ...loginCredentials } diff --git a/webapp/components/CommentForm/index.vue b/webapp/components/comments/CommentForm/index.vue similarity index 90% rename from webapp/components/CommentForm/index.vue rename to webapp/components/comments/CommentForm/index.vue index fdb1761dc..d59314e07 100644 --- a/webapp/components/CommentForm/index.vue +++ b/webapp/components/comments/CommentForm/index.vue @@ -20,6 +20,7 @@