diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 31c373fc7..7958e610d 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -176,6 +176,7 @@ const permissions = shield( enable: isModerator, disable: isModerator, CreateComment: isAuthenticated, + UpdateComment: isAuthor, DeleteComment: isAuthor, DeleteUser: isDeletingOwnAccount, requestPasswordReset: allow, diff --git a/backend/src/middleware/validation/index.js b/backend/src/middleware/validation/index.js deleted file mode 100644 index ca7a6b338..000000000 --- a/backend/src/middleware/validation/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import { UserInputError } from 'apollo-server' - -const validateUrl = async (resolve, root, args, context, info) => { - const { url } = args - const isValid = url.match(/^(?:https?:\/\/)(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g) - if (isValid) { - /* eslint-disable-next-line no-return-await */ - return await resolve(root, args, context, info) - } else { - throw new UserInputError('Input is not a URL') - } -} - -export default { - Mutation: { - CreateSocialMedia: validateUrl, - }, -} diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index 77282b6b5..319a9a6c0 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -45,9 +45,20 @@ const validateCommentCreation = async (resolve, root, args, context, info) => { } } +const validateUpdateComment = async (resolve, root, args, context, info) => { + const COMMENT_MIN_LENGTH = 1 + const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim() + if (!args.content || content.length < COMMENT_MIN_LENGTH) { + throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`) + } + + return resolve(root, args, context, info) +} + export default { Mutation: { CreateSocialMedia: validate(socialMediaSchema), CreateComment: validateCommentCreation, + UpdateComment: validateUpdateComment, }, } diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js index a12e2dfcc..890ba5feb 100644 --- a/backend/src/schema/resolvers/comments.spec.js +++ b/backend/src/schema/resolvers/comments.spec.js @@ -9,7 +9,28 @@ let createPostVariables let createCommentVariablesSansPostId let createCommentVariablesWithNonExistentPost let userParams -let authorParams +let headers + +const createPostMutation = gql` + mutation($id: ID!, $title: String!, $content: String!) { + CreatePost(id: $id, title: $title, content: $content) { + id + } + } +` +const createCommentMutation = gql` + mutation($id: ID, $postId: ID!, $content: String!) { + CreateComment(id: $id, postId: $postId, content: $content) { + id + content + } + } +` +createPostVariables = { + id: 'p1', + title: 'post to comment on', + content: 'please comment on me', +} beforeEach(async () => { userParams = { @@ -25,21 +46,6 @@ afterEach(async () => { }) describe('CreateComment', () => { - const createCommentMutation = gql` - mutation($postId: ID!, $content: String!) { - CreateComment(postId: $postId, content: $content) { - id - content - } - } - ` - const createPostMutation = gql` - mutation($id: ID!, $title: String!, $content: String!) { - CreatePost(id: $id, title: $title, content: $content) { - id - } - } - ` describe('unauthenticated', () => { it('throws authorization error', async () => { createCommentVariables = { @@ -54,7 +60,6 @@ describe('CreateComment', () => { }) describe('authenticated', () => { - let headers beforeEach(async () => { headers = await login(userParams) client = new GraphQLClient(host, { @@ -64,11 +69,6 @@ describe('CreateComment', () => { 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) }) @@ -187,19 +187,8 @@ describe('CreateComment', () => { }) }) -describe('DeleteComment', () => { - const deleteCommentMutation = gql` - mutation($id: ID!) { - DeleteComment(id: $id) { - id - } - } - ` - - let deleteCommentVariables = { - id: 'c1', - } - +describe('ManageComments', () => { + let authorParams beforeEach(async () => { authorParams = { email: 'author@example.org', @@ -213,51 +202,178 @@ describe('DeleteComment', () => { content: 'Post to be commented', }) await asAuthor.create('Comment', { - id: 'c1', + id: 'c456', postId: 'p1', content: 'Comment to be deleted', }) }) - describe('unauthenticated', () => { - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow( - 'Not Authorised', - ) - }) - }) - - describe('authenticated but not the author', () => { - beforeEach(async () => { - let headers - headers = await login(userParams) - client = new GraphQLClient(host, { headers }) - }) - - it('throws authorization error', async () => { - await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow( - 'Not Authorised', - ) - }) - }) - - describe('authenticated as author', () => { - beforeEach(async () => { - let headers - headers = await login(authorParams) - client = new GraphQLClient(host, { headers }) - }) - - it('deletes the comment', async () => { - const expected = { - DeleteComment: { - id: 'c1', - }, + describe('UpdateComment', () => { + const updateCommentMutation = gql` + mutation($content: String!, $id: ID!) { + UpdateComment(content: $content, id: $id) { + id + content + } } - await expect(client.request(deleteCommentMutation, deleteCommentVariables)).resolves.toEqual( - expected, - ) + ` + + let updateCommentVariables = { + id: 'c456', + content: 'The comment is updated', + } + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( + 'Not Authorised', + ) + }) + }) + + describe('authenticated but not the author', () => { + beforeEach(async () => { + headers = await login({ + email: 'test@example.org', + password: '1234', + }) + client = new GraphQLClient(host, { + headers, + }) + }) + + it('throws authorization error', async () => { + await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( + 'Not Authorised', + ) + }) + }) + + describe('authenticated as author', () => { + beforeEach(async () => { + headers = await login(authorParams) + client = new GraphQLClient(host, { + headers, + }) + }) + + it('updates the comment', async () => { + const expected = { + UpdateComment: { + id: 'c456', + content: 'The comment is updated', + }, + } + await expect( + client.request(updateCommentMutation, updateCommentVariables), + ).resolves.toEqual(expected) + }) + + it('throw an error if an empty string is sent from the editor as content', async () => { + updateCommentVariables = { + id: 'c456', + content: '
', + } + + await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( + 'Comment must be at least 1 character long!', + ) + }) + + it('throws an error if a comment sent from the editor does not contain a single letter character', async () => { + updateCommentVariables = { + id: 'c456', + content: '', + } + + await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( + 'Comment must be at least 1 character long!', + ) + }) + + it('throws an error if commentId is sent as an empty string', async () => { + updateCommentVariables = { + id: '', + content: '
Hello
', + } + + await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( + 'Not Authorised!', + ) + }) + + it('throws an error if the comment does not exist in the database', async () => { + updateCommentVariables = { + id: 'c1000', + content: 'Hello
', + } + + await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow( + 'Not Authorised!', + ) + }) + }) + }) + + describe('DeleteComment', () => { + const deleteCommentMutation = gql` + mutation($id: ID!) { + DeleteComment(id: $id) { + id + } + } + ` + + let deleteCommentVariables = { + id: 'c456', + } + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow( + 'Not Authorised', + ) + }) + }) + + describe('authenticated but not the author', () => { + beforeEach(async () => { + headers = await login({ + email: 'test@example.org', + password: '1234', + }) + client = new GraphQLClient(host, { + headers, + }) + }) + + it('throws authorization error', async () => { + await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow( + 'Not Authorised', + ) + }) + }) + + describe('authenticated as author', () => { + beforeEach(async () => { + headers = await login(authorParams) + client = new GraphQLClient(host, { + headers, + }) + }) + + it('deletes the comment', async () => { + const expected = { + DeleteComment: { + id: 'c456', + }, + } + await expect( + client.request(deleteCommentMutation, deleteCommentVariables), + ).resolves.toEqual(expected) + }) }) }) }) diff --git a/backend/src/schema/types/type/Comment.gql b/backend/src/schema/types/type/Comment.gql index 441fba179..4abebcba6 100644 --- a/backend/src/schema/types/type/Comment.gql +++ b/backend/src/schema/types/type/Comment.gql @@ -24,7 +24,7 @@ type Mutation { ): Comment UpdateComment( id: ID! - content: String + content: String! contentExcerpt: String deleted: Boolean disabled: Boolean diff --git a/webapp/components/Comment.vue b/webapp/components/Comment.vue index d8f63526f..7684c3f75 100644 --- a/webapp/components/Comment.vue +++ b/webapp/components/Comment.vue @@ -23,49 +23,61 @@ :modalsData="menuModalsData" style="float-right" :is-owner="isAuthor(author.id)" + @showEditCommentMenu="editCommentMenu" /> - - - -