From 314dfcf413bd8a458d4780142688a7e55e521dc8 Mon Sep 17 00:00:00 2001 From: kachulio1 Date: Thu, 15 Aug 2019 17:55:02 +0300 Subject: [PATCH 01/19] Throw error if no categories on post creation Co-authored-by: mattwr18 --- .../validation/validationMiddleware.js | 9 + backend/src/schema/resolvers/posts.js | 1 + backend/src/schema/resolvers/posts.spec.js | 165 ++++++++---------- backend/src/seed/factories/posts.js | 5 +- 4 files changed, 91 insertions(+), 89 deletions(-) diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index 2d354ad2b..04f6b92a4 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -40,9 +40,18 @@ const validateUpdateComment = async (resolve, root, args, context, info) => { return resolve(root, args, context, info) } +const validateCreatePost = async (resolve, root, args, context, info) => { + const { categoryIds } = args + if (!Array.isArray(categoryIds) || !categoryIds.length) { + throw new UserInputError('You cannot create a post without at least one category') + } + return resolve(root, args, context, info) +} + export default { Mutation: { CreateComment: validateCommentCreation, UpdateComment: validateUpdateComment, + CreatePost: validateCreatePost, }, } diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index ea1f680bd..37710dc38 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -110,6 +110,7 @@ export default { const { categoryIds } = params delete params.categoryIds params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) + params.id = params.id || uuid() let createPostCypher = `CREATE (post:Post {params}) diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 15376c8a4..ab90af27e 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -19,20 +19,9 @@ const oldTitle = 'Old title' const oldContent = 'Old content' const newTitle = 'New title' const newContent = 'New content' -const createPostVariables = { title: postTitle, content: postContent } -const createPostWithCategoriesMutation = gql` - mutation($title: String!, $content: String!, $categoryIds: [ID]) { - CreatePost(title: $title, content: $content, categoryIds: $categoryIds) { - id - title - } - } -` -const createPostWithCategoriesVariables = { - title: postTitle, - content: postContent, - categoryIds: ['cat9', 'cat4', 'cat15'], -} +const categoryIds = ['cat9', 'cat4', 'cat15'] +let createPostVariables + const postQueryWithCategories = gql` query($id: ID) { Post(id: $id) { @@ -42,11 +31,6 @@ const postQueryWithCategories = gql` } } ` -const createPostWithoutCategoriesVariables = { - title: 'This is a post without categories', - content: 'I should be able to filter it out', - categoryIds: null, -} const postQueryFilteredByCategory = gql` query Post($filter: _PostFilter) { Post(filter: $filter) { @@ -58,14 +42,14 @@ const postQueryFilteredByCategory = gql` } } ` -const postCategoriesFilterParam = { categories_some: { id_in: ['cat4'] } } +const postCategoriesFilterParam = { categories_some: { id_in: categoryIds } } const postQueryFilteredByCategoryVariables = { filter: postCategoriesFilterParam, } const createPostMutation = gql` - mutation($title: String!, $content: String!) { - CreatePost(title: $title, content: $content) { + mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) { + CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) { id title content @@ -88,6 +72,29 @@ beforeEach(async () => { password: '1234', } await factory.create('User', userParams) + await Promise.all([ + factory.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }), + factory.create('Category', { + id: 'cat4', + name: 'Environment & Nature', + icon: 'tree', + }), + factory.create('Category', { + id: 'cat15', + name: 'Consumption & Sustainability', + icon: 'shopping-cart', + }), + ]) + createPostVariables = { + id: 'p3589', + title: postTitle, + content: postContent, + categoryIds, + } }) afterEach(async () => { @@ -152,8 +159,13 @@ describe('CreatePost', () => { describe('language', () => { it('allows a user to set the language of the post', async () => { const createPostWithLanguageMutation = gql` - mutation($title: String!, $content: String!, $language: String) { - CreatePost(title: $title, content: $content, language: $language) { + mutation($title: String!, $content: String!, $language: String, $categoryIds: [ID]) { + CreatePost( + title: $title + content: $content + language: $language + categoryIds: $categoryIds + ) { language } } @@ -162,6 +174,7 @@ describe('CreatePost', () => { title: postTitle, content: postContent, language: 'en', + categoryIds, } const expected = { CreatePost: { language: 'en' } } await expect( @@ -171,51 +184,29 @@ describe('CreatePost', () => { }) describe('categories', () => { - let postWithCategories - beforeEach(async () => { - await Promise.all([ - factory.create('Category', { - id: 'cat9', - name: 'Democracy & Politics', - icon: 'university', - }), - factory.create('Category', { - id: 'cat4', - name: 'Environment & Nature', - icon: 'tree', - }), - factory.create('Category', { - id: 'cat15', - name: 'Consumption & Sustainability', - icon: 'shopping-cart', - }), - ]) - postWithCategories = await client.request( - createPostWithCategoriesMutation, - createPostWithCategoriesVariables, + it('throws an error if categoryIds is not an array', async () => { + createPostVariables.categoryIds = null + await expect(client.request(createPostMutation, createPostVariables)).rejects.toThrow( + 'You cannot create a post without at least one category', ) }) - it('allows a user to set the categories of the post', async () => { - const expected = [{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }] - const postQueryWithCategoriesVariables = { - id: postWithCategories.CreatePost.id, - } - - await expect( - client.request(postQueryWithCategories, postQueryWithCategoriesVariables), - ).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] }) + it('requires at least one category for successful creation', async () => { + createPostVariables.categoryIds = [] + await expect(client.request(createPostMutation, createPostVariables)).rejects.toThrow( + 'You cannot create a post without at least one category', + ) }) it('allows a user to filter for posts by category', async () => { - await client.request(createPostWithCategoriesMutation, createPostWithoutCategoriesVariables) - const categoryIds = [{ id: 'cat4' }, { id: 'cat15' }, { id: 'cat9' }] + await client.request(createPostMutation, createPostVariables) + const categoryIdsArray = [{ id: 'cat4' }, { id: 'cat15' }, { id: 'cat9' }] const expected = { Post: [ { title: postTitle, - id: postWithCategories.CreatePost.id, - categories: expect.arrayContaining(categoryIds), + id: 'p3589', + categories: expect.arrayContaining(categoryIdsArray), }, ], } @@ -231,6 +222,28 @@ describe('UpdatePost', () => { let updatePostMutation let updatePostVariables beforeEach(async () => { + await Promise.all([ + factory.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }), + factory.create('Category', { + id: 'cat4', + name: 'Environment & Nature', + icon: 'tree', + }), + factory.create('Category', { + id: 'cat15', + name: 'Consumption & Sustainability', + icon: 'shopping-cart', + }), + factory.create('Category', { + id: 'cat27', + name: 'Animal Protection', + icon: 'paw', + }), + ]) const asAuthor = Factory() await asAuthor.create('User', authorParams) await asAuthor.authenticateAs(authorParams) @@ -238,6 +251,7 @@ describe('UpdatePost', () => { id: 'p1', title: oldTitle, content: oldContent, + categoryIds, }) updatePostMutation = gql` mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) { @@ -294,36 +308,10 @@ describe('UpdatePost', () => { }) describe('categories', () => { - let postWithCategories beforeEach(async () => { - await Promise.all([ - factory.create('Category', { - id: 'cat9', - name: 'Democracy & Politics', - icon: 'university', - }), - factory.create('Category', { - id: 'cat4', - name: 'Environment & Nature', - icon: 'tree', - }), - factory.create('Category', { - id: 'cat15', - name: 'Consumption & Sustainability', - icon: 'shopping-cart', - }), - factory.create('Category', { - id: 'cat27', - name: 'Animal Protection', - icon: 'paw', - }), - ]) - postWithCategories = await client.request( - createPostWithCategoriesMutation, - createPostWithCategoriesVariables, - ) + await client.request(createPostMutation, createPostVariables) updatePostVariables = { - id: postWithCategories.CreatePost.id, + id: 'p3589', title: newTitle, content: newContent, categoryIds: ['cat27'], @@ -334,7 +322,7 @@ describe('UpdatePost', () => { await client.request(updatePostMutation, updatePostVariables) const expected = [{ id: 'cat27' }] const postQueryWithCategoriesVariables = { - id: postWithCategories.CreatePost.id, + id: 'p3589', } await expect( client.request(postQueryWithCategories, postQueryWithCategoriesVariables), @@ -365,6 +353,7 @@ describe('DeletePost', () => { await asAuthor.create('Post', { id: 'p1', content: 'To be deleted', + categoryIds, }) }) diff --git a/backend/src/seed/factories/posts.js b/backend/src/seed/factories/posts.js index b8e30ee2e..f2f1432dc 100644 --- a/backend/src/seed/factories/posts.js +++ b/backend/src/seed/factories/posts.js @@ -16,6 +16,7 @@ export default function(params) { image = faker.image.unsplash.imageUrl(), visibility = 'public', deleted = false, + categoryIds, } = params return { @@ -28,6 +29,7 @@ export default function(params) { $image: String $visibility: Visibility $deleted: Boolean + $categoryIds: [ID] ) { CreatePost( id: $id @@ -37,12 +39,13 @@ export default function(params) { image: $image visibility: $visibility deleted: $deleted + categoryIds: $categoryIds ) { title content } } `, - variables: { id, slug, title, content, image, visibility, deleted }, + variables: { id, slug, title, content, image, visibility, deleted, categoryIds }, } } From 555c5254bcb8947c4416d66c69b7da2ee3fe68ea Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Mon, 19 Aug 2019 17:31:55 +0200 Subject: [PATCH 02/19] Add tests, validations for too many categories - Co-authored-by: Joseph Ngugi --- .../validation/validationMiddleware.js | 11 ++- backend/src/schema/resolvers/posts.spec.js | 80 +++++++++++-------- 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index 04f6b92a4..6094911b1 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -40,10 +40,12 @@ const validateUpdateComment = async (resolve, root, args, context, info) => { return resolve(root, args, context, info) } -const validateCreatePost = async (resolve, root, args, context, info) => { +const validatePost = async (resolve, root, args, context, info) => { const { categoryIds } = args - if (!Array.isArray(categoryIds) || !categoryIds.length) { - throw new UserInputError('You cannot create a post without at least one category') + if (!Array.isArray(categoryIds) || !categoryIds.length || categoryIds.length > 3) { + throw new UserInputError( + 'You cannot save a post without at least one category or more than three', + ) } return resolve(root, args, context, info) } @@ -52,6 +54,7 @@ export default { Mutation: { CreateComment: validateCommentCreation, UpdateComment: validateUpdateComment, - CreatePost: validateCreatePost, + CreatePost: validatePost, + UpdatePost: validatePost, }, } diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index ab90af27e..f67b39da4 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -19,6 +19,7 @@ const oldTitle = 'Old title' const oldContent = 'Old content' const newTitle = 'New title' const newContent = 'New content' +const postSaveError = 'You cannot save a post without at least one category or more than three' const categoryIds = ['cat9', 'cat4', 'cat15'] let createPostVariables @@ -88,6 +89,11 @@ beforeEach(async () => { name: 'Consumption & Sustainability', icon: 'shopping-cart', }), + factory.create('Category', { + id: 'cat27', + name: 'Animal Protection', + icon: 'paw', + }), ]) createPostVariables = { id: 'p3589', @@ -187,14 +193,21 @@ describe('CreatePost', () => { it('throws an error if categoryIds is not an array', async () => { createPostVariables.categoryIds = null await expect(client.request(createPostMutation, createPostVariables)).rejects.toThrow( - 'You cannot create a post without at least one category', + postSaveError, ) }) it('requires at least one category for successful creation', async () => { createPostVariables.categoryIds = [] await expect(client.request(createPostMutation, createPostVariables)).rejects.toThrow( - 'You cannot create a post without at least one category', + postSaveError, + ) + }) + + it('allows a maximum of three category for successful update', async () => { + createPostVariables.categoryIds = ['cat9', 'cat27', 'cat15', 'cat4'] + await expect(client.request(createPostMutation, createPostVariables)).rejects.toThrow( + postSaveError, ) }) @@ -219,31 +232,16 @@ describe('CreatePost', () => { }) describe('UpdatePost', () => { - let updatePostMutation let updatePostVariables + const updatePostMutation = gql` + mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) { + UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) { + id + content + } + } + ` beforeEach(async () => { - await Promise.all([ - factory.create('Category', { - id: 'cat9', - name: 'Democracy & Politics', - icon: 'university', - }), - factory.create('Category', { - id: 'cat4', - name: 'Environment & Nature', - icon: 'tree', - }), - factory.create('Category', { - id: 'cat15', - name: 'Consumption & Sustainability', - icon: 'shopping-cart', - }), - factory.create('Category', { - id: 'cat27', - name: 'Animal Protection', - icon: 'paw', - }), - ]) const asAuthor = Factory() await asAuthor.create('User', authorParams) await asAuthor.authenticateAs(authorParams) @@ -253,14 +251,6 @@ describe('UpdatePost', () => { content: oldContent, categoryIds, }) - updatePostMutation = gql` - mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) { - UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) { - id - content - } - } - ` updatePostVariables = { id: 'p1', @@ -301,6 +291,7 @@ describe('UpdatePost', () => { }) it('updates a post', async () => { + updatePostVariables.categoryIds = ['cat9'] const expected = { UpdatePost: { id: 'p1', content: newContent } } await expect(client.request(updatePostMutation, updatePostVariables)).resolves.toEqual( expected, @@ -328,6 +319,27 @@ describe('UpdatePost', () => { client.request(postQueryWithCategories, postQueryWithCategoriesVariables), ).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] }) }) + + it('throws an error if categoryIds is not an array', async () => { + updatePostVariables.categoryIds = null + await expect(client.request(updatePostMutation, updatePostVariables)).rejects.toThrow( + postSaveError, + ) + }) + + it('requires at least one category for successful update', async () => { + updatePostVariables.categoryIds = [] + await expect(client.request(updatePostMutation, updatePostVariables)).rejects.toThrow( + postSaveError, + ) + }) + + it('allows a maximum of three category for a successful update', async () => { + updatePostVariables.categoryIds = ['cat9', 'cat27', 'cat15', 'cat4'] + await expect(client.request(updatePostMutation, updatePostVariables)).rejects.toThrow( + postSaveError, + ) + }) }) }) }) @@ -400,7 +412,7 @@ describe('emotions', () => { postQueryAction, postToEmote, postToEmoteNode - const PostsEmotionsCountQuery = ` + const PostsEmotionsCountQuery = gql` query($id: ID!) { Post(id: $id) { emotionsCount From c614e4de47134cebc3d49b4198565956aa51fcc9 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Tue, 20 Aug 2019 15:53:07 +0200 Subject: [PATCH 03/19] Update UI, component tests - enforce that a post cannot be created or updated with no categories nor more than 3 categories --- .../ContributionForm/ContributionForm.spec.js | 114 ++++++++++++------ .../ContributionForm/ContributionForm.vue | 28 +++-- 2 files changed, 91 insertions(+), 51 deletions(-) diff --git a/webapp/components/ContributionForm/ContributionForm.spec.js b/webapp/components/ContributionForm/ContributionForm.spec.js index 199d6ab18..e1f293c77 100644 --- a/webapp/components/ContributionForm/ContributionForm.spec.js +++ b/webapp/components/ContributionForm/ContributionForm.spec.js @@ -28,6 +28,7 @@ describe('ContributionForm.vue', () => { let cancelBtn let mocks let propsData + let categoryIds const postTitle = 'this is a title for a post' const postTitleTooShort = 'xx' let postTitleTooLong = '' @@ -60,6 +61,7 @@ describe('ContributionForm.vue', () => { content: postContent, contentExcerpt: postContent, language: 'en', + categoryIds, }, }, }), @@ -175,6 +177,23 @@ describe('ContributionForm.vue', () => { wrapper.find('.submit-button-for-test').trigger('click') expect(mocks.$apollo.mutate).not.toHaveBeenCalled() }) + + it('should have at least one category', async () => { + postTitleInput = wrapper.find('.ds-input') + postTitleInput.setValue(postTitle) + await wrapper.vm.updateEditorContent(postContent) + wrapper.find('.submit-button-for-test').trigger('click') + expect(mocks.$apollo.mutate).not.toHaveBeenCalled() + }) + + it('should have not have more than three categories', async () => { + postTitleInput = wrapper.find('.ds-input') + postTitleInput.setValue(postTitle) + await wrapper.vm.updateEditorContent(postContent) + wrapper.vm.form.categoryIds = ['cat4', 'cat9', 'cat15', 'cat27'] + wrapper.find('.submit-button-for-test').trigger('click') + expect(mocks.$apollo.mutate).not.toHaveBeenCalled() + }) }) describe('valid form submission', () => { @@ -186,7 +205,7 @@ describe('ContributionForm.vue', () => { content: postContent, language: 'en', id: null, - categoryIds: null, + categoryIds: ['cat12'], imageUpload: null, image: null, }, @@ -194,11 +213,13 @@ describe('ContributionForm.vue', () => { postTitleInput = wrapper.find('.ds-input') postTitleInput.setValue(postTitle) await wrapper.vm.updateEditorContent(postContent) + categoryIds = ['cat12'] + wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds) }) - it('with title and content', () => { - wrapper.find('.submit-button-for-test').trigger('click') - expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1) + it('creates a post with valid title, content, and at least one category', async () => { + await wrapper.find('.submit-button-for-test').trigger('click') + expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) }) it("sends a fallback language based on a user's locale", () => { @@ -214,14 +235,6 @@ describe('ContributionForm.vue', () => { expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) }) - it('supports adding categories', async () => { - const categoryIds = ['cat12', 'cat15', 'cat37'] - expectedParams.variables.categoryIds = categoryIds - wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds) - await wrapper.find('.submit-button-for-test').trigger('click') - expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) - }) - it('supports adding a teaser image', async () => { expectedParams.variables.imageUpload = imageUpload wrapper.find(TeaserImage).vm.$emit('addTeaserImage', imageUpload) @@ -260,6 +273,8 @@ describe('ContributionForm.vue', () => { postTitleInput = wrapper.find('.ds-input') postTitleInput.setValue(postTitle) await wrapper.vm.updateEditorContent(postContent) + categoryIds = ['cat12'] + wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds) }) it('shows an error toaster when apollo mutation rejects', async () => { @@ -307,35 +322,54 @@ describe('ContributionForm.vue', () => { expect(wrapper.vm.form.content).toEqual(propsData.contribution.content) }) - it('calls the UpdatePost apollo mutation', async () => { - expectedParams = { - mutation: PostMutations().UpdatePost, - variables: { - title: postTitle, - content: postContent, - language: propsData.contribution.language, - id: propsData.contribution.id, - categoryIds: ['cat12'], - image, - imageUpload: null, - }, - } - postTitleInput = wrapper.find('.ds-input') - postTitleInput.setValue(postTitle) - wrapper.vm.updateEditorContent(postContent) - await wrapper.find('.submit-button-for-test').trigger('click') - expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) - }) + describe('valid update', () => { + beforeEach(() => { + mocks.$apollo.mutate = jest.fn().mockResolvedValueOnce({ + data: { + UpdatePost: { + title: postTitle, + slug: 'this-is-a-title-for-a-post', + content: postContent, + contentExcerpt: postContent, + language: 'en', + categoryIds, + }, + }, + }) + wrapper = Wrapper() + expectedParams = { + mutation: PostMutations().UpdatePost, + variables: { + title: postTitle, + content: postContent, + language: propsData.contribution.language, + id: propsData.contribution.id, + categoryIds, + image, + imageUpload: null, + }, + } + }) - it('supports updating categories', async () => { - const categoryIds = ['cat3', 'cat51', 'cat37'] - postTitleInput = wrapper.find('.ds-input') - postTitleInput.setValue(postTitle) - wrapper.vm.updateEditorContent(postContent) - expectedParams.variables.categoryIds = categoryIds - wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds) - await wrapper.find('.submit-button-for-test').trigger('click') - expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) + it('calls the UpdatePost apollo mutation', async () => { + postTitleInput = wrapper.find('.ds-input') + postTitleInput.setValue(postTitle) + wrapper.vm.updateEditorContent(postContent) + wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds) + wrapper.find('.submit-button-for-test').trigger('click') + expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) + }) + + it('supports updating categories', async () => { + const categoryIds = ['cat3', 'cat51', 'cat37'] + postTitleInput = wrapper.find('.ds-input') + postTitleInput.setValue(postTitle) + wrapper.vm.updateEditorContent(postContent) + expectedParams.variables.categoryIds = categoryIds + wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds) + await wrapper.find('.submit-button-for-test').trigger('click') + expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) + }) }) }) }) diff --git a/webapp/components/ContributionForm/ContributionForm.vue b/webapp/components/ContributionForm/ContributionForm.vue index 9d1c92cc9..fcea30c59 100644 --- a/webapp/components/ContributionForm/ContributionForm.vue +++ b/webapp/components/ContributionForm/ContributionForm.vue @@ -57,7 +57,7 @@ type="submit" icon="check" :loading="loading" - :disabled="disabledByContent || errors" + :disabled="failsValidations || errors" primary @click.prevent="submit" > @@ -101,7 +101,7 @@ export default { image: null, language: null, languageOptions: [], - categoryIds: null, + categoryIds: [], }, formSchema: { title: { required: true, min: 3, max: 64 }, @@ -109,12 +109,11 @@ export default { }, id: null, loading: false, - disabledByContent: true, slug: null, users: [], contentMin: 3, contentMax: 2000, - + failsValidations: true, hashtags: [], } }, @@ -129,9 +128,9 @@ export default { this.slug = contribution.slug this.form.title = contribution.title this.form.content = contribution.content - this.manageContent(this.form.content) this.form.image = contribution.image this.form.categoryIds = this.categoryIds(contribution.categories) + this.manageContent(this.form.content) }, }, }, @@ -175,11 +174,11 @@ export default { imageUpload: teaserImage, }, }) - .then(res => { + .then(({ data }) => { this.loading = false this.$toast.success(this.$t('contribution.success')) - this.disabledByContent = true - const result = res.data[this.id ? 'UpdatePost' : 'CreatePost'] + this.failedValidations = true + const result = data[this.id ? 'UpdatePost' : 'CreatePost'] this.$router.push({ name: 'post-id-slug', @@ -189,7 +188,7 @@ export default { .catch(err => { this.$toast.error(err.message) this.loading = false - this.disabledByContent = false + this.failedValidations = false }) }, updateEditorContent(value) { @@ -202,8 +201,7 @@ export default { const str = content.replace(/<\/?[^>]+(>|$)/gm, '') // Set counter length of text this.form.contentLength = str.length - // Enable save button if requirements are met - this.disabledByContent = !(this.contentMin <= str.length && str.length <= this.contentMax) + this.validatePost() }, availableLocales() { orderBy(locales, 'name').map(locale => { @@ -212,6 +210,7 @@ export default { }, updateCategories(ids) { this.form.categoryIds = ids + this.validatePost() }, addTeaserImage(file) { this.form.teaserImage = file @@ -223,6 +222,13 @@ export default { }) return categoryIds }, + validatePost() { + const passesContentValidations = + this.form.contentLength >= this.contentMin && this.form.contentLength <= this.contentMax + const passesCategoryValidations = + this.form.categoryIds.length > 0 && this.form.categoryIds.length <= 3 + this.failsValidations = !(passesContentValidations && passesCategoryValidations) + }, }, apollo: { User: { From 29f39c4f45fe1d556d2735c4bf62e5ad6c5bab7a Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Tue, 20 Aug 2019 15:54:51 +0200 Subject: [PATCH 04/19] Update backend tests - Every test that created or updated a post needed to be updated to add categoryIds --- .../filterBubble/filterBubble.spec.js | 12 +- .../handleContentData.spec.js | 91 +++++++------- .../src/middleware/slugifyMiddleware.spec.js | 52 ++++---- .../middleware/softDeleteMiddleware.spec.js | 19 ++- backend/src/models/Category.js | 21 ++++ backend/src/models/index.js | 1 + backend/src/schema/resolvers/comments.spec.js | 16 ++- .../src/schema/resolvers/moderation.spec.js | 26 ++-- .../schema/resolvers/notifications.spec.js | 71 ++++++----- backend/src/schema/resolvers/reports.spec.js | 10 ++ backend/src/schema/resolvers/shout.spec.js | 112 +++++++++++------- backend/src/schema/resolvers/users.spec.js | 11 +- 12 files changed, 284 insertions(+), 158 deletions(-) create mode 100644 backend/src/models/Category.js diff --git a/backend/src/middleware/filterBubble/filterBubble.spec.js b/backend/src/middleware/filterBubble/filterBubble.spec.js index 62addeece..c71332db6 100644 --- a/backend/src/middleware/filterBubble/filterBubble.spec.js +++ b/backend/src/middleware/filterBubble/filterBubble.spec.js @@ -1,8 +1,10 @@ import { GraphQLClient } from 'graphql-request' import { host, login } from '../../jest/helpers' import Factory from '../../seed/factories' +import { neode } from '../../bootstrap/neo4j' const factory = Factory() +const instance = neode() const currentUserParams = { id: 'u1', @@ -21,6 +23,7 @@ const randomAuthorParams = { name: 'Someone else', password: 'else', } +const categoryIds = ['cat9'] beforeEach(async () => { await Promise.all([ @@ -28,14 +31,19 @@ beforeEach(async () => { factory.create('User', followedAuthorParams), factory.create('User', randomAuthorParams), ]) + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) const [asYourself, asFollowedUser, asSomeoneElse] = await Promise.all([ Factory().authenticateAs(currentUserParams), Factory().authenticateAs(followedAuthorParams), Factory().authenticateAs(randomAuthorParams), ]) await asYourself.follow({ id: 'u2', type: 'User' }) - await asFollowedUser.create('Post', { title: 'This is the post of a followed user' }) - await asSomeoneElse.create('Post', { title: 'This is some random post' }) + await asFollowedUser.create('Post', { title: 'This is the post of a followed user', categoryIds }) + await asSomeoneElse.create('Post', { title: 'This is some random post', categoryIds }) }) afterEach(async () => { diff --git a/backend/src/middleware/handleHtmlContent/handleContentData.spec.js b/backend/src/middleware/handleHtmlContent/handleContentData.spec.js index fb088e428..7f77b4589 100644 --- a/backend/src/middleware/handleHtmlContent/handleContentData.spec.js +++ b/backend/src/middleware/handleHtmlContent/handleContentData.spec.js @@ -4,14 +4,32 @@ import { createTestClient } from 'apollo-server-testing' import { neode, getDriver } from '../../bootstrap/neo4j' import createServer from '../../server' -const factory = Factory() -const driver = getDriver() -const instance = neode() let server let query let mutate let user let authenticatedUser +const factory = Factory() +const driver = getDriver() +const instance = neode() +const categoryIds = ['cat9'] +const createPostMutation = gql` + mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]!) { + CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) { + id + title + content + } + } +` +const updatePostMutation = gql` + mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]!) { + UpdatePost(id: $id, content: $content, title: $title, categoryIds: $categoryIds) { + title + content + } + } +` beforeAll(() => { const createServerResult = createServer({ @@ -37,6 +55,11 @@ beforeEach(async () => { email: 'test@example.org', password: '1234', }) + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) }) afterEach(async () => { @@ -78,19 +101,10 @@ describe('notifications', () => { 'Hey @al-capone how do you do?' const createPostAction = async () => { - const createPostMutation = gql` - mutation($id: ID, $title: String!, $content: String!) { - CreatePost(id: $id, title: $title, content: $content) { - id - title - content - } - } - ` authenticatedUser = await author.toJson() await mutate({ mutation: createPostMutation, - variables: { id: 'p47', title, content }, + variables: { id: 'p47', title, content, categoryIds }, }) authenticatedUser = await user.toJson() } @@ -126,14 +140,6 @@ describe('notifications', () => { @al-capone ` - const updatePostMutation = gql` - mutation($id: ID!, $title: String!, $content: String!) { - UpdatePost(id: $id, content: $content, title: $title) { - title - content - } - } - ` authenticatedUser = await author.toJson() await mutate({ mutation: updatePostMutation, @@ -141,6 +147,7 @@ describe('notifications', () => { id: 'p47', title, content: updatedContent, + categoryIds, }, }) authenticatedUser = await user.toJson() @@ -189,9 +196,9 @@ describe('notifications', () => { }) describe('Hashtags', () => { - const postId = 'p135' - const postTitle = 'Two Hashtags' - const postContent = + const id = 'p135' + const title = 'Two Hashtags' + const content = '

Hey Dude, #Democracy should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' const postWithHastagsQuery = gql` query($id: ID) { @@ -203,17 +210,8 @@ describe('Hashtags', () => { } ` const postWithHastagsVariables = { - id: postId, + id, } - const createPostMutation = gql` - mutation($postId: ID, $postTitle: String!, $postContent: String!) { - CreatePost(id: $postId, title: $postTitle, content: $postContent) { - id - title - content - } - } - ` describe('authenticated', () => { beforeEach(async () => { @@ -225,9 +223,10 @@ describe('Hashtags', () => { await mutate({ mutation: createPostMutation, variables: { - postId, - postTitle, - postContent, + id, + title, + content, + categoryIds, }, }) }) @@ -251,25 +250,17 @@ describe('Hashtags', () => { describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => { // The already existing Hashtag has no class at this point. - const updatedPostContent = + const content = '

Hey Dude, #Elections should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' - const updatePostMutation = gql` - mutation($postId: ID!, $postTitle: String!, $updatedPostContent: String!) { - UpdatePost(id: $postId, title: $postTitle, content: $updatedPostContent) { - id - title - content - } - } - ` it('only one previous Hashtag and the new Hashtag exists', async () => { await mutate({ mutation: updatePostMutation, variables: { - postId, - postTitle, - updatedPostContent, + id, + title, + content, + categoryIds, }, }) diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index 5ee4faa3c..0f42def85 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -1,13 +1,25 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../seed/factories' -import { host, login } from '../jest/helpers' +import { host, login, gql } from '../jest/helpers' import { neode } from '../bootstrap/neo4j' let authenticatedClient let headers const factory = Factory() const instance = neode() - +const categoryIds = ['cat9'] +const createPostMutation = gql` + mutation($title: String!, $content: String!, $categoryIds: [ID]!, $slug: String) { + CreatePost(title: $title, content: $content, categoryIds: $categoryIds, slug: $slug) { + slug + } + } +` +let createPostVariables = { + title: 'I am a brand new post', + content: 'Some content', + categoryIds, +} beforeEach(async () => { const adminParams = { role: 'admin', email: 'admin@example.org', password: '1234' } await factory.create('User', adminParams) @@ -15,6 +27,11 @@ beforeEach(async () => { email: 'someone@example.org', password: '1234', }) + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) // we need to be an admin, otherwise we're not authorized to create a user headers = await login(adminParams) authenticatedClient = new GraphQLClient(host, { headers }) @@ -27,12 +44,7 @@ afterEach(async () => { describe('slugify', () => { describe('CreatePost', () => { it('generates a slug based on title', async () => { - const response = await authenticatedClient.request(`mutation { - CreatePost( - title: "I am a brand new post", - content: "Some content" - ) { slug } - }`) + const response = await authenticatedClient.request(createPostMutation, createPostVariables) expect(response).toEqual({ CreatePost: { slug: 'i-am-a-brand-new-post' }, }) @@ -47,16 +59,14 @@ describe('slugify', () => { await asSomeoneElse.create('Post', { title: 'Pre-existing post', slug: 'pre-existing-post', + content: 'as Someone else content', + categoryIds, }) }) it('chooses another slug', async () => { - const response = await authenticatedClient.request(`mutation { - CreatePost( - title: "Pre-existing post", - content: "Some content" - ) { slug } - }`) + createPostVariables = { title: 'Pre-existing post', content: 'Some content', categoryIds } + const response = await authenticatedClient.request(createPostMutation, createPostVariables) expect(response).toEqual({ CreatePost: { slug: 'pre-existing-post-1' }, }) @@ -64,14 +74,14 @@ describe('slugify', () => { describe('but if the client specifies a slug', () => { it('rejects CreatePost', async () => { + createPostVariables = { + title: 'Pre-existing post', + content: 'Some content', + slug: 'pre-existing-post', + categoryIds, + } await expect( - authenticatedClient.request(`mutation { - CreatePost( - title: "Pre-existing post", - content: "Some content", - slug: "pre-existing-post" - ) { slug } - }`), + authenticatedClient.request(createPostMutation, createPostVariables), ).rejects.toThrow('already exists') }) }) diff --git a/backend/src/middleware/softDeleteMiddleware.spec.js b/backend/src/middleware/softDeleteMiddleware.spec.js index 388f44a3c..03c97c020 100644 --- a/backend/src/middleware/softDeleteMiddleware.spec.js +++ b/backend/src/middleware/softDeleteMiddleware.spec.js @@ -1,11 +1,15 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../seed/factories' import { host, login } from '../jest/helpers' +import { neode } from '../bootstrap/neo4j' const factory = Factory() +const instance = neode() + let client let query let action +const categoryIds = ['cat9'] beforeAll(async () => { // For performance reasons we do this only once @@ -26,13 +30,23 @@ beforeAll(async () => { email: 'troll@example.org', password: '1234', }), + instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }), ]) await factory.authenticateAs({ email: 'user@example.org', password: '1234' }) await Promise.all([ factory.follow({ id: 'u2', type: 'User' }), - factory.create('Post', { id: 'p1', title: 'Deleted post', deleted: true }), - factory.create('Post', { id: 'p3', title: 'Publicly visible post', deleted: false }), + factory.create('Post', { id: 'p1', title: 'Deleted post', deleted: true, categoryIds }), + factory.create('Post', { + id: 'p3', + title: 'Publicly visible post', + deleted: false, + categoryIds, + }), ]) await Promise.all([ @@ -53,6 +67,7 @@ beforeAll(async () => { content: 'This is an offensive post content', image: '/some/offensive/image.jpg', deleted: false, + categoryIds, }) await asTroll.create('Comment', { id: 'c1', postId: 'p3', content: 'Disabled comment' }) await Promise.all([asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' })]) diff --git a/backend/src/models/Category.js b/backend/src/models/Category.js new file mode 100644 index 000000000..d8f1bff6f --- /dev/null +++ b/backend/src/models/Category.js @@ -0,0 +1,21 @@ +import uuid from 'uuid/v4' + +module.exports = { + id: { type: 'string', primary: true, default: uuid }, + name: { type: 'string', required: true, default: false }, + slug: { type: 'string' }, + icon: { type: 'string', required: true, default: false }, + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + updatedAt: { + type: 'string', + isoDate: true, + required: true, + default: () => new Date().toISOString(), + }, + post: { + type: 'relationship', + relationship: 'CATEGORIZED', + target: 'Post', + direction: 'in', + }, +} diff --git a/backend/src/models/index.js b/backend/src/models/index.js index 6f6b300f8..295082de4 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -8,4 +8,5 @@ export default { SocialMedia: require('./SocialMedia.js'), Post: require('./Post.js'), Notification: require('./Notification.js'), + Category: require('./Category.js'), } diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js index e8280bd4b..0b6d5f727 100644 --- a/backend/src/schema/resolvers/comments.spec.js +++ b/backend/src/schema/resolvers/comments.spec.js @@ -1,18 +1,21 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' import { host, login, gql } from '../../jest/helpers' +import { neode } from '../../bootstrap/neo4j' -const factory = Factory() let client let createCommentVariables let createCommentVariablesSansPostId let createCommentVariablesWithNonExistentPost let userParams let headers +const factory = Factory() +const instance = neode() +const categoryIds = ['cat9'] const createPostMutation = gql` - mutation($id: ID!, $title: String!, $content: String!) { - CreatePost(id: $id, title: $title, content: $content) { + mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]!) { + CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) { id } } @@ -29,6 +32,7 @@ const createPostVariables = { id: 'p1', title: 'post to comment on', content: 'please comment on me', + categoryIds, } beforeEach(async () => { @@ -38,6 +42,11 @@ beforeEach(async () => { password: '1234', } await factory.create('User', userParams) + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) }) afterEach(async () => { @@ -199,6 +208,7 @@ describe('ManageComments', () => { await asAuthor.create('Post', { id: 'p1', content: 'Post to be commented', + categoryIds, }) await asAuthor.create('Comment', { id: 'c456', diff --git a/backend/src/schema/resolvers/moderation.spec.js b/backend/src/schema/resolvers/moderation.spec.js index db679f522..3107a5799 100644 --- a/backend/src/schema/resolvers/moderation.spec.js +++ b/backend/src/schema/resolvers/moderation.spec.js @@ -1,9 +1,12 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' +import { host, login, gql } from '../../jest/helpers' +import { neode } from '../../bootstrap/neo4j' -const factory = Factory() let client +const factory = Factory() +const instance = neode() +const categoryIds = ['cat9'] const setupAuthenticateClient = params => { const authenticateClient = async () => { @@ -19,11 +22,16 @@ let authenticateClient let createPostVariables let createCommentVariables -beforeEach(() => { +beforeEach(async () => { createResource = () => {} authenticateClient = () => { client = new GraphQLClient(host) } + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) }) const setup = async () => { @@ -36,7 +44,7 @@ afterEach(async () => { }) describe('disable', () => { - const mutation = ` + const mutation = gql` mutation($id: ID!) { disable(id: $id) } @@ -108,6 +116,7 @@ describe('disable', () => { id: 'p3', title: 'post to comment on', content: 'please comment on me', + categoryIds, } createCommentVariables = { id: 'c47', @@ -173,6 +182,7 @@ describe('disable', () => { await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) await factory.create('Post', { id: 'p9', // that's the ID we will look for + categoryIds, }) } }) @@ -214,7 +224,7 @@ describe('disable', () => { }) describe('enable', () => { - const mutation = ` + const mutation = gql` mutation($id: ID!) { enable(id: $id) } @@ -286,6 +296,7 @@ describe('enable', () => { id: 'p9', title: 'post to comment on', content: 'please comment on me', + categoryIds, } createCommentVariables = { id: 'c456', @@ -305,7 +316,7 @@ describe('enable', () => { await asAuthenticatedUser.create('Post', createPostVariables) await asAuthenticatedUser.create('Comment', createCommentVariables) - const disableMutation = ` + const disableMutation = gql` mutation { disable(id: "c456") } @@ -362,9 +373,10 @@ describe('enable', () => { await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) await factory.create('Post', { id: 'p9', // that's the ID we will look for + categoryIds, }) - const disableMutation = ` + const disableMutation = gql` mutation { disable(id: "p9") } diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index 8d51ebfbb..47bb0d515 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -1,17 +1,25 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' +import { host, login, gql } from '../../jest/helpers' +import { neode } from '../../bootstrap/neo4j' -const factory = Factory() let client +const factory = Factory() +const instance = neode() const userParams = { id: 'you', email: 'test@example.org', password: '1234', } +const categoryIds = ['cat9'] beforeEach(async () => { await factory.create('User', userParams) + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) }) afterEach(async () => { @@ -19,11 +27,13 @@ afterEach(async () => { }) describe('Notification', () => { - const query = `{ - Notification { - id + const query = gql` + query { + Notification { + id + } } - }` + ` describe('unauthenticated', () => { it('throws authorization error', async () => { @@ -57,28 +67,30 @@ describe('currentUser { notifications }', () => { ]) await factory.create('Notification', { id: 'unseen' }) await factory.authenticateAs(neighborParams) - await factory.create('Post', { id: 'p1' }) + await factory.create('Post', { id: 'p1', categoryIds }) await Promise.all([ factory.relate('Notification', 'User', { from: 'not-for-you', to: 'neighbor' }), - factory.relate('Notification', 'Post', { from: 'p1', to: 'not-for-you' }), + factory.relate('Notification', 'Post', { from: 'p1', to: 'not-for-you', categoryIds }), factory.relate('Notification', 'User', { from: 'unseen', to: 'you' }), - factory.relate('Notification', 'Post', { from: 'p1', to: 'unseen' }), + factory.relate('Notification', 'Post', { from: 'p1', to: 'unseen', categoryIds }), factory.relate('Notification', 'User', { from: 'already-seen', to: 'you' }), - factory.relate('Notification', 'Post', { from: 'p1', to: 'already-seen' }), + factory.relate('Notification', 'Post', { from: 'p1', to: 'already-seen', categoryIds }), ]) }) describe('filter for read: false', () => { - const query = `query($read: Boolean) { - currentUser { - notifications(read: $read, orderBy: createdAt_desc) { - id - post { + const query = gql` + query($read: Boolean) { + currentUser { + notifications(read: $read, orderBy: createdAt_desc) { id + post { + id + } } } } - }` + ` const variables = { read: false } it('returns only unread notifications of current user', async () => { const expected = { @@ -91,16 +103,18 @@ describe('currentUser { notifications }', () => { }) describe('no filters', () => { - const query = `{ - currentUser { - notifications(orderBy: createdAt_desc) { - id - post { + const query = gql` + query { + currentUser { + notifications(orderBy: createdAt_desc) { id + post { + id + } } } } - }` + ` it('returns all notifications of current user', async () => { const expected = { currentUser: { @@ -118,11 +132,14 @@ describe('currentUser { notifications }', () => { }) describe('UpdateNotification', () => { - const mutation = `mutation($id: ID!, $read: Boolean){ - UpdateNotification(id: $id, read: $read) { - id read + const mutation = gql` + mutation($id: ID!, $read: Boolean) { + UpdateNotification(id: $id, read: $read) { + id + read + } } - }` + ` const variables = { id: 'to-be-updated', read: true } describe('given a notifications', () => { @@ -138,7 +155,7 @@ describe('UpdateNotification', () => { await factory.create('User', mentionedParams) await factory.create('Notification', { id: 'to-be-updated' }) await factory.authenticateAs(userParams) - await factory.create('Post', { id: 'p1' }) + await factory.create('Post', { id: 'p1', categoryIds }) await Promise.all([ factory.relate('Notification', 'User', { from: 'to-be-updated', to: 'mentioned-1' }), factory.relate('Notification', 'Post', { from: 'p1', to: 'to-be-updated' }), diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js index 2a798f5ee..7287a79f4 100644 --- a/backend/src/schema/resolvers/reports.spec.js +++ b/backend/src/schema/resolvers/reports.spec.js @@ -1,8 +1,10 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' import { host, login } from '../../jest/helpers' +import { neode } from '../../bootstrap/neo4j' const factory = Factory() +const instance = neode() describe('report', () => { let mutation @@ -10,6 +12,7 @@ describe('report', () => { let returnedObject let variables let createPostVariables + const categoryIds = ['cat9'] beforeEach(async () => { returnedObject = '{ description }' @@ -28,6 +31,11 @@ describe('report', () => { role: 'user', email: 'abusive-user@example.org', }) + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) }) afterEach(async () => { @@ -126,6 +134,7 @@ describe('report', () => { await factory.create('Post', { id: 'p23', title: 'Matt and Robert having a pair-programming', + categoryIds, }) variables = { id: 'p23', @@ -171,6 +180,7 @@ describe('report', () => { id: 'p1', title: 'post to comment on', content: 'please comment on me', + categoryIds, } const asAuthenticatedUser = await factory.authenticateAs({ email: 'test@example.org', diff --git a/backend/src/schema/resolvers/shout.spec.js b/backend/src/schema/resolvers/shout.spec.js index 029e6998c..718a1e169 100644 --- a/backend/src/schema/resolvers/shout.spec.js +++ b/backend/src/schema/resolvers/shout.spec.js @@ -1,22 +1,39 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' +import { host, login, gql } from '../../jest/helpers' +import { neode } from '../../bootstrap/neo4j' -const factory = Factory() let clientUser1, clientUser2 let headersUser1, headersUser2 +const factory = Factory() +const instance = neode() +const categoryIds = ['cat9'] -const mutationShoutPost = id => ` - mutation { - shout(id: "${id}", type: Post) +const mutationShoutPost = gql` + mutation($id: ID!) { + shout(id: $id, type: Post) } ` -const mutationUnshoutPost = id => ` - mutation { - unshout(id: "${id}", type: Post) +const mutationUnshoutPost = gql` + mutation($id: ID!) { + unshout(id: $id, type: Post) } ` - +const createPostMutation = gql` + mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]!) { + CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) { + id + title + content + } + } +` +const createPostVariables = { + id: 'p1234', + title: 'Post Title 1234', + content: 'Some Post Content 1234', + categoryIds, +} beforeEach(async () => { await factory.create('User', { id: 'u1', @@ -28,28 +45,23 @@ beforeEach(async () => { email: 'test2@example.org', password: '1234', }) - + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) headersUser1 = await login({ email: 'test@example.org', password: '1234' }) headersUser2 = await login({ email: 'test2@example.org', password: '1234' }) clientUser1 = new GraphQLClient(host, { headers: headersUser1 }) clientUser2 = new GraphQLClient(host, { headers: headersUser2 }) - await clientUser1.request(` - mutation { - CreatePost(id: "p1", title: "Post Title 1", content: "Some Post Content 1") { - id - title - } - } - `) - await clientUser2.request(` - mutation { - CreatePost(id: "p2", title: "Post Title 2", content: "Some Post Content 2") { - id - title - } - } - `) + await clientUser1.request(createPostMutation, createPostVariables) + await clientUser2.request(createPostMutation, { + id: 'p12345', + title: 'Post Title 12345', + content: 'Some Post Content 12345', + categoryIds, + }) }) afterEach(async () => { @@ -61,22 +73,26 @@ describe('shout', () => { describe('unauthenticated shout', () => { it('throws authorization error', async () => { const client = new GraphQLClient(host) - await expect(client.request(mutationShoutPost('p1'))).rejects.toThrow('Not Authorised') + await expect(client.request(mutationShoutPost, { id: 'p1234' })).rejects.toThrow( + 'Not Authorised', + ) }) }) it('I shout a post of another user', async () => { - const res = await clientUser1.request(mutationShoutPost('p2')) + const res = await clientUser1.request(mutationShoutPost, { id: 'p12345' }) const expected = { shout: true, } expect(res).toMatchObject(expected) - const { Post } = await clientUser1.request(`{ - Post(id: "p2") { - shoutedByCurrentUser + const { Post } = await clientUser1.request(gql` + query { + Post(id: "p12345") { + shoutedByCurrentUser + } } - }`) + `) const expected2 = { shoutedByCurrentUser: true, } @@ -84,17 +100,19 @@ describe('shout', () => { }) it('I can`t shout my own post', async () => { - const res = await clientUser1.request(mutationShoutPost('p1')) + const res = await clientUser1.request(mutationShoutPost, { id: 'p1234' }) const expected = { shout: false, } expect(res).toMatchObject(expected) - const { Post } = await clientUser1.request(`{ - Post(id: "p1") { - shoutedByCurrentUser + const { Post } = await clientUser1.request(gql` + query { + Post(id: "p1234") { + shoutedByCurrentUser + } } - }`) + `) const expected2 = { shoutedByCurrentUser: false, } @@ -106,28 +124,32 @@ describe('shout', () => { describe('unauthenticated shout', () => { it('throws authorization error', async () => { // shout - await clientUser1.request(mutationShoutPost('p2')) + await clientUser1.request(mutationShoutPost, { id: 'p12345' }) // unshout const client = new GraphQLClient(host) - await expect(client.request(mutationUnshoutPost('p2'))).rejects.toThrow('Not Authorised') + await expect(client.request(mutationUnshoutPost, { id: 'p12345' })).rejects.toThrow( + 'Not Authorised', + ) }) }) it('I unshout a post of another user', async () => { // shout - await clientUser1.request(mutationShoutPost('p2')) + await clientUser1.request(mutationShoutPost, { id: 'p12345' }) const expected = { unshout: true, } // unshout - const res = await clientUser1.request(mutationUnshoutPost('p2')) + const res = await clientUser1.request(mutationUnshoutPost, { id: 'p12345' }) expect(res).toMatchObject(expected) - const { Post } = await clientUser1.request(`{ - Post(id: "p2") { - shoutedByCurrentUser + const { Post } = await clientUser1.request(gql` + query { + Post(id: "p12345") { + shoutedByCurrentUser + } } - }`) + `) const expected2 = { shoutedByCurrentUser: false, } diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index fff6acadb..454b457e6 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -1,9 +1,12 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' import { host, login, gql } from '../../jest/helpers' +import { neode } from '../../bootstrap/neo4j' -const factory = Factory() let client +const factory = Factory() +const instance = neode() +const categoryIds = ['cat9'] afterEach(async () => { await factory.cleanDatabase() @@ -195,9 +198,15 @@ describe('users', () => { email: 'test@example.org', password: '1234', }) + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) await asAuthor.create('Post', { id: 'p139', content: 'Post by user u343', + categoryIds, }) await asAuthor.create('Comment', { id: 'c155', From 7bbbf40bcc1d3fa211ed162f23fe7a09e001db62 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Tue, 20 Aug 2019 16:39:26 +0200 Subject: [PATCH 05/19] Update seeds to create post with a category --- backend/src/seed/seed-db.js | 81 ++++++++----------------------------- 1 file changed, 16 insertions(+), 65 deletions(-) diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index 86f755a24..bbacd2149 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -276,131 +276,82 @@ import Factory from './factories' asAdmin.create('Post', { id: 'p0', image: faker.image.unsplash.food(), + categoryIds: ['cat16'], }), asModerator.create('Post', { id: 'p1', image: faker.image.unsplash.technology(), + categoryIds: ['cat1'], }), asUser.create('Post', { id: 'p2', title: `Nature Philosophy Yoga`, content: `${hashtag1}`, + categoryIds: ['cat2'], }), asTick.create('Post', { id: 'p3', + categoryIds: ['cat3'], }), asTrick.create('Post', { id: 'p4', + categoryIds: ['cat4'], }), asTrack.create('Post', { id: 'p5', + categoryIds: ['cat5'], }), asAdmin.create('Post', { id: 'p6', image: faker.image.unsplash.buildings(), + categoryIds: ['cat6'], }), asModerator.create('Post', { id: 'p7', content: `${mention1} ${faker.lorem.paragraph()}`, + categoryIds: ['cat7'], }), asUser.create('Post', { id: 'p8', image: faker.image.unsplash.nature(), title: `Quantum Flow Theory explains Quantum Gravity`, content: `${hashtagAndMention1}`, + categoryIds: ['cat8'], }), asTick.create('Post', { id: 'p9', + categoryIds: ['cat9'], }), asTrick.create('Post', { id: 'p10', + categoryIds: ['cat10'], }), asTrack.create('Post', { id: 'p11', image: faker.image.unsplash.people(), + categoryIds: ['cat11'], }), asAdmin.create('Post', { id: 'p12', content: `${mention2} ${faker.lorem.paragraph()}`, + categoryIds: ['cat12'], }), asModerator.create('Post', { id: 'p13', + categoryIds: ['cat13'], }), asUser.create('Post', { id: 'p14', image: faker.image.unsplash.objects(), + categoryIds: ['cat14'], }), asTick.create('Post', { id: 'p15', + categoryIds: ['cat15'], }), ]) await Promise.all([ - f.relate('Post', 'Categories', { - from: 'p0', - to: 'cat16', - }), - f.relate('Post', 'Categories', { - from: 'p1', - to: 'cat1', - }), - f.relate('Post', 'Categories', { - from: 'p2', - to: 'cat2', - }), - f.relate('Post', 'Categories', { - from: 'p3', - to: 'cat3', - }), - f.relate('Post', 'Categories', { - from: 'p4', - to: 'cat4', - }), - f.relate('Post', 'Categories', { - from: 'p5', - to: 'cat5', - }), - f.relate('Post', 'Categories', { - from: 'p6', - to: 'cat6', - }), - f.relate('Post', 'Categories', { - from: 'p7', - to: 'cat7', - }), - f.relate('Post', 'Categories', { - from: 'p8', - to: 'cat8', - }), - f.relate('Post', 'Categories', { - from: 'p9', - to: 'cat9', - }), - f.relate('Post', 'Categories', { - from: 'p10', - to: 'cat10', - }), - f.relate('Post', 'Categories', { - from: 'p11', - to: 'cat11', - }), - f.relate('Post', 'Categories', { - from: 'p12', - to: 'cat12', - }), - f.relate('Post', 'Categories', { - from: 'p13', - to: 'cat13', - }), - f.relate('Post', 'Categories', { - from: 'p14', - to: 'cat14', - }), - f.relate('Post', 'Categories', { - from: 'p15', - to: 'cat15', - }), - f.relate('Post', 'Tags', { from: 'p0', to: 'Freiheit', From 8735045d1127e04804ac9617133d1432217394d9 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Tue, 20 Aug 2019 20:13:29 +0200 Subject: [PATCH 06/19] Update cypress tests, post query --- backend/src/schema/resolvers/posts.js | 35 +++----- backend/src/schema/resolvers/posts.spec.js | 8 +- .../administration/TagsAndCategories.feature | 8 +- cypress/integration/common/steps.js | 63 +++++++++---- .../integration/moderation/HidePosts.feature | 16 ++-- cypress/integration/post/WritePost.feature | 2 + .../blocked-users/Blocking.feature | 1 + .../CategoriesSelect/CategoriesSelect.vue | 6 +- .../ContributionForm/ContributionForm.vue | 18 ++-- webapp/graphql/CategoryQuery.js | 14 +-- webapp/graphql/PostQuery.js | 90 +++++++++---------- webapp/pages/post/_id/_slug/index.vue | 2 +- webapp/pages/post/edit/_id.vue | 33 +------ 13 files changed, 150 insertions(+), 146 deletions(-) diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 37710dc38..b5ffc7755 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -83,17 +83,14 @@ export default { await session.run(cypherDeletePreviousRelations, { params }) - let updatePostCypher = `MATCH (post:Post {id: $params.id}) + const updatePostCypher = `MATCH (post:Post {id: $params.id}) SET post = $params - ` - if (categoryIds && categoryIds.length) { - updatePostCypher += `WITH post - UNWIND $categoryIds AS categoryId - MATCH (category:Category {id: categoryId}) - MERGE (post)-[:CATEGORIZED]->(category) - ` - } - updatePostCypher += `RETURN post` + WITH post + UNWIND $categoryIds AS categoryId + MATCH (category:Category {id: categoryId}) + MERGE (post)-[:CATEGORIZED]->(category) + RETURN post` + const updatePostVariables = { categoryIds, params } const transactionRes = await session.run(updatePostCypher, updatePostVariables) @@ -110,22 +107,18 @@ export default { const { categoryIds } = params delete params.categoryIds params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) - params.id = params.id || uuid() - let createPostCypher = `CREATE (post:Post {params}) - WITH post - MATCH (author:User {id: $userId}) - MERGE (post)<-[:WROTE]-(author) - ` - if (categoryIds) { - createPostCypher += `WITH post + const createPostCypher = `CREATE (post:Post {params}) + WITH post + MATCH (author:User {id: $userId}) + MERGE (post)<-[:WROTE]-(author) + WITH post UNWIND $categoryIds AS categoryId MATCH (category:Category {id: categoryId}) MERGE (post)-[:CATEGORIZED]->(category) - ` - } - createPostCypher += `RETURN post` + RETURN post` + const createPostVariables = { userId: context.user.id, categoryIds, params } const session = context.driver.session() diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index f67b39da4..44618ecdc 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -74,22 +74,22 @@ beforeEach(async () => { } await factory.create('User', userParams) await Promise.all([ - factory.create('Category', { + instance.create('Category', { id: 'cat9', name: 'Democracy & Politics', icon: 'university', }), - factory.create('Category', { + instance.create('Category', { id: 'cat4', name: 'Environment & Nature', icon: 'tree', }), - factory.create('Category', { + instance.create('Category', { id: 'cat15', name: 'Consumption & Sustainability', icon: 'shopping-cart', }), - factory.create('Category', { + instance.create('Category', { id: 'cat27', name: 'Animal Protection', icon: 'paw', diff --git a/cypress/integration/administration/TagsAndCategories.feature b/cypress/integration/administration/TagsAndCategories.feature index e543b143b..7d0adb6a9 100644 --- a/cypress/integration/administration/TagsAndCategories.feature +++ b/cypress/integration/administration/TagsAndCategories.feature @@ -15,7 +15,9 @@ Feature: Tags and Categories Background: Given my user account has the role "admin" - And we have a selection of tags and categories as well as posts + And we have a selection of categories + And we have a selection of tags + And we have a selection of posts And I am logged in Scenario: See an overview of categories @@ -24,8 +26,8 @@ Feature: Tags and Categories Then I can see the following table: | | Name | Posts | | | Just For Fun | 2 | - | | Happyness & Values | 1 | - | | Health & Wellbeing | 0 | + | | Happiness & Values | 1 | + | | Health & Wellbeing | 1 | Scenario: See an overview of tags When I navigate to the administration dashboard diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 7192b097b..61ae5d589 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -20,20 +20,19 @@ const narratorParams = { Given("I am logged in", () => { cy.login(loginCredentials); }); - -Given("we have a selection of tags and categories as well as posts", () => { +Given("we have a selection of categories", () => { cy.factory() .authenticateAs(loginCredentials) .create("Category", { id: "cat1", name: "Just For Fun", - slug: "justforfun", + slug: "just-for-fun", icon: "smile" }) .create("Category", { id: "cat2", - name: "Happyness & Values", - slug: "happyness-values", + name: "Happiness & Values", + slug: "happiness-values", icon: "heart-o" }) .create("Category", { @@ -41,11 +40,18 @@ Given("we have a selection of tags and categories as well as posts", () => { name: "Health & Wellbeing", slug: "health-wellbeing", icon: "medkit" - }) + }); +}); + +Given("we have a selection of tags", () => { + cy.factory() + .authenticateAs(loginCredentials) .create("Tag", { id: "Ecology" }) .create("Tag", { id: "Nature" }) .create("Tag", { id: "Democracy" }); +}); +Given("we have a selection of posts", () => { const someAuthor = { id: "authorId", email: "author@example.org", @@ -59,18 +65,15 @@ Given("we have a selection of tags and categories as well as posts", () => { cy.factory() .create("User", someAuthor) .authenticateAs(someAuthor) - .create("Post", { id: "p0" }) - .create("Post", { id: "p1" }); + .create("Post", { id: "p0", categoryIds: ["cat1"] }) + .create("Post", { id: "p1", categoryIds: ["cat2"] }); cy.factory() .create("User", yetAnotherAuthor) .authenticateAs(yetAnotherAuthor) - .create("Post", { id: "p2" }); + .create("Post", { id: "p2", categoryIds: ["cat1"] }); cy.factory() .authenticateAs(loginCredentials) - .create("Post", { id: "p3" }) - .relate("Post", "Categories", { from: "p0", to: "cat1" }) - .relate("Post", "Categories", { from: "p1", to: "cat2" }) - .relate("Post", "Categories", { from: "p2", to: "cat1" }) + .create("Post", { id: "p3", categoryIds: ["cat3"] }) .relate("Post", "Tags", { from: "p0", to: "Ecology" }) .relate("Post", "Tags", { from: "p0", to: "Nature" }) .relate("Post", "Tags", { from: "p0", to: "Democracy" }) @@ -182,9 +185,17 @@ Given("we have the following posts in our database:", table => { }; postAttributes.deleted = Boolean(postAttributes.deleted); const disabled = Boolean(postAttributes.disabled); + postAttributes.categoryIds = [`cat${i}`]; + postAttributes; cy.factory() .create("User", userAttributes) .authenticateAs(userAttributes) + .create("Category", { + id: `cat${i}`, + name: "Just For Fun", + slug: `just-for-fun-${i}`, + icon: "smile" + }) .create("Post", postAttributes); if (disabled) { const moderatorParams = { @@ -218,6 +229,7 @@ When( Given("I previously created a post", () => { lastPost.title = "previously created post"; lastPost.content = "with some content"; + lastPost.categoryIds = "cat1"; cy.factory() .authenticateAs(loginCredentials) .create("Post", lastPost); @@ -233,6 +245,12 @@ When("I type in the following text:", text => { cy.get(".editor .ProseMirror").type(lastPost.content); }); +Then("I select a category", () => { + cy.get("span") + .contains("Just for Fun") + .click(); +}); + Then("the post shows up on the landing page at position {int}", index => { cy.openPage("landing"); const selector = `.post-card:nth-child(${index}) > .ds-card-content`; @@ -260,7 +278,9 @@ Then("the first post on the landing page has the title:", title => { Then( "the page {string} returns a 404 error with a message:", (route, message) => { - cy.request({ url: route, failOnStatusCode: false }).its('status').should('eq', 404) + cy.request({ url: route, failOnStatusCode: false }) + .its("status") + .should("eq", 404); cy.visit(route, { failOnStatusCode: false }); cy.get(".error").should("contain", message); } @@ -422,7 +442,7 @@ Given('"Spammy Spammer" wrote a post {string}', title => { email: "spammy-spammer@example.org", password: "1234" }) - .create("Post", { title }); + .create("Post", { title, categoryIds: ["cat2"] }); }); Then("the list of posts of this user is empty", () => { @@ -441,7 +461,7 @@ Then("nobody is following the user profile anymore", () => { Given("I wrote a post {string}", title => { cy.factory() .authenticateAs(loginCredentials) - .create("Post", { title }); + .create("Post", { title, categoryIds: ["cat2"] }); }); When("I block the user {string}", name => { @@ -466,3 +486,14 @@ Then("I see only one post with the title {string}", title => { .should("have.length", 1); cy.get(".main-container").contains(".post-link", title); }); + +And("some categories exist", () => { + cy.factory() + .authenticateAs(loginCredentials) + .create("Category", { + id: "cat1", + name: "Just For Fun", + slug: `just-for-fun`, + icon: "smile" + }); +}); diff --git a/cypress/integration/moderation/HidePosts.feature b/cypress/integration/moderation/HidePosts.feature index e886e5f95..bb82c7188 100644 --- a/cypress/integration/moderation/HidePosts.feature +++ b/cypress/integration/moderation/HidePosts.feature @@ -7,20 +7,20 @@ Feature: Hide Posts Given we have the following posts in our database: | id | title | deleted | disabled | | p1 | This post should be visible | | | - | p2 | This post is disabled | | x | - | p3 | This post is deleted | x | | + | p2 | This post is disabled | | x | + | p3 | This post is deleted | x | | Scenario: Disabled posts don't show up on the landing page Given I am logged in with a "user" role Then I should see only 1 post on the landing page And the first post on the landing page has the title: - """ - This post should be visible - """ + """ + This post should be visible + """ Scenario: Visiting a disabled post's page should return 404 Given I am logged in with a "user" role Then the page "/post/this-post-is-disabled" returns a 404 error with a message: - """ - This post could not be found - """ + """ + This post could not be found + """ diff --git a/cypress/integration/post/WritePost.feature b/cypress/integration/post/WritePost.feature index 06ac4a175..461766532 100644 --- a/cypress/integration/post/WritePost.feature +++ b/cypress/integration/post/WritePost.feature @@ -6,6 +6,7 @@ Feature: Create a post Background: Given I have a user account And I am logged in + And we have a selection of categories And I am on the "landing" page Scenario: Create a post @@ -16,6 +17,7 @@ Feature: Create a post Human Connection is a free and open-source social network for active citizenship. """ + Then I select a category And I click on "Save" Then I get redirected to ".../my-first-post" And the post was saved successfully diff --git a/cypress/integration/user_profile/blocked-users/Blocking.feature b/cypress/integration/user_profile/blocked-users/Blocking.feature index 9ab4fde6e..3ce4fd6c4 100644 --- a/cypress/integration/user_profile/blocked-users/Blocking.feature +++ b/cypress/integration/user_profile/blocked-users/Blocking.feature @@ -7,6 +7,7 @@ Feature: Block a User Given I have a user account And there is an annoying user called "Spammy Spammer" And I am logged in + And we have a selection of categories Scenario: Block a user Given I am on the profile page of the annoying user diff --git a/webapp/components/CategoriesSelect/CategoriesSelect.vue b/webapp/components/CategoriesSelect/CategoriesSelect.vue index e1928b240..cbe46b890 100644 --- a/webapp/components/CategoriesSelect/CategoriesSelect.vue +++ b/webapp/components/CategoriesSelect/CategoriesSelect.vue @@ -27,7 +27,7 @@