diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index df87f743e..31efb9316 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -122,7 +122,7 @@ const permissions = shield( embed: allow, Category: allow, Tag: allow, - Report: isModerator, + reports: isModerator, statistics: allow, currentUser: allow, Post: or(onlyEnabledContent, isModerator), diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index 0ecb6c115..1dbd36b72 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -57,11 +57,37 @@ const validateUpdatePost = async (resolve, root, args, context, info) => { return validatePost(resolve, root, args, context, info) } +const validateReport = async (resolve, root, args, context, info) => { + const { resourceId } = args + const { user, driver } = context + if (resourceId === user.id) throw new Error('You cannot report yourself!') + const session = driver.session() + const reportQueryRes = await session.run( + ` + MATCH (:User {id:$submitterId})-[:REPORTED]->(resource {id:$resourceId}) + RETURN labels(resource)[0] as label + `, + { + resourceId, + submitterId: user.id, + }, + ) + const [existingReportedResource] = reportQueryRes.records.map(record => { + return { + label: record.get('label'), + } + }) + + if (existingReportedResource) throw new Error(`${existingReportedResource.label}`) + return resolve(root, args, context, info) +} + export default { Mutation: { CreateComment: validateCommentCreation, UpdateComment: validateUpdateComment, CreatePost: validatePost, UpdatePost: validateUpdatePost, + report: validateReport, }, } diff --git a/backend/src/middleware/xssMiddleware.js b/backend/src/middleware/xssMiddleware.js index f98ab9d61..9b4e3e759 100644 --- a/backend/src/middleware/xssMiddleware.js +++ b/backend/src/middleware/xssMiddleware.js @@ -85,7 +85,7 @@ function clean(dirty) { return dirty } -const fields = ['content', 'contentExcerpt'] +const fields = ['content', 'contentExcerpt', 'reasonDescription'] export default { Mutation: async (resolve, root, args, context, info) => { diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index 4d1b9574e..95fa9ef61 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -23,6 +23,7 @@ export default applyScalars( 'Location', 'SocialMedia', 'NOTIFIED', + 'REPORTED', ], // add 'User' here as soon as possible }, @@ -35,7 +36,6 @@ export default applyScalars( 'Notfication', 'Post', 'Comment', - 'Report', 'Statistics', 'LoggedInUser', 'Location', @@ -43,6 +43,7 @@ export default applyScalars( 'User', 'EMOTED', 'NOTIFIED', + 'REPORTED', ], // add 'User' here as soon as possible }, diff --git a/backend/src/schema/resolvers/reports.js b/backend/src/schema/resolvers/reports.js index 79cae032b..48677429a 100644 --- a/backend/src/schema/resolvers/reports.js +++ b/backend/src/schema/resolvers/reports.js @@ -1,55 +1,76 @@ -import uuid from 'uuid/v4' - export default { Mutation: { - report: async (parent, { id, description }, { driver, req, user }, resolveInfo) => { - const reportId = uuid() + report: async (_parent, params, { driver, user }, _resolveInfo) => { + let createdRelationshipWithNestedAttributes + const { resourceId, reasonCategory, reasonDescription } = params const session = driver.session() - const reportData = { - id: reportId, - createdAt: new Date().toISOString(), - description: description, - } - - const reportQueryRes = await session.run( - ` - match (u:User {id:$submitterId}) -[:REPORTED]->(report)-[:REPORTED]-> (resource {id: $resourceId}) - return labels(resource)[0] as label - `, - { - resourceId: id, - submitterId: user.id, - }, - ) - const [rep] = reportQueryRes.records.map(record => { - return { - label: record.get('label'), - } + const writeTxResultPromise = session.writeTransaction(async txc => { + const reportRelationshipTransactionResponse = await txc.run( + ` + MATCH (submitter:User {id: $submitterId}) + MATCH (resource {id: $resourceId}) + WHERE resource:User OR resource:Comment OR resource:Post + CREATE (resource)<-[report:REPORTED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter) + RETURN report, submitter, resource, labels(resource)[0] as type + `, + { + resourceId, + submitterId: user.id, + createdAt: new Date().toISOString(), + reasonCategory, + reasonDescription, + }, + ) + return reportRelationshipTransactionResponse.records.map(record => ({ + report: record.get('report'), + submitter: record.get('submitter'), + resource: record.get('resource').properties, + type: record.get('type'), + })) }) - - if (rep) { - throw new Error(rep.label) + try { + const txResult = await writeTxResultPromise + if (!txResult[0]) return null + const { report, submitter, resource, type } = txResult[0] + createdRelationshipWithNestedAttributes = { + ...report.properties, + post: null, + comment: null, + user: null, + submitter: submitter.properties, + type, + } + switch (type) { + case 'Post': + createdRelationshipWithNestedAttributes.post = resource + break + case 'Comment': + createdRelationshipWithNestedAttributes.comment = resource + break + case 'User': + createdRelationshipWithNestedAttributes.user = resource + break + } + } finally { + session.close() } + return createdRelationshipWithNestedAttributes + }, + }, + Query: { + reports: async (_parent, _params, { driver }, _resolveInfo) => { + const session = driver.session() const res = await session.run( ` - MATCH (submitter:User {id: $userId}) - MATCH (resource {id: $resourceId}) - WHERE resource:User OR resource:Comment OR resource:Post - MERGE (report:Report {id: {reportData}.id }) - MERGE (resource)<-[:REPORTED]-(report) - MERGE (report)<-[:REPORTED]-(submitter) - RETURN report, submitter, resource, labels(resource)[0] as type + MATCH (submitter:User)-[report:REPORTED]->(resource) + WHERE resource:User OR resource:Comment OR resource:Post + RETURN report, submitter, resource, labels(resource)[0] as type `, - { - resourceId: id, - userId: user.id, - reportData, - }, + {}, ) - session.close() - const [dbResponse] = res.records.map(r => { + const dbResponse = res.records.map(r => { return { report: r.get('report'), submitter: r.get('submitter'), @@ -58,27 +79,33 @@ export default { } }) if (!dbResponse) return null - const { report, submitter, resource, type } = dbResponse - const 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 - } + const response = [] + dbResponse.forEach(ele => { + const { report, submitter, resource, type } = ele + + const responseEle = { + ...report.properties, + post: null, + comment: null, + user: null, + submitter: submitter.properties, + type, + } + + switch (type) { + case 'Post': + responseEle.post = resource.properties + break + case 'Comment': + responseEle.comment = resource.properties + break + case 'User': + responseEle.user = resource.properties + break + } + response.push(responseEle) + }) return response }, diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js index 512d8d956..4022be1b1 100644 --- a/backend/src/schema/resolvers/reports.spec.js +++ b/backend/src/schema/resolvers/reports.spec.js @@ -1,35 +1,73 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' -import { neode } from '../../bootstrap/neo4j' +import { host, login, gql } from '../../jest/helpers' +import { getDriver, neode } from '../../bootstrap/neo4j' +import { createTestClient } from 'apollo-server-testing' +import createServer from '../.././server' const factory = Factory() const instance = neode() +const driver = getDriver() -describe('report', () => { - let mutation +describe('report mutation', () => { + let reportMutation let headers - let returnedObject + let client let variables let createPostVariables let user const categoryIds = ['cat9'] + const action = () => { + reportMutation = gql` + mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { + report( + resourceId: $resourceId + reasonCategory: $reasonCategory + reasonDescription: $reasonDescription + ) { + createdAt + reasonCategory + reasonDescription + type + submitter { + email + } + user { + name + } + post { + title + } + comment { + content + } + } + } + ` + client = new GraphQLClient(host, { + headers, + }) + return client.request(reportMutation, variables) + } + beforeEach(async () => { - returnedObject = '{ description }' variables = { - id: 'whatever', + resourceId: 'whatever', + reasonCategory: 'other', + reasonDescription: 'Violates code of conduct !!!', } headers = {} user = await factory.create('User', { + id: 'u1', + role: 'user', email: 'test@example.org', password: '1234', - id: 'u1', }) await factory.create('User', { id: 'u2', - name: 'abusive-user', role: 'user', + name: 'abusive-user', email: 'abusive-user@example.org', }) await instance.create('Category', { @@ -43,59 +81,57 @@ describe('report', () => { await factory.cleanDatabase() }) - let client - const action = () => { - mutation = ` - mutation($id: ID!) { - report( - 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', () => { - beforeEach(async () => { - headers = await login({ - email: 'test@example.org', - password: '1234', + describe('authenticated', () => { + beforeEach(async () => { + headers = await login({ + email: 'test@example.org', + password: '1234', + }) + }) + + describe('invalid resource id', () => { + it('returns null', async () => { + await expect(action()).resolves.toEqual({ + report: null, }) }) + }) - describe('invalid resource id', () => { - it('returns null', async () => { - await expect(action()).resolves.toEqual({ - report: null, - }) - }) - }) - - describe('valid resource id', () => { + describe('valid resource id', () => { + describe('reported resource is a user', () => { beforeEach(async () => { variables = { - id: 'u2', + ...variables, + resourceId: 'u2', } }) - /* - it('creates a report', async () => { - await expect(action()).resolves.toEqual({ - type: null, - }) - }) - */ + + it('returns type "User"', async () => { + await expect(action()).resolves.toMatchObject({ + report: { + type: 'User', + }, + }) + }) + + it('returns resource in user attribute', async () => { + await expect(action()).resolves.toMatchObject({ + report: { + user: { + name: 'abusive-user', + }, + }, + }) + }) + it('returns the submitter', async () => { - returnedObject = '{ submitter { email } }' - await expect(action()).resolves.toEqual({ + await expect(action()).resolves.toMatchObject({ report: { submitter: { email: 'test@example.org', @@ -104,138 +140,382 @@ describe('report', () => { }) }) - 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', - }, - }, - }) + it('returns a date', async () => { + await expect(action()).resolves.toMatchObject({ + report: { + createdAt: expect.any(String), + }, }) }) - describe('reported resource is a post', () => { - beforeEach(async () => { - await factory.create('Post', { - author: user, - id: 'p23', - title: 'Matt and Robert having a pair-programming', - categoryIds, - }) - 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, - }, - }) + it('returns the reason category', async () => { + variables = { + ...variables, + reasonCategory: 'criminal_behavior_violation_german_law', + } + await expect(action()).resolves.toMatchObject({ + report: { + reasonCategory: 'criminal_behavior_violation_german_law', + }, }) }) - /* An der Stelle würde ich den p23 noch mal prüfen, diesmal muss aber eine error meldung kommen. + it('gives an error if the reason category is not in enum "ReasonCategory"', async () => { + variables = { + ...variables, + reasonCategory: 'my_category', + } + await expect(action()).rejects.toThrow( + 'got invalid value "my_category"; Expected type ReasonCategory', + ) + }) + + it('returns the reason description', async () => { + variables = { + ...variables, + reasonDescription: 'My reason!', + } + await expect(action()).resolves.toMatchObject({ + report: { + reasonDescription: 'My reason!', + }, + }) + }) + + it('sanitize the reason description', async () => { + variables = { + ...variables, + reasonDescription: 'My reason !', + } + await expect(action()).resolves.toMatchObject({ + report: { + reasonDescription: 'My reason !', + }, + }) + }) + }) + + describe('reported resource is a post', () => { + beforeEach(async () => { + await factory.create('Post', { + author: user, + id: 'p23', + title: 'Matt and Robert having a pair-programming', + categoryIds, + }) + variables = { + ...variables, + resourceId: 'p23', + } + }) + + it('returns type "Post"', async () => { + await expect(action()).resolves.toMatchObject({ + report: { + type: 'Post', + }, + }) + }) + + it('returns resource in post attribute', async () => { + await expect(action()).resolves.toMatchObject({ + report: { + post: { + title: 'Matt and Robert having a pair-programming', + }, + }, + }) + }) + + it('returns null in user attribute', async () => { + await expect(action()).resolves.toMatchObject({ + report: { + user: null, + }, + }) + }) + }) + + /* An der Stelle würde ich den p23 noch mal prüfen, diesmal muss aber eine error meldung kommen. At this point I would check the p23 again, but this time there must be an error message. */ - describe('reported resource is a comment', () => { - beforeEach(async () => { - createPostVariables = { - id: 'p1', - title: 'post to comment on', - content: 'please comment on me', - categoryIds, - } - await factory.create('Post', { ...createPostVariables, author: user }) - await factory.create('Comment', { - author: user, - postId: 'p1', - id: 'c34', - content: 'Robert getting tired.', - }) - variables = { - id: 'c34', - } + describe('reported resource is a comment', () => { + beforeEach(async () => { + createPostVariables = { + id: 'p1', + title: 'post to comment on', + content: 'please comment on me', + categoryIds, + } + await factory.create('Post', { ...createPostVariables, author: user }) + await factory.create('Comment', { + author: user, + postId: 'p1', + id: 'c34', + content: 'Robert getting tired.', }) + variables = { + ...variables, + resourceId: '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.', - }, - }, - }) + it('returns type "Comment"', async () => { + await expect(action()).resolves.toMatchObject({ + report: { + type: 'Comment', + }, }) }) - /* An der Stelle würde ich den c34 noch mal prüfen, diesmal muss aber eine error meldung kommen. + it('returns resource in comment attribute', async () => { + await expect(action()).resolves.toMatchObject({ + report: { + comment: { + content: 'Robert getting tired.', + }, + }, + }) + }) + }) + + /* An der Stelle würde ich den c34 noch mal prüfen, diesmal muss aber eine error meldung kommen. At this point I would check the c34 again, but this time there must be an error message. */ - 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, - }) + describe('reported resource is a tag', () => { + beforeEach(async () => { + await factory.create('Tag', { + id: 't23', }) + variables = { + ...variables, + resourceId: 't23', + } }) - /* An der Stelle würde ich den t23 noch mal prüfen, diesmal muss aber eine error meldung kommen. - At this point I would check the t23 again, but this time there must be an error message. */ + it('returns null', async () => { + await expect(action()).resolves.toMatchObject({ + report: null, + }) + }) }) + + /* An der Stelle würde ich den t23 noch mal prüfen, diesmal muss aber eine error meldung kommen. + At this point I would check the t23 again, but this time there must be an error message. */ + }) + }) +}) + +describe('reports query', () => { + let query, mutate, authenticatedUser, moderator, user, author + const categoryIds = ['cat9'] + + const reportMutation = gql` + mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { + report( + resourceId: $resourceId + reasonCategory: $reasonCategory + reasonDescription: $reasonDescription + ) { + type + } + } + ` + const reportsQuery = gql` + query { + reports(orderBy: createdAt_desc) { + createdAt + reasonCategory + reasonDescription + submitter { + id + } + type + user { + id + } + post { + id + } + comment { + id + } + } + } + ` + + beforeAll(async () => { + await factory.cleanDatabase() + const { server } = createServer({ + context: () => { + return { + driver, + user: authenticatedUser, + } + }, + }) + query = createTestClient(server).query + mutate = createTestClient(server).mutate + }) + + beforeEach(async () => { + authenticatedUser = null + + moderator = await factory.create('User', { + id: 'mod1', + role: 'moderator', + email: 'moderator@example.org', + password: '1234', + }) + user = await factory.create('User', { + id: 'user1', + role: 'user', + email: 'test@example.org', + password: '1234', + }) + author = await factory.create('User', { + id: 'auth1', + role: 'user', + name: 'abusive-user', + email: 'abusive-user@example.org', + }) + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) + + await Promise.all([ + factory.create('Post', { + author, + id: 'p1', + categoryIds, + content: 'Interesting Knowledge', + }), + factory.create('Post', { + author: moderator, + id: 'p2', + categoryIds, + content: 'More things to do …', + }), + factory.create('Post', { + author: user, + id: 'p3', + categoryIds, + content: 'I am at school …', + }), + ]) + await Promise.all([ + factory.create('Comment', { + author: user, + id: 'c1', + postId: 'p1', + }), + ]) + + authenticatedUser = await user.toJson() + await Promise.all([ + mutate({ + mutation: reportMutation, + variables: { + resourceId: 'p1', + reasonCategory: 'other', + reasonDescription: 'This comment is bigoted', + }, + }), + mutate({ + mutation: reportMutation, + variables: { + resourceId: 'c1', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This post is bigoted', + }, + }), + mutate({ + mutation: reportMutation, + variables: { + resourceId: 'auth1', + reasonCategory: 'doxing', + reasonDescription: 'This user is harassing me with bigoted remarks', + }, + }), + ]) + authenticatedUser = null + }) + + afterEach(async () => { + await factory.cleanDatabase() + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + expect(query({ query: reportsQuery })).resolves.toMatchObject({ + data: { reports: null }, + errors: [{ message: 'Not Authorised!' }], + }) + }) + + it('role "user" gets no reports', async () => { + authenticatedUser = await user.toJson() + expect(query({ query: reportsQuery })).resolves.toMatchObject({ + data: { reports: null }, + errors: [{ message: 'Not Authorised!' }], + }) + }) + + it('role "moderator" gets reports', async () => { + const expected = { + // to check 'orderBy: createdAt_desc' is not possible here, because 'createdAt' does not differ + reports: expect.arrayContaining([ + expect.objectContaining({ + createdAt: expect.any(String), + reasonCategory: 'doxing', + reasonDescription: 'This user is harassing me with bigoted remarks', + submitter: expect.objectContaining({ + id: 'user1', + }), + type: 'User', + user: expect.objectContaining({ + id: 'auth1', + }), + post: null, + comment: null, + }), + expect.objectContaining({ + createdAt: expect.any(String), + reasonCategory: 'other', + reasonDescription: 'This comment is bigoted', + submitter: expect.objectContaining({ + id: 'user1', + }), + type: 'Post', + user: null, + post: expect.objectContaining({ + id: 'p1', + }), + comment: null, + }), + expect.objectContaining({ + createdAt: expect.any(String), + reasonCategory: 'discrimination_etc', + reasonDescription: 'This post is bigoted', + submitter: expect.objectContaining({ + id: 'user1', + }), + type: 'Comment', + user: null, + post: null, + comment: expect.objectContaining({ + id: 'c1', + }), + }), + ]), + } + + authenticatedUser = await moderator.toJson() + const { data } = await query({ query: reportsQuery }) + expect(data).toEqual(expected) }) }) }) diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index 06d0e061b..27fd2206c 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -24,7 +24,6 @@ type Mutation { changePassword(oldPassword: String!, newPassword: String!): String! requestPasswordReset(email: String!): Boolean! resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean! - report(id: ID!, description: String): Report disable(id: ID!): ID enable(id: ID!): ID # Shout the given Type and ID @@ -35,18 +34,6 @@ type Mutation { unfollowUser(id: ID!): User } -type Report { - id: ID! - submitter: User @relation(name: "REPORTED", direction: "IN") - description: String - type: String! - @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]") - createdAt: String - comment: Comment @relation(name: "REPORTED", direction: "OUT") - post: Post @relation(name: "REPORTED", direction: "OUT") - user: User @relation(name: "REPORTED", direction: "OUT") -} - enum Deletable { Post Comment diff --git a/backend/src/schema/types/type/REPORTED.gql b/backend/src/schema/types/type/REPORTED.gql new file mode 100644 index 000000000..7455de8c7 --- /dev/null +++ b/backend/src/schema/types/type/REPORTED.gql @@ -0,0 +1,42 @@ +type REPORTED { + createdAt: String + reasonCategory: ReasonCategory + reasonDescription: String + submitter: User + @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN user") + # not yet supported + # resource: ReportResource + # @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN resource") + type: String + @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN labels(resource)[0]") + user: User + post: Post + comment: Comment +} + +# this list equals the strings of an array in file "webapp/constants/modals.js" +enum ReasonCategory { + other + discrimination_etc + pornographic_content_links + glorific_trivia_of_cruel_inhuman_acts + doxing + intentional_intimidation_stalking_persecution + advert_products_services_commercial + criminal_behavior_violation_german_law +} + +# not yet supported +# union ReportResource = User | Post | Comment + +enum ReportOrdering { + createdAt_desc +} + +type Query { + reports(orderBy: ReportOrdering): [REPORTED] +} + +type Mutation { + report(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): REPORTED +} diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index 1c57dd19b..76fbb4875 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -647,10 +647,15 @@ import { gql } from '../jest/helpers' ]) authenticatedUser = null + // There is no error logged or the 'try' fails if this mutation is wrong. Why? const reportMutation = gql` - mutation($id: ID!, $description: String!) { - report(description: $description, id: $id) { - id + mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { + report( + resourceId: $resourceId + reasonCategory: $reasonCategory + reasonDescription: $reasonDescription + ) { + type } } ` @@ -659,22 +664,25 @@ import { gql } from '../jest/helpers' mutate({ mutation: reportMutation, variables: { - description: 'This comment is bigoted', - id: 'c1', + resourceId: 'c1', + reasonCategory: 'other', + reasonDescription: 'This comment is bigoted', }, }), mutate({ mutation: reportMutation, variables: { - description: 'This post is bigoted', - id: 'p1', + resourceId: 'p1', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This post is bigoted', }, }), mutate({ mutation: reportMutation, variables: { - description: 'This user is harassing me with bigoted remarks', - id: 'u1', + resourceId: 'u1', + reasonCategory: 'doxing', + reasonDescription: 'This user is harassing me with bigoted remarks', }, }), ]) diff --git a/cypress/integration/common/report.js b/cypress/integration/common/report.js index c51c6b42e..c32d5e10a 100644 --- a/cypress/integration/common/report.js +++ b/cypress/integration/common/report.js @@ -107,6 +107,11 @@ Then(`I can't see the moderation menu item`, () => { When(/^I confirm the reporting dialog .*:$/, message => { cy.contains(message) // wait for element to become visible cy.get('.ds-modal').within(() => { + cy.get('.ds-radio-option-label') + .first() + .click({ + force: true + }) cy.get('button') .contains('Report') .click() @@ -114,21 +119,22 @@ When(/^I confirm the reporting dialog .*:$/, message => { }) Given('somebody reported the following posts:', table => { - table.hashes().forEach(({ id }) => { + table.hashes().forEach(({ submitterEmail, resourceId, reasonCategory, reasonDescription }) => { const submitter = { - email: `submitter${id}@example.org`, + email: submitterEmail, password: '1234' } cy.factory() .create('User', submitter) .authenticateAs(submitter) - .mutate(`mutation($id: ID!, $description: String!) { - report(description: $description, id: $id) { - id + .mutate(`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { + report(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) { + type } }`, { - id, - description: 'Offensive content' + resourceId, + reasonCategory, + reasonDescription }) }) }) diff --git a/cypress/integration/moderation/ReportContent.feature b/cypress/integration/moderation/ReportContent.feature index 0f8eec0e3..4eceb4bdf 100644 --- a/cypress/integration/moderation/ReportContent.feature +++ b/cypress/integration/moderation/ReportContent.feature @@ -50,8 +50,8 @@ Feature: Report and Moderate Scenario: Review reported content Given somebody reported the following posts: - | id | - | p1 | + | submitterEmail | resourceId | reasonCategory | reasonDescription | + | p1.submitter@example.org | p1 | discrimination_etc | Offensive content | And I am logged in with a "moderator" role When I click on the avatar menu in the top right corner And I click on "Moderation" @@ -60,8 +60,8 @@ Feature: Report and Moderate Scenario: Review reported posts of a user who's blocked a moderator Given somebody reported the following posts: - | id | - | p2 | + | submitterEmail | resourceId | reasonCategory | reasonDescription | + | p2.submitter@example.org | p2 | other | Offensive content | And my user account has the role "moderator" And there is an annoying user who has blocked me And I am logged in diff --git a/neo4j/change_report_node_to_relationship.sh b/neo4j/change_report_node_to_relationship.sh new file mode 100755 index 000000000..a17dc6fe0 --- /dev/null +++ b/neo4j/change_report_node_to_relationship.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +ENV_FILE=$(dirname "$0")/.env +[[ -f "$ENV_FILE" ]] && source "$ENV_FILE" + +if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then + echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables." + echo "Database manipulation is not possible without connecting to the database." + echo "E.g. you could \`cp .env.template .env\` unless you run the script in a docker container" +fi + +until echo 'RETURN "Connection successful" as info;' | cypher-shell +do + echo "Connecting to neo4j failed, trying again..." + sleep 1 +done + +echo " +MATCH (submitter:User)-[:REPORTED]->(report:Report)-[:REPORTED]->(resource) +DETACH DELETE report +CREATE (submitter)-[reported:REPORTED]->(resource) +SET reported.createdAt = toString(datetime()) +SET reported.reasonCategory = 'other' +SET reported.reasonDescription = '!!! Created automatically to ensure database consistency! createdAt is when the database manipulation happened.' +RETURN reported; +" | cypher-shell diff --git a/styleguide b/styleguide index d46fc1570..808b3c5a9 160000 --- a/styleguide +++ b/styleguide @@ -1 +1 @@ -Subproject commit d46fc1570c6bcea328ae4cc3a4892745edea7319 +Subproject commit 808b3c5a9523505cb80b20b50348d29ba9932845 diff --git a/webapp/components/Modal/ReportModal.spec.js b/webapp/components/Modal/ReportModal.spec.js index bcbb85931..d6350f595 100644 --- a/webapp/components/Modal/ReportModal.spec.js +++ b/webapp/components/Modal/ReportModal.spec.js @@ -20,7 +20,7 @@ describe('ReportModal.vue', () => { id: 'c43', } mocks = { - $t: jest.fn(), + $t: jest.fn(a => a), $filters: { truncate: a => a, }, @@ -29,7 +29,9 @@ describe('ReportModal.vue', () => { error: () => {}, }, $apollo: { - mutate: jest.fn().mockResolvedValue(), + mutate: jest.fn().mockResolvedValue({ + data: {}, + }), }, } }) @@ -154,6 +156,7 @@ describe('ReportModal.vue', () => { describe('click confirm button', () => { beforeEach(() => { + wrapper.find('.ds-radio-option-label').trigger('click') wrapper.find('button.confirm').trigger('click') }) diff --git a/webapp/components/Modal/ReportModal.vue b/webapp/components/Modal/ReportModal.vue index 16b6a469b..9b155e8b6 100644 --- a/webapp/components/Modal/ReportModal.vue +++ b/webapp/components/Modal/ReportModal.vue @@ -8,14 +8,36 @@

- -