From ce28de893bd6fe3faaae3ac4701437b0a03f8e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 27 Feb 2019 16:41:18 +0100 Subject: [PATCH 1/5] Write a test for #27 Moderators are allowed to see disabled or deleted posts if they ask for it. --- src/middleware/softDeleteMiddleware.spec.js | 119 ++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/middleware/softDeleteMiddleware.spec.js diff --git a/src/middleware/softDeleteMiddleware.spec.js b/src/middleware/softDeleteMiddleware.spec.js new file mode 100644 index 000000000..2e5dbe054 --- /dev/null +++ b/src/middleware/softDeleteMiddleware.spec.js @@ -0,0 +1,119 @@ +import Factory from '../seed/factories' +import { host, login } from '../jest/helpers' +import { GraphQLClient } from 'graphql-request' + +const factory = Factory() +let client +let query +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' }) + ]) + 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 }) + ]) +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('softDeleteMiddleware', () => { + describe('Post', () => { + action = () => { + return client.request(query) + } + + beforeEach(() => { + query = '{ Post { title } }' + }) + + describe('as user', () => { + beforeEach(async () => { + const headers = await login({ email: 'user@example.org', password: '1234' }) + 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('as moderator', () => { + beforeEach(async () => { + const headers = await login({ email: 'moderator@example.org', password: '1234' }) + 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('filter (deleted: true)', () => { + beforeEach(() => { + query = '{ Post(deleted: 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!') + }) + }) + + 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('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!') + }) + }) + + 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) + }) + }) + }) + }) +}) From 738ba4f51ca4dec9e61cb2372be64d767f4f3d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 27 Feb 2019 16:48:43 +0100 Subject: [PATCH 2/5] DRY softDeleteMiddleware --- src/middleware/softDeleteMiddleware.js | 43 +++++++++----------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/src/middleware/softDeleteMiddleware.js b/src/middleware/softDeleteMiddleware.js index 79e4a7d08..abc742bb3 100644 --- a/src/middleware/softDeleteMiddleware.js +++ b/src/middleware/softDeleteMiddleware.js @@ -1,38 +1,23 @@ +const normalize = (args) => { + if (typeof args.deleted !== 'boolean') { + args.deleted = false + } + if (typeof args.disabled !== 'boolean') { + args.disabled = false + } + return args +} + export default { Query: { - Post: async (resolve, root, args, context, info) => { - if (typeof args.deleted !== 'boolean') { - args.deleted = false - } - if (typeof args.disabled !== 'boolean') { - args.disabled = false - } - const result = await resolve(root, args, context, info) - return result + Post: (resolve, root, args, context, info) => { + return resolve(root, normalize(args), context, info) }, Comment: async (resolve, root, args, context, info) => { - if (typeof args.deleted !== 'boolean') { - args.deleted = false - } - if (typeof args.disabled !== 'boolean') { - args.disabled = false - } - const result = await resolve(root, args, context, info) - return result + return resolve(root, normalize(args), context, info) }, User: async (resolve, root, args, context, info) => { - if (typeof args.deleted !== 'boolean') { - args.deleted = false - } - if (typeof args.disabled !== 'boolean') { - args.disabled = false - } - // console.log('ROOT', root) - // console.log('ARGS', args) - // console.log('CONTEXT', context) - // console.log('info', info.fieldNodes[0].arguments) - const result = await resolve(root, args, context, info) - return result + return resolve(root, normalize(args), context, info) } } } From 911500a3bd3df99e9ca2cda3cddc2cd2743ad02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 27 Feb 2019 16:49:03 +0100 Subject: [PATCH 3/5] Don't override given { deleted, disabled } = args @appinteractive I guess this was done unintentionally? --- src/middleware/dateTimeMiddleware.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/middleware/dateTimeMiddleware.js b/src/middleware/dateTimeMiddleware.js index 97e6e2767..473dbf444 100644 --- a/src/middleware/dateTimeMiddleware.js +++ b/src/middleware/dateTimeMiddleware.js @@ -2,29 +2,21 @@ export default { Mutation: { CreateUser: async (resolve, root, args, context, info) => { args.createdAt = (new Date()).toISOString() - args.disabled = false - args.deleted = false const result = await resolve(root, args, context, info) return result }, CreatePost: async (resolve, root, args, context, info) => { args.createdAt = (new Date()).toISOString() - args.disabled = false - args.deleted = false const result = await resolve(root, args, context, info) return result }, CreateComment: async (resolve, root, args, context, info) => { args.createdAt = (new Date()).toISOString() - args.disabled = false - args.deleted = false const result = await resolve(root, args, context, info) return result }, CreateOrganization: async (resolve, root, args, context, info) => { args.createdAt = (new Date()).toISOString() - args.disabled = false - args.deleted = false const result = await resolve(root, args, context, info) return result }, From f3ab671f2146b4bf597a6f4f6dda523f07048592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Thu, 28 Feb 2019 01:19:39 +0100 Subject: [PATCH 4/5] Soft delete middleware test passes --- src/middleware/permissionsMiddleware.js | 22 ++++++++++++---------- src/middleware/softDeleteMiddleware.js | 8 ++++---- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 0c6723b4b..c070e536d 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -1,4 +1,4 @@ -import { rule, shield, allow } from 'graphql-shield' +import { rule, shield, allow, or } from 'graphql-shield' /* * TODO: implement @@ -11,36 +11,38 @@ const isAuthenticated = rule()(async (parent, args, ctx, info) => { const isAdmin = rule()(async (parent, args, ctx, info) => { return ctx.user.role === 'ADMIN' }) -const isModerator = rule()(async (parent, args, ctx, info) => { - return ctx.user.role === 'MODERATOR' -}) */ +const isModerator = rule()(async (parent, args, { user }, info) => { + return user && (user.role === 'moderator' || user.role === 'admin') +}) + const isMyOwn = rule({ cache: 'no_cache' })(async (parent, args, ctx, info) => { return ctx.user.id === parent.id }) +const onlyEnabledContent = rule({ cache: 'strict' })(async (parent, args, ctx, info) => { + const { disabled, deleted } = args + return !(disabled || deleted) +}) + // Permissions const permissions = shield({ Query: { statistics: allow, - currentUser: allow - // fruits: and(isAuthenticated, or(isAdmin, isModerator)), - // customers: and(isAuthenticated, isAdmin) + currentUser: allow, + Post: or(onlyEnabledContent, isModerator) }, Mutation: { CreatePost: isAuthenticated, // TODO UpdatePost: isOwner, // TODO DeletePost: isOwner, report: isAuthenticated - // addFruitToBasket: isAuthenticated - // CreateUser: allow, }, User: { email: isMyOwn, password: isMyOwn } - // Post: isAuthenticated }) export default permissions diff --git a/src/middleware/softDeleteMiddleware.js b/src/middleware/softDeleteMiddleware.js index abc742bb3..bed7b6ca0 100644 --- a/src/middleware/softDeleteMiddleware.js +++ b/src/middleware/softDeleteMiddleware.js @@ -1,4 +1,4 @@ -const normalize = (args) => { +const setDefaults = (args) => { if (typeof args.deleted !== 'boolean') { args.deleted = false } @@ -11,13 +11,13 @@ const normalize = (args) => { export default { Query: { Post: (resolve, root, args, context, info) => { - return resolve(root, normalize(args), context, info) + return resolve(root, setDefaults(args), context, info) }, Comment: async (resolve, root, args, context, info) => { - return resolve(root, normalize(args), context, info) + return resolve(root, setDefaults(args), context, info) }, User: async (resolve, root, args, context, info) => { - return resolve(root, normalize(args), context, info) + return resolve(root, setDefaults(args), context, info) } } } From 8febf147cef2dd3eccb3b3331f47add0c02d6988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Thu, 28 Feb 2019 02:53:11 +0100 Subject: [PATCH 5/5] Fix lint --- src/middleware/softDeleteMiddleware.spec.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/middleware/softDeleteMiddleware.spec.js b/src/middleware/softDeleteMiddleware.spec.js index 2e5dbe054..925a03ccc 100644 --- a/src/middleware/softDeleteMiddleware.spec.js +++ b/src/middleware/softDeleteMiddleware.spec.js @@ -9,13 +9,13 @@ let action beforeEach(async () => { await Promise.all([ - factory.create('User', { role: 'user', email: 'user@example.org', password: '1234' }), + factory.create('User', { role: 'user', email: 'user@example.org', password: '1234' }), factory.create('User', { role: 'moderator', email: 'moderator@example.org', password: '1234' }) ]) - await factory.authenticateAs({email: 'user@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: '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 }) ]) }) @@ -41,7 +41,7 @@ describe('softDeleteMiddleware', () => { }) it('hides deleted or disabled posts', async () => { - const expected = {Post: [{title: 'Publicly visible post'}]} + const expected = { Post: [{ title: 'Publicly visible post' }] } await expect(action()).resolves.toEqual(expected) }) }) @@ -53,7 +53,7 @@ describe('softDeleteMiddleware', () => { }) it('hides deleted or disabled posts', async () => { - const expected = {Post: [{title: 'Publicly visible post'}]} + const expected = { Post: [{ title: 'Publicly visible post' }] } await expect(action()).resolves.toEqual(expected) }) }) @@ -81,7 +81,7 @@ describe('softDeleteMiddleware', () => { }) it('shows deleted posts', async () => { - const expected = {Post: [{title: 'Deleted post'}]} + const expected = { Post: [{ title: 'Deleted post' }] } await expect(action()).resolves.toEqual(expected) }) }) @@ -110,7 +110,7 @@ describe('softDeleteMiddleware', () => { }) it('shows disabled posts', async () => { - const expected = {Post: [{title: 'Disabled post'}]} + const expected = { Post: [{ title: 'Disabled post' }] } await expect(action()).resolves.toEqual(expected) }) })