From f644507e4fc496a4e4ba0f6d2948d56d8a483bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 15:47:03 +0100 Subject: [PATCH 01/12] Intermediate commit --- src/middleware/softDeleteMiddleware.spec.js | 11 +++++++---- src/seed/factories/posts.js | 2 -- src/seed/seed-db.js | 7 ++++++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/middleware/softDeleteMiddleware.spec.js b/src/middleware/softDeleteMiddleware.spec.js index 925a03ccc..c37e7d426 100644 --- a/src/middleware/softDeleteMiddleware.spec.js +++ b/src/middleware/softDeleteMiddleware.spec.js @@ -10,14 +10,17 @@ let action beforeEach(async () => { await Promise.all([ factory.create('User', { role: 'user', email: 'user@example.org', password: '1234' }), - factory.create('User', { role: 'moderator', email: 'moderator@example.org', password: '1234' }) + factory.create('User', { id: 'm1', role: 'moderator', email: 'moderator@example.org', password: '1234' }) ]) await factory.authenticateAs({ email: 'user@example.org', password: '1234' }) await Promise.all([ - factory.create('Post', { title: 'Deleted post', deleted: true, disabled: false }), - factory.create('Post', { title: 'Disabled post', deleted: false, disabled: true }), - factory.create('Post', { title: 'Publicly visible post', deleted: false, disabled: false }) + factory.create('Post', { title: 'Deleted post', deleted: true }), + factory.create('Post', { id: 'p2', title: 'Disabled post', deleted: false }), + factory.create('Post', { title: 'Publicly visible post', deleted: false }) ]) + const moderatorFactory = Factory() + await moderatorFactory.authenticateAs({ email: 'moderator@example.org', password: '1234'}) + await moderatorFactory.relate('Post', 'DisabledBy', { from: 'm1', to: 'p2'}) }) afterEach(async () => { diff --git a/src/seed/factories/posts.js b/src/seed/factories/posts.js index d96cf4f73..e2bc2ab66 100644 --- a/src/seed/factories/posts.js +++ b/src/seed/factories/posts.js @@ -14,7 +14,6 @@ export default function (params) { ].join('. '), image = faker.image.image(), visibility = 'public', - disabled = false, deleted = false } = params @@ -26,7 +25,6 @@ export default function (params) { content: "${content}", image: "${image}", visibility: ${visibility}, - disabled: ${disabled}, deleted: ${deleted} ) { title, content } } diff --git a/src/seed/seed-db.js b/src/seed/seed-db.js index ed46a5716..c20d524ef 100644 --- a/src/seed/seed-db.js +++ b/src/seed/seed-db.js @@ -87,7 +87,7 @@ import Factory from './factories' asTrick.create('Post', { id: 'p4' }), asTrack.create('Post', { id: 'p5' }), asAdmin.create('Post', { id: 'p6' }), - asModerator.create('Post', { id: 'p7', disabled: true }), + asModerator.create('Post', { id: 'p7' }), asUser.create('Post', { id: 'p8' }), asTick.create('Post', { id: 'p9' }), asTrick.create('Post', { id: 'p10' }), @@ -98,6 +98,11 @@ import Factory from './factories' asTick.create('Post', { id: 'p15' }) ]) + await asModerator.relate('Post', 'DisabledBy', { + from: 'u2', + to: 'p15' + }) + await Promise.all([ f.relate('Post', 'Categories', { from: 'p0', to: 'cat16' }), f.relate('Post', 'Categories', { from: 'p1', to: 'cat1' }), From 420ea8a4d60747b77e0c0da8b0c70f1dfb28a9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 16:15:31 +0100 Subject: [PATCH 02/12] Scaffold some tests for disabledBy relation --- src/resolvers/posts.spec.js | 46 +++++++++++++++++++++++++++++++++++++ src/schema.graphql | 1 + 2 files changed, 47 insertions(+) diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index 5603683eb..1601e3348 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -200,3 +200,49 @@ describe('DeletePost', () => { }) }) }) + +describe('AddPostDisabledBy', () => { + const mutation = ` + mutation { + AddPostDisabledBy(from: { id: "u8" }, to: { id: "p9" }) { + from { + id + } + to { + id + } + } + } + ` + it.todo('throws authorization error') + + describe('authenticated', () => { + it.todo('throws authorization error') + + describe('as moderator', () => { + it.todo('throws authorization error') + + describe('current user matches provided user', () => { + it.todo('sets current user') + it.todo('updates .disabled on post') + }) + }) + }) +}) + +describe('RemovePostDisabledBy', () => { + it.todo('throws authorization error') + + describe('authenticated', () => { + it.todo('throws authorization error') + + describe('as moderator', () => { + it.todo('throws authorization error') + + describe('current user matches provided user', () => { + it.todo('sets current user') + it.todo('updates .disabled on post') + }) + }) + }) +}) diff --git a/src/schema.graphql b/src/schema.graphql index 1f9bcb477..4c2a58505 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -133,6 +133,7 @@ type Post { visibility: VisibilityEnum deleted: Boolean disabled: Boolean + disabledBy: User! @relation(name: "DISABLED", direction: "IN") createdAt: String updatedAt: String From 85d9d7043eef6080673f52db806892e11e2881e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 16:19:51 +0100 Subject: [PATCH 03/12] Setup isModerator permission for disable relation --- src/middleware/permissionsMiddleware.js | 5 ++- src/resolvers/posts.spec.js | 51 +++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index c40803e00..ec2261c5a 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -55,7 +55,10 @@ const permissions = shield({ report: isAuthenticated, CreateBadge: isAdmin, UpdateBadge: isAdmin, - DeleteBadge: isAdmin + DeleteBadge: isAdmin, + + AddPostDisabledBy: isModerator, + RemovePostDisabledBy: isModerator, // addFruitToBasket: isAuthenticated // CreateUser: allow, }, diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index 1601e3348..cbe836b21 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -214,10 +214,25 @@ describe('AddPostDisabledBy', () => { } } ` - it.todo('throws authorization error') + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) describe('authenticated', () => { - it.todo('throws authorization error') + let headers + beforeEach(async () => { + await factory.create('User', { + email: 'someUser@example.org', + password: '1234' + }) + headers = await login({ email: 'someUser@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorization error', async () => { + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) describe('as moderator', () => { it.todo('throws authorization error') @@ -231,10 +246,38 @@ describe('AddPostDisabledBy', () => { }) describe('RemovePostDisabledBy', () => { - it.todo('throws authorization error') + const mutation = ` + mutation { + AddPostDisabledBy(from: { id: "u8" }, to: { id: "p9" }) { + from { + id + } + to { + id + } + } + } + ` + + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) describe('authenticated', () => { - it.todo('throws authorization error') + let headers + beforeEach(async () => { + await factory.create('User', { + email: 'someUser@example.org', + password: '1234' + }) + headers = await login({ email: 'someUser@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorization error', async () => { + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) describe('as moderator', () => { it.todo('throws authorization error') From f2e7e515a4c1874bcb7df289b2d24f1c797a92f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 16:46:39 +0100 Subject: [PATCH 04/12] Check from: User! matches the authenticated user --- src/middleware/permissionsMiddleware.js | 11 +- src/resolvers/posts.spec.js | 166 ++++++++++++++---------- 2 files changed, 109 insertions(+), 68 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index ec2261c5a..8cf35c2a3 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -1,4 +1,4 @@ -import { rule, shield, allow, or } from 'graphql-shield' +import { rule, shield, allow, and, or } from 'graphql-shield' /* * TODO: implement @@ -41,6 +41,11 @@ const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver return authorId === user.id }) +const fromUserMatchesCurrentUser = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) => { + const { from: { id: fromId } } = args + return user.id === fromId +}) + // Permissions const permissions = shield({ Query: { @@ -57,8 +62,8 @@ const permissions = shield({ UpdateBadge: isAdmin, DeleteBadge: isAdmin, - AddPostDisabledBy: isModerator, - RemovePostDisabledBy: isModerator, + AddPostDisabledBy: and(isModerator, fromUserMatchesCurrentUser), + RemovePostDisabledBy: and(isModerator, fromUserMatchesCurrentUser), // addFruitToBasket: isAuthenticated // CreateUser: allow, }, diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index cbe836b21..515216f34 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -201,52 +201,22 @@ describe('DeletePost', () => { }) }) -describe('AddPostDisabledBy', () => { - const mutation = ` - mutation { - AddPostDisabledBy(from: { id: "u8" }, to: { id: "p9" }) { - from { - id - } - to { - id - } - } + + + +describe('disabledBy relation', () => { + const setup = async (params = {}) => { + let headers = {} + const { email, password } = params + if (email && password) { + await factory.create('User', params) + headers = await login({email, password}) } - ` - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) + client = new GraphQLClient(host, { headers }) + } - describe('authenticated', () => { - let headers - beforeEach(async () => { - await factory.create('User', { - email: 'someUser@example.org', - password: '1234' - }) - headers = await login({ email: 'someUser@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - - it('throws authorization error', async () => { - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('as moderator', () => { - it.todo('throws authorization error') - - describe('current user matches provided user', () => { - it.todo('sets current user') - it.todo('updates .disabled on post') - }) - }) - }) -}) - -describe('RemovePostDisabledBy', () => { - const mutation = ` + describe('AddPostDisabledBy', () => { + const mutation = ` mutation { AddPostDisabledBy(from: { id: "u8" }, to: { id: "p9" }) { from { @@ -259,32 +229,98 @@ describe('RemovePostDisabledBy', () => { } ` - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('authenticated', () => { - let headers - beforeEach(async () => { - await factory.create('User', { - email: 'someUser@example.org', - password: '1234' - }) - headers = await login({ email: 'someUser@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - it('throws authorization error', async () => { + await setup() await expect(client.request(mutation)).rejects.toThrow('Not Authorised') }) - describe('as moderator', () => { - it.todo('throws authorization error') + describe('authenticated', () => { + it('throws authorization error', async () => { + await setup({ + email: 'someUser@example.org', + password: '1234' + }) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) - describe('current user matches provided user', () => { - it.todo('sets current user') - it.todo('updates .disabled on post') + describe('as moderator', () => { + it('throws authorization error', async () => { + await setup({ + email: 'attributedUserMismatch@example.org', + password: '1234', + role: 'moderator' + }) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('current user matches provided user', () => { + beforeEach(async () => { + await setup({ + id: 'u7', + email: 'moderator@example.org', + password: '1234', + role: 'moderator' + }) + }) + + it.todo('sets current user') + it.todo('updates .disabled on post') + }) + }) + }) + }) + + describe('RemovePostDisabledBy', () => { + const mutation = ` + mutation { + AddPostDisabledBy(from: { id: "u8" }, to: { id: "p9" }) { + from { + id + } + to { + id + } + } + } + ` + + it('throws authorization error', async () => { + await setup() + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('authenticated', () => { + it('throws authorization error', async () => { + await setup({ + email: 'someUser@example.org', + password: '1234' + }) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('as moderator', () => { + it('throws authorization error', async () => { + await setup({ + role: 'moderator', + email: 'someUser@example.org', + password: '1234' + }) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('current user matches provided user', () => { + beforeEach(async () => { + await setup({ + id: 'u7', + role: 'moderator', + email: 'someUser@example.org', + password: '1234' + }) + }) + + it.todo('sets current user') + it.todo('updates .disabled on post') + }) }) }) }) From 99cebc8d64b46766a7ff618afc1f30669f59adc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 17:45:10 +0100 Subject: [PATCH 05/12] Implementation ready except disabled attr. --- src/middleware/permissionsMiddleware.js | 1 + src/resolvers/posts.spec.js | 64 ++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 8cf35c2a3..1c6e8310b 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -42,6 +42,7 @@ const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) const fromUserMatchesCurrentUser = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) => { + if (!user) return false const { from: { id: fromId } } = args return user.id === fromId }) diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index 515216f34..0715f221d 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -206,10 +206,16 @@ describe('DeletePost', () => { describe('disabledBy relation', () => { const setup = async (params = {}) => { + await factory.create('User', {email: 'author@example.org', password: '1234'}) + await factory.authenticateAs({email: 'author@example.org', password: '1234'}) + await factory.create('Post', { + id: 'p9' // that's the ID we will look for + }) + let headers = {} const { email, password } = params if (email && password) { - await factory.create('User', params) + const user = await factory.create('User', params) headers = await login({email, password}) } client = new GraphQLClient(host, { headers }) @@ -218,7 +224,7 @@ describe('disabledBy relation', () => { describe('AddPostDisabledBy', () => { const mutation = ` mutation { - AddPostDisabledBy(from: { id: "u8" }, to: { id: "p9" }) { + AddPostDisabledBy(from: { id: "u7" }, to: { id: "p9" }) { from { id } @@ -263,17 +269,47 @@ describe('disabledBy relation', () => { }) }) - it.todo('sets current user') - it.todo('updates .disabled on post') + it('returns created relation', async () => { + const expected = { + AddPostDisabledBy: { + from: { id: 'u7' }, + to: { id: 'p9' } + } + } + await expect(client.request(mutation)).resolves.toEqual(expected) + }) + + it('sets current user', async () => { + await client.request(mutation) + const query = `{ Post { id, disabledBy { id } } }` + const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } + await expect(client.request(query)).resolves.toEqual(expected) + }) + + it('updates .disabled on post', async () => { + await client.request(mutation) + const query = `{ Post { id disabled } }` + const expected = { Post: [ { id: 'p9', disabled: true } ] } + await expect(client.request(query)).resolves.toEqual(expected) + }) }) }) }) }) describe('RemovePostDisabledBy', () => { + beforeEach(async () => { + await factory.create('User', {email: 'anotherModerator@example.org', password: '1234', role: 'moderator'}) + await factory.authenticateAs({email: 'anotherModerator@example.org', password: '1234'}) + await factory.relate('Post', 'DisabledBy', { + from: 'u7', + to: 'p9' + }) + }) + const mutation = ` mutation { - AddPostDisabledBy(from: { id: "u8" }, to: { id: "p9" }) { + RemovePostDisabledBy(from: { id: "u7" }, to: { id: "p9" }) { from { id } @@ -318,8 +354,22 @@ describe('disabledBy relation', () => { }) }) - it.todo('sets current user') - it.todo('updates .disabled on post') + it('returns deleted relation', async () => { + const expected = { + RemovePostDisabledBy: { + from: { id: 'u7' }, + to: { id: 'p9' } + } + } + await expect(client.request(mutation)).resolves.toEqual(expected) + }) + + it('updates .disabled on post', async () => { + await client.request(mutation) + const query = `{ Post { id disabled } }` + const expected = { Post: [ { id: 'p9', disabled: false } ] } + await expect(client.request(query)).resolves.toEqual(expected) + }) }) }) }) From 592f25b978d4a6e38e4ec89504f4d59b73e6917c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 18:14:25 +0100 Subject: [PATCH 06/12] Implement update of .disabled field --- src/resolvers/posts.js | 20 ++++ src/resolvers/posts.spec.js | 179 ++++++++++++++++++++---------------- 2 files changed, 120 insertions(+), 79 deletions(-) diff --git a/src/resolvers/posts.js b/src/resolvers/posts.js index abf91e047..33934699b 100644 --- a/src/resolvers/posts.js +++ b/src/resolvers/posts.js @@ -2,6 +2,26 @@ import { neo4jgraphql } from 'neo4j-graphql-js' export default { Mutation: { + AddPostDisabledBy: async (object, params, context, resolveInfo) => { + const { to: { id: postId } } = params + const session = context.driver.session() + await session.run(` + MATCH (p:Post {id: $postId}) + SET p.disabled = true`, { postId }) + session.close() + return neo4jgraphql(object, params, context, resolveInfo, false) + }, + + RemovePostDisabledBy: async (object, params, context, resolveInfo) => { + const { to: { id: postId } } = params + const session = context.driver.session() + await session.run(` + MATCH (p:Post {id: $postId}) + SET p.disabled = false`, { postId }) + session.close() + return neo4jgraphql(object, params, context, resolveInfo, false) + }, + CreatePost: async (object, params, context, resolveInfo) => { const result = await neo4jgraphql(object, params, context, resolveInfo, false) diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index 0715f221d..427a5d925 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -204,7 +204,7 @@ describe('DeletePost', () => { -describe('disabledBy relation', () => { +describe('AddPostDisabledBy', () => { const setup = async (params = {}) => { await factory.create('User', {email: 'author@example.org', password: '1234'}) await factory.authenticateAs({email: 'author@example.org', password: '1234'}) @@ -221,8 +221,7 @@ describe('disabledBy relation', () => { client = new GraphQLClient(host, { headers }) } - describe('AddPostDisabledBy', () => { - const mutation = ` + const mutation = ` mutation { AddPostDisabledBy(from: { id: "u7" }, to: { id: "p9" }) { from { @@ -235,79 +234,96 @@ describe('disabledBy relation', () => { } ` + it('throws authorization error', async () => { + await setup() + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('authenticated', () => { it('throws authorization error', async () => { - await setup() + await setup({ + email: 'someUser@example.org', + password: '1234' + }) await expect(client.request(mutation)).rejects.toThrow('Not Authorised') }) - describe('authenticated', () => { + describe('as moderator', () => { it('throws authorization error', async () => { await setup({ - email: 'someUser@example.org', - password: '1234' + email: 'attributedUserMismatch@example.org', + password: '1234', + role: 'moderator' }) await expect(client.request(mutation)).rejects.toThrow('Not Authorised') }) - describe('as moderator', () => { - it('throws authorization error', async () => { + describe('current user matches provided user', () => { + beforeEach(async () => { await setup({ - email: 'attributedUserMismatch@example.org', + id: 'u7', + email: 'moderator@example.org', password: '1234', role: 'moderator' }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') }) - describe('current user matches provided user', () => { - beforeEach(async () => { - await setup({ - id: 'u7', - email: 'moderator@example.org', - password: '1234', - role: 'moderator' - }) - }) - - it('returns created relation', async () => { - const expected = { - AddPostDisabledBy: { - from: { id: 'u7' }, - to: { id: 'p9' } - } + it('returns created relation', async () => { + const expected = { + AddPostDisabledBy: { + from: { id: 'u7' }, + to: { id: 'p9' } } - await expect(client.request(mutation)).resolves.toEqual(expected) - }) + } + await expect(client.request(mutation)).resolves.toEqual(expected) + }) - it('sets current user', async () => { - await client.request(mutation) - const query = `{ Post { id, disabledBy { id } } }` - const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } - await expect(client.request(query)).resolves.toEqual(expected) - }) + it('sets current user', async () => { + await client.request(mutation) + const query = `{ Post(disabled: true) { id, disabledBy { id } } }` + const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } + await expect(client.request(query)).resolves.toEqual(expected) + }) - it('updates .disabled on post', async () => { - await client.request(mutation) - const query = `{ Post { id disabled } }` - const expected = { Post: [ { id: 'p9', disabled: true } ] } - await expect(client.request(query)).resolves.toEqual(expected) - }) + it('updates .disabled on post', async () => { + const before = { Post: [ { id: 'p9', disabled: false } ] } + const expected = { Post: [ { id: 'p9', disabled: true } ] } + + await expect(client.request( + `{ Post { id disabled } }` + )).resolves.toEqual(before) + await client.request(mutation) // this updates .disabled + await expect(client.request( + `{ Post(disabled: true) { id disabled } }` + )).resolves.toEqual(expected) }) }) }) }) +}) - describe('RemovePostDisabledBy', () => { - beforeEach(async () => { - await factory.create('User', {email: 'anotherModerator@example.org', password: '1234', role: 'moderator'}) - await factory.authenticateAs({email: 'anotherModerator@example.org', password: '1234'}) - await factory.relate('Post', 'DisabledBy', { - from: 'u7', - to: 'p9' - }) +describe('RemovePostDisabledBy', () => { + const setup = async (params = {}) => { + await factory.create('User', {email: 'anotherModerator@example.org', password: '1234', id: 'u123', role: 'moderator'}) + await factory.authenticateAs({email: 'anotherModerator@example.org', password: '1234'}) + await factory.create('Post', { + id: 'p9' // that's the ID we will look for }) + await factory.relate('Post', 'DisabledBy', { + from: 'u123', + to: 'p9' + }) // that's we want to delete - const mutation = ` + let headers = {} + const { email, password } = params + if (email && password) { + const user = await factory.create('User', params) + headers = await login({email, password}) + } + client = new GraphQLClient(host, { headers }) + } + + const mutation = ` mutation { RemovePostDisabledBy(from: { id: "u7" }, to: { id: "p9" }) { from { @@ -320,56 +336,61 @@ describe('disabledBy relation', () => { } ` + it('throws authorization error', async () => { + await setup() + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('authenticated', () => { it('throws authorization error', async () => { - await setup() + await setup({ + email: 'someUser@example.org', + password: '1234' + }) await expect(client.request(mutation)).rejects.toThrow('Not Authorised') }) - describe('authenticated', () => { + describe('as moderator', () => { it('throws authorization error', async () => { await setup({ + role: 'moderator', email: 'someUser@example.org', password: '1234' }) await expect(client.request(mutation)).rejects.toThrow('Not Authorised') }) - describe('as moderator', () => { - it('throws authorization error', async () => { + describe('current user matches provided user', () => { + beforeEach(async () => { await setup({ + id: 'u7', role: 'moderator', email: 'someUser@example.org', password: '1234' }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') }) - describe('current user matches provided user', () => { - beforeEach(async () => { - await setup({ - id: 'u7', - role: 'moderator', - email: 'someUser@example.org', - password: '1234' - }) - }) - - it('returns deleted relation', async () => { - const expected = { - RemovePostDisabledBy: { - from: { id: 'u7' }, - to: { id: 'p9' } - } + it('returns deleted relation', async () => { + const expected = { + RemovePostDisabledBy: { + from: { id: 'u7' }, + to: { id: 'p9' } } - await expect(client.request(mutation)).resolves.toEqual(expected) - }) + } + await expect(client.request(mutation)).resolves.toEqual(expected) + }) - it('updates .disabled on post', async () => { - await client.request(mutation) - const query = `{ Post { id disabled } }` - const expected = { Post: [ { id: 'p9', disabled: false } ] } - await expect(client.request(query)).resolves.toEqual(expected) - }) + it('updates .disabled on post', async () => { + const before = { Post: [ { id: 'p9', disabled: true } ] } + const expected = { Post: [ { id: 'p9', disabled: false } ] } + + await expect(client.request( + `{ Post(disabled: true) { id disabled } }` + )).resolves.toEqual(before) + await client.request(mutation) // this updates .disabled + await expect(client.request( + `{ Post { id disabled } }` + )).resolves.toEqual(expected) }) }) }) From 2b7576521cf73fd247884e1f0338c4cb19004639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 18:15:05 +0100 Subject: [PATCH 07/12] Fix lint + return more attributes in post factory for convenience --- src/middleware/permissionsMiddleware.js | 2 +- src/middleware/softDeleteMiddleware.spec.js | 4 +-- src/resolvers/posts.spec.js | 29 +++++++++------------ src/seed/factories/users.js | 3 +++ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 1c6e8310b..44ed2ed34 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -64,7 +64,7 @@ const permissions = shield({ DeleteBadge: isAdmin, AddPostDisabledBy: and(isModerator, fromUserMatchesCurrentUser), - RemovePostDisabledBy: and(isModerator, fromUserMatchesCurrentUser), + RemovePostDisabledBy: and(isModerator, fromUserMatchesCurrentUser) // addFruitToBasket: isAuthenticated // CreateUser: allow, }, diff --git a/src/middleware/softDeleteMiddleware.spec.js b/src/middleware/softDeleteMiddleware.spec.js index c37e7d426..283e16eb0 100644 --- a/src/middleware/softDeleteMiddleware.spec.js +++ b/src/middleware/softDeleteMiddleware.spec.js @@ -19,8 +19,8 @@ beforeEach(async () => { factory.create('Post', { title: 'Publicly visible post', deleted: false }) ]) const moderatorFactory = Factory() - await moderatorFactory.authenticateAs({ email: 'moderator@example.org', password: '1234'}) - await moderatorFactory.relate('Post', 'DisabledBy', { from: 'm1', to: 'p2'}) + await moderatorFactory.authenticateAs({ email: 'moderator@example.org', password: '1234' }) + await moderatorFactory.relate('Post', 'DisabledBy', { from: 'm1', to: 'p2' }) }) afterEach(async () => { diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index 427a5d925..89fb1c8e4 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -201,13 +201,10 @@ describe('DeletePost', () => { }) }) - - - describe('AddPostDisabledBy', () => { const setup = async (params = {}) => { - await factory.create('User', {email: 'author@example.org', password: '1234'}) - await factory.authenticateAs({email: 'author@example.org', password: '1234'}) + await factory.create('User', { email: 'author@example.org', password: '1234' }) + await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) await factory.create('Post', { id: 'p9' // that's the ID we will look for }) @@ -215,8 +212,8 @@ describe('AddPostDisabledBy', () => { let headers = {} const { email, password } = params if (email && password) { - const user = await factory.create('User', params) - headers = await login({email, password}) + await factory.create('User', params) + headers = await login({ email, password }) } client = new GraphQLClient(host, { headers }) } @@ -280,7 +277,7 @@ describe('AddPostDisabledBy', () => { it('sets current user', async () => { await client.request(mutation) - const query = `{ Post(disabled: true) { id, disabledBy { id } } }` + const query = '{ Post(disabled: true) { id, disabledBy { id } } }' const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } await expect(client.request(query)).resolves.toEqual(expected) }) @@ -290,11 +287,11 @@ describe('AddPostDisabledBy', () => { const expected = { Post: [ { id: 'p9', disabled: true } ] } await expect(client.request( - `{ Post { id disabled } }` + '{ Post { id disabled } }' )).resolves.toEqual(before) await client.request(mutation) // this updates .disabled await expect(client.request( - `{ Post(disabled: true) { id disabled } }` + '{ Post(disabled: true) { id disabled } }' )).resolves.toEqual(expected) }) }) @@ -304,8 +301,8 @@ describe('AddPostDisabledBy', () => { describe('RemovePostDisabledBy', () => { const setup = async (params = {}) => { - await factory.create('User', {email: 'anotherModerator@example.org', password: '1234', id: 'u123', role: 'moderator'}) - await factory.authenticateAs({email: 'anotherModerator@example.org', password: '1234'}) + await factory.create('User', { email: 'anotherModerator@example.org', password: '1234', id: 'u123', role: 'moderator' }) + await factory.authenticateAs({ email: 'anotherModerator@example.org', password: '1234' }) await factory.create('Post', { id: 'p9' // that's the ID we will look for }) @@ -317,8 +314,8 @@ describe('RemovePostDisabledBy', () => { let headers = {} const { email, password } = params if (email && password) { - const user = await factory.create('User', params) - headers = await login({email, password}) + await factory.create('User', params) + headers = await login({ email, password }) } client = new GraphQLClient(host, { headers }) } @@ -385,11 +382,11 @@ describe('RemovePostDisabledBy', () => { const expected = { Post: [ { id: 'p9', disabled: false } ] } await expect(client.request( - `{ Post(disabled: true) { id disabled } }` + '{ Post(disabled: true) { id disabled } }' )).resolves.toEqual(before) await client.request(mutation) // this updates .disabled await expect(client.request( - `{ Post { id disabled } }` + '{ Post { id disabled } }' )).resolves.toEqual(expected) }) }) diff --git a/src/seed/factories/users.js b/src/seed/factories/users.js index 8e0ee693c..c27b2b1ce 100644 --- a/src/seed/factories/users.js +++ b/src/seed/factories/users.js @@ -25,10 +25,13 @@ export default function create (params) { disabled: ${disabled}, deleted: ${deleted} ) { + id name email avatar role + deleted + disabled } } ` From 0a73ddd46d1aceeb982c1d427839845615c3065a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 5 Mar 2019 23:59:54 +0100 Subject: [PATCH 08/12] Refactor: custom resolvers for moderation --- src/graphql-schema.js | 2 + src/middleware/permissionsMiddleware.js | 4 +- src/resolvers/moderation.js | 10 ++ src/resolvers/moderation.spec.js | 170 +++++++++++++++++++++ src/resolvers/posts.spec.js | 193 ------------------------ src/schema.graphql | 4 +- 6 files changed, 187 insertions(+), 196 deletions(-) create mode 100644 src/resolvers/moderation.js create mode 100644 src/resolvers/moderation.spec.js diff --git a/src/graphql-schema.js b/src/graphql-schema.js index 6d10183c9..c2d96ce16 100644 --- a/src/graphql-schema.js +++ b/src/graphql-schema.js @@ -4,6 +4,7 @@ import userManagement from './resolvers/user_management.js' import statistics from './resolvers/statistics.js' import reports from './resolvers/reports.js' import posts from './resolvers/posts.js' +import moderation from './resolvers/moderation.js' export const typeDefs = fs.readFileSync(process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql')) @@ -17,6 +18,7 @@ export const resolvers = { Mutation: { ...userManagement.Mutation, ...reports.Mutation, + ...moderation.Mutation, ...posts.Mutation } } diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 44ed2ed34..dff11b888 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -63,8 +63,8 @@ const permissions = shield({ UpdateBadge: isAdmin, DeleteBadge: isAdmin, - AddPostDisabledBy: and(isModerator, fromUserMatchesCurrentUser), - RemovePostDisabledBy: and(isModerator, fromUserMatchesCurrentUser) + enable: isModerator, + disable: isModerator, // addFruitToBasket: isAuthenticated // CreateUser: allow, }, diff --git a/src/resolvers/moderation.js b/src/resolvers/moderation.js new file mode 100644 index 000000000..15fd291e9 --- /dev/null +++ b/src/resolvers/moderation.js @@ -0,0 +1,10 @@ +export default { + Mutation: { + disable: async (object, params, {user, driver}) => { + return true + }, + enable: async (object, params, {user, driver}) => { + return true + } + } +} diff --git a/src/resolvers/moderation.spec.js b/src/resolvers/moderation.spec.js new file mode 100644 index 000000000..b6249a8b7 --- /dev/null +++ b/src/resolvers/moderation.spec.js @@ -0,0 +1,170 @@ +import Factory from '../seed/factories' +import { GraphQLClient } from 'graphql-request' +import { host, login } from '../jest/helpers' + +const factory = Factory() +let client + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('disable', () => { + const setup = async (params = {}) => { + await factory.create('User', { email: 'author@example.org', password: '1234' }) + await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) + await factory.create('Post', { + id: 'p9' // that's the ID we will look for + }) + + // create the user we use in the scenario below + let headers = {} + const { email, password } = params + if (email && password) { + await factory.create('User', params) + headers = await login({ email, password }) + } + client = new GraphQLClient(host, { headers }) + } + + const mutation = ` + mutation { + disable(resource: { + id: "p9" + type: contribution + }) + } + ` + + it('throws authorization error', async () => { + await setup() + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('authenticated', () => { + it('throws authorization error', async () => { + await setup({ + email: 'someUser@example.org', + password: '1234' + }) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('as moderator', () => { + beforeEach(async () => { + await setup({ + id: 'u7', + email: 'moderator@example.org', + password: '1234', + role: 'moderator' + }) + }) + + it('returns true', async () => { + const expected = { disable: true } + await expect(client.request(mutation)).resolves.toEqual(expected) + }) + + it('sets current user', async () => { + const before = { Post: [{ id: 'p9', disabledBy: null }] } + const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } + + await expect(client.request( + '{ Post { id, disabledBy { id } } }' + )).resolves.toEqual(before) + await client.request(mutation) + await expect(client.request( + '{ Post(disabled: true) { id, disabledBy { id } } }' + )).resolves.toEqual(expected) + }) + + it('updates .disabled on post', async () => { + const before = { Post: [ { id: 'p9', disabled: false } ] } + const expected = { Post: [ { id: 'p9', disabled: true } ] } + + await expect(client.request( + '{ Post { id disabled } }' + )).resolves.toEqual(before) + await client.request(mutation) // this updates .disabled + await expect(client.request( + '{ Post(disabled: true) { id disabled } }' + )).resolves.toEqual(expected) + }) + }) + }) +}) + +describe('enable', () => { + const setup = async (params = {}) => { + await factory.create('User', { email: 'anotherModerator@example.org', password: '1234', id: 'u123', role: 'moderator' }) + await factory.authenticateAs({ email: 'anotherModerator@example.org', password: '1234' }) + await factory.create('Post', { + id: 'p9' // that's the ID we will look for + }) + await factory.relate('Post', 'DisabledBy', { + from: 'u123', + to: 'p9' + }) // that's we want to delete + + let headers = {} + const { email, password } = params + if (email && password) { + await factory.create('User', params) + headers = await login({ email, password }) + } + client = new GraphQLClient(host, { headers }) + } + + + const mutation = ` + mutation { + enable(resource: { + id: "p9" + type: contribution + }) + } + ` + + it('throws authorization error', async () => { + await setup() + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('authenticated', () => { + it('throws authorization error', async () => { + await setup({ + email: 'someUser@example.org', + password: '1234' + }) + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + describe('as moderator', () => { + beforeEach(async () => { + await setup({ + role: 'moderator', + email: 'someUser@example.org', + password: '1234' + }) + }) + + it('returns true', async () => { + const expected = { enable: true } + await expect(client.request(mutation)).resolves.toEqual(expected) + }) + + it('updates .disabled on post', async () => { + const before = { Post: [ { id: 'p9', disabled: true } ] } + const expected = { Post: [ { id: 'p9', disabled: false } ] } + + await expect(client.request( + '{ Post(disabled: true) { id disabled } }' + )).resolves.toEqual(before) + await client.request(mutation) // this updates .disabled + await expect(client.request( + '{ Post { id disabled } }' + )).resolves.toEqual(expected) + }) + }) + }) +}) diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index 89fb1c8e4..5603683eb 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -200,196 +200,3 @@ describe('DeletePost', () => { }) }) }) - -describe('AddPostDisabledBy', () => { - const setup = async (params = {}) => { - await factory.create('User', { email: 'author@example.org', password: '1234' }) - await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) - await factory.create('Post', { - id: 'p9' // that's the ID we will look for - }) - - let headers = {} - const { email, password } = params - if (email && password) { - await factory.create('User', params) - headers = await login({ email, password }) - } - client = new GraphQLClient(host, { headers }) - } - - const mutation = ` - mutation { - AddPostDisabledBy(from: { id: "u7" }, to: { id: "p9" }) { - from { - id - } - to { - id - } - } - } - ` - - it('throws authorization error', async () => { - await setup() - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('authenticated', () => { - it('throws authorization error', async () => { - await setup({ - email: 'someUser@example.org', - password: '1234' - }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('as moderator', () => { - it('throws authorization error', async () => { - await setup({ - email: 'attributedUserMismatch@example.org', - password: '1234', - role: 'moderator' - }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('current user matches provided user', () => { - beforeEach(async () => { - await setup({ - id: 'u7', - email: 'moderator@example.org', - password: '1234', - role: 'moderator' - }) - }) - - it('returns created relation', async () => { - const expected = { - AddPostDisabledBy: { - from: { id: 'u7' }, - to: { id: 'p9' } - } - } - await expect(client.request(mutation)).resolves.toEqual(expected) - }) - - it('sets current user', async () => { - await client.request(mutation) - const query = '{ Post(disabled: true) { id, disabledBy { id } } }' - const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } - await expect(client.request(query)).resolves.toEqual(expected) - }) - - it('updates .disabled on post', async () => { - const before = { Post: [ { id: 'p9', disabled: false } ] } - const expected = { Post: [ { id: 'p9', disabled: true } ] } - - await expect(client.request( - '{ Post { id disabled } }' - )).resolves.toEqual(before) - await client.request(mutation) // this updates .disabled - await expect(client.request( - '{ Post(disabled: true) { id disabled } }' - )).resolves.toEqual(expected) - }) - }) - }) - }) -}) - -describe('RemovePostDisabledBy', () => { - const setup = async (params = {}) => { - await factory.create('User', { email: 'anotherModerator@example.org', password: '1234', id: 'u123', role: 'moderator' }) - await factory.authenticateAs({ email: 'anotherModerator@example.org', password: '1234' }) - await factory.create('Post', { - id: 'p9' // that's the ID we will look for - }) - await factory.relate('Post', 'DisabledBy', { - from: 'u123', - to: 'p9' - }) // that's we want to delete - - let headers = {} - const { email, password } = params - if (email && password) { - await factory.create('User', params) - headers = await login({ email, password }) - } - client = new GraphQLClient(host, { headers }) - } - - const mutation = ` - mutation { - RemovePostDisabledBy(from: { id: "u7" }, to: { id: "p9" }) { - from { - id - } - to { - id - } - } - } - ` - - it('throws authorization error', async () => { - await setup() - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('authenticated', () => { - it('throws authorization error', async () => { - await setup({ - email: 'someUser@example.org', - password: '1234' - }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('as moderator', () => { - it('throws authorization error', async () => { - await setup({ - role: 'moderator', - email: 'someUser@example.org', - password: '1234' - }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') - }) - - describe('current user matches provided user', () => { - beforeEach(async () => { - await setup({ - id: 'u7', - role: 'moderator', - email: 'someUser@example.org', - password: '1234' - }) - }) - - it('returns deleted relation', async () => { - const expected = { - RemovePostDisabledBy: { - from: { id: 'u7' }, - to: { id: 'p9' } - } - } - await expect(client.request(mutation)).resolves.toEqual(expected) - }) - - it('updates .disabled on post', async () => { - const before = { Post: [ { id: 'p9', disabled: true } ] } - const expected = { Post: [ { id: 'p9', disabled: false } ] } - - await expect(client.request( - '{ Post(disabled: true) { id disabled } }' - )).resolves.toEqual(before) - await client.request(mutation) // this updates .disabled - await expect(client.request( - '{ Post { id disabled } }' - )).resolves.toEqual(expected) - }) - }) - }) - }) -}) diff --git a/src/schema.graphql b/src/schema.graphql index 4c2a58505..ddafd880e 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -7,6 +7,8 @@ type Mutation { login(email: String!, password: String!): String! signup(email: String!, password: String!): Boolean! report(resource: Resource!, description: String): Report + disable(resource: Resource!): Boolean! + enable(resource: Resource!): Boolean! } type Statistics { @@ -133,7 +135,7 @@ type Post { visibility: VisibilityEnum deleted: Boolean disabled: Boolean - disabledBy: User! @relation(name: "DISABLED", direction: "IN") + disabledBy: User @relation(name: "DISABLED", direction: "IN") createdAt: String updatedAt: String From 5cff508bd6d9b553e0bf8d9fcc5e29143062583b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 6 Mar 2019 00:55:26 +0100 Subject: [PATCH 09/12] Disable/enable fullfills tests --- src/middleware/permissionsMiddleware.js | 10 ++----- src/middleware/softDeleteMiddleware.spec.js | 10 ++++++- src/resolvers/moderation.js | 28 ++++++++++++++++--- src/resolvers/moderation.spec.js | 31 ++++++++++++++++----- src/resolvers/posts.js | 20 ------------- src/seed/factories/index.js | 5 ++++ src/seed/seed-db.js | 13 ++++++--- 7 files changed, 73 insertions(+), 44 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index dff11b888..7fb6e75b8 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -1,4 +1,4 @@ -import { rule, shield, allow, and, or } from 'graphql-shield' +import { rule, shield, allow, or } from 'graphql-shield' /* * TODO: implement @@ -41,12 +41,6 @@ const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver return authorId === user.id }) -const fromUserMatchesCurrentUser = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) => { - if (!user) return false - const { from: { id: fromId } } = args - return user.id === fromId -}) - // Permissions const permissions = shield({ Query: { @@ -64,7 +58,7 @@ const permissions = shield({ DeleteBadge: isAdmin, enable: isModerator, - disable: isModerator, + disable: isModerator // addFruitToBasket: isAuthenticated // CreateUser: allow, }, diff --git a/src/middleware/softDeleteMiddleware.spec.js b/src/middleware/softDeleteMiddleware.spec.js index 283e16eb0..e9bc461f1 100644 --- a/src/middleware/softDeleteMiddleware.spec.js +++ b/src/middleware/softDeleteMiddleware.spec.js @@ -20,7 +20,15 @@ beforeEach(async () => { ]) const moderatorFactory = Factory() await moderatorFactory.authenticateAs({ email: 'moderator@example.org', password: '1234' }) - await moderatorFactory.relate('Post', 'DisabledBy', { from: 'm1', to: 'p2' }) + const disableMutation = ` + mutation { + disable(resource: { + id: "p2" + type: contribution + }) + } + ` + await moderatorFactory.mutate(disableMutation) }) afterEach(async () => { diff --git a/src/resolvers/moderation.js b/src/resolvers/moderation.js index 15fd291e9..8e171dbf6 100644 --- a/src/resolvers/moderation.js +++ b/src/resolvers/moderation.js @@ -1,10 +1,30 @@ export default { Mutation: { - disable: async (object, params, {user, driver}) => { - return true + disable: async (object, params, { user, driver }) => { + const { resource: { id: postId } } = params + const { id: userId } = user + const cypher = ` + MATCH (u:User {id: $userId}) + MATCH (p:Post {id: $postId}) + SET p.disabled = true + MERGE (p)<-[:DISABLED]-(u) + ` + const session = driver.session() + const res = await session.run(cypher, { postId, userId }) + session.close() + return Boolean(res) }, - enable: async (object, params, {user, driver}) => { - return true + enable: async (object, params, { user, driver }) => { + const { resource: { id: postId } } = params + const cypher = ` + MATCH (p:Post {id: $postId})<-[d:DISABLED]-() + SET p.disabled = false + DELETE d + ` + const session = driver.session() + const res = await session.run(cypher, { postId }) + session.close() + return Boolean(res) } } } diff --git a/src/resolvers/moderation.spec.js b/src/resolvers/moderation.spec.js index b6249a8b7..081d7c38d 100644 --- a/src/resolvers/moderation.spec.js +++ b/src/resolvers/moderation.spec.js @@ -65,8 +65,8 @@ describe('disable', () => { await expect(client.request(mutation)).resolves.toEqual(expected) }) - it('sets current user', async () => { - const before = { Post: [{ id: 'p9', disabledBy: null }] } + it('changes .disabledBy', async () => { + const before = { Post: [{ id: 'p9', disabledBy: null }] } const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } await expect(client.request( @@ -101,10 +101,15 @@ describe('enable', () => { await factory.create('Post', { id: 'p9' // that's the ID we will look for }) - await factory.relate('Post', 'DisabledBy', { - from: 'u123', - to: 'p9' - }) // that's we want to delete + const disableMutation = ` + mutation { + disable(resource: { + id: "p9" + type: contribution + }) + } + ` + await factory.mutate(disableMutation) // that's we want to delete let headers = {} const { email, password } = params @@ -115,7 +120,6 @@ describe('enable', () => { client = new GraphQLClient(host, { headers }) } - const mutation = ` mutation { enable(resource: { @@ -153,6 +157,19 @@ describe('enable', () => { await expect(client.request(mutation)).resolves.toEqual(expected) }) + it('changes .disabledBy', async () => { + const before = { Post: [{ id: 'p9', disabledBy: { id: 'u123' } }] } + const expected = { Post: [{ id: 'p9', disabledBy: null }] } + + await expect(client.request( + '{ Post(disabled: true) { id, disabledBy { id } } }' + )).resolves.toEqual(before) + await client.request(mutation) + await expect(client.request( + '{ Post { id, disabledBy { id } } }' + )).resolves.toEqual(expected) + }) + it('updates .disabled on post', async () => { const before = { Post: [ { id: 'p9', disabled: true } ] } const expected = { Post: [ { id: 'p9', disabled: false } ] } diff --git a/src/resolvers/posts.js b/src/resolvers/posts.js index 33934699b..abf91e047 100644 --- a/src/resolvers/posts.js +++ b/src/resolvers/posts.js @@ -2,26 +2,6 @@ import { neo4jgraphql } from 'neo4j-graphql-js' export default { Mutation: { - AddPostDisabledBy: async (object, params, context, resolveInfo) => { - const { to: { id: postId } } = params - const session = context.driver.session() - await session.run(` - MATCH (p:Post {id: $postId}) - SET p.disabled = true`, { postId }) - session.close() - return neo4jgraphql(object, params, context, resolveInfo, false) - }, - - RemovePostDisabledBy: async (object, params, context, resolveInfo) => { - const { to: { id: postId } } = params - const session = context.driver.session() - await session.run(` - MATCH (p:Post {id: $postId}) - SET p.disabled = false`, { postId }) - session.close() - return neo4jgraphql(object, params, context, resolveInfo, false) - }, - CreatePost: async (object, params, context, resolveInfo) => { const result = await neo4jgraphql(object, params, context, resolveInfo, false) diff --git a/src/seed/factories/index.js b/src/seed/factories/index.js index ed35d2c3b..68dd99200 100644 --- a/src/seed/factories/index.js +++ b/src/seed/factories/index.js @@ -86,6 +86,10 @@ export default function Factory (options = {}) { this.lastResponse = await this.graphQLClient.request(mutation) return this }, + async mutate (mutation, variables) { + this.lastResponse = await this.graphQLClient.request(mutation, variables) + return this + }, async cleanDatabase () { this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver }) return this @@ -94,6 +98,7 @@ export default function Factory (options = {}) { result.authenticateAs.bind(result) result.create.bind(result) result.relate.bind(result) + result.mutate.bind(result) result.cleanDatabase.bind(result) return result } diff --git a/src/seed/seed-db.js b/src/seed/seed-db.js index c20d524ef..310089ef7 100644 --- a/src/seed/seed-db.js +++ b/src/seed/seed-db.js @@ -98,10 +98,15 @@ import Factory from './factories' asTick.create('Post', { id: 'p15' }) ]) - await asModerator.relate('Post', 'DisabledBy', { - from: 'u2', - to: 'p15' - }) + const disableMutation = ` + mutation { + disable(resource: { + id: "p11" + type: contribution + }) + } + ` + await asModerator.mutate(disableMutation) await Promise.all([ f.relate('Post', 'Categories', { from: 'p0', to: 'cat16' }), From 1c34f10f967280ebc69f28dba48341dca80e02b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 6 Mar 2019 15:14:08 +0100 Subject: [PATCH 10/12] Test refactoring: Check comments + posts --- src/resolvers/moderation.spec.js | 418 ++++++++++++++++++++++--------- 1 file changed, 303 insertions(+), 115 deletions(-) diff --git a/src/resolvers/moderation.spec.js b/src/resolvers/moderation.spec.js index 081d7c38d..6107074ef 100644 --- a/src/resolvers/moderation.spec.js +++ b/src/resolvers/moderation.spec.js @@ -5,54 +5,76 @@ import { host, login } from '../jest/helpers' const factory = Factory() let client +const setupAuthenticateClient = (params) => { + const authenticateClient = async () => { + await factory.create('User', params) + const headers = await login(params) + client = new GraphQLClient(host, { headers }) + } + return authenticateClient +} + +let setup +const runSetup = async () => { + await setup.createResource() + await setup.authenticateClient() +} + +beforeEach(() => { + setup = { + createResource: () => { + }, + authenticateClient: () => { + client = new GraphQLClient(host) + } + } +}) + afterEach(async () => { await factory.cleanDatabase() }) describe('disable', () => { - const setup = async (params = {}) => { - await factory.create('User', { email: 'author@example.org', password: '1234' }) - await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) - await factory.create('Post', { - id: 'p9' // that's the ID we will look for - }) - - // create the user we use in the scenario below - let headers = {} - const { email, password } = params - if (email && password) { - await factory.create('User', params) - headers = await login({ email, password }) - } - client = new GraphQLClient(host, { headers }) - } - const mutation = ` - mutation { - disable(resource: { - id: "p9" - type: contribution - }) + mutation($id: ID!, $type: ResourceEnum!) { + disable(resource: { id: $id, type: $type }) } ` + let variables + + beforeEach(() => { + // our defaul set of variables + variables = { + id: 'blabla', + type: 'contribution' + } + }) + + const action = async () => { + return client.request(mutation, variables) + } it('throws authorization error', async () => { - await setup() - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + await runSetup() + await expect(action()).rejects.toThrow('Not Authorised') }) describe('authenticated', () => { - it('throws authorization error', async () => { - await setup({ - email: 'someUser@example.org', + beforeEach(() => { + setup.authenticateClient = setupAuthenticateClient({ + email: 'user@example.org', password: '1234' }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + it('throws authorization error', async () => { + await runSetup() + await expect(action()).rejects.toThrow('Not Authorised') }) describe('as moderator', () => { - beforeEach(async () => { - await setup({ + beforeEach(() => { + setup.authenticateClient = setupAuthenticateClient({ id: 'u7', email: 'moderator@example.org', password: '1234', @@ -60,127 +82,293 @@ describe('disable', () => { }) }) - it('returns true', async () => { - const expected = { disable: true } - await expect(client.request(mutation)).resolves.toEqual(expected) + describe('on a comment', () => { + beforeEach(async () => { + variables = { + id: 'c47', + type: 'comment' + } + + setup.createResource = async () => { + await factory.create('User', { id: 'u45', email: 'commenter@example.org', password: '1234' }) + await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' }) + await factory.create('Post', { + id: 'p3' + }) + await factory.create('Comment', { + id: 'c47' + }) + await Promise.all([ + factory.relate('Comment', 'Author', { from: 'u45', to: 'c47' }), + factory.relate('Comment', 'Post', { from: 'c47', to: 'p3' }) + ]) + } + }) + + it('returns true', async () => { + const expected = { disable: true } + await runSetup() + await expect(action()).resolves.toEqual(expected) + }) + + it('changes .disabledBy', async () => { + const before = { Comment: [{ id: 'c47', disabledBy: null }] } + const expected = { Comment: [{ id: 'c47', disabledBy: { id: 'u7' } }] } + + await runSetup() + await expect(client.request( + '{ Comment { id, disabledBy { id } } }' + )).resolves.toEqual(before) + await action() + await expect(client.request( + '{ Comment(disabled: true) { id, disabledBy { id } } }' + )).resolves.toEqual(expected) + }) + + it('updates .disabled on comment', async () => { + const before = { Comment: [ { id: 'c47', disabled: false } ] } + const expected = { Comment: [ { id: 'c47', disabled: true } ] } + + await runSetup() + await expect(client.request( + '{ Comment { id disabled } }' + )).resolves.toEqual(before) + await action() + await expect(client.request( + '{ Comment(disabled: true) { id disabled } }' + )).resolves.toEqual(expected) + }) }) - it('changes .disabledBy', async () => { - const before = { Post: [{ id: 'p9', disabledBy: null }] } - const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } + describe('on a post', () => { + beforeEach(async () => { + variables = { + id: 'p9', + type: 'contribution' + } - await expect(client.request( - '{ Post { id, disabledBy { id } } }' - )).resolves.toEqual(before) - await client.request(mutation) - await expect(client.request( - '{ Post(disabled: true) { id, disabledBy { id } } }' - )).resolves.toEqual(expected) - }) + setup.createResource = async () => { + await factory.create('User', { email: 'author@example.org', password: '1234' }) + await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) + await factory.create('Post', { + id: 'p9' // that's the ID we will look for + }) + } + }) - it('updates .disabled on post', async () => { - const before = { Post: [ { id: 'p9', disabled: false } ] } - const expected = { Post: [ { id: 'p9', disabled: true } ] } + it('returns true', async () => { + const expected = { disable: true } + await runSetup() + await expect(action()).resolves.toEqual(expected) + }) - await expect(client.request( - '{ Post { id disabled } }' - )).resolves.toEqual(before) - await client.request(mutation) // this updates .disabled - await expect(client.request( - '{ Post(disabled: true) { id disabled } }' - )).resolves.toEqual(expected) + it('changes .disabledBy', async () => { + const before = { Post: [{ id: 'p9', disabledBy: null }] } + const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } + + await runSetup() + await expect(client.request( + '{ Post { id, disabledBy { id } } }' + )).resolves.toEqual(before) + await action() + await expect(client.request( + '{ Post(disabled: true) { id, disabledBy { id } } }' + )).resolves.toEqual(expected) + }) + + it('updates .disabled on post', async () => { + const before = { Post: [ { id: 'p9', disabled: false } ] } + const expected = { Post: [ { id: 'p9', disabled: true } ] } + + await runSetup() + await expect(client.request( + '{ Post { id disabled } }' + )).resolves.toEqual(before) + await action() + await expect(client.request( + '{ Post(disabled: true) { id disabled } }' + )).resolves.toEqual(expected) + }) }) }) }) }) describe('enable', () => { - const setup = async (params = {}) => { - await factory.create('User', { email: 'anotherModerator@example.org', password: '1234', id: 'u123', role: 'moderator' }) - await factory.authenticateAs({ email: 'anotherModerator@example.org', password: '1234' }) - await factory.create('Post', { - id: 'p9' // that's the ID we will look for - }) - const disableMutation = ` - mutation { - disable(resource: { - id: "p9" - type: contribution - }) - } - ` - await factory.mutate(disableMutation) // that's we want to delete - - let headers = {} - const { email, password } = params - if (email && password) { - await factory.create('User', params) - headers = await login({ email, password }) - } - client = new GraphQLClient(host, { headers }) - } - const mutation = ` - mutation { - enable(resource: { - id: "p9" - type: contribution - }) + mutation($id: ID!, $type: ResourceEnum!) { + enable(resource: { id: $id, type: $type }) } ` + let variables + + const action = async () => { + return client.request(mutation, variables) + } + + beforeEach(() => { + // our defaul set of variables + variables = { + id: 'blabla', + type: 'contribution' + } + }) it('throws authorization error', async () => { - await setup() - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + await runSetup() + await expect(action()).rejects.toThrow('Not Authorised') }) describe('authenticated', () => { - it('throws authorization error', async () => { - await setup({ - email: 'someUser@example.org', + beforeEach(() => { + setup.authenticateClient = setupAuthenticateClient({ + email: 'user@example.org', password: '1234' }) - await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + + it('throws authorization error', async () => { + await runSetup() + await expect(action()).rejects.toThrow('Not Authorised') }) describe('as moderator', () => { beforeEach(async () => { - await setup({ + setup.authenticateClient = setupAuthenticateClient({ role: 'moderator', email: 'someUser@example.org', password: '1234' }) }) - it('returns true', async () => { - const expected = { enable: true } - await expect(client.request(mutation)).resolves.toEqual(expected) + describe('on a comment', () => { + beforeEach(async () => { + variables = { + id: 'c456', + type: 'comment' + } + + setup.createResource = async () => { + await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' }) + await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) + await factory.create('Post', { + id: 'p9' // that's the ID we will look for + }) + + await factory.create('Comment', { + id: 'c456' + }) + await Promise.all([ + factory.relate('Comment', 'Author', { from: 'u123', to: 'c456' }), + factory.relate('Comment', 'Post', { from: 'c456', to: 'p9' }) + ]) + + const disableMutation = ` + mutation { + disable(resource: { + id: "c456" + type: comment + }) + } + ` + await factory.mutate(disableMutation) // that's we want to delete + } + }) + + it('returns true', async () => { + const expected = { enable: true } + await runSetup() + await expect(action()).resolves.toEqual(expected) + }) + + it('changes .disabledBy', async () => { + const before = { Comment: [{ id: 'c456', disabledBy: { id: 'u123' } }] } + const expected = { Comment: [{ id: 'c456', disabledBy: null }] } + + await runSetup() + await expect(client.request( + '{ Comment(disabled: true) { id, disabledBy { id } } }' + )).resolves.toEqual(before) + await action() + await expect(client.request( + '{ Comment { id, disabledBy { id } } }' + )).resolves.toEqual(expected) + }) + + it('updates .disabled on post', async () => { + const before = { Comment: [ { id: 'c456', disabled: true } ] } + const expected = { Comment: [ { id: 'c456', disabled: false } ] } + + await runSetup() + await expect(client.request( + '{ Comment(disabled: true) { id disabled } }' + )).resolves.toEqual(before) + await action() // this updates .disabled + await expect(client.request( + '{ Comment { id disabled } }' + )).resolves.toEqual(expected) + }) }) - it('changes .disabledBy', async () => { - const before = { Post: [{ id: 'p9', disabledBy: { id: 'u123' } }] } - const expected = { Post: [{ id: 'p9', disabledBy: null }] } + describe('on a post', () => { + beforeEach(async () => { + variables = { + id: 'p9', + type: 'contribution' + } - await expect(client.request( - '{ Post(disabled: true) { id, disabledBy { id } } }' - )).resolves.toEqual(before) - await client.request(mutation) - await expect(client.request( - '{ Post { id, disabledBy { id } } }' - )).resolves.toEqual(expected) - }) + setup.createResource = async () => { + await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' }) + await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) + await factory.create('Post', { + id: 'p9' // that's the ID we will look for + }) - it('updates .disabled on post', async () => { - const before = { Post: [ { id: 'p9', disabled: true } ] } - const expected = { Post: [ { id: 'p9', disabled: false } ] } + const disableMutation = ` + mutation { + disable(resource: { + id: "p9" + type: contribution + }) + } + ` + await factory.mutate(disableMutation) // that's we want to delete + } + }) - await expect(client.request( - '{ Post(disabled: true) { id disabled } }' - )).resolves.toEqual(before) - await client.request(mutation) // this updates .disabled - await expect(client.request( - '{ Post { id disabled } }' - )).resolves.toEqual(expected) + it('returns true', async () => { + const expected = { enable: true } + await runSetup() + await expect(action()).resolves.toEqual(expected) + }) + + it('changes .disabledBy', async () => { + const before = { Post: [{ id: 'p9', disabledBy: { id: 'u123' } }] } + const expected = { Post: [{ id: 'p9', disabledBy: null }] } + + await runSetup() + await expect(client.request( + '{ Post(disabled: true) { id, disabledBy { id } } }' + )).resolves.toEqual(before) + await action() + await expect(client.request( + '{ Post { id, disabledBy { id } } }' + )).resolves.toEqual(expected) + }) + + it('updates .disabled on post', async () => { + const before = { Post: [ { id: 'p9', disabled: true } ] } + const expected = { Post: [ { id: 'p9', disabled: false } ] } + + await runSetup() + await expect(client.request( + '{ Post(disabled: true) { id disabled } }' + )).resolves.toEqual(before) + await action() // this updates .disabled + await expect(client.request( + '{ Post { id disabled } }' + )).resolves.toEqual(expected) + }) }) }) }) From f40a67b7a85c6aa24ea7d005e62cf6ce29fb82ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 6 Mar 2019 17:12:30 +0100 Subject: [PATCH 11/12] Implement disabling of comments+users+posts --- src/resolvers/moderation.js | 16 ++++++++-------- src/schema.graphql | 2 ++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/resolvers/moderation.js b/src/resolvers/moderation.js index 8e171dbf6..10046dd03 100644 --- a/src/resolvers/moderation.js +++ b/src/resolvers/moderation.js @@ -1,28 +1,28 @@ export default { Mutation: { disable: async (object, params, { user, driver }) => { - const { resource: { id: postId } } = params + const { resource: { id } } = params const { id: userId } = user const cypher = ` MATCH (u:User {id: $userId}) - MATCH (p:Post {id: $postId}) - SET p.disabled = true - MERGE (p)<-[:DISABLED]-(u) + MATCH (r {id: $id}) + SET r.disabled = true + MERGE (r)<-[:DISABLED]-(u) ` const session = driver.session() - const res = await session.run(cypher, { postId, userId }) + const res = await session.run(cypher, { id, userId }) session.close() return Boolean(res) }, enable: async (object, params, { user, driver }) => { - const { resource: { id: postId } } = params + const { resource: { id } } = params const cypher = ` - MATCH (p:Post {id: $postId})<-[d:DISABLED]-() + MATCH (p {id: $id})<-[d:DISABLED]-() SET p.disabled = false DELETE d ` const session = driver.session() - const res = await session.run(cypher, { postId }) + const res = await session.run(cypher, { id }) session.close() return Boolean(res) } diff --git a/src/schema.graphql b/src/schema.graphql index ddafd880e..5e58d5f1d 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -76,6 +76,7 @@ type User { avatar: String deleted: Boolean disabled: Boolean + disabledBy: User @relation(name: "DISABLED", direction: "IN") role: UserGroupEnum location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") @@ -165,6 +166,7 @@ type Comment { updatedAt: String deleted: Boolean disabled: Boolean + disabledBy: User @relation(name: "DISABLED", direction: "IN") } type Report { From 80729394586e6923cde23f8480e75dd4d5dcedad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 6 Mar 2019 17:21:49 +0100 Subject: [PATCH 12/12] Tiny performance improvement --- src/resolvers/moderation.js | 4 ++-- src/resolvers/moderation.spec.js | 21 ++++++++------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/resolvers/moderation.js b/src/resolvers/moderation.js index 10046dd03..db44790b9 100644 --- a/src/resolvers/moderation.js +++ b/src/resolvers/moderation.js @@ -17,8 +17,8 @@ export default { enable: async (object, params, { user, driver }) => { const { resource: { id } } = params const cypher = ` - MATCH (p {id: $id})<-[d:DISABLED]-() - SET p.disabled = false + MATCH (r {id: $id})<-[d:DISABLED]-() + SET r.disabled = false DELETE d ` const session = driver.session() diff --git a/src/resolvers/moderation.spec.js b/src/resolvers/moderation.spec.js index 6107074ef..c1d4a75fe 100644 --- a/src/resolvers/moderation.spec.js +++ b/src/resolvers/moderation.spec.js @@ -92,12 +92,10 @@ describe('disable', () => { setup.createResource = async () => { await factory.create('User', { id: 'u45', email: 'commenter@example.org', password: '1234' }) await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' }) - await factory.create('Post', { - id: 'p3' - }) - await factory.create('Comment', { - id: 'c47' - }) + await Promise.all([ + factory.create('Post', { id: 'p3' }), + factory.create('Comment', { id: 'c47' }) + ]) await Promise.all([ factory.relate('Comment', 'Author', { from: 'u45', to: 'c47' }), factory.relate('Comment', 'Post', { from: 'c47', to: 'p3' }) @@ -251,13 +249,10 @@ describe('enable', () => { setup.createResource = async () => { await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' }) await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) - await factory.create('Post', { - id: 'p9' // that's the ID we will look for - }) - - await factory.create('Comment', { - id: 'c456' - }) + await Promise.all([ + factory.create('Post', { id: 'p9' }), + factory.create('Comment', { id: 'c456' }) + ]) await Promise.all([ factory.relate('Comment', 'Author', { from: 'u123', to: 'c456' }), factory.relate('Comment', 'Post', { from: 'c456', to: 'p9' })