From 154082a251a944633395594f326569de50191f15 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Thu, 2 May 2019 15:54:44 -0300 Subject: [PATCH 01/13] Add authorship to commments at creation - Co-authored-by: Mike Aono --- backend/src/resolvers/comments.js | 10 +++++++ backend/src/resolvers/comments.spec.js | 28 ++++++++++++++---- backend/src/seed/seed-db.js | 39 ++++++++------------------ 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/backend/src/resolvers/comments.js b/backend/src/resolvers/comments.js index 97b99f4ab..b2dc0fbcf 100644 --- a/backend/src/resolvers/comments.js +++ b/backend/src/resolvers/comments.js @@ -44,6 +44,16 @@ export default { commentId: comment.id } ) + + await session.run(` + MATCH (comment:Comment {id: $commentId}), (author:User {id: $userId}) + MERGE (comment)<-[:WROTE]-(author) + RETURN comment {.id, .content}`, { + commentId: comment.id, + userId: context.user.id + } + ) + session.close() return comment diff --git a/backend/src/resolvers/comments.spec.js b/backend/src/resolvers/comments.spec.js index 34a18d807..e572e206b 100644 --- a/backend/src/resolvers/comments.spec.js +++ b/backend/src/resolvers/comments.spec.js @@ -19,12 +19,12 @@ afterEach(async () => { describe('CreateComment', () => { const mutation = ` - mutation($postId: ID, $content: String!) { - CreateComment(postId: $postId, content: $content) { - id - content - } + mutation($postId: ID, $content: String!) { + CreateComment(postId: $postId, content: $content) { + id + content } + } ` describe('unauthenticated', () => { it('throws authorization error', async () => { @@ -58,6 +58,24 @@ describe('CreateComment', () => { await expect(client.request(mutation, variables)).resolves.toMatchObject(expected) }) + it('assigns the authenticated user as author', async () => { + variables = { + postId: 'p1', + content: 'I\'m authorised to comment' + } + await client.request(mutation, variables) + + 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 as content', async () => { variables = { postId: 'p1', 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) }' From 346158da7fba9da7de1a915d5da273023fa06a67 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Thu, 2 May 2019 19:55:54 -0300 Subject: [PATCH 02/13] Update to use autogenerated Comment Query - using the custom CommentByPost Query was not returning the author of the Post, therefore the users avatar was not showing up - there are some weird bugs with the comments, if you dblclick on the button, two comments with the same content are created - sometimes the comments appear as the bottom of the list, sometimes they appear three comments from the bottom(?) --- backend/src/resolvers/comments.js | 45 +++++++-------------------- webapp/graphql/CommentQuery.js | 26 ++++++++++++++-- webapp/pages/post/_id/_slug/index.vue | 6 ++-- 3 files changed, 38 insertions(+), 39 deletions(-) diff --git a/backend/src/resolvers/comments.js b/backend/src/resolvers/comments.js index b2dc0fbcf..56280d6ea 100644 --- a/backend/src/resolvers/comments.js +++ b/backend/src/resolvers/comments.js @@ -3,26 +3,6 @@ import { UserInputError } from 'apollo-server' const COMMENT_MIN_LENGTH = 1 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() @@ -31,29 +11,26 @@ export default { 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) const session = context.driver.session() await session.run(` - MATCH (post:Post {id: $postId}), (comment:Comment {id: $commentId}) - MERGE (post)<-[:COMMENTS]-(comment) - RETURN comment {.id, .content}`, { + MATCH (author:User {id: $userId}), (comment:Comment {id: $commentId}) + MERGE (comment)<-[:WROTE]-(author) + RETURN author`, { + userId: context.user.id, + commentId: comment.id + } + ) + await session.run(` + MATCH (post:Post {id: $postId}), (comment:Comment {id: $commentId}) + MERGE (post)<-[:COMMENTS]-(comment) + RETURN post`, { postId, commentId: comment.id } ) - - await session.run(` - MATCH (comment:Comment {id: $commentId}), (author:User {id: $userId}) - MERGE (comment)<-[:WROTE]-(author) - RETURN comment {.id, .content}`, { - commentId: comment.id, - userId: context.user.id - } - ) - session.close() return comment diff --git a/webapp/graphql/CommentQuery.js b/webapp/graphql/CommentQuery.js index 299916823..0e61a8a6c 100644 --- a/webapp/graphql/CommentQuery.js +++ b/webapp/graphql/CommentQuery.js @@ -1,12 +1,34 @@ import gql from 'graphql-tag' export default app => { + const lang = app.$i18n.locale().toUpperCase() return gql(` - query CommentByPost($postId: ID!) { - CommentByPost(postId: $postId) { + query Comment($postId: ID) { + Comment(postId: $postId) { id contentExcerpt createdAt + author { + id + slug + name + avatar + disabled + deleted + shoutedCount + contributionsCount + commentsCount + followedByCount + followedByCurrentUser + location { + name: name${lang} + } + badges { + id + key + icon + } + } } } `) diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue index f83925f68..7ae683425 100644 --- a/webapp/pages/post/_id/_slug/index.vue +++ b/webapp/pages/post/_id/_slug/index.vue @@ -179,7 +179,7 @@ export default { this.post = post[0] || {} this.title = this.post.title }, - CommentByPost(comments) { + Comment(comments) { this.comments = comments || [] } }, @@ -291,11 +291,11 @@ export default { return this.$store.getters['auth/user'].id === id }, addComment(comment) { - this.$apollo.queries.CommentByPost.refetch() + this.$apollo.queries.Comment.refetch() } }, apollo: { - CommentByPost: { + Comment: { query() { return require('~/graphql/CommentQuery.js').default(this) }, From 40f5b6acd5545dcbf8a5bc60abed29cf9b2bb435 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Thu, 2 May 2019 21:03:31 -0300 Subject: [PATCH 03/13] Extract CommentsList component - this allows to Query for comments by Post --- webapp/components/CommentForm/index.vue | 2 +- webapp/components/CommentList/index.vue | 79 +++++++++++++++++++++++++ webapp/graphql/PostCommentsQuery.js | 40 +++++++++++++ webapp/pages/post/_id/_slug/index.vue | 62 ++----------------- 4 files changed, 125 insertions(+), 58 deletions(-) create mode 100644 webapp/components/CommentList/index.vue create mode 100644 webapp/graphql/PostCommentsQuery.js diff --git a/webapp/components/CommentForm/index.vue b/webapp/components/CommentForm/index.vue index fdb1761dc..6019b0965 100644 --- a/webapp/components/CommentForm/index.vue +++ b/webapp/components/CommentForm/index.vue @@ -91,7 +91,7 @@ export default { } }) .then(res => { - this.$emit('addComment', res.data.CreateComment) + this.$root.$emit('addComment', res.data.CreateComment) this.$refs.editor.clear() this.$toast.success(this.$t('post.comment.submitted')) }) diff --git a/webapp/components/CommentList/index.vue b/webapp/components/CommentList/index.vue new file mode 100644 index 000000000..ba9ada717 --- /dev/null +++ b/webapp/components/CommentList/index.vue @@ -0,0 +1,79 @@ + + diff --git a/webapp/graphql/PostCommentsQuery.js b/webapp/graphql/PostCommentsQuery.js new file mode 100644 index 000000000..2c37f2933 --- /dev/null +++ b/webapp/graphql/PostCommentsQuery.js @@ -0,0 +1,40 @@ +import gql from 'graphql-tag' + +export default app => { + const lang = app.$i18n.locale().toUpperCase() + return gql(` + query Post($slug: String!) { + Post(slug: $slug) { + commentsCount + comments(orderBy: createdAt_asc) { + id + contentExcerpt + createdAt + disabled + deleted + author { + id + slug + name + avatar + disabled + deleted + shoutedCount + contributionsCount + commentsCount + followedByCount + followedByCurrentUser + location { + name: name${lang} + } + badges { + id + key + icon + } + } + } + } + } + `) +} diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue index 7ae683425..692dbe69e 100644 --- a/webapp/pages/post/_id/_slug/index.vue +++ b/webapp/pages/post/_id/_slug/index.vue @@ -96,39 +96,9 @@ -

- - - {{ comments.length }}  Comments - -

+ -
- -
- - - +
@@ -142,9 +112,8 @@ import HcTag from '~/components/Tag' import ContentMenu from '~/components/ContentMenu' import HcUser from '~/components/User' import HcShoutButton from '~/components/ShoutButton.vue' -import HcEmpty from '~/components/Empty.vue' import HcCommentForm from '~/components/CommentForm' -import Comment from '~/components/Comment.vue' +import HcCommentList from '~/components/CommentList' export default { transition: { @@ -156,10 +125,9 @@ export default { HcCategory, HcUser, HcShoutButton, - HcEmpty, - Comment, ContentMenu, - HcCommentForm + HcCommentForm, + HcCommentList }, head() { return { @@ -169,7 +137,6 @@ export default { data() { return { post: null, - comments: null, ready: false, title: 'loading' } @@ -178,9 +145,6 @@ export default { Post(post) { this.post = post[0] || {} this.title = this.post.title - }, - Comment(comments) { - this.comments = comments || [] } }, async asyncData(context) { @@ -289,22 +253,6 @@ export default { methods: { isAuthor(id) { return this.$store.getters['auth/user'].id === id - }, - addComment(comment) { - this.$apollo.queries.Comment.refetch() - } - }, - apollo: { - Comment: { - query() { - return require('~/graphql/CommentQuery.js').default(this) - }, - variables() { - return { - postId: this.post.id - } - }, - fetchPolicy: 'cache-and-network' } } } From 957d8943227bef2c4c4c8acd8352c1e00c969254 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Fri, 3 May 2019 11:18:14 -0300 Subject: [PATCH 04/13] Start writing component test - restructured directories --- .../comments/CommentForm/CommentForm.test.js | 74 +++++++++++++++++++ .../{ => comments}/CommentForm/index.vue | 2 + .../{ => comments}/CommentList/index.vue | 0 webapp/graphql/PostCommentsQuery.js | 1 - webapp/pages/post/_id/_slug/index.vue | 4 +- 5 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 webapp/components/comments/CommentForm/CommentForm.test.js rename webapp/components/{ => comments}/CommentForm/index.vue (96%) rename webapp/components/{ => comments}/CommentList/index.vue (100%) diff --git a/webapp/components/comments/CommentForm/CommentForm.test.js b/webapp/components/comments/CommentForm/CommentForm.test.js new file mode 100644 index 000000000..6453f689a --- /dev/null +++ b/webapp/components/comments/CommentForm/CommentForm.test.js @@ -0,0 +1,74 @@ +import { config, mount, createLocalVue } from '@vue/test-utils' +import CommentForm from './index.vue' +import Vue from 'vue' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Styleguide) + +config.stubs['no-ssr'] = '' + +describe('CommentForm.vue', () => { + let mocks + let wrapper + let form + let propsData + let submitBtn + let cancelBtn + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $apollo: { + mutate: jest + .fn() + .mockRejectedValue({ message: 'Ouch!' }) + .mockResolvedValueOnce({ data: { CreateComment: { contentExcerpt: 'this is a comment' } } }) + }, + $toast: { + error: jest.fn(), + success: jest.fn() + }, + $root: { + $emit: jest.fn() + } + }, + propsData = { + post: { id: 1 } + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(CommentForm, { mocks, localVue, propsData }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('calls the apollo mutation when form is submitted', () => { + wrapper.vm.updateEditorContent('ok') + form = wrapper.find('form') + form.trigger('submit') + expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1) + }) + + it("calls clear method when the cancel button is clicked", () => { + const spy = jest.spyOn(wrapper.vm, 'clear') + wrapper.vm.updateEditorContent('ok') + cancelBtn = wrapper.find('.cancelBtn') + cancelBtn.trigger('click') + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('shows a success toaster if the mutation resolves', () => { + wrapper.vm.updateEditorContent('ok') + form = wrapper.find('form') + form.trigger('submit') + expect(mocks.$root.$emit).toHaveBeenCalledTimes(1) + expect(mocks.$toast.success).toHaveBeenCalledTimes(1) + }) + }) +}) \ No newline at end of file diff --git a/webapp/components/CommentForm/index.vue b/webapp/components/comments/CommentForm/index.vue similarity index 96% rename from webapp/components/CommentForm/index.vue rename to webapp/components/comments/CommentForm/index.vue index 6019b0965..4377e2e2c 100644 --- a/webapp/components/CommentForm/index.vue +++ b/webapp/components/comments/CommentForm/index.vue @@ -21,6 +21,7 @@ :disabled="disabled" ghost @click.prevent="clear" + class="cancelBtn" > {{ $t('actions.cancel') }} @@ -91,6 +92,7 @@ export default { } }) .then(res => { + // console.log(this.$toast.success.mockResolvedValue()) this.$root.$emit('addComment', res.data.CreateComment) this.$refs.editor.clear() this.$toast.success(this.$t('post.comment.submitted')) diff --git a/webapp/components/CommentList/index.vue b/webapp/components/comments/CommentList/index.vue similarity index 100% rename from webapp/components/CommentList/index.vue rename to webapp/components/comments/CommentList/index.vue diff --git a/webapp/graphql/PostCommentsQuery.js b/webapp/graphql/PostCommentsQuery.js index 2c37f2933..c0ab1320a 100644 --- a/webapp/graphql/PostCommentsQuery.js +++ b/webapp/graphql/PostCommentsQuery.js @@ -5,7 +5,6 @@ export default app => { return gql(` query Post($slug: String!) { Post(slug: $slug) { - commentsCount comments(orderBy: createdAt_asc) { id contentExcerpt diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue index 692dbe69e..87538eb6d 100644 --- a/webapp/pages/post/_id/_slug/index.vue +++ b/webapp/pages/post/_id/_slug/index.vue @@ -112,8 +112,8 @@ import HcTag from '~/components/Tag' import ContentMenu from '~/components/ContentMenu' import HcUser from '~/components/User' import HcShoutButton from '~/components/ShoutButton.vue' -import HcCommentForm from '~/components/CommentForm' -import HcCommentList from '~/components/CommentList' +import HcCommentForm from '~/components/comments/CommentForm' +import HcCommentList from '~/components/comments/CommentList' export default { transition: { From ebc290c6af3ebd51a297fc58db472bcf4c454e77 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Fri, 3 May 2019 13:39:43 -0300 Subject: [PATCH 05/13] Delete CommentForm.test.js, remove console.log - this test is not part of this PR and I'll create another issue and put in a different PR for it --- .../comments/CommentForm/CommentForm.test.js | 74 ------------------- .../components/comments/CommentForm/index.vue | 3 +- 2 files changed, 1 insertion(+), 76 deletions(-) delete mode 100644 webapp/components/comments/CommentForm/CommentForm.test.js diff --git a/webapp/components/comments/CommentForm/CommentForm.test.js b/webapp/components/comments/CommentForm/CommentForm.test.js deleted file mode 100644 index 6453f689a..000000000 --- a/webapp/components/comments/CommentForm/CommentForm.test.js +++ /dev/null @@ -1,74 +0,0 @@ -import { config, mount, createLocalVue } from '@vue/test-utils' -import CommentForm from './index.vue' -import Vue from 'vue' -import Styleguide from '@human-connection/styleguide' - -const localVue = createLocalVue() - -localVue.use(Styleguide) - -config.stubs['no-ssr'] = '' - -describe('CommentForm.vue', () => { - let mocks - let wrapper - let form - let propsData - let submitBtn - let cancelBtn - - beforeEach(() => { - mocks = { - $t: jest.fn(), - $apollo: { - mutate: jest - .fn() - .mockRejectedValue({ message: 'Ouch!' }) - .mockResolvedValueOnce({ data: { CreateComment: { contentExcerpt: 'this is a comment' } } }) - }, - $toast: { - error: jest.fn(), - success: jest.fn() - }, - $root: { - $emit: jest.fn() - } - }, - propsData = { - post: { id: 1 } - } - }) - - describe('mount', () => { - const Wrapper = () => { - return mount(CommentForm, { mocks, localVue, propsData }) - } - - beforeEach(() => { - wrapper = Wrapper() - }) - - it('calls the apollo mutation when form is submitted', () => { - wrapper.vm.updateEditorContent('ok') - form = wrapper.find('form') - form.trigger('submit') - expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1) - }) - - it("calls clear method when the cancel button is clicked", () => { - const spy = jest.spyOn(wrapper.vm, 'clear') - wrapper.vm.updateEditorContent('ok') - cancelBtn = wrapper.find('.cancelBtn') - cancelBtn.trigger('click') - expect(spy).toHaveBeenCalledTimes(1) - }) - - it('shows a success toaster if the mutation resolves', () => { - wrapper.vm.updateEditorContent('ok') - form = wrapper.find('form') - form.trigger('submit') - expect(mocks.$root.$emit).toHaveBeenCalledTimes(1) - expect(mocks.$toast.success).toHaveBeenCalledTimes(1) - }) - }) -}) \ No newline at end of file diff --git a/webapp/components/comments/CommentForm/index.vue b/webapp/components/comments/CommentForm/index.vue index 4377e2e2c..585b97dba 100644 --- a/webapp/components/comments/CommentForm/index.vue +++ b/webapp/components/comments/CommentForm/index.vue @@ -20,8 +20,8 @@ {{ $t('actions.cancel') }} @@ -92,7 +92,6 @@ export default { } }) .then(res => { - // console.log(this.$toast.success.mockResolvedValue()) this.$root.$emit('addComment', res.data.CreateComment) this.$refs.editor.clear() this.$toast.success(this.$t('post.comment.submitted')) From 31c8b6e35d2af573c2ef1b1088d381a314a25986 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Fri, 3 May 2019 13:41:20 -0300 Subject: [PATCH 06/13] Use single cypher query in CreateComments - to create relation between post, comment, author - fix test to create a post so said cypher query doesn't silently fail --- backend/src/resolvers/comments.js | 13 ++------ backend/src/resolvers/comments.spec.js | 44 ++++++++++++++++---------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/backend/src/resolvers/comments.js b/backend/src/resolvers/comments.js index 56280d6ea..eb792ecb8 100644 --- a/backend/src/resolvers/comments.js +++ b/backend/src/resolvers/comments.js @@ -16,17 +16,10 @@ export default { const session = context.driver.session() await session.run(` - MATCH (author:User {id: $userId}), (comment:Comment {id: $commentId}) - MERGE (comment)<-[:WROTE]-(author) - RETURN author`, { - userId: context.user.id, - commentId: comment.id - } - ) - await session.run(` - MATCH (post:Post {id: $postId}), (comment:Comment {id: $commentId}) - MERGE (post)<-[:COMMENTS]-(comment) + 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 e572e206b..9a54acd17 100644 --- a/backend/src/resolvers/comments.spec.js +++ b/backend/src/resolvers/comments.spec.js @@ -4,7 +4,8 @@ import { host, login } from '../jest/helpers' const factory = Factory() let client -let variables +let createCommentVariables +let createPostVariables beforeEach(async () => { await factory.create('User', { @@ -18,7 +19,7 @@ afterEach(async () => { }) describe('CreateComment', () => { - const mutation = ` + const createCommentMutation = ` mutation($postId: ID, $content: String!) { CreateComment(postId: $postId, content: $content) { id @@ -26,14 +27,21 @@ describe('CreateComment', () => { } } ` + const createPostMutation = ` + mutation($id: ID!, $title: String!, $content: String!) { + CreatePost(id: $id, title: $title, content: $content) { + id + } + } + ` 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,28 +50,30 @@ 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' } + }) + + 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('assigns the authenticated user as author', async () => { - variables = { - postId: 'p1', - content: 'I\'m authorised to comment' + createPostVariables = { + id: 'p1', + title: 'post to comment on', + content: 'please comment on me' } - await client.request(mutation, variables) + await client.request(createPostMutation, createPostVariables) + await client.request(createCommentMutation, createCommentVariables) const { User } = await client.request(`{ User(email: "test@example.org") { @@ -77,22 +87,22 @@ describe('CreateComment', () => { }) it('throw an error if an empty string is sent as content', async () => { - variables = { + 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 = { + 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!') }) }) From d1c86827104fb2df3c3e90d4e279ae5ea8430fa1 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Fri, 3 May 2019 15:47:54 -0300 Subject: [PATCH 07/13] Update method name to be more descriptive/accurate --- webapp/components/comments/CommentForm/index.vue | 2 +- webapp/components/comments/CommentList/index.vue | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/webapp/components/comments/CommentForm/index.vue b/webapp/components/comments/CommentForm/index.vue index 585b97dba..9f5197a06 100644 --- a/webapp/components/comments/CommentForm/index.vue +++ b/webapp/components/comments/CommentForm/index.vue @@ -92,7 +92,7 @@ export default { } }) .then(res => { - this.$root.$emit('addComment', res.data.CreateComment) + this.$root.$emit('refetchPostComments', res.data.CreateComment) this.$refs.editor.clear() this.$toast.success(this.$t('post.comment.submitted')) }) diff --git a/webapp/components/comments/CommentList/index.vue b/webapp/components/comments/CommentList/index.vue index ba9ada717..57b720087 100644 --- a/webapp/components/comments/CommentList/index.vue +++ b/webapp/components/comments/CommentList/index.vue @@ -26,6 +26,7 @@ @@ -44,21 +45,21 @@ export default { }, data() { return { - comments: null + comments: [] } }, watch: { Post(post) { - this.comments = post[0].comments || {} + this.comments = post[0].comments || [] } }, mounted() { - this.$root.$on('addComment', comment => { - this.addComment(comment) + this.$root.$on('refetchPostComments', comment => { + this.refetchPostComments(comment) }) }, methods: { - addComment(comment) { + refetchPostComments(comment) { this.$apollo.queries.Post.refetch() } }, From da218f8a58b2dd596f0f0a92174bd43dce37abe5 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Fri, 3 May 2019 15:48:20 -0300 Subject: [PATCH 08/13] Add component test for extracted CommentList.vue --- .../comments/CommentList/CommentList.spec.js | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 webapp/components/comments/CommentList/CommentList.spec.js diff --git a/webapp/components/comments/CommentList/CommentList.spec.js b/webapp/components/comments/CommentList/CommentList.spec.js new file mode 100644 index 000000000..fa6394e53 --- /dev/null +++ b/webapp/components/comments/CommentList/CommentList.spec.js @@ -0,0 +1,64 @@ +import { config, mount, createLocalVue } from '@vue/test-utils' +import CommentList from '.' +import Empty from '~/components/Empty' +import Vue from 'vue' +import Vuex from 'vuex' +import Filters from '~/plugins/vue-filters' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Styleguide) +localVue.use(Vuex) +localVue.filter('truncate', string => string) + +config.stubs['v-popover'] = '' +config.stubs['nuxt-link'] = '' +config.stubs['no-ssr'] = '' + +describe('CommentList.vue', () => { + let mocks + let store + let wrapper + let propsData + + propsData = { + post: { id: 1 } + } + store = new Vuex.Store({ + getters: { + 'auth/user': () => { + return {} + } + } + }) + mocks = { + $t: jest.fn() + } + + describe('shallowMount', () => { + const Wrapper = () => { + return mount(CommentList, { store, mocks, localVue, propsData }) + } + + beforeEach(() => { + wrapper = Wrapper() + wrapper.setData({ + comments: [{ id: 'c1', contentExcerpt: 'this is a comment' }] + }) + }) + + it('displays a comments counter', () => { + expect(wrapper.find('span.ds-tag').text()).toEqual('1') + }) + + it('displays comments when there are comments to display', () => { + expect(wrapper.find('div#comments').text()).toEqual('this is a comment') + }) + + it('displays a message icon when there are no comments to display', () => { + wrapper.setData({ comments: [] }) + expect(wrapper.findAll(Empty)).toHaveLength(1) + }) + }) +}) From 70fb34345dde30ae4df302d80e9b9ae721168e42 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Fri, 3 May 2019 15:52:26 -0300 Subject: [PATCH 09/13] Improve test by mounting CommentList with data --- .../comments/CommentList/CommentList.spec.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/webapp/components/comments/CommentList/CommentList.spec.js b/webapp/components/comments/CommentList/CommentList.spec.js index fa6394e53..6a96f3e19 100644 --- a/webapp/components/comments/CommentList/CommentList.spec.js +++ b/webapp/components/comments/CommentList/CommentList.spec.js @@ -21,6 +21,7 @@ describe('CommentList.vue', () => { let store let wrapper let propsData + let data propsData = { post: { id: 1 } @@ -35,10 +36,15 @@ describe('CommentList.vue', () => { mocks = { $t: jest.fn() } + data = () => { + return { + comments: [] + } + } describe('shallowMount', () => { const Wrapper = () => { - return mount(CommentList, { store, mocks, localVue, propsData }) + return mount(CommentList, { store, mocks, localVue, propsData, data }) } beforeEach(() => { @@ -48,6 +54,10 @@ describe('CommentList.vue', () => { }) }) + it('displays a message icon when there are no comments to display', () => { + expect(Wrapper().findAll(Empty)).toHaveLength(1) + }) + it('displays a comments counter', () => { expect(wrapper.find('span.ds-tag').text()).toEqual('1') }) @@ -55,10 +65,5 @@ describe('CommentList.vue', () => { it('displays comments when there are comments to display', () => { expect(wrapper.find('div#comments').text()).toEqual('this is a comment') }) - - it('displays a message icon when there are no comments to display', () => { - wrapper.setData({ comments: [] }) - expect(wrapper.findAll(Empty)).toHaveLength(1) - }) }) }) From 4d631c452d09bc5ea44cb3a04aec14c4a917cea7 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Sat, 4 May 2019 17:42:42 -0300 Subject: [PATCH 10/13] Add more unit tests, add back postId deletion --- backend/src/resolvers/comments.js | 27 +++++++- backend/src/resolvers/comments.spec.js | 85 +++++++++++++++++++++++--- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/backend/src/resolvers/comments.js b/backend/src/resolvers/comments.js index eb792ecb8..d4775b235 100644 --- a/backend/src/resolvers/comments.js +++ b/backend/src/resolvers/comments.js @@ -2,18 +2,41 @@ 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 { 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 - 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}), (author:User {id: $userId}) diff --git a/backend/src/resolvers/comments.spec.js b/backend/src/resolvers/comments.spec.js index 9a54acd17..87a0df270 100644 --- a/backend/src/resolvers/comments.spec.js +++ b/backend/src/resolvers/comments.spec.js @@ -6,6 +6,8 @@ const factory = Factory() let client let createCommentVariables let createPostVariables +let createCommentVariablesSansPostId +let createCommentVariablesWithNonExistentPost beforeEach(async () => { await factory.create('User', { @@ -34,6 +36,13 @@ describe('CreateComment', () => { } } ` + const commentQueryForPostId = ` + query($content: String) { + Comment(content: $content) { + postId + } + } + ` describe('unauthenticated', () => { it('throws authorization error', async () => { createCommentVariables = { @@ -54,6 +63,12 @@ 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) }) it('creates a comment', async () => { @@ -67,12 +82,6 @@ describe('CreateComment', () => { }) it('assigns the authenticated user as author', async () => { - createPostVariables = { - id: 'p1', - title: 'post to comment on', - content: 'please comment on me' - } - await client.request(createPostMutation, createPostVariables) await client.request(createCommentMutation, createCommentVariables) const { User } = await client.request(`{ @@ -86,7 +95,7 @@ describe('CreateComment', () => { expect(User).toEqual([ { comments: [ { content: 'I\'m authorised to comment' } ] } ]) }) - it('throw an error if an empty string is sent as content', async () => { + it('throw an error if an empty string is sent from the editor as content', async () => { createCommentVariables = { postId: 'p1', content: '

' @@ -96,7 +105,7 @@ describe('CreateComment', () => { .rejects.toThrow('Comment must be at least 1 character long!') }) - it('throws an error if a comment does not contain a single character', async () => { + it('throws an error if a comment sent from the editor does not contain a single character', async () => { createCommentVariables = { postId: 'p1', content: '

' @@ -105,5 +114,65 @@ describe('CreateComment', () => { 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 }]) + }) }) }) From 1fff9bbc46563d5a2f8f0e90dfc4a43056faa0c5 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Sat, 4 May 2019 18:46:34 -0300 Subject: [PATCH 11/13] Add cypress test to expose comment creation bug - at the moment, one can create the same comment by clicking rapidly on the "Comment" button - an idea for a fix https://stackoverflow.com/questions/53101521/prevent-repeated-queries-in-apollo-server-2 --- cypress/integration/common/post.js | 19 +++++++++++++++++++ cypress/integration/common/steps.js | 1 + cypress/integration/post/Comment.feature | 11 +++++++++++ .../components/comments/CommentForm/index.vue | 6 ++++++ 4 files changed, 37 insertions(+) diff --git a/cypress/integration/common/post.js b/cypress/integration/common/post.js index 85a9f3339..ca7e62b30 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,9 +14,26 @@ 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', () => { cy.get('.ProseMirror p') .should('have.class', 'is-empty') }) + +Then('I rapidly double click on the {string} button', text => { + cy.get('button').contains(text).click().click() +}) + +Then('I should see my comment once', () => { + cy.get('div.comment p') + .should('contain', 'Human Connection rocks') + .and('have.length', 1) + .get('.ds-avatar img') + .should('have.attr', 'src') + .and('contain', narratorAvatar) + .and('have.length', 1) +}) \ No newline at end of file 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/cypress/integration/post/Comment.feature b/cypress/integration/post/Comment.feature index e7e462824..afac543d9 100644 --- a/cypress/integration/post/Comment.feature +++ b/cypress/integration/post/Comment.feature @@ -20,3 +20,14 @@ Feature: Post Comment Then my comment should be successfully created And I should see my comment And the editor should be cleared + + Scenario: Prevention of multiple comment creation + Given I visit "post/bWBjpkTKZp/101-essays" + And I type in the following text: + """ + Human Connection rocks + """ + And I rapidly double click on the "Comment" button + Then my comment should be successfully created + And I should see my comment once + And the editor should be cleared diff --git a/webapp/components/comments/CommentForm/index.vue b/webapp/components/comments/CommentForm/index.vue index 9f5197a06..3efb3a625 100644 --- a/webapp/components/comments/CommentForm/index.vue +++ b/webapp/components/comments/CommentForm/index.vue @@ -29,6 +29,7 @@ @@ -56,6 +57,7 @@ export default { data() { return { disabled: true, + loading: false, form: { content: '' }, @@ -76,6 +78,8 @@ export default { this.$refs.editor.clear() }, handleSubmit() { + this.loading = true + this.loading = false this.$apollo .mutate({ mutation: gql` @@ -92,9 +96,11 @@ export default { } }) .then(res => { + this.loading = false this.$root.$emit('refetchPostComments', res.data.CreateComment) this.$refs.editor.clear() this.$toast.success(this.$t('post.comment.submitted')) + this.disabled = false }) .catch(err => { this.$toast.error(err.message) From ad46c2d059671b43c81cfb51f0bfc42590a61f90 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Sun, 5 May 2019 12:26:02 -0300 Subject: [PATCH 12/13] Remove cypress test - it is not part of the scope of this ticket, and therefore should be extracted and dealt with separately --- cypress/integration/common/post.js | 14 -------------- cypress/integration/post/Comment.feature | 11 ----------- webapp/components/comments/CommentForm/index.vue | 2 +- 3 files changed, 1 insertion(+), 26 deletions(-) diff --git a/cypress/integration/common/post.js b/cypress/integration/common/post.js index ca7e62b30..f6a1bbedd 100644 --- a/cypress/integration/common/post.js +++ b/cypress/integration/common/post.js @@ -23,17 +23,3 @@ Then('the editor should be cleared', () => { cy.get('.ProseMirror p') .should('have.class', 'is-empty') }) - -Then('I rapidly double click on the {string} button', text => { - cy.get('button').contains(text).click().click() -}) - -Then('I should see my comment once', () => { - cy.get('div.comment p') - .should('contain', 'Human Connection rocks') - .and('have.length', 1) - .get('.ds-avatar img') - .should('have.attr', 'src') - .and('contain', narratorAvatar) - .and('have.length', 1) -}) \ No newline at end of file diff --git a/cypress/integration/post/Comment.feature b/cypress/integration/post/Comment.feature index afac543d9..e7e462824 100644 --- a/cypress/integration/post/Comment.feature +++ b/cypress/integration/post/Comment.feature @@ -20,14 +20,3 @@ Feature: Post Comment Then my comment should be successfully created And I should see my comment And the editor should be cleared - - Scenario: Prevention of multiple comment creation - Given I visit "post/bWBjpkTKZp/101-essays" - And I type in the following text: - """ - Human Connection rocks - """ - And I rapidly double click on the "Comment" button - Then my comment should be successfully created - And I should see my comment once - And the editor should be cleared diff --git a/webapp/components/comments/CommentForm/index.vue b/webapp/components/comments/CommentForm/index.vue index 3efb3a625..d59314e07 100644 --- a/webapp/components/comments/CommentForm/index.vue +++ b/webapp/components/comments/CommentForm/index.vue @@ -79,7 +79,7 @@ export default { }, handleSubmit() { this.loading = true - this.loading = false + this.disabled = true this.$apollo .mutate({ mutation: gql` From 4c09268f49f1911bae816f607cd35dc29c166779 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Sun, 5 May 2019 13:57:12 -0300 Subject: [PATCH 13/13] Update tests after backend validations - Now a comment cannot be created without a post to associate it with --- backend/src/resolvers/moderation.spec.js | 48 ++++++++++++++---------- backend/src/resolvers/reports.spec.js | 11 +++++- 2 files changed, 38 insertions(+), 21 deletions(-) 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' } })