Merge branch 'master' of github.com:Human-Connection/Human-Connection into remove-data-test-attributes-in-production

This commit is contained in:
mattwr18 2019-12-10 13:31:10 +01:00
commit 27a2ccd9ae
137 changed files with 3864 additions and 1555 deletions

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 490.1 105.6" style="enable-background:new 0 0 490.1 105.6;" xml:space="preserve">
<style type="text/css">
.st0{fill:#F4B960;}
.st1{fill:#E66F32;}
.st2{fill:#E43C41;}
.st3{fill:#BDD041;}
.st4{fill:#6DB54C;}
.st5{fill:#AEDAE6;}
.st6{fill:#56B8DE;}
.st7{fill:#00B1D5;}
.st8{fill:url(#SVGID_1_);}
.st9{fill:#221F1F;}
.st10{fill:#FFFFFF;}
.st11{fill:#000111;}
</style>
<title>Browserstack-logo-white</title>
<circle class="st0" cx="52.8" cy="52.8" r="52.8"/>
<circle class="st1" cx="47.5" cy="47.5" r="47.5"/>
<circle class="st2" cx="53.8" cy="41.1" r="41.1"/>
<circle class="st3" cx="57.1" cy="44.4" r="37.8"/>
<circle class="st4" cx="54.3" cy="47.2" r="35.1"/>
<circle class="st5" cx="48.8" cy="41.7" r="29.5"/>
<circle class="st6" cx="53.6" cy="36.8" r="24.7"/>
<circle class="st7" cx="56.6" cy="39.9" r="21.7"/>
<radialGradient id="SVGID_1_" cx="53.45" cy="63.02" r="18.57" gradientTransform="matrix(1 0 0 -1 0 106)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#797979"/>
<stop offset="1" style="stop-color:#4C4C4C"/>
</radialGradient>
<circle class="st8" cx="53.5" cy="43" r="18.6"/>
<circle class="st9" cx="53.5" cy="43" r="18.6"/>
<ellipse transform="matrix(0.4094 -0.9123 0.9123 0.4094 2.8913 76.9251)" class="st10" cx="60.9" cy="36.2" rx="5.7" ry="3.7"/>
<path class="st11" d="M122.5,32.6c0-0.3,0.3-0.6,0.6-0.6c0,0,0,0,0.1,0h16.6c9.5,0,13.9,4.4,13.9,11c0.2,3.7-1.8,7.2-5.2,8.8v0.1
c3.7,1.5,6.1,5.2,6,9.3c0,8.2-5.6,12.2-15.4,12.2h-16c-0.3,0-0.6-0.2-0.7-0.5c0,0,0,0,0-0.1L122.5,32.6L122.5,32.6z M139.6,49.1
c3.9,0,6.4-2.2,6.4-5.4s-2.4-5.5-6.4-5.5h-8.9c-0.2,0-0.4,0.1-0.4,0.3c0,0,0,0,0,0.1v10.2c0,0.2,0.1,0.3,0.3,0.4c0,0,0,0,0.1,0
H139.6L139.6,49.1z M130.6,66.9h9.3c4.3,0,6.8-2.3,6.8-5.8s-2.4-5.7-6.7-5.7h-9.3c-0.2,0-0.4,0.1-0.4,0.3c0,0,0,0,0,0.1v10.7
C130.3,66.8,130.4,66.9,130.6,66.9C130.6,66.9,130.6,66.9,130.6,66.9L130.6,66.9z"/>
<path class="st11" d="M159.9,73.3c-0.3,0-0.6-0.2-0.7-0.5c0,0,0,0,0-0.1V44.6c0-0.3,0.3-0.6,0.6-0.6c0,0,0,0,0.1,0h6
c0.3,0,0.6,0.2,0.7,0.5c0,0,0,0,0,0.1v2.5h0.1c1.5-2.2,4.2-3.8,8.2-3.8c2.4,0,4.8,0.8,6.6,2.4c0.3,0.3,0.4,0.5,0.1,0.8l-3.5,4.1
c-0.2,0.3-0.6,0.4-0.9,0.2c0,0,0,0-0.1,0c-1.4-0.9-3-1.4-4.7-1.4c-4.1,0-6,2.7-6,7.4v15.9c0,0.3-0.3,0.6-0.6,0.6c0,0,0,0-0.1,0
H159.9L159.9,73.3z"/>
<path class="st11" d="M182.9,65.8c-0.8-2.3-1.1-4.8-1.1-7.2c-0.1-2.5,0.3-4.9,1.1-7.2c1.8-5.1,6.6-8.1,13.1-8.1s11.2,3,13,8.1
c0.8,2.3,1.1,4.8,1.1,7.2c0.1,2.5-0.3,4.9-1.1,7.2c-1.8,5.1-6.6,8.1-13,8.1S184.7,71,182.9,65.8z M201.9,64c0.5-1.7,0.8-3.6,0.7-5.4
c0.1-1.8-0.1-3.7-0.7-5.4c-0.9-2.5-3.3-4-5.9-3.8c-2.6-0.2-5.1,1.4-6,3.8c-0.5,1.8-0.8,3.6-0.7,5.4c-0.1,1.8,0.1,3.7,0.7,5.4
c0.9,2.5,3.4,4,6,3.8C198.6,68,201,66.5,201.9,64L201.9,64z"/>
<path class="st11" d="M241.9,73.3c-0.4,0-0.7-0.3-0.8-0.6L235,53.9h-0.1l-6.2,18.7c-0.1,0.4-0.4,0.6-0.8,0.6h-5.4
c-0.4,0-0.7-0.3-0.8-0.6l-10-28.1c-0.1-0.2,0-0.5,0.2-0.6c0.1,0,0.2-0.1,0.3,0h6.3c0.4,0,0.8,0.2,0.9,0.6l6.1,19.3h0.1l6-19.3
c0.1-0.4,0.5-0.6,0.9-0.6h4.7c0.4,0,0.7,0.2,0.9,0.6l6.4,19.3h0.1l5.8-19.3c0.1-0.4,0.5-0.7,0.9-0.6h6.3c0.2-0.1,0.5,0.1,0.5,0.3
c0,0.1,0,0.2,0,0.3l-10,28.1c-0.1,0.4-0.4,0.6-0.8,0.6L241.9,73.3L241.9,73.3z"/>
<path class="st11" d="M259.3,69.3c-0.2-0.2-0.3-0.6-0.1-0.8c0,0,0,0,0.1-0.1l3.7-3.6c0.3-0.2,0.7-0.2,0.9,0c2.6,2.1,5.9,3.3,9.3,3.3
c3.9,0,5.9-1.5,5.9-3.5c0-1.8-1.1-2.9-5.2-3.2l-3.4-0.3c-6.4-0.6-9.7-3.6-9.7-8.6c0-5.7,4.4-9.2,12.3-9.2c4.2-0.1,8.4,1.2,11.9,3.6
c0.3,0.2,0.3,0.5,0.2,0.8c0,0,0,0,0,0.1l-3.2,3.6c-0.2,0.3-0.6,0.3-0.9,0.1c-2.5-1.5-5.4-2.4-8.3-2.4c-3.1,0-4.8,1.3-4.8,3
s1.1,2.7,5.2,3.1l3.4,0.3c6.6,0.6,9.8,3.8,9.8,8.6c0,5.8-4.6,9.9-13.3,9.9C268,74,263.2,72.4,259.3,69.3z"/>
<path class="st11" d="M291.2,65.8c-0.8-2.3-1.2-4.7-1.1-7.2c-0.1-2.5,0.3-4.9,1-7.2c1.8-5.1,6.6-8.1,12.9-8.1c6.5,0,11.2,3.1,13,8.1
c0.7,2.1,1,4.1,1,8.8c0,0.3-0.3,0.6-0.6,0.6c0,0-0.1,0-0.1,0h-19.5c-0.2,0-0.4,0.1-0.4,0.3c0,0,0,0,0,0.1c0,0.8,0.2,1.5,0.5,2.2
c1,2.9,3.5,4.4,7.1,4.4c2.7,0.1,5.4-0.9,7.4-2.8c0.2-0.3,0.7-0.4,1-0.1c0,0,0,0,0,0l3.9,3.2c0.2,0.1,0.3,0.5,0.2,0.7
c0,0.1-0.1,0.1-0.1,0.1c-2.7,2.9-7.2,5-13,5C297.8,73.9,293,70.9,291.2,65.8z M310.4,52.8c-0.9-2.4-3.2-3.8-6.2-3.8
s-5.4,1.4-6.2,3.8c-0.3,0.8-0.4,1.6-0.4,2.5c0,0.2,0.1,0.3,0.3,0.4c0,0,0,0,0.1,0h12.4c0.2,0,0.4-0.1,0.4-0.3c0,0,0,0,0-0.1
C310.8,54.5,310.6,53.6,310.4,52.8L310.4,52.8z"/>
<path class="st11" d="M323.6,73.3c-0.3,0-0.6-0.2-0.7-0.5c0,0,0,0,0-0.1V44.6c0-0.3,0.3-0.6,0.6-0.6c0,0,0,0,0.1,0h6
c0.3,0,0.6,0.2,0.7,0.5c0,0,0,0,0,0.1v2.5h0.1c1.5-2.2,4.2-3.8,8.2-3.8c2.4,0,4.8,0.8,6.6,2.4c0.3,0.3,0.4,0.5,0.1,0.8l-3.5,4.1
c-0.2,0.3-0.6,0.4-0.9,0.2c0,0,0,0-0.1,0c-1.4-0.9-3-1.4-4.7-1.4c-4.1,0-6,2.7-6,7.4v15.9c0,0.3-0.3,0.6-0.6,0.6c0,0,0,0-0.1,0
H323.6L323.6,73.3z"/>
<path class="st11" d="M346.5,68.5c-0.3-0.2-0.4-0.6-0.2-0.9c0,0,0,0,0,0l4.1-4.4c0.2-0.3,0.6-0.3,0.9-0.1c0,0,0,0,0,0
c3.5,2.7,7.7,4.2,12.1,4.4c5.3,0,8.4-2.5,8.4-6c0-3-2-4.9-8.1-5.7l-2.4-0.3c-8.6-1.1-13.5-4.9-13.5-11.8c0-7.5,5.9-12.4,15.1-12.4
c5.1-0.1,10.1,1.4,14.5,4.2c0.3,0.1,0.4,0.4,0.2,0.7c0,0.1-0.1,0.1-0.1,0.2l-3.1,4.5c-0.2,0.3-0.6,0.4-0.9,0.2
c-3.2-2.1-6.9-3.2-10.7-3.2c-4.5,0-7,2.3-7,5.5c0,2.9,2.2,4.8,8.2,5.6l2.4,0.3c8.6,1.1,13.3,4.9,13.3,12c0,7.3-5.7,12.8-16.8,12.8
C356.3,73.9,350,71.5,346.5,68.5z"/>
<path class="st11" d="M393.3,73.8c-6.4,0-8.8-2.9-8.8-8.6V49.8c0-0.2-0.1-0.3-0.3-0.4c0,0,0,0-0.1,0H382c-0.3,0-0.6-0.2-0.7-0.5
c0,0,0,0,0-0.1v-4.1c0-0.3,0.3-0.6,0.6-0.6c0,0,0,0,0.1,0h2.1c0.2,0,0.4-0.1,0.4-0.3c0,0,0,0,0-0.1v-8c0-0.3,0.3-0.6,0.6-0.6
c0,0,0,0,0.1,0h6c0.3,0,0.6,0.2,0.7,0.5c0,0,0,0,0,0.1v8c0,0.2,0.1,0.3,0.3,0.4c0,0,0,0,0.1,0h4.2c0.3,0,0.6,0.2,0.7,0.5
c0,0,0,0,0,0.1v4.1c0,0.3-0.3,0.6-0.6,0.6c0,0,0,0-0.1,0h-4.2c-0.2,0-0.4,0.1-0.4,0.3c0,0,0,0,0,0.1V65c0,2.1,0.9,2.7,3,2.7h1.6
c0.3,0,0.6,0.2,0.7,0.5c0,0,0,0,0,0.1v4.9c0,0.3-0.3,0.6-0.6,0.6c0,0,0,0-0.1,0L393.3,73.8L393.3,73.8z"/>
<path class="st11" d="M421.2,73.3c-0.3,0-0.6-0.2-0.7-0.5c0,0,0,0,0-0.1v-2.1h0c-1.5,2-4.5,3.4-8.9,3.4c-5.8,0-10.6-2.8-10.6-8.9
c0-6.4,4.9-9.3,12.7-9.3h6.4c0.2,0,0.4-0.1,0.4-0.3c0,0,0,0,0-0.1v-1.4c0-3.3-1.7-4.9-7-4.9c-2.6-0.1-5.1,0.6-7.2,2
c-0.3,0.2-0.7,0.2-0.9-0.1c0,0,0,0,0-0.1l-2.4-4c-0.2-0.2-0.1-0.6,0.1-0.8c0,0,0,0,0,0c2.6-1.7,6-2.9,11.2-2.9
c9.6,0,13.2,3,13.2,10.2v19.1c0,0.3-0.3,0.6-0.6,0.6c0,0,0,0-0.1,0H421.2L421.2,73.3z M420.4,63.4v-2.2c0-0.2-0.1-0.3-0.3-0.4
c0,0,0,0-0.1,0h-5.2c-4.7,0-6.8,1.2-6.8,3.9c0,2.4,1.9,3.6,5.5,3.6C417.9,68.4,420.4,66.8,420.4,63.4L420.4,63.4z"/>
<path class="st11" d="M433.1,65.8c-0.7-2.3-1.1-4.8-1-7.2c-0.1-2.4,0.3-4.9,1-7.2c1.8-5.2,6.7-8.1,13.1-8.1c4.2-0.2,8.2,1.5,11,4.6
c0.2,0.2,0.2,0.6,0,0.8c0,0,0,0-0.1,0.1l-4.1,3.3c-0.3,0.2-0.7,0.2-0.9-0.1c0,0,0,0,0-0.1c-1.5-1.7-3.6-2.6-5.9-2.5
c-2.8,0-5,1.3-5.9,3.8c-0.5,1.8-0.8,3.6-0.7,5.4c-0.1,1.8,0.1,3.7,0.7,5.5c0.9,2.5,3.1,3.8,5.9,3.8c2.2,0.1,4.4-0.9,5.9-2.6
c0.2-0.3,0.6-0.3,0.9-0.1c0,0,0,0,0,0l4.1,3.3c0.3,0.2,0.3,0.5,0.1,0.8c0,0,0,0-0.1,0.1c-2.9,3-6.9,4.6-11,4.5
C439.8,73.9,435,71.1,433.1,65.8z"/>
<path class="st11" d="M482.8,73.3c-0.4,0-0.8-0.2-1-0.6l-8-12.3l-4.3,4.6v7.7c0,0.3-0.3,0.6-0.6,0.6c0,0,0,0-0.1,0h-6
c-0.3,0-0.6-0.2-0.7-0.5c0,0,0,0,0-0.1V32.6c0-0.3,0.3-0.6,0.6-0.6c0,0,0,0,0.1,0h6c0.3,0,0.6,0.2,0.7,0.5c0,0,0,0,0,0.1v23.8
l10.8-11.8c0.3-0.4,0.8-0.6,1.2-0.6h6.7c0.2,0,0.4,0.1,0.4,0.3c0,0.1,0,0.3-0.1,0.3l-10.1,10.7L490,72.7c0.1,0.2,0.1,0.4,0,0.5
c-0.1,0.1-0.2,0.1-0.3,0.1H482.8L482.8,73.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -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

View File

@ -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/).
<img alt="BrowserStack Logo" src=".gitbook/assets/browserstack-logo.svg" width="256">
## License
See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT).

View File

@ -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",

View File

@ -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
}

View File

@ -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
}

View File

@ -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()

View File

@ -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',

View File

@ -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 <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.'
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 <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.'
commentAuthor = await instance.create('User', {
commentAuthor = await neode.create('User', {
id: 'commentAuthor',
name: 'Mrs Comment',
slug: 'mrs-comment',

View File

@ -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'

View File

@ -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,

View File

@ -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()

View File

@ -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'

View File

@ -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 () => {

View File

@ -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,
},
}

View File

@ -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!' }],
})
})
})
})

View File

@ -25,12 +25,6 @@ module.exports = {
target: 'User',
direction: 'in',
},
disabledBy: {
type: 'relationship',
relationship: 'DISABLED',
target: 'User',
direction: 'in',
},
notified: {
type: 'relationship',
relationship: 'NOTIFIED',

View File

@ -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 },
}

View File

@ -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 },
},
},
}

View File

@ -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',

View File

@ -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 () => {

View File

@ -12,4 +12,5 @@ export default {
Tag: require('./Tag.js'),
Location: require('./Location.js'),
Donations: require('./Donations.js'),
Report: require('./Report.js'),
}

View File

@ -17,7 +17,9 @@ export default makeAugmentedSchema({
'Location',
'SocialMedia',
'NOTIFIED',
'REPORTED',
'FILED',
'REVIEWED',
'Report',
'Donations',
],
},

View File

@ -78,7 +78,6 @@ export default {
hasOne: {
author: '<-[:WROTE]-(related:User)',
post: '-[:COMMENTS]->(related:Post)',
disabledBy: '<-[:DISABLED]-(related:User)',
},
}),
},

View File

@ -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()

View File

@ -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

View File

@ -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'

View File

@ -1,4 +1,4 @@
import { neode as getNeode } from '../../bootstrap/neo4j'
import { getNeode } from '../../bootstrap/neo4j'
const neode = getNeode()

View File

@ -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'

View File

@ -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 = [],

View File

@ -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'

View File

@ -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
},
},
}

View File

@ -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)
})
})
})

View File

@ -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'

View File

@ -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: {

View File

@ -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()

View File

@ -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'),

View File

@ -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'

View File

@ -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
},
},
}

View File

@ -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',
}),
]),
}),
]),
}

View File

@ -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 }

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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) => {

View File

@ -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

View File

@ -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))) {

View File

@ -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(() => {

View File

@ -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: {

View File

@ -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'

View File

@ -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',
})

View File

@ -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

View File

@ -47,7 +47,6 @@ type Comment {
updatedAt: String
deleted: Boolean
disabled: Boolean
disabledBy: User @relation(name: "DISABLED", direction: "IN")
}
type Query {

View File

@ -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
}

View File

@ -26,7 +26,7 @@ enum NotificationReason {
type Query {
notifications(read: Boolean, orderBy: NotificationOrdering, first: Int, offset: Int): [NOTIFIED]
}
type Mutation {
markAsRead(id: ID!): NOTIFIED
}

View File

@ -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]

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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,

View File

@ -19,6 +19,7 @@ export default function create() {
visibility: 'public',
deleted: false,
categoryIds: [],
imageAspectRatio: 1.333,
}
args = {
...defaults,

View File

@ -0,0 +1,7 @@
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
return neodeInstance.create('Report', args)
},
}
}

View File

@ -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 <a class="hashtag" data-hashtag-id="QuantenFlussTheorie" href="/?hashtag=QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" data-hashtag-id="QuantumGravity" href="/?hashtag=QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> 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 => {

View File

@ -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'

View File

@ -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"

View File

@ -1,6 +0,0 @@
{
"BACKEND_HOST": "http://localhost:4000",
"NEO4J_URI": "bolt://localhost:7687",
"NEO4J_USERNAME": "neo4j",
"NEO4J_PASSWORD": "letmein"
}

View File

@ -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/

View File

@ -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");
});

View File

@ -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,

View File

@ -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

View File

@ -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 })
}
)

View File

@ -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',

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",

@ -1 +1 @@
Subproject commit 808b3c5a9523505cb80b20b50348d29ba9932845
Subproject commit 7ef83405006b016fe45b476ed6e34ec189d7d283

View File

@ -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.

View File

@ -1,19 +1,9 @@
<template>
<div id="comments">
<h3 style="margin-top: -10px;">
<span>
<base-icon name="comments" />
<ds-tag
v-if="post.comments.length"
style="margin-top: -4px; margin-left: -12px; position: absolute;"
color="primary"
size="small"
round
>
{{ post.comments.length }}
</ds-tag>
<span class="list-title">{{ $t('common.comment', null, 0) }}</span>
</span>
<counter-icon icon="comments" :count="post.comments.length">
{{ $t('common.comment', null, 0) }}
</counter-icon>
</h3>
<ds-space margin-bottom="large" />
<div v-if="post.comments && post.comments.length" id="comments" class="comments">
@ -31,12 +21,14 @@
</div>
</template>
<script>
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
import Comment from '~/components/Comment/Comment'
import scrollToAnchor from '~/mixins/scrollToAnchor'
export default {
mixins: [scrollToAnchor],
components: {
CounterIcon,
Comment,
},
props: {
@ -58,9 +50,3 @@ export default {
},
}
</script>
<style lang="scss" scoped>
.list-title {
margin-left: $space-x-small;
}
</style>

View File

@ -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')
})
})

View File

@ -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] : {},
},
})
},

View File

@ -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,
},
}
})

View File

@ -7,7 +7,11 @@
@submit="submit"
>
<template slot-scope="{ errors }">
<hc-teaser-image :contribution="contribution" @addTeaserImage="addTeaserImage">
<hc-teaser-image
:contribution="contribution"
@addTeaserImage="addTeaserImage"
@addImageAspectRatio="addImageAspectRatio"
>
<img
v-if="contribution"
class="contribution-image"
@ -128,6 +132,7 @@ export default {
title: '',
content: '',
teaserImage: null,
imageAspectRatio: null,
image: null,
language: null,
categoryIds: [],
@ -190,6 +195,7 @@ export default {
content,
image,
teaserImage,
imageAspectRatio,
categoryIds,
} = this.form
this.loading = true
@ -204,6 +210,7 @@ export default {
language,
image,
imageUpload: teaserImage,
imageAspectRatio,
},
})
.then(({ data }) => {
@ -227,6 +234,9 @@ export default {
addTeaserImage(file) {
this.form.teaserImage = file
},
addImageAspectRatio(aspectRatio) {
this.form.imageAspectRatio = aspectRatio
},
categoryIds(categories) {
return categories.map(c => c.id)
},

View File

@ -64,9 +64,9 @@ describe('DropdownFilter.vue', () => {
expect(unreadLink.text()).toEqual('Unread')
})
it('clicking on menu item emits filterNotifications', () => {
it('clicking on menu item emits filter', () => {
allLink.trigger('click')
expect(wrapper.emitted().filterNotifications[0]).toEqual(
expect(wrapper.emitted().filter[0]).toEqual(
propsData.filterOptions.filter(option => option.label === 'All'),
)
})

View File

@ -20,10 +20,10 @@ storiesOf('DropdownFilter', module)
selected: filterOptions[0].label,
}),
methods: {
filterNotifications: action('filterNotifications'),
filter: action('filter'),
},
template: `<dropdown-filter
@filterNotifications="filterNotifications"
@filter="filter"
:filterOptions="filterOptions"
:selected="selected"
/>`,

View File

@ -25,7 +25,7 @@
class="dropdown-menu-item"
:route="item.route"
:parents="item.parents"
@click.stop.prevent="filterNotifications(item.route, toggleMenu)"
@click.stop.prevent="filter(item.route, toggleMenu)"
>
{{ item.route.label }}
</ds-menu-item>
@ -44,8 +44,8 @@ export default {
filterOptions: { type: Array, default: () => [] },
},
methods: {
filterNotifications(option, toggleMenu) {
this.$emit('filterNotifications', option)
filter(option, toggleMenu) {
this.$emit('filter', option)
toggleMenu()
},
},

View File

@ -6,23 +6,36 @@ const localVue = global.localVue
describe('MasonryGrid', () => {
let wrapper
let masonryGrid
let masonryGridItem
beforeEach(() => {
wrapper = mount(MasonryGrid, { localVue })
masonryGrid = wrapper.vm.$children[0]
masonryGridItem = wrapper.vm.$children[0]
})
it('adds the "reset-grid-height" class when one or more children are updating', () => {
masonryGrid.$emit('calculating-item-height')
it('adds the "reset-grid-height" class when itemsCalculating is more than 0', () => {
wrapper.setData({ itemsCalculating: 1 })
expect(wrapper.classes()).toContain('reset-grid-height')
})
it('removes the "reset-grid-height" class when all children have completed updating', () => {
wrapper.setData({ itemsCalculating: 1 })
masonryGrid.$emit('finished-calculating-item-height')
it('removes the "reset-grid-height" class when itemsCalculating is 0', () => {
wrapper.setData({ itemsCalculating: 0 })
expect(wrapper.classes()).not.toContain('reset-grid-height')
})
it('adds 1 to itemsCalculating when a child emits "calculating-item-height"', () => {
wrapper.setData({ itemsCalculating: 0 })
masonryGridItem.$emit('calculating-item-height')
expect(wrapper.vm.itemsCalculating).toBe(1)
})
it('subtracts 1 from itemsCalculating when a child emits "finished-calculating-item-height"', () => {
wrapper.setData({ itemsCalculating: 2 })
masonryGridItem.$emit('finished-calculating-item-height')
expect(wrapper.vm.itemsCalculating).toBe(1)
})
})

View File

@ -1,6 +1,5 @@
<template>
<ds-grid
:min-column-width="300"
v-on:calculating-item-height="startCalculation"
v-on:finished-calculating-item-height="endCalculation"
:class="[itemsCalculating ? 'reset-grid-height' : '']"
@ -27,7 +26,14 @@ export default {
}
</script>
<style>
<style lang="scss">
/* dirty fix to override broken styleguide inline-styles */
.ds-grid {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)) !important;
gap: 16px !important;
grid-auto-rows: 20px;
}
.reset-grid-height {
grid-auto-rows: auto !important;
align-items: self-start;

View File

@ -1,5 +1,4 @@
import { config, shallowMount } from '@vue/test-utils'
import { config, mount } from '@vue/test-utils'
import MasonryGridItem from './MasonryGridItem'
const localVue = global.localVue
@ -9,24 +8,41 @@ config.stubs['ds-grid-item'] = '<span><slot /></span>'
describe('MasonryGridItem', () => {
let wrapper
beforeEach(() => {
wrapper = shallowMount(MasonryGridItem, { localVue })
wrapper.vm.$parent.$emit = jest.fn()
describe('given an imageAspectRatio', () => {
it('sets the initial rowSpan to 13 when the ratio is higher than 1.3', () => {
const propsData = { imageAspectRatio: 2 }
wrapper = mount(MasonryGridItem, { localVue, propsData })
expect(wrapper.vm.rowSpan).toBe(13)
})
it('sets the initial rowSpan to 15 when the ratio is between 1.3 and 1', () => {
const propsData = { imageAspectRatio: 1.1 }
wrapper = mount(MasonryGridItem, { localVue, propsData })
expect(wrapper.vm.rowSpan).toBe(15)
})
it('sets the initial rowSpan to 18 when the ratio is between 1 and 0.7', () => {
const propsData = { imageAspectRatio: 0.7 }
wrapper = mount(MasonryGridItem, { localVue, propsData })
expect(wrapper.vm.rowSpan).toBe(18)
})
it('sets the initial rowSpan to 25 when the ratio is lower than 0.7', () => {
const propsData = { imageAspectRatio: 0.3 }
wrapper = mount(MasonryGridItem, { localVue, propsData })
expect(wrapper.vm.rowSpan).toBe(25)
})
})
it('emits "calculating-item-height" when starting calculation', async () => {
wrapper.vm.calculateItemHeight()
await wrapper.vm.$nextTick()
describe('given no aspect ratio', () => {
it('sets the initial rowSpan to 8 when not given an imageAspectRatio', () => {
wrapper = mount(MasonryGridItem, { localVue })
const firstCallArgument = wrapper.vm.$parent.$emit.mock.calls[0][0]
expect(firstCallArgument).toBe('calculating-item-height')
})
it('emits "finished-calculating-item-height" after the calculation', async () => {
wrapper.vm.calculateItemHeight()
await wrapper.vm.$nextTick()
const secondCallArgument = wrapper.vm.$parent.$emit.mock.calls[1][0]
expect(secondCallArgument).toBe('finished-calculating-item-height')
expect(wrapper.vm.rowSpan).toBe(8)
})
})
})

View File

@ -5,15 +5,33 @@
</template>
<script>
const landscapeRatio = 1.3
const squareRatio = 1
const portraitRatio = 0.7
const getRowSpan = aspectRatio => {
if (aspectRatio >= landscapeRatio) return 13
else if (aspectRatio >= squareRatio) return 15
else if (aspectRatio >= portraitRatio) return 18
else return 25
}
export default {
props: {
imageAspectRatio: {
type: Number,
default: null,
},
},
data() {
return {
rowSpan: 10,
rowSpan: this.imageAspectRatio ? getRowSpan(this.imageAspectRatio) : 8,
}
},
methods: {
calculateItemHeight() {
this.$parent.$emit('calculating-item-height')
this.$nextTick(() => {
const gridStyle = this.$parent.$el.style
const rowHeight = parseInt(gridStyle.gridAutoRows)
@ -27,13 +45,7 @@ export default {
},
},
mounted() {
const image = this.$el.querySelector('img')
if (image) {
image.onload = () => this.calculateItemHeight()
} else {
// use timeout to make sure layout is set up before calculation
setTimeout(() => this.calculateItemHeight(), 0)
}
this.calculateItemHeight()
},
}
</script>

View File

@ -23,11 +23,11 @@
@close="close"
/>
<confirm-modal
v-if="open === 'delete'"
v-if="open === 'confirm'"
:id="data.resource.id"
:type="data.type"
:name="name"
:modalData="data.modalsData.delete"
:modalData="data.modalData"
@close="close"
/>
</div>

View File

@ -77,7 +77,7 @@ export default {
}, 500)
}, 1500)
} catch (err) {
this.success = false
this.isOpen = false
} finally {
this.loading = false
}

View File

@ -26,9 +26,7 @@ describe('DisableModal.vue', () => {
$apollo: {
mutate: jest
.fn()
.mockResolvedValueOnce({
enable: 'u4711',
})
.mockResolvedValueOnce()
.mockRejectedValue({
message: 'Not Authorised!',
}),
@ -159,11 +157,13 @@ describe('DisableModal.vue', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalled()
})
it('passes id to mutation', () => {
it('passes parameters to mutation', () => {
const calls = mocks.$apollo.mutate.mock.calls
const [[{ variables }]] = calls
expect(variables).toEqual({
id: 'u4711',
expect(variables).toMatchObject({
resourceId: 'u4711',
disable: true,
closed: false,
})
})

View File

@ -54,11 +54,13 @@ export default {
// await this.modalData.buttons.confirm.callback()
await this.$apollo.mutate({
mutation: gql`
mutation($id: ID!) {
disable(id: $id)
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
disable
}
}
`,
variables: { id: this.id },
variables: { resourceId: this.id, disable: true, closed: false },
})
this.$toast.success(this.$t('disable.success'))
this.isOpen = false
@ -67,6 +69,7 @@ export default {
}, 1000)
} catch (err) {
this.$toast.error(err.message)
this.isOpen = false
}
},
},

View File

@ -149,6 +149,7 @@ export default {
default:
this.$toast.error(err.message)
}
this.isOpen = false
this.loading = false
})
},

View File

@ -13,7 +13,7 @@
</nuxt-link>
<ds-space margin-bottom="small" />
<!-- Username, Image & Date of Post -->
<div>
<div class="user-wrapper">
<client-only>
<hc-user :user="post.author" :trunc="35" :date-time="post.createdAt" />
</client-only>
@ -141,10 +141,19 @@ export default {
this.$emit('unpinPost', post)
},
},
mounted() {
const width = this.$el.offsetWidth
const height = Math.min(width / this.post.imageAspectRatio, 2000)
const imageElement = this.$el.querySelector('.ds-card-image')
if (imageElement) {
imageElement.style.height = `${height}px`
}
},
}
</script>
<style lang="scss" scoped>
<style lang="scss">
.ds-card-image img {
width: 100%;
max-height: 2000px;
@ -159,9 +168,21 @@ export default {
cursor: pointer;
position: relative;
z-index: 1;
justify-content: space-between;
/*.ds-card-footer {
}*/
> .ds-card-content {
flex-grow: 0;
}
/* workaround to avoid jumping layout when footer is rendered */
> .ds-card-footer {
height: 75px;
}
/* workaround to avoid jumping layout when hc-user is rendered */
.user-wrapper {
height: 36px;
}
.content-menu {
display: inline-block;

View File

@ -27,7 +27,7 @@ describe('ReleaseModal.vue', () => {
$apollo: {
mutate: jest
.fn()
.mockResolvedValueOnce({ enable: 'u4711' })
.mockResolvedValueOnce()
.mockRejectedValue({ message: 'Not Authorised!' }),
},
location: {
@ -154,11 +154,13 @@ describe('ReleaseModal.vue', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalled()
})
it('passes id to mutation', () => {
it('passes parameters to mutation', () => {
const calls = mocks.$apollo.mutate.mock.calls
const [[{ variables }]] = calls
expect(variables).toEqual({
id: 'u4711',
expect(variables).toMatchObject({
resourceId: 'u4711',
disable: false,
closed: false,
})
})

View File

@ -53,11 +53,13 @@ export default {
// await this.modalData.buttons.confirm.callback()
await this.$apollo.mutate({
mutation: gql`
mutation($id: ID!) {
enable(id: $id)
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
disable
}
}
`,
variables: { id: this.id },
variables: { resourceId: this.id, disable: false, closed: false },
})
this.$toast.success(this.$t('release.success'))
this.isOpen = false
@ -66,6 +68,7 @@ export default {
}, 1000)
} catch (err) {
this.$toast.error(err.message)
this.isOpen = false
}
},
},

Some files were not shown because too many files have changed in this diff Show More