diff --git a/.gitbook/assets/browserstack-logo.svg b/.gitbook/assets/browserstack-logo.svg new file mode 100644 index 000000000..195f64d2f --- /dev/null +++ b/.gitbook/assets/browserstack-logo.svg @@ -0,0 +1,90 @@ + + + + +Browserstack-logo-white + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.travis.yml b/.travis.yml index a2aae3cdb..0a73c5baf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ install: - yarn global add wait-on # Install Codecov - yarn install - - cp cypress.env.template.json cypress.env.json + - cp backend/.env.template backend/.env before_script: - docker-compose -f docker-compose.yml build --parallel diff --git a/README.md b/README.md index 115a6760c..eaf71acb8 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,11 @@ Check out the [contribution guideline](./CONTRIBUTING.md), too! ## Attributions -Locale Icons made by [Freepik](http://www.freepik.com/) from [www.flaticon.com](https://www.flaticon.com/) is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/) +Locale Icons made by [Freepik](http://www.freepik.com/) from [www.flaticon.com](https://www.flaticon.com/) is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/). + +Browser compatibility testing with [BrowserStack](https://www.browserstack.com/). + +BrowserStack Logo ## License See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT). diff --git a/backend/package.json b/backend/package.json index f641c528c..d1045a577 100644 --- a/backend/package.json +++ b/backend/package.json @@ -33,12 +33,12 @@ }, "dependencies": { "@hapi/joi": "^16.1.8", - "@sentry/node": "^5.9.0", + "@sentry/node": "^5.10.1", "apollo-cache-inmemory": "~1.6.3", "apollo-client": "~2.6.4", "apollo-link-context": "~1.0.19", "apollo-link-http": "~1.5.16", - "apollo-server": "~2.9.12", + "apollo-server": "~2.9.13", "apollo-server-express": "^2.9.7", "babel-plugin-transform-runtime": "^6.23.0", "bcryptjs": "~2.4.3", @@ -62,7 +62,7 @@ "linkifyjs": "~2.1.8", "lodash": "~4.17.14", "merge-graphql-schemas": "^1.7.3", - "metascraper": "^5.8.8", + "metascraper": "^5.8.9", "metascraper-audio": "^5.8.7", "metascraper-author": "^5.8.7", "metascraper-clearbit-logo": "^5.3.0", @@ -76,15 +76,15 @@ "metascraper-soundcloud": "^5.8.9", "metascraper-title": "^5.8.7", "metascraper-url": "^5.8.7", - "metascraper-video": "^5.8.7", + "metascraper-video": "^5.8.9", "metascraper-youtube": "^5.8.9", "minimatch": "^3.0.4", "mustache": "^3.1.0", "neo4j-driver": "~1.7.6", - "neo4j-graphql-js": "^2.9.3", + "neo4j-graphql-js": "^2.10.0", "neode": "^0.3.3", "node-fetch": "~2.6.0", - "nodemailer": "^6.3.1", + "nodemailer": "^6.4.1", "nodemailer-html-to-text": "^3.1.0", "npm-run-all": "~4.1.5", "request": "~2.88.0", @@ -98,12 +98,12 @@ }, "devDependencies": { "@babel/cli": "~7.7.4", - "@babel/core": "~7.7.4", + "@babel/core": "~7.7.5", "@babel/node": "~7.7.4", "@babel/plugin-proposal-throw-expressions": "^7.7.4", "@babel/preset-env": "~7.7.4", "@babel/register": "~7.7.0", - "apollo-server-testing": "~2.9.12", + "apollo-server-testing": "~2.9.13", "babel-core": "~7.0.0-0", "babel-eslint": "~10.0.3", "babel-jest": "~24.9.0", diff --git a/backend/src/bootstrap/neo4j.js b/backend/src/bootstrap/neo4j.js index f9e3a997d..404e8a2c0 100644 --- a/backend/src/bootstrap/neo4j.js +++ b/backend/src/bootstrap/neo4j.js @@ -1,15 +1,17 @@ import { v1 as neo4j } from 'neo4j-driver' import CONFIG from './../config' -import setupNeode from './neode' +import Neode from 'neode' +import models from '../models' let driver +const defaultOptions = { + uri: CONFIG.NEO4J_URI, + username: CONFIG.NEO4J_USERNAME, + password: CONFIG.NEO4J_PASSWORD, +} export function getDriver(options = {}) { - const { - uri = CONFIG.NEO4J_URI, - username = CONFIG.NEO4J_USERNAME, - password = CONFIG.NEO4J_PASSWORD, - } = options + const { uri, username, password } = { ...defaultOptions, ...options } if (!driver) { driver = neo4j.driver(uri, neo4j.auth.basic(username, password)) } @@ -17,10 +19,11 @@ export function getDriver(options = {}) { } let neodeInstance -export function neode() { +export function getNeode(options = {}) { if (!neodeInstance) { - const { NEO4J_URI: uri, NEO4J_USERNAME: username, NEO4J_PASSWORD: password } = CONFIG - neodeInstance = setupNeode({ uri, username, password }) + const { uri, username, password } = { ...defaultOptions, ...options } + neodeInstance = new Neode(uri, username, password).with(models) + return neodeInstance } return neodeInstance } diff --git a/backend/src/bootstrap/neode.js b/backend/src/bootstrap/neode.js deleted file mode 100644 index 65a2074be..000000000 --- a/backend/src/bootstrap/neode.js +++ /dev/null @@ -1,9 +0,0 @@ -import Neode from 'neode' -import models from '../models' - -export default function setupNeode(options) { - const { uri, username, password } = options - const neodeInstance = new Neode(uri, username, password) - neodeInstance.with(models) - return neodeInstance -} diff --git a/backend/src/jwt/decode.spec.js b/backend/src/jwt/decode.spec.js index 9ea858304..7aa703d97 100644 --- a/backend/src/jwt/decode.spec.js +++ b/backend/src/jwt/decode.spec.js @@ -1,5 +1,5 @@ import Factory from '../seed/factories/index' -import { getDriver, neode as getNeode } from '../bootstrap/neo4j' +import { getDriver, getNeode } from '../bootstrap/neo4j' import decode from './decode' const factory = Factory() diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js index 6e97f34c4..0fa1e2dc5 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js @@ -1,7 +1,7 @@ import { gql } from '../../helpers/jest' import Factory from '../../seed/factories' import { createTestClient } from 'apollo-server-testing' -import { neode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' import createServer from '../../server' let server @@ -11,7 +11,7 @@ let hashtagingUser let authenticatedUser const factory = Factory() const driver = getDriver() -const instance = neode() +const neode = getNeode() const categoryIds = ['cat9'] const createPostMutation = gql` mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) { @@ -36,7 +36,7 @@ beforeAll(() => { context: () => { return { user: authenticatedUser, - neode: instance, + neode, driver, } }, @@ -48,14 +48,14 @@ beforeAll(() => { }) beforeEach(async () => { - hashtagingUser = await instance.create('User', { + hashtagingUser = await neode.create('User', { id: 'you', name: 'Al Capone', slug: 'al-capone', email: 'test@example.org', password: '1234', }) - await instance.create('Category', { + await neode.create('Category', { id: 'cat9', name: 'Democracy & Politics', icon: 'university', diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.js b/backend/src/middleware/notifications/notificationsMiddleware.spec.js index 502ddaa8e..53fa80ce8 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.js @@ -1,7 +1,7 @@ import { gql } from '../../helpers/jest' import Factory from '../../seed/factories' import { createTestClient } from 'apollo-server-testing' -import { neode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' import createServer from '../../server' let server @@ -11,7 +11,7 @@ let notifiedUser let authenticatedUser const factory = Factory() const driver = getDriver() -const instance = neode() +const neode = getNeode() const categoryIds = ['cat9'] const createPostMutation = gql` mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) { @@ -44,7 +44,7 @@ beforeAll(() => { context: () => { return { user: authenticatedUser, - neode: instance, + neode: neode, driver, } }, @@ -56,14 +56,14 @@ beforeAll(() => { }) beforeEach(async () => { - notifiedUser = await instance.create('User', { + notifiedUser = await neode.create('User', { id: 'you', name: 'Al Capone', slug: 'al-capone', email: 'test@example.org', password: '1234', }) - await instance.create('Category', { + await neode.create('Category', { id: 'cat9', name: 'Democracy & Politics', icon: 'university', @@ -146,7 +146,7 @@ describe('notifications', () => { describe('commenter is not me', () => { beforeEach(async () => { commentContent = 'Commenters comment.' - commentAuthor = await instance.create('User', { + commentAuthor = await neode.create('User', { id: 'commentAuthor', name: 'Mrs Comment', slug: 'mrs-comment', @@ -228,7 +228,7 @@ describe('notifications', () => { }) beforeEach(async () => { - postAuthor = await instance.create('User', { + postAuthor = await neode.create('User', { id: 'postAuthor', name: 'Mrs Post', slug: 'mrs-post', @@ -371,7 +371,7 @@ describe('notifications', () => { expect(readAfter).toEqual(false) }) - it('updates the `createdAt` attribute', async () => { + it('does not update the `createdAt` attribute', async () => { await createPostAction() await markAsReadAction() const { @@ -432,7 +432,7 @@ describe('notifications', () => { beforeEach(async () => { commentContent = 'One mention about me with @al-capone.' - commentAuthor = await instance.create('User', { + commentAuthor = await neode.create('User', { id: 'commentAuthor', name: 'Mrs Comment', slug: 'mrs-comment', @@ -442,7 +442,7 @@ describe('notifications', () => { }) it('sends only one notification with reason mentioned_in_comment', async () => { - postAuthor = await instance.create('User', { + postAuthor = await neode.create('User', { id: 'MrPostAuthor', name: 'Mr Author', slug: 'mr-author', @@ -518,7 +518,7 @@ describe('notifications', () => { await postAuthor.relateTo(notifiedUser, 'blocked') commentContent = 'One mention about me with @al-capone.' - commentAuthor = await instance.create('User', { + commentAuthor = await neode.create('User', { id: 'commentAuthor', name: 'Mrs Comment', slug: 'mrs-comment', diff --git a/backend/src/middleware/orderByMiddleware.spec.js b/backend/src/middleware/orderByMiddleware.spec.js index a7b31da0a..129f3a8b4 100644 --- a/backend/src/middleware/orderByMiddleware.spec.js +++ b/backend/src/middleware/orderByMiddleware.spec.js @@ -1,6 +1,6 @@ import { gql } from '../helpers/jest' import Factory from '../seed/factories' -import { neode as getNeode, getDriver } from '../bootstrap/neo4j' +import { getNeode, getDriver } from '../bootstrap/neo4j' import { createTestClient } from 'apollo-server-testing' import createServer from '../server' diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index f3c8ca65e..8f139f4c7 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -1,11 +1,11 @@ import { rule, shield, deny, allow, or } from 'graphql-shield' -import { neode } from '../bootstrap/neo4j' +import { getNeode } from '../bootstrap/neo4j' import CONFIG from '../config' const debug = !!CONFIG.DEBUG const allowExternalErrors = true -const instance = neode() +const neode = getNeode() const isAuthenticated = rule({ cache: 'contextual', @@ -36,7 +36,7 @@ const isMyOwn = rule({ const isMySocialMedia = rule({ cache: 'no_cache', })(async (_, args, { user }) => { - let socialMedia = await instance.find('SocialMedia', args.id) + let socialMedia = await neode.find('SocialMedia', args.id) socialMedia = await socialMedia.toJson() return socialMedia.ownedBy.node.id === user.id }) @@ -112,7 +112,7 @@ export default shield( CreatePost: isAuthenticated, UpdatePost: isAuthor, DeletePost: isAuthor, - report: isAuthenticated, + fileReport: isAuthenticated, CreateSocialMedia: isAuthenticated, UpdateSocialMedia: isMySocialMedia, DeleteSocialMedia: isMySocialMedia, @@ -125,8 +125,7 @@ export default shield( shout: isAuthenticated, unshout: isAuthenticated, changePassword: isAuthenticated, - enable: isModerator, - disable: isModerator, + review: isModerator, CreateComment: isAuthenticated, UpdateComment: isAuthor, DeleteComment: isAuthor, diff --git a/backend/src/middleware/permissionsMiddleware.spec.js b/backend/src/middleware/permissionsMiddleware.spec.js index 340766136..60aff961d 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.js +++ b/backend/src/middleware/permissionsMiddleware.spec.js @@ -2,7 +2,7 @@ import { createTestClient } from 'apollo-server-testing' import createServer from '../server' import Factory from '../seed/factories' import { gql } from '../helpers/jest' -import { getDriver, neode as getNeode } from '../bootstrap/neo4j' +import { getDriver, getNeode } from '../bootstrap/neo4j' const factory = Factory() const instance = getNeode() diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index 02699f7b2..1c2e59317 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -1,6 +1,6 @@ import Factory from '../seed/factories' import { gql } from '../helpers/jest' -import { neode as getNeode, getDriver } from '../bootstrap/neo4j' +import { getNeode, getDriver } from '../bootstrap/neo4j' import createServer from '../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js index 6da080ebb..b7c16dfd3 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js @@ -1,6 +1,6 @@ import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' -import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' @@ -8,14 +8,8 @@ const factory = Factory() const neode = getNeode() const driver = getDriver() -let query -let mutate -let graphqlQuery const categoryIds = ['cat9'] -let authenticatedUser -let user -let moderator -let troll +let query, graphqlQuery, authenticatedUser, user, moderator, troll const action = () => { return query({ query: graphqlQuery }) @@ -38,18 +32,17 @@ beforeAll(async () => { avatar: '/some/offensive/avatar.jpg', about: 'This self description is very offensive', }), + neode.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }), ]) user = users[0] moderator = users[1] troll = users[2] - await neode.create('Category', { - id: 'cat9', - name: 'Democracy & Politics', - icon: 'university', - }) - await Promise.all([ user.relateTo(troll, 'following'), factory.create('Post', { @@ -70,33 +63,32 @@ beforeAll(async () => { }), ]) - await Promise.all([ + const resources = await Promise.all([ factory.create('Comment', { author: user, id: 'c2', postId: 'p3', content: 'Enabled comment on public post', }), + factory.create('Post', { + id: 'p2', + author: troll, + title: 'Disabled post', + content: 'This is an offensive post content', + contentExcerpt: 'This is an offensive post content', + image: '/some/offensive/image.jpg', + deleted: false, + categoryIds, + }), + factory.create('Comment', { + id: 'c1', + author: troll, + postId: 'p3', + content: 'Disabled comment', + contentExcerpt: 'Disabled comment', + }), ]) - await factory.create('Post', { - id: 'p2', - author: troll, - title: 'Disabled post', - content: 'This is an offensive post content', - contentExcerpt: 'This is an offensive post content', - image: '/some/offensive/image.jpg', - deleted: false, - categoryIds, - }) - await factory.create('Comment', { - id: 'c1', - author: troll, - postId: 'p3', - content: 'Disabled comment', - contentExcerpt: 'Disabled comment', - }) - const { server } = createServer({ context: () => { return { @@ -108,20 +100,57 @@ beforeAll(async () => { }) const client = createTestClient(server) query = client.query - mutate = client.mutate - authenticatedUser = await moderator.toJson() - const disableMutation = gql` - mutation($id: ID!) { - disable(id: $id) - } - ` - await Promise.all([ - mutate({ mutation: disableMutation, variables: { id: 'c1' } }), - mutate({ mutation: disableMutation, variables: { id: 'u2' } }), - mutate({ mutation: disableMutation, variables: { id: 'p2' } }), + const trollingPost = resources[1] + const trollingComment = resources[2] + + const reports = await Promise.all([ + factory.create('Report'), + factory.create('Report'), + factory.create('Report'), + ]) + const reportAgainstTroll = reports[0] + const reportAgainstTrollingPost = reports[1] + const reportAgainstTrollingComment = reports[2] + + const reportVariables = { + resourceId: 'undefined-resource', + reasonCategory: 'discrimination_etc', + reasonDescription: 'I am what I am !!!', + } + + await Promise.all([ + reportAgainstTroll.relateTo(user, 'filed', { ...reportVariables, resourceId: 'u2' }), + reportAgainstTroll.relateTo(troll, 'belongsTo'), + reportAgainstTrollingPost.relateTo(user, 'filed', { ...reportVariables, resourceId: 'p2' }), + reportAgainstTrollingPost.relateTo(trollingPost, 'belongsTo'), + reportAgainstTrollingComment.relateTo(moderator, 'filed', { + ...reportVariables, + resourceId: 'c1', + }), + reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'), + ]) + + const disableVariables = { + resourceId: 'undefined-resource', + disable: true, + closed: false, + } + + await Promise.all([ + reportAgainstTroll.relateTo(moderator, 'reviewed', { ...disableVariables, resourceId: 'u2' }), + troll.update({ disabled: true, updatedAt: new Date().toISOString() }), + reportAgainstTrollingPost.relateTo(moderator, 'reviewed', { + ...disableVariables, + resourceId: 'p2', + }), + trollingPost.update({ disabled: true, updatedAt: new Date().toISOString() }), + reportAgainstTrollingComment.relateTo(moderator, 'reviewed', { + ...disableVariables, + resourceId: 'c1', + }), + trollingComment.update({ disabled: true, updatedAt: new Date().toISOString() }), ]) - authenticatedUser = null }) afterAll(async () => { diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index 4954a5584..f36458e61 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -61,31 +61,58 @@ const validateUpdatePost = async (resolve, root, args, context, info) => { const validateReport = async (resolve, root, args, context, info) => { const { resourceId } = args - const { user, driver } = context + const { user } = context if (resourceId === user.id) throw new Error('You cannot report yourself!') + return resolve(root, args, context, info) +} + +const validateReview = async (resolve, root, args, context, info) => { + const { resourceId } = args + let existingReportedResource + const { user, driver } = context + if (resourceId === user.id) throw new Error('You cannot review yourself!') const session = driver.session() - try { - const reportQueryRes = await session.run( + const reportReadTxPromise = session.writeTransaction(async txc => { + const validateReviewTransactionResponse = await txc.run( ` - MATCH (:User {id:$submitterId})-[:REPORTED]->(resource {id:$resourceId}) - RETURN labels(resource)[0] as label - `, + MATCH (resource {id: $resourceId}) + WHERE resource:User OR resource:Post OR resource:Comment + OPTIONAL MATCH (:User)-[filed:FILED]->(:Report {closed: false})-[:BELONGS_TO]->(resource) + OPTIONAL MATCH (resource)<-[:WROTE]-(author:User) + RETURN labels(resource)[0] AS label, author, filed + `, { 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) + return validateReviewTransactionResponse.records.map(record => ({ + label: record.get('label'), + author: record.get('author'), + filed: record.get('filed'), + })) + }) + try { + const txResult = await reportReadTxPromise + existingReportedResource = txResult + if (!existingReportedResource || !existingReportedResource.length) + throw new Error(`Resource not found or is not a Post|Comment|User!`) + existingReportedResource = existingReportedResource[0] + if (!existingReportedResource.filed) + throw new Error( + `Before starting the review process, please report the ${existingReportedResource.label}!`, + ) + const authorId = + existingReportedResource.label !== 'User' && existingReportedResource.author + ? existingReportedResource.author.properties.id + : null + if (authorId && authorId === user.id) + throw new Error(`You cannot review your own ${existingReportedResource.label}!`) } finally { session.close() } + + return resolve(root, args, context, info) } export default { @@ -94,6 +121,7 @@ export default { UpdateComment: validateUpdateComment, CreatePost: validatePost, UpdatePost: validateUpdatePost, - report: validateReport, + fileReport: validateReport, + review: validateReview, }, } diff --git a/backend/src/middleware/validation/validationMiddleware.spec.js b/backend/src/middleware/validation/validationMiddleware.spec.js index ec5f3e012..c3d0512ad 100644 --- a/backend/src/middleware/validation/validationMiddleware.spec.js +++ b/backend/src/middleware/validation/validationMiddleware.spec.js @@ -1,13 +1,22 @@ import { gql } from '../../helpers/jest' import Factory from '../../seed/factories' -import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' import { createTestClient } from 'apollo-server-testing' import createServer from '../../server' const factory = Factory() const neode = getNeode() const driver = getDriver() -let mutate, authenticatedUser, user +let authenticatedUser, + mutate, + users, + offensivePost, + reportVariables, + disableVariables, + reportingUser, + moderatingUser, + commentingUser + const createCommentMutation = gql` mutation($id: ID, $postId: ID!, $content: String!) { CreateComment(id: $id, postId: $postId, content: $content) { @@ -23,8 +32,14 @@ const updateCommentMutation = gql` } ` const createPostMutation = gql` - mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) { - CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) { + mutation($id: ID, $title: String!, $content: String!, $language: String, $categoryIds: [ID]) { + CreatePost( + id: $id + title: $title + content: $content + language: $language + categoryIds: $categoryIds + ) { id } } @@ -37,7 +52,25 @@ const updatePostMutation = gql` } } ` - +const reportMutation = gql` + mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { + fileReport( + resourceId: $resourceId + reasonCategory: $reasonCategory + reasonDescription: $reasonDescription + ) { + id + } + } +` +const reviewMutation = gql` + mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) { + review(resourceId: $resourceId, disable: $disable, closed: $closed) { + createdAt + updatedAt + } + } +` beforeAll(() => { const { server } = createServer({ context: () => { @@ -52,13 +85,42 @@ beforeAll(() => { }) beforeEach(async () => { - user = await factory.create('User', { - id: 'user-id', - }) - await factory.create('Post', { - id: 'post-4-commenting', - authorId: 'user-id', - }) + users = await Promise.all([ + factory.create('User', { + id: 'reporting-user', + }), + factory.create('User', { + id: 'moderating-user', + role: 'moderator', + }), + factory.create('User', { + id: 'commenting-user', + }), + ]) + reportVariables = { + resourceId: 'whatever', + reasonCategory: 'other', + reasonDescription: 'Violates code of conduct !!!', + } + disableVariables = { + resourceId: 'undefined-resource', + disable: true, + closed: false, + } + reportingUser = users[0] + moderatingUser = users[1] + commentingUser = users[2] + const posts = await Promise.all([ + factory.create('Post', { + id: 'offensive-post', + authorId: 'moderating-user', + }), + factory.create('Post', { + id: 'post-4-commenting', + authorId: 'commenting-user', + }), + ]) + offensivePost = posts[0] }) afterEach(async () => { @@ -72,7 +134,7 @@ describe('validateCreateComment', () => { postId: 'whatever', content: '', } - authenticatedUser = await user.toJson() + authenticatedUser = await commentingUser.toJson() }) it('throws an error if content is empty', async () => { @@ -114,13 +176,13 @@ describe('validateCreateComment', () => { beforeEach(async () => { await factory.create('Comment', { id: 'comment-id', - authorId: 'user-id', + authorId: 'commenting-user', }) updateCommentVariables = { id: 'whatever', content: '', } - authenticatedUser = await user.toJson() + authenticatedUser = await commentingUser.toJson() }) it('throws an error if content is empty', async () => { @@ -151,7 +213,7 @@ describe('validateCreateComment', () => { title: 'I am a title', content: 'Some content', } - authenticatedUser = await user.toJson() + authenticatedUser = await commentingUser.toJson() }) describe('categories', () => { @@ -242,3 +304,97 @@ describe('validateCreateComment', () => { }) }) }) + +describe('validateReport', () => { + it('throws an error if a user tries to report themself', async () => { + authenticatedUser = await reportingUser.toJson() + reportVariables = { ...reportVariables, resourceId: 'reporting-user' } + await expect( + mutate({ mutation: reportMutation, variables: reportVariables }), + ).resolves.toMatchObject({ + data: { fileReport: null }, + errors: [{ message: 'You cannot report yourself!' }], + }) + }) +}) + +describe('validateReview', () => { + beforeEach(async () => { + const reportAgainstModerator = await factory.create('Report') + await Promise.all([ + reportAgainstModerator.relateTo(reportingUser, 'filed', { + ...reportVariables, + resourceId: 'moderating-user', + }), + reportAgainstModerator.relateTo(moderatingUser, 'belongsTo'), + ]) + authenticatedUser = await moderatingUser.toJson() + }) + + it('throws an error if a user tries to review a report against them', async () => { + disableVariables = { ...disableVariables, resourceId: 'moderating-user' } + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: null }, + errors: [{ message: 'You cannot review yourself!' }], + }) + }) + + it('throws an error for invaild resource', async () => { + disableVariables = { ...disableVariables, resourceId: 'non-existent-resource' } + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: null }, + errors: [{ message: 'Resource not found or is not a Post|Comment|User!' }], + }) + }) + + it('throws an error if no report exists', async () => { + disableVariables = { ...disableVariables, resourceId: 'offensive-post' } + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: null }, + errors: [{ message: 'Before starting the review process, please report the Post!' }], + }) + }) + + it('throws an error if a moderator tries to review their own resource(Post|Comment)', async () => { + const reportAgainstOffensivePost = await factory.create('Report') + await Promise.all([ + reportAgainstOffensivePost.relateTo(reportingUser, 'filed', { + ...reportVariables, + resourceId: 'offensive-post', + }), + reportAgainstOffensivePost.relateTo(offensivePost, 'belongsTo'), + ]) + disableVariables = { ...disableVariables, resourceId: 'offensive-post' } + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: null }, + errors: [{ message: 'You cannot review your own Post!' }], + }) + }) + + describe('moderate a resource that is not a (Comment|Post|User) ', () => { + beforeEach(async () => { + await Promise.all([factory.create('Tag', { id: 'tag-id' })]) + }) + + it('returns null', async () => { + disableVariables = { + ...disableVariables, + resourceId: 'tag-id', + } + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: null }, + errors: [{ message: 'Resource not found or is not a Post|Comment|User!' }], + }) + }) + }) +}) diff --git a/backend/src/models/Comment.js b/backend/src/models/Comment.js index c89103e5d..54cbda675 100644 --- a/backend/src/models/Comment.js +++ b/backend/src/models/Comment.js @@ -25,12 +25,6 @@ module.exports = { target: 'User', direction: 'in', }, - disabledBy: { - type: 'relationship', - relationship: 'DISABLED', - target: 'User', - direction: 'in', - }, notified: { type: 'relationship', relationship: 'NOTIFIED', diff --git a/backend/src/models/Post.js b/backend/src/models/Post.js index 5ac8378c2..18dc0e464 100644 --- a/backend/src/models/Post.js +++ b/backend/src/models/Post.js @@ -17,12 +17,6 @@ module.exports = { image: { type: 'string', allow: [null] }, deleted: { type: 'boolean', default: false }, disabled: { type: 'boolean', default: false }, - disabledBy: { - type: 'relationship', - relationship: 'DISABLED', - target: 'User', - direction: 'in', - }, notified: { type: 'relationship', relationship: 'NOTIFIED', @@ -45,4 +39,5 @@ module.exports = { default: () => new Date().toISOString(), }, language: { type: 'string', allow: [null] }, + imageAspectRatio: { type: 'float', default: 1.0 }, } diff --git a/backend/src/models/Report.js b/backend/src/models/Report.js new file mode 100644 index 000000000..2ace4ea73 --- /dev/null +++ b/backend/src/models/Report.js @@ -0,0 +1,52 @@ +import uuid from 'uuid/v4' + +module.exports = { + id: { type: 'string', primary: true, default: uuid }, + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + rule: { type: 'string', default: 'latestReviewUpdatedAtRules' }, + closed: { type: 'boolean', default: false }, + belongsTo: { + type: 'relationship', + relationship: 'BELONGS_TO', + target: ['User', 'Comment', 'Post'], + direction: 'out', + }, + filed: { + type: 'relationship', + relationship: 'FILED', + target: 'User', + direction: 'in', + properties: { + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + resourceId: { type: 'string', primary: true, default: uuid }, + reasonCategory: { + type: 'string', + valid: [ + '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', + ], + invalid: [null], + }, + reasonDescription: { type: 'string', allow: [null] }, + }, + }, + reviewed: { + type: 'relationship', + relationship: 'REVIEWED', + target: 'User', + direction: 'in', + properties: { + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + disable: { type: 'boolean', default: false }, + closed: { type: 'boolean', default: false }, + }, + }, +} diff --git a/backend/src/models/User.js b/backend/src/models/User.js index fd6e88c27..32f053e2b 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -42,12 +42,6 @@ module.exports = { }, }, friends: { type: 'relationship', relationship: 'FRIENDS', target: 'User', direction: 'both' }, - disabledBy: { - type: 'relationship', - relationship: 'DISABLED', - target: 'User', - direction: 'in', - }, rewarded: { type: 'relationship', relationship: 'REWARDED', diff --git a/backend/src/models/User.spec.js b/backend/src/models/User.spec.js index 7c4a26c55..332e6a3ea 100644 --- a/backend/src/models/User.spec.js +++ b/backend/src/models/User.spec.js @@ -1,8 +1,8 @@ import Factory from '../seed/factories' -import { neode } from '../bootstrap/neo4j' +import { getNeode } from '../bootstrap/neo4j' const factory = Factory() -const instance = neode() +const neode = getNeode() afterEach(async () => { await factory.cleanDatabase() @@ -10,7 +10,7 @@ afterEach(async () => { describe('role', () => { it('defaults to `user`', async () => { - const user = await instance.create('User', { name: 'John' }) + const user = await neode.create('User', { name: 'John' }) await expect(user.toJson()).resolves.toEqual( expect.objectContaining({ role: 'user', @@ -21,7 +21,7 @@ describe('role', () => { describe('slug', () => { it('normalizes to lowercase letters', async () => { - const user = await instance.create('User', { slug: 'Matt' }) + const user = await neode.create('User', { slug: 'Matt' }) await expect(user.toJson()).resolves.toEqual( expect.objectContaining({ slug: 'matt', @@ -30,9 +30,9 @@ describe('slug', () => { }) it('must be unique', async done => { - await instance.create('User', { slug: 'Matt' }) + await neode.create('User', { slug: 'Matt' }) try { - await expect(instance.create('User', { slug: 'Matt' })).rejects.toThrow('already exists') + await expect(neode.create('User', { slug: 'Matt' })).rejects.toThrow('already exists') done() } catch (error) { throw new Error(` @@ -54,7 +54,7 @@ describe('slug', () => { describe('characters', () => { const createUser = attrs => { - return instance.create('User', attrs).then(user => user.toJson()) + return neode.create('User', attrs).then(user => user.toJson()) } it('-', async () => { diff --git a/backend/src/models/index.js b/backend/src/models/index.js index 239076adc..0b9378162 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -12,4 +12,5 @@ export default { Tag: require('./Tag.js'), Location: require('./Location.js'), Donations: require('./Donations.js'), + Report: require('./Report.js'), } diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index 4252bd817..274697238 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -17,7 +17,9 @@ export default makeAugmentedSchema({ 'Location', 'SocialMedia', 'NOTIFIED', - 'REPORTED', + 'FILED', + 'REVIEWED', + 'Report', 'Donations', ], }, diff --git a/backend/src/schema/resolvers/comments.js b/backend/src/schema/resolvers/comments.js index 20869a73a..97b461511 100644 --- a/backend/src/schema/resolvers/comments.js +++ b/backend/src/schema/resolvers/comments.js @@ -78,7 +78,6 @@ export default { hasOne: { author: '<-[:WROTE]-(related:User)', post: '-[:COMMENTS]->(related:Post)', - disabledBy: '<-[:DISABLED]-(related:User)', }, }), }, diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js index d2692aa8a..e2d20d1bd 100644 --- a/backend/src/schema/resolvers/comments.spec.js +++ b/backend/src/schema/resolvers/comments.spec.js @@ -2,7 +2,7 @@ import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' import { createTestClient } from 'apollo-server-testing' import createServer from '../../server' -import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' const driver = getDriver() const neode = getNeode() diff --git a/backend/src/schema/resolvers/donations.spec.js b/backend/src/schema/resolvers/donations.spec.js index 9e701059d..d8dd5db06 100644 --- a/backend/src/schema/resolvers/donations.spec.js +++ b/backend/src/schema/resolvers/donations.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' -import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' import createServer from '../../server' let mutate, query, authenticatedUser, variables diff --git a/backend/src/schema/resolvers/emails.spec.js b/backend/src/schema/resolvers/emails.spec.js index 156007435..82ce43337 100644 --- a/backend/src/schema/resolvers/emails.spec.js +++ b/backend/src/schema/resolvers/emails.spec.js @@ -1,6 +1,6 @@ import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' -import { getDriver, neode as getNeode } from '../../bootstrap/neo4j' +import { getDriver, getNeode } from '../../bootstrap/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/follow.js b/backend/src/schema/resolvers/follow.js index ada417cff..0416fe3d2 100644 --- a/backend/src/schema/resolvers/follow.js +++ b/backend/src/schema/resolvers/follow.js @@ -1,4 +1,4 @@ -import { neode as getNeode } from '../../bootstrap/neo4j' +import { getNeode } from '../../bootstrap/neo4j' const neode = getNeode() diff --git a/backend/src/schema/resolvers/follow.spec.js b/backend/src/schema/resolvers/follow.spec.js index 8402842e2..ff884666e 100644 --- a/backend/src/schema/resolvers/follow.spec.js +++ b/backend/src/schema/resolvers/follow.spec.js @@ -1,6 +1,6 @@ import { createTestClient } from 'apollo-server-testing' import Factory from '../../seed/factories' -import { getDriver, neode as getNeode } from '../../bootstrap/neo4j' +import { getDriver, getNeode } from '../../bootstrap/neo4j' import createServer from '../../server' import { gql } from '../../helpers/jest' diff --git a/backend/src/schema/resolvers/helpers/Resolver.js b/backend/src/schema/resolvers/helpers/Resolver.js index 03c0d4176..b094231c0 100644 --- a/backend/src/schema/resolvers/helpers/Resolver.js +++ b/backend/src/schema/resolvers/helpers/Resolver.js @@ -1,4 +1,4 @@ -import { neode } from '../../../bootstrap/neo4j' +import { getNeode } from '../../../bootstrap/neo4j' export const undefinedToNullResolver = list => { const resolvers = {} @@ -11,7 +11,7 @@ export const undefinedToNullResolver = list => { } export default function Resolver(type, options = {}) { - const instance = neode() + const instance = getNeode() const { idAttribute = 'id', undefinedToNull = [], diff --git a/backend/src/schema/resolvers/locations.spec.js b/backend/src/schema/resolvers/locations.spec.js index 51dafcc2e..f4a846afd 100644 --- a/backend/src/schema/resolvers/locations.spec.js +++ b/backend/src/schema/resolvers/locations.spec.js @@ -1,6 +1,6 @@ import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' -import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/moderation.js b/backend/src/schema/resolvers/moderation.js index de756b7b2..4bdf82d50 100644 --- a/backend/src/schema/resolvers/moderation.js +++ b/backend/src/schema/resolvers/moderation.js @@ -1,47 +1,51 @@ +const transformReturnType = record => { + return { + ...record.get('review').properties, + report: record.get('report').properties, + resource: { + __typename: record.get('type'), + ...record.get('resource').properties, + }, + } +} + export default { Mutation: { - disable: async (object, params, { user, driver }) => { - const { id } = params - const { id: userId } = user - const cypher = ` - MATCH (u:User {id: $userId}) - MATCH (resource {id: $id}) - WHERE resource:User OR resource:Comment OR resource:Post - SET resource.disabled = true - MERGE (resource)<-[:DISABLED]-(u) - RETURN resource {.id} - ` + review: async (_object, params, context, _resolveInfo) => { + const { user: moderator, driver } = context + + let createdRelationshipWithNestedAttributes = null // return value const session = driver.session() try { - const res = await session.run(cypher, { id, userId }) - const [resource] = res.records.map(record => { - return record.get('resource') + const cypher = ` + MATCH (moderator:User {id: $moderatorId}) + MATCH (resource {id: $params.resourceId})<-[:BELONGS_TO]-(report:Report {closed: false}) + WHERE resource:User OR resource:Post OR resource:Comment + MERGE (report)<-[review:REVIEWED]-(moderator) + ON CREATE SET review.createdAt = $dateTime, review.updatedAt = review.createdAt + ON MATCH SET review.updatedAt = $dateTime + SET review.disable = $params.disable + SET report.updatedAt = $dateTime, report.closed = $params.closed + SET resource.disabled = review.disable + + RETURN review, report, resource, labels(resource)[0] AS type + ` + const reviewWriteTxResultPromise = session.writeTransaction(async txc => { + const reviewTransactionResponse = await txc.run(cypher, { + params, + moderatorId: moderator.id, + dateTime: new Date().toISOString(), + }) + return reviewTransactionResponse.records.map(transformReturnType) }) - if (!resource) return null - return resource.id - } finally { - session.close() - } - }, - enable: async (object, params, { user, driver }) => { - const { id } = params - const cypher = ` - MATCH (resource {id: $id})<-[d:DISABLED]-() - SET resource.disabled = false - DELETE d - RETURN resource {.id} - ` - const session = driver.session() - try { - const res = await session.run(cypher, { id }) - const [resource] = res.records.map(record => { - return record.get('resource') - }) - if (!resource) return null - return resource.id + const txResult = await reviewWriteTxResultPromise + if (!txResult[0]) return null + createdRelationshipWithNestedAttributes = txResult[0] } finally { session.close() } + + return createdRelationshipWithNestedAttributes }, }, } diff --git a/backend/src/schema/resolvers/moderation.spec.js b/backend/src/schema/resolvers/moderation.spec.js index 765126c52..f76cbdf46 100644 --- a/backend/src/schema/resolvers/moderation.spec.js +++ b/backend/src/schema/resolvers/moderation.spec.js @@ -1,52 +1,60 @@ import { createTestClient } from 'apollo-server-testing' import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' -import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' import createServer from '../../server' const factory = Factory() const neode = getNeode() const driver = getDriver() -let query, mutate, authenticatedUser, variables, moderator, nonModerator +let mutate, + authenticatedUser, + disableVariables, + enableVariables, + moderator, + nonModerator, + closeReportVariables -const disableMutation = gql` - mutation($id: ID!) { - disable(id: $id) - } -` -const enableMutation = gql` - mutation($id: ID!) { - enable(id: $id) - } -` - -const commentQuery = gql` - query($id: ID!) { - Comment(id: $id) { - id - disabled - disabledBy { - id +const reviewMutation = gql` + mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) { + review(resourceId: $resourceId, disable: $disable, closed: $closed) { + createdAt + updatedAt + resource { + __typename + ... on User { + id + disabled + } + ... on Post { + id + disabled + } + ... on Comment { + id + disabled + } } - } - } -` - -const postQuery = gql` - query($id: ID) { - Post(id: $id) { - id - disabled - disabledBy { + report { id + createdAt + updatedAt + closed + reviewed { + createdAt + moderator { + id + } + } } } } ` describe('moderate resources', () => { - beforeAll(() => { + beforeAll(async () => { + await factory.cleanDatabase() authenticatedUser = undefined const { server } = createServer({ context: () => { @@ -58,11 +66,19 @@ describe('moderate resources', () => { }, }) mutate = createTestClient(server).mutate - query = createTestClient(server).query }) beforeEach(async () => { - variables = {} + disableVariables = { + resourceId: 'undefined-resource', + disable: true, + closed: false, + } + enableVariables = { + resourceId: 'undefined-resource', + disable: false, + closed: false, + } authenticatedUser = null moderator = await factory.create('User', { id: 'moderator-id', @@ -71,155 +87,392 @@ describe('moderate resources', () => { password: '1234', role: 'moderator', }) + nonModerator = await factory.create('User', { + id: 'non-moderator', + name: 'Non Moderator', + email: 'non.moderator@example.org', + password: '1234', + }) }) afterEach(async () => { await factory.cleanDatabase() }) - describe('disable', () => { - beforeEach(() => { - variables = { - id: 'some-resource', - } - }) + describe('review to close report, leaving resource enabled', () => { describe('unauthenticated', () => { it('throws authorization error', async () => { - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ errors: [{ message: 'Not Authorised!' }], }) }) }) + describe('authenticated', () => { - describe('non moderator', () => { - beforeEach(async () => { - nonModerator = await factory.create('User', { - id: 'non-moderator', - name: 'Non Moderator', - email: 'non.moderator@example.org', - password: '1234', - }) - authenticatedUser = await nonModerator.toJson() + beforeEach(async () => { + authenticatedUser = await nonModerator.toJson() + }) + + it('non-moderator receives an authorization error', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], }) - it('throws authorization error', async () => { - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + }) + }) + + describe('moderator', () => { + beforeEach(async () => { + authenticatedUser = await moderator.toJson() + const questionablePost = await factory.create('Post', { + id: 'should-i-be-disabled', + }) + const reportAgainstQuestionablePost = await factory.create('Report') + await Promise.all([ + reportAgainstQuestionablePost.relateTo(nonModerator, 'filed', { + resourceId: 'should-i-be-disabled', + reasonCategory: 'doxing', + reasonDescription: "This shouldn't be shown to anybody else! It's my private thing!", + }), + reportAgainstQuestionablePost.relateTo(questionablePost, 'belongsTo'), + ]) + closeReportVariables = { + resourceId: 'should-i-be-disabled', + disable: false, + closed: true, + } + }) + + it('report can be closed without disabling resource', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: closeReportVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Post', id: 'should-i-be-disabled', disabled: false }, + report: { id: expect.any(String), closed: true }, + }, + }, + errors: undefined, + }) + }) + + it('creates only one review for multiple reviews by the same moderator on same resource', async () => { + await Promise.all([ + mutate({ + mutation: reviewMutation, + variables: { ...disableVariables, resourceId: 'should-i-be-disabled' }, + }), + mutate({ + mutation: reviewMutation, + variables: { ...enableVariables, resourceId: 'should-i-be-disabled' }, + }), + ]) + const cypher = + 'MATCH (:Report)<-[review:REVIEWED]-(moderator:User {id: "moderator-id"}) RETURN review' + const reviews = await neode.cypher(cypher) + expect(reviews.records).toHaveLength(1) + }) + + it('updates the updatedAt attribute', async () => { + const [firstReview, secondReview] = await Promise.all([ + mutate({ + mutation: reviewMutation, + variables: { ...disableVariables, resourceId: 'should-i-be-disabled' }, + }), + mutate({ + mutation: reviewMutation, + variables: { ...enableVariables, resourceId: 'should-i-be-disabled' }, + }), + ]) + expect(firstReview.data.review.updatedAt).toBeTruthy() + expect(Date.parse(firstReview.data.review.updatedAt)).toEqual(expect.any(Number)) + expect(secondReview.data.review.updatedAt).toBeTruthy() + expect(Date.parse(secondReview.data.review.updatedAt)).toEqual(expect.any(Number)) + expect(firstReview.data.review.updatedAt).not.toEqual(secondReview.data.review.updatedAt) + }) + }) + }) + + describe('review to disable', () => { + describe('unauthenticated', () => { + it('throws authorization error', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await nonModerator.toJson() + }) + + it('non-moderator receives an authorization error', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorised!' }], + }) + }) + }) + + describe('moderator', () => { + beforeEach(async () => { + authenticatedUser = await moderator.toJson() + }) + + describe('moderate a comment', () => { + beforeEach(async () => { + const trollingComment = await factory.create('Comment', { + id: 'comment-id', + }) + const reportAgainstTrollingComment = await factory.create('Report') + await Promise.all([ + reportAgainstTrollingComment.relateTo(nonModerator, 'filed', { + resourceId: 'comment-id', + reasonCategory: 'other', + reasonDescription: 'This comment is bigoted', + }), + reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'), + ]) + disableVariables = { + ...disableVariables, + resourceId: 'comment-id', + } + }) + + it('returns disabled resource id', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: { resource: { __typename: 'Comment', id: 'comment-id' } } }, + errors: undefined, + }) + }) + + it('returns .reviewed', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Comment', id: 'comment-id' }, + report: { + id: expect.any(String), + reviewed: expect.arrayContaining([ + { createdAt: expect.any(String), moderator: { id: 'moderator-id' } }, + ]), + }, + }, + }, + errors: undefined, + }) + }) + + it('updates .disabled on comment', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { + review: { resource: { __typename: 'Comment', id: 'comment-id', disabled: true } }, + }, + errors: undefined, + }) + }) + + it('can be closed with one review', async () => { + closeReportVariables = { + ...disableVariables, + closed: true, + } + await expect( + mutate({ mutation: reviewMutation, variables: closeReportVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Comment', id: 'comment-id' }, + report: { id: expect.any(String), closed: true }, + }, + }, + errors: undefined, }) }) }) - describe('moderator', () => { + describe('moderate a post', () => { beforeEach(async () => { - authenticatedUser = await moderator.toJson() + const trollingPost = await factory.create('Post', { + id: 'post-id', + }) + const reportAgainstTrollingPost = await factory.create('Report') + await Promise.all([ + reportAgainstTrollingPost.relateTo(nonModerator, 'filed', { + resourceId: 'post-id', + reasonCategory: 'doxing', + reasonDescription: "This shouldn't be shown to anybody else! It's my private thing!", + }), + reportAgainstTrollingPost.relateTo(trollingPost, 'belongsTo'), + ]) + disableVariables = { + ...disableVariables, + resourceId: 'post-id', + } }) - describe('moderate a resource that is not a (Comment|Post|User) ', () => { - beforeEach(async () => { - variables = { - id: 'sample-tag-id', - } - await factory.create('Tag', { id: 'sample-tag-id' }) - }) - - it('returns null', async () => { - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - data: { disable: null }, - }) + it('returns disabled resource id', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Post', id: 'post-id' }, + }, + }, + errors: undefined, }) }) - describe('moderate a comment', () => { - beforeEach(async () => { - variables = {} - await factory.create('Comment', { - id: 'comment-id', - }) - }) - - it('returns disabled resource id', async () => { - variables = { id: 'comment-id' } - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - data: { disable: 'comment-id' }, - errors: undefined, - }) - }) - - it('changes .disabledBy', async () => { - variables = { id: 'comment-id' } - const before = { data: { Comment: [{ id: 'comment-id', disabledBy: null }] } } - const expected = { - data: { Comment: [{ id: 'comment-id', disabledBy: { id: 'moderator-id' } }] }, - } - await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(before) - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - data: { disable: 'comment-id' }, - }) - await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected) - }) - - it('updates .disabled on comment', async () => { - variables = { id: 'comment-id' } - const before = { data: { Comment: [{ id: 'comment-id', disabled: false }] } } - const expected = { data: { Comment: [{ id: 'comment-id', disabled: true }] } } - - await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(before) - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - data: { disable: 'comment-id' }, - }) - await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected) + it('returns .reviewed', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Post', id: 'post-id' }, + report: { + id: expect.any(String), + reviewed: expect.arrayContaining([ + { createdAt: expect.any(String), moderator: { id: 'moderator-id' } }, + ]), + }, + }, + }, + errors: undefined, }) }) - describe('moderate a post', () => { - beforeEach(async () => { - variables = {} - await factory.create('Post', { - id: 'sample-post-id', - }) + it('updates .disabled on post', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: { resource: { __typename: 'Post', id: 'post-id', disabled: true } } }, + errors: undefined, }) + }) - it('returns disabled resource id', async () => { - variables = { id: 'sample-post-id' } - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - data: { disable: 'sample-post-id' }, - }) + it('can be closed with one review', async () => { + closeReportVariables = { + ...disableVariables, + closed: true, + } + await expect( + mutate({ mutation: reviewMutation, variables: closeReportVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Post', id: 'post-id' }, + report: { id: expect.any(String), closed: true }, + }, + }, + errors: undefined, }) + }) + }) - it('changes .disabledBy', async () => { - variables = { id: 'sample-post-id' } - const before = { data: { Post: [{ id: 'sample-post-id', disabledBy: null }] } } - const expected = { - data: { Post: [{ id: 'sample-post-id', disabledBy: { id: 'moderator-id' } }] }, - } - - await expect(query({ query: postQuery, variables })).resolves.toMatchObject(before) - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - data: { disable: 'sample-post-id' }, - }) - await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected) + describe('moderate a user', () => { + beforeEach(async () => { + const troll = await factory.create('User', { + id: 'user-id', }) + const reportAgainstTroll = await factory.create('Report') + await Promise.all([ + reportAgainstTroll.relateTo(nonModerator, 'filed', { + resourceId: 'user-id', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This user is harassing me with bigoted remarks!', + }), + reportAgainstTroll.relateTo(troll, 'belongsTo'), + ]) + disableVariables = { + ...disableVariables, + resourceId: 'user-id', + } + }) - it('updates .disabled on post', async () => { - const before = { data: { Post: [{ id: 'sample-post-id', disabled: false }] } } - const expected = { data: { Post: [{ id: 'sample-post-id', disabled: true }] } } - variables = { id: 'sample-post-id' } + it('returns disabled resource id', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: { resource: { __typename: 'User', id: 'user-id' } } }, + errors: undefined, + }) + }) - await expect(query({ query: postQuery, variables })).resolves.toMatchObject(before) - await expect(mutate({ mutation: disableMutation, variables })).resolves.toMatchObject({ - data: { disable: 'sample-post-id' }, - }) - await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected) + it('returns .reviewed', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'User', id: 'user-id' }, + report: { + id: expect.any(String), + reviewed: expect.arrayContaining([ + { createdAt: expect.any(String), moderator: { id: 'moderator-id' } }, + ]), + }, + }, + }, + errors: undefined, + }) + }) + + it('updates .disabled on user', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: { resource: { __typename: 'User', id: 'user-id', disabled: true } } }, + errors: undefined, + }) + }) + + it('can be closed with one review', async () => { + closeReportVariables = { + ...disableVariables, + closed: true, + } + await expect( + mutate({ mutation: reviewMutation, variables: closeReportVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'User', id: 'user-id' }, + report: { id: expect.any(String), closed: true }, + }, + }, + errors: undefined, }) }) }) }) }) - describe('enable', () => { + describe('review to re-enable after disabled', () => { describe('unautenticated user', () => { it('throws authorization error', async () => { - variables = { id: 'sample-post-id' } - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ + enableVariables = { + ...enableVariables, + resourceId: 'post-id', + } + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ errors: [{ message: 'Not Authorised!' }], }) }) @@ -228,17 +481,17 @@ describe('moderate resources', () => { describe('authenticated user', () => { describe('non moderator', () => { beforeEach(async () => { - nonModerator = await factory.create('User', { - id: 'non-moderator', - name: 'Non Moderator', - email: 'non.moderator@example.org', - password: '1234', - }) authenticatedUser = await nonModerator.toJson() }) + it('throws authorization error', async () => { - variables = { id: 'sample-post-id' } - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ + enableVariables = { + ...enableVariables, + resourceId: 'post-id', + } + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ errors: [{ message: 'Not Authorised!' }], }) }) @@ -248,101 +501,197 @@ describe('moderate resources', () => { beforeEach(async () => { authenticatedUser = await moderator.toJson() }) - describe('moderate a resource that is not a (Comment|Post|User) ', () => { - beforeEach(async () => { - await Promise.all([factory.create('Tag', { id: 'sample-tag-id' })]) - }) - - it('returns null', async () => { - await expect( - mutate({ mutation: enableMutation, variables: { id: 'sample-tag-id' } }), - ).resolves.toMatchObject({ - data: { enable: null }, - }) - }) - }) describe('moderate a comment', () => { beforeEach(async () => { - variables = { id: 'comment-id' } - await factory.create('Comment', { + const trollingComment = await factory.create('Comment', { id: 'comment-id', }) - await mutate({ mutation: disableMutation, variables }) + const reportAgainstTrollingComment = await factory.create('Report') + await Promise.all([ + reportAgainstTrollingComment.relateTo(nonModerator, 'filed', { + resourceId: 'comment-id', + reasonCategory: 'other', + reasonDescription: 'This comment is bigoted', + }), + reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'), + ]) + await Promise.all([ + reportAgainstTrollingComment.relateTo(moderator, 'reviewed', { + ...disableVariables, + resourceId: 'comment-id', + }), + trollingComment.update({ disabled: true, updatedAt: new Date().toISOString() }), + ]) + enableVariables = { + ...enableVariables, + resourceId: 'comment-id', + } }) it('returns enabled resource id', async () => { - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ - data: { enable: 'comment-id' }, - errors: undefined, + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { review: { resource: { __typename: 'Comment', id: 'comment-id' } } }, }) }) - it('changes .disabledBy', async () => { - const expected = { - data: { Comment: [{ id: 'comment-id', disabledBy: null }] }, - errors: undefined, - } - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ - data: { enable: 'comment-id' }, - errors: undefined, + it('returns .reviewed', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Comment', id: 'comment-id' }, + report: { + id: expect.any(String), + reviewed: expect.arrayContaining([ + { createdAt: expect.any(String), moderator: { id: 'moderator-id' } }, + ]), + }, + }, + }, }) - await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected) }) it('updates .disabled on comment', async () => { - const expected = { - data: { Comment: [{ id: 'comment-id', disabled: false }] }, - errors: undefined, - } - - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ - data: { enable: 'comment-id' }, - errors: undefined, + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { + review: { resource: { __typename: 'Comment', id: 'comment-id', disabled: false } }, + }, }) - await expect(query({ query: commentQuery, variables })).resolves.toMatchObject(expected) }) }) describe('moderate a post', () => { beforeEach(async () => { - variables = { id: 'post-id' } - await factory.create('Post', { + const trollingPost = await factory.create('Post', { id: 'post-id', }) - await mutate({ mutation: disableMutation, variables }) + const reportAgainstTrollingPost = await factory.create('Report') + await Promise.all([ + reportAgainstTrollingPost.relateTo(nonModerator, 'filed', { + resourceId: 'post-id', + reasonCategory: 'doxing', + reasonDescription: + "This shouldn't be shown to anybody else! It's my private thing!", + }), + reportAgainstTrollingPost.relateTo(trollingPost, 'belongsTo'), + ]) + await Promise.all([ + reportAgainstTrollingPost.relateTo(moderator, 'reviewed', { + ...disableVariables, + resourceId: 'comment-id', + }), + trollingPost.update({ disabled: true, updatedAt: new Date().toISOString() }), + ]) + enableVariables = { + ...enableVariables, + resourceId: 'post-id', + } }) it('returns enabled resource id', async () => { - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ - data: { enable: 'post-id' }, - errors: undefined, + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { review: { resource: { __typename: 'Post', id: 'post-id' } } }, }) }) - it('changes .disabledBy', async () => { - const expected = { - data: { Post: [{ id: 'post-id', disabledBy: null }] }, - errors: undefined, - } - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ - data: { enable: 'post-id' }, - errors: undefined, + it('returns .reviewed', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'Post', id: 'post-id' }, + report: { + id: expect.any(String), + reviewed: expect.arrayContaining([ + { createdAt: expect.any(String), moderator: { id: 'moderator-id' } }, + ]), + }, + }, + }, }) - await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected) }) it('updates .disabled on post', async () => { - const expected = { - data: { Post: [{ id: 'post-id', disabled: false }] }, - errors: undefined, - } - - await expect(mutate({ mutation: enableMutation, variables })).resolves.toMatchObject({ - data: { enable: 'post-id' }, - errors: undefined, + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { + review: { resource: { __typename: 'Post', id: 'post-id', disabled: false } }, + }, + }) + }) + }) + + describe('moderate a user', () => { + beforeEach(async () => { + const troll = await factory.create('User', { + id: 'user-id', + }) + const reportAgainstTroll = await factory.create('Report') + await Promise.all([ + reportAgainstTroll.relateTo(nonModerator, 'filed', { + resourceId: 'user-id', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This user is harassing me with bigoted remarks!', + }), + reportAgainstTroll.relateTo(troll, 'belongsTo'), + ]) + await Promise.all([ + reportAgainstTroll.relateTo(moderator, 'reviewed', { + ...disableVariables, + resourceId: 'comment-id', + }), + troll.update({ disabled: true, updatedAt: new Date().toISOString() }), + ]) + enableVariables = { + ...enableVariables, + resourceId: 'user-id', + } + }) + + it('returns enabled resource id', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { review: { resource: { __typename: 'User', id: 'user-id' } } }, + }) + }) + + it('returns .reviewed', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { + review: { + resource: { __typename: 'User', id: 'user-id' }, + report: { + id: expect.any(String), + reviewed: expect.arrayContaining([ + { createdAt: expect.any(String), moderator: { id: 'moderator-id' } }, + ]), + }, + }, + }, + }) + }) + + it('updates .disabled on user', async () => { + await expect( + mutate({ mutation: reviewMutation, variables: enableVariables }), + ).resolves.toMatchObject({ + data: { + review: { resource: { __typename: 'User', id: 'user-id', disabled: false } }, + }, }) - await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected) }) }) }) diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index 97aa6a020..a1968d288 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -1,6 +1,6 @@ import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' -import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' import createPasswordReset from './helpers/createPasswordReset' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 1322b10e4..b37a4abd5 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -5,6 +5,7 @@ import { getBlockedUsers, getBlockedByUsers } from './users.js' import { mergeWith, isArray, isEmpty } from 'lodash' import { UserInputError } from 'apollo-server' import Resolver from './helpers/Resolver' + const filterForBlockedUsers = async (params, context) => { if (!context.user) return params const [blockedUsers, blockedByUsers] = await Promise.all([ @@ -308,7 +309,15 @@ export default { }, Post: { ...Resolver('Post', { - undefinedToNull: ['activityId', 'objectId', 'image', 'language', 'pinnedAt', 'pinned'], + undefinedToNull: [ + 'activityId', + 'objectId', + 'image', + 'language', + 'pinnedAt', + 'pinned', + 'imageAspectRatio', + ], hasMany: { tags: '-[:TAGGED]->(related:Tag)', categories: '-[:CATEGORIZED]->(related:Category)', @@ -318,7 +327,6 @@ export default { }, hasOne: { author: '<-[:WROTE]-(related:User)', - disabledBy: '<-[:DISABLED]-(related:User)', pinnedBy: '<-[:PINNED]-(related:User)', }, count: { diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 98475b182..752602fd9 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' -import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' import createServer from '../../server' const driver = getDriver() diff --git a/backend/src/schema/resolvers/registration.js b/backend/src/schema/resolvers/registration.js index 206c8db74..9d5d5f09a 100644 --- a/backend/src/schema/resolvers/registration.js +++ b/backend/src/schema/resolvers/registration.js @@ -1,12 +1,12 @@ import { UserInputError } from 'apollo-server' -import { neode } from '../../bootstrap/neo4j' +import { getNeode } from '../../bootstrap/neo4j' import fileUpload from './fileUpload' import encryptPassword from '../../helpers/encryptPassword' import generateNonce from './helpers/generateNonce' import existingEmailAddress from './helpers/existingEmailAddress' import normalizeEmail from './helpers/normalizeEmail' -const instance = neode() +const neode = getNeode() export default { Mutation: { @@ -16,7 +16,7 @@ export default { let emailAddress = await existingEmailAddress({ args, context }) if (emailAddress) return emailAddress try { - emailAddress = await instance.create('EmailAddress', args) + emailAddress = await neode.create('EmailAddress', args) return emailAddress.toJson() } catch (e) { throw new UserInputError(e.message) @@ -32,7 +32,7 @@ export default { let { nonce, email } = args email = normalizeEmail(email) - const result = await instance.cypher( + const result = await neode.cypher( ` MATCH(email:EmailAddress {nonce: {nonce}, email: {email}}) WHERE NOT (email)-[:BELONGS_TO]->() @@ -40,12 +40,12 @@ export default { `, { nonce, email }, ) - const emailAddress = await instance.hydrateFirst(result, 'email', instance.model('Email')) + const emailAddress = await neode.hydrateFirst(result, 'email', neode.model('Email')) if (!emailAddress) throw new UserInputError('Invalid email or nonce') args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' }) args = await encryptPassword(args) try { - const user = await instance.create('User', args) + const user = await neode.create('User', args) await Promise.all([ user.relateTo(emailAddress, 'primaryEmail'), emailAddress.relateTo(user, 'belongsTo'), diff --git a/backend/src/schema/resolvers/registration.spec.js b/backend/src/schema/resolvers/registration.spec.js index 35b16b9bb..8f3a7ac39 100644 --- a/backend/src/schema/resolvers/registration.spec.js +++ b/backend/src/schema/resolvers/registration.spec.js @@ -1,6 +1,6 @@ import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' -import { getDriver, neode as getNeode } from '../../bootstrap/neo4j' +import { getDriver, getNeode } from '../../bootstrap/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/reports.js b/backend/src/schema/resolvers/reports.js index 8e12f1dba..a1d98bb41 100644 --- a/backend/src/schema/resolvers/reports.js +++ b/backend/src/schema/resolvers/reports.js @@ -1,57 +1,47 @@ +const transformReturnType = record => { + return { + ...record.get('report').properties, + resource: { + __typename: record.get('type'), + ...record.get('resource').properties, + }, + } +} + export default { Mutation: { - report: async (_parent, params, context, _resolveInfo) => { + fileReport: async (_parent, params, context, _resolveInfo) => { let createdRelationshipWithNestedAttributes const { resourceId, reasonCategory, reasonDescription } = params const { driver, user } = context const session = driver.session() - try { - const writeTxResultPromise = session.writeTransaction(async txc => { - const reportRelationshipTransactionResponse = await txc.run( - ` + const reportWriteTxResultPromise = session.writeTransaction(async txc => { + const reportTransactionResponse = 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 + WHERE resource:User OR resource:Post OR resource:Comment + MERGE (resource)<-[:BELONGS_TO]-(report:Report {closed: false}) + ON CREATE SET report.id = randomUUID(), report.createdAt = $createdAt, report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.disable = resource.disabled, report.closed = false + WITH submitter, resource, report + CREATE (report)<-[filed:FILED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter) + + RETURN report, 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'), - })) - }) - const txResult = await writeTxResultPromise + { + resourceId, + submitterId: user.id, + createdAt: new Date().toISOString(), + reasonCategory, + reasonDescription, + }, + ) + return reportTransactionResponse.records.map(transformReturnType) + }) + try { + const txResult = await reportWriteTxResultPromise 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 - } + createdRelationshipWithNestedAttributes = txResult[0] } finally { session.close() } @@ -61,8 +51,8 @@ export default { Query: { reports: async (_parent, params, context, _resolveInfo) => { const { driver } = context - let response - let orderByClause + const session = driver.session() + let reports, orderByClause, filterClause switch (params.orderBy) { case 'createdAt_asc': orderByClause = 'ORDER BY report.createdAt ASC' @@ -73,56 +63,123 @@ export default { default: orderByClause = '' } - const session = driver.session() + + switch (params.reviewed) { + case true: + filterClause = 'AND ((report)<-[:REVIEWED]-(:User))' + break + case false: + filterClause = 'AND NOT ((report)<-[:REVIEWED]-(:User))' + break + default: + filterClause = '' + } + + if (params.closed) filterClause = 'AND report.closed = true' + + const offset = + params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : '' + const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : '' + + const reportReadTxPromise = session.readTransaction(async tx => { + const allReportsTransactionResponse = await tx.run( + ` + MATCH (report:Report)-[:BELONGS_TO]->(resource) + WHERE (resource:User OR resource:Post OR resource:Comment) + ${filterClause} + WITH report, resource, + [(submitter:User)-[filed:FILED]->(report) | filed {.*, submitter: properties(submitter)} ] as filed, + [(moderator:User)-[reviewed:REVIEWED]->(report) | reviewed {.*, moderator: properties(moderator)} ] as reviewed, + [(resource)<-[:WROTE]-(author:User) | author {.*} ] as optionalAuthors, + [(resource)-[:COMMENTS]->(post:Post) | post {.*} ] as optionalCommentedPosts, + resource {.*, __typename: labels(resource)[0] } as resourceWithType + WITH report, optionalAuthors, optionalCommentedPosts, reviewed, filed, + resourceWithType {.*, post: optionalCommentedPosts[0], author: optionalAuthors[0] } as finalResource + RETURN report {.*, resource: finalResource, filed: filed, reviewed: reviewed } + ${orderByClause} + ${offset} ${limit} + `, + ) + return allReportsTransactionResponse.records.map(record => record.get('report')) + }) try { - const cypher = ` - MATCH (submitter:User)-[report:REPORTED]->(resource) - WHERE resource:User OR resource:Comment OR resource:Post - RETURN report, submitter, resource, labels(resource)[0] as type - ${orderByClause} - ` - const result = await session.run(cypher, {}) - const dbResponse = result.records.map(r => { - return { - report: r.get('report'), - submitter: r.get('submitter'), - resource: r.get('resource'), - type: r.get('type'), + const txResult = await reportReadTxPromise + if (!txResult[0]) return null + reports = txResult + } finally { + session.close() + } + return reports + }, + }, + Report: { + filed: async (parent, _params, context, _resolveInfo) => { + if (typeof parent.filed !== 'undefined') return parent.filed + const session = context.driver.session() + const { id } = parent + let filed + const readTxPromise = session.readTransaction(async tx => { + const allReportsTransactionResponse = await tx.run( + ` + MATCH (submitter:User)-[filed:FILED]->(report:Report {id: $id}) + RETURN filed, submitter + `, + { id }, + ) + return allReportsTransactionResponse.records.map(record => ({ + submitter: record.get('submitter').properties, + filed: record.get('filed').properties, + })) + }) + try { + const txResult = await readTxPromise + if (!txResult[0]) return null + filed = txResult.map(reportedRecord => { + const { submitter, filed } = reportedRecord + const relationshipWithNestedAttributes = { + ...filed, + submitter, } - }) - if (!dbResponse) return null - - 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 relationshipWithNestedAttributes }) } finally { session.close() } - - return response + return filed + }, + reviewed: async (parent, _params, context, _resolveInfo) => { + if (typeof parent.reviewed !== 'undefined') return parent.reviewed + const session = context.driver.session() + const { id } = parent + let reviewed + const readTxPromise = session.readTransaction(async tx => { + const allReportsTransactionResponse = await tx.run( + ` + MATCH (resource)<-[:BELONGS_TO]-(report:Report {id: $id})<-[review:REVIEWED]-(moderator:User) + RETURN moderator, review + ORDER BY report.updatedAt DESC, review.updatedAt DESC + `, + { id }, + ) + return allReportsTransactionResponse.records.map(record => ({ + review: record.get('review').properties, + moderator: record.get('moderator').properties, + })) + }) + try { + const txResult = await readTxPromise + reviewed = txResult.map(reportedRecord => { + const { review, moderator } = reportedRecord + const relationshipWithNestedAttributes = { + ...review, + moderator, + } + return relationshipWithNestedAttributes + }) + } finally { + session.close() + } + return reviewed }, }, } diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js index 2ecd1f20d..cd8b61985 100644 --- a/backend/src/schema/resolvers/reports.spec.js +++ b/backend/src/schema/resolvers/reports.spec.js @@ -2,37 +2,47 @@ import { createTestClient } from 'apollo-server-testing' import createServer from '../.././server' import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' -import { getDriver, neode as getNeode } from '../../bootstrap/neo4j' +import { getDriver, getNeode } from '../../bootstrap/neo4j' const factory = Factory() const instance = getNeode() const driver = getDriver() -describe('report resources', () => { - let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser +describe('file a report on a resource', () => { + let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser, otherReportingUser const categoryIds = ['cat9'] const reportMutation = gql` mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { - report( + fileReport( resourceId: $resourceId reasonCategory: $reasonCategory reasonDescription: $reasonDescription ) { + id createdAt - reasonCategory - reasonDescription - type - submitter { - email + updatedAt + disable + closed + rule + resource { + __typename + ... on User { + name + } + ... on Post { + title + } + ... on Comment { + content + } } - user { - name - } - post { - title - } - comment { - content + filed { + submitter { + id + } + createdAt + reasonCategory + reasonDescription } } } @@ -67,7 +77,7 @@ describe('report resources', () => { it('throws authorization error', async () => { authenticatedUser = null await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({ - data: { report: null }, + data: { fileReport: null }, errors: [{ message: 'Not Authorised!' }], }) }) @@ -81,6 +91,12 @@ describe('report resources', () => { email: 'test@example.org', password: '1234', }) + otherReportingUser = await factory.create('User', { + id: 'other-reporting-user-id', + role: 'user', + email: 'reporting@example.org', + password: '1234', + }) await factory.create('User', { id: 'abusive-user-id', role: 'user', @@ -99,15 +115,15 @@ describe('report resources', () => { describe('invalid resource id', () => { it('returns null', async () => { await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({ - data: { report: null }, + data: { fileReport: null }, errors: undefined, }) }) }) describe('valid resource', () => { - describe('reported resource is a user', () => { - it('returns type "User"', async () => { + describe('creates report', () => { + it('which belongs to resource', async () => { await expect( mutate({ mutation: reportMutation, @@ -115,15 +131,28 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - type: 'User', + fileReport: { + id: expect.any(String), }, }, errors: undefined, }) }) - it('returns resource in user attribute', async () => { + it('creates only one report for multiple reports on the same resource', async () => { + const firstReport = await mutate({ + mutation: reportMutation, + variables: { ...variables, resourceId: 'abusive-user-id' }, + }) + authenticatedUser = await otherReportingUser.toJson() + const secondReport = await mutate({ + mutation: reportMutation, + 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 () => { await expect( mutate({ mutation: reportMutation, @@ -131,8 +160,46 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - user: { + fileReport: { + rule: 'latestReviewUpdatedAtRules', + }, + }, + errors: undefined, + }) + }) + it.todo('creates multiple filed reports') + }) + + describe('reported resource is a user', () => { + it('returns __typename "User"', async () => { + await expect( + mutate({ + mutation: reportMutation, + variables: { ...variables, resourceId: 'abusive-user-id' }, + }), + ).resolves.toMatchObject({ + data: { + fileReport: { + resource: { + __typename: 'User', + }, + }, + }, + errors: undefined, + }) + }) + + it('returns user attribute info', async () => { + await expect( + mutate({ + mutation: reportMutation, + variables: { ...variables, resourceId: 'abusive-user-id' }, + }), + ).resolves.toMatchObject({ + data: { + fileReport: { + resource: { + __typename: 'User', name: 'abusive-user', }, }, @@ -149,10 +216,14 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - submitter: { - email: 'test@example.org', - }, + fileReport: { + filed: [ + { + submitter: { + id: 'current-user-id', + }, + }, + ], }, }, errors: undefined, @@ -167,7 +238,7 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { + fileReport: { createdAt: expect.any(String), }, }, @@ -187,8 +258,12 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - reasonCategory: 'criminal_behavior_violation_german_law', + fileReport: { + filed: [ + { + reasonCategory: 'criminal_behavior_violation_german_law', + }, + ], }, }, errors: undefined, @@ -228,15 +303,19 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - reasonDescription: 'My reason!', + fileReport: { + filed: [ + { + reasonDescription: 'My reason!', + }, + ], }, }, errors: undefined, }) }) - it('sanitize the reason description', async () => { + it('sanitizes the reason description', async () => { await expect( mutate({ mutation: reportMutation, @@ -248,8 +327,12 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - reasonDescription: 'My reason !', + fileReport: { + filed: [ + { + reasonDescription: 'My reason !', + }, + ], }, }, errors: undefined, @@ -278,8 +361,10 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - type: 'Post', + fileReport: { + resource: { + __typename: 'Post', + }, }, }, errors: undefined, @@ -297,8 +382,9 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - post: { + fileReport: { + resource: { + __typename: 'Post', title: 'This is a post that is going to be reported', }, }, @@ -306,25 +392,6 @@ describe('report resources', () => { errors: undefined, }) }) - - it('returns null in user attribute', async () => { - await expect( - mutate({ - mutation: reportMutation, - variables: { - ...variables, - resourceId: 'post-to-report-id', - }, - }), - ).resolves.toMatchObject({ - data: { - report: { - user: null, - }, - }, - errors: undefined, - }) - }) }) describe('reported resource is a comment', () => { @@ -356,8 +423,10 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - type: 'Comment', + fileReport: { + resource: { + __typename: 'Comment', + }, }, }, errors: undefined, @@ -375,8 +444,9 @@ describe('report resources', () => { }), ).resolves.toMatchObject({ data: { - report: { - comment: { + fileReport: { + resource: { + __typename: 'Comment', content: 'Post comment to be reported.', }, }, @@ -403,7 +473,7 @@ describe('report resources', () => { }, }), ).resolves.toMatchObject({ - data: { report: null }, + data: { fileReport: null }, errors: undefined, }) }) @@ -411,25 +481,35 @@ describe('report resources', () => { }) }) }) + describe('query for reported resource', () => { const reportsQuery = gql` query { reports(orderBy: createdAt_desc) { + id createdAt - reasonCategory - reasonDescription - submitter { - id + updatedAt + disable + closed + resource { + __typename + ... on User { + id + } + ... on Post { + id + } + ... on Comment { + id + } } - type - user { - id - } - post { - id - } - comment { - id + filed { + submitter { + id + } + createdAt + reasonCategory + reasonDescription } } } @@ -437,7 +517,6 @@ describe('report resources', () => { beforeEach(async () => { authenticatedUser = null - moderator = await factory.create('User', { id: 'moderator-1', role: 'moderator', @@ -518,6 +597,7 @@ describe('report resources', () => { ]) authenticatedUser = null }) + describe('unauthenticated', () => { it('throws authorization error', async () => { authenticatedUser = null @@ -527,6 +607,7 @@ describe('report resources', () => { }) }) }) + describe('authenticated', () => { it('role "user" gets no reports', async () => { authenticatedUser = await currentUser.toJson() @@ -538,49 +619,69 @@ describe('report resources', () => { 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({ + id: expect.any(String), createdAt: expect.any(String), - reasonCategory: 'doxing', - reasonDescription: 'This user is harassing me with bigoted remarks', - submitter: expect.objectContaining({ - id: 'current-user-id', - }), - type: 'User', - user: expect.objectContaining({ + updatedAt: expect.any(String), + disable: false, + closed: false, + resource: { + __typename: 'User', id: 'abusive-user-1', - }), - post: null, - comment: null, + }, + filed: expect.arrayContaining([ + expect.objectContaining({ + submitter: expect.objectContaining({ + id: 'current-user-id', + }), + createdAt: expect.any(String), + reasonCategory: 'doxing', + reasonDescription: 'This user is harassing me with bigoted remarks', + }), + ]), }), expect.objectContaining({ + id: expect.any(String), createdAt: expect.any(String), - reasonCategory: 'other', - reasonDescription: 'This comment is bigoted', - submitter: expect.objectContaining({ - id: 'current-user-id', - }), - type: 'Post', - user: null, - post: expect.objectContaining({ + updatedAt: expect.any(String), + disable: false, + closed: false, + resource: { + __typename: 'Post', id: 'abusive-post-1', - }), - comment: null, + }, + filed: expect.arrayContaining([ + expect.objectContaining({ + submitter: expect.objectContaining({ + id: 'current-user-id', + }), + createdAt: expect.any(String), + reasonCategory: 'other', + reasonDescription: 'This comment is bigoted', + }), + ]), }), expect.objectContaining({ + id: expect.any(String), createdAt: expect.any(String), - reasonCategory: 'discrimination_etc', - reasonDescription: 'This post is bigoted', - submitter: expect.objectContaining({ - id: 'current-user-id', - }), - type: 'Comment', - user: null, - post: null, - comment: expect.objectContaining({ + updatedAt: expect.any(String), + disable: false, + closed: false, + resource: { + __typename: 'Comment', id: 'abusive-comment-1', - }), + }, + filed: expect.arrayContaining([ + expect.objectContaining({ + submitter: expect.objectContaining({ + id: 'current-user-id', + }), + createdAt: expect.any(String), + reasonCategory: 'discrimination_etc', + reasonDescription: 'This post is bigoted', + }), + ]), }), ]), } diff --git a/backend/src/schema/resolvers/rewards.js b/backend/src/schema/resolvers/rewards.js index 74c7860e4..4d5d62aea 100644 --- a/backend/src/schema/resolvers/rewards.js +++ b/backend/src/schema/resolvers/rewards.js @@ -1,11 +1,11 @@ -import { neode } from '../../bootstrap/neo4j' +import { getNeode } from '../../bootstrap/neo4j' import { UserInputError } from 'apollo-server' -const instance = neode() +const neode = getNeode() const getUserAndBadge = async ({ badgeKey, userId }) => { - const user = await instance.first('User', 'id', userId) - const badge = await instance.first('Badge', 'id', badgeKey) + const user = await neode.first('User', 'id', userId) + const badge = await neode.first('Badge', 'id', badgeKey) if (!user) throw new UserInputError("Couldn't find a user with that id") if (!badge) throw new UserInputError("Couldn't find a badge with that id") return { user, badge } diff --git a/backend/src/schema/resolvers/rewards.spec.js b/backend/src/schema/resolvers/rewards.spec.js index 2dcdd5b53..e6f67ecab 100644 --- a/backend/src/schema/resolvers/rewards.spec.js +++ b/backend/src/schema/resolvers/rewards.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' -import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' import createServer from '../../server' const factory = Factory() diff --git a/backend/src/schema/resolvers/shout.spec.js b/backend/src/schema/resolvers/shout.spec.js index f39e4d137..e747946aa 100644 --- a/backend/src/schema/resolvers/shout.spec.js +++ b/backend/src/schema/resolvers/shout.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' -import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' import createServer from '../../server' let mutate, query, authenticatedUser, variables diff --git a/backend/src/schema/resolvers/socialMedia.js b/backend/src/schema/resolvers/socialMedia.js index 49aa6788d..c206778e5 100644 --- a/backend/src/schema/resolvers/socialMedia.js +++ b/backend/src/schema/resolvers/socialMedia.js @@ -1,14 +1,14 @@ -import { neode } from '../../bootstrap/neo4j' +import { getNeode } from '../../bootstrap/neo4j' import Resolver from './helpers/Resolver' -const instance = neode() +const neode = getNeode() export default { Mutation: { CreateSocialMedia: async (object, params, context, resolveInfo) => { const [user, socialMedia] = await Promise.all([ - instance.find('User', context.user.id), - instance.create('SocialMedia', params), + neode.find('User', context.user.id), + neode.create('SocialMedia', params), ]) await socialMedia.relateTo(user, 'ownedBy') const response = await socialMedia.toJson() @@ -16,14 +16,14 @@ export default { return response }, UpdateSocialMedia: async (object, params, context, resolveInfo) => { - const socialMedia = await instance.find('SocialMedia', params.id) + const socialMedia = await neode.find('SocialMedia', params.id) await socialMedia.update({ url: params.url }) const response = await socialMedia.toJson() return response }, DeleteSocialMedia: async (object, { id }, context, resolveInfo) => { - const socialMedia = await instance.find('SocialMedia', id) + const socialMedia = await neode.find('SocialMedia', id) if (!socialMedia) return null await socialMedia.delete() return socialMedia.toJson() diff --git a/backend/src/schema/resolvers/socialMedia.spec.js b/backend/src/schema/resolvers/socialMedia.spec.js index 092139747..8f6d91d43 100644 --- a/backend/src/schema/resolvers/socialMedia.spec.js +++ b/backend/src/schema/resolvers/socialMedia.spec.js @@ -2,11 +2,11 @@ import { createTestClient } from 'apollo-server-testing' import createServer from '../../server' import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' -import { neode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' const driver = getDriver() const factory = Factory() -const instance = neode() +const neode = getNeode() describe('SocialMedia', () => { let socialMediaAction, someUser, ownerNode, owner @@ -27,15 +27,15 @@ describe('SocialMedia', () => { const newUrl = 'https://twitter.com/bullerby' const setUpSocialMedia = async () => { - const socialMediaNode = await instance.create('SocialMedia', { url }) + const socialMediaNode = await neode.create('SocialMedia', { url }) await socialMediaNode.relateTo(ownerNode, 'ownedBy') return socialMediaNode.toJson() } beforeEach(async () => { - const someUserNode = await instance.create('User', userParams) + const someUserNode = await neode.create('User', userParams) someUser = await someUserNode.toJson() - ownerNode = await instance.create('User', ownerParams) + ownerNode = await neode.create('User', ownerParams) owner = await ownerNode.toJson() socialMediaAction = async (user, mutation, variables) => { diff --git a/backend/src/schema/resolvers/statistics.spec.js b/backend/src/schema/resolvers/statistics.spec.js index 7ffa8ebd0..48baf00cd 100644 --- a/backend/src/schema/resolvers/statistics.spec.js +++ b/backend/src/schema/resolvers/statistics.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' -import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' import createServer from '../../server' let query, authenticatedUser diff --git a/backend/src/schema/resolvers/user_management.js b/backend/src/schema/resolvers/user_management.js index 4c4c3fc90..d5c6cd5ad 100644 --- a/backend/src/schema/resolvers/user_management.js +++ b/backend/src/schema/resolvers/user_management.js @@ -1,10 +1,10 @@ import encode from '../../jwt/encode' import bcrypt from 'bcryptjs' import { AuthenticationError } from 'apollo-server' -import { neode } from '../../bootstrap/neo4j' +import { getNeode } from '../../bootstrap/neo4j' import normalizeEmail from './helpers/normalizeEmail' -const instance = neode() +const neode = getNeode() export default { Query: { @@ -13,7 +13,7 @@ export default { }, currentUser: async (object, params, ctx, resolveInfo) => { if (!ctx.user) return null - const user = await instance.find('User', ctx.user.id) + const user = await neode.find('User', ctx.user.id) return user.toJson() }, }, @@ -53,7 +53,7 @@ export default { } }, changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => { - const currentUser = await instance.find('User', user.id) + const currentUser = await neode.find('User', user.id) const encryptedPassword = currentUser.get('encryptedPassword') if (!(await bcrypt.compareSync(oldPassword, encryptedPassword))) { diff --git a/backend/src/schema/resolvers/user_management.spec.js b/backend/src/schema/resolvers/user_management.spec.js index df8454ebb..3527e5dc2 100644 --- a/backend/src/schema/resolvers/user_management.spec.js +++ b/backend/src/schema/resolvers/user_management.spec.js @@ -5,29 +5,29 @@ import { gql } from '../../helpers/jest' import { createTestClient } from 'apollo-server-testing' import createServer, { context } from '../../server' import encode from '../../jwt/encode' -import { neode as getNeode } from '../../bootstrap/neo4j' +import { getNeode } from '../../bootstrap/neo4j' const factory = Factory() const neode = getNeode() -let query -let mutate -let variables -let req -let user +let query, mutate, variables, req, user const disable = async id => { - await factory.create('User', { id: 'u2', role: 'moderator' }) - const moderatorBearerToken = encode({ id: 'u2' }) - req = { headers: { authorization: `Bearer ${moderatorBearerToken}` } } - await mutate({ - mutation: gql` - mutation($id: ID!) { - disable(id: $id) - } - `, - variables: { id }, - }) - req = { headers: {} } + const moderator = await factory.create('User', { id: 'u2', role: 'moderator' }) + const user = await neode.find('User', id) + const reportAgainstUser = await factory.create('Report') + await Promise.all([ + reportAgainstUser.relateTo(moderator, 'filed', { + resourceId: id, + reasonCategory: 'discrimination_etc', + reasonDescription: 'This user is harassing me with bigoted remarks!', + }), + reportAgainstUser.relateTo(user, 'belongsTo'), + ]) + const disableVariables = { resourceId: user.id, disable: true, closed: false } + await Promise.all([ + reportAgainstUser.relateTo(moderator, 'reviewed', disableVariables), + user.update({ disabled: true, updatedAt: new Date().toISOString() }), + ]) } beforeEach(() => { diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index 02ed8dbac..d8d5fbb73 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -1,10 +1,10 @@ import { neo4jgraphql } from 'neo4j-graphql-js' import fileUpload from './fileUpload' -import { neode } from '../../bootstrap/neo4j' +import { getNeode } from '../../bootstrap/neo4j' import { UserInputError, ForbiddenError } from 'apollo-server' import Resolver from './helpers/Resolver' -const instance = neode() +const neode = getNeode() export const getBlockedUsers = async context => { const { neode } = context @@ -73,7 +73,7 @@ export default { block: async (object, args, context, resolveInfo) => { const { user: currentUser } = context if (currentUser.id === args.id) return null - await instance.cypher( + await neode.cypher( ` MATCH(u:User {id: $currentUser.id})-[r:FOLLOWS]->(b:User {id: $args.id}) DELETE r @@ -81,8 +81,8 @@ export default { { currentUser, args }, ) const [user, blockedUser] = await Promise.all([ - instance.find('User', currentUser.id), - instance.find('User', args.id), + neode.find('User', currentUser.id), + neode.find('User', args.id), ]) await user.relateTo(blockedUser, 'blocked') return blockedUser.toJson() @@ -90,14 +90,14 @@ export default { unblock: async (object, args, context, resolveInfo) => { const { user: currentUser } = context if (currentUser.id === args.id) return null - await instance.cypher( + await neode.cypher( ` MATCH(u:User {id: $currentUser.id})-[r:BLOCKED]->(b:User {id: $args.id}) DELETE r `, { currentUser, args }, ) - const blockedUser = await instance.find('User', args.id) + const blockedUser = await neode.find('User', args.id) return blockedUser.toJson() }, UpdateUser: async (object, args, context, resolveInfo) => { @@ -111,7 +111,7 @@ export default { } args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' }) try { - const user = await instance.find('User', args.id) + const user = await neode.find('User', args.id) if (!user) return null await user.update({ ...args, updatedAt: new Date().toISOString() }) return user.toJson() @@ -173,7 +173,7 @@ export default { if (typeof parent.email !== 'undefined') return parent.email const { id } = parent const statement = `MATCH(u:User {id: {id}})-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e` - const result = await instance.cypher(statement, { id }) + const result = await neode.cypher(statement, { id }) const [{ email }] = result.records.map(r => r.get('e').properties) return email }, @@ -212,7 +212,6 @@ export default { }, hasOne: { invitedBy: '<-[:INVITED]-(related:User)', - disabledBy: '<-[:DISABLED]-(related:User)', location: '-[:IS_IN]->(related:Location)', }, hasMany: { diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index 483c70214..26e977a31 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -1,6 +1,6 @@ import Factory from '../../seed/factories' import { gql } from '../../helpers/jest' -import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../bootstrap/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/users/blockedUsers.spec.js b/backend/src/schema/resolvers/users/blockedUsers.spec.js index e0ab00448..11bcb823d 100644 --- a/backend/src/schema/resolvers/users/blockedUsers.spec.js +++ b/backend/src/schema/resolvers/users/blockedUsers.spec.js @@ -2,11 +2,11 @@ import { createTestClient } from 'apollo-server-testing' import createServer from '../../../server' import Factory from '../../../seed/factories' import { gql } from '../../../helpers/jest' -import { neode, getDriver } from '../../../bootstrap/neo4j' +import { getNeode, getDriver } from '../../../bootstrap/neo4j' const driver = getDriver() const factory = Factory() -const instance = neode() +const neode = getNeode() let currentUser let blockedUser @@ -20,7 +20,7 @@ beforeEach(() => { return { user: authenticatedUser, driver, - neode: instance, + neode, cypherParams: { currentUserId: authenticatedUser ? authenticatedUser.id : null, }, @@ -55,11 +55,11 @@ describe('blockedUsers', () => { describe('authenticated and given a blocked user', () => { beforeEach(async () => { - currentUser = await instance.create('User', { + currentUser = await neode.create('User', { name: 'Current User', id: 'u1', }) - blockedUser = await instance.create('User', { + blockedUser = await neode.create('User', { name: 'Blocked User', id: 'u2', }) @@ -113,7 +113,7 @@ describe('block', () => { describe('authenticated', () => { beforeEach(async () => { - currentUser = await instance.create('User', { + currentUser = await neode.create('User', { name: 'Current User', id: 'u1', }) @@ -138,7 +138,7 @@ describe('block', () => { describe('given a to-be-blocked user', () => { beforeEach(async () => { - blockedUser = await instance.create('User', { + blockedUser = await neode.create('User', { name: 'Blocked User', id: 'u2', }) @@ -181,11 +181,11 @@ describe('block', () => { let postQuery beforeEach(async () => { - const post1 = await instance.create('Post', { + const post1 = await neode.create('Post', { id: 'p12', title: 'A post written by the current user', }) - const post2 = await instance.create('Post', { + const post2 = await neode.create('Post', { id: 'p23', title: 'A post written by the blocked user', }) @@ -323,7 +323,7 @@ describe('unblock', () => { describe('authenticated', () => { beforeEach(async () => { - currentUser = await instance.create('User', { + currentUser = await neode.create('User', { name: 'Current User', id: 'u1', }) @@ -348,7 +348,7 @@ describe('unblock', () => { describe('given another user', () => { beforeEach(async () => { - blockedUser = await instance.create('User', { + blockedUser = await neode.create('User', { name: 'Blocked User', id: 'u2', }) diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index 27fd2206c..35998b935 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -24,8 +24,6 @@ type Mutation { changePassword(oldPassword: String!, newPassword: String!): String! requestPasswordReset(email: String!): Boolean! resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean! - disable(id: ID!): ID - enable(id: ID!): ID # Shout the given Type and ID shout(id: ID!, type: ShoutTypeEnum): Boolean! # Unshout the given Type and ID diff --git a/backend/src/schema/types/type/Comment.gql b/backend/src/schema/types/type/Comment.gql index ba9d7a3fc..cf53df41d 100644 --- a/backend/src/schema/types/type/Comment.gql +++ b/backend/src/schema/types/type/Comment.gql @@ -47,7 +47,6 @@ type Comment { updatedAt: String deleted: Boolean disabled: Boolean - disabledBy: User @relation(name: "DISABLED", direction: "IN") } type Query { diff --git a/backend/src/schema/types/type/FILED.gql b/backend/src/schema/types/type/FILED.gql new file mode 100644 index 000000000..cdce62116 --- /dev/null +++ b/backend/src/schema/types/type/FILED.gql @@ -0,0 +1,18 @@ +type FILED { + createdAt: String! + reasonCategory: ReasonCategory! + reasonDescription: String! + submitter: User +} + +# 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 +} diff --git a/backend/src/schema/types/type/NOTIFIED.gql b/backend/src/schema/types/type/NOTIFIED.gql index 5557cbd54..af91460f7 100644 --- a/backend/src/schema/types/type/NOTIFIED.gql +++ b/backend/src/schema/types/type/NOTIFIED.gql @@ -26,7 +26,7 @@ enum NotificationReason { type Query { notifications(read: Boolean, orderBy: NotificationOrdering, first: Int, offset: Int): [NOTIFIED] } - + type Mutation { markAsRead(id: ID!): NOTIFIED } diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index b29ac5386..0f1817971 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -114,16 +114,16 @@ type Post { objectId: String author: User @relation(name: "WROTE", direction: "IN") title: String! - slug: String + slug: String! content: String! contentExcerpt: String image: String imageUpload: Upload + imageAspectRatio: Float visibility: Visibility deleted: Boolean disabled: Boolean pinned: Boolean - disabledBy: User @relation(name: "DISABLED", direction: "IN") createdAt: String updatedAt: String language: String @@ -183,6 +183,7 @@ type Mutation { language: String categoryIds: [ID] contentExcerpt: String + imageAspectRatio: Float ): Post UpdatePost( id: ID! @@ -195,6 +196,7 @@ type Mutation { visibility: Visibility language: String categoryIds: [ID] + imageAspectRatio: Float ): Post DeletePost(id: ID!): Post AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED @@ -219,6 +221,7 @@ type Query { offset: Int orderBy: [_PostOrdering] filter: _PostFilter + imageAspectRatio: Float ): [Post] PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int! PostsEmotionsByCurrentUser(postId: ID!): [String] diff --git a/backend/src/schema/types/type/REPORTED.gql b/backend/src/schema/types/type/REPORTED.gql deleted file mode 100644 index 5042672a7..000000000 --- a/backend/src/schema/types/type/REPORTED.gql +++ /dev/null @@ -1,43 +0,0 @@ -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_asc - createdAt_desc -} - -type Query { - reports(orderBy: ReportOrdering): [REPORTED] -} - -type Mutation { - report(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): REPORTED -} diff --git a/backend/src/schema/types/type/REVIEWED.gql b/backend/src/schema/types/type/REVIEWED.gql new file mode 100644 index 000000000..aea005abe --- /dev/null +++ b/backend/src/schema/types/type/REVIEWED.gql @@ -0,0 +1,15 @@ +type REVIEWED { + createdAt: String! + updatedAt: String! + disable: Boolean! + closed: Boolean! + report: Report + # @cypher(statement: "MATCH (report:Report)<-[this:REVIEWED]-(:User) RETURN report") + moderator: User + resource: ReviewedResource +} +union ReviewedResource = User | Post | Comment + +type Mutation { + review(resourceId: ID!, disable: Boolean, closed: Boolean): REVIEWED +} diff --git a/backend/src/schema/types/type/Report.gql b/backend/src/schema/types/type/Report.gql new file mode 100644 index 000000000..ad0015d01 --- /dev/null +++ b/backend/src/schema/types/type/Report.gql @@ -0,0 +1,30 @@ +type Report { + id: ID! + createdAt: String! + updatedAt: String! + rule: ReportRule! + disable: Boolean! + closed: Boolean! + filed: [FILED] + reviewed: [REVIEWED]! + resource: ReportedResource +} + +union ReportedResource = User | Post | Comment + +enum ReportRule { + latestReviewUpdatedAtRules +} + +type Mutation { + fileReport(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): Report +} + +type Query { + reports(orderBy: ReportOrdering, first: Int, offset: Int, reviewed: Boolean, closed: Boolean): [Report] +} + +enum ReportOrdering { + createdAt_asc + createdAt_desc +} diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 53e739988..243f45322 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -33,7 +33,6 @@ type User { coverImg: String deleted: Boolean disabled: Boolean - disabledBy: User @relation(name: "DISABLED", direction: "IN") role: UserGroup! publicKey: String invitedBy: User @relation(name: "INVITED", direction: "IN") @@ -44,8 +43,6 @@ type User { about: String socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN") - # createdAt: DateTime - # updatedAt: DateTime createdAt: String updatedAt: String diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index 1b090530b..10db5cc03 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -1,4 +1,4 @@ -import { getDriver, neode } from '../../bootstrap/neo4j' +import { getDriver, getNeode } from '../../bootstrap/neo4j' import createBadge from './badges.js' import createUser from './users.js' import createPost from './posts.js' @@ -10,6 +10,7 @@ import createLocation from './locations.js' import createEmailAddress from './emailAddresses.js' import createDonations from './donations.js' import createUnverifiedEmailAddresss from './unverifiedEmailAddresses.js' +import createReport from './reports.js' const factories = { Badge: createBadge, @@ -23,6 +24,7 @@ const factories = { EmailAddress: createEmailAddress, UnverifiedEmailAddress: createUnverifiedEmailAddresss, Donations: createDonations, + Report: createReport, } export const cleanDatabase = async (options = {}) => { @@ -37,7 +39,7 @@ export const cleanDatabase = async (options = {}) => { } export default function Factory(options = {}) { - const { neo4jDriver = getDriver(), neodeInstance = neode() } = options + const { neo4jDriver = getDriver(), neodeInstance = getNeode() } = options const result = { neo4jDriver, diff --git a/backend/src/seed/factories/posts.js b/backend/src/seed/factories/posts.js index 3058204a1..2443619ae 100644 --- a/backend/src/seed/factories/posts.js +++ b/backend/src/seed/factories/posts.js @@ -19,6 +19,7 @@ export default function create() { visibility: 'public', deleted: false, categoryIds: [], + imageAspectRatio: 1.333, } args = { ...defaults, diff --git a/backend/src/seed/factories/reports.js b/backend/src/seed/factories/reports.js new file mode 100644 index 000000000..e2d5ec4dc --- /dev/null +++ b/backend/src/seed/factories/reports.js @@ -0,0 +1,7 @@ +export default function create() { + return { + factory: async ({ args, neodeInstance }) => { + return neodeInstance.create('Report', args) + }, + } +} diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index 692d95542..475a7b54f 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -3,7 +3,7 @@ import sample from 'lodash/sample' import { createTestClient } from 'apollo-server-testing' import createServer from '../server' import Factory from './factories' -import { neode as getNeode, getDriver } from '../bootstrap/neo4j' +import { getNeode, getDriver } from '../bootstrap/neo4j' import { gql } from '../helpers/jest' const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] @@ -350,15 +350,17 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] author: peterLustig, id: 'p0', language: sample(languages), - image: faker.image.unsplash.food(), + image: faker.image.unsplash.food(300, 169), categoryIds: ['cat16'], + imageAspectRatio: 300 / 169, }), factory.create('Post', { author: bobDerBaumeister, id: 'p1', language: sample(languages), - image: faker.image.unsplash.technology(), + image: faker.image.unsplash.technology(300, 1500), categoryIds: ['cat1'], + imageAspectRatio: 300 / 1500, }), factory.create('Post', { author: huey, @@ -382,8 +384,9 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] authorId: 'u1', id: 'p6', language: sample(languages), - image: faker.image.unsplash.buildings(), + image: faker.image.unsplash.buildings(300, 857), categoryIds: ['cat6'], + imageAspectRatio: 300 / 857, }), factory.create('Post', { author: huey, @@ -400,8 +403,9 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] author: louie, id: 'p11', language: sample(languages), - image: faker.image.unsplash.people(), + image: faker.image.unsplash.people(300, 901), categoryIds: ['cat11'], + imageAspectRatio: 300 / 901, }), factory.create('Post', { author: bobDerBaumeister, @@ -413,8 +417,9 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] author: jennyRostock, id: 'p14', language: sample(languages), - image: faker.image.unsplash.objects(), + image: faker.image.unsplash.objects(300, 200), categoryIds: ['cat14'], + imageAspectRatio: 300 / 450, }), factory.create('Post', { author: huey, @@ -434,8 +439,20 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] const hashtagAndMention1 = 'The new physics of #QuantenFlussTheorie can explain #QuantumGravity! @peter-lustig got that already. ;-)' const createPostMutation = gql` - mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) { - CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) { + mutation( + $id: ID + $title: String! + $content: String! + $categoryIds: [ID] + $imageAspectRatio: Float + ) { + CreatePost( + id: $id + title: $title + content: $content + categoryIds: $categoryIds + imageAspectRatio: $imageAspectRatio + ) { id } } @@ -449,6 +466,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] title: `Nature Philosophy Yoga`, content: hashtag1, categoryIds: ['cat2'], + imageAspectRatio: 300 / 200, }, }), mutate({ @@ -458,6 +476,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] title: 'This is post #7', content: `${mention1} ${faker.lorem.paragraph()}`, categoryIds: ['cat7'], + imageAspectRatio: 300 / 180, }, }), mutate({ @@ -468,6 +487,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] title: `Quantum Flow Theory explains Quantum Gravity`, content: hashtagAndMention1, categoryIds: ['cat8'], + imageAspectRatio: 300 / 900, }, }), mutate({ @@ -477,6 +497,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] title: 'This is post #12', content: `${mention2} ${faker.lorem.paragraph()}`, categoryIds: ['cat12'], + imageAspectRatio: 300 / 200, }, }), ]) @@ -524,7 +545,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] ]) authenticatedUser = null - await Promise.all([ + const comments = await Promise.all([ factory.create('Comment', { author: jennyRostock, id: 'c1', @@ -541,7 +562,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p3', }), factory.create('Comment', { - author: bobDerBaumeister, + author: jennyRostock, id: 'c5', postId: 'p3', }), @@ -581,6 +602,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] postId: 'p15', }), ]) + const trollingComment = comments[0] await Promise.all([ democracy.relateTo(p3, 'post'), @@ -644,68 +666,115 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] louie.relateTo(p10, 'shouted'), ]) - const disableMutation = gql` - mutation($id: ID!) { - disable(id: $id) - } - ` - authenticatedUser = await bobDerBaumeister.toJson() - await Promise.all([ - mutate({ - mutation: disableMutation, - variables: { - id: 'p11', - }, - }), - mutate({ - mutation: disableMutation, - variables: { - id: 'c5', - }, - }), + const reports = await Promise.all([ + factory.create('Report'), + factory.create('Report'), + factory.create('Report'), + factory.create('Report'), ]) - authenticatedUser = null + const reportAgainstDagobert = reports[0] + const reportAgainstTrollingPost = reports[1] + const reportAgainstTrollingComment = reports[2] + const reportAgainstDewey = reports[3] - // There is no error logged or the 'try' fails if this mutation is wrong. Why? - const reportMutation = gql` - mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { - report( - resourceId: $resourceId - reasonCategory: $reasonCategory - reasonDescription: $reasonDescription - ) { - type - } - } - ` - authenticatedUser = await huey.toJson() + // report resource first time await Promise.all([ - mutate({ - mutation: reportMutation, - variables: { - resourceId: 'c1', - reasonCategory: 'other', - reasonDescription: 'This comment is bigoted', - }, + reportAgainstDagobert.relateTo(jennyRostock, 'filed', { + resourceId: 'u7', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This user is harassing me with bigoted remarks!', }), - mutate({ - mutation: reportMutation, - variables: { - resourceId: 'p1', - reasonCategory: 'discrimination_etc', - reasonDescription: 'This post is bigoted', - }, + reportAgainstDagobert.relateTo(dagobert, 'belongsTo'), + reportAgainstTrollingPost.relateTo(jennyRostock, 'filed', { + resourceId: 'p2', + reasonCategory: 'doxing', + reasonDescription: "This shouldn't be shown to anybody else! It's my private thing!", }), - mutate({ - mutation: reportMutation, - variables: { - resourceId: 'u1', - reasonCategory: 'doxing', - reasonDescription: 'This user is harassing me with bigoted remarks', - }, + reportAgainstTrollingPost.relateTo(p2, 'belongsTo'), + reportAgainstTrollingComment.relateTo(huey, 'filed', { + resourceId: 'c1', + reasonCategory: 'other', + reasonDescription: 'This comment is bigoted', }), + reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'), + reportAgainstDewey.relateTo(dagobert, 'filed', { + resourceId: 'u5', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This user is harassing me!', + }), + reportAgainstDewey.relateTo(dewey, 'belongsTo'), + ]) + + // report resource a second time + await Promise.all([ + reportAgainstDagobert.relateTo(louie, 'filed', { + resourceId: 'u7', + reasonCategory: 'discrimination_etc', + reasonDescription: 'this user is attacking me for who I am!', + }), + reportAgainstDagobert.relateTo(dagobert, 'belongsTo'), + reportAgainstTrollingPost.relateTo(peterLustig, 'filed', { + resourceId: 'p2', + reasonCategory: 'discrimination_etc', + reasonDescription: 'This post is bigoted', + }), + reportAgainstTrollingPost.relateTo(p2, 'belongsTo'), + + reportAgainstTrollingComment.relateTo(bobDerBaumeister, 'filed', { + resourceId: 'c1', + reasonCategory: 'pornographic_content_links', + reasonDescription: 'This comment is porno!!!', + }), + reportAgainstTrollingComment.relateTo(trollingComment, 'belongsTo'), + ]) + + const disableVariables = { + resourceId: 'undefined-resource', + disable: true, + closed: false, + } + + // review resource first time + await Promise.all([ + reportAgainstDagobert.relateTo(bobDerBaumeister, 'reviewed', { + ...disableVariables, + resourceId: 'u7', + }), + dagobert.update({ disabled: true, updatedAt: new Date().toISOString() }), + reportAgainstTrollingPost.relateTo(peterLustig, 'reviewed', { + ...disableVariables, + resourceId: 'p2', + }), + p2.update({ disabled: true, updatedAt: new Date().toISOString() }), + reportAgainstTrollingComment.relateTo(bobDerBaumeister, 'reviewed', { + ...disableVariables, + resourceId: 'c1', + }), + trollingComment.update({ disabled: true, updatedAt: new Date().toISOString() }), + ]) + + // second review of resource and close report + await Promise.all([ + reportAgainstDagobert.relateTo(peterLustig, 'reviewed', { + resourceId: 'u7', + disable: false, + closed: true, + }), + dagobert.update({ disabled: false, updatedAt: new Date().toISOString(), closed: true }), + reportAgainstTrollingPost.relateTo(bobDerBaumeister, 'reviewed', { + resourceId: 'p2', + disable: true, + closed: true, + }), + p2.update({ disabled: true, updatedAt: new Date().toISOString(), closed: true }), + reportAgainstTrollingComment.relateTo(peterLustig, 'reviewed', { + ...disableVariables, + resourceId: 'c1', + disable: true, + closed: true, + }), + trollingComment.update({ disabled: true, updatedAt: new Date().toISOString(), closed: true }), ]) - authenticatedUser = null await Promise.all( [...Array(30).keys()].map(i => { diff --git a/backend/src/server.js b/backend/src/server.js index 053a3e4b3..91b9a13aa 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -3,7 +3,7 @@ import helmet from 'helmet' import { ApolloServer } from 'apollo-server-express' import CONFIG, { requiredConfigs } from './config' import middleware from './middleware' -import { neode as getNeode, getDriver } from './bootstrap/neo4j' +import { getNeode, getDriver } from './bootstrap/neo4j' import decode from './jwt/decode' import schema from './schema' import webfinger from './activitypub/routes/webfinger' diff --git a/backend/yarn.lock b/backend/yarn.lock index 8e2ca5446..cba2455d1 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -56,15 +56,15 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@^7.1.0", "@babel/core@~7.7.4": - version "7.7.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.4.tgz#37e864532200cb6b50ee9a4045f5f817840166ab" - integrity sha512-+bYbx56j4nYBmpsWtnPUsKW3NdnYxbqyfrP2w9wILBuHzdfIKz9prieZK0DFPyIzkjYVUe4QkusGL07r5pXznQ== +"@babel/core@^7.1.0", "@babel/core@~7.7.5": + version "7.7.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.5.tgz#ae1323cd035b5160293307f50647e83f8ba62f7e" + integrity sha512-M42+ScN4+1S9iB6f+TL7QBpoQETxbclx+KNoKJABghnKYE+fMzSGqst0BZJc8CpI625bwPwYgUyRvxZ+0mZzpw== dependencies: "@babel/code-frame" "^7.5.5" "@babel/generator" "^7.7.4" "@babel/helpers" "^7.7.4" - "@babel/parser" "^7.7.4" + "@babel/parser" "^7.7.5" "@babel/template" "^7.7.4" "@babel/traverse" "^7.7.4" "@babel/types" "^7.7.4" @@ -280,10 +280,10 @@ regenerator-runtime "^0.13.3" v8flags "^3.1.1" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.7.4": - version "7.7.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.4.tgz#75ab2d7110c2cf2fa949959afb05fa346d2231bb" - integrity sha512-jIwvLO0zCL+O/LmEJQjWA75MQTWwx3c3u2JOTDK5D3/9egrWRRA0/0hk9XXywYnXZVVpzrBYeIQTmhwUaePI9g== +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.7.4", "@babel/parser@^7.7.5": + version "7.7.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.5.tgz#cbf45321619ac12d83363fcf9c94bb67fa646d71" + integrity sha512-KNlOe9+/nk4i29g0VXgl8PEXIRms5xKLJeuZ6UptN0fHv+jDiriG+y94X6qAgWTR0h3KaoM1wK5G5h7MHFRSig== "@babel/plugin-proposal-async-generator-functions@^7.7.4": version "7.7.4" @@ -1101,60 +1101,72 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@sentry/core@5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.8.0.tgz#bbfd2f4711491951a8e3a0e8fa8b172fdf7bff6f" - integrity sha512-aAh2KLidIXJVGrxmHSVq2eVKbu7tZiYn5ylW6yzJXFetS5z4MA+JYaSBaG2inVYDEEqqMIkb17TyWxxziUDieg== +"@sentry/apm@5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.10.1.tgz#2ec20cef0f87f9f638ff78dd5092e1e9d36c4b7d" + integrity sha512-VSFK8giRG5/lN0YSaOw8+Cru/8MVevmoHZ5JC9iDIt0H6sGTUjOBKIqTZ0eq2Y99Vn0N9dkxjeT0rOIvsrg0gA== dependencies: - "@sentry/hub" "5.8.0" - "@sentry/minimal" "5.8.0" - "@sentry/types" "5.7.1" - "@sentry/utils" "5.8.0" + "@sentry/hub" "5.10.1" + "@sentry/minimal" "5.10.1" + "@sentry/types" "5.10.0" + "@sentry/utils" "5.10.1" tslib "^1.9.3" -"@sentry/hub@5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.8.0.tgz#56aaeb7324cb66d90db838011cb0127f5558007f" - integrity sha512-VdApn1ZCNwH1wwQwoO6pu53PM/qgHG+DQege0hbByluImpLBhAj9w50nXnF/8KzV4UoMIVbzCb6jXzMRmqqp9A== +"@sentry/core@5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.10.1.tgz#356551f111d4df38e60852607cc8cde0ed8ccc76" + integrity sha512-MbiasA/cuMB0+9zVBGi5YLWRj7CdFQJOM29Vp8rm3xMaQDH0KHarpny1gOgMiLu/O/r8itjiZwKu+9pxOWGbeA== dependencies: - "@sentry/types" "5.7.1" - "@sentry/utils" "5.8.0" + "@sentry/hub" "5.10.1" + "@sentry/minimal" "5.10.1" + "@sentry/types" "5.10.0" + "@sentry/utils" "5.10.1" tslib "^1.9.3" -"@sentry/minimal@5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.8.0.tgz#b7ad5113504ab67f1ef2b0f465b7ba608e6b8dc5" - integrity sha512-MIlFOgd+JvAUrBBmq7vr9ovRH1HvckhnwzHdoUPpKRBN+rQgTyZy1o6+kA2fASCbrRqFCP+Zk7EHMACKg8DpIw== +"@sentry/hub@5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.10.1.tgz#3be4a0705cd0cd074be0aab0dc418ecb72885989" + integrity sha512-g+P+0cj6vKdf6Ct4S47MxHwSMIjtIadOwBhb4Lqwij5YPtQ4LpVr10peKbE+FMMvCNQSvQnJEhTDko+AE7AoYw== dependencies: - "@sentry/hub" "5.8.0" - "@sentry/types" "5.7.1" + "@sentry/types" "5.10.0" + "@sentry/utils" "5.10.1" tslib "^1.9.3" -"@sentry/node@^5.9.0": - version "5.9.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.9.0.tgz#9a8da70990e64c88a391ef86dcf29f43e0a52e59" - integrity sha512-1CWwSGhRfMr4Bvt1i0vIms+BBZd4dBzlDyWpyCboodCXF1rTJRci9roQ+Wh9XWwFEWvgDD2PzuKzfvu638v2Wg== +"@sentry/minimal@5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.10.1.tgz#37104f81ef3b333c0f9e77ac94bfed348070dea3" + integrity sha512-oKrLvKaah0xGVIYbS1I7dVbo73aWssfiT2ypl9DYt8MAFiwfiiXz68FlG4z9dPZ2jSz9Jm2SAYHFaYLvU26TBQ== dependencies: - "@sentry/core" "5.8.0" - "@sentry/hub" "5.8.0" - "@sentry/types" "5.7.1" - "@sentry/utils" "5.8.0" + "@sentry/hub" "5.10.1" + "@sentry/types" "5.10.0" + tslib "^1.9.3" + +"@sentry/node@^5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.10.1.tgz#cafbf3b0918c98fb9f99803ffe50056e32194bef" + integrity sha512-kard7OXQDvYqmQD93bOkYhznqrbsiFNZ6+dIi13eo/kc2Au+v1Th1mGvr9JDRE/X07z6vJMYMiorKd351G3p/A== + dependencies: + "@sentry/apm" "5.10.1" + "@sentry/core" "5.10.1" + "@sentry/hub" "5.10.1" + "@sentry/types" "5.10.0" + "@sentry/utils" "5.10.1" cookie "^0.3.1" https-proxy-agent "^3.0.0" lru_map "^0.3.3" tslib "^1.9.3" -"@sentry/types@5.7.1": - version "5.7.1" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.7.1.tgz#4c4c1d4d891b6b8c2c3c7b367d306a8b1350f090" - integrity sha512-tbUnTYlSliXvnou5D4C8Zr+7/wJrHLbpYX1YkLXuIJRU0NSi81bHMroAuHWILcQKWhVjaV/HZzr7Y/hhWtbXVQ== +"@sentry/types@5.10.0": + version "5.10.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.10.0.tgz#4f0ba31b6e4d5371112c38279f11f66c73b43746" + integrity sha512-TW20GzkCWsP6uAxR2JIpIkiitCKyIOfkyDsKBeLqYj4SaZjfvBPnzgNCcYR0L0UsP1/Es6oHooZfIGSkp6GGxQ== -"@sentry/utils@5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.8.0.tgz#34683088159b9935f973b6e6cad1a1cc26bbddac" - integrity sha512-KDxUvBSYi0/dHMdunbxAxD3389pcQioLtcO6CI6zt/nJXeVFolix66cRraeQvqupdLhvOk/el649W4fCPayTHw== +"@sentry/utils@5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.10.1.tgz#eeb3ede85a9b5b1cd1aad7e3157052bee0d42551" + integrity sha512-zdv03sINfJ8QXSHP49845qhkbdNUrX20AagUY+Arq2zxmM4XxnRVA7dtWDkyy55bTt0ziRuSikBxR3266t8mDg== dependencies: - "@sentry/types" "5.7.1" + "@sentry/types" "5.10.0" tslib "^1.9.3" "@sindresorhus/is@^0.14.0": @@ -1638,10 +1650,10 @@ apollo-engine-reporting-protobuf@^0.4.4: dependencies: "@apollo/protobufjs" "^1.0.3" -apollo-engine-reporting@^1.4.10: - version "1.4.10" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.10.tgz#cca245133906ed4ece125e48cb95dd959f3af2f6" - integrity sha512-0nEawO9cudbXHCxRvnDUWKqCxPAGEstghUFd5sB67lIGuh91MYeLuwN1iTfqUdwF1feEGHn636zVVUYlXGOlvQ== +apollo-engine-reporting@^1.4.11: + version "1.4.11" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.11.tgz#ea4501925c201e62729a11ce36284a89f1eaa4f5" + integrity sha512-7ZkbOGvPfWppN8+1KHzyHPrJTMOmrMUy38unao2c9TTToOAnEvx2MtUTo6mr3aw/g8UQYUf0x2Cq+K2YSlUTPw== dependencies: apollo-engine-reporting-protobuf "^0.4.4" apollo-graphql "^0.3.4" @@ -1719,10 +1731,10 @@ apollo-server-caching@^0.5.0: dependencies: lru-cache "^5.0.0" -apollo-server-core@^2.9.12: - version "2.9.12" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.12.tgz#c8ed48540762913242eef5fce0da8b59b131a1e8" - integrity sha512-jhGr2R655PSwUUBweXDl+0F3oa74Elu5xXF+88ymUUej34EwBUCqz97wPqR07BEuyxaAlRfZwPMvKaHhMUKg5g== +apollo-server-core@^2.9.13: + version "2.9.13" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.13.tgz#29fee69be56d30605b0a06cd755fd39e0409915f" + integrity sha512-iXTGNCtouB0Xe37ySovuZO69NBYOByJlZfUc87gj0pdcz0WbdfUp7qUtNzy3onp63Zo60TFkHWhGNcBJYFluzw== dependencies: "@apollographql/apollo-tools" "^0.4.0" "@apollographql/graphql-playground-html" "1.6.24" @@ -1730,7 +1742,7 @@ apollo-server-core@^2.9.12: "@types/ws" "^6.0.0" apollo-cache-control "^0.8.8" apollo-datasource "^0.6.3" - apollo-engine-reporting "^1.4.10" + apollo-engine-reporting "^1.4.11" apollo-server-caching "^0.5.0" apollo-server-env "^2.4.3" apollo-server-errors "^2.3.4" @@ -1759,10 +1771,10 @@ apollo-server-errors@^2.3.4: resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.4.tgz#b70ef01322f616cbcd876f3e0168a1a86b82db34" integrity sha512-Y0PKQvkrb2Kd18d1NPlHdSqmlr8TgqJ7JQcNIfhNDgdb45CnqZlxL1abuIRhr8tiw8OhVOcFxz2KyglBi8TKdA== -apollo-server-express@^2.9.12, apollo-server-express@^2.9.7: - version "2.9.12" - resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.12.tgz#e779ea2c107fcc63b0c9b888a4cbf0f65af6d505" - integrity sha512-4Ev8MY7m23mSzwO/BvLTy97a/68IP/wZoCRBn2R81OoZt9/GxlvvYZGvozJCXYsQt1qAbIT4Sn05LmqawsI98w== +apollo-server-express@^2.9.13, apollo-server-express@^2.9.7: + version "2.9.13" + resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.13.tgz#abb00bcf85d86a6e0e9105ce3b7fae9a7748156b" + integrity sha512-M306e07dpZ8YpZx4VBYa0FWlt+wopj4Bwn0Iy1iJ6VjaRyGx2HCUJvLpHZ+D0TIXtQ2nX3DTYeOouVaDDwJeqQ== dependencies: "@apollographql/graphql-playground-html" "1.6.24" "@types/accepts" "^1.3.5" @@ -1770,7 +1782,7 @@ apollo-server-express@^2.9.12, apollo-server-express@^2.9.7: "@types/cors" "^2.8.4" "@types/express" "4.17.1" accepts "^1.3.5" - apollo-server-core "^2.9.12" + apollo-server-core "^2.9.13" apollo-server-types "^0.2.8" body-parser "^1.18.3" cors "^2.8.4" @@ -1788,12 +1800,12 @@ apollo-server-plugin-base@^0.6.8: dependencies: apollo-server-types "^0.2.8" -apollo-server-testing@~2.9.12: - version "2.9.12" - resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.12.tgz#2dcad49f399f50bf3d8bbaa0c753eb7eca48ff10" - integrity sha512-TFHXA8HdD++FzbCvrQryFqALvX2Mrea1bNu7pi5L5wpjB5Ug3FudasYGhy6tl8BaStPxsugWngchuD3IPSBrgg== +apollo-server-testing@~2.9.13: + version "2.9.13" + resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.13.tgz#7a4efc0eb01d7297716f089121c7440a620bb640" + integrity sha512-c1xl4g5KhMfPpL5xdzxPJLY53+yK/kMAWxIASthRrOSZNgStTe7pCAJ06Nk3NB8M5GwfJK3cJiVkLfZRSt9+jQ== dependencies: - apollo-server-core "^2.9.12" + apollo-server-core "^2.9.13" apollo-server-types@^0.2.8: version "0.2.8" @@ -1804,13 +1816,13 @@ apollo-server-types@^0.2.8: apollo-server-caching "^0.5.0" apollo-server-env "^2.4.3" -apollo-server@~2.9.12: - version "2.9.12" - resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.12.tgz#3fe28c361ee373d52ae38ca190869508b0c532c0" - integrity sha512-Q+qaBTgTxb2vwqyh7NTHs9rOmadbuKw34SgeAOLsCnr3MLVjisa50fL3nQrGbhOGfRaroF8SSZYgya0tvnefig== +apollo-server@~2.9.13: + version "2.9.13" + resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.13.tgz#f93005a2a9d2b29a047f170eeb900bf464bfe62d" + integrity sha512-Aedj/aHRMCDMUwtM+hXiliX1OkFNl1NyiQUADbwm6AMV3OrfT9TUbbSI1AN2qsx+rg6dIhpAiHLUf73uDy3V/g== dependencies: - apollo-server-core "^2.9.12" - apollo-server-express "^2.9.12" + apollo-server-core "^2.9.13" + apollo-server-express "^2.9.13" express "^4.0.0" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" @@ -5842,10 +5854,10 @@ metascraper-url@^5.8.7: dependencies: "@metascraper/helpers" "^5.8.7" -metascraper-video@^5.8.7: - version "5.8.7" - resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-5.8.7.tgz#7a5d1e8955f9a65891908eef319683b6176765a2" - integrity sha512-J4OJlB+nla8ITwqH2H6dgQ+nrecYILVhsGFKG54p2qsSokXwgZrQ4P7WhUMd0VpBsYuebcRgdzY8OGUDb+7l0Q== +metascraper-video@^5.8.9: + version "5.8.9" + resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-5.8.9.tgz#23c0fe71fae5088bc8e11bfa537eff80658aa6d9" + integrity sha512-xaimkGz1Txsd9qHUN2U5HyFMP8tkrb5LuW8bCo+0kdTu5c00HGurvs0/BpWrTW/CzUQBNl/uEybeDXm8J++03g== dependencies: "@metascraper/helpers" "^5.8.7" lodash "~4.17.15" @@ -5860,10 +5872,10 @@ metascraper-youtube@^5.8.9: is-reachable "~4.0.0" p-locate "~4.1.0" -metascraper@^5.8.8: - version "5.8.8" - resolved "https://registry.yarnpkg.com/metascraper/-/metascraper-5.8.8.tgz#9fbf6913f55bb448a9195e40e38f3599bc5a818f" - integrity sha512-z4G3SXGBVnd0+FSHqR3LJF+6emO03GlY2KoOTqsFCnRuY0B72nJyR/NRRYLn4PRX6PMQ6QZ+GWKa7oxBX6hZqQ== +metascraper@^5.8.9: + version "5.8.9" + resolved "https://registry.yarnpkg.com/metascraper/-/metascraper-5.8.9.tgz#7bb468f9660bd86be8dd774cab3457d098b87e61" + integrity sha512-vuOwnSaGIG8346ZAQCE+YqvpzFVXfaMvCUdLbb8spobz7BG3945WNa43NjSl2HK5iH1WYOibvSYRZdL6wQsRJg== dependencies: "@metascraper/helpers" "^5.8.7" cheerio "~1.0.0-rc.2" @@ -6097,10 +6109,10 @@ neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.6: text-encoding-utf-8 "^1.0.2" uri-js "^4.2.2" -neo4j-graphql-js@^2.9.3: - version "2.9.3" - resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.9.3.tgz#91afb0631eb35014110022a74e572c9eb065d281" - integrity sha512-SzIX3BYE3EsKp/XU8Wog97TzfsrQdrKp/t7le7tnODojcBd5eSVJyKPrbaKqcnWMkLzKzO/SRX9PMQ2cDdXUKw== +neo4j-graphql-js@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.10.0.tgz#4298793756d839dedb98bc3e50a2bd40a311874d" + integrity sha512-jRdIyw+DHg9gfB6pWKb1ZHMR9rXIl7qf51efjUHIRHRbVR3RCcw1cKyONkq4LE8v2bHc7QDrKwJs+GQ1SRxDug== dependencies: "@babel/runtime" "^7.5.5" "@babel/runtime-corejs2" "^7.5.5" @@ -6206,10 +6218,10 @@ nodemailer-html-to-text@^3.1.0: dependencies: html-to-text "^5.1.1" -nodemailer@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.3.1.tgz#2784beebac6b9f014c424c54dbdcc5c4d1221346" - integrity sha512-j0BsSyaMlyadEDEypK/F+xlne2K5m6wzPYMXS/yxKI0s7jmT1kBx6GEKRVbZmyYfKOsjkeC/TiMVDJBI/w5gMQ== +nodemailer@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.1.tgz#f70b40355b7b08f1f80344b353970a4f8f664370" + integrity sha512-mSQAzMim8XIC1DemK9TifDTIgASfoJEllG5aC1mEtZeZ+FQyrSOdGBRth6JRA1ERzHQCET3QHVSd9Kc6mh356g== nodemon@~2.0.1: version "2.0.1" @@ -7440,11 +7452,6 @@ serve-static@1.14.1: version "1.14.1" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.17.1" set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" diff --git a/cypress.env.template.json b/cypress.env.template.json deleted file mode 100644 index 8eda47154..000000000 --- a/cypress.env.template.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "BACKEND_HOST": "http://localhost:4000", - "NEO4J_URI": "bolt://localhost:7687", - "NEO4J_USERNAME": "neo4j", - "NEO4J_PASSWORD": "letmein" -} diff --git a/cypress/README.md b/cypress/README.md index 2adcff925..662d0b51c 100644 --- a/cypress/README.md +++ b/cypress/README.md @@ -16,12 +16,7 @@ First, you have to tell cypress how to connect to your local neo4j database among other things. You can copy our template configuration and change the new file according to your needs. -Make sure you are at the root level of the project. Then: -```bash -# in the top level folder Human-Connection/ -$ cp cypress.env.template.json cypress.env.json -``` -To start the services that are required for cypress testing, run this: +To start the services that are required for cypress testing, run: ```bash # in the top level folder Human-Connection/ diff --git a/cypress/integration/common/post.js b/cypress/integration/common/post.js index 814159a34..a680986f4 100644 --- a/cypress/integration/common/post.js +++ b/cypress/integration/common/post.js @@ -3,6 +3,14 @@ import { When, Then } from "cypress-cucumber-preprocessor/steps"; const narratorAvatar = "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg"; +When("I type in a comment with {int} characters", size => { + var c=""; + for (var i = 0; i < size; i++) { + c += "c" + } + cy.get(".editor .ProseMirror").type(c); +}); + Then("I click on the {string} button", text => { cy.get("button") .contains(text) @@ -23,6 +31,16 @@ Then("I should see my comment", () => { .should("contain", "today at"); }); +Then("I should see the entirety of my comment", () => { + cy.get("div.comment") + .should("not.contain", "show more") +}); + +Then("I should see an abreviated version of my comment", () => { + cy.get("div.comment") + .should("contain", "show more") +}); + Then("the editor should be cleared", () => { cy.get(".ProseMirror p").should("have.class", "is-empty"); }); diff --git a/cypress/integration/common/report.js b/cypress/integration/common/report.js index 17481befe..25f4c6e35 100644 --- a/cypress/integration/common/report.js +++ b/cypress/integration/common/report.js @@ -1,5 +1,6 @@ import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps' import { VERSION } from '../../constants/terms-and-conditions-version.js' +import { gql } from '../../../backend/src/helpers/jest' /* global cy */ @@ -128,9 +129,9 @@ Given('somebody reported the following posts:', table => { cy.factory() .create('User', submitter) .authenticateAs(submitter) - .mutate(`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { - report(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) { - type + .mutate(gql`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { + fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) { + id } }`, { resourceId, diff --git a/cypress/integration/post/Comment.feature b/cypress/integration/post/Comment.feature index e7e462824..50284d6f5 100644 --- a/cypress/integration/post/Comment.feature +++ b/cypress/integration/post/Comment.feature @@ -20,3 +20,19 @@ Feature: Post Comment Then my comment should be successfully created And I should see my comment And the editor should be cleared + + Scenario: View medium length comments + Given I visit "post/bWBjpkTKZp/101-essays" + And I type in a comment with 305 characters + And I click on the "Comment" button + Then my comment should be successfully created + And I should see the entirety of my comment + And the editor should be cleared + + Scenario: View long comments + Given I visit "post/bWBjpkTKZp/101-essays" + And I type in a comment with 1205 characters + And I click on the "Comment" button + Then my comment should be successfully created + And I should see an abreviated version of my comment + And the editor should be cleared diff --git a/cypress/support/commands.js b/cypress/support/commands.js index f8bc76d50..f52b38faf 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -18,8 +18,8 @@ import helpers from "./helpers"; import users from "../fixtures/users.json"; import { GraphQLClient, request } from 'graphql-request' import { gql } from '../../backend/src/helpers/jest' +import config from '../../backend/src/config' -const backendHost = Cypress.env('BACKEND_HOST') const switchLang = name => { cy.get(".locale-menu").click(); cy.contains(".locale-menu-popover a", name).click(); @@ -31,7 +31,7 @@ const authenticatedHeaders = async (variables) => { login(email: $email, password: $password) } ` - const response = await request(backendHost, mutation, variables) + const response = await request(config.GRAPHQL_URI, mutation, variables) return { authorization: `Bearer ${response.login}` } } @@ -100,8 +100,7 @@ Cypress.Commands.add( 'authenticateAs', async ({email, password}) => { const headers = await authenticatedHeaders({ email, password }) - console.log(headers) - return new GraphQLClient(backendHost, { headers }) + return new GraphQLClient(config.GRAPHQL_URI, { headers }) } ) diff --git a/cypress/support/factories.js b/cypress/support/factories.js index da67debd5..234584e09 100644 --- a/cypress/support/factories.js +++ b/cypress/support/factories.js @@ -1,16 +1,10 @@ import Factory from '../../backend/src/seed/factories' -import { getDriver, neode as getNeode } from '../../backend/src/bootstrap/neo4j' -import setupNeode from '../../backend/src/bootstrap/neode' +import { getDriver, getNeode } from '../../backend/src/bootstrap/neo4j' import neode from 'neode' -const backendHost = Cypress.env('SEED_SERVER_HOST') -const neo4jConfigs = { - uri: Cypress.env('NEO4J_URI'), - username: Cypress.env('NEO4J_USERNAME'), - password: Cypress.env('NEO4J_PASSWORD') -} -const neo4jDriver = getDriver(neo4jConfigs) -const factoryOptions = { seedServerHost: backendHost, neo4jDriver, neodeInstance: setupNeode(neo4jConfigs)} +const neo4jDriver = getDriver() +const neodeInstance = getNeode() +const factoryOptions = { neo4jDriver, neodeInstance } const factory = Factory(factoryOptions) beforeEach(async () => { @@ -18,7 +12,7 @@ beforeEach(async () => { }) Cypress.Commands.add('neode', () => { - return setupNeode(neo4jConfigs) + return neodeInstance }) Cypress.Commands.add( 'first', diff --git a/neo4j/change_disabled_relationship_to_report_node.sh b/neo4j/change_disabled_relationship_to_report_node.sh new file mode 100755 index 000000000..2f44b8e59 --- /dev/null +++ b/neo4j/change_disabled_relationship_to_report_node.sh @@ -0,0 +1,55 @@ +#!/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 " +// convert old DISABLED to new REVIEWED-Report-BELONGS_TO structure +MATCH (moderator:User)-[disabled:DISABLED]->(disabledResource) +WHERE disabledResource:User OR disabledResource:Comment OR disabledResource:Post +DELETE disabled +CREATE (moderator)-[review:REVIEWED]->(report:Report)-[:BELONGS_TO]->(disabledResource) +SET review.createdAt = toString(datetime()), review.updatedAt = review.createdAt, review.disable = true +SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false + +// if disabledResource has no filed report, then create a moderators default filed report +WITH moderator, disabledResource, report +OPTIONAL MATCH (disabledResourceReporter:User)-[existingFiledReport:FILED]->(disabledResource) +FOREACH(disabledResource IN CASE WHEN existingFiledReport IS NULL THEN [1] ELSE [] END | + CREATE (moderator)-[addModeratorReport:FILED]->(report) + SET addModeratorReport.createdAt = toString(datetime()), addModeratorReport.reasonCategory = 'other', addModeratorReport.reasonDescription = 'Old DISABLED relations didn't enforce mandatory reporting !!! Created automatically to ensure database consistency! Creation date is when the database manipulation happened.' +) +FOREACH(disabledResource IN CASE WHEN existingFiledReport IS NOT NULL THEN [1] ELSE [] END | + CREATE (disabledResourceReporter)-[moveModeratorReport:FILED]->(report) + SET moveModeratorReport = existingFiledReport + DELETE existingFiledReport +) + +RETURN disabledResource {.id}; +" | cypher-shell + +echo " +// for FILED resources without DISABLED relation which are handled above, create new FILED-Report-BELONGS_TO structure +MATCH (reporter:User)-[oldReport:REPORTED]->(notDisabledResource) +WHERE notDisabledResource:User OR notDisabledResource:Comment OR notDisabledResource:Post +MERGE (report:Report)-[:BELONGS_TO]->(notDisabledResource) +ON CREATE SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false +CREATE (reporter)-[filed:FILED]->(report) +SET report = oldReport +DELETE oldReport + +RETURN notDisabledResource {.id}; +" | cypher-shell + diff --git a/neo4j/change_report_node_to_relationship.sh b/neo4j/change_report_node_to_relationship.sh deleted file mode 100755 index f8dd639be..000000000 --- a/neo4j/change_report_node_to_relationship.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/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! Creation date is when the database manipulation happened.' -RETURN reported; -" | cypher-shell diff --git a/neo4j/db_manipulation/add_image_aspect_ratio.sh b/neo4j/db_manipulation/add_image_aspect_ratio.sh new file mode 100755 index 000000000..7fe2c5871 --- /dev/null +++ b/neo4j/db_manipulation/add_image_aspect_ratio.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +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 + +shopt -s nullglob +for image in uploads/*; do + [ -e "$image" ] || continue + IMAGE_WIDTH=$( identify -format '%w' "$image" ) + IMAGE_HEIGHT=$( identify -format '%h' "$image" ) + IMAGE_ASPECT_RATIO=$(echo | awk "{ print ${IMAGE_WIDTH}/${IMAGE_HEIGHT}}") + + + echo "$image" + echo "$IMAGE_ASPECT_RATIO" + echo " + match (post:Post {image: '/"${image}"'}) + set post.imageAspectRatio = "${IMAGE_ASPECT_RATIO}" + return post; + " | cypher-shell +done diff --git a/neo4j/db_manipulation/change_disabled_relationship_to_report_node.sh b/neo4j/db_manipulation/change_disabled_relationship_to_report_node.sh new file mode 100755 index 000000000..e611382f0 --- /dev/null +++ b/neo4j/db_manipulation/change_disabled_relationship_to_report_node.sh @@ -0,0 +1,51 @@ +#!/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 " + :begin + MATCH(user)-[reported:REPORTED]->(resource) + WITH reported, resource, COLLECT(user) as users + MERGE(report:Report)-[:BELONGS_TO]->(resource) + SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false + WITH report, users, reported + UNWIND users as user + MERGE (user)-[filed:FILED]->(report) + SET filed = reported + DELETE reported; + + MATCH(moderator)-[disabled:DISABLED]->(resource) + MATCH(report:Report)-[:BELONGS_TO]->(resource) + WITH disabled, resource, COLLECT(moderator) as moderators, report + DELETE disabled + WITH report, moderators, disabled + UNWIND moderators as moderator + MERGE (moderator)-[review:REVIEWED {disable: true}]->(report) + SET review.createdAt = toString(datetime()), review.updatedAt = review.createdAt, review.disable = true; + + MATCH(moderator)-[disabled:DISABLED]->(resource) + WITH disabled, resource, COLLECT(moderator) as moderators + MERGE(report:Report)-[:BELONGS_TO]->(resource) + SET report.id = randomUUID(), report.createdAt = toString(datetime()), report.updatedAt = report.createdAt, report.rule = 'latestReviewUpdatedAtRules', report.closed = false + DELETE disabled + WITH report, moderators, disabled + UNWIND moderators as moderator + MERGE(moderator)-[filed:FILED]->(report) + SET filed.createdAt = toString(datetime()), filed.reasonCategory = 'other', filed.reasonDescription = 'Old DISABLED relations didn\'t enforce mandatory reporting !!! Created automatically to ensure database consistency! Creation date is when the database manipulation happened.' + MERGE (moderator)-[review:REVIEWED {disable: true}]->(report) + SET review.createdAt = toString(datetime()), review.updatedAt = review.createdAt, review.disable = true; + :commit +" | cypher-shell \ No newline at end of file diff --git a/package.json b/package.json index ac7e575df..d20232f20 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "cross-env": "^6.0.3", "cucumber": "^6.0.5", "cypress": "^3.7.0", - "cypress-cucumber-preprocessor": "^1.17.0", + "cypress-cucumber-preprocessor": "^1.18.0", "cypress-file-upload": "^3.5.0", "cypress-plugin-retries": "^1.5.0", "date-fns": "^2.8.1", diff --git a/styleguide b/styleguide index 808b3c5a9..7ef834050 160000 --- a/styleguide +++ b/styleguide @@ -1 +1 @@ -Subproject commit 808b3c5a9523505cb80b20b50348d29ba9932845 +Subproject commit 7ef83405006b016fe45b476ed6e34ec189d7d283 diff --git a/webapp/README.md b/webapp/README.md index 185557fc7..897bb56ca 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -6,14 +6,15 @@ ```bash # install all dependencies +$ cd webapp/ $ yarn install ``` Copy: ```text +# in webapp/ cp .env.template .env -cp cypress.env.template.json cypress.env.json ``` Configure the files according to your needs and your local setup. diff --git a/webapp/components/CommentList/CommentList.vue b/webapp/components/CommentList/CommentList.vue index 888c167e9..25ed62f68 100644 --- a/webapp/components/CommentList/CommentList.vue +++ b/webapp/components/CommentList/CommentList.vue @@ -1,19 +1,9 @@ - - diff --git a/webapp/components/ContentMenu/ContentMenu.spec.js b/webapp/components/ContentMenu/ContentMenu.spec.js index 485c43145..8f93aa4a4 100644 --- a/webapp/components/ContentMenu/ContentMenu.spec.js +++ b/webapp/components/ContentMenu/ContentMenu.spec.js @@ -85,7 +85,7 @@ describe('ContentMenu.vue', () => { .filter(item => item.text() === 'post.menu.delete') .at(0) .trigger('click') - expect(openModalSpy).toHaveBeenCalledWith('delete') + expect(openModalSpy).toHaveBeenCalledWith('confirm', 'delete') }) }) @@ -166,7 +166,7 @@ describe('ContentMenu.vue', () => { .filter(item => item.text() === 'comment.menu.delete') .at(0) .trigger('click') - expect(openModalSpy).toHaveBeenCalledWith('delete') + expect(openModalSpy).toHaveBeenCalledWith('confirm', 'delete') }) }) @@ -332,7 +332,7 @@ describe('ContentMenu.vue', () => { .filter(item => item.text() === 'release.contribution.title') .at(0) .trigger('click') - expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8') + expect(openModalSpy).toHaveBeenCalledWith('release') }) it('can release comments', () => { @@ -350,7 +350,7 @@ describe('ContentMenu.vue', () => { .filter(item => item.text() === 'release.comment.title') .at(0) .trigger('click') - expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8') + expect(openModalSpy).toHaveBeenCalledWith('release') }) it('can release users', () => { @@ -368,7 +368,7 @@ describe('ContentMenu.vue', () => { .filter(item => item.text() === 'release.user.title') .at(0) .trigger('click') - expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8') + expect(openModalSpy).toHaveBeenCalledWith('release') }) it('can release organizations', () => { @@ -386,7 +386,7 @@ describe('ContentMenu.vue', () => { .filter(item => item.text() === 'release.organization.title') .at(0) .trigger('click') - expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8') + expect(openModalSpy).toHaveBeenCalledWith('release') }) }) diff --git a/webapp/components/ContentMenu/ContentMenu.vue b/webapp/components/ContentMenu/ContentMenu.vue index f0d9dc8d3..d4c567437 100644 --- a/webapp/components/ContentMenu/ContentMenu.vue +++ b/webapp/components/ContentMenu/ContentMenu.vue @@ -70,7 +70,7 @@ export default { routes.push({ name: this.$t(`post.menu.delete`), callback: () => { - this.openModal('delete') + this.openModal('confirm', 'delete') }, icon: 'trash', }) @@ -108,7 +108,7 @@ export default { routes.push({ name: this.$t(`comment.menu.delete`), callback: () => { - this.openModal('delete') + this.openModal('confirm', 'delete') }, icon: 'trash', }) @@ -137,7 +137,7 @@ export default { routes.push({ name: this.$t(`release.${this.resourceType}.title`), callback: () => { - this.openModal('release', this.resource.id) + this.openModal('release') }, icon: 'eye', }) @@ -190,13 +190,13 @@ export default { } toggleMenu() }, - openModal(dialog) { + openModal(dialog, modalDataName = null) { this.$store.commit('modal/SET_OPEN', { name: dialog, data: { type: this.resourceType, resource: this.resource, - modalsData: this.modalsData, + modalData: modalDataName ? this.modalsData[modalDataName] : {}, }, }) }, diff --git a/webapp/components/ContributionForm/ContributionForm.spec.js b/webapp/components/ContributionForm/ContributionForm.spec.js index 3ec538bde..8c50f30b6 100644 --- a/webapp/components/ContributionForm/ContributionForm.spec.js +++ b/webapp/components/ContributionForm/ContributionForm.spec.js @@ -198,6 +198,7 @@ describe('ContributionForm.vue', () => { id: null, categoryIds: ['cat12'], imageUpload: null, + imageAspectRatio: null, image: null, }, } @@ -352,6 +353,7 @@ describe('ContributionForm.vue', () => { categoryIds: ['cat12'], image, imageUpload: null, + imageAspectRatio: null, }, } }) diff --git a/webapp/components/ContributionForm/ContributionForm.vue b/webapp/components/ContributionForm/ContributionForm.vue index 734e3be81..ec9fe9616 100644 --- a/webapp/components/ContributionForm/ContributionForm.vue +++ b/webapp/components/ContributionForm/ContributionForm.vue @@ -7,7 +7,11 @@ @submit="submit" >