diff --git a/src/middleware/permissionsMiddleware.spec.js b/src/middleware/permissionsMiddleware.spec.js index 78766aad2..89904a7bf 100644 --- a/src/middleware/permissionsMiddleware.spec.js +++ b/src/middleware/permissionsMiddleware.spec.js @@ -34,7 +34,7 @@ describe('authorization', () => { return graphQLClient.request('{User(name: "Owner") { email } }') } - describe('not logged in', async () => { + describe('not logged in', () => { it('rejects', async () => { await expect(action()).rejects.toThrow('Not Authorised!') }) diff --git a/src/middleware/softDeleteMiddleware.spec.js b/src/middleware/softDeleteMiddleware.spec.js index e9bc461f1..ed132104e 100644 --- a/src/middleware/softDeleteMiddleware.spec.js +++ b/src/middleware/softDeleteMiddleware.spec.js @@ -22,10 +22,9 @@ beforeEach(async () => { await moderatorFactory.authenticateAs({ email: 'moderator@example.org', password: '1234' }) const disableMutation = ` mutation { - disable(resource: { + disable( id: "p2" - type: contribution - }) + ) } ` await moderatorFactory.mutate(disableMutation) diff --git a/src/resolvers/moderation.js b/src/resolvers/moderation.js index db44790b9..7bc1227ff 100644 --- a/src/resolvers/moderation.js +++ b/src/resolvers/moderation.js @@ -1,30 +1,41 @@ export default { Mutation: { disable: async (object, params, { user, driver }) => { - const { resource: { id } } = params + const { id } = params const { id: userId } = user const cypher = ` MATCH (u:User {id: $userId}) - MATCH (r {id: $id}) - SET r.disabled = true - MERGE (r)<-[:DISABLED]-(u) + MATCH (resource {id: $id}) + WHERE resource:User OR resource:Comment OR resource:Post + SET resource.disabled = true + MERGE (resource)<-[:DISABLED]-(u) + RETURN resource {.id} ` const session = driver.session() const res = await session.run(cypher, { id, userId }) session.close() - return Boolean(res) + const [resource] = res.records.map((record) => { + return record.get('resource') + }) + if (!resource) return null + return resource.id }, enable: async (object, params, { user, driver }) => { - const { resource: { id } } = params + const { id } = params const cypher = ` - MATCH (r {id: $id})<-[d:DISABLED]-() - SET r.disabled = false + MATCH (resource {id: $id})<-[d:DISABLED]-() + SET resource.disabled = false DELETE d + RETURN resource {.id} ` const session = driver.session() const res = await session.run(cypher, { id }) session.close() - return Boolean(res) + const [resource] = res.records.map((record) => { + return record.get('resource') + }) + if (!resource) return null + return resource.id } } } diff --git a/src/resolvers/moderation.spec.js b/src/resolvers/moderation.spec.js index c1d4a75fe..dfbcac80f 100644 --- a/src/resolvers/moderation.spec.js +++ b/src/resolvers/moderation.spec.js @@ -14,30 +14,28 @@ const setupAuthenticateClient = (params) => { return authenticateClient } -let setup -const runSetup = async () => { - await setup.createResource() - await setup.authenticateClient() -} - +let createResource +let authenticateClient beforeEach(() => { - setup = { - createResource: () => { - }, - authenticateClient: () => { - client = new GraphQLClient(host) - } + createResource = () => {} + authenticateClient = () => { + client = new GraphQLClient(host) } }) +const setup = async () => { + await createResource() + await authenticateClient() +} + afterEach(async () => { await factory.cleanDatabase() }) describe('disable', () => { const mutation = ` - mutation($id: ID!, $type: ResourceEnum!) { - disable(resource: { id: $id, type: $type }) + mutation($id: ID!) { + disable(id: $id) } ` let variables @@ -45,8 +43,7 @@ describe('disable', () => { beforeEach(() => { // our defaul set of variables variables = { - id: 'blabla', - type: 'contribution' + id: 'blabla' } }) @@ -55,26 +52,26 @@ describe('disable', () => { } it('throws authorization error', async () => { - await runSetup() + await setup() await expect(action()).rejects.toThrow('Not Authorised') }) describe('authenticated', () => { beforeEach(() => { - setup.authenticateClient = setupAuthenticateClient({ + authenticateClient = setupAuthenticateClient({ email: 'user@example.org', password: '1234' }) }) it('throws authorization error', async () => { - await runSetup() + await setup() await expect(action()).rejects.toThrow('Not Authorised') }) describe('as moderator', () => { beforeEach(() => { - setup.authenticateClient = setupAuthenticateClient({ + authenticateClient = setupAuthenticateClient({ id: 'u7', email: 'moderator@example.org', password: '1234', @@ -82,14 +79,32 @@ describe('disable', () => { }) }) + describe('on something that is not a (Comment|Post|User) ', () => { + beforeEach(async () => { + variables = { + id: 't23' + } + createResource = () => { + return Promise.all([ + factory.create('Tag', { id: 't23' }) + ]) + } + }) + + it('returns null', async () => { + const expected = { disable: null } + await setup() + await expect(action()).resolves.toEqual(expected) + }) + }) + describe('on a comment', () => { beforeEach(async () => { variables = { - id: 'c47', - type: 'comment' + id: 'c47' } - setup.createResource = async () => { + createResource = async () => { await factory.create('User', { id: 'u45', email: 'commenter@example.org', password: '1234' }) await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' }) await Promise.all([ @@ -103,9 +118,9 @@ describe('disable', () => { } }) - it('returns true', async () => { - const expected = { disable: true } - await runSetup() + it('returns disabled resource id', async () => { + const expected = { disable: 'c47' } + await setup() await expect(action()).resolves.toEqual(expected) }) @@ -113,7 +128,7 @@ describe('disable', () => { const before = { Comment: [{ id: 'c47', disabledBy: null }] } const expected = { Comment: [{ id: 'c47', disabledBy: { id: 'u7' } }] } - await runSetup() + await setup() await expect(client.request( '{ Comment { id, disabledBy { id } } }' )).resolves.toEqual(before) @@ -127,7 +142,7 @@ describe('disable', () => { const before = { Comment: [ { id: 'c47', disabled: false } ] } const expected = { Comment: [ { id: 'c47', disabled: true } ] } - await runSetup() + await setup() await expect(client.request( '{ Comment { id disabled } }' )).resolves.toEqual(before) @@ -141,11 +156,10 @@ describe('disable', () => { describe('on a post', () => { beforeEach(async () => { variables = { - id: 'p9', - type: 'contribution' + id: 'p9' } - setup.createResource = async () => { + 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', { @@ -154,9 +168,9 @@ describe('disable', () => { } }) - it('returns true', async () => { - const expected = { disable: true } - await runSetup() + it('returns disabled resource id', async () => { + const expected = { disable: 'p9' } + await setup() await expect(action()).resolves.toEqual(expected) }) @@ -164,7 +178,7 @@ describe('disable', () => { const before = { Post: [{ id: 'p9', disabledBy: null }] } const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] } - await runSetup() + await setup() await expect(client.request( '{ Post { id, disabledBy { id } } }' )).resolves.toEqual(before) @@ -178,7 +192,7 @@ describe('disable', () => { const before = { Post: [ { id: 'p9', disabled: false } ] } const expected = { Post: [ { id: 'p9', disabled: true } ] } - await runSetup() + await setup() await expect(client.request( '{ Post { id disabled } }' )).resolves.toEqual(before) @@ -194,8 +208,8 @@ describe('disable', () => { describe('enable', () => { const mutation = ` - mutation($id: ID!, $type: ResourceEnum!) { - enable(resource: { id: $id, type: $type }) + mutation($id: ID!) { + enable(id: $id) } ` let variables @@ -207,46 +221,64 @@ describe('enable', () => { beforeEach(() => { // our defaul set of variables variables = { - id: 'blabla', - type: 'contribution' + id: 'blabla' } }) it('throws authorization error', async () => { - await runSetup() + await setup() await expect(action()).rejects.toThrow('Not Authorised') }) describe('authenticated', () => { beforeEach(() => { - setup.authenticateClient = setupAuthenticateClient({ + authenticateClient = setupAuthenticateClient({ email: 'user@example.org', password: '1234' }) }) it('throws authorization error', async () => { - await runSetup() + await setup() await expect(action()).rejects.toThrow('Not Authorised') }) describe('as moderator', () => { beforeEach(async () => { - setup.authenticateClient = setupAuthenticateClient({ + authenticateClient = setupAuthenticateClient({ role: 'moderator', email: 'someUser@example.org', password: '1234' }) }) + describe('on something that is not a (Comment|Post|User) ', () => { + beforeEach(async () => { + variables = { + id: 't23' + } + createResource = () => { + // we cannot create a :DISABLED relationship here + return Promise.all([ + factory.create('Tag', { id: 't23' }) + ]) + } + }) + + it('returns null', async () => { + const expected = { enable: null } + await setup() + await expect(action()).resolves.toEqual(expected) + }) + }) + describe('on a comment', () => { beforeEach(async () => { variables = { - id: 'c456', - type: 'comment' + id: 'c456' } - setup.createResource = async () => { + createResource = async () => { await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' }) await factory.authenticateAs({ email: 'author@example.org', password: '1234' }) await Promise.all([ @@ -260,19 +292,16 @@ describe('enable', () => { const disableMutation = ` mutation { - disable(resource: { - id: "c456" - type: comment - }) + disable(id: "c456") } ` await factory.mutate(disableMutation) // that's we want to delete } }) - it('returns true', async () => { - const expected = { enable: true } - await runSetup() + it('returns disabled resource id', async () => { + const expected = { enable: 'c456' } + await setup() await expect(action()).resolves.toEqual(expected) }) @@ -280,7 +309,7 @@ describe('enable', () => { const before = { Comment: [{ id: 'c456', disabledBy: { id: 'u123' } }] } const expected = { Comment: [{ id: 'c456', disabledBy: null }] } - await runSetup() + await setup() await expect(client.request( '{ Comment(disabled: true) { id, disabledBy { id } } }' )).resolves.toEqual(before) @@ -294,7 +323,7 @@ describe('enable', () => { const before = { Comment: [ { id: 'c456', disabled: true } ] } const expected = { Comment: [ { id: 'c456', disabled: false } ] } - await runSetup() + await setup() await expect(client.request( '{ Comment(disabled: true) { id disabled } }' )).resolves.toEqual(before) @@ -308,11 +337,10 @@ describe('enable', () => { describe('on a post', () => { beforeEach(async () => { variables = { - id: 'p9', - type: 'contribution' + id: 'p9' } - setup.createResource = async () => { + 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', { @@ -321,19 +349,16 @@ describe('enable', () => { const disableMutation = ` mutation { - disable(resource: { - id: "p9" - type: contribution - }) + disable(id: "p9") } ` await factory.mutate(disableMutation) // that's we want to delete } }) - it('returns true', async () => { - const expected = { enable: true } - await runSetup() + it('returns disabled resource id', async () => { + const expected = { enable: 'p9' } + await setup() await expect(action()).resolves.toEqual(expected) }) @@ -341,7 +366,7 @@ describe('enable', () => { const before = { Post: [{ id: 'p9', disabledBy: { id: 'u123' } }] } const expected = { Post: [{ id: 'p9', disabledBy: null }] } - await runSetup() + await setup() await expect(client.request( '{ Post(disabled: true) { id, disabledBy { id } } }' )).resolves.toEqual(before) @@ -355,7 +380,7 @@ describe('enable', () => { const before = { Post: [ { id: 'p9', disabled: true } ] } const expected = { Post: [ { id: 'p9', disabled: false } ] } - await runSetup() + await setup() await expect(client.request( '{ Post(disabled: true) { id disabled } }' )).resolves.toEqual(before) diff --git a/src/resolvers/reports.js b/src/resolvers/reports.js index c471d7b7a..fb912a557 100644 --- a/src/resolvers/reports.js +++ b/src/resolvers/reports.js @@ -2,50 +2,62 @@ import uuid from 'uuid/v4' export default { Mutation: { - report: async (parent, { resource, description }, { driver, req, user }, resolveInfo) => { - const contextId = uuid() + report: async (parent, { id, description }, { driver, req, user }, resolveInfo) => { + const reportId = uuid() const session = driver.session() - const data = { - id: contextId, - type: resource.type, + const reportData = { + id: reportId, createdAt: (new Date()).toISOString(), - description: resource.description - } - await session.run( - 'CREATE (r:Report $report) ' + - 'RETURN r.id, r.type, r.description', { - report: data - } - ) - let contentType - - switch (resource.type) { - case 'post': - case 'contribution': - contentType = 'Post' - break - case 'comment': - contentType = 'Comment' - break - case 'user': - contentType = 'User' - break + description: description } - await session.run( - `MATCH (author:User {id: $userId}), (context:${contentType} {id: $resourceId}), (report:Report {id: $contextId}) ` + - 'MERGE (report)<-[:REPORTED]-(author) ' + - 'MERGE (context)<-[:REPORTED]-(report) ' + - 'RETURN context', { - resourceId: resource.id, - userId: user.id, - contextId: contextId - } + const res = await session.run(` + MATCH (submitter:User {id: $userId}) + MATCH (resource {id: $resourceId}) + WHERE resource:User OR resource:Comment OR resource:Post + CREATE (report:Report $reportData) + MERGE (resource)<-[:REPORTED]-(report) + MERGE (report)<-[:REPORTED]-(submitter) + RETURN report, submitter, resource, labels(resource)[0] as type + `, { + resourceId: id, + userId: user.id, + reportData + } ) session.close() - // TODO: output Report compatible object - return data + const [dbResponse] = res.records.map(r => { + return { + report: r.get('report'), + submitter: r.get('submitter'), + resource: r.get('resource'), + type: r.get('type') + } + }) + if (!dbResponse) return null + const { report, submitter, resource, type } = dbResponse + + let response = { + ...report.properties, + post: null, + comment: null, + user: null, + submitter: submitter.properties, + type + } + switch (type) { + case 'Post': + response.post = resource.properties + break + case 'Comment': + response.comment = resource.properties + break + case 'User': + response.user = resource.properties + break + } + return response } } } diff --git a/src/resolvers/reports.spec.js b/src/resolvers/reports.spec.js index 253cdadcc..ae8894572 100644 --- a/src/resolvers/reports.spec.js +++ b/src/resolvers/reports.spec.js @@ -5,8 +5,17 @@ import { host, login } from '../jest/helpers' const factory = Factory() describe('report', () => { + let mutation + let headers + let returnedObject + let variables + beforeEach(async () => { + returnedObject = '{ description }' + variables = { id: 'whatever' } + headers = {} await factory.create('User', { + id: 'u1', email: 'test@example.org', password: '1234' }) @@ -22,45 +31,132 @@ describe('report', () => { await factory.cleanDatabase() }) - describe('unauthenticated', () => { - let client - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect( - client.request(`mutation { + let client + const action = () => { + mutation = ` + mutation($id: ID!) { report( - description: "I don't like this user", - resource: { - id: "u2", - type: user - } - ) { id, createdAt } - }`) - ).rejects.toThrow('Not Authorised') + id: $id, + description: "Violates code of conduct" + ) ${returnedObject} + } + ` + client = new GraphQLClient(host, { headers }) + return client.request(mutation, variables) + } + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + await expect(action()).rejects.toThrow('Not Authorised') }) 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 { - report( - description: "I don't like this user", - resource: { - id: "u2", - type: user - } - ) { id, createdAt } - }`, - { headers } - ) }) - it('creates a report', () => { - let { id, createdAt } = response.report - expect(response).toEqual({ - report: { id, createdAt } + + describe('invalid resource id', () => { + it('returns null', async () => { + await expect(action()).resolves.toEqual({ + report: null + }) + }) + }) + + describe('valid resource id', () => { + beforeEach(async () => { + variables = { id: 'u2' } + }) + + it('creates a report', async () => { + await expect(action()).resolves.toEqual({ + report: { description: 'Violates code of conduct' } + }) + }) + + it('returns the submitter', async () => { + returnedObject = '{ submitter { email } }' + await expect(action()).resolves.toEqual({ + report: { submitter: { email: 'test@example.org' } } + }) + }) + + describe('reported resource is a user', () => { + it('returns type "User"', async () => { + returnedObject = '{ type }' + await expect(action()).resolves.toEqual({ + report: { type: 'User' } + }) + }) + + it('returns resource in user attribute', async () => { + returnedObject = '{ user { name } }' + await expect(action()).resolves.toEqual({ + report: { user: { name: 'abusive-user' } } + }) + }) + }) + + describe('reported resource is a post', () => { + beforeEach(async () => { + await factory.authenticateAs({ email: 'test@example.org', password: '1234' }) + await factory.create('Post', { id: 'p23', title: 'Matt and Robert having a pair-programming' }) + variables = { id: 'p23' } + }) + + it('returns type "Post"', async () => { + returnedObject = '{ type }' + await expect(action()).resolves.toEqual({ + report: { type: 'Post' } + }) + }) + + it('returns resource in post attribute', async () => { + returnedObject = '{ post { title } }' + await expect(action()).resolves.toEqual({ + report: { post: { title: 'Matt and Robert having a pair-programming' } } + }) + }) + + it('returns null in user attribute', async () => { + returnedObject = '{ user { name } }' + await expect(action()).resolves.toEqual({ + report: { user: null } + }) + }) + }) + + describe('reported resource is a comment', () => { + beforeEach(async () => { + await factory.authenticateAs({ email: 'test@example.org', password: '1234' }) + await factory.create('Comment', { id: 'c34', content: 'Robert getting tired.' }) + variables = { id: 'c34' } + }) + + it('returns type "Comment"', async () => { + returnedObject = '{ type }' + await expect(action()).resolves.toEqual({ + report: { type: 'Comment' } + }) + }) + + it('returns resource in comment attribute', async () => { + returnedObject = '{ comment { content } }' + await expect(action()).resolves.toEqual({ + report: { comment: { content: 'Robert getting tired.' } } + }) + }) + }) + + describe('reported resource is a tag', () => { + beforeEach(async () => { + await factory.create('Tag', { id: 't23' }) + variables = { id: 't23' } + }) + + it('returns null', async () => { + await expect(action()).resolves.toEqual({ report: null }) + }) }) }) }) diff --git a/src/schema.graphql b/src/schema.graphql index d566be159..cd6573e83 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -9,7 +9,9 @@ type Mutation { "Get a JWT Token for the given Email and password" login(email: String!, password: String!): String! signup(email: String!, password: String!): Boolean! - report(resource: Resource!, description: String): Report + report(id: ID!, description: String): Report + disable(id: ID!): ID + enable(id: ID!): ID "Shout the given Type and ID" shout(id: ID!, type: ShoutTypeEnum): Boolean! @cypher(statement: """ MATCH (n {id: $id})<-[:WROTE]-(wu:User), (u:User {id: $cypherParams.currentUserId}) @@ -39,8 +41,6 @@ type Mutation { DELETE r RETURN COUNT(r) > 0 """) - disable(resource: Resource!): Boolean! - enable(resource: Resource!): Boolean! } type Statistics { @@ -59,17 +59,6 @@ scalar Date scalar Time scalar DateTime -input Resource { - id: ID!, - type: ResourceEnum! -} - -enum ResourceEnum { - contribution - comment - user -} - enum VisibilityEnum { public friends @@ -220,12 +209,12 @@ type Comment { type Report { id: ID! - reporter: User @relation(name: "REPORTED", direction: "IN") + submitter: User @relation(name: "REPORTED", direction: "IN") description: String - type: ResourceEnum! + type: String! @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]") createdAt: String comment: Comment @relation(name: "REPORTED", direction: "OUT") - contribution: Post @relation(name: "REPORTED", direction: "OUT") + post: Post @relation(name: "REPORTED", direction: "OUT") user: User @relation(name: "REPORTED", direction: "OUT") } diff --git a/src/seed/factories/reports.js b/src/seed/factories/reports.js index 4dcd479f1..ccf5299bd 100644 --- a/src/seed/factories/reports.js +++ b/src/seed/factories/reports.js @@ -3,17 +3,14 @@ import faker from 'faker' export default function create (params) { const { description = faker.lorem.sentence(), - resource: { id: resourceId, type } + id } = params return ` mutation { report( description: "${description}", - resource: { - id: "${resourceId}", - type: ${type} - } + id: "${id}", ) { id, createdAt diff --git a/src/seed/seed-db.js b/src/seed/seed-db.js index b16e9f323..e8c757db8 100644 --- a/src/seed/seed-db.js +++ b/src/seed/seed-db.js @@ -107,14 +107,7 @@ import Factory from './factories' asTick.create('Post', { id: 'p15' }) ]) - const disableMutation = ` - mutation { - disable(resource: { - id: "p11" - type: contribution - }) - } - ` + const disableMutation = 'mutation { disable( id: "p11") }' await asModerator.mutate(disableMutation) await Promise.all([ @@ -153,6 +146,26 @@ import Factory from './factories' f.relate('Post', 'Tags', { from: 'p15', to: 't3' }) ]) + await Promise.all([ + asAdmin + .shout({ id: 'p2', type: 'Post' }), + asAdmin + .shout({ id: 'p6', type: 'Post' }), + asModerator + .shout({ id: 'p0', type: 'Post' }), + asModerator + .shout({ id: 'p6', type: 'Post' }), + asUser + .shout({ id: 'p6', type: 'Post' }), + asUser + .shout({ id: 'p7', type: 'Post' }), + asTick + .shout({ id: 'p8', type: 'Post' }), + asTick + .shout({ id: 'p9', type: 'Post' }), + asTrack + .shout({ id: 'p10', type: 'Post' }) + ]) await Promise.all([ asAdmin .shout({ id: 'p2', type: 'Post' }), @@ -202,9 +215,9 @@ import Factory from './factories' ]) await Promise.all([ - asTick.create('Report', { description: 'I don\'t like this comment', resource: { id: 'c1', type: 'comment' } }), - asTrick.create('Report', { description: 'I don\'t like this post', resource: { id: 'p1', type: 'contribution' } }), - asTrack.create('Report', { description: 'I don\'t like this user', resource: { id: 'u1', type: 'user' } }) + asTick.create('Report', { description: 'I don\'t like this comment', id: 'c1' }), + asTrick.create('Report', { description: 'I don\'t like this post', id: 'p1' }), + asTrack.create('Report', { description: 'I don\'t like this user', id: 'u1' }) ]) await Promise.all([