diff --git a/backend/package.json b/backend/package.json index b1175e893..ed4e8e2a7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -56,7 +56,7 @@ "cheerio": "~1.0.0-rc.3", "cors": "~2.8.5", "cross-env": "~5.2.0", - "date-fns": "2.0.0-beta.5", + "date-fns": "2.0.0", "debug": "~4.1.1", "dotenv": "~8.1.0", "express": "^4.17.1", @@ -110,7 +110,7 @@ "@babel/plugin-proposal-throw-expressions": "^7.2.0", "@babel/preset-env": "~7.5.5", "@babel/register": "~7.5.5", - "apollo-server-testing": "~2.8.1", + "apollo-server-testing": "~2.8.2", "babel-core": "~7.0.0-0", "babel-eslint": "~10.0.2", "babel-jest": "~24.9.0", 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/handleNotifications/handleNotificationsMiddleware.spec.js b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js index f5d2c61c8..d982f318d 100644 --- a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js +++ b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js @@ -1,17 +1,50 @@ -import { gql } from '../../jest/helpers' +import { + gql +} from '../../jest/helpers' import Factory from '../../seed/factories' -import { createTestClient } from 'apollo-server-testing' -import { neode, getDriver } from '../../bootstrap/neo4j' +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 notifiedUser let authenticatedUser +const factory = Factory() +const driver = getDriver() +const instance = neode() +const categoryIds = ['cat9'] +const createPostMutation = gql ` + mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) { + CreatePost(id: $id, title: $title, content: $postContent, categoryIds: $categoryIds) { + id + title + content + } + } +` +const updatePostMutation = gql ` + mutation($id: ID!, $title: String!, $postContent: String!, $categoryIds: [ID]!) { + UpdatePost(id: $id, content: $postContent, title: $title, categoryIds: $categoryIds) { + title + content + } + } +` +const createCommentMutation = gql ` + mutation($id: ID, $postId: ID!, $commentContent: String!) { + CreateComment(id: $id, postId: $postId, content: $commentContent) { + id + content + } + } +` beforeAll(() => { const createServerResult = createServer({ @@ -30,13 +63,18 @@ beforeAll(() => { }) beforeEach(async () => { - user = await instance.create('User', { + notifiedUser = await instance.create('User', { id: 'you', name: 'Al Capone', slug: 'al-capone', email: 'test@example.org', password: '1234', }) + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) }) afterEach(async () => { @@ -44,7 +82,7 @@ afterEach(async () => { }) describe('notifications', () => { - const notificationQuery = gql` + const notificationQuery = gql ` query($read: Boolean) { currentUser { notifications(read: $read, orderBy: createdAt_desc) { @@ -63,47 +101,31 @@ describe('notifications', () => { describe('authenticated', () => { beforeEach(async () => { - authenticatedUser = await user.toJson() + authenticatedUser = await notifiedUser.toJson() }) describe('given another user', () => { - let postTitle + let title let postContent let postAuthor const createPostAction = async () => { - const createPostMutation = gql` - mutation($id: ID, $postTitle: String!, $postContent: String!) { - CreatePost(id: $id, title: $postTitle, content: $postContent) { - id - title - content - } - } - ` authenticatedUser = await postAuthor.toJson() await mutate({ mutation: createPostMutation, variables: { id: 'p47', - postTitle, + title, postContent, + categoryIds, }, }) - authenticatedUser = await user.toJson() + authenticatedUser = await notifiedUser.toJson() } let commentContent let commentAuthor const createCommentOnPostAction = async () => { await createPostAction() - const createCommentMutation = gql` - mutation($id: ID, $postId: ID!, $commentContent: String!) { - CreateComment(id: $id, postId: $postId, content: $commentContent) { - id - content - } - } - ` authenticatedUser = await commentAuthor.toJson() await mutate({ mutation: createCommentMutation, @@ -113,14 +135,14 @@ describe('notifications', () => { commentContent, }, }) - authenticatedUser = await user.toJson() + authenticatedUser = await notifiedUser.toJson() } describe('comments on my post', () => { beforeEach(async () => { - postTitle = 'My post' + title = 'My post' postContent = 'My post content.' - postAuthor = user + postAuthor = notifiedUser }) describe('commenter is not me', () => { @@ -140,20 +162,20 @@ describe('notifications', () => { const expected = expect.objectContaining({ data: { currentUser: { - notifications: [ - { - read: false, - reason: 'comment_on_post', - post: null, - comment: { - content: commentContent, - }, + notifications: [{ + read: false, + reason: 'comment_on_post', + post: null, + comment: { + content: commentContent, }, - ], + }, ], }, }, }) - const { query } = createTestClient(server) + const { + query + } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -164,8 +186,8 @@ describe('notifications', () => { ).resolves.toEqual(expected) }) - it('sends me no notification if I block the comment author', async () => { - await user.relateTo(commentAuthor, 'blocked') + it('sends me no notification if I have blocked the comment author', async () => { + await notifiedUser.relateTo(commentAuthor, 'blocked') await createCommentOnPostAction() const expected = expect.objectContaining({ data: { @@ -174,7 +196,9 @@ describe('notifications', () => { }, }, }) - const { query } = createTestClient(server) + const { + query + } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -189,11 +213,11 @@ describe('notifications', () => { describe('commenter is me', () => { beforeEach(async () => { commentContent = 'My comment.' - commentAuthor = user + commentAuthor = notifiedUser }) it('sends me no notification', async () => { - await user.relateTo(commentAuthor, 'blocked') + await notifiedUser.relateTo(commentAuthor, 'blocked') await createCommentOnPostAction() const expected = expect.objectContaining({ data: { @@ -202,7 +226,9 @@ describe('notifications', () => { }, }, }) - const { query } = createTestClient(server) + const { + query + } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -227,7 +253,7 @@ describe('notifications', () => { describe('mentions me in a post', () => { beforeEach(async () => { - postTitle = 'Mentioning Al Capone' + title = 'Mentioning Al Capone' postContent = 'Hey @al-capone how do you do?' }) @@ -239,20 +265,20 @@ describe('notifications', () => { const expected = expect.objectContaining({ data: { currentUser: { - notifications: [ - { - read: false, - reason: 'mentioned_in_post', - post: { - content: expectedContent, - }, - comment: null, + notifications: [{ + read: false, + reason: 'mentioned_in_post', + post: { + content: expectedContent, }, - ], + comment: null, + }, ], }, }, }) - const { query } = createTestClient(server) + const { + query + } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -263,7 +289,7 @@ describe('notifications', () => { ).resolves.toEqual(expected) }) - describe('who mentions me many times', () => { + describe('many times', () => { const updatePostAction = async () => { const updatedContent = ` One more mention to @@ -279,24 +305,17 @@ describe('notifications', () => { @al-capone ` - const updatePostMutation = gql` - mutation($id: ID!, $postTitle: String!, $postContent: String!) { - UpdatePost(id: $id, content: $postContent, title: $postTitle) { - title - content - } - } - ` authenticatedUser = await postAuthor.toJson() await mutate({ mutation: updatePostMutation, variables: { id: 'p47', - postTitle, + title, postContent: updatedContent, + categoryIds, }, }) - authenticatedUser = await user.toJson() + authenticatedUser = await notifiedUser.toJson() } it('creates exactly one more notification', async () => { @@ -307,8 +326,7 @@ describe('notifications', () => { const expected = expect.objectContaining({ data: { currentUser: { - notifications: [ - { + notifications: [{ read: false, reason: 'mentioned_in_post', post: { @@ -341,7 +359,7 @@ describe('notifications', () => { describe('but the author of the post blocked me', () => { beforeEach(async () => { - await postAuthor.relateTo(user, 'blocked') + await postAuthor.relateTo(notifiedUser, 'blocked') }) it('sends no notification', async () => { @@ -353,7 +371,9 @@ describe('notifications', () => { }, }, }) - const { query } = createTestClient(server) + const { + query + } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -368,7 +388,7 @@ describe('notifications', () => { describe('mentions me in a comment', () => { beforeEach(async () => { - postTitle = 'Post where I get mentioned in a comment' + title = 'Post where I get mentioned in a comment' postContent = 'Content of post where I get mentioned in a comment.' }) @@ -390,20 +410,20 @@ describe('notifications', () => { const expected = expect.objectContaining({ data: { currentUser: { - notifications: [ - { - read: false, - reason: 'mentioned_in_comment', - post: null, - comment: { - content: commentContent, - }, + notifications: [{ + read: false, + reason: 'mentioned_in_comment', + post: null, + comment: { + content: commentContent, }, - ], + }, ], }, }, }) - const { query } = createTestClient(server) + const { + query + } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -417,7 +437,7 @@ describe('notifications', () => { describe('but the author of the post blocked me', () => { beforeEach(async () => { - await postAuthor.relateTo(user, 'blocked') + await postAuthor.relateTo(notifiedUser, 'blocked') commentContent = 'One mention about me with @al-capone.' commentAuthor = await instance.create('User', { @@ -438,7 +458,9 @@ describe('notifications', () => { }, }, }) - const { query } = createTestClient(server) + const { + query + } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -455,11 +477,11 @@ describe('notifications', () => { }) describe('Hashtags', () => { - const postId = 'p135' - const postTitle = 'Two Hashtags' + const id = 'p135' + const title = 'Two Hashtags' const postContent = '
Hey Dude, #Democracy should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.
' - const postWithHastagsQuery = gql` + const postWithHastagsQuery = gql ` query($id: ID) { Post(id: $id) { tags { @@ -469,21 +491,12 @@ 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 () => { - authenticatedUser = await user.toJson() + authenticatedUser = await notifiedUser.toJson() }) describe('create a Post with Hashtags', () => { @@ -491,16 +504,16 @@ describe('Hashtags', () => { await mutate({ mutation: createPostMutation, variables: { - postId, - postTitle, + id, + title, postContent, + categoryIds, }, }) }) it('both Hashtags are created with the "id" set to their "name"', async () => { - const expected = [ - { + const expected = [{ id: 'Democracy', }, { @@ -515,11 +528,9 @@ describe('Hashtags', () => { ).resolves.toEqual( expect.objectContaining({ data: { - Post: [ - { - tags: expect.arrayContaining(expected), - }, - ], + Post: [{ + tags: expect.arrayContaining(expected), + }, ], }, }), ) @@ -527,30 +538,21 @@ 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 postContent = '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, + postContent, + categoryIds, }, }) - const expected = [ - { + const expected = [{ id: 'Elections', }, { @@ -565,11 +567,9 @@ describe('Hashtags', () => { ).resolves.toEqual( expect.objectContaining({ data: { - Post: [ - { - tags: expect.arrayContaining(expected), - }, - ], + Post: [{ + tags: expect.arrayContaining(expected), + }, ], }, }), ) @@ -577,4 +577,4 @@ describe('Hashtags', () => { }) }) }) -}) +}) \ No newline at end of file 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/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index 2d354ad2b..6094911b1 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -40,9 +40,21 @@ const validateUpdateComment = async (resolve, root, args, context, info) => { return resolve(root, args, context, info) } +const validatePost = async (resolve, root, args, context, info) => { + const { categoryIds } = args + 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) +} + export default { Mutation: { CreateComment: validateCommentCreation, UpdateComment: validateUpdateComment, + CreatePost: validatePost, + UpdatePost: validatePost, }, } 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 99dc2fcd3..3ca7727e4 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -1,26 +1,34 @@ 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 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 () => { await factory.cleanDatabase() }) -describe('query for notification', () => { +describe('Notification', () => { const notificationQuery = gql` - { + query { Notification { id } @@ -87,12 +95,7 @@ describe('currentUser notifications', () => { }), ]) await factory.authenticateAs(neighborParams) - // Post and its notifications - await Promise.all([ - factory.create('Post', { - id: 'p1', - }), - ]) + await factory.create('Post', { id: 'p1', categoryIds }) await Promise.all([ factory.relate('Notification', 'User', { from: 'post-mention-not-for-you', @@ -170,9 +173,7 @@ describe('currentUser notifications', () => { } } ` - const variables = { - read: false, - } + const variables = { read: false } it('returns only unread notifications of current user', async () => { const expected = { currentUser: { @@ -202,7 +203,7 @@ describe('currentUser notifications', () => { describe('no filters', () => { const queryCurrentUserNotifications = gql` - { + query { currentUser { notifications(orderBy: createdAt_desc) { id @@ -300,12 +301,7 @@ describe('UpdateNotification', () => { }), ]) await factory.authenticateAs(userParams) - // Post and its notifications - await Promise.all([ - factory.create('Post', { - id: 'p1', - }), - ]) + await factory.create('Post', { id: 'p1', categoryIds }) await Promise.all([ factory.relate('Notification', 'User', { from: 'post-mention-to-be-updated', diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index ea1f680bd..5bb0c4f81 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}) - SET post = $params - ` - if (categoryIds && categoryIds.length) { - updatePostCypher += `WITH post + const updatePostCypher = `MATCH (post:Post {id: $params.id}) + SET post = $params + WITH post UNWIND $categoryIds AS categoryId MATCH (category:Category {id: categoryId}) MERGE (post)-[:CATEGORIZED]->(category) - ` - } - updatePostCypher += `RETURN post` + RETURN post` + const updatePostVariables = { categoryIds, params } const transactionRes = await session.run(updatePostCypher, updatePostVariables) @@ -112,19 +109,16 @@ export default { 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 15376c8a4..44618ecdc 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -19,20 +19,10 @@ 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 postSaveError = 'You cannot save a post without at least one category or more than three' +const categoryIds = ['cat9', 'cat4', 'cat15'] +let createPostVariables + const postQueryWithCategories = gql` query($id: ID) { Post(id: $id) { @@ -42,11 +32,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 +43,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 +73,34 @@ beforeEach(async () => { password: '1234', } await factory.create('User', userParams) + await Promise.all([ + instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }), + instance.create('Category', { + id: 'cat4', + name: 'Environment & Nature', + icon: 'tree', + }), + instance.create('Category', { + id: 'cat15', + name: 'Consumption & Sustainability', + icon: 'shopping-cart', + }), + instance.create('Category', { + id: 'cat27', + name: 'Animal Protection', + icon: 'paw', + }), + ]) + createPostVariables = { + id: 'p3589', + title: postTitle, + content: postContent, + categoryIds, + } }) afterEach(async () => { @@ -152,8 +165,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 +180,7 @@ describe('CreatePost', () => { title: postTitle, content: postContent, language: 'en', + categoryIds, } const expected = { CreatePost: { language: 'en' } } await expect( @@ -171,51 +190,36 @@ 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( + postSaveError, ) }) - 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, - } + it('requires at least one category for successful creation', async () => { + createPostVariables.categoryIds = [] + await expect(client.request(createPostMutation, createPostVariables)).rejects.toThrow( + postSaveError, + ) + }) - await expect( - client.request(postQueryWithCategories, postQueryWithCategoriesVariables), - ).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] }) + 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, + ) }) 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), }, ], } @@ -228,8 +232,15 @@ 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 () => { const asAuthor = Factory() await asAuthor.create('User', authorParams) @@ -238,15 +249,8 @@ describe('UpdatePost', () => { id: 'p1', title: oldTitle, 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', @@ -287,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, @@ -294,36 +299,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,12 +313,33 @@ 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), ).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, + ) + }) }) }) }) @@ -365,6 +365,7 @@ describe('DeletePost', () => { await asAuthor.create('Post', { id: 'p1', content: 'To be deleted', + categoryIds, }) }) @@ -411,7 +412,7 @@ describe('emotions', () => { postQueryAction, postToEmote, postToEmoteNode - const PostsEmotionsCountQuery = ` + const PostsEmotionsCountQuery = gql` query($id: ID!) { Post(id: $id) { emotionsCount 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', 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 }, } } 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', diff --git a/backend/yarn.lock b/backend/yarn.lock index 55c4f7494..da337eb33 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1826,12 +1826,12 @@ apollo-server-plugin-base@0.6.1: dependencies: apollo-server-types "0.2.1" -apollo-server-testing@~2.8.1: - version "2.8.1" - resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.8.1.tgz#70026e1b6abab1ca51ffee21bfda61f5b5ad92c1" - integrity sha512-bWKczu9HPBWBOz3GDtPA1pykmIvK2TOTLaK03AVSZODvZX0YLWizB0bq5I5Ox6rG+wmW638v1Kq+BhADVHovdg== +apollo-server-testing@~2.8.2: + version "2.8.2" + resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.8.2.tgz#8faa8f1778fa4893f5bf705f7cea84a69477aa3f" + integrity sha512-ccp1DpmjdmLT98ww4NtSiDPbeIPlVZJ5Iy408ToyhAGwNXRHk5f8Czf+JAgSayvgt4cxCm1fzxnVe1OjO8oIvA== dependencies: - apollo-server-core "2.8.1" + apollo-server-core "2.8.2" apollo-server-types@0.2.1: version "0.2.1" @@ -2954,10 +2954,10 @@ data-urls@^1.0.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -date-fns@2.0.0-beta.5: - version "2.0.0-beta.5" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-beta.5.tgz#90885db3772802d55519cd12acd49de56aca1059" - integrity sha512-GS5yi964NDFNoja9yOdWFj9T97T67yLrUeJZgddHaVfc/6tHWtX7RXocuubmZkNzrZUZ9BqBOW7jTR5OoWjJ1w== +date-fns@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0.tgz#52f05c6ae1fe0e395670082c72b690ab781682d0" + integrity sha512-nGZDA64Ktq5uTWV4LEH3qX+foV4AguT5qxwRlJDzJtf57d4xLNwtwrfb7SzKCoikoae8Bvxf0zdaEG/xWssp/w== debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" diff --git a/cypress/integration/administration/TagsAndCategories.feature b/cypress/integration/administration/TagsAndCategories.feature index e543b143b..f93cdb59c 100644 --- a/cypress/integration/administration/TagsAndCategories.feature +++ b/cypress/integration/administration/TagsAndCategories.feature @@ -24,8 +24,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..98e2d862b 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -21,31 +21,17 @@ Given("I am logged in", () => { cy.login(loginCredentials); }); +Given("we have a selection of categories", () => { + cy.createCategories("cat0", "just-for-fun"); +}); + Given("we have a selection of tags and categories as well as posts", () => { - cy.factory() + cy.createCategories("cat12") + .factory() .authenticateAs(loginCredentials) - .create("Category", { - id: "cat1", - name: "Just For Fun", - slug: "justforfun", - icon: "smile" - }) - .create("Category", { - id: "cat2", - name: "Happyness & Values", - slug: "happyness-values", - icon: "heart-o" - }) - .create("Category", { - id: "cat3", - name: "Health & Wellbeing", - slug: "health-wellbeing", - icon: "medkit" - }) .create("Tag", { id: "Ecology" }) .create("Tag", { id: "Nature" }) .create("Tag", { id: "Democracy" }); - const someAuthor = { id: "authorId", email: "author@example.org", @@ -59,18 +45,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: ["cat12"] }) + .create("Post", { id: "p1", categoryIds: ["cat121"] }); cy.factory() .create("User", yetAnotherAuthor) .authenticateAs(yetAnotherAuthor) - .create("Post", { id: "p2" }); + .create("Post", { id: "p2", categoryIds: ["cat12"] }); 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: ["cat122"] }) .relate("Post", "Tags", { from: "p0", to: "Ecology" }) .relate("Post", "Tags", { from: "p0", to: "Nature" }) .relate("Post", "Tags", { from: "p0", to: "Democracy" }) @@ -182,9 +165,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 +209,7 @@ When( Given("I previously created a post", () => { lastPost.title = "previously created post"; lastPost.content = "with some content"; + lastPost.categoryIds = "cat0"; cy.factory() .authenticateAs(loginCredentials) .create("Post", lastPost); @@ -233,6 +225,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 +258,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); } @@ -354,7 +354,7 @@ When("mention {string} in the text", mention => { }); Then("the notification gets marked as read", () => { - cy.get(".notification") + cy.get(".post.createdAt") .first() .should("have.class", "read"); }); @@ -417,12 +417,13 @@ Given("I follow the user {string}", name => { }); Given('"Spammy Spammer" wrote a post {string}', title => { - cy.factory() + cy.createCategories("cat21") + .factory() .authenticateAs({ email: "spammy-spammer@example.org", password: "1234" }) - .create("Post", { title }); + .create("Post", { title, categoryIds: ["cat21"] }); }); Then("the list of posts of this user is empty", () => { @@ -439,9 +440,10 @@ Then("nobody is following the user profile anymore", () => { }); Given("I wrote a post {string}", title => { - cy.factory() + cy.createCategories(`cat213`, title) + .factory() .authenticateAs(loginCredentials) - .create("Post", { title }); + .create("Post", { title, categoryIds: ["cat213"] }); }); When("I block the user {string}", name => { 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/notifications/Mentions.feature b/cypress/integration/notifications/Mentions.feature index 28f7cf456..d3c123863 100644 --- a/cypress/integration/notifications/Mentions.feature +++ b/cypress/integration/notifications/Mentions.feature @@ -4,25 +4,27 @@ Feature: Notifications for a mentions In order join conversations about or related to me Background: - Given we have the following user accounts: - | name | slug | email | password | - | Wolle aus Hamburg | wolle-aus-hamburg | wolle@example.org | 1234 | - | Matt Rider | matt-rider | matt@example.org | 4321 | + Given we have a selection of categories + And we have the following user accounts: + | name | slug | email | password | + | Wolle aus Hamburg | wolle-aus-hamburg | wolle@example.org | 1234 | + | Matt Rider | matt-rider | matt@example.org | 4321 | Scenario: Mention another user, re-login as this user and see notifications Given I log in with the following credentials: | email | password | | wolle@example.org | 1234 | And I start to write a new post with the title "Hey Matt" beginning with: - """ - Big shout to our fellow contributor - """ + """ + Big shout to our fellow contributor + """ And mention "@matt-rider" in the text + And I select a category And I click on "Save" When I log out And I log in with the following credentials: - | email | password | - | matt@example.org | 4321 | + | email | password | + | matt@example.org | 4321 | And see 1 unread notifications in the top menu And open the notification menu and click on the first item Then I get to the post page of ".../hey-matt" 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/cypress/support/commands.js b/cypress/support/commands.js index f6253af20..e69d296dc 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -13,55 +13,74 @@ // Cypress.Commands.add('login', (email, password) => { ... }) /* globals Cypress cy */ -import 'cypress-file-upload' -import { getLangByName } from './helpers' -import users from '../fixtures/users.json' +import "cypress-file-upload"; +import { getLangByName } from "./helpers"; +import users from "../fixtures/users.json"; const switchLang = name => { - cy.get('.locale-menu').click() - cy.contains('.locale-menu-popover a', name).click() -} + cy.get(".locale-menu").click(); + cy.contains(".locale-menu-popover a", name).click(); +}; -Cypress.Commands.add('switchLanguage', (name, force) => { - const code = getLangByName(name).code +Cypress.Commands.add("switchLanguage", (name, force) => { + const code = getLangByName(name).code; if (force) { - switchLang(name) + switchLang(name); } else { - cy.get('html').then($html => { - if ($html && $html.attr('lang') !== code) { - switchLang(name) + cy.get("html").then($html => { + if ($html && $html.attr("lang") !== code) { + switchLang(name); } + }); + } +}); + +Cypress.Commands.add("login", ({ email, password }) => { + cy.visit(`/login`); + cy.get("input[name=email]") + .trigger("focus") + .type(email); + cy.get("input[name=password]") + .trigger("focus") + .type(password); + cy.get("button[name=submit]") + .as("submitButton") + .click(); + cy.get(".iziToast-message").should("contain", "You are logged in!"); + cy.get(".iziToast-close").click(); +}); + +Cypress.Commands.add("logout", (email, password) => { + cy.visit(`/logout`); + cy.location("pathname").should("contain", "/login"); // we're out +}); + +Cypress.Commands.add("openPage", page => { + if (page === "landing") { + page = ""; + } + cy.visit(`/${page}`); +}); + +Cypress.Commands.add("createCategories", (id, slug) => { + cy.neode() + .create("Category", { + id: `${id}`, + name: "Just For Fun", + slug: `${slug}`, + icon: "smile" }) - } -}) - -Cypress.Commands.add('login', ({ email, password }) => { - cy.visit(`/login`) - cy.get('input[name=email]') - .trigger('focus') - .type(email) - cy.get('input[name=password]') - .trigger('focus') - .type(password) - cy.get('button[name=submit]') - .as('submitButton') - .click() - cy.get('.iziToast-message').should('contain', 'You are logged in!') - cy.get('.iziToast-close').click() -}) - -Cypress.Commands.add('logout', (email, password) => { - cy.visit(`/logout`) - cy.location('pathname').should('contain', '/login') // we're out -}) - -Cypress.Commands.add('openPage', page => { - if (page === 'landing') { - page = '' - } - cy.visit(`/${page}`) -}) - + .create("Category", { + id: `${id}1`, + name: "Happiness & Values", + icon: "heart-o" + }) + .create("Category", { + id: `${id}2`, + name: "Health & Wellbeing", + icon: "medkit" + }); +}); // // // -- This is a child command -- 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 @@