diff --git a/backend/package.json b/backend/package.json index 16b0df487..d9672ec6b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -89,7 +89,7 @@ "@babel/plugin-proposal-throw-expressions": "^7.2.0", "@babel/preset-env": "~7.5.4", "@babel/register": "~7.4.4", - "apollo-server-testing": "~2.6.9", + "apollo-server-testing": "~2.7.0", "babel-core": "~7.0.0-0", "babel-eslint": "~10.0.2", "babel-jest": "~24.8.0", @@ -99,7 +99,7 @@ "eslint-config-prettier": "~6.0.0", "eslint-config-standard": "~12.0.0", "eslint-plugin-import": "~2.18.0", - "eslint-plugin-jest": "~22.8.0", + "eslint-plugin-jest": "~22.9.0", "eslint-plugin-node": "~9.1.0", "eslint-plugin-prettier": "~3.1.0", "eslint-plugin-promise": "~4.2.1", 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/backend/yarn.lock b/backend/yarn.lock index 23ebc2de3..d8f6991d2 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1336,6 +1336,14 @@ apollo-cache-control@0.7.5: apollo-server-env "2.4.0" graphql-extensions "0.7.7" +apollo-cache-control@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.0.tgz#08b157e5f8cd86f63608b05d45222de0725ebd5a" + integrity sha512-BBnfUmSWRws5dRSDD+R56RLJCE9v6xQuob+i/1Ju9EX4LZszU5JKVmxEvnkJ1bk/BkihjoQXTnP6fJCnt6fCmA== + dependencies: + apollo-server-env "2.4.0" + graphql-extensions "0.8.0" + apollo-cache-inmemory@~1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.2.tgz#bbf2e4e1eacdf82b2d526f5c2f3b37e5acee3c5e" @@ -1377,6 +1385,14 @@ apollo-datasource@0.5.0: apollo-server-caching "0.4.0" apollo-server-env "2.4.0" +apollo-datasource@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.0.tgz#823d6be8a3804613b5c56d2972c07db662293fc6" + integrity sha512-DOzzYWEOReYRu2vWPKEulqlTb9Xjg67sjVCzve5MXa7GUXjfr8IKioljvfoBMlqm/PpbJVk2ci4n5NIFqoYsrQ== + dependencies: + apollo-server-caching "0.5.0" + apollo-server-env "2.4.0" + apollo-engine-reporting-protobuf@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.3.1.tgz#a581257fa8e3bb115ce38bf1b22e052d1475ad69" @@ -1384,6 +1400,13 @@ apollo-engine-reporting-protobuf@0.3.1: dependencies: protobufjs "^6.8.6" +apollo-engine-reporting-protobuf@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.0.tgz#e34c192d86493b33a73181fd6be75721559111ec" + integrity sha512-cXHZSienkis8v4RhqB3YG3DkaksqLpcxApRLTpRMs7IXNozgV7CUPYGFyFBEra1ZFgUyHXx4G9MpelV+n2cCfA== + dependencies: + protobufjs "^6.8.6" + apollo-engine-reporting@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.6.tgz#579ba2da85ff848bd92be1b0f1ad61f0c57e3585" @@ -1396,6 +1419,18 @@ apollo-engine-reporting@1.3.6: async-retry "^1.2.1" graphql-extensions "0.7.7" +apollo-engine-reporting@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.0.tgz#3a9bd011b271593e16d7057044898d0a817b197d" + integrity sha512-NMiO3h1cuEBt6QZNGHxivwuyZQnoU/2MMx0gUA8Gyy1ERBhK6P235qoMnvoi34rLmqJuyGPX6tXcab8MpMIzYQ== + dependencies: + apollo-engine-reporting-protobuf "0.4.0" + apollo-graphql "^0.3.3" + apollo-server-env "2.4.0" + apollo-server-types "0.2.0" + async-retry "^1.2.1" + graphql-extensions "0.8.0" + apollo-env@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.5.1.tgz#b9b0195c16feadf0fe9fd5563edb0b9b7d9e97d3" @@ -1464,6 +1499,13 @@ apollo-server-caching@0.4.0: dependencies: lru-cache "^5.0.0" +apollo-server-caching@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.0.tgz#446a37ce2d4e24c81833e276638330a634f7bd46" + integrity sha512-l7ieNCGxUaUAVAAp600HjbUJxVaxjJygtPV0tPTe1Q3HkPy6LEWoY6mNHV7T268g1hxtPTxcdRu7WLsJrg7ufw== + dependencies: + lru-cache "^5.0.0" + apollo-server-core@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.9.tgz#75542ad206782e5c31a023b54962e9fdc6404a91" @@ -1490,6 +1532,34 @@ apollo-server-core@2.6.9: subscriptions-transport-ws "^0.9.11" ws "^6.0.0" +apollo-server-core@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.7.0.tgz#c444347dea11149b5b453890506e43dc7e711257" + integrity sha512-CXjXAkgcMBCJZpsZgfAY5W7f5thdxUhn75UgzeH28RTUZ2aKi/LjoCixPWRSF1lU4vuEWneAnM8Vg/KCD+29lQ== + dependencies: + "@apollographql/apollo-tools" "^0.3.6" + "@apollographql/graphql-playground-html" "1.6.24" + "@types/ws" "^6.0.0" + apollo-cache-control "0.8.0" + apollo-datasource "0.6.0" + apollo-engine-reporting "1.4.0" + apollo-engine-reporting-protobuf "0.4.0" + apollo-server-caching "0.5.0" + apollo-server-env "2.4.0" + apollo-server-errors "2.3.1" + apollo-server-plugin-base "0.6.0" + apollo-server-types "0.2.0" + apollo-tracing "0.8.0" + fast-json-stable-stringify "^2.0.0" + graphql-extensions "0.8.0" + graphql-subscriptions "^1.0.0" + graphql-tag "^2.9.2" + graphql-tools "^4.0.0" + graphql-upload "^8.0.2" + sha.js "^2.4.11" + subscriptions-transport-ws "^0.9.11" + ws "^6.0.0" + apollo-server-env@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.0.tgz#6611556c6b627a1636eed31317d4f7ea30705872" @@ -1526,12 +1596,28 @@ apollo-server-plugin-base@0.5.8: resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.8.tgz#77b4127aff4e3514a9d49e3cc61256aee4d9422e" integrity sha512-ICbaXr0ycQZL5llbtZhg8zyHbxuZ4khdAJsJgiZaUXXP6+F47XfDQ5uwnl/4Sq9fvkpwS0ctvfZ1D+Ks4NvUzA== -apollo-server-testing@~2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.9.tgz#6c1d20a89c0676bf32714405d729c302d62adfb1" - integrity sha512-MQfXAjNsI63O9sY60tQnGy102sqJSr++Yzm+IR44WrK3Z7FHUDisoh6UATly04EDGtO034xtqukzdUNQCK7+rw== +apollo-server-plugin-base@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.0.tgz#4186296ea5d52cfe613961d252a8a2f9e13e6ba6" + integrity sha512-BjfyWpHyKwHOe819gk3wEFwbnVp9Xvos03lkkYTTcXS/8G7xO78aUcE65mmyAC56/ZQ0aodNFkFrhwNtWBQWUQ== dependencies: - apollo-server-core "2.6.9" + apollo-server-types "0.2.0" + +apollo-server-testing@~2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.7.0.tgz#df8f8fb0df85f9781b6822fc92ee36fdc039b024" + integrity sha512-geBTK5T8mqZ2UOscC3oHQ/QoKMINLK+Mmq5ZfZe9UhmdXi+WqQ3/6B+3HMoQ7eQ7D6bNILe8b7N2EKuyUkePdg== + dependencies: + apollo-server-core "2.7.0" + +apollo-server-types@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.0.tgz#270d7298f709fd8237ebfa48753249e5286df5f2" + integrity sha512-5dgiyXsM90vnfmdXO1ixHvsLn0d9NP4tWufmr3ZmjKv00r4JAQNUaUdgOSGbRIKoHELQGwxUuTySTZ/tYfGaNQ== + dependencies: + apollo-engine-reporting-protobuf "0.4.0" + apollo-server-caching "0.5.0" + apollo-server-env "2.4.0" apollo-server@~2.6.9: version "2.6.9" @@ -1552,6 +1638,14 @@ apollo-tracing@0.7.4: apollo-server-env "2.4.0" graphql-extensions "0.7.7" +apollo-tracing@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.8.0.tgz#28cd9c61a4db12b2c24dad67fdedd309806c1650" + integrity sha512-cNOtOlyZ56iJRsCjnxjM1V0SnQ2ZZttuyoeOejdat6llPfk5bfYTVOKMjdbSfDvU33LS9g9sqNJCT0MwrEPFKQ== + dependencies: + apollo-server-env "2.4.0" + graphql-extensions "0.8.0" + apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9" @@ -2989,10 +3083,10 @@ eslint-plugin-import@~2.18.0: read-pkg-up "^2.0.0" resolve "^1.11.0" -eslint-plugin-jest@~22.8.0: - version "22.8.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.8.0.tgz#242ef5459e8da25d2c41438e95eb546e03d7fae1" - integrity sha512-2VftZMfILmlhL3VMq5ptHRIuyyXb3ShDEDb1J1UjvWNzm4l+UK/YmwNuTuJcM0gv8pJuOfiR/8ZptJ8Ou68pFw== +eslint-plugin-jest@~22.9.0: + version "22.9.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.9.0.tgz#2573dbcb4f1066b96a6e6d3b9aa439c80b28975a" + integrity sha512-V89BUiwf76FHlhj1mlNhNyvpzTy8VbWCh2RZpKYz/XDSl/pcuwFiE/LMt7r3q1sRKygzEMjbYeDob8MMuvakXg== eslint-plugin-node@~9.1.0: version "9.1.0" @@ -3649,6 +3743,15 @@ graphql-extensions@0.7.7: "@apollographql/apollo-tools" "^0.3.6" apollo-server-env "2.4.0" +graphql-extensions@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.8.0.tgz#b3fe7915aa84eef5a39135840840cc4d2e700c46" + integrity sha512-zV9RefkusIXqi9ZJtl7IJ5ecjDKdb7PLAb5E3CmxX3OK1GwNCIubp0vE7Fp4fXlCUKgTB1Woubs0zj71JT8o0A== + dependencies: + "@apollographql/apollo-tools" "^0.3.6" + apollo-server-env "2.4.0" + apollo-server-types "0.2.0" + graphql-iso-date@~3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/graphql-iso-date/-/graphql-iso-date-3.6.1.tgz#bd2d0dc886e0f954cbbbc496bbf1d480b57ffa96" diff --git a/package.json b/package.json index b4447bdd2..87ec4e9ef 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "cross-env": "^5.2.0", "cypress": "^3.4.0", "cypress-cucumber-preprocessor": "^1.12.0", - "cypress-file-upload": "^3.3.1", + "cypress-file-upload": "^3.3.2", "cypress-plugin-retries": "^1.2.2", "dotenv": "^8.0.0", "faker": "Marak/faker.js#master", 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" /> - - - -