mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
Merge branch 'master' into 1746-Blur_explicit_Image_Content
This commit is contained in:
commit
dee22d33db
90
.gitbook/assets/browserstack-logo.svg
Normal file
90
.gitbook/assets/browserstack-logo.svg
Normal 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 |
@ -8,13 +8,13 @@ addons:
|
||||
- docker
|
||||
- chromium
|
||||
|
||||
before_install:
|
||||
install:
|
||||
- yarn global add wait-on
|
||||
# Install Codecov
|
||||
- yarn install
|
||||
- cp cypress.env.template.json cypress.env.json
|
||||
|
||||
install:
|
||||
before_script:
|
||||
- docker-compose -f docker-compose.yml build --parallel
|
||||
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml build # just tagging, just be quite fast
|
||||
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml up -d
|
||||
@ -30,10 +30,6 @@ script:
|
||||
- docker-compose exec backend yarn run test --ci --verbose=false --coverage
|
||||
- docker-compose exec backend yarn run db:seed
|
||||
- docker-compose exec backend yarn run db:reset
|
||||
# ActivityPub cucumber testing temporarily disabled because it's too buggy
|
||||
# - docker-compose exec backend yarn run test:cucumber --tags "not @wip"
|
||||
# - docker-compose exec backend yarn run db:reset
|
||||
# - docker-compose exec backend yarn run db:seed
|
||||
# Frontend
|
||||
- docker-compose exec webapp yarn run lint
|
||||
- docker-compose exec webapp yarn run test --ci --verbose=false --coverage
|
||||
@ -42,6 +38,7 @@ script:
|
||||
- docker-compose -f docker-compose.yml up -d
|
||||
- wait-on http://localhost:7474
|
||||
- yarn run cypress:run --record
|
||||
- yarn run cucumber
|
||||
# Coverage
|
||||
- yarn run codecov
|
||||
|
||||
|
||||
@ -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).
|
||||
|
||||
12
babel.config.json
Normal file
12
babel.config.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": "10"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@ -10,8 +10,6 @@
|
||||
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,gql",
|
||||
"lint": "eslint src --config .eslintrc.js",
|
||||
"test": "jest --forceExit --detectOpenHandles --runInBand",
|
||||
"test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/",
|
||||
"test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --",
|
||||
"db:reset": "babel-node src/seed/reset-db.js",
|
||||
"db:seed": "babel-node src/seed/seed-db.js"
|
||||
},
|
||||
@ -35,7 +33,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/joi": "^16.1.8",
|
||||
"@sentry/node": "^5.9.0",
|
||||
"@sentry/node": "^5.10.0",
|
||||
"apollo-cache-inmemory": "~1.6.3",
|
||||
"apollo-client": "~2.6.4",
|
||||
"apollo-link-context": "~1.0.19",
|
||||
@ -86,7 +84,7 @@
|
||||
"neo4j-graphql-js": "^2.9.3",
|
||||
"neode": "^0.3.3",
|
||||
"node-fetch": "~2.6.0",
|
||||
"nodemailer": "^6.3.1",
|
||||
"nodemailer": "^6.4.0",
|
||||
"nodemailer-html-to-text": "^3.1.0",
|
||||
"npm-run-all": "~4.1.5",
|
||||
"request": "~2.88.0",
|
||||
|
||||
@ -1,27 +1,29 @@
|
||||
import user from './user'
|
||||
import inbox from './inbox'
|
||||
import webFinger from './webFinger'
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import verify from './verify'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/.well-known/webFinger', cors(), express.urlencoded({ extended: true }), webFinger)
|
||||
router.use(
|
||||
'/activitypub/users',
|
||||
cors(),
|
||||
express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
|
||||
express.urlencoded({ extended: true }),
|
||||
user,
|
||||
)
|
||||
router.use(
|
||||
'/activitypub/inbox',
|
||||
cors(),
|
||||
express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
|
||||
express.urlencoded({ extended: true }),
|
||||
verify,
|
||||
inbox,
|
||||
)
|
||||
|
||||
export default router
|
||||
export default function() {
|
||||
const router = express.Router()
|
||||
router.use(
|
||||
'/activitypub/users',
|
||||
cors(),
|
||||
express.json({
|
||||
type: ['application/activity+json', 'application/ld+json', 'application/json'],
|
||||
}),
|
||||
express.urlencoded({ extended: true }),
|
||||
user,
|
||||
)
|
||||
router.use(
|
||||
'/activitypub/inbox',
|
||||
cors(),
|
||||
express.json({
|
||||
type: ['application/activity+json', 'application/ld+json', 'application/json'],
|
||||
}),
|
||||
express.urlencoded({ extended: true }),
|
||||
verify,
|
||||
inbox,
|
||||
)
|
||||
return router
|
||||
}
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
import express from 'express'
|
||||
import { createWebFinger } from '../utils/actor'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', async function(req, res) {
|
||||
const resource = req.query.resource
|
||||
if (!resource || !resource.includes('acct:')) {
|
||||
return res
|
||||
.status(400)
|
||||
.send(
|
||||
'Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.',
|
||||
)
|
||||
} else {
|
||||
const nameAndDomain = resource.replace('acct:', '')
|
||||
const name = nameAndDomain.split('@')[0]
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await req.app.get('ap').dataSource.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug: "${name}") {
|
||||
slug
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error })
|
||||
}
|
||||
|
||||
if (result.data && result.data.User.length > 0) {
|
||||
const webFinger = createWebFinger(name)
|
||||
return res.contentType('application/jrd+json').json(webFinger)
|
||||
} else {
|
||||
return res.status(404).json({ error: `No record found for ${nameAndDomain}.` })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
59
backend/src/activitypub/routes/webfinger.js
Normal file
59
backend/src/activitypub/routes/webfinger.js
Normal file
@ -0,0 +1,59 @@
|
||||
import express from 'express'
|
||||
import CONFIG from '../../config/'
|
||||
import cors from 'cors'
|
||||
|
||||
const debug = require('debug')('ea:webfinger')
|
||||
const regex = /acct:([a-z0-9_-]*)@([a-z0-9_-]*)/
|
||||
|
||||
const createWebFinger = name => {
|
||||
const { host } = new URL(CONFIG.CLIENT_URI)
|
||||
return {
|
||||
subject: `acct:${name}@${host}`,
|
||||
links: [
|
||||
{
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
href: `${CONFIG.CLIENT_URI}/activitypub/users/${name}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export async function handler(req, res) {
|
||||
const { resource = '' } = req.query
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [_, name, domain] = resource.match(regex) || []
|
||||
if (!(name && domain))
|
||||
return res.status(400).json({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
|
||||
const session = req.app.get('driver').session()
|
||||
try {
|
||||
const [slug] = await session.readTransaction(async t => {
|
||||
const result = await t.run('MATCH (u:User {slug: $slug}) RETURN u.slug AS slug', {
|
||||
slug: name,
|
||||
})
|
||||
return result.records.map(record => record.get('slug'))
|
||||
})
|
||||
if (!slug)
|
||||
return res.status(404).json({
|
||||
error: `No record found for "${name}@${domain}".`,
|
||||
})
|
||||
const webFinger = createWebFinger(name)
|
||||
return res.contentType('application/jrd+json').json(webFinger)
|
||||
} catch (error) {
|
||||
debug(error)
|
||||
return res.status(500).json({
|
||||
error: 'Something went terribly wrong. Please contact support@human-connection.org',
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export default function() {
|
||||
const router = express.Router()
|
||||
router.use('/webfinger', cors(), express.urlencoded({ extended: true }), handler)
|
||||
return router
|
||||
}
|
||||
113
backend/src/activitypub/routes/webfinger.spec.js
Normal file
113
backend/src/activitypub/routes/webfinger.spec.js
Normal file
@ -0,0 +1,113 @@
|
||||
import { handler } from './webfinger'
|
||||
import Factory from '../../seed/factories'
|
||||
import { getDriver } from '../../bootstrap/neo4j'
|
||||
|
||||
let resource, res, json, status, contentType
|
||||
|
||||
const factory = Factory()
|
||||
const driver = getDriver()
|
||||
|
||||
const request = () => {
|
||||
json = jest.fn()
|
||||
status = jest.fn(() => ({ json }))
|
||||
contentType = jest.fn(() => ({ json }))
|
||||
res = { status, contentType }
|
||||
const req = {
|
||||
app: {
|
||||
get: key => {
|
||||
return {
|
||||
driver,
|
||||
}[key]
|
||||
},
|
||||
},
|
||||
query: {
|
||||
resource,
|
||||
},
|
||||
}
|
||||
return handler(req, res)
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('webfinger', () => {
|
||||
describe('no ressource', () => {
|
||||
beforeEach(() => {
|
||||
resource = undefined
|
||||
})
|
||||
|
||||
it('sends HTTP 400', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(400)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('?resource query param', () => {
|
||||
describe('is missing acct:', () => {
|
||||
beforeEach(() => {
|
||||
resource = 'some-user@domain'
|
||||
})
|
||||
|
||||
it('sends HTTP 400', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(400)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('has no domain', () => {
|
||||
beforeEach(() => {
|
||||
resource = 'acct:some-user@'
|
||||
})
|
||||
|
||||
it('sends HTTP 400', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(400)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with acct:', () => {
|
||||
beforeEach(() => {
|
||||
resource = 'acct:some-user@domain'
|
||||
})
|
||||
|
||||
it('returns error as json', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(404)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'No record found for "some-user@domain".',
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a user for acct', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', { slug: 'some-user' })
|
||||
})
|
||||
|
||||
it('returns user object', async () => {
|
||||
await request()
|
||||
expect(contentType).toHaveBeenCalledWith('application/jrd+json')
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
links: [
|
||||
{
|
||||
href: 'http://localhost:3000/activitypub/users/some-user',
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
},
|
||||
],
|
||||
subject: 'acct:some-user@localhost:3000',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -22,17 +22,3 @@ export function createActor(name, pubkey) {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function createWebFinger(name) {
|
||||
const { host } = new URL(activityPub.endpoint)
|
||||
return {
|
||||
subject: `acct:${name}@${host}`,
|
||||
links: [
|
||||
{
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
href: `${activityPub.endpoint}/activitypub/users/${name}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import dotenv from 'dotenv'
|
||||
import path from 'path'
|
||||
|
||||
dotenv.config()
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../.env') })
|
||||
|
||||
const {
|
||||
MAPBOX_TOKEN,
|
||||
|
||||
@ -11,15 +11,21 @@ export default async (driver, authorizationHeader) => {
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
const session = driver.session()
|
||||
const query = `
|
||||
MATCH (user:User {id: $id, deleted: false, disabled: false })
|
||||
SET user.lastActiveAt = toString(datetime())
|
||||
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
|
||||
LIMIT 1
|
||||
`
|
||||
const result = await session.run(query, { id })
|
||||
session.close()
|
||||
const session = driver.session()
|
||||
let result
|
||||
|
||||
try {
|
||||
result = await session.run(query, { id })
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
const [currentUser] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
|
||||
@ -3,7 +3,6 @@ import extractHashtags from '../hashtags/extractHashtags'
|
||||
const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
||||
if (!hashtags.length) return
|
||||
|
||||
const session = context.driver.session()
|
||||
// We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement
|
||||
// functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted
|
||||
// and no new Hashtags and relations will be created.
|
||||
@ -19,14 +18,18 @@ const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
||||
MERGE (p)-[:TAGGED]->(t)
|
||||
RETURN p, t
|
||||
`
|
||||
await session.run(cypherDeletePreviousRelations, {
|
||||
postId,
|
||||
})
|
||||
await session.run(cypherCreateNewTagsAndRelations, {
|
||||
postId,
|
||||
hashtags,
|
||||
})
|
||||
session.close()
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
await session.run(cypherDeletePreviousRelations, {
|
||||
postId,
|
||||
})
|
||||
await session.run(cypherCreateNewTagsAndRelations, {
|
||||
postId,
|
||||
hashtags,
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
import extractMentionedUsers from './mentions/extractMentionedUsers'
|
||||
|
||||
const postAuthorOfComment = async (comment, { context }) => {
|
||||
const session = context.driver.session()
|
||||
const cypherFindUser = `
|
||||
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||
RETURN user { .id }
|
||||
`
|
||||
const result = await session.run(cypherFindUser, {
|
||||
commentId: comment.id,
|
||||
})
|
||||
session.close()
|
||||
const session = context.driver.session()
|
||||
let result
|
||||
try {
|
||||
result = await session.run(cypherFindUser, {
|
||||
commentId: comment.id,
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
const [postAuthor] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
@ -31,7 +35,6 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
||||
throw new Error('Notification does not fit the reason!')
|
||||
}
|
||||
|
||||
const session = context.driver.session()
|
||||
let cypher
|
||||
switch (reason) {
|
||||
case 'mentioned_in_post': {
|
||||
@ -85,12 +88,16 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
||||
break
|
||||
}
|
||||
}
|
||||
await session.run(cypher, {
|
||||
id,
|
||||
idsOfUsers,
|
||||
reason,
|
||||
})
|
||||
session.close()
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
await session.run(cypher, {
|
||||
id,
|
||||
idsOfUsers,
|
||||
reason,
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
@ -123,15 +130,19 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) =>
|
||||
const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo)
|
||||
|
||||
if (comment) {
|
||||
const session = context.driver.session()
|
||||
const cypherFindUser = `
|
||||
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||
RETURN user { .id }
|
||||
`
|
||||
const result = await session.run(cypherFindUser, {
|
||||
commentId: comment.id,
|
||||
})
|
||||
session.close()
|
||||
const session = context.driver.session()
|
||||
let result
|
||||
try {
|
||||
result = await session.run(cypherFindUser, {
|
||||
commentId: comment.id,
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
const [postAuthor] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -45,8 +45,8 @@ const isAuthor = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!user) return false
|
||||
const session = driver.session()
|
||||
const { id: resourceId } = args
|
||||
const session = driver.session()
|
||||
try {
|
||||
const result = await session.run(
|
||||
`
|
||||
@ -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,
|
||||
|
||||
@ -3,11 +3,14 @@ import uniqueSlug from './slugify/uniqueSlug'
|
||||
const isUniqueFor = (context, type) => {
|
||||
return async slug => {
|
||||
const session = context.driver.session()
|
||||
const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, {
|
||||
slug,
|
||||
})
|
||||
session.close()
|
||||
return response.records.length === 0
|
||||
try {
|
||||
const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, {
|
||||
slug,
|
||||
})
|
||||
return response.records.length === 0
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -5,7 +5,7 @@ const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
|
||||
const NO_CATEGORIES_ERR_MESSAGE =
|
||||
'You cannot save a post without at least one category or more than three'
|
||||
|
||||
const validateCommentCreation = async (resolve, root, args, context, info) => {
|
||||
const validateCreateComment = async (resolve, root, args, context, info) => {
|
||||
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||
const { postId } = args
|
||||
|
||||
@ -13,28 +13,30 @@ const validateCommentCreation = async (resolve, root, args, context, info) => {
|
||||
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
|
||||
}
|
||||
const session = context.driver.session()
|
||||
const postQueryRes = await session.run(
|
||||
`
|
||||
try {
|
||||
const postQueryRes = await session.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
RETURN post`,
|
||||
{
|
||||
postId,
|
||||
},
|
||||
)
|
||||
session.close()
|
||||
const [post] = postQueryRes.records.map(record => {
|
||||
return record.get('post')
|
||||
})
|
||||
{
|
||||
postId,
|
||||
},
|
||||
)
|
||||
const [post] = postQueryRes.records.map(record => {
|
||||
return record.get('post')
|
||||
})
|
||||
|
||||
if (!post) {
|
||||
throw new UserInputError(NO_POST_ERR_MESSAGE)
|
||||
} else {
|
||||
return resolve(root, args, context, info)
|
||||
if (!post) {
|
||||
throw new UserInputError(NO_POST_ERR_MESSAGE)
|
||||
} else {
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const validateUpdateComment = async (resolve, root, args, context, info) => {
|
||||
const COMMENT_MIN_LENGTH = 1
|
||||
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||
if (!args.content || content.length < COMMENT_MIN_LENGTH) {
|
||||
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
|
||||
@ -59,36 +61,67 @@ 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!')
|
||||
const session = driver.session()
|
||||
const reportQueryRes = await session.run(
|
||||
`
|
||||
MATCH (:User {id:$submitterId})-[:REPORTED]->(resource {id:$resourceId})
|
||||
RETURN labels(resource)[0] as label
|
||||
`,
|
||||
{
|
||||
resourceId,
|
||||
submitterId: user.id,
|
||||
},
|
||||
)
|
||||
session.close()
|
||||
const [existingReportedResource] = reportQueryRes.records.map(record => {
|
||||
return {
|
||||
label: record.get('label'),
|
||||
}
|
||||
})
|
||||
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()
|
||||
const reportReadTxPromise = session.writeTransaction(async txc => {
|
||||
const validateReviewTransactionResponse = await txc.run(
|
||||
`
|
||||
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,
|
||||
},
|
||||
)
|
||||
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()
|
||||
}
|
||||
|
||||
if (existingReportedResource) throw new Error(`${existingReportedResource.label}`)
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateComment: validateCommentCreation,
|
||||
CreateComment: validateCreateComment,
|
||||
UpdateComment: validateUpdateComment,
|
||||
CreatePost: validatePost,
|
||||
UpdatePost: validateUpdatePost,
|
||||
report: validateReport,
|
||||
fileReport: validateReport,
|
||||
review: validateReview,
|
||||
},
|
||||
}
|
||||
|
||||
400
backend/src/middleware/validation/validationMiddleware.spec.js
Normal file
400
backend/src/middleware/validation/validationMiddleware.spec.js
Normal file
@ -0,0 +1,400 @@
|
||||
import { gql } from '../../helpers/jest'
|
||||
import Factory from '../../seed/factories'
|
||||
import { neode as 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 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) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
const updateCommentMutation = gql`
|
||||
mutation($content: String!, $id: ID!) {
|
||||
UpdateComment(content: $content, id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
const createPostMutation = gql`
|
||||
mutation($id: ID, $title: String!, $content: String!, $language: String, $categoryIds: [ID]) {
|
||||
CreatePost(
|
||||
id: $id
|
||||
title: $title
|
||||
content: $content
|
||||
language: $language
|
||||
categoryIds: $categoryIds
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const updatePostMutation = gql`
|
||||
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
|
||||
UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
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: () => {
|
||||
return {
|
||||
user: authenticatedUser,
|
||||
neode,
|
||||
driver,
|
||||
}
|
||||
},
|
||||
})
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
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 () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('validateCreateComment', () => {
|
||||
let createCommentVariables
|
||||
beforeEach(async () => {
|
||||
createCommentVariables = {
|
||||
postId: 'whatever',
|
||||
content: '',
|
||||
}
|
||||
authenticatedUser = await commentingUser.toJson()
|
||||
})
|
||||
|
||||
it('throws an error if content is empty', async () => {
|
||||
createCommentVariables = { ...createCommentVariables, postId: 'post-4-commenting' }
|
||||
await expect(
|
||||
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreateComment: null },
|
||||
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('sanitizes content and throws an error if not longer than 1 character', async () => {
|
||||
createCommentVariables = { postId: 'post-4-commenting', content: '<a></a>' }
|
||||
await expect(
|
||||
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreateComment: null },
|
||||
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an error if there is no post with given id in the database', async () => {
|
||||
createCommentVariables = {
|
||||
...createCommentVariables,
|
||||
postId: 'non-existent-post',
|
||||
content: 'valid content',
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreateComment: null },
|
||||
errors: [{ message: 'Comment cannot be created without a post!' }],
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateUpdateComment', () => {
|
||||
let updateCommentVariables
|
||||
beforeEach(async () => {
|
||||
await factory.create('Comment', {
|
||||
id: 'comment-id',
|
||||
authorId: 'commenting-user',
|
||||
})
|
||||
updateCommentVariables = {
|
||||
id: 'whatever',
|
||||
content: '',
|
||||
}
|
||||
authenticatedUser = await commentingUser.toJson()
|
||||
})
|
||||
|
||||
it('throws an error if content is empty', async () => {
|
||||
updateCommentVariables = { ...updateCommentVariables, id: 'comment-id' }
|
||||
await expect(
|
||||
mutate({ mutation: updateCommentMutation, variables: updateCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdateComment: null },
|
||||
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('sanitizes content and throws an error if not longer than 1 character', async () => {
|
||||
updateCommentVariables = { id: 'comment-id', content: '<a></a>' }
|
||||
await expect(
|
||||
mutate({ mutation: updateCommentMutation, variables: updateCommentVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdateComment: null },
|
||||
errors: [{ message: 'Comment must be at least 1 character long!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validatePost', () => {
|
||||
let createPostVariables
|
||||
beforeEach(async () => {
|
||||
createPostVariables = {
|
||||
title: 'I am a title',
|
||||
content: 'Some content',
|
||||
}
|
||||
authenticatedUser = await commentingUser.toJson()
|
||||
})
|
||||
|
||||
describe('categories', () => {
|
||||
describe('null', () => {
|
||||
it('throws UserInputError', async () => {
|
||||
createPostVariables = { ...createPostVariables, categoryIds: null }
|
||||
await expect(
|
||||
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreatePost: null },
|
||||
errors: [
|
||||
{
|
||||
message: 'You cannot save a post without at least one category or more than three',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('empty', () => {
|
||||
it('throws UserInputError', async () => {
|
||||
createPostVariables = { ...createPostVariables, categoryIds: [] }
|
||||
await expect(
|
||||
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreatePost: null },
|
||||
errors: [
|
||||
{
|
||||
message: 'You cannot save a post without at least one category or more than three',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('more than 3 categoryIds', () => {
|
||||
it('throws UserInputError', async () => {
|
||||
createPostVariables = {
|
||||
...createPostVariables,
|
||||
categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'],
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: createPostMutation, variables: createPostVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { CreatePost: null },
|
||||
errors: [
|
||||
{
|
||||
message: 'You cannot save a post without at least one category or more than three',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateUpdatePost', () => {
|
||||
describe('post created without categories somehow', () => {
|
||||
let owner, updatePostVariables
|
||||
beforeEach(async () => {
|
||||
const postSomehowCreated = await neode.create('Post', {
|
||||
id: 'how-was-this-created',
|
||||
})
|
||||
owner = await neode.create('User', {
|
||||
id: 'author-of-post-without-category',
|
||||
slug: 'hacker',
|
||||
})
|
||||
await postSomehowCreated.relateTo(owner, 'author')
|
||||
authenticatedUser = await owner.toJson()
|
||||
updatePostVariables = {
|
||||
id: 'how-was-this-created',
|
||||
title: 'I am a title',
|
||||
content: 'Some content',
|
||||
categoryIds: [],
|
||||
}
|
||||
})
|
||||
|
||||
it('requires at least one category for successful update', async () => {
|
||||
await expect(
|
||||
mutate({ mutation: updatePostMutation, variables: updatePostVariables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdatePost: null },
|
||||
errors: [
|
||||
{ message: 'You cannot save a post without at least one category or more than three' },
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -25,12 +25,6 @@ module.exports = {
|
||||
target: 'User',
|
||||
direction: 'in',
|
||||
},
|
||||
disabledBy: {
|
||||
type: 'relationship',
|
||||
relationship: 'DISABLED',
|
||||
target: 'User',
|
||||
direction: 'in',
|
||||
},
|
||||
notified: {
|
||||
type: 'relationship',
|
||||
relationship: 'NOTIFIED',
|
||||
|
||||
@ -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',
|
||||
@ -46,4 +40,5 @@ module.exports = {
|
||||
},
|
||||
language: { type: 'string', allow: [null] },
|
||||
blurImage: { type: 'boolean', default: false },
|
||||
imageAspectRatio: { type: 'float', default: 1.0 },
|
||||
}
|
||||
|
||||
52
backend/src/models/Report.js
Normal file
52
backend/src/models/Report.js
Normal 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 },
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -12,4 +12,5 @@ export default {
|
||||
Tag: require('./Tag.js'),
|
||||
Location: require('./Location.js'),
|
||||
Donations: require('./Donations.js'),
|
||||
Report: require('./Report.js'),
|
||||
}
|
||||
|
||||
@ -17,7 +17,9 @@ export default makeAugmentedSchema({
|
||||
'Location',
|
||||
'SocialMedia',
|
||||
'NOTIFIED',
|
||||
'REPORTED',
|
||||
'FILED',
|
||||
'REVIEWED',
|
||||
'Report',
|
||||
'Donations',
|
||||
],
|
||||
},
|
||||
|
||||
@ -13,7 +13,8 @@ export default {
|
||||
params.id = params.id || uuid()
|
||||
|
||||
const session = context.driver.session()
|
||||
const createCommentCypher = `
|
||||
try {
|
||||
const createCommentCypher = `
|
||||
MATCH (post:Post {id: $postId})
|
||||
MATCH (author:User {id: $userId})
|
||||
WITH post, author
|
||||
@ -23,45 +24,53 @@ export default {
|
||||
MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author)
|
||||
RETURN comment
|
||||
`
|
||||
const transactionRes = await session.run(createCommentCypher, {
|
||||
userId: context.user.id,
|
||||
postId,
|
||||
params,
|
||||
})
|
||||
session.close()
|
||||
const transactionRes = await session.run(createCommentCypher, {
|
||||
userId: context.user.id,
|
||||
postId,
|
||||
params,
|
||||
})
|
||||
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
|
||||
return comment
|
||||
return comment
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
UpdateComment: async (_parent, params, context, _resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const updateCommentCypher = `
|
||||
try {
|
||||
const updateCommentCypher = `
|
||||
MATCH (comment:Comment {id: $params.id})
|
||||
SET comment += $params
|
||||
SET comment.updatedAt = toString(datetime())
|
||||
RETURN comment
|
||||
`
|
||||
const transactionRes = await session.run(updateCommentCypher, { params })
|
||||
session.close()
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
return comment
|
||||
const transactionRes = await session.run(updateCommentCypher, { params })
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
return comment
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
DeleteComment: async (_parent, args, context, _resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const transactionRes = await session.run(
|
||||
`
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`
|
||||
MATCH (comment:Comment {id: $commentId})
|
||||
SET comment.deleted = TRUE
|
||||
SET comment.content = 'UNAVAILABLE'
|
||||
SET comment.contentExcerpt = 'UNAVAILABLE'
|
||||
RETURN comment
|
||||
`,
|
||||
{ commentId: args.id },
|
||||
)
|
||||
session.close()
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
return comment
|
||||
{ commentId: args.id },
|
||||
)
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
return comment
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
Comment: {
|
||||
@ -69,7 +78,6 @@ export default {
|
||||
hasOne: {
|
||||
author: '<-[:WROTE]-(related:User)',
|
||||
post: '-[:COMMENTS]->(related:Post)',
|
||||
disabledBy: '<-[:DISABLED]-(related:User)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@ -111,42 +111,6 @@ describe('CreateComment', () => {
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('comment content is empty', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, content: '<p></p>' }
|
||||
})
|
||||
|
||||
it('throw UserInput error', async () => {
|
||||
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||
expect(data).toEqual({ CreateComment: null })
|
||||
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('comment content contains only whitespaces', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, content: ' <p> </p> ' }
|
||||
})
|
||||
|
||||
it('throw UserInput error', async () => {
|
||||
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||
expect(data).toEqual({ CreateComment: null })
|
||||
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('invalid post id', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, postId: 'does-not-exist' }
|
||||
})
|
||||
|
||||
it('throw UserInput error', async () => {
|
||||
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||
expect(data).toEqual({ CreateComment: null })
|
||||
expect(errors[0]).toHaveProperty('message', 'Comment cannot be created without a post!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -226,17 +190,6 @@ describe('UpdateComment', () => {
|
||||
expect(newlyCreatedComment.updatedAt).not.toEqual(UpdateComment.updatedAt)
|
||||
})
|
||||
|
||||
describe('if `content` empty', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, content: ' <p> </p>' }
|
||||
})
|
||||
|
||||
it('throws InputError', async () => {
|
||||
const { errors } = await mutate({ mutation: updateCommentMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('if comment does not exist for given id', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, id: 'does-not-exist' }
|
||||
|
||||
@ -2,8 +2,8 @@ export default {
|
||||
Mutation: {
|
||||
UpdateDonations: async (_parent, params, context, _resolveInfo) => {
|
||||
const { driver } = context
|
||||
const session = driver.session()
|
||||
let donations
|
||||
const session = driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async txc => {
|
||||
const updateDonationsTransactionResponse = await txc.run(
|
||||
`
|
||||
|
||||
@ -4,7 +4,6 @@ export default async function createPasswordReset(options) {
|
||||
const { driver, nonce, email, issuedAt = new Date() } = options
|
||||
const normalizedEmail = normalizeEmail(email)
|
||||
const session = driver.session()
|
||||
let response = {}
|
||||
try {
|
||||
const cypher = `
|
||||
MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email})
|
||||
@ -23,9 +22,8 @@ export default async function createPasswordReset(options) {
|
||||
const { name } = record.get('u').properties
|
||||
return { email, nonce, name }
|
||||
})
|
||||
response = records[0] || {}
|
||||
return records[0] || {}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
@ -1,41 +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()
|
||||
const res = await session.run(cypher, { id, userId })
|
||||
session.close()
|
||||
const [resource] = res.records.map(record => {
|
||||
return record.get('resource')
|
||||
})
|
||||
if (!resource) return null
|
||||
return resource.id
|
||||
},
|
||||
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()
|
||||
const res = await session.run(cypher, { id })
|
||||
session.close()
|
||||
const [resource] = res.records.map(record => {
|
||||
return record.get('resource')
|
||||
})
|
||||
if (!resource) return null
|
||||
return resource.id
|
||||
try {
|
||||
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)
|
||||
})
|
||||
const txResult = await reviewWriteTxResultPromise
|
||||
if (!txResult[0]) return null
|
||||
createdRelationshipWithNestedAttributes = txResult[0]
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
return createdRelationshipWithNestedAttributes
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -8,45 +8,53 @@ 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -18,7 +18,7 @@ export default {
|
||||
notifications: async (_parent, args, context, _resolveInfo) => {
|
||||
const { user: currentUser } = context
|
||||
const session = context.driver.session()
|
||||
let notifications, whereClause, orderByClause
|
||||
let whereClause, orderByClause
|
||||
|
||||
switch (args.read) {
|
||||
case true:
|
||||
@ -42,27 +42,25 @@ export default {
|
||||
}
|
||||
const offset = args.offset && typeof args.offset === 'number' ? `SKIP ${args.offset}` : ''
|
||||
const limit = args.first && typeof args.first === 'number' ? `LIMIT ${args.first}` : ''
|
||||
try {
|
||||
const cypher = `
|
||||
const cypher = `
|
||||
MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id})
|
||||
${whereClause}
|
||||
RETURN resource, notification, user
|
||||
${orderByClause}
|
||||
${offset} ${limit}
|
||||
`
|
||||
try {
|
||||
const result = await session.run(cypher, { id: currentUser.id })
|
||||
notifications = await result.records.map(transformReturnType)
|
||||
return result.records.map(transformReturnType)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return notifications
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
markAsRead: async (parent, args, context, resolveInfo) => {
|
||||
const { user: currentUser } = context
|
||||
const session = context.driver.session()
|
||||
let notification
|
||||
try {
|
||||
const cypher = `
|
||||
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
|
||||
@ -71,11 +69,10 @@ export default {
|
||||
`
|
||||
const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id })
|
||||
const notifications = await result.records.map(transformReturnType)
|
||||
notification = notifications[0]
|
||||
return notifications[0]
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return notification
|
||||
},
|
||||
},
|
||||
NOTIFIED: {
|
||||
|
||||
@ -9,7 +9,6 @@ export default {
|
||||
return createPasswordReset({ driver, nonce, email })
|
||||
},
|
||||
resetPassword: async (_parent, { email, nonce, newPassword }, { driver }) => {
|
||||
const session = driver.session()
|
||||
const stillValid = new Date()
|
||||
stillValid.setDate(stillValid.getDate() - 1)
|
||||
const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10)
|
||||
@ -21,16 +20,20 @@ export default {
|
||||
SET u.encryptedPassword = $encryptedNewPassword
|
||||
RETURN pr
|
||||
`
|
||||
const transactionRes = await session.run(cypher, {
|
||||
stillValid,
|
||||
email,
|
||||
nonce,
|
||||
encryptedNewPassword,
|
||||
})
|
||||
const [reset] = transactionRes.records.map(record => record.get('pr'))
|
||||
const response = !!(reset && reset.properties.usedAt)
|
||||
session.close()
|
||||
return response
|
||||
const session = driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(cypher, {
|
||||
stillValid,
|
||||
email,
|
||||
nonce,
|
||||
encryptedNewPassword,
|
||||
})
|
||||
const [reset] = transactionRes.records.map(record => record.get('pr'))
|
||||
const response = !!(reset && reset.properties.usedAt)
|
||||
return response
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -15,10 +15,13 @@ let variables
|
||||
|
||||
const getAllPasswordResets = async () => {
|
||||
const session = driver.session()
|
||||
const transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r')
|
||||
const resets = transactionRes.records.map(record => record.get('r'))
|
||||
session.close()
|
||||
return resets
|
||||
try {
|
||||
const transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r')
|
||||
const resets = transactionRes.records.map(record => record.get('r'))
|
||||
return resets
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@ -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([
|
||||
@ -54,37 +55,41 @@ export default {
|
||||
return neo4jgraphql(object, params, context, resolveInfo)
|
||||
},
|
||||
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const { postId, data } = params
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
|
||||
RETURN COUNT(DISTINCT emoted) as emotionsCount
|
||||
`,
|
||||
{ postId, data },
|
||||
)
|
||||
session.close()
|
||||
{ postId, data },
|
||||
)
|
||||
|
||||
const [emotionsCount] = transactionRes.records.map(record => {
|
||||
return record.get('emotionsCount').low
|
||||
})
|
||||
|
||||
return emotionsCount
|
||||
const [emotionsCount] = transactionRes.records.map(record => {
|
||||
return record.get('emotionsCount').low
|
||||
})
|
||||
return emotionsCount
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const { postId } = params
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
|
||||
RETURN collect(emoted.emotion) as emotion`,
|
||||
{ userId: context.user.id, postId },
|
||||
)
|
||||
{ userId: context.user.id, postId },
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
const [emotions] = transactionRes.records.map(record => {
|
||||
return record.get('emotion')
|
||||
})
|
||||
return emotions
|
||||
const [emotions] = transactionRes.records.map(record => {
|
||||
return record.get('emotion')
|
||||
})
|
||||
return emotions
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
@ -93,8 +98,6 @@ export default {
|
||||
delete params.categoryIds
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
params.id = params.id || uuid()
|
||||
let post
|
||||
|
||||
const createPostCypher = `CREATE (post:Post {params})
|
||||
SET post.createdAt = toString(datetime())
|
||||
SET post.updatedAt = toString(datetime())
|
||||
@ -113,7 +116,7 @@ export default {
|
||||
try {
|
||||
const transactionRes = await session.run(createPostCypher, createPostVariables)
|
||||
const posts = transactionRes.records.map(record => record.get('post').properties)
|
||||
post = posts[0]
|
||||
return posts[0]
|
||||
} catch (e) {
|
||||
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||
throw new UserInputError('Post with this slug already exists!')
|
||||
@ -121,55 +124,55 @@ export default {
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
return post
|
||||
},
|
||||
UpdatePost: async (_parent, params, context, _resolveInfo) => {
|
||||
const { categoryIds } = params
|
||||
delete params.categoryIds
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
const session = context.driver.session()
|
||||
let updatePostCypher = `MATCH (post:Post {id: $params.id})
|
||||
SET post += $params
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
`
|
||||
|
||||
if (categoryIds && categoryIds.length) {
|
||||
const cypherDeletePreviousRelations = `
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
if (categoryIds && categoryIds.length) {
|
||||
const cypherDeletePreviousRelations = `
|
||||
MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
|
||||
DELETE previousRelations
|
||||
RETURN post, category
|
||||
`
|
||||
|
||||
await session.run(cypherDeletePreviousRelations, { params })
|
||||
await session.run(cypherDeletePreviousRelations, { params })
|
||||
|
||||
updatePostCypher += `
|
||||
updatePostCypher += `
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (post)-[:CATEGORIZED]->(category)
|
||||
WITH post
|
||||
`
|
||||
}
|
||||
|
||||
updatePostCypher += `RETURN post`
|
||||
const updatePostVariables = { categoryIds, params }
|
||||
|
||||
const transactionRes = await session.run(updatePostCypher, updatePostVariables)
|
||||
const [post] = transactionRes.records.map(record => {
|
||||
return record.get('post').properties
|
||||
})
|
||||
return post
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
updatePostCypher += `RETURN post`
|
||||
const updatePostVariables = { categoryIds, params }
|
||||
|
||||
const transactionRes = await session.run(updatePostCypher, updatePostVariables)
|
||||
const [post] = transactionRes.records.map(record => {
|
||||
return record.get('post').properties
|
||||
})
|
||||
|
||||
session.close()
|
||||
|
||||
return post
|
||||
},
|
||||
|
||||
DeletePost: async (object, args, context, resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
// we cannot set slug to 'UNAVAILABE' because of unique constraints
|
||||
const transactionRes = await session.run(
|
||||
`
|
||||
try {
|
||||
// we cannot set slug to 'UNAVAILABE' because of unique constraints
|
||||
const transactionRes = await session.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
|
||||
SET post.deleted = TRUE
|
||||
@ -180,51 +183,60 @@ export default {
|
||||
REMOVE post.image
|
||||
RETURN post
|
||||
`,
|
||||
{ postId: args.id },
|
||||
)
|
||||
session.close()
|
||||
const [post] = transactionRes.records.map(record => record.get('post').properties)
|
||||
return post
|
||||
{ postId: args.id },
|
||||
)
|
||||
const [post] = transactionRes.records.map(record => record.get('post').properties)
|
||||
return post
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
AddPostEmotions: async (object, params, context, resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const { to, data } = params
|
||||
const { user } = context
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id})
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id})
|
||||
MERGE (userFrom)-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo)
|
||||
RETURN userFrom, postTo, emotedRelation`,
|
||||
{ user, to, data },
|
||||
)
|
||||
session.close()
|
||||
const [emoted] = transactionRes.records.map(record => {
|
||||
return {
|
||||
from: { ...record.get('userFrom').properties },
|
||||
to: { ...record.get('postTo').properties },
|
||||
...record.get('emotedRelation').properties,
|
||||
}
|
||||
})
|
||||
return emoted
|
||||
{ user, to, data },
|
||||
)
|
||||
|
||||
const [emoted] = transactionRes.records.map(record => {
|
||||
return {
|
||||
from: { ...record.get('userFrom').properties },
|
||||
to: { ...record.get('postTo').properties },
|
||||
...record.get('emotedRelation').properties,
|
||||
}
|
||||
})
|
||||
return emoted
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
RemovePostEmotions: async (object, params, context, resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const { to, data } = params
|
||||
const { id: from } = context.user
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id})
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id})
|
||||
DELETE emotedRelation
|
||||
RETURN userFrom, postTo`,
|
||||
{ from, to, data },
|
||||
)
|
||||
session.close()
|
||||
const [emoted] = transactionRes.records.map(record => {
|
||||
return {
|
||||
from: { ...record.get('userFrom').properties },
|
||||
to: { ...record.get('postTo').properties },
|
||||
emotion: data.emotion,
|
||||
}
|
||||
})
|
||||
return emoted
|
||||
{ from, to, data },
|
||||
)
|
||||
const [emoted] = transactionRes.records.map(record => {
|
||||
return {
|
||||
from: { ...record.get('userFrom').properties },
|
||||
to: { ...record.get('postTo').properties },
|
||||
emotion: data.emotion,
|
||||
}
|
||||
})
|
||||
return emoted
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
pinPost: async (_parent, params, context, _resolveInfo) => {
|
||||
let pinnedPostWithNestedAttributes
|
||||
@ -242,25 +254,25 @@ export default {
|
||||
)
|
||||
return deletePreviousRelationsResponse.records.map(record => record.get('post').properties)
|
||||
})
|
||||
await writeTxResultPromise
|
||||
try {
|
||||
await writeTxResultPromise
|
||||
|
||||
writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const pinPostTransactionResponse = await transaction.run(
|
||||
`
|
||||
writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const pinPostTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $userId}) WHERE user.role = 'admin'
|
||||
MATCH (post:Post {id: $params.id})
|
||||
MERGE (user)-[pinned:PINNED {createdAt: toString(datetime())}]->(post)
|
||||
SET post.pinned = true
|
||||
RETURN post, pinned.createdAt as pinnedAt
|
||||
`,
|
||||
{ userId, params },
|
||||
)
|
||||
return pinPostTransactionResponse.records.map(record => ({
|
||||
pinnedPost: record.get('post').properties,
|
||||
pinnedAt: record.get('pinnedAt'),
|
||||
}))
|
||||
})
|
||||
try {
|
||||
{ userId, params },
|
||||
)
|
||||
return pinPostTransactionResponse.records.map(record => ({
|
||||
pinnedPost: record.get('post').properties,
|
||||
pinnedAt: record.get('pinnedAt'),
|
||||
}))
|
||||
})
|
||||
const [transactionResult] = await writeTxResultPromise
|
||||
const { pinnedPost, pinnedAt } = transactionResult
|
||||
pinnedPostWithNestedAttributes = {
|
||||
@ -305,6 +317,7 @@ export default {
|
||||
'pinnedAt',
|
||||
'pinned',
|
||||
'blurImage',
|
||||
'imageAspectRatio',
|
||||
],
|
||||
hasMany: {
|
||||
tags: '-[:TAGGED]->(related:Tag)',
|
||||
@ -315,7 +328,6 @@ export default {
|
||||
},
|
||||
hasOne: {
|
||||
author: '<-[:WROTE]-(related:User)',
|
||||
disabledBy: '<-[:DISABLED]-(related:User)',
|
||||
pinnedBy: '<-[:PINNED]-(related:User)',
|
||||
},
|
||||
count: {
|
||||
|
||||
@ -316,53 +316,6 @@ describe('CreatePost', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('categories', () => {
|
||||
describe('null', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, categoryIds: null }
|
||||
})
|
||||
it('throws UserInputError', async () => {
|
||||
const {
|
||||
errors: [error],
|
||||
} = await mutate({ mutation: createPostMutation, variables })
|
||||
expect(error).toHaveProperty(
|
||||
'message',
|
||||
'You cannot save a post without at least one category or more than three',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('empty', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, categoryIds: [] }
|
||||
})
|
||||
it('throws UserInputError', async () => {
|
||||
const {
|
||||
errors: [error],
|
||||
} = await mutate({ mutation: createPostMutation, variables })
|
||||
expect(error).toHaveProperty(
|
||||
'message',
|
||||
'You cannot save a post without at least one category or more than three',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('more than 3 items', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'] }
|
||||
})
|
||||
it('throws UserInputError', async () => {
|
||||
const {
|
||||
errors: [error],
|
||||
} = await mutate({ mutation: createPostMutation, variables })
|
||||
expect(error).toHaveProperty(
|
||||
'message',
|
||||
'You cannot save a post without at least one category or more than three',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -493,74 +446,6 @@ describe('UpdatePost', () => {
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
describe('more than 3 categories', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'] }
|
||||
})
|
||||
|
||||
it('allows a maximum of three category for a successful update', async () => {
|
||||
const {
|
||||
errors: [error],
|
||||
} = await mutate({ mutation: updatePostMutation, variables })
|
||||
expect(error).toHaveProperty(
|
||||
'message',
|
||||
'You cannot save a post without at least one category or more than three',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('post created without categories somehow', () => {
|
||||
let owner
|
||||
|
||||
beforeEach(async () => {
|
||||
const postSomehowCreated = await neode.create('Post', {
|
||||
id: 'how-was-this-created',
|
||||
})
|
||||
owner = await neode.create('User', {
|
||||
id: 'author-of-post-without-category',
|
||||
name: 'Hacker',
|
||||
slug: 'hacker',
|
||||
email: 'hacker@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
await postSomehowCreated.relateTo(owner, 'author')
|
||||
authenticatedUser = await owner.toJson()
|
||||
variables = { ...variables, id: 'how-was-this-created' }
|
||||
})
|
||||
|
||||
it('throws an error if categoryIds is not an array', async () => {
|
||||
const {
|
||||
errors: [error],
|
||||
} = await mutate({
|
||||
mutation: updatePostMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
categoryIds: null,
|
||||
},
|
||||
})
|
||||
expect(error).toHaveProperty(
|
||||
'message',
|
||||
'You cannot save a post without at least one category or more than three',
|
||||
)
|
||||
})
|
||||
|
||||
it('requires at least one category for successful update', async () => {
|
||||
const {
|
||||
errors: [error],
|
||||
} = await mutate({
|
||||
mutation: updatePostMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
categoryIds: [],
|
||||
},
|
||||
})
|
||||
expect(error).toHaveProperty(
|
||||
'message',
|
||||
'You cannot save a post without at least one category or more than three',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,18 +1,32 @@
|
||||
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()
|
||||
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,
|
||||
@ -22,36 +36,12 @@ export default {
|
||||
reasonDescription,
|
||||
},
|
||||
)
|
||||
return reportRelationshipTransactionResponse.records.map(record => ({
|
||||
report: record.get('report'),
|
||||
submitter: record.get('submitter'),
|
||||
resource: record.get('resource').properties,
|
||||
type: record.get('type'),
|
||||
}))
|
||||
return reportTransactionResponse.records.map(transformReturnType)
|
||||
})
|
||||
try {
|
||||
const txResult = await writeTxResultPromise
|
||||
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()
|
||||
}
|
||||
@ -62,8 +52,7 @@ export default {
|
||||
reports: async (_parent, params, context, _resolveInfo) => {
|
||||
const { driver } = context
|
||||
const session = driver.session()
|
||||
let response
|
||||
let orderByClause
|
||||
let reports, orderByClause
|
||||
switch (params.orderBy) {
|
||||
case 'createdAt_asc':
|
||||
orderByClause = 'ORDER BY report.createdAt ASC'
|
||||
@ -74,55 +63,97 @@ export default {
|
||||
default:
|
||||
orderByClause = ''
|
||||
}
|
||||
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
|
||||
const reportReadTxPromise = session.readTransaction(async tx => {
|
||||
const allReportsTransactionResponse = await tx.run(
|
||||
`
|
||||
MATCH (submitter:User)-[filed:FILED]->(report:Report)-[:BELONGS_TO]->(resource)
|
||||
WHERE resource:User OR resource:Post OR resource:Comment
|
||||
RETURN DISTINCT report, 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'),
|
||||
`,
|
||||
{},
|
||||
)
|
||||
return allReportsTransactionResponse.records.map(transformReturnType)
|
||||
})
|
||||
try {
|
||||
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
|
||||
if (!txResult[0]) return null
|
||||
reviewed = txResult.map(reportedRecord => {
|
||||
const { review, moderator } = reportedRecord
|
||||
const relationshipWithNestedAttributes = {
|
||||
...review,
|
||||
moderator,
|
||||
}
|
||||
return relationshipWithNestedAttributes
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return reviewed
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -8,31 +8,41 @@ 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',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}
|
||||
|
||||
@ -4,48 +4,51 @@ export default {
|
||||
const { id, type } = params
|
||||
|
||||
const session = context.driver.session()
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId})
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId})
|
||||
WHERE $type IN labels(node) AND NOT userWritten.id = $userId
|
||||
MERGE (user)-[relation:SHOUTED{createdAt:toString(datetime())}]->(node)
|
||||
RETURN COUNT(relation) > 0 as isShouted`,
|
||||
{
|
||||
id,
|
||||
type,
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
{
|
||||
id,
|
||||
type,
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
|
||||
const [isShouted] = transactionRes.records.map(record => {
|
||||
return record.get('isShouted')
|
||||
})
|
||||
const [isShouted] = transactionRes.records.map(record => {
|
||||
return record.get('isShouted')
|
||||
})
|
||||
|
||||
session.close()
|
||||
|
||||
return isShouted
|
||||
return isShouted
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
|
||||
unshout: async (_object, params, context, _resolveInfo) => {
|
||||
const { id, type } = params
|
||||
const session = context.driver.session()
|
||||
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id})
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id})
|
||||
WHERE $type IN labels(node)
|
||||
DELETE relation
|
||||
RETURN COUNT(relation) > 0 as isShouted`,
|
||||
{
|
||||
id,
|
||||
type,
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
const [isShouted] = transactionRes.records.map(record => {
|
||||
return record.get('isShouted')
|
||||
})
|
||||
session.close()
|
||||
|
||||
return isShouted
|
||||
{
|
||||
id,
|
||||
type,
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
const [isShouted] = transactionRes.records.map(record => {
|
||||
return record.get('isShouted')
|
||||
})
|
||||
return isShouted
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export default {
|
||||
Query: {
|
||||
statistics: async (parent, args, { driver, user }) => {
|
||||
statistics: async (_parent, _args, { driver }) => {
|
||||
const session = driver.session()
|
||||
const response = {}
|
||||
try {
|
||||
@ -33,10 +33,10 @@ export default {
|
||||
* Note: invites count is calculated this way because invitation codes are not in use yet
|
||||
*/
|
||||
response.countInvites = response.countEmails - response.countUsers
|
||||
return response
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return response
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
140
backend/src/schema/resolvers/statistics.spec.js
Normal file
140
backend/src/schema/resolvers/statistics.spec.js
Normal file
@ -0,0 +1,140 @@
|
||||
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 createServer from '../../server'
|
||||
|
||||
let query, authenticatedUser
|
||||
const factory = Factory()
|
||||
const instance = getNeode()
|
||||
const driver = getDriver()
|
||||
|
||||
const statisticsQuery = gql`
|
||||
query {
|
||||
statistics {
|
||||
countUsers
|
||||
countPosts
|
||||
countComments
|
||||
countNotifications
|
||||
countInvites
|
||||
countFollows
|
||||
countShouts
|
||||
}
|
||||
}
|
||||
`
|
||||
beforeAll(() => {
|
||||
authenticatedUser = undefined
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode: instance,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('statistics', () => {
|
||||
describe('countUsers', () => {
|
||||
beforeEach(async () => {
|
||||
await Promise.all(
|
||||
[...Array(6).keys()].map(() => {
|
||||
return factory.create('User')
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns the count of all users', async () => {
|
||||
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||
data: { statistics: { countUsers: 6 } },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('countPosts', () => {
|
||||
beforeEach(async () => {
|
||||
await Promise.all(
|
||||
[...Array(3).keys()].map(() => {
|
||||
return factory.create('Post')
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns the count of all posts', async () => {
|
||||
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||
data: { statistics: { countPosts: 3 } },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('countComments', () => {
|
||||
beforeEach(async () => {
|
||||
await Promise.all(
|
||||
[...Array(2).keys()].map(() => {
|
||||
return factory.create('Comment')
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns the count of all comments', async () => {
|
||||
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||
data: { statistics: { countComments: 2 } },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('countFollows', () => {
|
||||
let users
|
||||
beforeEach(async () => {
|
||||
users = await Promise.all(
|
||||
[...Array(2).keys()].map(() => {
|
||||
return factory.create('User')
|
||||
}),
|
||||
)
|
||||
await users[0].relateTo(users[1], 'following')
|
||||
})
|
||||
|
||||
it('returns the count of all follows', async () => {
|
||||
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||
data: { statistics: { countFollows: 1 } },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('countShouts', () => {
|
||||
let users, posts
|
||||
beforeEach(async () => {
|
||||
users = await Promise.all(
|
||||
[...Array(2).keys()].map(() => {
|
||||
return factory.create('User')
|
||||
}),
|
||||
)
|
||||
posts = await Promise.all(
|
||||
[...Array(3).keys()].map(() => {
|
||||
return factory.create('Post')
|
||||
}),
|
||||
)
|
||||
await Promise.all([
|
||||
users[0].relateTo(posts[1], 'shouted'),
|
||||
users[1].relateTo(posts[0], 'shouted'),
|
||||
])
|
||||
})
|
||||
|
||||
it('returns the count of all shouts', async () => {
|
||||
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
|
||||
data: { statistics: { countShouts: 2 } },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -24,29 +24,32 @@ export default {
|
||||
// }
|
||||
email = normalizeEmail(email)
|
||||
const session = driver.session()
|
||||
const result = await session.run(
|
||||
`
|
||||
try {
|
||||
const result = await session.run(
|
||||
`
|
||||
MATCH (user:User {deleted: false})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})
|
||||
RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1
|
||||
`,
|
||||
{ userEmail: email },
|
||||
)
|
||||
session.close()
|
||||
const [currentUser] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
{ userEmail: email },
|
||||
)
|
||||
const [currentUser] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
|
||||
if (
|
||||
currentUser &&
|
||||
(await bcrypt.compareSync(password, currentUser.encryptedPassword)) &&
|
||||
!currentUser.disabled
|
||||
) {
|
||||
delete currentUser.encryptedPassword
|
||||
return encode(currentUser)
|
||||
} else if (currentUser && currentUser.disabled) {
|
||||
throw new AuthenticationError('Your account has been disabled.')
|
||||
} else {
|
||||
throw new AuthenticationError('Incorrect email address or password.')
|
||||
if (
|
||||
currentUser &&
|
||||
(await bcrypt.compareSync(password, currentUser.encryptedPassword)) &&
|
||||
!currentUser.disabled
|
||||
) {
|
||||
delete currentUser.encryptedPassword
|
||||
return encode(currentUser)
|
||||
} else if (currentUser && currentUser.disabled) {
|
||||
throw new AuthenticationError('Your account has been disabled.')
|
||||
} else {
|
||||
throw new AuthenticationError('Incorrect email address or password.')
|
||||
}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
|
||||
|
||||
@ -9,25 +9,25 @@ import { neode as 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(() => {
|
||||
|
||||
@ -212,7 +212,6 @@ export default {
|
||||
},
|
||||
hasOne: {
|
||||
invitedBy: '<-[:INVITED]-(related:User)',
|
||||
disabledBy: '<-[:DISABLED]-(related:User)',
|
||||
location: '-[:IS_IN]->(related:Location)',
|
||||
},
|
||||
hasMany: {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -47,7 +47,6 @@ type Comment {
|
||||
updatedAt: String
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
||||
}
|
||||
|
||||
type Query {
|
||||
|
||||
23
backend/src/schema/types/type/FILED.gql
Normal file
23
backend/src/schema/types/type/FILED.gql
Normal file
@ -0,0 +1,23 @@
|
||||
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
|
||||
}
|
||||
|
||||
enum ReportOrdering {
|
||||
createdAt_asc
|
||||
createdAt_desc
|
||||
}
|
||||
@ -26,7 +26,7 @@ enum NotificationReason {
|
||||
type Query {
|
||||
notifications(read: Boolean, orderBy: NotificationOrdering, first: Int, offset: Int): [NOTIFIED]
|
||||
}
|
||||
|
||||
|
||||
type Mutation {
|
||||
markAsRead(id: ID!): NOTIFIED
|
||||
}
|
||||
|
||||
@ -115,16 +115,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
|
||||
@ -185,6 +185,7 @@ type Mutation {
|
||||
categoryIds: [ID]
|
||||
contentExcerpt: String
|
||||
blurImage: Boolean
|
||||
imageAspectRatio: Float
|
||||
): Post
|
||||
UpdatePost(
|
||||
id: ID!
|
||||
@ -198,6 +199,7 @@ type Mutation {
|
||||
language: String
|
||||
categoryIds: [ID]
|
||||
blurImage: Boolean
|
||||
imageAspectRatio: Float
|
||||
): Post
|
||||
DeletePost(id: ID!): Post
|
||||
AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
|
||||
@ -223,6 +225,7 @@ type Query {
|
||||
offset: Int
|
||||
orderBy: [_PostOrdering]
|
||||
filter: _PostFilter
|
||||
imageAspectRatio: Float
|
||||
): [Post]
|
||||
PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int!
|
||||
PostsEmotionsByCurrentUser(postId: ID!): [String]
|
||||
|
||||
@ -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
|
||||
}
|
||||
15
backend/src/schema/types/type/REVIEWED.gql
Normal file
15
backend/src/schema/types/type/REVIEWED.gql
Normal 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
|
||||
}
|
||||
25
backend/src/schema/types/type/Report.gql
Normal file
25
backend/src/schema/types/type/Report.gql
Normal file
@ -0,0 +1,25 @@
|
||||
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): [Report]
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,12 +24,13 @@ const factories = {
|
||||
EmailAddress: createEmailAddress,
|
||||
UnverifiedEmailAddress: createUnverifiedEmailAddresss,
|
||||
Donations: createDonations,
|
||||
Report: createReport,
|
||||
}
|
||||
|
||||
export const cleanDatabase = async (options = {}) => {
|
||||
const { driver = getDriver() } = options
|
||||
const session = driver.session()
|
||||
const cypher = 'MATCH (n) DETACH DELETE n'
|
||||
const session = driver.session()
|
||||
try {
|
||||
return await session.run(cypher)
|
||||
} finally {
|
||||
|
||||
@ -19,6 +19,7 @@ export default function create() {
|
||||
visibility: 'public',
|
||||
deleted: false,
|
||||
categoryIds: [],
|
||||
imageAspectRatio: 1.333,
|
||||
}
|
||||
args = {
|
||||
...defaults,
|
||||
|
||||
7
backend/src/seed/factories/reports.js
Normal file
7
backend/src/seed/factories/reports.js
Normal file
@ -0,0 +1,7 @@
|
||||
export default function create() {
|
||||
return {
|
||||
factory: async ({ args, neodeInstance }) => {
|
||||
return neodeInstance.create('Report', args)
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -350,17 +350,19 @@ 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'],
|
||||
blurImage: true,
|
||||
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'],
|
||||
blurImage: false,
|
||||
imageAspectRatio: 300 / 1500,
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: huey,
|
||||
@ -387,9 +389,10 @@ 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'],
|
||||
blurImage: false,
|
||||
imageAspectRatio: 300 / 857,
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: huey,
|
||||
@ -408,9 +411,10 @@ 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'],
|
||||
blurImage: false,
|
||||
imageAspectRatio: 300 / 901,
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: bobDerBaumeister,
|
||||
@ -423,9 +427,10 @@ 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'],
|
||||
blurImage: false,
|
||||
imageAspectRatio: 300 / 450,
|
||||
}),
|
||||
factory.create('Post', {
|
||||
author: huey,
|
||||
@ -452,6 +457,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
$content: String!
|
||||
$categoryIds: [ID]
|
||||
$blurImage: Boolean
|
||||
$imageAspectRatio: Float
|
||||
) {
|
||||
CreatePost(
|
||||
id: $id
|
||||
@ -459,6 +465,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
content: $content
|
||||
categoryIds: $categoryIds
|
||||
blurImage: $blurImage
|
||||
imageAspectRatio: $imageAspectRatio
|
||||
) {
|
||||
id
|
||||
}
|
||||
@ -474,6 +481,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
content: hashtag1,
|
||||
categoryIds: ['cat2'],
|
||||
blurImage: false,
|
||||
imageAspectRatio: 300 / 200,
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
@ -484,6 +492,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
content: `${mention1} ${faker.lorem.paragraph()}`,
|
||||
categoryIds: ['cat7'],
|
||||
blurImage: false,
|
||||
imageAspectRatio: 300 / 180,
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
@ -495,6 +504,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
content: hashtagAndMention1,
|
||||
categoryIds: ['cat8'],
|
||||
blurImage: false,
|
||||
imageAspectRatio: 300 / 900,
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
@ -505,6 +515,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
content: `${mention2} ${faker.lorem.paragraph()}`,
|
||||
categoryIds: ['cat12'],
|
||||
blurImage: false,
|
||||
imageAspectRatio: 300 / 200,
|
||||
},
|
||||
}),
|
||||
])
|
||||
@ -552,7 +563,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',
|
||||
@ -569,7 +580,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
postId: 'p3',
|
||||
}),
|
||||
factory.create('Comment', {
|
||||
author: bobDerBaumeister,
|
||||
author: jennyRostock,
|
||||
id: 'c5',
|
||||
postId: 'p3',
|
||||
}),
|
||||
@ -609,6 +620,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
postId: 'p15',
|
||||
}),
|
||||
])
|
||||
const trollingComment = comments[0]
|
||||
|
||||
await Promise.all([
|
||||
democracy.relateTo(p3, 'post'),
|
||||
@ -672,68 +684,107 @@ 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'),
|
||||
])
|
||||
authenticatedUser = null
|
||||
const reportAgainstDagobert = reports[0]
|
||||
const reportAgainstTrollingPost = reports[1]
|
||||
const reportAgainstTrollingComment = reports[2]
|
||||
|
||||
// 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'),
|
||||
])
|
||||
|
||||
// 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 => {
|
||||
|
||||
@ -6,6 +6,7 @@ import middleware from './middleware'
|
||||
import { neode as getNeode, getDriver } from './bootstrap/neo4j'
|
||||
import decode from './jwt/decode'
|
||||
import schema from './schema'
|
||||
import webfinger from './activitypub/routes/webfinger'
|
||||
|
||||
// check required configs and throw error
|
||||
// TODO check this directly in config file - currently not possible due to testsetup
|
||||
@ -41,7 +42,10 @@ const createServer = options => {
|
||||
const server = new ApolloServer(Object.assign({}, defaults, options))
|
||||
|
||||
const app = express()
|
||||
|
||||
app.set('driver', driver)
|
||||
app.use(helmet())
|
||||
app.use('/.well-known/', webfinger())
|
||||
app.use(express.static('public'))
|
||||
server.applyMiddleware({ app, path: '/' })
|
||||
|
||||
|
||||
@ -9,32 +9,6 @@ Feature: Webfinger discovery
|
||||
| Slug |
|
||||
| peter-lustiger |
|
||||
|
||||
Scenario: Search
|
||||
When I send a GET request to "/.well-known/webfinger?resource=acct:peter-lustiger@localhost"
|
||||
Then I receive the following json:
|
||||
"""
|
||||
{
|
||||
"subject": "acct:peter-lustiger@localhost:4123",
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": "http://localhost:4123/activitypub/users/peter-lustiger"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
And I expect the Content-Type to be "application/jrd+json; charset=utf-8"
|
||||
|
||||
Scenario: User does not exist
|
||||
When I send a GET request to "/.well-known/webfinger?resource=acct:nonexisting@localhost"
|
||||
Then I receive the following json:
|
||||
"""
|
||||
{
|
||||
"error": "No record found for nonexisting@localhost."
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Receiving an actor object
|
||||
When I send a GET request to "/activitypub/users/peter-lustiger"
|
||||
Then I receive the following json:
|
||||
|
||||
@ -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.0":
|
||||
version "5.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.10.0.tgz#ba0c34298f599c8821d03b7fa0e95435b6340801"
|
||||
integrity sha512-GyMWR38DaTOZ0Zdu677kt3/HDbZI4SyNNGvt/8/kzqRhmPUhEuLfuh1CJVA8ysUMD+ucllJifCGP2TflMA7LYQ==
|
||||
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.0"
|
||||
"@sentry/minimal" "5.10.0"
|
||||
"@sentry/types" "5.10.0"
|
||||
"@sentry/utils" "5.10.0"
|
||||
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.0":
|
||||
version "5.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.10.0.tgz#9f65ce9077e980a370bd5410f6464f01962a8f67"
|
||||
integrity sha512-sPtgZIRFDKgIvmASi5/kLn+bTRuqhj/NkBlY2SkVgCKfo4Plu1uLJt4zEFF7UC3+MP+2PQA4F6gnAwWIqisbXQ==
|
||||
dependencies:
|
||||
"@sentry/types" "5.7.1"
|
||||
"@sentry/utils" "5.8.0"
|
||||
"@sentry/hub" "5.10.0"
|
||||
"@sentry/minimal" "5.10.0"
|
||||
"@sentry/types" "5.10.0"
|
||||
"@sentry/utils" "5.10.0"
|
||||
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.0":
|
||||
version "5.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.10.0.tgz#7f64f7d86a754e5aaba4d4ac0f8b39a54e24deaa"
|
||||
integrity sha512-GJjsmu6oI02uL+HnO504XvExhsD6TW7qwOKuIdy27Apq9d/+ZGsjnMigI9bR9UT3JqVQr3OzreDC4LBCGehTqw==
|
||||
dependencies:
|
||||
"@sentry/hub" "5.8.0"
|
||||
"@sentry/types" "5.7.1"
|
||||
"@sentry/types" "5.10.0"
|
||||
"@sentry/utils" "5.10.0"
|
||||
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.0":
|
||||
version "5.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.10.0.tgz#8bf22cfd362da2679afe29495d3bdb7ed712d22b"
|
||||
integrity sha512-ZZd+IJewSZDuxKKQgzLdSKGNDsDIL6IW/9jGHY+uX1D9t7NnZIBmfpaIUsMPe1rJxag+fEk0FJH+g/z4uIZI2w==
|
||||
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.0"
|
||||
"@sentry/types" "5.10.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/node@^5.10.0":
|
||||
version "5.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.10.0.tgz#950f763e68361fbca822e9474de78ee1e00fd5c8"
|
||||
integrity sha512-G8fiwYRq/KB3/fNsGQ4A8OByH0LNbyUvoJGUhsfkkQS7GqC/vtn6CrR+GuKIwFjxTF4MN5amIPntSdVZjehxug==
|
||||
dependencies:
|
||||
"@sentry/apm" "5.10.0"
|
||||
"@sentry/core" "5.10.0"
|
||||
"@sentry/hub" "5.10.0"
|
||||
"@sentry/types" "5.10.0"
|
||||
"@sentry/utils" "5.10.0"
|
||||
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.0":
|
||||
version "5.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.10.0.tgz#98ee0db868438c4572b0bad03231ab2e888c134d"
|
||||
integrity sha512-wcxwqtAomr1O65aXx41oHsgl/AGJTJ9C4c03FAMg9wHWEfzEby0el6BZCMq3IAG09zY7vY43zhEFWFghI5u2eg==
|
||||
dependencies:
|
||||
"@sentry/types" "5.7.1"
|
||||
"@sentry/types" "5.10.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sindresorhus/is@^0.14.0":
|
||||
@ -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.0:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.0.tgz#91482ebc09d39156d933eb9e6159642cd27bf02c"
|
||||
integrity sha512-UBqPOfQGD1cM3HnjhuQe+0u3DWx47WWK7lBjG5UtPnGOysr7oDK5lNCzcjK6zzeBSdTk4m1tGx1xNbWFZQmMNA==
|
||||
|
||||
nodemon@~2.0.1:
|
||||
version "2.0.1"
|
||||
|
||||
@ -129,8 +129,8 @@ Given('somebody reported the following posts:', table => {
|
||||
.create('User', submitter)
|
||||
.authenticateAs(submitter)
|
||||
.mutate(`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||
report(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
|
||||
type
|
||||
fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
|
||||
id
|
||||
}
|
||||
}`, {
|
||||
resourceId,
|
||||
|
||||
@ -9,7 +9,7 @@ open your minikube dashboard:
|
||||
$ minikube dashboard
|
||||
```
|
||||
|
||||
This will give you an overview. Some of the steps below need some timing to make ressources available to other dependent deployments. Keeping an eye on the dashboard is a great way to check that.
|
||||
This will give you an overview. Some of the steps below need some timing to make resources available to other dependent deployments. Keeping an eye on the dashboard is a great way to check that.
|
||||
|
||||
Follow the installation instruction for [Human Connection](../human-connection/README.md).
|
||||
If all the pods and services have settled and everything looks green in your
|
||||
|
||||
48
features/support/steps.js
Normal file
48
features/support/steps.js
Normal file
@ -0,0 +1,48 @@
|
||||
// features/support/steps.js
|
||||
import { Given, When, Then, After, AfterAll } from 'cucumber'
|
||||
import Factory from '../../backend/src/seed/factories'
|
||||
import dotenv from 'dotenv'
|
||||
import expect from 'expect'
|
||||
|
||||
const debug = require('debug')('ea:test:steps')
|
||||
const factory = Factory()
|
||||
|
||||
|
||||
After(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
Given('our CLIENT_URI is {string}', function (string) {
|
||||
expect(string).toEqual('http://localhost:3000')
|
||||
// This is just for documentation. When you see URLs in the response of
|
||||
// scenarios you, should be able to tell that it's coming from this
|
||||
// environment variable.
|
||||
});
|
||||
|
||||
Given('we have the following users in our database:', function (dataTable) {
|
||||
return Promise.all(dataTable.hashes().map(({ slug, name }) => {
|
||||
return factory.create('User', {
|
||||
name,
|
||||
slug,
|
||||
})
|
||||
}))
|
||||
})
|
||||
|
||||
When('I send a GET request to {string}', async function (pathname) {
|
||||
const response = await this.get(pathname)
|
||||
this.lastContentType = response.lastContentType
|
||||
|
||||
this.lastResponses.push(response.lastResponse)
|
||||
this.statusCode = response.statusCode
|
||||
})
|
||||
|
||||
Then('the server responds with a HTTP Status {int} and the following json:', function (statusCode, docString) {
|
||||
expect(this.statusCode).toEqual(statusCode)
|
||||
const [ lastResponse ] = this.lastResponses
|
||||
expect(JSON.parse(lastResponse)).toMatchObject(JSON.parse(docString))
|
||||
})
|
||||
|
||||
Then('the Content-Type is {string}', function (contentType) {
|
||||
expect(this.lastContentType).toEqual(contentType)
|
||||
})
|
||||
|
||||
36
features/webfinger.feature
Normal file
36
features/webfinger.feature
Normal file
@ -0,0 +1,36 @@
|
||||
Feature: Webfinger discovery
|
||||
From an external server, e.g. Mastodon
|
||||
I want to search for an actor alias
|
||||
In order to follow the actor
|
||||
|
||||
Background:
|
||||
Given our CLIENT_URI is "http://localhost:3000"
|
||||
And we have the following users in our database:
|
||||
| name | slug |
|
||||
| Peter Lustiger | peter-lustiger |
|
||||
|
||||
Scenario: Search a user
|
||||
When I send a GET request to "/.well-known/webfinger?resource=acct:peter-lustiger@localhost"
|
||||
Then the server responds with a HTTP Status 200 and the following json:
|
||||
"""
|
||||
{
|
||||
"subject": "acct:peter-lustiger@localhost:3000",
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": "http://localhost:3000/activitypub/users/peter-lustiger"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
And the Content-Type is "application/jrd+json; charset=utf-8"
|
||||
|
||||
Scenario: Search without result
|
||||
When I send a GET request to "/.well-known/webfinger?resource=acct:nonexisting@localhost"
|
||||
Then the server responds with a HTTP Status 404 and the following json:
|
||||
"""
|
||||
{
|
||||
"error": "No record found for \"nonexisting@localhost\"."
|
||||
}
|
||||
"""
|
||||
38
features/world.js
Normal file
38
features/world.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { setWorldConstructor } from 'cucumber'
|
||||
import request from 'request'
|
||||
|
||||
class CustomWorld {
|
||||
constructor () {
|
||||
// webFinger.feature
|
||||
this.lastResponses = []
|
||||
this.lastContentType = null
|
||||
this.lastInboxUrl = null
|
||||
this.lastActivity = null
|
||||
// object-article.feature
|
||||
this.statusCode = null
|
||||
}
|
||||
get (pathname) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(`http://localhost:4000/${this.replaceSlashes(pathname)}`, {
|
||||
headers: {
|
||||
'Accept': 'application/activity+json'
|
||||
}}, (error, response, body) => {
|
||||
if (!error) {
|
||||
resolve({
|
||||
lastResponse: body,
|
||||
lastContentType: response.headers['content-type'],
|
||||
statusCode: response.statusCode
|
||||
})
|
||||
} else {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
replaceSlashes (pathname) {
|
||||
return pathname.replace(/^\/+/, '')
|
||||
}
|
||||
}
|
||||
|
||||
setWorldConstructor(CustomWorld)
|
||||
55
neo4j/change_disabled_relationship_to_report_node.sh
Executable file
55
neo4j/change_disabled_relationship_to_report_node.sh
Executable 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
|
||||
|
||||
@ -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
|
||||
30
neo4j/db_manipulation/add_image_aspect_ratio.sh
Executable file
30
neo4j/db_manipulation/add_image_aspect_ratio.sh
Executable 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
|
||||
51
neo4j/db_manipulation/change_disabled_relationship_to_report_node.sh
Executable file
51
neo4j/db_manipulation/change_disabled_relationship_to_report_node.sh
Executable 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
|
||||
11
package.json
11
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "nitro-cypress",
|
||||
"name": "human-connection",
|
||||
"version": "0.1.11",
|
||||
"description": "Fullstack tests with cypress for Human Connection",
|
||||
"description": "Fullstack and API tests with cypress and cucumber for Human Connection",
|
||||
"author": "Human Connection gGmbh",
|
||||
"license": "MIT",
|
||||
"cypress-cucumber-preprocessor": {
|
||||
@ -16,19 +16,26 @@
|
||||
"cypress:setup": "run-p cypress:backend cypress:webapp",
|
||||
"cypress:run": "cross-env cypress run --browser chromium",
|
||||
"cypress:open": "cross-env cypress open --browser chromium",
|
||||
"cucumber:setup": "cd backend && yarn run dev",
|
||||
"cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit",
|
||||
"version": "auto-changelog -p"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/preset-env": "^7.7.4",
|
||||
"@babel/register": "^7.7.4",
|
||||
"auto-changelog": "^1.16.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"codecov": "^3.6.1",
|
||||
"cross-env": "^6.0.3",
|
||||
"cucumber": "^6.0.5",
|
||||
"cypress": "^3.7.0",
|
||||
"cypress-cucumber-preprocessor": "^1.17.0",
|
||||
"cypress-file-upload": "^3.5.0",
|
||||
"cypress-plugin-retries": "^1.5.0",
|
||||
"date-fns": "^2.8.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"expect": "^24.9.0",
|
||||
"faker": "Marak/faker.js#master",
|
||||
"graphql-request": "^1.8.2",
|
||||
"neo4j-driver": "^1.7.6",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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] : {},
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
@ -198,6 +198,7 @@ describe('ContributionForm.vue', () => {
|
||||
id: null,
|
||||
categoryIds: ['cat12'],
|
||||
imageUpload: null,
|
||||
imageAspectRatio: null,
|
||||
image: null,
|
||||
blurImage: false,
|
||||
},
|
||||
@ -364,6 +365,7 @@ describe('ContributionForm.vue', () => {
|
||||
categoryIds: ['cat12'],
|
||||
image,
|
||||
imageUpload: null,
|
||||
imageAspectRatio: null,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
:contribution="contribution"
|
||||
@addTeaserImage="addTeaserImage"
|
||||
:class="{ 'images-set-blur': checkedBlur }"
|
||||
@addImageAspectRatio="addImageAspectRatio"
|
||||
>
|
||||
<img
|
||||
v-if="contribution"
|
||||
@ -172,6 +173,7 @@ export default {
|
||||
title: '',
|
||||
content: '',
|
||||
teaserImage: null,
|
||||
imageAspectRatio: null,
|
||||
image: null,
|
||||
language: null,
|
||||
categoryIds: [],
|
||||
@ -251,6 +253,7 @@ export default {
|
||||
content,
|
||||
image,
|
||||
teaserImage,
|
||||
imageAspectRatio,
|
||||
categoryIds,
|
||||
} = this.form
|
||||
this.loading = true
|
||||
@ -266,6 +269,7 @@ export default {
|
||||
image,
|
||||
imageUpload: teaserImage,
|
||||
blurImage: this.form.checkbox,
|
||||
imageAspectRatio,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
@ -289,6 +293,9 @@ export default {
|
||||
addTeaserImage(file) {
|
||||
this.form.teaserImage = file
|
||||
},
|
||||
addImageAspectRatio(aspectRatio) {
|
||||
this.form.imageAspectRatio = aspectRatio
|
||||
},
|
||||
categoryIds(categories) {
|
||||
return categories.map(c => c.id)
|
||||
},
|
||||
|
||||
@ -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'),
|
||||
)
|
||||
})
|
||||
|
||||
@ -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"
|
||||
/>`,
|
||||
|
||||
@ -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()
|
||||
},
|
||||
},
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -77,7 +77,7 @@ export default {
|
||||
}, 500)
|
||||
}, 1500)
|
||||
} catch (err) {
|
||||
this.success = false
|
||||
this.isOpen = false
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@ -149,6 +149,7 @@ export default {
|
||||
default:
|
||||
this.$toast.error(err.message)
|
||||
}
|
||||
this.isOpen = false
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
@ -18,7 +18,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>
|
||||
@ -146,6 +146,15 @@ 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>
|
||||
|
||||
@ -165,6 +174,7 @@ export default {
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.ds-card-image {
|
||||
width: 100%;
|
||||
max-height: 2000px;
|
||||
@ -179,9 +189,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;
|
||||
|
||||
@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@ -113,10 +113,12 @@ export default {
|
||||
this.showCropper = false
|
||||
const canvas = this.cropper.getCroppedCanvas()
|
||||
canvas.toBlob(blob => {
|
||||
const imageAspectRatio = canvas.width / canvas.height
|
||||
this.setupPreview(canvas)
|
||||
this.removeCropper()
|
||||
const croppedImageFile = new File([blob], this.file.name, { type: 'image/jpeg' })
|
||||
const croppedImageFile = new File([blob], this.file.name, { type: this.file.type })
|
||||
this.$emit('addTeaserImage', croppedImageFile)
|
||||
this.$emit('addImageAspectRatio', imageAspectRatio)
|
||||
}, 'image/jpeg')
|
||||
},
|
||||
setupPreview(canvas) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="user" v-if="displayAnonymous">
|
||||
<hc-avatar class="avatar" />
|
||||
<hc-avatar v-if="showAvatar" class="avatar" />
|
||||
<div>
|
||||
<b class="username">{{ $t('profile.userAnonym') }}</b>
|
||||
</div>
|
||||
@ -9,21 +9,19 @@
|
||||
<template slot="default" slot-scope="{ openMenu, closeMenu, isOpen }">
|
||||
<nuxt-link :to="userLink" :class="['user', isOpen && 'active']">
|
||||
<div @mouseover="openMenu(true)" @mouseleave="closeMenu(true)">
|
||||
<hc-avatar class="avatar" :user="user" />
|
||||
<hc-avatar v-if="showAvatar" class="avatar" :user="user" />
|
||||
<div>
|
||||
<ds-text class="userinfo">
|
||||
<b class="username">{{ userName | truncate(18) }}</b>
|
||||
<ds-text v-if="dateTime" size="small" color="soft">
|
||||
<base-icon name="clock" />
|
||||
<client-only>
|
||||
<hc-relative-date-time :date-time="dateTime" />
|
||||
</client-only>
|
||||
<slot name="dateTime"></slot>
|
||||
</ds-text>
|
||||
<b>{{ userSlug }}</b>
|
||||
</ds-text>
|
||||
</div>
|
||||
<ds-text align="left" size="small" color="soft">
|
||||
{{ userSlug }}
|
||||
<ds-text class="username" align="left" size="small" color="soft">
|
||||
{{ userName | truncate(18) }}
|
||||
<template v-if="dateTime">
|
||||
<base-icon name="clock" />
|
||||
<hc-relative-date-time :date-time="dateTime" />
|
||||
<slot name="dateTime"></slot>
|
||||
</template>
|
||||
</ds-text>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
@ -105,7 +103,8 @@ export default {
|
||||
},
|
||||
props: {
|
||||
user: { type: Object, default: null },
|
||||
trunc: { type: Number, default: null },
|
||||
showAvatar: { type: Boolean, default: true },
|
||||
trunc: { type: Number, default: 18 }, // "-1" is no trunc
|
||||
dateTime: { type: [Date, String], default: null },
|
||||
},
|
||||
computed: {
|
||||
@ -149,7 +148,6 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.avatar {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
margin-right: 4px;
|
||||
height: 100%;
|
||||
@ -178,4 +176,8 @@ export default {
|
||||
z-index: 999;
|
||||
}
|
||||
}
|
||||
|
||||
.user-slug {
|
||||
margin-bottom: $space-xx-small;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CounterIcon from './CounterIcon'
|
||||
import BaseIcon from '../BaseIcon/BaseIcon'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('CounterIcon.vue', () => {
|
||||
let propsData, wrapper, tag
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(CounterIcon, { propsData, localVue })
|
||||
}
|
||||
|
||||
describe('given a valid icon name and count', () => {
|
||||
beforeEach(() => {
|
||||
propsData = { icon: 'comments', count: 1 }
|
||||
wrapper = Wrapper()
|
||||
tag = wrapper.find('.ds-tag')
|
||||
})
|
||||
|
||||
it('renders BaseIcon', () => {
|
||||
expect(wrapper.find(BaseIcon).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders the count', () => {
|
||||
expect(tag.text()).toEqual('1')
|
||||
})
|
||||
|
||||
it('uses a round tag', () => {
|
||||
expect(tag.classes()).toContain('ds-tag-round')
|
||||
})
|
||||
|
||||
it('uses a primary button', () => {
|
||||
expect(tag.classes()).toContain('ds-tag-primary')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,19 @@
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
import helpers from '~/storybook/helpers'
|
||||
import CounterIcon from './CounterIcon.vue'
|
||||
|
||||
storiesOf('CounterIcon', module)
|
||||
.addDecorator(helpers.layout)
|
||||
.add('flag icon with button in slot position', () => ({
|
||||
components: { CounterIcon },
|
||||
data() {
|
||||
return { icon: 'flag', count: 3 }
|
||||
},
|
||||
template: `
|
||||
<counter-icon icon="pizza" :count="count">
|
||||
<ds-button ghost primary>
|
||||
Report Details
|
||||
</ds-button>
|
||||
</counter-icon>
|
||||
`,
|
||||
}))
|
||||
29
webapp/components/_new/generic/CounterIcon/CounterIcon.vue
Normal file
29
webapp/components/_new/generic/CounterIcon/CounterIcon.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<span>
|
||||
<base-icon :name="icon" />
|
||||
<ds-tag
|
||||
style="margin-top: -4px; margin-left: -12px; position: absolute;"
|
||||
color="primary"
|
||||
size="small"
|
||||
round
|
||||
>
|
||||
{{ count }}
|
||||
</ds-tag>
|
||||
<span class="counter-icon-text">
|
||||
<slot />
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: { type: String, required: true },
|
||||
count: { type: Number, required: true },
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.counter-icon-text {
|
||||
margin-left: $space-xx-small;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,93 @@
|
||||
import { config, mount, RouterLinkStub } from '@vue/test-utils'
|
||||
import Vuex from 'vuex'
|
||||
import FiledReportsTable from './FiledReportsTable'
|
||||
import { reports } from '~/components/features/ReportList/ReportList.story.js'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
localVue.filter('truncate', string => string)
|
||||
|
||||
config.stubs['client-only'] = '<span><slot /></span>'
|
||||
|
||||
describe('FiledReportsTable.vue', () => {
|
||||
let wrapper, mocks, propsData, stubs, filed
|
||||
|
||||
beforeEach(() => {
|
||||
mocks = {
|
||||
$t: jest.fn(string => string),
|
||||
}
|
||||
stubs = {
|
||||
NuxtLink: RouterLinkStub,
|
||||
}
|
||||
propsData = {}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
const Wrapper = () => {
|
||||
const store = new Vuex.Store({
|
||||
getters: {
|
||||
'auth/isModerator': () => true,
|
||||
'auth/user': () => {
|
||||
return { id: 'moderator' }
|
||||
},
|
||||
},
|
||||
})
|
||||
return mount(FiledReportsTable, {
|
||||
propsData,
|
||||
mocks,
|
||||
localVue,
|
||||
store,
|
||||
stubs,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
describe('given reports', () => {
|
||||
beforeEach(() => {
|
||||
filed = reports.map(report => report.filed)
|
||||
propsData.filed = filed[0]
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders a table', () => {
|
||||
expect(wrapper.find('.ds-table').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has 4 columns', () => {
|
||||
expect(wrapper.findAll('.ds-table-col')).toHaveLength(4)
|
||||
})
|
||||
|
||||
describe('FiledReport', () => {
|
||||
it('renders the reporting user', () => {
|
||||
const userSlug = wrapper.find('[data-test="filing-user"]')
|
||||
expect(userSlug.text()).toContain('@community-moderator')
|
||||
})
|
||||
|
||||
it('renders the reported date', () => {
|
||||
const date = wrapper.find('[data-test="filed-date"]')
|
||||
expect(date.text()).toEqual('10/02/2019')
|
||||
})
|
||||
|
||||
it('renders the category text', () => {
|
||||
const columns = wrapper.findAll('.ds-table-col')
|
||||
const reasonCategory = columns.filter(
|
||||
category =>
|
||||
category.text() === 'report.reason.category.options.pornographic_content_links',
|
||||
)
|
||||
expect(reasonCategory.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it("renders the Post's content", () => {
|
||||
const columns = wrapper.findAll('.ds-table-col')
|
||||
const reasonDescription = columns.filter(
|
||||
column => column.text() === 'This comment is porno!!!',
|
||||
)
|
||||
expect(reasonDescription.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,25 @@
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
import { withA11y } from '@storybook/addon-a11y'
|
||||
import FiledReportsTable from '~/components/features/FiledReportsTable/FiledReportsTable'
|
||||
import helpers from '~/storybook/helpers'
|
||||
import { reports } from '~/components/features/ReportList/ReportList.story.js'
|
||||
|
||||
storiesOf('FiledReportsTable', module)
|
||||
.addDecorator(withA11y)
|
||||
.addDecorator(helpers.layout)
|
||||
.add('with filed reports', () => ({
|
||||
components: { FiledReportsTable },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
filed: reports[0].filed,
|
||||
}),
|
||||
template: `<table>
|
||||
<tbody class="report-row">
|
||||
<tr class="row">
|
||||
<td colspan="100%">
|
||||
<filed-reports-table :filed="filed" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`,
|
||||
}))
|
||||
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<ds-table
|
||||
class="nested-table"
|
||||
v-if="filed && filed.length"
|
||||
:data="filed"
|
||||
:fields="fields"
|
||||
condensed
|
||||
>
|
||||
<template #submitter="scope">
|
||||
<hc-user
|
||||
:user="scope.row.submitter"
|
||||
:showAvatar="false"
|
||||
:trunc="30"
|
||||
data-test="filing-user"
|
||||
/>
|
||||
</template>
|
||||
<template #reportedOn="scope">
|
||||
<ds-text size="small">
|
||||
<hc-relative-date-time :date-time="scope.row.createdAt" data-test="filed-date" />
|
||||
</ds-text>
|
||||
</template>
|
||||
<template #reasonCategory="scope">
|
||||
{{ $t('report.reason.category.options.' + scope.row.reasonCategory) }}
|
||||
</template>
|
||||
<template #reasonDescription="scope">
|
||||
{{ scope.row.reasonDescription.length ? scope.row.reasonDescription : '—' }}
|
||||
</template>
|
||||
</ds-table>
|
||||
</template>
|
||||
<script>
|
||||
import HcUser from '~/components/User/User'
|
||||
import HcRelativeDateTime from '~/components/RelativeDateTime'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HcUser,
|
||||
HcRelativeDateTime,
|
||||
},
|
||||
props: {
|
||||
filed: { type: Array, default: () => [] },
|
||||
},
|
||||
computed: {
|
||||
fields() {
|
||||
return {
|
||||
submitter: {
|
||||
label: this.$t('moderation.reports.submitter'),
|
||||
width: '15%',
|
||||
},
|
||||
reportedOn: {
|
||||
label: this.$t('moderation.reports.reportedOn'),
|
||||
width: '20%',
|
||||
},
|
||||
reasonCategory: {
|
||||
label: this.$t('moderation.reports.reasonCategory'),
|
||||
width: '30%',
|
||||
},
|
||||
reasonDescription: {
|
||||
label: this.$t('moderation.reports.reasonDescription'),
|
||||
width: '35%',
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.nested-table {
|
||||
padding: $space-small;
|
||||
border-top: $border-size-base solid $color-neutral-60;
|
||||
border-bottom: $border-size-base solid $color-neutral-60;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user