diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 5515d5b7a..c40803e00 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -25,6 +25,22 @@ const onlyEnabledContent = rule({ cache: 'strict' })(async (parent, args, ctx, i return !(disabled || deleted) }) +const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) => { + if (!user) return false + const session = driver.session() + const { id: postId } = args + const result = await session.run(` + MATCH (post:Post {id: $postId})<-[:WROTE]-(author) + RETURN author + `, { postId }) + const [author] = result.records.map((record) => { + return record.get('author') + }) + const { properties: { id: authorId } } = author + session.close() + return authorId === user.id +}) + // Permissions const permissions = shield({ Query: { @@ -34,8 +50,8 @@ const permissions = shield({ }, Mutation: { CreatePost: isAuthenticated, - // TODO UpdatePost: isOwner, - // TODO DeletePost: isOwner, + UpdatePost: isAuthor, + DeletePost: isAuthor, report: isAuthenticated, CreateBadge: isAdmin, UpdateBadge: isAdmin, diff --git a/src/middleware/softDeleteMiddleware.js b/src/middleware/softDeleteMiddleware.js index bed7b6ca0..0c12e7a72 100644 --- a/src/middleware/softDeleteMiddleware.js +++ b/src/middleware/softDeleteMiddleware.js @@ -19,5 +19,8 @@ export default { User: async (resolve, root, args, context, info) => { return resolve(root, setDefaults(args), context, info) } + }, + Mutation: async (resolve, root, args, context, info) => { + return resolve(root, setDefaults(args), context, info) } } diff --git a/src/resolvers/posts.js b/src/resolvers/posts.js index 6a8a0c25f..abf91e047 100644 --- a/src/resolvers/posts.js +++ b/src/resolvers/posts.js @@ -2,21 +2,20 @@ import { neo4jgraphql } from 'neo4j-graphql-js' export default { Mutation: { - CreatePost: async (object, params, ctx, resolveInfo) => { - const result = await neo4jgraphql(object, params, ctx, resolveInfo, false) + CreatePost: async (object, params, context, resolveInfo) => { + const result = await neo4jgraphql(object, params, context, resolveInfo, false) - const session = ctx.driver.session() + const session = context.driver.session() await session.run( 'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' + 'MERGE (post)<-[:WROTE]-(author) ' + 'RETURN author', { - userId: ctx.user.id, + userId: context.user.id, postId: result.id }) session.close() return result } - } } diff --git a/src/resolvers/posts.spec.js b/src/resolvers/posts.spec.js index a6c1d7e3e..5603683eb 100644 --- a/src/resolvers/posts.spec.js +++ b/src/resolvers/posts.spec.js @@ -3,6 +3,7 @@ import { GraphQLClient } from 'graphql-request' import { host, login } from '../jest/helpers' const factory = Factory() +let client beforeEach(async () => { await factory.create('User', { @@ -16,46 +17,186 @@ afterEach(async () => { }) describe('CreatePost', () => { + const mutation = ` + mutation { + CreatePost(title: "I am a title", content: "Some content") { + title + content + slug + disabled + deleted + } + } + ` + describe('unauthenticated', () => { - let client it('throws authorization error', async () => { client = new GraphQLClient(host) - await expect(client.request(`mutation { - CreatePost( - title: "I am a post", - content: "Some content" - ) { slug } - }`)).rejects.toThrow('Not Authorised') + await expect(client.request(mutation)).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated', () => { + let headers + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) }) - describe('authenticated', () => { - let headers - let response - beforeEach(async () => { - headers = await login({ email: 'test@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - response = await client.request(`mutation { - CreatePost( - title: "A title", - content: "Some content" - ) { title, content } - }`, { headers }) - }) + it('creates a post', async () => { + const expected = { + CreatePost: { + title: 'I am a title', + content: 'Some content' + } + } + await expect(client.request(mutation)).resolves.toMatchObject(expected) + }) - it('creates a post', () => { - expect(response).toEqual({ CreatePost: { title: 'A title', content: 'Some content' } }) - }) - - it('assigns the authenticated user as author', async () => { - const { User } = await client.request(`{ + it('assigns the authenticated user as author', async () => { + await client.request(mutation) + const { User } = await client.request(`{ User(email:"test@example.org") { contributions { title } } }`, { headers }) - expect(User).toEqual([ { contributions: [ { title: 'A title' } ] } ]) + expect(User).toEqual([ { contributions: [ { title: 'I am a title' } ] } ]) + }) + + describe('disabled and deleted', () => { + it('initially false', async () => { + const expected = { CreatePost: { disabled: false, deleted: false } } + await expect(client.request(mutation)).resolves.toMatchObject(expected) }) }) }) }) + +describe('UpdatePost', () => { + const mutation = ` + mutation($id: ID!, $content: String) { + UpdatePost(id: $id, content: $content) { + id + content + } + } + ` + + let variables = { + id: 'p1', + content: 'New content' + } + + beforeEach(async () => { + const asAuthor = Factory() + await asAuthor.create('User', { + email: 'author@example.org', + password: '1234' + }) + await asAuthor.authenticateAs({ + email: 'author@example.org', + password: '1234' + }) + await asAuthor.create('Post', { + id: 'p1', + content: 'Old content' + }) + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated but not the author', () => { + let headers + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorization error', async () => { + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated as author', () => { + let headers + beforeEach(async () => { + headers = await login({ email: 'author@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('updates a post', async () => { + const expected = { UpdatePost: { id: 'p1', content: 'New content' } } + await expect(client.request(mutation, variables)).resolves.toEqual(expected) + }) + }) +}) + +describe('DeletePost', () => { + const mutation = ` + mutation($id: ID!) { + DeletePost(id: $id) { + id + content + } + } + ` + + let variables = { + id: 'p1' + } + + beforeEach(async () => { + const asAuthor = Factory() + await asAuthor.create('User', { + email: 'author@example.org', + password: '1234' + }) + await asAuthor.authenticateAs({ + email: 'author@example.org', + password: '1234' + }) + await asAuthor.create('Post', { + id: 'p1', + content: 'To be deleted' + }) + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated but not the author', () => { + let headers + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorization error', async () => { + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated as author', () => { + let headers + beforeEach(async () => { + headers = await login({ email: 'author@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('deletes a post', async () => { + const expected = { DeletePost: { id: 'p1', content: 'To be deleted' } } + await expect(client.request(mutation, variables)).resolves.toEqual(expected) + }) + }) +}) diff --git a/src/seed/seed-db.js b/src/seed/seed-db.js index b2ee8fbdb..ed46a5716 100644 --- a/src/seed/seed-db.js +++ b/src/seed/seed-db.js @@ -82,12 +82,12 @@ import Factory from './factories' await Promise.all([ asAdmin.create('Post', { id: 'p0' }), asModerator.create('Post', { id: 'p1' }), - asUser.create('Post', { id: 'p2' }), + asUser.create('Post', { id: 'p2', deleted: true }), asTick.create('Post', { id: 'p3' }), asTrick.create('Post', { id: 'p4' }), asTrack.create('Post', { id: 'p5' }), asAdmin.create('Post', { id: 'p6' }), - asModerator.create('Post', { id: 'p7' }), + asModerator.create('Post', { id: 'p7', disabled: true }), asUser.create('Post', { id: 'p8' }), asTick.create('Post', { id: 'p9' }), asTrick.create('Post', { id: 'p10' }),