From 7d82b27aaa53a422954a9e18bd7e203147fbf59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 12 Mar 2019 12:59:45 +0100 Subject: [PATCH 1/3] Hide disabled comments and posts by default I had to trick neo4j-graphql-js to return disabled and deleted fields by default on any neo4j node. If disabled is not queried, then we don't have it in our middleware and we're not able to filter it. --- src/middleware/idMiddleware.js | 28 -------- src/middleware/includedFieldsMiddleware.js | 29 ++++++++ src/middleware/index.js | 4 +- src/middleware/softDeleteMiddleware.js | 45 +++++++++---- src/middleware/softDeleteMiddleware.spec.js | 73 +++++++++++++++++++-- src/schema.graphql | 4 +- src/seed/seed-db.js | 9 ++- 7 files changed, 136 insertions(+), 56 deletions(-) delete mode 100644 src/middleware/idMiddleware.js create mode 100644 src/middleware/includedFieldsMiddleware.js diff --git a/src/middleware/idMiddleware.js b/src/middleware/idMiddleware.js deleted file mode 100644 index 2f1854fa6..000000000 --- a/src/middleware/idMiddleware.js +++ /dev/null @@ -1,28 +0,0 @@ -import cloneDeep from 'lodash/cloneDeep' - -const includeId = async (resolve, root, args, context, resolveInfo) => { - // Keeping the graphql resolveInfo untouched ensures that we don't add the - // following attributes to the result set returned to the graphQL client. - // We only want to pass these attributes to our resolver for internal - // purposes e.g. authorization. - const copy = cloneDeep(resolveInfo) - - copy.fieldNodes[0].selectionSet.selections.unshift({ - kind: 'Field', - name: { kind: 'Name', value: 'id' } - }) - return resolve(root, args, context, copy) -} - -export default { - Query: { - User: (resolve, root, args, context, info) => { - return includeId(resolve, root, args, context, info) - } - }, - Mutation: { - CreatePost: (resolve, root, args, context, info) => { - return includeId(resolve, root, args, context, info) - } - } -} diff --git a/src/middleware/includedFieldsMiddleware.js b/src/middleware/includedFieldsMiddleware.js new file mode 100644 index 000000000..6d8ccf8f1 --- /dev/null +++ b/src/middleware/includedFieldsMiddleware.js @@ -0,0 +1,29 @@ +import cloneDeep from 'lodash/cloneDeep' + +const _includeFieldsRecursively = (selectionSet, includedFields) => { + if (!selectionSet) return + includedFields.forEach((includedField) => { + selectionSet.selections.unshift({ + kind: 'Field', + name: { kind: 'Name', value: includedField } + }) + }) + selectionSet.selections.forEach((selection) => { + _includeFieldsRecursively(selection.selectionSet, includedFields) + }) +} + +const includeFieldsRecursively = (includedFields) => { + return (resolve, root, args, context, resolveInfo) => { + const copy = cloneDeep(resolveInfo) + copy.fieldNodes.forEach((fieldNode) => { + _includeFieldsRecursively(fieldNode.selectionSet, includedFields) + }) + return resolve(root, args, context, copy) + } +} + +export default { + Query: includeFieldsRecursively(['id', 'disabled', 'deleted']), + Mutation: includeFieldsRecursively(['id', 'disabled', 'deleted']) +} diff --git a/src/middleware/index.js b/src/middleware/index.js index 5cc20f969..6f95c7451 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -7,7 +7,7 @@ import dateTimeMiddleware from './dateTimeMiddleware' import xssMiddleware from './xssMiddleware' import permissionsMiddleware from './permissionsMiddleware' import userMiddleware from './userMiddleware' -import idMiddleware from './idMiddleware' +import includedFieldsMiddleware from './includedFieldsMiddleware' export default schema => { let middleware = [ @@ -19,7 +19,7 @@ export default schema => { fixImageUrlsMiddleware, softDeleteMiddleware, userMiddleware, - idMiddleware + includedFieldsMiddleware ] // add permisions middleware at the first position (unless we're seeding) diff --git a/src/middleware/softDeleteMiddleware.js b/src/middleware/softDeleteMiddleware.js index 0c12e7a72..042219916 100644 --- a/src/middleware/softDeleteMiddleware.js +++ b/src/middleware/softDeleteMiddleware.js @@ -1,26 +1,43 @@ -const setDefaults = (args) => { +const isModerator = ({ user }) => { + return user && (user.role === 'moderator' || user.role === 'admin') +} + +const setDefaultFilters = (resolve, root, args, context, info) => { if (typeof args.deleted !== 'boolean') { args.deleted = false } - if (typeof args.disabled !== 'boolean') { + + if (!isModerator(context)) { args.disabled = false } - return args + return resolve(root, args, context, info) +} + +const hideDisabledComments = async (resolve, root, args, context, info) => { + const { comments } = root + if (!Array.isArray(comments)) return resolve(root, args, context, info) + if (!isModerator(context)) { + root.comments = comments.filter((comment) => { + return !comment.disabled + }) + } + return resolve(root, args, context, info) } export default { Query: { - Post: (resolve, root, args, context, info) => { - return resolve(root, setDefaults(args), context, info) - }, - Comment: async (resolve, root, args, context, info) => { - return resolve(root, setDefaults(args), context, info) - }, - User: async (resolve, root, args, context, info) => { - return resolve(root, setDefaults(args), context, info) - } + Post: setDefaultFilters, + Comment: setDefaultFilters, + User: setDefaultFilters }, Mutation: async (resolve, root, args, context, info) => { - return resolve(root, setDefaults(args), context, info) - } + args.disabled = false + // TODO: remove as soon as our factories don't need this anymore + if (typeof args.deleted !== 'boolean') { + args.deleted = false + } + return resolve(root, args, context, info) + }, + Post: hideDisabledComments, + User: hideDisabledComments } diff --git a/src/middleware/softDeleteMiddleware.spec.js b/src/middleware/softDeleteMiddleware.spec.js index ed132104e..f478feb7c 100644 --- a/src/middleware/softDeleteMiddleware.spec.js +++ b/src/middleware/softDeleteMiddleware.spec.js @@ -9,14 +9,14 @@ let action beforeEach(async () => { await Promise.all([ - factory.create('User', { role: 'user', email: 'user@example.org', password: '1234' }), + factory.create('User', { id: 'u1', role: 'user', email: 'user@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 }), - factory.create('Post', { id: 'p2', title: 'Disabled post', deleted: false }), - factory.create('Post', { title: 'Publicly visible post', deleted: false }) + await factory.create('Post', { id: 'p1', title: 'Deleted post', deleted: true }), + await factory.create('Post', { id: 'p2', title: 'Disabled post', deleted: false }), + await factory.create('Post', { id: 'p3', title: 'Publicly visible post', deleted: false }) ]) const moderatorFactory = Factory() await moderatorFactory.authenticateAs({ email: 'moderator@example.org', password: '1234' }) @@ -62,9 +62,68 @@ describe('softDeleteMiddleware', () => { client = new GraphQLClient(host, { headers }) }) - it('hides deleted or disabled posts', async () => { - const expected = { Post: [{ title: 'Publicly visible post' }] } - await expect(action()).resolves.toEqual(expected) + it('shows disabled but hides deleted posts', async () => { + const expected = [ + { title: 'Disabled post' }, + { title: 'Publicly visible post' } + ] + const { Post } = await action() + await expect(Post).toEqual(expect.arrayContaining(expected)) + }) + }) + + describe('.comments', () => { + beforeEach(async () => { + query = '{ Post(id: "p3") { title comments { content } } }' + + const asModerator = Factory() + await asModerator.authenticateAs({ email: 'moderator@example.org', password: '1234' }) + await Promise.all([ + asModerator.create('Comment', { id: 'c1', content: 'Disabled comment' }), + asModerator.create('Comment', { id: 'c2', content: 'Enabled comment on public post' }) + ]) + await Promise.all([ + asModerator.relate('Comment', 'Author', { from: 'u1', to: 'c1' }), + asModerator.relate('Comment', 'Post', { from: 'c1', to: 'p3' }), + asModerator.relate('Comment', 'Author', { from: 'u1', to: 'c2' }), + asModerator.relate('Comment', 'Post', { from: 'c2', to: 'p3' }) + ]) + await asModerator.mutate('mutation { disable( id: "c1") }') + }) + + describe('as user', () => { + beforeEach(async () => { + const headers = await login({ email: 'user@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('hides disabled comments', async () => { + const expected = { Post: [ { + title: 'Publicly visible post', + comments: [ + { content: 'Enabled comment on public post' } + ] + } + ] } + await expect(action()).resolves.toEqual(expected) + }) + }) + + describe('as moderator', () => { + beforeEach(async () => { + const headers = await login({ email: 'moderator@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('shows disabled comments', async () => { + const expected = [ + { content: 'Enabled comment on public post' }, + { content: 'Disabled comment' } + ] + const { Post: [{ comments }] } = await action() + + await expect(comments).toEqual(expect.arrayContaining(expected)) + }) }) }) diff --git a/src/schema.graphql b/src/schema.graphql index 152301715..801eb4502 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -148,7 +148,7 @@ type User { ) comments: [Comment]! @relation(name: "WROTE", direction: "OUT") - commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) WHERE NOT r.deleted = true RETURN COUNT(r)") + commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)") shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT") shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") @@ -189,7 +189,7 @@ type Post { categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") comments: [Comment]! @relation(name: "COMMENTS", direction: "IN") - commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) RETURN COUNT(r)") + commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)") shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN") shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") diff --git a/src/seed/seed-db.js b/src/seed/seed-db.js index e8c757db8..d526518b9 100644 --- a/src/seed/seed-db.js +++ b/src/seed/seed-db.js @@ -107,9 +107,6 @@ import Factory from './factories' asTick.create('Post', { id: 'p15' }) ]) - const disableMutation = 'mutation { disable( id: "p11") }' - await asModerator.mutate(disableMutation) - await Promise.all([ f.relate('Post', 'Categories', { from: 'p0', to: 'cat16' }), f.relate('Post', 'Categories', { from: 'p1', to: 'cat1' }), @@ -214,6 +211,12 @@ import Factory from './factories' f.relate('Comment', 'Post', { from: 'c7', to: 'p2' }) ]) + const disableMutation = 'mutation($id: ID!) { disable(id: $id) }' + await Promise.all([ + asModerator.mutate(disableMutation, { id: 'p11' }), + asModerator.mutate(disableMutation, { id: 'c5' }) + ]) + await Promise.all([ asTick.create('Report', { description: 'I don\'t like this comment', id: 'c1' }), asTrick.create('Report', { description: 'I don\'t like this post', id: 'p1' }), From d71cb16a01e98f7d5c069fac2142e762be58dbff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 13 Mar 2019 20:57:50 +0100 Subject: [PATCH 2/3] Spec a new behaviour of softDisable If we have nested content, the content should be obfuscated instead of being filtered. cc @datenbrei --- src/middleware/softDeleteMiddleware.spec.js | 297 +++++++++++++------- src/seed/factories/users.js | 2 + 2 files changed, 192 insertions(+), 107 deletions(-) diff --git a/src/middleware/softDeleteMiddleware.spec.js b/src/middleware/softDeleteMiddleware.spec.js index f478feb7c..6b8b4bc49 100644 --- a/src/middleware/softDeleteMiddleware.spec.js +++ b/src/middleware/softDeleteMiddleware.spec.js @@ -7,41 +7,103 @@ let client let query let action -beforeEach(async () => { +beforeAll(async () => { + // For performance reasons we do this only once await Promise.all([ factory.create('User', { id: 'u1', role: 'user', email: 'user@example.org', password: '1234' }), - factory.create('User', { id: 'm1', role: 'moderator', email: 'moderator@example.org', password: '1234' }) + factory.create('User', { id: 'm1', role: 'moderator', email: 'moderator@example.org', password: '1234' }), + factory.create('User', { id: 'u2', role: 'user', avatar: '/some/offensive/avatar.jpg', about: 'This self description is very offensive', email: 'troll@example.org', password: '1234' }) ]) + await factory.authenticateAs({ email: 'user@example.org', password: '1234' }) await Promise.all([ - await factory.create('Post', { id: 'p1', title: 'Deleted post', deleted: true }), - await factory.create('Post', { id: 'p2', title: 'Disabled post', deleted: false }), - await factory.create('Post', { id: 'p3', title: 'Publicly visible post', deleted: false }) + factory.follow({ id: 'u2', type: 'User' }), + factory.create('Post', { id: 'p1', title: 'Deleted post', deleted: true }), + factory.create('Post', { id: 'p3', title: 'Publicly visible post', deleted: false }) ]) - const moderatorFactory = Factory() - await moderatorFactory.authenticateAs({ email: 'moderator@example.org', password: '1234' }) - const disableMutation = ` - mutation { - disable( - id: "p2" - ) - } - ` - await moderatorFactory.mutate(disableMutation) + + await Promise.all([ + factory.create('Comment', { id: 'c2', content: 'Enabled comment on public post' }) + ]) + + await Promise.all([ + factory.relate('Comment', 'Author', { from: 'u1', to: 'c2' }), + factory.relate('Comment', 'Post', { from: 'c2', to: 'p3' }) + ]) + + const asTroll = Factory() + await asTroll.authenticateAs({ email: 'troll@example.org', password: '1234' }) + await asTroll.create('Post', { id: 'p2', title: 'Disabled post', content: 'This is an offensive post content', image: '/some/offensive/image.jpg', deleted: false }) + await asTroll.create('Comment', { id: 'c1', content: 'Disabled comment' }) + await Promise.all([ + asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' }), + asTroll.relate('Comment', 'Post', { from: 'c1', to: 'p3' }) + ]) + + const asModerator = Factory() + await asModerator.authenticateAs({ email: 'moderator@example.org', password: '1234' }) + await asModerator.mutate('mutation { disable( id: "p2") }') + await asModerator.mutate('mutation { disable( id: "c1") }') + await asModerator.mutate('mutation { disable( id: "u2") }') }) -afterEach(async () => { +afterAll(async () => { await factory.cleanDatabase() }) describe('softDeleteMiddleware', () => { - describe('Post', () => { + describe('read disabled content', () => { + let user + let post + let comment + const beforeComment = async () => { + query = '{ User(id: "u1") { following { comments { content contentExcerpt } } } }' + const response = await action() + comment = response.User[0].following[0].comments[0] + } + const beforeUser = async () => { + query = '{ User(id: "u1") { following { about avatar } } }' + const response = await action() + user = response.User[0].following[0] + } + const beforePost = async () => { + query = '{ User(id: "u1") { following { contributions { title image content contentExcerpt } } } }' + const response = await action() + post = response.User[0].following[0].contributions[0] + } + action = () => { return client.request(query) } - beforeEach(() => { - query = '{ Post { title } }' + describe('as moderator', () => { + beforeEach(async () => { + const headers = await login({ email: 'moderator@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + describe('User', () => { + beforeEach(beforeUser) + + it('displays about', () => expect(user.about).toEqual('This self description is very offensive')) + it('displays avatar', () => expect(user.avatar).toEqual('/some/offensive/avatar.jpg')) + }) + + describe('Post', () => { + beforeEach(beforePost) + + it('displays title', () => expect(post.title).toEqual('Disabled post')) + it('displays content', () => expect(post.content).toEqual('This is an offensive post content')) + it('displays contentExcerpt', () => expect(post.contentExcerpt).toEqual('This is an offensive post content')) + it('displays image', () => expect(post.image).toEqual('/some/offensive/image.jpg')) + }) + + describe('Comment', () => { + beforeEach(beforeComment) + + it('displays content', () => expect(comment.content).toEqual('Disabled comment')) + it('displays contentExcerpt', () => expect(comment.contentExcerpt).toEqual('Disabled comment')) + }) }) describe('as user', () => { @@ -50,45 +112,35 @@ describe('softDeleteMiddleware', () => { client = new GraphQLClient(host, { headers }) }) - it('hides deleted or disabled posts', async () => { - const expected = { Post: [{ title: 'Publicly visible post' }] } - await expect(action()).resolves.toEqual(expected) + describe('User', () => { + beforeEach(beforeUser) + + it('obfuscates about', () => expect(user.about).toEqual('DELETED')) + it('obfuscates avatar', () => expect(user.avatar).toEqual('DELETED')) + }) + + describe('Post', () => { + beforeEach(beforePost) + + it('obfuscates title', () => expect(post.title).toEqual('DELETED')) + it('obfuscates content', () => expect(post.content).toEqual('DELETED')) + it('obfuscates contentExcerpt', () => expect(post.contentExcerpt).toEqual('DELETED')) + it('obfuscates image', () => expect(post.image).toEqual('DELETED')) + }) + + describe('Comment', () => { + beforeEach(beforeComment) + + it('obfuscates content', () => expect(comment.content).toEqual('DELETED')) + it('obfuscates contentExcerpt', () => expect(comment.contentExcerpt).toEqual('DELETED')) }) }) + }) - describe('as moderator', () => { + describe('Query', () => { + describe('Post', () => { beforeEach(async () => { - const headers = await login({ email: 'moderator@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - - it('shows disabled but hides deleted posts', async () => { - const expected = [ - { title: 'Disabled post' }, - { title: 'Publicly visible post' } - ] - const { Post } = await action() - await expect(Post).toEqual(expect.arrayContaining(expected)) - }) - }) - - describe('.comments', () => { - beforeEach(async () => { - query = '{ Post(id: "p3") { title comments { content } } }' - - const asModerator = Factory() - await asModerator.authenticateAs({ email: 'moderator@example.org', password: '1234' }) - await Promise.all([ - asModerator.create('Comment', { id: 'c1', content: 'Disabled comment' }), - asModerator.create('Comment', { id: 'c2', content: 'Enabled comment on public post' }) - ]) - await Promise.all([ - asModerator.relate('Comment', 'Author', { from: 'u1', to: 'c1' }), - asModerator.relate('Comment', 'Post', { from: 'c1', to: 'p3' }), - asModerator.relate('Comment', 'Author', { from: 'u1', to: 'c2' }), - asModerator.relate('Comment', 'Post', { from: 'c2', to: 'p3' }) - ]) - await asModerator.mutate('mutation { disable( id: "c1") }') + query = '{ Post { title } }' }) describe('as user', () => { @@ -97,14 +149,8 @@ describe('softDeleteMiddleware', () => { client = new GraphQLClient(host, { headers }) }) - it('hides disabled comments', async () => { - const expected = { Post: [ { - title: 'Publicly visible post', - comments: [ - { content: 'Enabled comment on public post' } - ] - } - ] } + it('hides deleted or disabled posts', async () => { + const expected = { Post: [{ title: 'Publicly visible post' }] } await expect(action()).resolves.toEqual(expected) }) }) @@ -115,72 +161,109 @@ describe('softDeleteMiddleware', () => { client = new GraphQLClient(host, { headers }) }) - it('shows disabled comments', async () => { + it('shows disabled but hides deleted posts', async () => { const expected = [ - { content: 'Enabled comment on public post' }, - { content: 'Disabled comment' } + { title: 'Disabled post' }, + { title: 'Publicly visible post' } ] - const { Post: [{ comments }] } = await action() - - await expect(comments).toEqual(expect.arrayContaining(expected)) + const { Post } = await action() + await expect(Post).toEqual(expect.arrayContaining(expected)) }) }) - }) - describe('filter (deleted: true)', () => { - beforeEach(() => { - query = '{ Post(deleted: true) { title } }' - }) - - describe('as user', () => { + describe('.comments', () => { beforeEach(async () => { - const headers = await login({ email: 'user@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) + query = '{ Post(id: "p3") { title comments { content } } }' }) - it('throws authorisation error', async () => { - await expect(action()).rejects.toThrow('Not Authorised!') + describe('as user', () => { + beforeEach(async () => { + const headers = await login({ email: 'user@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('conceals disabled comments', async () => { + const expected = [ + { content: 'Enabled comment on public post' }, + { content: 'DELETED' } + ] + const { Post: [{ comments }] } = await action() + await expect(comments).toEqual(expect.arrayContaining(expected)) + }) + }) + + describe('as moderator', () => { + beforeEach(async () => { + const headers = await login({ email: 'moderator@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('shows disabled comments', async () => { + const expected = [ + { content: 'Enabled comment on public post' }, + { content: 'Disabled comment' } + ] + const { Post: [{ comments }] } = await action() + await expect(comments).toEqual(expect.arrayContaining(expected)) + }) }) }) - describe('as moderator', () => { - beforeEach(async () => { - const headers = await login({ email: 'moderator@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) + describe('filter (deleted: true)', () => { + beforeEach(() => { + query = '{ Post(deleted: true) { title } }' }) - it('shows deleted posts', async () => { - const expected = { Post: [{ title: 'Deleted post' }] } - await expect(action()).resolves.toEqual(expected) - }) - }) - }) + describe('as user', () => { + beforeEach(async () => { + const headers = await login({ email: 'user@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) - describe('filter (disabled: true)', () => { - beforeEach(() => { - query = '{ Post(disabled: true) { title } }' - }) - - describe('as user', () => { - beforeEach(async () => { - const headers = await login({ email: 'user@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) + it('throws authorisation error', async () => { + await expect(action()).rejects.toThrow('Not Authorised!') + }) }) - it('throws authorisation error', async () => { - await expect(action()).rejects.toThrow('Not Authorised!') + describe('as moderator', () => { + beforeEach(async () => { + const headers = await login({ email: 'moderator@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('shows deleted posts', async () => { + const expected = { Post: [{ title: 'Deleted post' }] } + await expect(action()).resolves.toEqual(expected) + }) }) }) - describe('as moderator', () => { - beforeEach(async () => { - const headers = await login({ email: 'moderator@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) + describe('filter (disabled: true)', () => { + beforeEach(() => { + query = '{ Post(disabled: true) { title } }' }) - it('shows disabled posts', async () => { - const expected = { Post: [{ title: 'Disabled post' }] } - await expect(action()).resolves.toEqual(expected) + describe('as user', () => { + beforeEach(async () => { + const headers = await login({ email: 'user@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorisation error', async () => { + await expect(action()).rejects.toThrow('Not Authorised!') + }) + }) + + describe('as moderator', () => { + beforeEach(async () => { + const headers = await login({ email: 'moderator@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('shows disabled posts', async () => { + const expected = { Post: [{ title: 'Disabled post' }] } + await expect(action()).resolves.toEqual(expected) + }) }) }) }) diff --git a/src/seed/factories/users.js b/src/seed/factories/users.js index c27b2b1ce..491b3f9e1 100644 --- a/src/seed/factories/users.js +++ b/src/seed/factories/users.js @@ -9,6 +9,7 @@ export default function create (params) { password = '1234', role = 'user', avatar = faker.internet.avatar(), + about = faker.lorem.paragraph(), disabled = false, deleted = false } = params @@ -21,6 +22,7 @@ export default function create (params) { password: "${password}", email: "${email}", avatar: "${avatar}", + about: "${about}", role: ${role}, disabled: ${disabled}, deleted: ${deleted} From a252879964d1a715de8692e3b56d25ad75a910e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 13 Mar 2019 21:03:40 +0100 Subject: [PATCH 3/3] Implement obfuscation behaviour --- src/middleware/softDeleteMiddleware.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/middleware/softDeleteMiddleware.js b/src/middleware/softDeleteMiddleware.js index 042219916..24105c435 100644 --- a/src/middleware/softDeleteMiddleware.js +++ b/src/middleware/softDeleteMiddleware.js @@ -13,13 +13,14 @@ const setDefaultFilters = (resolve, root, args, context, info) => { return resolve(root, args, context, info) } -const hideDisabledComments = async (resolve, root, args, context, info) => { - const { comments } = root - if (!Array.isArray(comments)) return resolve(root, args, context, info) - if (!isModerator(context)) { - root.comments = comments.filter((comment) => { - return !comment.disabled - }) +const obfuscateDisabled = async (resolve, root, args, context, info) => { + if (!isModerator(context) && root.disabled) { + root.content = 'DELETED' + root.contentExcerpt = 'DELETED' + root.title = 'DELETED' + root.image = 'DELETED' + root.avatar = 'DELETED' + root.about = 'DELETED' } return resolve(root, args, context, info) } @@ -38,6 +39,7 @@ export default { } return resolve(root, args, context, info) }, - Post: hideDisabledComments, - User: hideDisabledComments + Post: obfuscateDisabled, + User: obfuscateDisabled, + Comment: obfuscateDisabled }