From 16f077fe65260f56e9fc95d7619cee7217940adb Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Tue, 16 Jul 2019 11:02:43 -0300 Subject: [PATCH 01/17] Add authentication tests for AddPostEmotions, emotionsCount --- .../src/middleware/permissionsMiddleware.js | 2 + backend/src/schema/resolvers/posts.js | 1 + backend/src/schema/resolvers/posts.spec.js | 74 +++++++++++++++++++ backend/src/schema/types/type/Post.gql | 4 + 4 files changed, 81 insertions(+) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 31c373fc7..8e3e2311a 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -167,6 +167,7 @@ const permissions = shield( // RemoveBadgeRewarded: isAdmin, reward: isAdmin, unreward: isAdmin, + // why is this here? will we support buying/selling fruit?? // addFruitToBasket: isAuthenticated follow: isAuthenticated, unfollow: isAuthenticated, @@ -180,6 +181,7 @@ const permissions = shield( DeleteUser: isDeletingOwnAccount, requestPasswordReset: allow, resetPassword: allow, + AddPostEmotions: isAuthenticated, }, User: { email: isMyOwn, diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 0c8dfb7f0..74f88d62e 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -44,6 +44,7 @@ export default { 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}) diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 233be450a..49cb2123b 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -383,3 +383,77 @@ describe('DeletePost', () => { }) }) }) + +describe('AddPostEmotions', () => { + let addPostEmotionsVariables + const addPostEmotionsMutation = ` + mutation($from: _UserInput!, $to: _PostInput!, $data: _EMOTEDInput!) { + AddPostEmotions(from: $from, to: $to, data: $data) { + from { + id + } + to { + id + } + emotion + } + } + ` + describe('emotions', () => { + beforeEach(async () => { + const asAuthor = Factory() + authorParams = { + id: 'u25', + email: 'wanna-add-emotions@example.org', + password: '1234', + } + await asAuthor.create('User', authorParams) + await asAuthor.authenticateAs(authorParams) + await asAuthor.create('Post', { + id: 'p1376', + title: postTitle, + content: postContent, + }) + addPostEmotionsVariables = { + from: { id: authorParams.id }, + to: { id: 'p1376' }, + data: { emotion: 'happy' }, + } + }) + // it('supports setting emotions for a post', () => {}) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect( + client.request(addPostEmotionsMutation, { + from: { id: 'u25' }, + to: { id: 'p1376' }, + data: { emotion: 'happy' }, + }), + ).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated as author', () => { + let headers + beforeEach(async () => { + headers = await login(authorParams) + client = new GraphQLClient(host, { headers }) + }) + + it('adds an emotion to the post', async () => { + const expected = { + AddPostEmotions: { + from: addPostEmotionsVariables.from, + to: addPostEmotionsVariables.to, + emotion: 'happy', + }, + } + await expect( + client.request(addPostEmotionsMutation, addPostEmotionsVariables), + ).resolves.toEqual(expected) + }) + }) + }) +}) diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index d254a9a9c..30cdd71d4 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -50,6 +50,10 @@ type Post { ) emotions: [EMOTED] + emotionsCount: Int! + @cypher( + statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)" + ) } type Mutation { From 3e43539e23b874fa884d890b79a708e41b86cd5b Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Mon, 29 Jul 2019 17:08:07 +0200 Subject: [PATCH 02/17] Set up some backend tests for emotions --- .../src/middleware/permissionsMiddleware.js | 1 + backend/src/schema/resolvers/posts.js | 34 ++++ backend/src/schema/resolvers/posts.spec.js | 151 ++++++++++++++---- backend/src/schema/types/type/Post.gql | 5 +- backend/yarn.lock | 104 ------------ 5 files changed, 154 insertions(+), 141 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index fe33cd4c4..fe001d676 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -184,6 +184,7 @@ const permissions = shield( requestPasswordReset: allow, resetPassword: allow, AddPostEmotions: isAuthenticated, + RemovePostEmotions: isAuthenticated, }, User: { email: isMyOwn, diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 74f88d62e..c4ed494b7 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -71,5 +71,39 @@ export default { return post.properties }, + AddPostEmotions: async (object, params, context, resolveInfo) => { + const session = context.driver.session() + const { from, to, data } = params + const transactionRes = await session.run( + `MATCH (userFrom:User {id: $from.id}), (postTo:Post {id: $to.id}) + MERGE (userFrom)-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo) + RETURN userFrom, postTo, emotedRelation`, + { from, to, data }, + ) + session.close() + const [emoted] = transactionRes.records.map(record => { + return { + from: { id: record.get('userFrom').properties.id }, + to: { id: record.get('postTo').properties.id }, + emotion: record.get('emotedRelation').properties.emotion, + } + }) + return emoted + }, + RemovePostEmotions: async (object, params, context, resolveInfo) => { + const session = context.driver.session() + const { from, to, data } = params + const transactionRes = await session.run( + `MATCH (userFrom:User {id: $from.id})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id}) + DELETE emotedRelation + RETURN emotedRelation`, + { from, to, data }, + ) + session.close() + const [removed] = transactionRes.records.map(record => { + return record.get('emotedRelation') + }) + return !!removed + }, }, } diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 49cb2123b..e1c85974b 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -58,6 +58,7 @@ const postQueryFilteredByCategoryVariables = { } beforeEach(async () => { userParams = { + id: 'u198', name: 'TestUser', email: 'test@example.org', password: '1234', @@ -384,43 +385,59 @@ describe('DeletePost', () => { }) }) -describe('AddPostEmotions', () => { +describe('emotions', () => { let addPostEmotionsVariables - const addPostEmotionsMutation = ` - mutation($from: _UserInput!, $to: _PostInput!, $data: _EMOTEDInput!) { - AddPostEmotions(from: $from, to: $to, data: $data) { - from { - id - } - to { - id - } - emotion - } + let postEmotionsVariables + beforeEach(async () => { + const asAuthor = Factory() + authorParams = { + id: 'u25', + email: 'wanna-add-emotions@example.org', + password: '1234', } - ` - describe('emotions', () => { - beforeEach(async () => { - const asAuthor = Factory() - authorParams = { - id: 'u25', - email: 'wanna-add-emotions@example.org', - password: '1234', - } - await asAuthor.create('User', authorParams) - await asAuthor.authenticateAs(authorParams) - await asAuthor.create('Post', { - id: 'p1376', - title: postTitle, - content: postContent, - }) - addPostEmotionsVariables = { - from: { id: authorParams.id }, - to: { id: 'p1376' }, - data: { emotion: 'happy' }, - } + await asAuthor.create('User', authorParams) + await asAuthor.authenticateAs(authorParams) + await asAuthor.create('Post', { + id: 'p1376', + title: postTitle, + content: postContent, }) - // it('supports setting emotions for a post', () => {}) + addPostEmotionsVariables = { + from: { id: authorParams.id }, + to: { id: 'p1376' }, + data: { emotion: 'happy' }, + } + postEmotionsVariables = { + id: 'p1376', + } + }) + + describe('AddPostEmotions', () => { + const addPostEmotionsMutation = ` + mutation($from: _UserInput!, $to: _PostInput!, $data: _EMOTEDInput!) { + AddPostEmotions(from: $from, to: $to, data: $data) { + from { id } + to { id } + emotion + } + } + ` + const postEmotionsCountQuery = ` + query($id: ID!) { + Post(id: $id) { + emotionsCount + } + } + ` + const postEmotionsQuery = ` + query($id: ID!) { + Post(id: $id) { + emotions { + emotion + } + } + } + ` describe('unauthenticated', () => { it('throws authorization error', async () => { @@ -435,6 +452,52 @@ describe('AddPostEmotions', () => { }) }) + describe('authenticated and not the author', () => { + let headers + beforeEach(async () => { + headers = await login(userParams) + client = new GraphQLClient(host, { headers }) + }) + + it('adds an emotion to the post', async () => { + addPostEmotionsVariables.from.id = userParams.id + const expected = { + AddPostEmotions: { + from: addPostEmotionsVariables.from, + to: addPostEmotionsVariables.to, + emotion: 'happy', + }, + } + await expect( + client.request(addPostEmotionsMutation, addPostEmotionsVariables), + ).resolves.toEqual(expected) + }) + it('limits the addition of the same emotion to 1', async () => { + const expected = { + Post: [ + { + emotionsCount: 1, + }, + ], + } + await client.request(addPostEmotionsMutation, addPostEmotionsVariables) + await client.request(addPostEmotionsMutation, addPostEmotionsVariables) + await expect( + client.request(postEmotionsCountQuery, postEmotionsVariables), + ).resolves.toEqual(expected) + }) + + it('allows a user to add more than one emotion', async () => { + const expected = [{ emotion: 'happy' }, { emotion: 'surprised' }] + await client.request(addPostEmotionsMutation, addPostEmotionsVariables) + addPostEmotionsVariables.data.emotion = 'surprised' + await client.request(addPostEmotionsMutation, addPostEmotionsVariables) + await expect(client.request(postEmotionsQuery, postEmotionsVariables)).resolves.toEqual({ + Post: [{ emotions: expect.arrayContaining(expected) }], + }) + }) + }) + describe('authenticated as author', () => { let headers beforeEach(async () => { @@ -456,4 +519,24 @@ describe('AddPostEmotions', () => { }) }) }) + + describe('RemovePostEmotions', () => { + const removePostEmotionsMutation = ` + mutation($from: _UserInput!, $to: _PostInput!, $data: _EMOTEDInput!) { + RemovePostEmotions(from: $from, to: $to, data: $data) + } + ` + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect( + client.request(removePostEmotionsMutation, { + from: { id: 'u25' }, + to: { id: 'p1376' }, + data: { emotion: 'happy' }, + }), + ).rejects.toThrow('Not Authorised') + }) + }) + }) }) diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 30cdd71d4..422ab2867 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -51,9 +51,7 @@ type Post { emotions: [EMOTED] emotionsCount: Int! - @cypher( - statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)" - ) + @cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)") } type Mutation { @@ -93,4 +91,5 @@ type Mutation { language: String categoryIds: [ID] ): Post + RemovePostEmotions(from: _UserInput!, to: _PostInput!, data: _EMOTEDInput!): Boolean! } diff --git a/backend/yarn.lock b/backend/yarn.lock index beeaa91e1..e799968b3 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -2,13 +2,6 @@ # yarn lockfile v1 -"@apollographql/apollo-tools@^0.3.6": - version "0.3.7" - resolved "https://registry.yarnpkg.com/@apollographql/apollo-tools/-/apollo-tools-0.3.7.tgz#3bc9c35b9fff65febd4ddc0c1fc04677693a3d40" - integrity sha512-+ertvzAwzkYmuUtT8zH3Zi6jPdyxZwOgnYaZHY7iLnMVJDhQKWlkyjLMF8wyzlPiEdDImVUMm5lOIBZo7LkGlg== - dependencies: - apollo-env "0.5.1" - "@apollographql/apollo-tools@^0.4.0": version "0.4.0" resolved "https://registry.yarnpkg.com/@apollographql/apollo-tools/-/apollo-tools-0.4.0.tgz#8a1a0ab7a0bb12ccc03b72e4a104cfa5d969fd5f" @@ -1454,14 +1447,6 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -apollo-cache-control@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.0.tgz#08b157e5f8cd86f63608b05d45222de0725ebd5a" - integrity sha512-BBnfUmSWRws5dRSDD+R56RLJCE9v6xQuob+i/1Ju9EX4LZszU5JKVmxEvnkJ1bk/BkihjoQXTnP6fJCnt6fCmA== - dependencies: - apollo-server-env "2.4.0" - graphql-extensions "0.8.0" - apollo-cache-control@0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.1.tgz#707c0b958c02c5b47ddf49a02f60ea88a64783fb" @@ -1503,14 +1488,6 @@ apollo-client@~2.6.3: tslib "^1.9.3" zen-observable "^0.8.0" -apollo-datasource@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.0.tgz#823d6be8a3804613b5c56d2972c07db662293fc6" - integrity sha512-DOzzYWEOReYRu2vWPKEulqlTb9Xjg67sjVCzve5MXa7GUXjfr8IKioljvfoBMlqm/PpbJVk2ci4n5NIFqoYsrQ== - dependencies: - apollo-server-caching "0.5.0" - apollo-server-env "2.4.0" - apollo-datasource@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.1.tgz#697870f564da90bee53fa30d07875cb46c4d6b06" @@ -1526,18 +1503,6 @@ apollo-engine-reporting-protobuf@0.4.0: dependencies: protobufjs "^6.8.6" -apollo-engine-reporting@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.0.tgz#3a9bd011b271593e16d7057044898d0a817b197d" - integrity sha512-NMiO3h1cuEBt6QZNGHxivwuyZQnoU/2MMx0gUA8Gyy1ERBhK6P235qoMnvoi34rLmqJuyGPX6tXcab8MpMIzYQ== - dependencies: - apollo-engine-reporting-protobuf "0.4.0" - apollo-graphql "^0.3.3" - apollo-server-env "2.4.0" - apollo-server-types "0.2.0" - async-retry "^1.2.1" - graphql-extensions "0.8.0" - apollo-engine-reporting@1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.2.tgz#f6c1e964c3c2c09bdb25c449f6b7ab05952ff459" @@ -1618,34 +1583,6 @@ apollo-server-caching@0.5.0: dependencies: lru-cache "^5.0.0" -apollo-server-core@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.7.0.tgz#c444347dea11149b5b453890506e43dc7e711257" - integrity sha512-CXjXAkgcMBCJZpsZgfAY5W7f5thdxUhn75UgzeH28RTUZ2aKi/LjoCixPWRSF1lU4vuEWneAnM8Vg/KCD+29lQ== - dependencies: - "@apollographql/apollo-tools" "^0.3.6" - "@apollographql/graphql-playground-html" "1.6.24" - "@types/ws" "^6.0.0" - apollo-cache-control "0.8.0" - apollo-datasource "0.6.0" - apollo-engine-reporting "1.4.0" - apollo-engine-reporting-protobuf "0.4.0" - apollo-server-caching "0.5.0" - apollo-server-env "2.4.0" - apollo-server-errors "2.3.1" - apollo-server-plugin-base "0.6.0" - apollo-server-types "0.2.0" - apollo-tracing "0.8.0" - fast-json-stable-stringify "^2.0.0" - graphql-extensions "0.8.0" - graphql-subscriptions "^1.0.0" - graphql-tag "^2.9.2" - graphql-tools "^4.0.0" - graphql-upload "^8.0.2" - sha.js "^2.4.11" - subscriptions-transport-ws "^0.9.11" - ws "^6.0.0" - apollo-server-core@2.7.2: version "2.7.2" resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.7.2.tgz#4acd9f4d0d235bef0e596e2a821326dfc07ae7b2" @@ -1672,14 +1609,6 @@ apollo-server-core@2.7.2: subscriptions-transport-ws "^0.9.11" ws "^6.0.0" -apollo-server-env@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.0.tgz#6611556c6b627a1636eed31317d4f7ea30705872" - integrity sha512-7ispR68lv92viFeu5zsRUVGP+oxsVI3WeeBNniM22Cx619maBUwcYTIC3+Y3LpXILhLZCzA1FASZwusgSlyN9w== - dependencies: - node-fetch "^2.1.2" - util.promisify "^1.0.0" - apollo-server-env@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.1.tgz#58264ecfeb151919e0f480320b4e3769be9f18f3" @@ -1713,13 +1642,6 @@ apollo-server-express@2.7.2, apollo-server-express@^2.7.2: subscriptions-transport-ws "^0.9.16" type-is "^1.6.16" -apollo-server-plugin-base@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.0.tgz#4186296ea5d52cfe613961d252a8a2f9e13e6ba6" - integrity sha512-BjfyWpHyKwHOe819gk3wEFwbnVp9Xvos03lkkYTTcXS/8G7xO78aUcE65mmyAC56/ZQ0aodNFkFrhwNtWBQWUQ== - dependencies: - apollo-server-types "0.2.0" - apollo-server-plugin-base@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.1.tgz#b9c209aa2102a26c6134f51bfa1e4a8307b63b11" @@ -1734,15 +1656,6 @@ apollo-server-testing@~2.7.2: dependencies: apollo-server-core "2.7.2" -apollo-server-types@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.0.tgz#270d7298f709fd8237ebfa48753249e5286df5f2" - integrity sha512-5dgiyXsM90vnfmdXO1ixHvsLn0d9NP4tWufmr3ZmjKv00r4JAQNUaUdgOSGbRIKoHELQGwxUuTySTZ/tYfGaNQ== - dependencies: - apollo-engine-reporting-protobuf "0.4.0" - apollo-server-caching "0.5.0" - apollo-server-env "2.4.0" - apollo-server-types@0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.1.tgz#553da40ea1ad779ef0390c250ddad7eb782fdf64" @@ -1763,14 +1676,6 @@ apollo-server@~2.7.2: graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" -apollo-tracing@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.8.0.tgz#28cd9c61a4db12b2c24dad67fdedd309806c1650" - integrity sha512-cNOtOlyZ56iJRsCjnxjM1V0SnQ2ZZttuyoeOejdat6llPfk5bfYTVOKMjdbSfDvU33LS9g9sqNJCT0MwrEPFKQ== - dependencies: - apollo-server-env "2.4.0" - graphql-extensions "0.8.0" - apollo-tracing@0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.8.1.tgz#220aeac6ad598c67f9333739155b7a56bd63ccab" @@ -4071,15 +3976,6 @@ graphql-custom-directives@~0.2.14: moment "^2.22.2" numeral "^2.0.6" -graphql-extensions@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.8.0.tgz#b3fe7915aa84eef5a39135840840cc4d2e700c46" - integrity sha512-zV9RefkusIXqi9ZJtl7IJ5ecjDKdb7PLAb5E3CmxX3OK1GwNCIubp0vE7Fp4fXlCUKgTB1Woubs0zj71JT8o0A== - dependencies: - "@apollographql/apollo-tools" "^0.3.6" - apollo-server-env "2.4.0" - apollo-server-types "0.2.0" - graphql-extensions@0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.8.1.tgz#f5f1fed5fe49620c4e70c5d08bdbd0039e91c402" From 0a98113c2df5afcd3d0a184f4ca3c2ae1961539a Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Tue, 30 Jul 2019 13:28:30 +0200 Subject: [PATCH 03/17] Add backend tests for RemovePostEmotions --- .../src/middleware/permissionsMiddleware.js | 8 +- backend/src/schema/resolvers/posts.js | 9 +- backend/src/schema/resolvers/posts.spec.js | 118 +++++++++++++----- backend/src/seed/factories/index.js | 17 +++ backend/src/seed/seed-db.js | 80 ++++++++++++ 5 files changed, 199 insertions(+), 33 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index fe001d676..8a7aced85 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -24,6 +24,12 @@ const onlyYourself = rule({ return context.user.id === args.id }) +const isMyEmotion = rule({ + cache: 'no_cache', +})(async (parent, args, context, info) => { + return context.user.id === args.from.id +}) + const isMyOwn = rule({ cache: 'no_cache', })(async (parent, args, context, info) => { @@ -184,7 +190,7 @@ const permissions = shield( requestPasswordReset: allow, resetPassword: allow, AddPostEmotions: isAuthenticated, - RemovePostEmotions: isAuthenticated, + RemovePostEmotions: isMyEmotion, }, User: { email: isMyOwn, diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index c4ed494b7..754d9a6ed 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -96,14 +96,15 @@ export default { const transactionRes = await session.run( `MATCH (userFrom:User {id: $from.id})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id}) DELETE emotedRelation - RETURN emotedRelation`, + `, { from, to, data }, ) session.close() - const [removed] = transactionRes.records.map(record => { - return record.get('emotedRelation') + const [emoted] = transactionRes.records.map(record => { + return record.get('emotedRelation').properties.emotion }) - return !!removed + + return !emoted }, }, } diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index e1c85974b..68de51e79 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -64,6 +64,7 @@ beforeEach(async () => { password: '1234', } authorParams = { + id: 'u25', email: 'author@example.org', password: '1234', } @@ -387,11 +388,23 @@ describe('DeletePost', () => { describe('emotions', () => { let addPostEmotionsVariables - let postEmotionsVariables + let postEmotionsQueryVariables + const postEmotionsQuery = ` + query($id: ID!) { + Post(id: $id) { + emotions { + emotion + User { + id + } + } + } + } + ` beforeEach(async () => { const asAuthor = Factory() authorParams = { - id: 'u25', + id: authorParams.id, email: 'wanna-add-emotions@example.org', password: '1234', } @@ -402,17 +415,18 @@ describe('emotions', () => { title: postTitle, content: postContent, }) - addPostEmotionsVariables = { - from: { id: authorParams.id }, - to: { id: 'p1376' }, - data: { emotion: 'happy' }, - } - postEmotionsVariables = { - id: 'p1376', - } + postEmotionsQueryVariables = { id: 'p1376' } }) describe('AddPostEmotions', () => { + beforeEach(() => { + addPostEmotionsVariables = { + from: { id: authorParams.id }, + to: { id: 'p1376' }, + data: { emotion: 'happy' }, + } + }) + const addPostEmotionsMutation = ` mutation($from: _UserInput!, $to: _PostInput!, $data: _EMOTEDInput!) { AddPostEmotions(from: $from, to: $to, data: $data) { @@ -429,22 +443,13 @@ describe('emotions', () => { } } ` - const postEmotionsQuery = ` - query($id: ID!) { - Post(id: $id) { - emotions { - emotion - } - } - } - ` describe('unauthenticated', () => { it('throws authorization error', async () => { client = new GraphQLClient(host) await expect( client.request(addPostEmotionsMutation, { - from: { id: 'u25' }, + from: { id: authorParams.id }, to: { id: 'p1376' }, data: { emotion: 'happy' }, }), @@ -472,6 +477,7 @@ describe('emotions', () => { client.request(addPostEmotionsMutation, addPostEmotionsVariables), ).resolves.toEqual(expected) }) + it('limits the addition of the same emotion to 1', async () => { const expected = { Post: [ @@ -483,16 +489,21 @@ describe('emotions', () => { await client.request(addPostEmotionsMutation, addPostEmotionsVariables) await client.request(addPostEmotionsMutation, addPostEmotionsVariables) await expect( - client.request(postEmotionsCountQuery, postEmotionsVariables), + client.request(postEmotionsCountQuery, postEmotionsQueryVariables), ).resolves.toEqual(expected) }) it('allows a user to add more than one emotion', async () => { - const expected = [{ emotion: 'happy' }, { emotion: 'surprised' }] + const expected = [ + { emotion: 'happy', User: { id: authorParams.id } }, + { emotion: 'surprised', User: { id: authorParams.id } }, + ] await client.request(addPostEmotionsMutation, addPostEmotionsVariables) addPostEmotionsVariables.data.emotion = 'surprised' await client.request(addPostEmotionsMutation, addPostEmotionsVariables) - await expect(client.request(postEmotionsQuery, postEmotionsVariables)).resolves.toEqual({ + await expect( + client.request(postEmotionsQuery, postEmotionsQueryVariables), + ).resolves.toEqual({ Post: [{ emotions: expect.arrayContaining(expected) }], }) }) @@ -521,22 +532,73 @@ describe('emotions', () => { }) describe('RemovePostEmotions', () => { + let removePostEmotionsVariables const removePostEmotionsMutation = ` mutation($from: _UserInput!, $to: _PostInput!, $data: _EMOTEDInput!) { RemovePostEmotions(from: $from, to: $to, data: $data) } ` + beforeEach(() => { + removePostEmotionsVariables = { + from: { id: authorParams.id }, + to: { id: 'p1376' }, + data: { emotion: 'cry' }, + } + }) + describe('unauthenticated', () => { it('throws authorization error', async () => { client = new GraphQLClient(host) await expect( - client.request(removePostEmotionsMutation, { - from: { id: 'u25' }, - to: { id: 'p1376' }, - data: { emotion: 'happy' }, - }), + client.request(removePostEmotionsMutation, removePostEmotionsVariables), ).rejects.toThrow('Not Authorised') }) }) + + describe('authenticated', () => { + let headers + beforeEach(async () => { + headers = await login(authorParams) + client = new GraphQLClient(host, { headers }) + await factory.emote({ + from: authorParams.id, + to: 'p1376', + data: 'cry', + }) + await factory.emote({ + from: authorParams.id, + to: 'p1376', + data: 'happy', + }) + }) + + describe('but not the emoter', () => { + it('throws an authorization error', async () => { + removePostEmotionsVariables.from.id = userParams.id + await expect( + client.request(removePostEmotionsMutation, removePostEmotionsVariables), + ).rejects.toThrow('Not Authorised') + }) + }) + + describe('as the emoter', () => { + it('removes an emotion from a post', async () => { + const expected = { RemovePostEmotions: true } + await expect( + client.request(removePostEmotionsMutation, removePostEmotionsVariables), + ).resolves.toEqual(expected) + }) + + it('removes only the requested emotion, not all emotions', async () => { + const expected = [{ emotion: 'happy', User: { id: authorParams.id } }] + await client.request(removePostEmotionsMutation, removePostEmotionsVariables) + await expect( + client.request(postEmotionsQuery, postEmotionsQueryVariables), + ).resolves.toEqual({ + Post: [{ emotions: expect.arrayContaining(expected) }], + }) + }) + }) + }) }) }) diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index e841b0beb..6dd04f64b 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -130,6 +130,23 @@ export default function Factory(options = {}) { this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver }) return this }, + async emote({ from, to, data }) { + const mutation = ` + mutation { + AddPostEmotions( + from: { id: "${from}" }, + to: { id: "${to}" }, + data: { emotion: ${data} } + ) { + from { id } + to { id } + emotion + } + } + ` + this.lastResponse = await this.graphQLClient.request(mutation) + return this + }, } result.authenticateAs.bind(result) result.create.bind(result) diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index 8f693cfd3..1d63e1958 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -487,6 +487,86 @@ import Factory from './factories' from: 'p15', to: 'Demokratie', }), + f.emote({ + from: 'u1', + to: 'p15', + data: 'surprised', + }), + f.emote({ + from: 'u2', + to: 'p14', + data: 'cry', + }), + f.emote({ + from: 'u3', + to: 'p13', + data: 'angry', + }), + f.emote({ + from: 'u4', + to: 'p12', + data: 'funny', + }), + f.emote({ + from: 'u5', + to: 'p11', + data: 'surprised', + }), + f.emote({ + from: 'u6', + to: 'p10', + data: 'cry', + }), + f.emote({ + from: 'u5', + to: 'p9', + data: 'happy', + }), + f.emote({ + from: 'u4', + to: 'p8', + data: 'angry', + }), + f.emote({ + from: 'u3', + to: 'p7', + data: 'funny', + }), + f.emote({ + from: 'u2', + to: 'p6', + data: 'surprised', + }), + f.emote({ + from: 'u1', + to: 'p5', + data: 'cry', + }), + f.emote({ + from: 'u2', + to: 'p4', + data: 'happy', + }), + f.emote({ + from: 'u3', + to: 'p3', + data: 'angry', + }), + f.emote({ + from: 'u4', + to: 'p2', + data: 'funny', + }), + f.emote({ + from: 'u5', + to: 'p1', + data: 'surprised', + }), + f.emote({ + from: 'u6', + to: 'p0', + data: 'cry', + }), ]) await Promise.all([ From 54234129580118ef60c35b2732081bf4bba9330b Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Wed, 31 Jul 2019 14:58:05 +0200 Subject: [PATCH 04/17] Set up UI --- .../src/middleware/permissionsMiddleware.js | 1 + backend/src/schema/resolvers/posts.js | 18 +++ backend/src/schema/types/type/EMOTED.gql | 2 +- backend/src/schema/types/type/Post.gql | 4 + .../EmotionsButtons/EmotionsButtons.vue | 126 ++++++++++++++++++ webapp/graphql/PostQuery.js | 1 + webapp/pages/post/_id/_slug/index.vue | 26 ++-- webapp/static/img/svg/emoji/angry.svg | 1 + webapp/static/img/svg/emoji/angry_color.svg | 1 + webapp/static/img/svg/emoji/cry.svg | 1 + webapp/static/img/svg/emoji/cry_color.svg | 1 + webapp/static/img/svg/emoji/funny.svg | 1 + webapp/static/img/svg/emoji/funny_color.svg | 1 + webapp/static/img/svg/emoji/happy.svg | 1 + webapp/static/img/svg/emoji/happy_color.svg | 1 + webapp/static/img/svg/emoji/surprised.svg | 1 + .../static/img/svg/emoji/surprised_color.svg | 1 + 17 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 webapp/components/EmotionsButtons/EmotionsButtons.vue create mode 100644 webapp/static/img/svg/emoji/angry.svg create mode 100644 webapp/static/img/svg/emoji/angry_color.svg create mode 100644 webapp/static/img/svg/emoji/cry.svg create mode 100644 webapp/static/img/svg/emoji/cry_color.svg create mode 100644 webapp/static/img/svg/emoji/funny.svg create mode 100644 webapp/static/img/svg/emoji/funny_color.svg create mode 100644 webapp/static/img/svg/emoji/happy.svg create mode 100644 webapp/static/img/svg/emoji/happy_color.svg create mode 100644 webapp/static/img/svg/emoji/surprised.svg create mode 100644 webapp/static/img/svg/emoji/surprised_color.svg diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 8a7aced85..29a171f29 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -154,6 +154,7 @@ const permissions = shield( User: or(noEmailFilter, isAdmin), isLoggedIn: allow, Badge: allow, + postsEmotionsCountByEmotion: allow, }, Mutation: { '*': deny, diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 754d9a6ed..54351ffbb 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -107,4 +107,22 @@ export default { return !emoted }, }, + Query: { + postsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => { + const session = context.driver.session() + const { id, data } = params + const transactionRes = await session.run( + `MATCH (post:Post {id: $id})<-[emoted:EMOTED {emotion: $data.emotion}]-() + RETURN COUNT(DISTINCT emoted) as emotionsCount + `, + { id, data }, + ) + session.close() + + const [emotionsCount] = transactionRes.records.map(record => { + return record.get('emotionsCount').low + }) + return emotionsCount + }, + }, } diff --git a/backend/src/schema/types/type/EMOTED.gql b/backend/src/schema/types/type/EMOTED.gql index 80d655b5c..d40eed0c4 100644 --- a/backend/src/schema/types/type/EMOTED.gql +++ b/backend/src/schema/types/type/EMOTED.gql @@ -7,4 +7,4 @@ type EMOTED @relation(name: "EMOTED") { #updatedAt: DateTime createdAt: String updatedAt: String -} \ No newline at end of file +} diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 422ab2867..8aa2aee92 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -93,3 +93,7 @@ type Mutation { ): Post RemovePostEmotions(from: _UserInput!, to: _PostInput!, data: _EMOTEDInput!): Boolean! } + +type Query { + postsEmotionsCountByEmotion(id: ID!, data: _EMOTEDInput!): Int! +} diff --git a/webapp/components/EmotionsButtons/EmotionsButtons.vue b/webapp/components/EmotionsButtons/EmotionsButtons.vue new file mode 100644 index 000000000..0bc217f64 --- /dev/null +++ b/webapp/components/EmotionsButtons/EmotionsButtons.vue @@ -0,0 +1,126 @@ + + + diff --git a/webapp/graphql/PostQuery.js b/webapp/graphql/PostQuery.js index d2bba23ef..34a874d35 100644 --- a/webapp/graphql/PostQuery.js +++ b/webapp/graphql/PostQuery.js @@ -71,6 +71,7 @@ export default i18n => { } shoutedCount shoutedByCurrentUser + emotionsCount } } ` diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue index d5e79f4b8..a4b7b37e3 100644 --- a/webapp/pages/post/_id/_slug/index.vue +++ b/webapp/pages/post/_id/_slug/index.vue @@ -43,14 +43,22 @@ - - + + + + + + + + + + @@ -71,6 +79,7 @@ import HcCommentForm from '~/components/comments/CommentForm' import HcCommentList from '~/components/comments/CommentList' import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers' import PostQuery from '~/graphql/PostQuery.js' +import HcEmotionsButtons from '~/components/EmotionsButtons/EmotionsButtons' export default { name: 'PostSlug', @@ -86,6 +95,7 @@ export default { ContentMenu, HcCommentForm, HcCommentList, + HcEmotionsButtons, }, head() { return { diff --git a/webapp/static/img/svg/emoji/angry.svg b/webapp/static/img/svg/emoji/angry.svg new file mode 100644 index 000000000..74abe161f --- /dev/null +++ b/webapp/static/img/svg/emoji/angry.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/static/img/svg/emoji/angry_color.svg b/webapp/static/img/svg/emoji/angry_color.svg new file mode 100644 index 000000000..f6b4bd9a8 --- /dev/null +++ b/webapp/static/img/svg/emoji/angry_color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/static/img/svg/emoji/cry.svg b/webapp/static/img/svg/emoji/cry.svg new file mode 100644 index 000000000..d375fd2fd --- /dev/null +++ b/webapp/static/img/svg/emoji/cry.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/static/img/svg/emoji/cry_color.svg b/webapp/static/img/svg/emoji/cry_color.svg new file mode 100644 index 000000000..6a32bc2c5 --- /dev/null +++ b/webapp/static/img/svg/emoji/cry_color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/static/img/svg/emoji/funny.svg b/webapp/static/img/svg/emoji/funny.svg new file mode 100644 index 000000000..d23792d8c --- /dev/null +++ b/webapp/static/img/svg/emoji/funny.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/static/img/svg/emoji/funny_color.svg b/webapp/static/img/svg/emoji/funny_color.svg new file mode 100644 index 000000000..3ac2087e8 --- /dev/null +++ b/webapp/static/img/svg/emoji/funny_color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/static/img/svg/emoji/happy.svg b/webapp/static/img/svg/emoji/happy.svg new file mode 100644 index 000000000..d0d8a4e80 --- /dev/null +++ b/webapp/static/img/svg/emoji/happy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/static/img/svg/emoji/happy_color.svg b/webapp/static/img/svg/emoji/happy_color.svg new file mode 100644 index 000000000..d541639e3 --- /dev/null +++ b/webapp/static/img/svg/emoji/happy_color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/static/img/svg/emoji/surprised.svg b/webapp/static/img/svg/emoji/surprised.svg new file mode 100644 index 000000000..8a02a5a50 --- /dev/null +++ b/webapp/static/img/svg/emoji/surprised.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/static/img/svg/emoji/surprised_color.svg b/webapp/static/img/svg/emoji/surprised_color.svg new file mode 100644 index 000000000..398c34f35 --- /dev/null +++ b/webapp/static/img/svg/emoji/surprised_color.svg @@ -0,0 +1 @@ + \ No newline at end of file From fb9a632d558d9e6eb8a4d1b3f499ce91a093645e Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Thu, 1 Aug 2019 15:11:41 +0200 Subject: [PATCH 05/17] Query a currentUsers emotions for a post, translations --- .../src/middleware/permissionsMiddleware.js | 1 + backend/src/schema/resolvers/posts.js | 23 ++++- backend/src/schema/types/type/Post.gql | 3 +- .../EmotionsButtons/EmotionsButtons.vue | 83 ++++++++++++------- webapp/locales/de.json | 8 ++ webapp/locales/en.json | 8 ++ 6 files changed, 93 insertions(+), 33 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 9e8f5dacb..dc685e20c 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -166,6 +166,7 @@ const permissions = shield( isLoggedIn: allow, Badge: allow, postsEmotionsCountByEmotion: allow, + postsEmotionsCountByCurrentUser: allow, }, Mutation: { '*': deny, diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 54351ffbb..cca2ba463 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -110,12 +110,12 @@ export default { Query: { postsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => { const session = context.driver.session() - const { id, data } = params + const { postId, data } = params const transactionRes = await session.run( - `MATCH (post:Post {id: $id})<-[emoted:EMOTED {emotion: $data.emotion}]-() + `MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-() RETURN COUNT(DISTINCT emoted) as emotionsCount `, - { id, data }, + { postId, data }, ) session.close() @@ -124,5 +124,22 @@ export default { }) return emotionsCount }, + postsEmotionsCountByCurrentUser: async (object, params, context, resolveInfo) => { + const session = context.driver.session() + const { postId } = params + const transactionRes = await session.run( + `MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId}) + RETURN emoted.emotion as emotion`, + { userId: context.user.id, postId }, + ) + + session.close() + let emotionsArray = [] + transactionRes.records.map(record => { + emotionsArray.push(record.get('emotion')) + }) + + return emotionsArray + }, }, } diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 8aa2aee92..ba2b6ceef 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -95,5 +95,6 @@ type Mutation { } type Query { - postsEmotionsCountByEmotion(id: ID!, data: _EMOTEDInput!): Int! + postsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int! + postsEmotionsCountByCurrentUser(postId: ID!): [String] } diff --git a/webapp/components/EmotionsButtons/EmotionsButtons.vue b/webapp/components/EmotionsButtons/EmotionsButtons.vue index 0bc217f64..b115e4bf3 100644 --- a/webapp/components/EmotionsButtons/EmotionsButtons.vue +++ b/webapp/components/EmotionsButtons/EmotionsButtons.vue @@ -1,7 +1,7 @@