mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #3075 from Human-Connection/3074-don’t-expose-all-properties-of-report
feat: 🍰 Expose sensitive report type to moderators only
This commit is contained in:
commit
e164104791
@ -152,6 +152,7 @@ export default shield(
|
|||||||
User: {
|
User: {
|
||||||
email: or(isMyOwn, isAdmin),
|
email: or(isMyOwn, isAdmin),
|
||||||
},
|
},
|
||||||
|
Report: isModerator,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
debug,
|
debug,
|
||||||
|
|||||||
@ -9,14 +9,6 @@ const driver = getDriver()
|
|||||||
|
|
||||||
let query, authenticatedUser, owner, anotherRegularUser, administrator, variables, moderator
|
let query, authenticatedUser, owner, anotherRegularUser, administrator, variables, moderator
|
||||||
|
|
||||||
const userQuery = gql`
|
|
||||||
query($name: String) {
|
|
||||||
User(name: $name) {
|
|
||||||
email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
describe('authorization', () => {
|
describe('authorization', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await cleanDatabase()
|
await cleanDatabase()
|
||||||
@ -30,7 +22,11 @@ describe('authorization', () => {
|
|||||||
query = createTestClient(server).query
|
query = createTestClient(server).query
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('given two existing users', () => {
|
afterEach(async () => {
|
||||||
|
await cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given an owner, an other user, an admin, a moderator', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
;[owner, anotherRegularUser, administrator, moderator] = await Promise.all([
|
;[owner, anotherRegularUser, administrator, moderator] = await Promise.all([
|
||||||
Factory.build(
|
Factory.build(
|
||||||
@ -79,15 +75,20 @@ describe('authorization', () => {
|
|||||||
variables = {}
|
variables = {}
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await cleanDatabase()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('access email address', () => {
|
describe('access email address', () => {
|
||||||
|
const userQuery = gql`
|
||||||
|
query($name: String) {
|
||||||
|
User(name: $name) {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
authenticatedUser = null
|
authenticatedUser = null
|
||||||
})
|
})
|
||||||
|
|
||||||
it("throws an error and does not expose the owner's email address", async () => {
|
it("throws an error and does not expose the owner's email address", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
query({ query: userQuery, variables: { name: 'Owner' } }),
|
query({ query: userQuery, variables: { name: 'Owner' } }),
|
||||||
@ -143,7 +144,7 @@ describe('authorization', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('administrator', () => {
|
describe('as an administrator', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
authenticatedUser = await administrator.toJson()
|
authenticatedUser = await administrator.toJson()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -58,7 +58,7 @@ const reportMutation = gql`
|
|||||||
reasonCategory: $reasonCategory
|
reasonCategory: $reasonCategory
|
||||||
reasonDescription: $reasonDescription
|
reasonDescription: $reasonDescription
|
||||||
) {
|
) {
|
||||||
id
|
reportId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -1,20 +1,10 @@
|
|||||||
const transformReturnType = record => {
|
import log from './helpers/databaseLogger'
|
||||||
return {
|
|
||||||
...record.get('review').properties,
|
|
||||||
report: record.get('report').properties,
|
|
||||||
resource: {
|
|
||||||
__typename: record.get('type'),
|
|
||||||
...record.get('resource').properties,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
review: async (_object, params, context, _resolveInfo) => {
|
review: async (_object, params, context, _resolveInfo) => {
|
||||||
const { user: moderator, driver } = context
|
const { user: moderator, driver } = context
|
||||||
|
|
||||||
let createdRelationshipWithNestedAttributes = null // return value
|
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
try {
|
try {
|
||||||
const cypher = `
|
const cypher = `
|
||||||
@ -25,10 +15,11 @@ export default {
|
|||||||
ON CREATE SET review.createdAt = $dateTime, review.updatedAt = review.createdAt
|
ON CREATE SET review.createdAt = $dateTime, review.updatedAt = review.createdAt
|
||||||
ON MATCH SET review.updatedAt = $dateTime
|
ON MATCH SET review.updatedAt = $dateTime
|
||||||
SET review.disable = $params.disable
|
SET review.disable = $params.disable
|
||||||
SET report.updatedAt = $dateTime, report.closed = $params.closed
|
SET report.updatedAt = $dateTime, report.disable = review.disable, report.closed = $params.closed
|
||||||
SET resource.disabled = review.disable
|
SET resource.disabled = report.disable
|
||||||
|
|
||||||
RETURN review, report, resource, labels(resource)[0] AS type
|
WITH review, report, resource {.*, __typename: labels(resource)[0]} AS finalResource
|
||||||
|
RETURN review {.*, report: properties(report), resource: properties(finalResource)}
|
||||||
`
|
`
|
||||||
const reviewWriteTxResultPromise = session.writeTransaction(async txc => {
|
const reviewWriteTxResultPromise = session.writeTransaction(async txc => {
|
||||||
const reviewTransactionResponse = await txc.run(cypher, {
|
const reviewTransactionResponse = await txc.run(cypher, {
|
||||||
@ -36,16 +27,14 @@ export default {
|
|||||||
moderatorId: moderator.id,
|
moderatorId: moderator.id,
|
||||||
dateTime: new Date().toISOString(),
|
dateTime: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
return reviewTransactionResponse.records.map(transformReturnType)
|
log(reviewTransactionResponse)
|
||||||
|
return reviewTransactionResponse.records.map(record => record.get('review'))
|
||||||
})
|
})
|
||||||
const txResult = await reviewWriteTxResultPromise
|
const [reviewed] = await reviewWriteTxResultPromise
|
||||||
if (!txResult[0]) return null
|
return reviewed || null
|
||||||
createdRelationshipWithNestedAttributes = txResult[0]
|
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdRelationshipWithNestedAttributes
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,13 @@
|
|||||||
import log from './helpers/databaseLogger'
|
import log from './helpers/databaseLogger'
|
||||||
|
|
||||||
const transformReturnType = record => {
|
|
||||||
return {
|
|
||||||
...record.get('report').properties,
|
|
||||||
resource: {
|
|
||||||
__typename: record.get('type'),
|
|
||||||
...record.get('resource').properties,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
fileReport: async (_parent, params, context, _resolveInfo) => {
|
fileReport: async (_parent, params, context, _resolveInfo) => {
|
||||||
const { resourceId, reasonCategory, reasonDescription } = params
|
const { resourceId, reasonCategory, reasonDescription } = params
|
||||||
const { driver, user } = context
|
const { driver, user } = context
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
const reportWriteTxResultPromise = session.writeTransaction(async transaction => {
|
const fileReportWriteTxResultPromise = session.writeTransaction(async transaction => {
|
||||||
const reportTransactionResponse = await transaction.run(
|
const fileReportTransactionResponse = await transaction.run(
|
||||||
`
|
`
|
||||||
MATCH (submitter:User {id: $submitterId})
|
MATCH (submitter:User {id: $submitterId})
|
||||||
MATCH (resource {id: $resourceId})
|
MATCH (resource {id: $resourceId})
|
||||||
@ -27,7 +17,8 @@ export default {
|
|||||||
WITH submitter, resource, report
|
WITH submitter, resource, report
|
||||||
CREATE (report)<-[filed:FILED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter)
|
CREATE (report)<-[filed:FILED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter)
|
||||||
|
|
||||||
RETURN report, resource, labels(resource)[0] AS type
|
WITH filed, report, resource {.*, __typename: labels(resource)[0]} AS finalResource
|
||||||
|
RETURN filed {.*, reportId: report.id, resource: properties(finalResource)} AS filedReport
|
||||||
`,
|
`,
|
||||||
{
|
{
|
||||||
resourceId,
|
resourceId,
|
||||||
@ -37,13 +28,12 @@ export default {
|
|||||||
reasonDescription,
|
reasonDescription,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
log(reportTransactionResponse)
|
log(fileReportTransactionResponse)
|
||||||
return reportTransactionResponse.records.map(transformReturnType)
|
return fileReportTransactionResponse.records.map(record => record.get('filedReport'))
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const [createdRelationshipWithNestedAttributes] = await reportWriteTxResultPromise
|
const [filedReport] = await fileReportWriteTxResultPromise
|
||||||
if (!createdRelationshipWithNestedAttributes) return null
|
return filedReport || null
|
||||||
return createdRelationshipWithNestedAttributes
|
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
@ -76,14 +66,24 @@ export default {
|
|||||||
filterClause = ''
|
filterClause = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.closed) filterClause = 'AND report.closed = true'
|
switch (params.closed) {
|
||||||
|
case true:
|
||||||
|
filterClause = 'AND report.closed = true'
|
||||||
|
break
|
||||||
|
case false:
|
||||||
|
filterClause = 'AND report.closed = false'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
const offset =
|
const offset =
|
||||||
params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : ''
|
params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : ''
|
||||||
const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : ''
|
const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : ''
|
||||||
|
|
||||||
const reportReadTxPromise = session.readTransaction(async transaction => {
|
const reportsReadTxPromise = session.readTransaction(async transaction => {
|
||||||
const allReportsTransactionResponse = await transaction.run(
|
const reportsTransactionResponse = await transaction.run(
|
||||||
|
// !!! this Cypher query returns multiple reports on the same resource! i will create an issue for refactoring (bug fixing)
|
||||||
`
|
`
|
||||||
MATCH (report:Report)-[:BELONGS_TO]->(resource)
|
MATCH (report:Report)-[:BELONGS_TO]->(resource)
|
||||||
WHERE (resource:User OR resource:Post OR resource:Comment)
|
WHERE (resource:User OR resource:Post OR resource:Comment)
|
||||||
@ -101,11 +101,11 @@ export default {
|
|||||||
${offset} ${limit}
|
${offset} ${limit}
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
log(allReportsTransactionResponse)
|
log(reportsTransactionResponse)
|
||||||
return allReportsTransactionResponse.records.map(record => record.get('report'))
|
return reportsTransactionResponse.records.map(record => record.get('report'))
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const reports = await reportReadTxPromise
|
const reports = await reportsReadTxPromise
|
||||||
return reports
|
return reports
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
|
|||||||
@ -10,18 +10,17 @@ const driver = getDriver()
|
|||||||
describe('file a report on a resource', () => {
|
describe('file a report on a resource', () => {
|
||||||
let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser, otherReportingUser
|
let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser, otherReportingUser
|
||||||
const categoryIds = ['cat9']
|
const categoryIds = ['cat9']
|
||||||
const reportMutation = gql`
|
const fileReportMutation = gql`
|
||||||
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||||
fileReport(
|
fileReport(
|
||||||
resourceId: $resourceId
|
resourceId: $resourceId
|
||||||
reasonCategory: $reasonCategory
|
reasonCategory: $reasonCategory
|
||||||
reasonDescription: $reasonDescription
|
reasonDescription: $reasonDescription
|
||||||
) {
|
) {
|
||||||
id
|
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
reasonCategory
|
||||||
closed
|
reasonDescription
|
||||||
rule
|
reportId
|
||||||
resource {
|
resource {
|
||||||
__typename
|
__typename
|
||||||
... on User {
|
... on User {
|
||||||
@ -34,6 +33,35 @@ describe('file a report on a resource', () => {
|
|||||||
content
|
content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const variables = {
|
||||||
|
resourceId: 'invalid',
|
||||||
|
reasonCategory: 'other',
|
||||||
|
reasonDescription: 'Violates code of conduct !!!',
|
||||||
|
}
|
||||||
|
const reportsQuery = gql`
|
||||||
|
query($closed: Boolean) {
|
||||||
|
reports(orderBy: createdAt_desc, closed: $closed) {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
rule
|
||||||
|
disable
|
||||||
|
closed
|
||||||
|
resource {
|
||||||
|
__typename
|
||||||
|
... on User {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
... on Post {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
... on Comment {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
filed {
|
filed {
|
||||||
submitter {
|
submitter {
|
||||||
id
|
id
|
||||||
@ -45,11 +73,31 @@ describe('file a report on a resource', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const variables = {
|
const reviewMutation = gql`
|
||||||
resourceId: 'whatever',
|
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
|
||||||
reasonCategory: 'other',
|
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
|
||||||
reasonDescription: 'Violates code of conduct !!!',
|
createdAt
|
||||||
|
resource {
|
||||||
|
__typename
|
||||||
|
... on User {
|
||||||
|
id
|
||||||
|
disabled
|
||||||
}
|
}
|
||||||
|
... on Post {
|
||||||
|
id
|
||||||
|
disabled
|
||||||
|
}
|
||||||
|
... on Comment {
|
||||||
|
id
|
||||||
|
disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report {
|
||||||
|
disable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await cleanDatabase()
|
await cleanDatabase()
|
||||||
@ -74,7 +122,7 @@ describe('file a report on a resource', () => {
|
|||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
authenticatedUser = null
|
authenticatedUser = null
|
||||||
await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({
|
await expect(mutate({ mutation: fileReportMutation, variables })).resolves.toMatchObject({
|
||||||
data: { fileReport: null },
|
data: { fileReport: null },
|
||||||
errors: [{ message: 'Not Authorised!' }],
|
errors: [{ message: 'Not Authorised!' }],
|
||||||
})
|
})
|
||||||
@ -94,6 +142,17 @@ describe('file a report on a resource', () => {
|
|||||||
password: '1234',
|
password: '1234',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
moderator = await Factory.build(
|
||||||
|
'user',
|
||||||
|
{
|
||||||
|
id: 'moderator-id',
|
||||||
|
role: 'moderator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'moderator@example.org',
|
||||||
|
password: '1234',
|
||||||
|
},
|
||||||
|
)
|
||||||
otherReportingUser = await Factory.build(
|
otherReportingUser = await Factory.build(
|
||||||
'user',
|
'user',
|
||||||
{
|
{
|
||||||
@ -127,7 +186,7 @@ describe('file a report on a resource', () => {
|
|||||||
|
|
||||||
describe('invalid resource id', () => {
|
describe('invalid resource id', () => {
|
||||||
it('returns null', async () => {
|
it('returns null', async () => {
|
||||||
await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({
|
await expect(mutate({ mutation: fileReportMutation, variables })).resolves.toMatchObject({
|
||||||
data: { fileReport: null },
|
data: { fileReport: null },
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
@ -139,47 +198,112 @@ describe('file a report on a resource', () => {
|
|||||||
it('which belongs to resource', async () => {
|
it('which belongs to resource', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
fileReport: {
|
fileReport: {
|
||||||
id: expect.any(String),
|
reportId: expect.any(String),
|
||||||
|
resource: {
|
||||||
|
name: 'abusive-user',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates only one report for multiple reports on the same resource', async () => {
|
it('only one report for multiple reports on the same resource', async () => {
|
||||||
const firstReport = await mutate({
|
const firstReport = await mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
})
|
})
|
||||||
authenticatedUser = await otherReportingUser.toJson()
|
authenticatedUser = await otherReportingUser.toJson()
|
||||||
const secondReport = await mutate({
|
const secondReport = await mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
})
|
})
|
||||||
expect(firstReport.data.fileReport.id).toEqual(secondReport.data.fileReport.id)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns the rule for how the report was decided', async () => {
|
expect(firstReport.data.fileReport.reportId).toEqual(
|
||||||
await expect(
|
secondReport.data.fileReport.reportId,
|
||||||
mutate({
|
)
|
||||||
mutation: reportMutation,
|
})
|
||||||
|
|
||||||
|
describe('report properties are set correctly', () => {
|
||||||
|
const reportsCypherQuery =
|
||||||
|
'MATCH (resource:User {id: $resourceId})<-[:BELONGS_TO]-(report:Report {closed: false})<-[filed:FILED]-(user:User {id: $currentUserId}) RETURN report'
|
||||||
|
|
||||||
|
it('with the rule for how the report will be decided', async () => {
|
||||||
|
await mutate({
|
||||||
|
mutation: fileReportMutation,
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
}),
|
})
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: {
|
const reportsCypherQueryResponse = await instance.cypher(reportsCypherQuery, {
|
||||||
fileReport: {
|
resourceId: 'abusive-user-id',
|
||||||
rule: 'latestReviewUpdatedAtRules',
|
currentUserId: authenticatedUser.id,
|
||||||
|
})
|
||||||
|
expect(reportsCypherQueryResponse.records).toHaveLength(1)
|
||||||
|
const [reportProperties] = reportsCypherQueryResponse.records.map(
|
||||||
|
record => record.get('report').properties,
|
||||||
|
)
|
||||||
|
expect(reportProperties).toMatchObject({ rule: 'latestReviewUpdatedAtRules' })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with overtaken disabled from resource in disable property', () => {
|
||||||
|
it('disable is false', async () => {
|
||||||
|
await mutate({
|
||||||
|
mutation: fileReportMutation,
|
||||||
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const reportsCypherQueryResponse = await instance.cypher(reportsCypherQuery, {
|
||||||
|
resourceId: 'abusive-user-id',
|
||||||
|
currentUserId: authenticatedUser.id,
|
||||||
|
})
|
||||||
|
expect(reportsCypherQueryResponse.records).toHaveLength(1)
|
||||||
|
const [reportProperties] = reportsCypherQueryResponse.records.map(
|
||||||
|
record => record.get('report').properties,
|
||||||
|
)
|
||||||
|
expect(reportProperties).toMatchObject({ disable: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('disable is true', async () => {
|
||||||
|
// first time filling a report to enable a moderator the disable the resource
|
||||||
|
await mutate({
|
||||||
|
mutation: fileReportMutation,
|
||||||
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
|
})
|
||||||
|
authenticatedUser = await moderator.toJson()
|
||||||
|
await mutate({
|
||||||
|
mutation: reviewMutation,
|
||||||
|
variables: {
|
||||||
|
resourceId: 'abusive-user-id',
|
||||||
|
disable: true,
|
||||||
|
closed: true,
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
errors: undefined,
|
authenticatedUser = await currentUser.toJson()
|
||||||
|
// second time filling a report to see if the "disable is true" of the resource is overtaken
|
||||||
|
await mutate({
|
||||||
|
mutation: fileReportMutation,
|
||||||
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const reportsCypherQueryResponse = await instance.cypher(reportsCypherQuery, {
|
||||||
|
resourceId: 'abusive-user-id',
|
||||||
|
currentUserId: authenticatedUser.id,
|
||||||
|
})
|
||||||
|
expect(reportsCypherQueryResponse.records).toHaveLength(1)
|
||||||
|
const [reportProperties] = reportsCypherQueryResponse.records.map(
|
||||||
|
record => record.get('report').properties,
|
||||||
|
)
|
||||||
|
expect(reportProperties).toMatchObject({ disable: true })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it.todo('creates multiple filed reports')
|
it.todo('creates multiple filed reports')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -187,7 +311,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns __typename "User"', async () => {
|
it('returns __typename "User"', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
@ -205,7 +329,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns user attribute info', async () => {
|
it('returns user attribute info', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
@ -221,32 +345,10 @@ describe('file a report on a resource', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns the submitter', async () => {
|
it('returns a createdAt', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
|
||||||
}),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: {
|
|
||||||
fileReport: {
|
|
||||||
filed: [
|
|
||||||
{
|
|
||||||
submitter: {
|
|
||||||
id: 'current-user-id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errors: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns a date', async () => {
|
|
||||||
await expect(
|
|
||||||
mutate({
|
|
||||||
mutation: reportMutation,
|
|
||||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
@ -262,7 +364,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns the reason category', async () => {
|
it('returns the reason category', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'abusive-user-id',
|
resourceId: 'abusive-user-id',
|
||||||
@ -272,12 +374,8 @@ describe('file a report on a resource', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
fileReport: {
|
fileReport: {
|
||||||
filed: [
|
|
||||||
{
|
|
||||||
reasonCategory: 'criminal_behavior_violation_german_law',
|
reasonCategory: 'criminal_behavior_violation_german_law',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
@ -286,7 +384,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('gives an error if the reason category is not in enum "ReasonCategory"', async () => {
|
it('gives an error if the reason category is not in enum "ReasonCategory"', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'abusive-user-id',
|
resourceId: 'abusive-user-id',
|
||||||
@ -307,7 +405,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns the reason description', async () => {
|
it('returns the reason description', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'abusive-user-id',
|
resourceId: 'abusive-user-id',
|
||||||
@ -317,12 +415,8 @@ describe('file a report on a resource', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
fileReport: {
|
fileReport: {
|
||||||
filed: [
|
|
||||||
{
|
|
||||||
reasonDescription: 'My reason!',
|
reasonDescription: 'My reason!',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
@ -331,7 +425,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('sanitizes the reason description', async () => {
|
it('sanitizes the reason description', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'abusive-user-id',
|
resourceId: 'abusive-user-id',
|
||||||
@ -341,12 +435,8 @@ describe('file a report on a resource', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
fileReport: {
|
fileReport: {
|
||||||
filed: [
|
|
||||||
{
|
|
||||||
reasonDescription: 'My reason !',
|
reasonDescription: 'My reason !',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
@ -371,7 +461,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns type "Post"', async () => {
|
it('returns type "Post"', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'post-to-report-id',
|
resourceId: 'post-to-report-id',
|
||||||
@ -392,7 +482,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns resource in post attribute', async () => {
|
it('returns resource in post attribute', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'post-to-report-id',
|
resourceId: 'post-to-report-id',
|
||||||
@ -442,7 +532,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns type "Comment"', async () => {
|
it('returns type "Comment"', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'comment-to-report-id',
|
resourceId: 'comment-to-report-id',
|
||||||
@ -463,7 +553,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns resource in comment attribute', async () => {
|
it('returns resource in comment attribute', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'comment-to-report-id',
|
resourceId: 'comment-to-report-id',
|
||||||
@ -493,7 +583,7 @@ describe('file a report on a resource', () => {
|
|||||||
it('returns null', async () => {
|
it('returns null', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
...variables,
|
...variables,
|
||||||
resourceId: 'tag-to-report-id',
|
resourceId: 'tag-to-report-id',
|
||||||
@ -510,37 +600,6 @@ describe('file a report on a resource', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('query for reported resource', () => {
|
describe('query for reported resource', () => {
|
||||||
const reportsQuery = gql`
|
|
||||||
query {
|
|
||||||
reports(orderBy: createdAt_desc) {
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
closed
|
|
||||||
resource {
|
|
||||||
__typename
|
|
||||||
... on User {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
... on Post {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
... on Comment {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filed {
|
|
||||||
submitter {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
createdAt
|
|
||||||
reasonCategory
|
|
||||||
reasonDescription
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
authenticatedUser = null
|
authenticatedUser = null
|
||||||
moderator = await Factory.build(
|
moderator = await Factory.build(
|
||||||
@ -632,7 +691,7 @@ describe('file a report on a resource', () => {
|
|||||||
authenticatedUser = await currentUser.toJson()
|
authenticatedUser = await currentUser.toJson()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
resourceId: 'abusive-post-1',
|
resourceId: 'abusive-post-1',
|
||||||
reasonCategory: 'other',
|
reasonCategory: 'other',
|
||||||
@ -640,7 +699,7 @@ describe('file a report on a resource', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
resourceId: 'abusive-comment-1',
|
resourceId: 'abusive-comment-1',
|
||||||
reasonCategory: 'discrimination_etc',
|
reasonCategory: 'discrimination_etc',
|
||||||
@ -648,7 +707,7 @@ describe('file a report on a resource', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
mutate({
|
mutate({
|
||||||
mutation: reportMutation,
|
mutation: fileReportMutation,
|
||||||
variables: {
|
variables: {
|
||||||
resourceId: 'abusive-user-1',
|
resourceId: 'abusive-user-1',
|
||||||
reasonCategory: 'doxing',
|
reasonCategory: 'doxing',
|
||||||
|
|||||||
@ -251,12 +251,12 @@ export default {
|
|||||||
boolean: {
|
boolean: {
|
||||||
followedByCurrentUser:
|
followedByCurrentUser:
|
||||||
'MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
'MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||||
|
isBlocked:
|
||||||
|
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||||
blocked:
|
blocked:
|
||||||
'MATCH (this)-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
'MATCH (this)-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||||
isMuted:
|
isMuted:
|
||||||
'MATCH (this)<-[:MUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
'MATCH (this)<-[:MUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||||
isBlocked:
|
|
||||||
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
|
||||||
},
|
},
|
||||||
count: {
|
count: {
|
||||||
contributionsCount:
|
contributionsCount:
|
||||||
|
|||||||
@ -16,3 +16,15 @@ enum ReasonCategory {
|
|||||||
advert_products_services_commercial
|
advert_products_services_commercial
|
||||||
criminal_behavior_violation_german_law
|
criminal_behavior_violation_german_law
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FiledReport {
|
||||||
|
createdAt: String!
|
||||||
|
reasonCategory: ReasonCategory!
|
||||||
|
reasonDescription: String!
|
||||||
|
reportId: ID!
|
||||||
|
resource: ReportedResource!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
fileReport(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): FiledReport
|
||||||
|
}
|
||||||
@ -4,7 +4,6 @@ type REVIEWED {
|
|||||||
disable: Boolean!
|
disable: Boolean!
|
||||||
closed: Boolean!
|
closed: Boolean!
|
||||||
report: Report
|
report: Report
|
||||||
# @cypher(statement: "MATCH (report:Report)<-[this:REVIEWED]-(:User) RETURN report")
|
|
||||||
moderator: User
|
moderator: User
|
||||||
resource: ReviewedResource
|
resource: ReviewedResource
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,9 +5,9 @@ type Report {
|
|||||||
rule: ReportRule!
|
rule: ReportRule!
|
||||||
disable: Boolean!
|
disable: Boolean!
|
||||||
closed: Boolean!
|
closed: Boolean!
|
||||||
filed: [FILED]
|
filed: [FILED]!
|
||||||
reviewed: [REVIEWED]!
|
reviewed: [REVIEWED]!
|
||||||
resource: ReportedResource
|
resource: ReportedResource!
|
||||||
}
|
}
|
||||||
|
|
||||||
union ReportedResource = User | Post | Comment
|
union ReportedResource = User | Post | Comment
|
||||||
@ -16,10 +16,6 @@ enum ReportRule {
|
|||||||
latestReviewUpdatedAtRules
|
latestReviewUpdatedAtRules
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
|
||||||
fileReport(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): Report
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
reports(orderBy: ReportOrdering, first: Int, offset: Int, reviewed: Boolean, closed: Boolean): [Report]
|
reports(orderBy: ReportOrdering, first: Int, offset: Int, reviewed: Boolean, closed: Boolean): [Report]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,10 +64,11 @@ type User {
|
|||||||
# Is the currently logged in user following that user?
|
# Is the currently logged in user following that user?
|
||||||
followedByCurrentUser: Boolean! @cypher(
|
followedByCurrentUser: Boolean! @cypher(
|
||||||
statement: """
|
statement: """
|
||||||
MATCH (this)<-[: FOLLOWS]-(u: User { id: $cypherParams.currentUserId})
|
MATCH (this)<-[:FOLLOWS]-(u:User { id: $cypherParams.currentUserId})
|
||||||
RETURN COUNT(u) >= 1
|
RETURN COUNT(u) >= 1
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
isBlocked: Boolean! @cypher(
|
isBlocked: Boolean! @cypher(
|
||||||
statement: """
|
statement: """
|
||||||
MATCH (this)<-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId})
|
MATCH (this)<-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId})
|
||||||
|
|||||||
@ -139,7 +139,7 @@ Given('somebody reported the following posts:', table => {
|
|||||||
.authenticateAs(submitter)
|
.authenticateAs(submitter)
|
||||||
.mutate(gql`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
.mutate(gql`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||||
fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
|
fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
|
||||||
id
|
reportId
|
||||||
}
|
}
|
||||||
}`, {
|
}`, {
|
||||||
resourceId,
|
resourceId,
|
||||||
|
|||||||
@ -44,13 +44,22 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
filterOptions() {
|
filterOptions() {
|
||||||
return [
|
return [
|
||||||
{ label: this.$t('moderation.reports.filterLabel.all'), value: { reviewed: null } },
|
{
|
||||||
|
label: this.$t('moderation.reports.filterLabel.all'),
|
||||||
|
value: { reviewed: null, closed: null },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: this.$t('moderation.reports.filterLabel.unreviewed'),
|
label: this.$t('moderation.reports.filterLabel.unreviewed'),
|
||||||
value: { reviewed: false },
|
value: { reviewed: false, closed: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('moderation.reports.filterLabel.reviewed'),
|
||||||
|
value: { reviewed: true, closed: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('moderation.reports.filterLabel.closed'),
|
||||||
|
value: { reviewed: null, closed: true },
|
||||||
},
|
},
|
||||||
{ label: this.$t('moderation.reports.filterLabel.reviewed'), value: { reviewed: true } },
|
|
||||||
{ label: this.$t('moderation.reports.filterLabel.closed'), value: { closed: true } },
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
modalData() {
|
modalData() {
|
||||||
@ -108,13 +117,8 @@ export default {
|
|||||||
filter(option) {
|
filter(option) {
|
||||||
this.selected = option.label
|
this.selected = option.label
|
||||||
this.offset = 0
|
this.offset = 0
|
||||||
if (option.value.closed) {
|
|
||||||
this.closed = option.value.closed
|
|
||||||
this.reviewed = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.closed = null
|
|
||||||
this.reviewed = option.value.reviewed
|
this.reviewed = option.value.reviewed
|
||||||
|
this.closed = option.value.closed
|
||||||
},
|
},
|
||||||
async confirmCallback(resource) {
|
async confirmCallback(resource) {
|
||||||
const { disabled: disable, id: resourceId } = resource
|
const { disabled: disable, id: resourceId } = resource
|
||||||
|
|||||||
@ -24,11 +24,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<template v-for="report in reports">
|
<template v-for="report in reports">
|
||||||
<report-row
|
<!-- should be ':key="report.resource.id"' for having one element for every resource, but this crashes at the moment, because the 'reports' query returns multiple reports on the same resource! I will create an issue -->
|
||||||
:key="report.resource.id"
|
<report-row :key="report.id" :report="report" @confirm-report="$emit('confirm', report)" />
|
||||||
:report="report"
|
|
||||||
@confirm-report="$emit('confirm', report)"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</table>
|
</table>
|
||||||
<hc-empty v-else icon="alert" :message="$t('moderation.reports.empty')" />
|
<hc-empty v-else icon="alert" :message="$t('moderation.reports.empty')" />
|
||||||
|
|||||||
@ -100,7 +100,7 @@ export const reportMutation = () => {
|
|||||||
reasonCategory: $reasonCategory
|
reasonCategory: $reasonCategory
|
||||||
reasonDescription: $reasonDescription
|
reasonDescription: $reasonDescription
|
||||||
) {
|
) {
|
||||||
id
|
reportId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user