Merge master

This commit is contained in:
Wolfgang Huß 2020-03-09 07:36:06 +01:00
parent 84fccdd470
commit 3ea1348bd4
254 changed files with 12732 additions and 10279 deletions

24
.github/ISSUE_TEMPLATE/devops_ticket.md vendored Normal file
View File

@ -0,0 +1,24 @@
---
name: 💥 DevOps ticket
about: Help us manage our deployed App.
labels: devops
title: 💥 [DevOps]
---
## :fire: DevOps ticket
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the problem is.-->
### Motive
<!-- Why does this task need to be done? What can we benefit from this? -->
### Related issues
<!-- Are there any related issues to link to? Please paste them below for reference. -->
### Implementation
<!-- Please, document any ideas of how the task can be performed. -->
### Validation
<!-- How can we make sure that this task was successful? -->
### Additional context
<!-- Add other context or background about the feature request here.-->

View File

@ -6,7 +6,8 @@ addons:
- libgconf-2-4
snaps:
- docker
firefox: "latest-esr"
install:
- yarn global add wait-on
# Install Codecov
@ -24,6 +25,9 @@ script:
- export CYPRESS_RETRIES=1
- export BRANCH=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then echo $TRAVIS_BRANCH; else echo $TRAVIS_PULL_REQUEST_BRANCH; fi)
- echo "TRAVIS_BRANCH=$TRAVIS_BRANCH, PR=$PR, BRANCH=$BRANCH"
# Miscellaneous
- ./scripts/translations/sort.sh
- ./scripts/translations/missing-keys.sh
# Backend
- docker-compose exec backend yarn run lint
- docker-compose exec backend yarn run test --ci --verbose=false --coverage

11
.vscode/settings.json vendored
View File

@ -1,11 +0,0 @@
{
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "vue",
"autoFix": true
}
],
"editor.formatOnSave": false,
}

View File

@ -4,10 +4,124 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.4.0](https://github.com/Human-Connection/Human-Connection/compare/v0.3.1...v0.4.0)
> 21 February 2020
- fix: Favor Cypress.Promise over async/await in e2e tests [`#3115`](https://github.com/Human-Connection/Human-Connection/pull/3115)
- docs(setup): Fix links in tests [`#3120`](https://github.com/Human-Connection/Human-Connection/pull/3120)
- feat: 🍰 Expose sensitive report type to moderators only [`#3075`](https://github.com/Human-Connection/Human-Connection/pull/3075)
- refactor: migrate card component [`#2870`](https://github.com/Human-Connection/Human-Connection/pull/2870)
- build(deps): bump metascraper-youtube from 5.10.7 to 5.11.1 in /backend [`#3114`](https://github.com/Human-Connection/Human-Connection/pull/3114)
- fix(cypress): Upgrade cypress, remove log out step [`#3119`](https://github.com/Human-Connection/Human-Connection/pull/3119)
- build(deps): bump metascraper-date from 5.10.7 to 5.11.1 in /backend [`#3069`](https://github.com/Human-Connection/Human-Connection/pull/3069)
- build(deps): bump metascraper-author from 5.10.7 to 5.11.1 in /backend [`#3070`](https://github.com/Human-Connection/Human-Connection/pull/3070)
- build(deps): bump xregexp from 4.2.4 to 4.3.0 in /webapp [`#3047`](https://github.com/Human-Connection/Human-Connection/pull/3047)
- build(deps): bump metascraper-publisher from 5.10.7 to 5.11.1 in /backend [`#3068`](https://github.com/Human-Connection/Human-Connection/pull/3068)
- build(deps): bump @sentry/node from 5.12.3 to 5.12.4 in /backend [`#3113`](https://github.com/Human-Connection/Human-Connection/pull/3113)
- feat: German Translations Update By Andreas Plank [`#3109`](https://github.com/Human-Connection/Human-Connection/pull/3109)
- fix(frontend): Remove Hover Menu from User Teaser [`#3093`](https://github.com/Human-Connection/Human-Connection/pull/3093)
- build(deps-dev): bump eslint-plugin-jest from 23.6.0 to 23.7.0 in /webapp [`#3030`](https://github.com/Human-Connection/Human-Connection/pull/3030)
- fix(frontend): Post page won't crash on anonymous user [`#2981`](https://github.com/Human-Connection/Human-Connection/pull/2981)
- chore(cypress): Remove debug statements [`#3110`](https://github.com/Human-Connection/Human-Connection/pull/3110)
- build(deps): bump metascraper-audio from 5.10.7 to 5.11.1 in /backend [`#3066`](https://github.com/Human-Connection/Human-Connection/pull/3066)
- build(deps): bump @nuxtjs/sentry from 3.2.3 to 3.2.4 in /webapp [`#3081`](https://github.com/Human-Connection/Human-Connection/pull/3081)
- build(deps-dev): bump apollo-server-testing from 2.10.0 to 2.10.1 in /backend [`#3078`](https://github.com/Human-Connection/Human-Connection/pull/3078)
- fix(subscriptions): Don't publish undefined [`#3101`](https://github.com/Human-Connection/Human-Connection/pull/3101)
- build(deps): [security] bump yarn from 1.17.3 to 1.22.0 in /webapp [`#3077`](https://github.com/Human-Connection/Human-Connection/pull/3077)
- feat: Normalize locales/json files [`#3003`](https://github.com/Human-Connection/Human-Connection/pull/3003)
- 🍰feat: Delete teaser image [`#2585`](https://github.com/Human-Connection/Human-Connection/pull/2585)
- fix: swap lat and lng [`#2589`](https://github.com/Human-Connection/Human-Connection/pull/2589)
- fix(frontend): avatar image covers full circle [`#3102`](https://github.com/Human-Connection/Human-Connection/pull/3102)
- fix(jwt): Whitelist encoded JWT attributes [`#3090`](https://github.com/Human-Connection/Human-Connection/pull/3090)
- test: Write cypress tests for ImageUploader [`#3056`](https://github.com/Human-Connection/Human-Connection/pull/3056)
- build(deps-dev): bump eslint-plugin-vue from 6.1.2 to 6.2.1 in /webapp [`#3092`](https://github.com/Human-Connection/Human-Connection/pull/3092)
- build: Fix intermittent failing tests [`#3087`](https://github.com/Human-Connection/Human-Connection/pull/3087)
- fix(nuxt-env): Configuration issue with websockets [`#3089`](https://github.com/Human-Connection/Human-Connection/pull/3089)
- build(deps-dev): bump eslint-plugin-jest from 23.6.0 to 23.7.0 in /backend [`#3029`](https://github.com/Human-Connection/Human-Connection/pull/3029)
- build(deps): bump cookie-universal-nuxt from 2.1.1 to 2.1.2 in /webapp [`#3073`](https://github.com/Human-Connection/Human-Connection/pull/3073)
- build(deps): bump @nuxtjs/sentry from 3.2.2 to 3.2.3 in /webapp [`#3072`](https://github.com/Human-Connection/Human-Connection/pull/3072)
- build(deps): bump metascraper-image from 5.10.7 to 5.11.1 in /backend [`#3067`](https://github.com/Human-Connection/Human-Connection/pull/3067)
- build(deps-dev): bump vue-loader from 15.8.3 to 15.9.0 in /webapp [`#3060`](https://github.com/Human-Connection/Human-Connection/pull/3060)
- build(deps-dev): bump @storybook/addon-actions from 5.3.12 to 5.3.13 in /webapp [`#3049`](https://github.com/Human-Connection/Human-Connection/pull/3049)
- refactor(cypress): Speed up builds, avoid login through UI [`#3042`](https://github.com/Human-Connection/Human-Connection/pull/3042)
- feat: 🍰 Set up Vue-Apollo Subscriptions [`#1705`](https://github.com/Human-Connection/Human-Connection/pull/1705)
- fix: Update devops_ticket.md [`#3053`](https://github.com/Human-Connection/Human-Connection/pull/3053)
- build(deps-dev): bump @storybook/addon-notes from 5.3.12 to 5.3.13 in /webapp [`#3048`](https://github.com/Human-Connection/Human-Connection/pull/3048)
- build(deps-dev): bump @storybook/addon-a11y from 5.3.12 to 5.3.13 in /webapp [`#3050`](https://github.com/Human-Connection/Human-Connection/pull/3050)
- build(deps): Node v13 compatbility [`#3041`](https://github.com/Human-Connection/Human-Connection/pull/3041)
- build(deps): bump request from 2.88.0 to 2.88.2 in /backend [`#3045`](https://github.com/Human-Connection/Human-Connection/pull/3045)
- build(deps-dev): bump @storybook/vue from 5.3.12 to 5.3.13 in /webapp [`#3046`](https://github.com/Human-Connection/Human-Connection/pull/3046)
- feat(deployment): Add helm charts for deploy [`#1613`](https://github.com/Human-Connection/Human-Connection/pull/1613)
- build(deps-dev): bump vue-svg-loader from 0.15.0 to 0.16.0 in /webapp [`#3039`](https://github.com/Human-Connection/Human-Connection/pull/3039)
- fix: Increase body parser limit [`#3037`](https://github.com/Human-Connection/Human-Connection/pull/3037)
- chore: Update to v0.3.1 [`#3035`](https://github.com/Human-Connection/Human-Connection/pull/3035)
- fix(subscriptions): Don't publish undefined [`#3088`](https://github.com/Human-Connection/Human-Connection/issues/3088)
- locales sorted. [`fa906ef`](https://github.com/Human-Connection/Human-Connection/commit/fa906efb1f40dc5bd80c9678f33c7b607a320099)
- Upgrade cypress, remove log out step [`0df4038`](https://github.com/Human-Connection/Human-Connection/commit/0df40386dd866c6b9ce540b966dfe00089507d31)
- Refactor GQL and tests, first approach [`f380915`](https://github.com/Human-Connection/Human-Connection/commit/f380915b2c679d42e5db136ea1d923cf00bbcf10)
#### [v0.3.1](https://github.com/Human-Connection/Human-Connection/compare/v0.3.0...v0.3.1)
> 10 February 2020
- fix: Display unblock feature only for blocking user [`#3034`](https://github.com/Human-Connection/Human-Connection/pull/3034)
- refactor(factories): Refactor test factories with rosie.js [`#2921`](https://github.com/Human-Connection/Human-Connection/pull/2921)
- build(deps-dev): bump @vue/cli-shared-utils from 4.1.2 to 4.2.2 in /webapp [`#3031`](https://github.com/Human-Connection/Human-Connection/pull/3031)
- build(deps): bump graphql-shield from 7.0.10 to 7.0.11 in /backend [`#3028`](https://github.com/Human-Connection/Human-Connection/pull/3028)
- build(deps-dev): bump codecov from 3.6.4 to 3.6.5 [`#3027`](https://github.com/Human-Connection/Human-Connection/pull/3027)
- chore: Add DevOps issue template [`#2999`](https://github.com/Human-Connection/Human-Connection/pull/2999)
- fix: Error pages can be translated [`#2826`](https://github.com/Human-Connection/Human-Connection/pull/2826)
- build(deps-dev): bump apollo-server-testing from 2.9.16 to 2.10.0 in /backend [`#3020`](https://github.com/Human-Connection/Human-Connection/pull/3020)
- build(deps): bump apollo-server from 2.9.16 to 2.10.0 in /backend [`#3019`](https://github.com/Human-Connection/Human-Connection/pull/3019)
- build(deps): bump graphql-tag from 2.10.2 to 2.10.3 in /backend [`#3011`](https://github.com/Human-Connection/Human-Connection/pull/3011)
- build(deps): bump graphql-shield from 7.0.9 to 7.0.10 in /backend [`#3010`](https://github.com/Human-Connection/Human-Connection/pull/3010)
- build(deps-dev): bump @storybook/addon-actions from 5.3.10 to 5.3.12 in /webapp [`#2998`](https://github.com/Human-Connection/Human-Connection/pull/2998)
- build(deps-dev): bump @storybook/addon-notes from 5.3.10 to 5.3.12 in /webapp [`#2997`](https://github.com/Human-Connection/Human-Connection/pull/2997)
- build(deps-dev): bump @storybook/addon-a11y from 5.3.10 to 5.3.12 in /webapp [`#2996`](https://github.com/Human-Connection/Human-Connection/pull/2996)
- build(deps): bump metascraper-author from 5.10.6 to 5.10.7 in /backend [`#2994`](https://github.com/Human-Connection/Human-Connection/pull/2994)
- build(deps): bump metascraper-title from 5.10.6 to 5.10.7 in /backend [`#2978`](https://github.com/Human-Connection/Human-Connection/pull/2978)
- build(deps-dev): bump @storybook/vue from 5.3.10 to 5.3.12 in /webapp [`#2995`](https://github.com/Human-Connection/Human-Connection/pull/2995)
- build(deps): bump metascraper-audio from 5.10.6 to 5.10.7 in /backend [`#2993`](https://github.com/Human-Connection/Human-Connection/pull/2993)
- build(deps): bump graphql-tag from 2.10.1 to 2.10.2 in /backend [`#2992`](https://github.com/Human-Connection/Human-Connection/pull/2992)
- build(deps): bump metascraper-url from 5.10.6 to 5.10.7 in /backend [`#2991`](https://github.com/Human-Connection/Human-Connection/pull/2991)
- build(deps): bump @sentry/node from 5.12.0 to 5.12.3 in /backend [`#2990`](https://github.com/Human-Connection/Human-Connection/pull/2990)
- build(deps-dev): bump @storybook/addon-notes from 5.3.9 to 5.3.10 in /webapp [`#2951`](https://github.com/Human-Connection/Human-Connection/pull/2951)
- build(deps): bump metascraper from 5.10.6 to 5.11.0 in /backend [`#2976`](https://github.com/Human-Connection/Human-Connection/pull/2976)
- build(deps): bump metascraper-logo from 5.10.6 to 5.10.7 in /backend [`#2975`](https://github.com/Human-Connection/Human-Connection/pull/2975)
- chore: Add issue template for Refactoring tickets [`#2983`](https://github.com/Human-Connection/Human-Connection/pull/2983)
- build(deps): bump @nuxtjs/sentry from 3.1.0 to 3.2.2 in /webapp [`#2974`](https://github.com/Human-Connection/Human-Connection/pull/2974)
- build(deps): bump metascraper-video from 5.10.6 to 5.10.7 in /backend [`#2952`](https://github.com/Human-Connection/Human-Connection/pull/2952)
- build(deps): bump metascraper-lang from 5.10.6 to 5.10.7 in /backend [`#2950`](https://github.com/Human-Connection/Human-Connection/pull/2950)
- build(deps): bump metascraper-description from 5.10.6 to 5.11.0 in /backend [`#2948`](https://github.com/Human-Connection/Human-Connection/pull/2948)
- build(deps): bump @sentry/node from 5.11.2 to 5.12.0 in /backend [`#2977`](https://github.com/Human-Connection/Human-Connection/pull/2977)
- build(deps): bump @nuxtjs/pwa from 3.0.0-beta.19 to 3.0.0-beta.20 in /webapp [`#2959`](https://github.com/Human-Connection/Human-Connection/pull/2959)
- build(deps-dev): bump @storybook/addon-a11y from 5.3.9 to 5.3.10 in /webapp [`#2956`](https://github.com/Human-Connection/Human-Connection/pull/2956)
- build(deps-dev): bump eslint-plugin-import from 2.20.0 to 2.20.1 in /webapp [`#2949`](https://github.com/Human-Connection/Human-Connection/pull/2949)
- build(deps): bump metascraper-soundcloud from 5.10.6 to 5.10.7 in /backend [`#2945`](https://github.com/Human-Connection/Human-Connection/pull/2945)
- build(deps): bump metascraper-date from 5.10.6 to 5.10.7 in /backend [`#2944`](https://github.com/Human-Connection/Human-Connection/pull/2944)
- build(deps-dev): bump codecov from 3.6.2 to 3.6.4 [`#2943`](https://github.com/Human-Connection/Human-Connection/pull/2943)
- build(deps-dev): bump @storybook/addon-actions in /webapp [`#2953`](https://github.com/Human-Connection/Human-Connection/pull/2953)
- build(deps): bump metascraper-publisher in /backend [`#2954`](https://github.com/Human-Connection/Human-Connection/pull/2954)
- build(deps-dev): bump eslint-plugin-import in /backend [`#2955`](https://github.com/Human-Connection/Human-Connection/pull/2955)
- build(deps): bump metascraper-youtube from 5.10.6 to 5.10.7 in /backend [`#2957`](https://github.com/Human-Connection/Human-Connection/pull/2957)
- build(deps): bump metascraper-image from 5.10.6 to 5.10.7 in /backend [`#2960`](https://github.com/Human-Connection/Human-Connection/pull/2960)
- build(deps-dev): bump @storybook/vue from 5.3.9 to 5.3.10 in /webapp [`#2961`](https://github.com/Human-Connection/Human-Connection/pull/2961)
- build(deps): bump @nuxtjs/axios from 5.9.4 to 5.9.5 in /webapp [`#2962`](https://github.com/Human-Connection/Human-Connection/pull/2962)
- fix: Update mute/unmute icon to unused icon [`#2973`](https://github.com/Human-Connection/Human-Connection/pull/2973)
- fix: Remove github release script breaking build [`#2971`](https://github.com/Human-Connection/Human-Connection/pull/2971)
- Use original createdAt for merged users/emails [`#2969`](https://github.com/Human-Connection/Human-Connection/pull/2969)
- Fix typo [`#2966`](https://github.com/Human-Connection/Human-Connection/pull/2966)
- chore: Update to v0.3.0 [`#2941`](https://github.com/Human-Connection/Human-Connection/pull/2941)
- Replace buildList with array of Promises [`46edc3f`](https://github.com/Human-Connection/Human-Connection/commit/46edc3fdd5b83c2f00506f595b1254d7597767e0)
- refactor TeaserImage component [`e14cbf8`](https://github.com/Human-Connection/Human-Connection/commit/e14cbf8173e3040b5285ba6a5c73e2d2d2a47860)
- refactor DeleteData template and CSS [`509892b`](https://github.com/Human-Connection/Human-Connection/commit/509892b6caee6c4ca8384fb0090122ced98edfd4)
#### [v0.3.0](https://github.com/Human-Connection/Human-Connection/compare/v0.2.1...v0.3.0)
> 31 January 2020
- build(deps-dev): bump @babel/core from 7.8.3 to 7.8.4 in /webapp [`#2939`](https://github.com/Human-Connection/Human-Connection/pull/2939)
- feat: 🍰 Direct Reply On Comment [`#2608`](https://github.com/Human-Connection/Human-Connection/pull/2608)
- build(deps-dev): bump @babel/core from 7.8.3 to 7.8.4 in /backend [`#2938`](https://github.com/Human-Connection/Human-Connection/pull/2938)
- fix: deploy script with new naming convention [`#2930`](https://github.com/Human-Connection/Human-Connection/pull/2930)
@ -181,9 +295,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- refactor(modules): Various import fixes [`#2773`](https://github.com/Human-Connection/Human-Connection/issues/2773) [`#2774`](https://github.com/Human-Connection/Human-Connection/issues/2774)
- feat(webapp): Display deployed version in footer [`#1831`](https://github.com/Human-Connection/Human-Connection/issues/1831)
- fix #2229 [`#2229`](https://github.com/Human-Connection/Human-Connection/issues/2229)
- refactor: Make `db:setup` init stage of `migrate` [`b063847`](https://github.com/Human-Connection/Human-Connection/commit/b063847849a84db885337dc8e84e75ddaf87011f)
- Improve styling per @alina-beck review [`bcc1ab1`](https://github.com/Human-Connection/Human-Connection/commit/bcc1ab167e8b1dfdac1ec0a05a0c14e8234bcabc)
- test(cypress): Cover "Pinned post" feature [`d49afc2`](https://github.com/Human-Connection/Human-Connection/commit/d49afc25cfa1c1f98ed04f78dd3ff826cd85ae25)
- Get rid of different factory files [`fc36729`](https://github.com/Human-Connection/Human-Connection/commit/fc367297e3e054f09b7f8f31788ab68d87f6babf)
- Refactor factory for comments [`2fc71d7`](https://github.com/Human-Connection/Human-Connection/commit/2fc71d75a5d5eab9c3467e94e00257ef6dd7d8a0)
- Refactor user factory [`2a79c53`](https://github.com/Human-Connection/Human-Connection/commit/2a79c53765b73f9b91691eb75f55cf8c9e48306e)
#### [v0.2.1](https://github.com/Human-Connection/Human-Connection/compare/v0.2.0...v0.2.1)
@ -292,7 +406,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- fixes #2659 [`#2659`](https://github.com/Human-Connection/Human-Connection/issues/2659)
- Convert block/unblock to blacklist/whitelist [`c297b83`](https://github.com/Human-Connection/Human-Connection/commit/c297b83f873edc61ddec370633b9b65896c56591)
- Rename blacklist/whitelist to mute/unmute [`ba3e9e1`](https://github.com/Human-Connection/Human-Connection/commit/ba3e9e1025bf432151c9bf1002045179b338ff7f)
- build(deps-dev): bump storybook-design-token in /webapp [`88d39c4`](https://github.com/Human-Connection/Human-Connection/commit/88d39c4a427cb86527b06201f3f5e96d53ac09a0)
- manage button states and color schemes with mixin [`1b9249c`](https://github.com/Human-Connection/Human-Connection/commit/1b9249c685e34eb2e94b31ee0ec22421c6aa6a73)
#### [v0.2.0](https://github.com/Human-Connection/Human-Connection/compare/v0.1.13...v0.2.0)

View File

@ -1,6 +1,6 @@
{
"name": "human-connection-backend",
"version": "0.2.2",
"version": "0.4.0",
"description": "GraphQL Backend for Human Connection",
"main": "src/index.js",
"scripts": {
@ -38,19 +38,19 @@
},
"dependencies": {
"@hapi/joi": "^17.1.0",
"@sentry/node": "^5.12.3",
"@sentry/node": "^5.13.1",
"apollo-cache-inmemory": "~1.6.5",
"apollo-client": "~2.6.8",
"apollo-link-context": "~1.0.19",
"apollo-link-http": "~1.5.16",
"apollo-server": "~2.10.0",
"apollo-server-express": "^2.9.16",
"apollo-server": "~2.11.0",
"apollo-server-express": "^2.11.0",
"babel-plugin-transform-runtime": "^6.23.0",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~7.0.0",
"date-fns": "2.9.0",
"cross-env": "~7.0.1",
"date-fns": "2.10.0",
"debug": "~4.1.1",
"dotenv": "~8.2.0",
"express": "^4.17.1",
@ -60,29 +60,31 @@
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~4.0.2",
"graphql-middleware-sentry": "^3.2.1",
"graphql-shield": "~7.0.10",
"graphql-redis-subscriptions": "^2.2.1",
"graphql-shield": "~7.0.14",
"graphql-tag": "~2.10.3",
"helmet": "~3.21.2",
"helmet": "~3.21.3",
"ioredis": "^4.16.0",
"jsonwebtoken": "~8.5.1",
"linkifyjs": "~2.1.8",
"lodash": "~4.17.14",
"merge-graphql-schemas": "^1.7.6",
"metascraper": "^5.11.0",
"metascraper-audio": "^5.10.7",
"metascraper-author": "^5.10.7",
"metascraper": "^5.11.4",
"metascraper-audio": "^5.11.1",
"metascraper-author": "^5.11.1",
"metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.10.7",
"metascraper-description": "^5.11.0",
"metascraper-image": "^5.10.7",
"metascraper-lang": "^5.10.7",
"metascraper-date": "^5.11.1",
"metascraper-description": "^5.11.1",
"metascraper-image": "^5.11.1",
"metascraper-lang": "^5.11.1",
"metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.10.7",
"metascraper-publisher": "^5.10.7",
"metascraper-soundcloud": "^5.10.7",
"metascraper-title": "^5.10.7",
"metascraper-url": "^5.10.7",
"metascraper-video": "^5.10.7",
"metascraper-youtube": "^5.10.7",
"metascraper-logo": "^5.11.1",
"metascraper-publisher": "^5.11.1",
"metascraper-soundcloud": "^5.11.5",
"metascraper-title": "^5.11.1",
"metascraper-url": "^5.11.1",
"metascraper-video": "^5.11.1",
"metascraper-youtube": "^5.11.1",
"migrate": "^1.6.2",
"minimatch": "^3.0.4",
"mustache": "^4.0.0",
@ -90,28 +92,29 @@
"neo4j-graphql-js": "^2.11.5",
"neode": "^0.3.7",
"node-fetch": "~2.6.0",
"nodemailer": "^6.4.2",
"nodemailer": "^6.4.4",
"nodemailer-html-to-text": "^3.1.0",
"npm-run-all": "~4.1.5",
"request": "~2.88.0",
"sanitize-html": "~1.21.1",
"request": "~2.88.2",
"sanitize-html": "~1.22.0",
"slug": "~2.1.1",
"subscriptions-transport-ws": "^0.9.16",
"trunc-html": "~1.1.2",
"uuid": "~3.4.0",
"uuid": "~7.0.1",
"validator": "^12.2.0",
"wait-on": "~4.0.0",
"xregexp": "^4.2.4"
"wait-on": "~4.0.1",
"xregexp": "^4.3.0"
},
"devDependencies": {
"@babel/cli": "~7.8.4",
"@babel/core": "~7.8.4",
"@babel/node": "~7.8.4",
"@babel/core": "~7.8.7",
"@babel/node": "~7.8.7",
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
"@babel/preset-env": "~7.8.4",
"@babel/register": "^7.8.3",
"apollo-server-testing": "~2.10.0",
"@babel/preset-env": "~7.8.7",
"@babel/register": "^7.8.6",
"apollo-server-testing": "~2.11.0",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.3",
"babel-eslint": "~10.1.0",
"babel-jest": "~25.1.0",
"chai": "~4.2.0",
"cucumber": "~6.0.5",
@ -119,7 +122,7 @@
"eslint-config-prettier": "~6.10.0",
"eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.20.1",
"eslint-plugin-jest": "~23.6.0",
"eslint-plugin-jest": "~23.8.1",
"eslint-plugin-node": "~11.0.0",
"eslint-plugin-prettier": "~3.1.2",
"eslint-plugin-promise": "~4.2.1",
@ -127,6 +130,10 @@
"jest": "~25.1.0",
"nodemon": "~2.0.2",
"prettier": "~1.19.1",
"rosie": "^2.0.1",
"supertest": "~4.0.2"
},
"resolutions": {
"fs-capacitor": "6.0.0"
}
}

View File

@ -1,10 +1,9 @@
import { handler } from './webfinger'
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { getDriver } from '../../db/neo4j'
let resource, res, json, status, contentType
const factory = Factory()
const driver = getDriver()
const request = () => {
@ -28,7 +27,7 @@ const request = () => {
}
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('webfinger', () => {
@ -90,7 +89,7 @@ describe('webfinger', () => {
describe('given a user for acct', () => {
beforeEach(async () => {
await factory.create('User', { slug: 'some-user' })
await Factory.build('user', { slug: 'some-user' })
})
it('returns user object', async () => {

View File

@ -4,6 +4,9 @@ if (require.resolve) {
dotenv.config({ path: require.resolve('../../.env') })
}
// eslint-disable-next-line no-undef
const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env
const {
MAPBOX_TOKEN,
JWT_SECRET,
@ -20,7 +23,10 @@ const {
NEO4J_PASSWORD = 'neo4j',
CLIENT_URI = 'http://localhost:3000',
GRAPHQL_URI = 'http://localhost:4000',
} = process.env
REDIS_DOMAIN,
REDIS_PORT,
REDIS_PASSWORD,
} = env
export const requiredConfigs = {
MAPBOX_TOKEN,
@ -58,7 +64,7 @@ export const developmentConfigs = {
}
export const sentryConfigs = { SENTRY_DSN_BACKEND, COMMIT }
export const redisConfiig = { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD }
export default {
...requiredConfigs,
...smtpConfigs,
@ -66,4 +72,5 @@ export default {
...serverConfigs,
...developmentConfigs,
...sentryConfigs,
...redisConfiig,
}

View File

@ -1,4 +1,4 @@
import { cleanDatabase } from '../factories'
import { cleanDatabase } from '../db/factories'
if (process.env.NODE_ENV === 'production') {
throw new Error(`You cannot clean the database in production environment!`)

229
backend/src/db/factories.js Normal file
View File

@ -0,0 +1,229 @@
import { v4 as uuid } from 'uuid'
import faker from 'faker'
import slugify from 'slug'
import { hashSync } from 'bcryptjs'
import { Factory } from 'rosie'
import { getDriver, getNeode } from './neo4j'
const neode = getNeode()
export const cleanDatabase = async (options = {}) => {
const { driver = getDriver() } = options
const session = driver.session()
try {
await session.writeTransaction(transaction => {
return transaction.run(
`
MATCH (everything)
DETACH DELETE everything
`,
)
})
} finally {
session.close()
}
}
Factory.define('category')
.attr('id', uuid)
.attr('icon', 'globe')
.attr('name', 'Global Peace & Nonviolence')
.after((buildObject, options) => {
return neode.create('Category', buildObject)
})
Factory.define('badge')
.attr('type', 'crowdfunding')
.attr('status', 'permanent')
.after((buildObject, options) => {
return neode.create('Badge', buildObject)
})
Factory.define('userWithoutEmailAddress')
.option('password', '1234')
.attrs({
id: uuid,
name: faker.name.findName,
password: '1234',
role: 'user',
avatar: faker.internet.avatar,
about: faker.lorem.paragraph,
termsAndConditionsAgreedVersion: '0.0.1',
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
allowEmbedIframes: false,
showShoutsPublicly: false,
locale: 'en',
})
.attr('slug', ['slug', 'name'], (slug, name) => {
return slug || slugify(name, { lower: true })
})
.attr('encryptedPassword', ['password'], password => {
return hashSync(password, 10)
})
.after(async (buildObject, options) => {
return neode.create('User', buildObject)
})
Factory.define('user')
.extend('userWithoutEmailAddress')
.option('email', faker.internet.exampleEmail)
.after(async (buildObject, options) => {
const [user, email] = await Promise.all([
buildObject,
neode.create('EmailAddress', { email: options.email }),
])
await Promise.all([user.relateTo(email, 'primaryEmail'), email.relateTo(user, 'belongsTo')])
return user
})
Factory.define('post')
.option('categoryIds', [])
.option('categories', ['categoryIds'], categoryIds => {
if (categoryIds.length) return Promise.all(categoryIds.map(id => neode.find('Category', id)))
// there must be at least one category
return Promise.all([Factory.build('category')])
})
.option('tagIds', [])
.option('tags', ['tagIds'], tagIds => {
return Promise.all(tagIds.map(id => neode.find('Tag', id)))
})
.option('authorId', null)
.option('author', ['authorId'], authorId => {
if (authorId) return neode.find('User', authorId)
return Factory.build('user')
})
.option('pinnedBy', null)
.attrs({
id: uuid,
title: faker.lorem.sentence,
content: faker.lorem.paragraphs,
image: faker.image.unsplash.imageUrl,
visibility: 'public',
deleted: false,
imageBlurred: false,
imageAspectRatio: 1.333,
})
.attr('pinned', ['pinned'], pinned => {
// Convert false to null
return pinned || null
})
.attr('contentExcerpt', ['contentExcerpt', 'content'], (contentExcerpt, content) => {
return contentExcerpt || content
})
.attr('slug', ['slug', 'title'], (slug, title) => {
return slug || slugify(title, { lower: true })
})
.attr('language', ['language'], language => {
return language || 'en'
})
.after(async (buildObject, options) => {
const [post, author, categories, tags] = await Promise.all([
neode.create('Post', buildObject),
options.author,
options.categories,
options.tags,
])
await Promise.all([
post.relateTo(author, 'author'),
Promise.all(categories.map(c => c.relateTo(post, 'post'))),
Promise.all(tags.map(t => t.relateTo(post, 'post'))),
])
if (buildObject.pinned) {
const pinnedBy = await (options.pinnedBy || Factory.build('user', { role: 'admin' }))
await pinnedBy.relateTo(post, 'pinned')
}
return post
})
Factory.define('comment')
.option('postId', null)
.option('post', ['postId'], postId => {
if (postId) return neode.find('Post', postId)
return Factory.build('post')
})
.option('authorId', null)
.option('author', ['authorId'], authorId => {
if (authorId) return neode.find('User', authorId)
return Factory.build('user')
})
.attrs({
id: uuid,
content: faker.lorem.sentence,
})
.attr('contentExcerpt', ['contentExcerpt', 'content'], (contentExcerpt, content) => {
return contentExcerpt || content
})
.after(async (buildObject, options) => {
const [comment, author, post] = await Promise.all([
neode.create('Comment', buildObject),
options.author,
options.post,
])
await Promise.all([comment.relateTo(author, 'author'), comment.relateTo(post, 'post')])
return comment
})
Factory.define('donations')
.attr('id', uuid)
.attr('goal', 15000)
.attr('progress', 0)
.after((buildObject, options) => {
return neode.create('Donations', buildObject)
})
const emailDefaults = {
email: faker.internet.email,
verifiedAt: () => new Date().toISOString(),
}
Factory.define('emailAddress')
.attr(emailDefaults)
.after((buildObject, options) => {
return neode.create('EmailAddress', buildObject)
})
Factory.define('unverifiedEmailAddress')
.attr(emailDefaults)
.after((buildObject, options) => {
return neode.create('UnverifiedEmailAddress', buildObject)
})
Factory.define('location')
.attrs({
name: 'Germany',
namePT: 'Alemanha',
nameDE: 'Deutschland',
nameES: 'Alemania',
nameNL: 'Duitsland',
namePL: 'Niemcy',
nameFR: 'Allemagne',
nameIT: 'Germania',
nameEN: 'Germany',
id: 'country.10743216036480410',
type: 'country',
})
.after((buildObject, options) => {
return neode.create('Location', buildObject)
})
Factory.define('report').after((buildObject, options) => {
return neode.create('Report', buildObject)
})
Factory.define('tag')
.attrs({
name: '#human-connection',
})
.after((buildObject, options) => {
return neode.create('Tag', buildObject)
})
Factory.define('socialMedia')
.attrs({
url: 'https://mastodon.social/@Gargron',
})
.after((buildObject, options) => {
return neode.create('SocialMedia', buildObject)
})
export default Factory

View File

@ -18,6 +18,7 @@ export async function up(next) {
await transaction.rollback()
// eslint-disable-next-line no-console
console.log('rolled back')
throw new Error(error)
} finally {
session.close()
}

View File

@ -0,0 +1,42 @@
import { getDriver } from '../../db/neo4j'
export const description = `
This migration swaps the value stored in Location.lat with the value
of Location.lng. This is necessary as the values of lat and lng were
stored incorrectly. For example Hamburg, Germany, was stored with the
values lat=10.0 and lng=53.55, which is close to the horn of Africa,
but it is lat=53.55 and lng=10.0
`
const swap = async function(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
// Implement your migration here.
await transaction.run(`
MATCH (l:Location) WHERE NOT(l.lat IS NULL)
WITH l.lng AS longitude, l.lat AS latitude, l AS location
SET location.lat = longitude, location.lng = latitude
`)
await transaction.commit()
next()
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
await transaction.rollback()
// eslint-disable-next-line no-console
console.log('rolled back')
throw new Error(error)
} finally {
session.close()
}
}
export async function up(next) {
swap(next)
}
export async function down(next) {
swap(next)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +0,0 @@
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
const defaults = {
type: 'crowdfunding',
status: 'permanent',
}
args = {
...defaults,
...args,
}
return neodeInstance.create('Badge', args)
},
}
}

View File

@ -1,18 +0,0 @@
import uuid from 'uuid/v4'
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
const defaults = {
id: uuid(),
icon: 'img/badges/fundraisingbox_de_airship.svg',
name: 'Some category name',
}
args = {
...defaults,
...args,
}
return neodeInstance.create('Category', args)
},
}
}

View File

@ -1,38 +0,0 @@
import faker from 'faker'
import uuid from 'uuid/v4'
export default function create() {
return {
factory: async ({ args, neodeInstance, factoryInstance }) => {
const defaults = {
id: uuid(),
content: [faker.lorem.sentence(), faker.lorem.sentence()].join('. '),
}
args = {
...defaults,
...args,
}
args.contentExcerpt = args.contentExcerpt || args.content
let { post, postId } = args
delete args.post
delete args.postId
if (post && postId) throw new Error('You provided both post and postId')
if (postId) post = await neodeInstance.find('Post', postId)
post = post || (await factoryInstance.create('Post'))
let { author, authorId } = args
delete args.author
delete args.authorId
if (author && authorId) throw new Error('You provided both author and authorId')
if (authorId) author = await neodeInstance.find('User', authorId)
author = author || (await factoryInstance.create('User'))
delete args.author
const comment = await neodeInstance.create('Comment', args)
await comment.relateTo(post, 'post')
await comment.relateTo(author, 'author')
return comment
},
}
}

View File

@ -1,18 +0,0 @@
import uuid from 'uuid/v4'
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
const defaults = {
id: uuid(),
goal: 15000,
progress: 0,
}
args = {
...defaults,
...args,
}
return neodeInstance.create('Donations', args)
},
}
}

View File

@ -1,22 +0,0 @@
import faker from 'faker'
export function defaults({ args }) {
const defaults = {
email: faker.internet.email(),
verifiedAt: new Date().toISOString(),
}
args = {
...defaults,
...args,
}
return args
}
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
args = defaults({ args })
return neodeInstance.create('EmailAddress', args)
},
}
}

View File

@ -1,63 +0,0 @@
import { getDriver, getNeode } from '../db/neo4j'
const factories = {
Badge: require('./badges.js').default,
User: require('./users.js').default,
Post: require('./posts.js').default,
Comment: require('./comments.js').default,
Category: require('./categories.js').default,
Tag: require('./tags.js').default,
SocialMedia: require('./socialMedia.js').default,
Location: require('./locations.js').default,
EmailAddress: require('./emailAddresses.js').default,
UnverifiedEmailAddress: require('./unverifiedEmailAddresses.js').default,
Donations: require('./donations.js').default,
Report: require('./reports.js').default,
}
export const cleanDatabase = async (options = {}) => {
const { driver = getDriver() } = options
const session = driver.session()
try {
await session.writeTransaction(transaction => {
return transaction.run(
`
MATCH (everything)
DETACH DELETE everything
`,
)
})
} finally {
session.close()
}
}
export default function Factory(options = {}) {
const { neo4jDriver = getDriver(), neodeInstance = getNeode() } = options
const result = {
neo4jDriver,
factories,
lastResponse: null,
neodeInstance,
async create(node, args = {}) {
const { factory } = this.factories[node](args)
this.lastResponse = await factory({
args,
neodeInstance,
factoryInstance: this,
})
return this.lastResponse
},
async cleanDatabase() {
this.lastResponse = await cleanDatabase({
driver: this.neo4jDriver,
})
return this
},
}
result.create.bind(result)
result.cleanDatabase.bind(result)
return result
}

View File

@ -1,24 +0,0 @@
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
const defaults = {
name: 'Germany',
namePT: 'Alemanha',
nameDE: 'Deutschland',
nameES: 'Alemania',
nameNL: 'Duitsland',
namePL: 'Niemcy',
nameFR: 'Allemagne',
nameIT: 'Germania',
nameEN: 'Germany',
id: 'country.10743216036480410',
type: 'country',
}
args = {
...defaults,
...args,
}
return neodeInstance.create('Location', args)
},
}
}

View File

@ -1,89 +0,0 @@
import faker from 'faker'
import slugify from 'slug'
import uuid from 'uuid/v4'
export default function create() {
return {
factory: async ({ args, neodeInstance, factoryInstance }) => {
const defaults = {
id: uuid(),
title: faker.lorem.sentence(),
content: [
faker.lorem.sentence(),
faker.lorem.sentence(),
faker.lorem.sentence(),
faker.lorem.sentence(),
faker.lorem.sentence(),
].join('. '),
image: faker.image.unsplash.imageUrl(),
visibility: 'public',
deleted: false,
categoryIds: [],
imageBlurred: false,
imageAspectRatio: 1.333,
pinned: null,
}
args = {
...defaults,
...args,
}
// Convert false to null
args.pinned = args.pinned || null
args.slug = args.slug || slugify(args.title, { lower: true })
args.contentExcerpt = args.contentExcerpt || args.content
let { categories, categoryIds } = args
delete args.categories
delete args.categoryIds
if (categories && categoryIds) throw new Error('You provided both categories and categoryIds')
if (categoryIds)
categories = await Promise.all(categoryIds.map(id => neodeInstance.find('Category', id)))
categories = categories || (await Promise.all([factoryInstance.create('Category')]))
const { tagIds = [] } = args
delete args.tags
const tags = await Promise.all(
tagIds.map(t => {
return neodeInstance.find('Tag', t)
}),
)
let { author, authorId } = args
delete args.author
delete args.authorId
if (author && authorId) throw new Error('You provided both author and authorId')
if (authorId) author = await neodeInstance.find('User', authorId)
author = author || (await factoryInstance.create('User'))
const post = await neodeInstance.create('Post', args)
const { commentContent } = args
let comment
delete args.commentContent
if (commentContent)
comment = await factoryInstance.create('Comment', {
contentExcerpt: commentContent,
post,
author,
})
await post.relateTo(author, 'author')
if (comment) await post.relateTo(comment, 'comments')
if (args.pinned) {
args.pinnedAt = args.pinnedAt || new Date().toISOString()
if (!args.pinnedBy) {
const admin = await factoryInstance.create('User', {
role: 'admin',
updatedAt: new Date().toISOString(),
})
await admin.relateTo(post, 'pinned')
args.pinnedBy = admin
}
}
await Promise.all(categories.map(c => c.relateTo(post, 'post')))
await Promise.all(tags.map(t => t.relateTo(post, 'post')))
return post
},
}
}

View File

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

View File

@ -1,14 +0,0 @@
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
const defaults = {
url: 'https://mastodon.social/@Gargron',
}
args = {
...defaults,
...args,
}
return neodeInstance.create('SocialMedia', args)
},
}
}

View File

@ -1,12 +0,0 @@
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
const defaults = { name: '#human-connection' }
args = {
...defaults,
...args,
}
return neodeInstance.create('Tag', args)
},
}
}

View File

@ -1,10 +0,0 @@
import { defaults } from './emailAddresses.js'
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
args = defaults({ args })
return neodeInstance.create('UnverifiedEmailAddress', args)
},
}
}

View File

@ -1,44 +0,0 @@
import faker from 'faker'
import uuid from 'uuid/v4'
import encryptPassword from '../helpers/encryptPassword'
import slugify from 'slug'
export default function create() {
return {
factory: async ({ args, neodeInstance, factoryInstance }) => {
const defaults = {
id: uuid(),
name: faker.name.findName(),
email: faker.internet.email(),
password: '1234',
role: 'user',
avatar: faker.internet.avatar(),
about: faker.lorem.paragraph(),
termsAndConditionsAgreedVersion: '0.0.1',
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
allowEmbedIframes: false,
showShoutsPublicly: false,
locale: 'en',
}
defaults.slug = slugify(defaults.name, { lower: true })
args = {
...defaults,
...args,
}
args = await encryptPassword(args)
const user = await neodeInstance.create('User', args)
let email
if (typeof args.email === 'object') {
// probably a neode node
email = args.email
} else {
email = await factoryInstance.create('EmailAddress', { email: args.email })
}
await user.relateTo(email, 'primaryEmail')
await email.relateTo(user, 'belongsTo')
return user
},
}
}

View File

@ -1,9 +1,11 @@
import createServer from './server'
import CONFIG from './config'
const { app } = createServer()
const { server, httpServer } = createServer()
const url = new URL(CONFIG.GRAPHQL_URI)
app.listen({ port: url.port }, () => {
httpServer.listen({ port: url.port }, () => {
/* eslint-disable-next-line no-console */
console.log(`GraphQLServer ready at ${CONFIG.GRAPHQL_URI} 🚀`)
console.log(`🚀 Server ready at http://localhost:${url.port}${server.graphqlPath}`)
/* eslint-disable-next-line no-console */
console.log(`🚀 Subscriptions ready at ws://localhost:${url.port}${server.subscriptionsPath}`)
})

View File

@ -1,8 +1,7 @@
import Factory from '../factories/index'
import Factory, { cleanDatabase } from '../db/factories'
import { getDriver, getNeode } from '../db/neo4j'
import decode from './decode'
const factory = Factory()
const driver = getDriver()
const neode = getNeode()
@ -26,7 +25,7 @@ export const validAuthorizationHeader =
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc'
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('decode', () => {
@ -65,14 +64,19 @@ describe('decode', () => {
describe('and corresponding user in the database', () => {
let user
beforeEach(async () => {
user = await factory.create('User', {
role: 'user',
name: 'Jenny Rostock',
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg',
id: 'u3',
email: 'user@example.org',
slug: 'jenny-rostock',
})
user = await Factory.build(
'user',
{
role: 'user',
name: 'Jenny Rostock',
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg',
id: 'u3',
slug: 'jenny-rostock',
},
{
email: 'user@example.org',
},
)
})
it('returns user object except email', async () => {

View File

@ -3,14 +3,12 @@ import CONFIG from './../config'
// Generate an Access Token for the given User ID
export default function encode(user) {
const token = jwt.sign(user, CONFIG.JWT_SECRET, {
const { id, name, slug } = user
const token = jwt.sign({ id, name, slug }, CONFIG.JWT_SECRET, {
expiresIn: '1d',
issuer: CONFIG.GRAPHQL_URI,
audience: CONFIG.CLIENT_URI,
subject: user.id.toString(),
})
// jwt.verifySignature(token, CONFIG.JWT_SECRET, (err, data) => {
// console.log('token verification:', err, data)
// })
return token
}

View File

@ -0,0 +1,62 @@
import encode from './encode'
import jwt from 'jsonwebtoken'
import CONFIG from './../config'
describe('encode', () => {
let payload
beforeEach(() => {
payload = {
name: 'Some body',
slug: 'some-body',
id: 'some-id',
}
})
it('encodes a valided JWT bearer token', () => {
const token = encode(payload)
expect(token.split('.')).toHaveLength(3)
const decoded = jwt.verify(token, CONFIG.JWT_SECRET)
expect(decoded).toEqual({
name: 'Some body',
slug: 'some-body',
id: 'some-id',
sub: 'some-id',
aud: expect.any(String),
iss: expect.any(String),
iat: expect.any(Number),
exp: expect.any(Number),
})
})
describe('given sensitive data', () => {
beforeEach(() => {
payload = {
...payload,
email: 'none-of-your-business@example.org',
password: 'topsecret',
}
})
it('does not encode sensitive data', () => {
const token = encode(payload)
expect(payload).toEqual({
email: 'none-of-your-business@example.org',
password: 'topsecret',
name: 'Some body',
slug: 'some-body',
id: 'some-id',
})
const decoded = jwt.verify(token, CONFIG.JWT_SECRET)
expect(decoded).toEqual({
name: 'Some body',
slug: 'some-body',
id: 'some-id',
sub: 'some-id',
aud: expect.any(String),
iss: expect.any(String),
iat: expect.any(Number),
exp: expect.any(Number),
})
})
})
})

View File

@ -1,5 +1,5 @@
import { gql } from '../../helpers/jest'
import Factory from '../../factories'
import { cleanDatabase } from '../../db/factories'
import { createTestClient } from 'apollo-server-testing'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
@ -9,7 +9,6 @@ let query
let mutate
let hashtagingUser
let authenticatedUser
const factory = Factory()
const driver = getDriver()
const neode = getNeode()
const categoryIds = ['cat9']
@ -48,13 +47,18 @@ beforeAll(() => {
})
beforeEach(async () => {
hashtagingUser = await neode.create('User', {
id: 'you',
name: 'Al Capone',
slug: 'al-capone',
email: 'test@example.org',
password: '1234',
})
hashtagingUser = await neode.create(
'User',
{
id: 'you',
name: 'Al Capone',
slug: 'al-capone',
},
{
password: '1234',
email: 'test@example.org',
},
)
await neode.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
@ -63,7 +67,7 @@ beforeEach(async () => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('hashtags', () => {

View File

@ -1,13 +1,24 @@
import extractMentionedUsers from './mentions/extractMentionedUsers'
import { validateNotifyUsers } from '../validation/validationMiddleware'
import { pubsub, NOTIFICATION_ADDED } from '../../server'
const publishNotifications = async (...promises) => {
const notifications = await Promise.all(promises)
notifications
.flat()
.forEach(notificationAdded => pubsub.publish(NOTIFICATION_ADDED, { notificationAdded }))
}
const debug = require('debug')('human-connection-backend:notificationsMiddleware')
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
const idsOfUsers = extractMentionedUsers(args.content)
const post = await resolve(root, args, context, resolveInfo)
if (post && idsOfUsers && idsOfUsers.length)
await notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context)
if (post) {
await publishNotifications(
notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context),
)
}
return post
}
@ -17,10 +28,10 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI
const comment = await resolve(root, args, context, resolveInfo)
const [postAuthor] = await postAuthorOfComment(comment.id, { context })
idsOfUsers = idsOfUsers.filter(id => id !== postAuthor.id)
if (idsOfUsers && idsOfUsers.length)
await notifyUsersOfMention('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context)
if (context.user.id !== postAuthor.id)
await notifyUsersOfComment('Comment', comment.id, postAuthor.id, 'commented_on_post', context)
await publishNotifications(
notifyUsersOfMention('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context),
notifyUsersOfComment('Comment', comment.id, postAuthor.id, 'commented_on_post', context),
)
return comment
}
@ -46,6 +57,7 @@ const postAuthorOfComment = async (commentId, { context }) => {
}
const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
if (!(idsOfUsers && idsOfUsers.length)) return []
await validateNotifyUsers(label, reason)
let mentionedCypher
switch (reason) {
@ -56,6 +68,7 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
WHERE user.id in $idsOfUsers
AND NOT (user)-[:BLOCKED]-(author)
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
WITH post AS resource, notification, user
`
break
}
@ -72,16 +85,29 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
}
}
mentionedCypher += `
WITH notification, user, resource,
[(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors,
[(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts
WITH resource, user, notification, authors, posts,
resource {.*, __typename: labels(resource)[0], author: authors[0], post: posts[0]} AS finalResource
SET notification.read = FALSE
// Wolle SET ( CASE WHEN notification.createdAt IS NULL THEN notification END ).createdAt = toString(datetime())
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
SET notification.updatedAt = toString(datetime())
RETURN notification {.*, from: finalResource, to: properties(user)}
`
const session = context.driver.session()
try {
await session.writeTransaction(transaction => {
return transaction.run(mentionedCypher, { id, idsOfUsers, reason })
const writeTxResultPromise = session.writeTransaction(async transaction => {
const notificationTransactionResponse = await transaction.run(mentionedCypher, {
id,
idsOfUsers,
reason,
})
return notificationTransactionResponse.records.map(record => record.get('notification'))
})
try {
const notifications = await writeTxResultPromise
return notifications
} catch (error) {
debug(error)
} finally {
@ -90,23 +116,29 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
}
const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, context) => {
if (context.user.id === postAuthorId) return []
await validateNotifyUsers(label, reason)
const session = context.driver.session()
const writeTxResultPromise = await session.writeTransaction(async transaction => {
const notificationTransactionResponse = await transaction.run(
`
MATCH (postAuthor:User {id: $postAuthorId})-[:WROTE]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User)
WHERE NOT (postAuthor)-[:BLOCKED]-(commenter)
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(postAuthor)
SET notification.read = FALSE
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
SET notification.updatedAt = toString(datetime())
WITH notification, postAuthor, post,
comment {.*, __typename: labels(comment)[0], author: properties(commenter), post: post {.*, author: properties(postAuthor) } } AS finalResource
RETURN notification {.*, from: finalResource, to: properties(postAuthor)}
`,
{ commentId, postAuthorId, reason },
)
return notificationTransactionResponse.records.map(record => record.get('notification'))
})
try {
await session.writeTransaction(async transaction => {
await transaction.run(
`
MATCH (postAuthor:User {id: $postAuthorId})-[:WROTE]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User)
WHERE NOT (postAuthor)-[:BLOCKED]-(commenter)
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(postAuthor)
SET notification.read = FALSE
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
SET notification.updatedAt = toString(datetime())
`,
{ commentId, postAuthorId, reason },
)
})
const notifications = await writeTxResultPromise
return notifications
} catch (error) {
debug(error)
} finally {

View File

@ -1,11 +1,11 @@
import { gql } from '../../helpers/jest'
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../factories'
import { createTestClient } from 'apollo-server-testing'
import { getDriver } from '../../db/neo4j'
import createServer from '../../server'
import createServer, { pubsub } from '../../server'
let server, query, mutate, notifiedUser, authenticatedUser
const factory = Factory()
let publishSpy
const driver = getDriver()
const categoryIds = ['cat9']
const createPostMutation = gql`
@ -46,7 +46,8 @@ const fileReportMutation = gql`
`
beforeAll(async () => {
await factory.cleanDatabase()
await cleanDatabase()
publishSpy = jest.spyOn(pubsub, 'publish')
const createServerResult = createServer({
context: () => {
return {
@ -62,22 +63,29 @@ beforeAll(async () => {
})
beforeEach(async () => {
notifiedUser = await factory.create('User', {
id: 'you',
name: 'Al Capone',
slug: 'al-capone',
email: 'test@example.org',
password: '1234',
})
await factory.create('Category', {
publishSpy.mockClear()
notifiedUser = await Factory.build(
'user',
{
id: 'you',
name: 'Al Capone',
slug: 'al-capone',
},
{
email: 'test@example.org',
password: '1234',
},
)
await Factory.build('category', {
id: 'cat9',
name: 'Democracy & Politics',
slug: 'democracy-politics',
icon: 'university',
})
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('notifications', () => {
@ -173,13 +181,18 @@ describe('notifications', () => {
describe('commenter is not me', () => {
beforeEach(async () => {
commentContent = 'Commenters comment.'
commentAuthor = await factory.create('User', {
id: 'commentAuthor',
name: 'Mrs Comment',
slug: 'mrs-comment',
email: 'commentauthor@example.org',
password: '1234',
})
commentAuthor = await Factory.build(
'user',
{
id: 'commentAuthor',
name: 'Mrs Comment',
slug: 'mrs-comment',
},
{
email: 'commentauthor@example.org',
password: '1234',
},
)
})
it('sends me a notification', async () => {
@ -254,13 +267,18 @@ describe('notifications', () => {
})
beforeEach(async () => {
postAuthor = await factory.create('User', {
id: 'postAuthor',
name: 'Mrs Post',
slug: 'mrs-post',
email: 'post-author@example.org',
password: '1234',
})
postAuthor = await Factory.build(
'user',
{
id: 'postAuthor',
name: 'Mrs Post',
slug: 'mrs-post',
},
{
email: 'post-author@example.org',
password: '1234',
},
)
})
describe('mentions me in a post', () => {
@ -275,7 +293,15 @@ describe('notifications', () => {
await createPostAction()
const expectedContent =
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
const expected = expect.objectContaining({
await expect(
query({
query: notificationQuery,
variables: {
read: false,
},
}),
).resolves.toMatchObject({
errors: undefined,
data: {
notifications: [
{
@ -291,15 +317,22 @@ describe('notifications', () => {
],
},
})
})
await expect(
query({
query: notificationQuery,
variables: {
read: false,
},
it('publishes `NOTIFICATION_ADDED` to me', async () => {
await createPostAction()
expect(publishSpy).toHaveBeenCalledWith(
'NOTIFICATION_ADDED',
expect.objectContaining({
notificationAdded: expect.objectContaining({
reason: 'mentioned_in_post',
to: expect.objectContaining({
id: 'you',
}),
}),
}),
).resolves.toEqual(expected)
)
expect(publishSpy).toHaveBeenCalledTimes(1)
})
describe('updates the post and mentions me again', () => {
@ -445,6 +478,11 @@ describe('notifications', () => {
}),
).resolves.toEqual(expected)
})
it('does not publish `NOTIFICATION_ADDED`', async () => {
await createPostAction()
expect(publishSpy).not.toHaveBeenCalled()
})
})
})
@ -458,23 +496,33 @@ describe('notifications', () => {
beforeEach(async () => {
commentContent =
'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.'
commentAuthor = await factory.create('User', {
id: 'commentAuthor',
name: 'Mrs Comment',
slug: 'mrs-comment',
email: 'comment-author@example.org',
password: '1234',
})
commentAuthor = await Factory.build(
'user',
{
id: 'commentAuthor',
name: 'Mrs Comment',
slug: 'mrs-comment',
},
{
email: 'comment-author@example.org',
password: '1234',
},
)
})
it('sends only one notification with reason mentioned_in_comment', async () => {
postAuthor = await factory.create('User', {
id: 'MrAuthor',
name: 'Mr Author',
slug: 'mr-author',
email: 'mr-author@example.org',
password: '1234',
})
postAuthor = await Factory.build(
'user',
{
id: 'MrPostAuthor',
name: 'Mr Author',
slug: 'mr-author',
},
{
email: 'post-author@example.org',
password: '1234',
},
)
await createCommentOnPostAction()
const expected = expect.objectContaining({
@ -511,7 +559,7 @@ describe('notifications', () => {
})
it('sends only one notification with reason commented_on_post, no notification with reason mentioned_in_comment', async () => {
await createCommentOnPostAction()
const expected = expect.objectContaining({
const expected = {
data: {
notifications: [
{
@ -526,7 +574,7 @@ describe('notifications', () => {
},
],
},
})
}
await expect(
query({
@ -535,7 +583,7 @@ describe('notifications', () => {
read: false,
},
}),
).resolves.toEqual(expected)
).resolves.toMatchObject(expected, { errors: undefined })
})
})
@ -544,21 +592,22 @@ describe('notifications', () => {
await postAuthor.relateTo(notifiedUser, 'blocked')
commentContent =
'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.'
commentAuthor = await factory.create('User', {
id: 'commentAuthor',
name: 'Mrs Comment',
slug: 'mrs-comment',
email: 'comment-author@example.org',
password: '1234',
})
commentAuthor = await Factory.build(
'user',
{
id: 'commentAuthor',
name: 'Mrs Comment',
slug: 'mrs-comment',
},
{
email: 'comment-author@example.org',
password: '1234',
},
)
})
it('sends no notification', async () => {
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: { notifications: [] },
})
await expect(
query({
query: notificationQuery,
@ -566,7 +615,26 @@ describe('notifications', () => {
read: false,
},
}),
).resolves.toEqual(expected)
).resolves.toMatchObject({
data: { notifications: [] },
errors: undefined,
})
})
it('does not publish `NOTIFICATION_ADDED` to authenticated user', async () => {
await createCommentOnPostAction()
expect(publishSpy).toHaveBeenCalledWith(
'NOTIFICATION_ADDED',
expect.objectContaining({
notificationAdded: expect.objectContaining({
reason: 'commented_on_post',
to: expect.objectContaining({
id: 'postAuthor', // that's expected, it's not me but the post author
}),
}),
}),
)
expect(publishSpy).toHaveBeenCalledTimes(1)
})
})
})
@ -628,7 +696,7 @@ describe('notifications', () => {
describe('user', () => {
it('sends me a notification for filing a report on a user', async () => {
await factory.create('User', reportedUserOrAuthorData)
await Factory.create('User', reportedUserOrAuthorData)
resourceId = 'reportedUser'
await fileReportAction()
@ -653,7 +721,7 @@ describe('notifications', () => {
beforeEach(async () => {
title = 'My post'
postContent = 'My post content.'
postAuthor = await factory.create('User', reportedUserOrAuthorData)
postAuthor = await Factory.create('User', reportedUserOrAuthorData)
})
describe('post', () => {
@ -682,7 +750,7 @@ describe('notifications', () => {
describe('comment', () => {
beforeEach(async () => {
commentContent = "Commenter's comment."
commentAuthor = await factory.create('User', {
commentAuthor = await Factory.create('User', {
id: 'commentAuthor',
name: 'Mrs Comment',
slug: 'mrs-comment',

View File

@ -1,10 +1,9 @@
import { gql } from '../helpers/jest'
import Factory from '../factories'
import { cleanDatabase } from '../db/factories'
import { getNeode, getDriver } from '../db/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../server'
const factory = Factory()
const neode = getNeode()
const driver = getDriver()
@ -27,7 +26,7 @@ beforeEach(async () => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('Query', () => {

View File

@ -152,6 +152,7 @@ export default shield(
User: {
email: or(isMyOwn, isAdmin),
},
Report: isModerator,
},
{
debug,

View File

@ -1,26 +1,17 @@
import { createTestClient } from 'apollo-server-testing'
import createServer from '../server'
import Factory from '../factories'
import Factory, { cleanDatabase } from '../db/factories'
import { gql } from '../helpers/jest'
import { getDriver, getNeode } from '../db/neo4j'
const factory = Factory()
const instance = getNeode()
const driver = getDriver()
let query, authenticatedUser, owner, anotherRegularUser, administrator, variables, moderator
const userQuery = gql`
query($name: String) {
User(name: $name) {
email
}
}
`
describe('authorization', () => {
beforeAll(async () => {
await factory.cleanDatabase()
await cleanDatabase()
const { server } = createServer({
context: () => ({
driver,
@ -31,44 +22,73 @@ describe('authorization', () => {
query = createTestClient(server).query
})
describe('given two existing users', () => {
afterEach(async () => {
await cleanDatabase()
})
describe('given an owner, an other user, an admin, a moderator', () => {
beforeEach(async () => {
;[owner, anotherRegularUser, administrator, moderator] = await Promise.all([
factory.create('User', {
email: 'owner@example.org',
name: 'Owner',
password: 'iamtheowner',
}),
factory.create('User', {
email: 'another.regular.user@example.org',
name: 'Another Regular User',
password: 'else',
}),
factory.create('User', {
email: 'admin@example.org',
name: 'Admin',
password: 'admin',
role: 'admin',
}),
factory.create('User', {
email: 'moderator@example.org',
name: 'Moderator',
password: 'moderator',
role: 'moderator',
}),
Factory.build(
'user',
{
name: 'Owner',
},
{
email: 'owner@example.org',
password: 'iamtheowner',
},
),
Factory.build(
'user',
{
name: 'Another Regular User',
},
{
email: 'another.regular.user@example.org',
password: 'else',
},
),
Factory.build(
'user',
{
name: 'Admin',
role: 'admin',
},
{
email: 'admin@example.org',
password: 'admin',
},
),
Factory.build(
'user',
{
name: 'Moderator',
role: 'moderator',
},
{
email: 'moderator@example.org',
password: 'moderator',
},
),
])
variables = {}
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('access email address', () => {
const userQuery = gql`
query($name: String) {
User(name: $name) {
email
}
}
`
describe('unauthenticated', () => {
beforeEach(() => {
authenticatedUser = null
})
it("throws an error and does not expose the owner's email address", async () => {
await expect(
query({ query: userQuery, variables: { name: 'Owner' } }),
@ -124,7 +144,7 @@ describe('authorization', () => {
})
})
describe('administrator', () => {
describe('as an administrator', () => {
beforeEach(async () => {
authenticatedUser = await administrator.toJson()
})

View File

@ -1,11 +1,9 @@
import Factory from '../factories'
import Factory, { cleanDatabase } from '../db/factories'
import { gql } from '../helpers/jest'
import { getNeode, getDriver } from '../db/neo4j'
import createServer from '../server'
import { createTestClient } from 'apollo-server-testing'
const factory = Factory()
let mutate
let authenticatedUser
let variables
@ -28,14 +26,18 @@ beforeAll(() => {
beforeEach(async () => {
variables = {}
const admin = await factory.create('User', {
const admin = await Factory.build('user', {
role: 'admin',
})
await factory.create('User', {
email: 'someone@example.org',
password: '1234',
})
await factory.create('Category', {
await Factory.build(
'user',
{},
{
email: 'someone@example.org',
password: '1234',
},
)
await Factory.build('category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
@ -44,7 +46,7 @@ beforeEach(async () => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('slugifyMiddleware', () => {
@ -84,12 +86,17 @@ describe('slugifyMiddleware', () => {
describe('if slug exists', () => {
beforeEach(async () => {
await factory.create('Post', {
title: 'Pre-existing post',
slug: 'pre-existing-post',
content: 'as Someone else content',
categoryIds,
})
await Factory.build(
'post',
{
title: 'Pre-existing post',
slug: 'pre-existing-post',
content: 'as Someone else content',
},
{
categoryIds,
},
)
})
it('chooses another slug', async () => {
@ -146,7 +153,7 @@ describe('slugifyMiddleware', () => {
\`\`\`
Learn how to setup the database here:
https://docs.human-connection.org/human-connection/neo4j
https://docs.human-connection.org/human-connection/backend#database-indices-and-constraints
`)
}
})
@ -190,7 +197,7 @@ describe('slugifyMiddleware', () => {
describe('given a user has signed up with their email address', () => {
beforeEach(async () => {
await factory.create('EmailAddress', {
await Factory.build('emailAddress', {
email: '123@example.org',
nonce: '123456',
verifiedAt: null,
@ -214,7 +221,7 @@ describe('slugifyMiddleware', () => {
describe('if slug exists', () => {
beforeEach(async () => {
await factory.create('User', {
await Factory.build('user', {
name: 'I am a user',
slug: 'i-am-a-user',
})

View File

@ -1,10 +1,9 @@
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
const factory = Factory()
const neode = getNeode()
const driver = getDriver()
@ -18,13 +17,18 @@ const action = () => {
beforeAll(async () => {
// For performance reasons we do this only once
const users = await Promise.all([
factory.create('User', { id: 'u1', role: 'user' }),
factory.create('User', {
id: 'm1',
role: 'moderator',
password: '1234',
}),
factory.create('User', {
Factory.build('user', { id: 'u1', role: 'user' }),
Factory.build(
'user',
{
id: 'm1',
role: 'moderator',
},
{
password: '1234',
},
),
Factory.build('user', {
id: 'u2',
role: 'user',
name: 'Offensive Name',
@ -45,48 +49,73 @@ beforeAll(async () => {
await Promise.all([
user.relateTo(troll, 'following'),
factory.create('Post', {
author: user,
id: 'p1',
title: 'Deleted post',
slug: 'deleted-post',
deleted: true,
categoryIds,
}),
factory.create('Post', {
author: user,
id: 'p3',
title: 'Publicly visible post',
slug: 'publicly-visible-post',
deleted: false,
categoryIds,
}),
Factory.build(
'post',
{
id: 'p1',
title: 'Deleted post',
slug: 'deleted-post',
deleted: true,
},
{
author: user,
categoryIds,
},
),
Factory.build(
'post',
{
id: 'p3',
title: 'Publicly visible post',
slug: 'publicly-visible-post',
deleted: false,
},
{
author: user,
categoryIds,
},
),
])
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',
}),
Factory.build(
'comment',
{
id: 'c2',
content: 'Enabled comment on public post',
},
{
author: user,
postId: 'p3',
},
),
Factory.build(
'post',
{
id: 'p2',
title: 'Disabled post',
content: 'This is an offensive post content',
contentExcerpt: 'This is an offensive post content',
image: '/some/offensive/image.jpg',
deleted: false,
},
{
author: troll,
categoryIds,
},
),
Factory.build(
'comment',
{
id: 'c1',
content: 'Disabled comment',
contentExcerpt: 'Disabled comment',
},
{
author: troll,
postId: 'p3',
},
),
])
const { server } = createServer({
@ -105,9 +134,9 @@ beforeAll(async () => {
const trollingComment = resources[2]
const reports = await Promise.all([
factory.create('Report'),
factory.create('Report'),
factory.create('Report'),
Factory.build('report'),
Factory.build('report'),
Factory.build('report'),
])
const reportAgainstTroll = reports[0]
const reportAgainstTrollingPost = reports[1]
@ -154,7 +183,7 @@ beforeAll(async () => {
})
afterAll(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('softDeleteMiddleware', () => {

View File

@ -1,10 +1,9 @@
import { gql } from '../../helpers/jest'
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { getNeode, getDriver } from '../../db/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server'
const factory = Factory()
const neode = getNeode()
const driver = getDriver()
let authenticatedUser,
@ -59,7 +58,7 @@ const reportMutation = gql`
reasonCategory: $reasonCategory
reasonDescription: $reasonDescription
) {
id
reportId
}
}
`
@ -94,14 +93,14 @@ beforeAll(() => {
beforeEach(async () => {
users = await Promise.all([
factory.create('User', {
Factory.build('user', {
id: 'reporting-user',
}),
factory.create('User', {
Factory.build('user', {
id: 'moderating-user',
role: 'moderator',
}),
factory.create('User', {
Factory.build('user', {
id: 'commenting-user',
}),
])
@ -119,20 +118,30 @@ beforeEach(async () => {
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',
}),
Factory.build(
'post',
{
id: 'offensive-post',
},
{
authorId: 'moderating-user',
},
),
Factory.build(
'post',
{
id: 'post-4-commenting',
},
{
authorId: 'commenting-user',
},
),
])
offensivePost = posts[0]
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('validateCreateComment', () => {
@ -182,10 +191,15 @@ describe('validateCreateComment', () => {
describe('validateUpdateComment', () => {
let updateCommentVariables
beforeEach(async () => {
await factory.create('Comment', {
id: 'comment-id',
authorId: 'commenting-user',
})
await Factory.build(
'comment',
{
id: 'comment-id',
},
{
authorId: 'commenting-user',
},
)
updateCommentVariables = {
id: 'whatever',
content: '',
@ -328,7 +342,7 @@ describe('validateReport', () => {
describe('validateReview', () => {
beforeEach(async () => {
const reportAgainstModerator = await factory.create('Report')
const reportAgainstModerator = await Factory.build('report')
await Promise.all([
reportAgainstModerator.relateTo(reportingUser, 'filed', {
...reportVariables,
@ -370,7 +384,7 @@ describe('validateReview', () => {
})
it('throws an error if a moderator tries to review their own resource(Post|Comment)', async () => {
const reportAgainstOffensivePost = await factory.create('Report')
const reportAgainstOffensivePost = await Factory.build('report')
await Promise.all([
reportAgainstOffensivePost.relateTo(reportingUser, 'filed', {
...reportVariables,
@ -389,7 +403,7 @@ describe('validateReview', () => {
describe('moderate a resource that is not a (Comment|Post|User) ', () => {
beforeEach(async () => {
await Promise.all([factory.create('Tag', { id: 'tag-id' })])
await Promise.all([Factory.build('tag', { id: 'tag-id' })])
})
it('returns null', async () => {
@ -419,7 +433,7 @@ describe('validateReview', () => {
id: 'updating-user',
name: 'John Doughnut',
}
updatingUser = await factory.create('User', userParams)
updatingUser = await Factory.build('user', userParams)
authenticatedUser = await updatingUser.toJson()
})

View File

@ -1,4 +1,4 @@
import uuid from 'uuid/v4'
import { v4 as uuid } from 'uuid'
export default {
id: { type: 'string', primary: true, default: uuid },

View File

@ -1,4 +1,4 @@
import uuid from 'uuid/v4'
import { v4 as uuid } from 'uuid'
export default {
id: { type: 'string', primary: true, default: uuid },

View File

@ -1,4 +1,4 @@
import uuid from 'uuid/v4'
import { v4 as uuid } from 'uuid'
export default {
id: { type: 'string', primary: true, default: uuid },

View File

@ -1,4 +1,4 @@
import uuid from 'uuid/v4'
import { v4 as uuid } from 'uuid'
export default {
id: { type: 'string', primary: true, default: uuid },
@ -38,5 +38,4 @@ export default {
},
},
pinned: { type: 'boolean', default: null, valid: [null, true] },
pinnedAt: { type: 'string', isoDate: true },
}

View File

@ -1,4 +1,4 @@
import uuid from 'uuid/v4'
import { v4 as uuid } from 'uuid'
export default {
id: { type: 'string', primary: true, default: uuid },

View File

@ -1,4 +1,4 @@
import uuid from 'uuid/v4'
import { v4 as uuid } from 'uuid'
export default {
id: { type: 'string', primary: true, default: uuid },

View File

@ -1,4 +1,4 @@
import uuid from 'uuid/v4'
import { v4 as uuid } from 'uuid'
export default {
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests

View File

@ -1,11 +1,10 @@
import Factory from '../factories'
import { cleanDatabase } from '../db/factories'
import { getNeode } from '../db/neo4j'
const factory = Factory()
const neode = getNeode()
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('role', () => {
@ -47,7 +46,7 @@ describe('slug', () => {
\`\`\`
Learn how to setup the database here:
https://docs.human-connection.org/human-connection/neo4j
https://docs.human-connection.org/human-connection/backend#database-indices-and-constraints
`)
}
})

View File

@ -1,4 +1,4 @@
import uuid from 'uuid/v4'
import { v4 as uuid } from 'uuid'
import Resolver from './helpers/Resolver'
export default {

View File

@ -1,4 +1,4 @@
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server'
@ -6,12 +6,11 @@ import { getNeode, getDriver } from '../../db/neo4j'
const driver = getDriver()
const neode = getNeode()
const factory = Factory()
let variables, mutate, authenticatedUser, commentAuthor, newlyCreatedComment
beforeAll(async () => {
await factory.cleanDatabase()
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
@ -33,7 +32,7 @@ beforeEach(async () => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
const createCommentMutation = gql`
@ -48,18 +47,28 @@ const createCommentMutation = gql`
}
`
const setupPostAndComment = async () => {
commentAuthor = await factory.create('User')
await factory.create('Post', {
id: 'p1',
content: 'Post to be commented',
categoryIds: ['cat9'],
})
newlyCreatedComment = await factory.create('Comment', {
id: 'c456',
postId: 'p1',
author: commentAuthor,
content: 'Comment to be deleted',
})
commentAuthor = await Factory.build('user')
await Factory.build(
'post',
{
id: 'p1',
content: 'Post to be commented',
},
{
categoryIds: ['cat9'],
},
)
newlyCreatedComment = await Factory.build(
'comment',
{
id: 'c456',
content: 'Comment to be deleted',
},
{
postId: 'p1',
author: commentAuthor,
},
)
variables = {
...variables,
id: 'c456',
@ -88,7 +97,7 @@ describe('CreateComment', () => {
describe('given a post', () => {
beforeEach(async () => {
await factory.create('Post', { categoryIds: ['cat9'], id: 'p1' })
await Factory.build('post', { id: 'p1' }, { categoryIds: ['cat9'] })
variables = {
...variables,
postId: 'p1',
@ -141,7 +150,7 @@ describe('UpdateComment', () => {
describe('authenticated but not the author', () => {
beforeEach(async () => {
const randomGuy = await factory.create('User')
const randomGuy = await Factory.build('user')
authenticatedUser = await randomGuy.toJson()
})
@ -233,7 +242,7 @@ describe('DeleteComment', () => {
describe('authenticated but not the author', () => {
beforeEach(async () => {
const randomGuy = await factory.create('User')
const randomGuy = await Factory.build('user')
authenticatedUser = await randomGuy.toJson()
})

View File

@ -1,11 +1,10 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
let mutate, query, authenticatedUser, variables
const factory = Factory()
const instance = getNeode()
const driver = getDriver()
@ -33,7 +32,7 @@ const donationsQuery = gql`
describe('donations', () => {
let currentUser, newlyCreatedDonations
beforeAll(async () => {
await factory.cleanDatabase()
await cleanDatabase()
authenticatedUser = undefined
const { server } = createServer({
context: () => {
@ -50,11 +49,11 @@ describe('donations', () => {
beforeEach(async () => {
variables = {}
newlyCreatedDonations = await factory.create('Donations')
newlyCreatedDonations = await Factory.build('donations')
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('query for donations', () => {
@ -68,7 +67,7 @@ describe('donations', () => {
describe('authenticated', () => {
beforeEach(async () => {
currentUser = await factory.create('User', {
currentUser = await Factory.build('user', {
id: 'normal-user',
role: 'user',
})
@ -102,7 +101,7 @@ describe('donations', () => {
describe('authenticated', () => {
describe('as a normal user', () => {
beforeEach(async () => {
currentUser = await factory.create('User', {
currentUser = await Factory.build('user', {
id: 'normal-user',
role: 'user',
})
@ -121,7 +120,7 @@ describe('donations', () => {
describe('as a moderator', () => {
beforeEach(async () => {
currentUser = await factory.create('User', {
currentUser = await Factory.build('user', {
id: 'moderator',
role: 'moderator',
})
@ -140,7 +139,7 @@ describe('donations', () => {
describe('as an admin', () => {
beforeEach(async () => {
currentUser = await factory.create('User', {
currentUser = await Factory.build('user', {
id: 'admin',
role: 'admin',
})

View File

@ -1,10 +1,9 @@
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
const factory = Factory()
const neode = getNeode()
let mutate
@ -31,7 +30,7 @@ beforeAll(() => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('AddEmailAddress', () => {
@ -63,7 +62,7 @@ describe('AddEmailAddress', () => {
describe('authenticated', () => {
beforeEach(async () => {
user = await factory.create('User', { id: '567', email: 'user@example.org' })
user = await Factory.build('user', { id: '567' }, { email: 'user@example.org' })
authenticatedUser = await user.toJson()
})
@ -110,7 +109,7 @@ describe('AddEmailAddress', () => {
describe('if another `UnverifiedEmailAddress` node already exists with that email', () => {
it('throws no unique constraint violation error', async () => {
await factory.create('UnverifiedEmailAddress', {
await Factory.build('unverifiedEmailAddress', {
createdAt: '2019-09-24T14:00:01.565Z',
email: 'new-email@example.org',
})
@ -128,7 +127,7 @@ describe('AddEmailAddress', () => {
describe('but if another user owns an `EmailAddress` already with that email', () => {
it('throws UserInputError because of unique constraints', async () => {
await factory.create('User', { email: 'new-email@example.org' })
await Factory.build('user', {}, { email: 'new-email@example.org' })
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { AddEmailAddress: null },
errors: [{ message: 'A user account with this email already exists.' }],
@ -169,7 +168,7 @@ describe('VerifyEmailAddress', () => {
describe('authenticated', () => {
beforeEach(async () => {
user = await factory.create('User', { id: '567', email: 'user@example.org' })
user = await Factory.build('user', { id: '567' }, { email: 'user@example.org' })
authenticatedUser = await user.toJson()
})
@ -185,7 +184,7 @@ describe('VerifyEmailAddress', () => {
describe('given a `UnverifiedEmailAddress`', () => {
let emailAddress
beforeEach(async () => {
emailAddress = await factory.create('UnverifiedEmailAddress', {
emailAddress = await Factory.build('unverifiedEmailAddress', {
nonce: 'abcdef',
verifiedAt: null,
createdAt: new Date().toISOString(),
@ -281,7 +280,7 @@ describe('VerifyEmailAddress', () => {
describe('Edge case: In the meantime someone created an `EmailAddress` node with the given email', () => {
beforeEach(async () => {
await factory.create('EmailAddress', { email: 'to-be-verified@example.org' })
await Factory.build('emailAddress', { email: 'to-be-verified@example.org' })
})
it('throws UserInputError because of unique constraints', async () => {

View File

@ -1,24 +1,27 @@
import { createWriteStream } from 'fs'
import path from 'path'
import slug from 'slug'
import { v4 as uuid } from 'uuid'
const storeUpload = ({ createReadStream, fileLocation }) =>
new Promise((resolve, reject) =>
const localFileUpload = async ({ createReadStream, uniqueFilename }) => {
await new Promise((resolve, reject) =>
createReadStream()
.pipe(createWriteStream(`public${fileLocation}`))
.pipe(createWriteStream(`public${uniqueFilename}`))
.on('finish', resolve)
.on('error', reject),
)
return uniqueFilename
}
export default async function fileUpload(params, { file, url }, uploadCallback = storeUpload) {
export default async function fileUpload(params, { file, url }, uploadCallback = localFileUpload) {
const upload = params[file]
if (upload) {
const { createReadStream, filename } = await upload
const { name } = path.parse(filename)
const fileLocation = `/uploads/${Date.now()}-${slug(name)}`
await uploadCallback({ createReadStream, fileLocation })
const { name, ext } = path.parse(filename)
const uniqueFilename = `/uploads/${uuid()}-${slug(name)}${ext}`
const location = await uploadCallback({ createReadStream, uniqueFilename })
delete params[file]
params[url] = fileLocation
params[url] = location
}
return params

View File

@ -1,5 +1,7 @@
import fileUpload from '.'
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}'
describe('fileUpload', () => {
let params
let uploadCallback
@ -13,7 +15,7 @@ describe('fileUpload', () => {
createReadStream: jest.fn(),
},
}
uploadCallback = jest.fn()
uploadCallback = jest.fn(({ uniqueFilename }) => uniqueFilename)
})
it('calls uploadCallback', async () => {
@ -24,20 +26,13 @@ describe('fileUpload', () => {
describe('file name', () => {
it('saves the upload url in params[url]', async () => {
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
expect(params.attribute).toMatch(/^\/uploads\/\d+-avatar$/)
})
it('uses the name without file ending', async () => {
params.uploadAttribute.filename = 'somePng.png'
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
expect(params.attribute).toMatch(/^\/uploads\/\d+-somePng/)
expect(params.attribute).toMatch(new RegExp(`^/uploads/${uuid}-avatar.jpg`))
})
it('creates a url safe name', async () => {
params.uploadAttribute.filename =
'/path/to/awkward?/ file-location/?foo- bar-avatar.jpg?foo- bar'
params.uploadAttribute.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg'
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
expect(params.attribute).toMatch(/^\/uploads\/\d+-foo-bar-avatar$/)
expect(params.attribute).toMatch(new RegExp(`/uploads/${uuid}-foo-bar-avatar.jpg$`))
})
describe('in case of duplicates', () => {
@ -50,7 +45,6 @@ describe('fileUpload', () => {
uploadCallback,
)
await new Promise(resolve => setTimeout(resolve, 1000))
const { attribute: second } = await fileUpload(
{
...params,

View File

@ -1,10 +1,9 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server'
import { gql } from '../../helpers/jest'
const factory = Factory()
const driver = getDriver()
const neode = getNeode()
@ -54,7 +53,7 @@ const userQuery = gql`
`
beforeAll(async () => {
await factory.cleanDatabase()
await cleanDatabase()
const { server } = createServer({
context: () => ({
driver,
@ -72,29 +71,35 @@ beforeAll(async () => {
})
beforeEach(async () => {
user1 = await factory
.create('User', {
user1 = await Factory.build(
'user',
{
id: 'u1',
name: 'user1',
},
{
email: 'test@example.org',
password: '1234',
})
.then(user => user.toJson())
user2 = await factory
.create('User', {
},
).then(user => user.toJson())
user2 = await Factory.build(
'user',
{
id: 'u2',
name: 'user2',
},
{
email: 'test2@example.org',
password: '1234',
})
.then(user => user.toJson())
},
).then(user => user.toJson())
authenticatedUser = user1
variables = { id: user2.id }
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('follow', () => {

View File

@ -1,4 +1,4 @@
import uuid from 'uuid/v4'
import { v4 as uuid } from 'uuid'
export default function generateNonce() {
return uuid().substring(0, 6)
}

View File

@ -1,11 +1,9 @@
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
const factory = Factory()
let mutate, authenticatedUser
const driver = getDriver()
@ -25,7 +23,7 @@ beforeAll(() => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('resolvers', () => {
@ -49,16 +47,16 @@ describe('resolvers', () => {
id: 'u47',
name: 'John Doughnut',
}
const Paris = await factory.create('Location', {
const Paris = await Factory.build('location', {
id: 'region.9397217726497330',
name: 'Paris',
type: 'region',
lat: 2.35183,
lng: 48.85658,
lng: 2.35183,
lat: 48.85658,
nameEN: 'Paris',
})
const user = await factory.create('User', {
const user = await Factory.build('user', {
id: 'u47',
name: 'John Doe',
})

View File

@ -1,20 +1,10 @@
const transformReturnType = record => {
return {
...record.get('review').properties,
report: record.get('report').properties,
resource: {
__typename: record.get('type'),
...record.get('resource').properties,
},
}
}
import log from './helpers/databaseLogger'
export default {
Mutation: {
review: async (_object, params, context, _resolveInfo) => {
const { user: moderator, driver } = context
let createdRelationshipWithNestedAttributes = null // return value
const session = driver.session()
try {
const cypher = `
@ -25,10 +15,11 @@ export default {
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
SET report.updatedAt = $dateTime, report.disable = review.disable, report.closed = $params.closed
SET resource.disabled = report.disable
RETURN review, report, resource, labels(resource)[0] AS type
WITH review, report, resource {.*, __typename: labels(resource)[0]} AS finalResource
RETURN review {.*, report: properties(report), resource: properties(finalResource)}
`
const reviewWriteTxResultPromise = session.writeTransaction(async txc => {
const reviewTransactionResponse = await txc.run(cypher, {
@ -36,16 +27,14 @@ export default {
moderatorId: moderator.id,
dateTime: new Date().toISOString(),
})
return reviewTransactionResponse.records.map(transformReturnType)
log(reviewTransactionResponse)
return reviewTransactionResponse.records.map(record => record.get('review'))
})
const txResult = await reviewWriteTxResultPromise
if (!txResult[0]) return null
createdRelationshipWithNestedAttributes = txResult[0]
const [reviewed] = await reviewWriteTxResultPromise
return reviewed || null
} finally {
session.close()
}
return createdRelationshipWithNestedAttributes
},
},
}

View File

@ -1,10 +1,9 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
const factory = Factory()
const neode = getNeode()
const driver = getDriver()
@ -54,7 +53,7 @@ const reviewMutation = gql`
describe('moderate resources', () => {
beforeAll(async () => {
await factory.cleanDatabase()
await cleanDatabase()
authenticatedUser = undefined
const { server } = createServer({
context: () => {
@ -80,23 +79,33 @@ describe('moderate resources', () => {
closed: false,
}
authenticatedUser = null
moderator = await factory.create('User', {
id: 'moderator-id',
name: 'Moderator',
email: 'moderator@example.org',
password: '1234',
role: 'moderator',
})
nonModerator = await factory.create('User', {
id: 'non-moderator',
name: 'Non Moderator',
email: 'non.moderator@example.org',
password: '1234',
})
moderator = await Factory.build(
'user',
{
id: 'moderator-id',
name: 'Moderator',
role: 'moderator',
},
{
email: 'moderator@example.org',
password: '1234',
},
)
nonModerator = await Factory.build(
'user',
{
id: 'non-moderator',
name: 'Non Moderator',
},
{
email: 'non.moderator@example.org',
password: '1234',
},
)
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('review to close report, leaving resource enabled', () => {
@ -127,10 +136,10 @@ describe('moderate resources', () => {
describe('moderator', () => {
beforeEach(async () => {
authenticatedUser = await moderator.toJson()
const questionablePost = await factory.create('Post', {
const questionablePost = await Factory.build('post', {
id: 'should-i-be-disabled',
})
const reportAgainstQuestionablePost = await factory.create('Report')
const reportAgainstQuestionablePost = await Factory.build('report')
await Promise.all([
reportAgainstQuestionablePost.relateTo(nonModerator, 'filed', {
resourceId: 'should-i-be-disabled',
@ -229,10 +238,10 @@ describe('moderate resources', () => {
describe('moderate a comment', () => {
beforeEach(async () => {
const trollingComment = await factory.create('Comment', {
const trollingComment = await Factory.build('comment', {
id: 'comment-id',
})
const reportAgainstTrollingComment = await factory.create('Report')
const reportAgainstTrollingComment = await Factory.build('report')
await Promise.all([
reportAgainstTrollingComment.relateTo(nonModerator, 'filed', {
resourceId: 'comment-id',
@ -307,10 +316,10 @@ describe('moderate resources', () => {
describe('moderate a post', () => {
beforeEach(async () => {
const trollingPost = await factory.create('Post', {
const trollingPost = await Factory.build('post', {
id: 'post-id',
})
const reportAgainstTrollingPost = await factory.create('Report')
const reportAgainstTrollingPost = await Factory.build('report')
await Promise.all([
reportAgainstTrollingPost.relateTo(nonModerator, 'filed', {
resourceId: 'post-id',
@ -387,10 +396,10 @@ describe('moderate resources', () => {
describe('moderate a user', () => {
beforeEach(async () => {
const troll = await factory.create('User', {
const troll = await Factory.build('user', {
id: 'user-id',
})
const reportAgainstTroll = await factory.create('Report')
const reportAgainstTroll = await Factory.build('report')
await Promise.all([
reportAgainstTroll.relateTo(nonModerator, 'filed', {
resourceId: 'user-id',
@ -504,10 +513,10 @@ describe('moderate resources', () => {
describe('moderate a comment', () => {
beforeEach(async () => {
const trollingComment = await factory.create('Comment', {
const trollingComment = await Factory.build('comment', {
id: 'comment-id',
})
const reportAgainstTrollingComment = await factory.create('Report')
const reportAgainstTrollingComment = await Factory.build('report')
await Promise.all([
reportAgainstTrollingComment.relateTo(nonModerator, 'filed', {
resourceId: 'comment-id',
@ -568,10 +577,10 @@ describe('moderate resources', () => {
describe('moderate a post', () => {
beforeEach(async () => {
const trollingPost = await factory.create('Post', {
const trollingPost = await Factory.build('post', {
id: 'post-id',
})
const reportAgainstTrollingPost = await factory.create('Report')
const reportAgainstTrollingPost = await Factory.build('report')
await Promise.all([
reportAgainstTrollingPost.relateTo(nonModerator, 'filed', {
resourceId: 'post-id',
@ -633,10 +642,10 @@ describe('moderate resources', () => {
describe('moderate a user', () => {
beforeEach(async () => {
const troll = await factory.create('User', {
const troll = await Factory.build('user', {
id: 'user-id',
})
const reportAgainstTroll = await factory.create('Report')
const reportAgainstTroll = await Factory.build('report')
await Promise.all([
reportAgainstTroll.relateTo(nonModerator, 'filed', {
resourceId: 'user-id',

View File

@ -1,21 +1,18 @@
import log from './helpers/databaseLogger'
const resourceTypes = ['Post', 'Comment', 'Report']
const transformReturnType = record => {
return {
...record.get('notification').properties,
from: {
__typename: record.get('resource').labels.find(l => resourceTypes.includes(l)),
...record.get('resource').properties,
},
to: {
...record.get('user').properties,
},
}
}
import { withFilter } from 'graphql-subscriptions'
import { pubsub, NOTIFICATION_ADDED } from '../../server'
export default {
Subscription: {
notificationAdded: {
subscribe: withFilter(
() => pubsub.asyncIterator(NOTIFICATION_ADDED),
(payload, variables) => {
return payload.notificationAdded.to.id === variables.userId
},
),
},
},
Query: {
notifications: async (_parent, args, context, _resolveInfo) => {
const { user: currentUser } = context
@ -85,7 +82,12 @@ export default {
`
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id: $id})
SET notification.read = TRUE
RETURN resource, notification, user
WITH user, notification, resource,
[(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors,
[(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts
WITH resource, user, notification, authors, posts,
resource {.*, __typename: labels(resource)[0], author: authors[0], post: posts[0]} AS finalResource
RETURN notification {.*, from: finalResource, to: properties(user)}
`,
{
resourceId: args.id,
@ -93,7 +95,9 @@ export default {
},
)
log(markNotificationAsReadTransactionResponse)
return markNotificationAsReadTransactionResponse.records.map(transformReturnType)
return markNotificationAsReadTransactionResponse.records.map(record =>
record.get('notification'),
)
})
try {
const [notifications] = await writeTxResultPromise

View File

@ -1,10 +1,9 @@
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getDriver } from '../../db/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server'
const factory = Factory()
const driver = getDriver()
let authenticatedUser
let user
@ -32,66 +31,155 @@ beforeEach(async () => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('given some notifications', () => {
beforeEach(async () => {
const categoryIds = ['cat1']
author = await factory.create('User', { id: 'author' })
user = await factory.create('User', { id: 'you' })
// author = await factory.create('User', { id: 'author' })
// user = await factory.create('User', { id: 'you' })
// const [neighbor, badWomen] = await Promise.all([
// factory.create('User', { id: 'neighbor' }),
// factory.create('User', { id: 'badWomen', name: 'Mrs. Badwomen' }),
// factory.create('Category', { id: 'cat1' }),
// ])
// const [post1, post2, post3, post4] = await Promise.all([
// factory.create('Post', { author, id: 'p1', categoryIds, content: 'Not for you' }),
// factory.create('Post', {
// author,
// id: 'p2',
// categoryIds,
// content: 'Already seen post mention',
// }),
// factory.create('Post', {
// author,
// id: 'p3',
// categoryIds,
// content: 'You have been mentioned in a post',
// }),
// factory.create('Post', {
// author,
// id: 'p4',
// categoryIds,
// title: 'Bad Post',
// content: 'I am bad content !!!',
// }),
// ])
// const [comment1, comment2, comment3, comment4] = await Promise.all([
// factory.create('Comment', {
// author,
// postId: 'p3',
// id: 'c1',
// content: 'You have seen this comment mentioning already',
// }),
// factory.create('Comment', {
// author,
// postId: 'p3',
// id: 'c2',
// content: 'You have been mentioned in a comment',
// }),
// factory.create('Comment', {
// author,
// postId: 'p3',
// id: 'c3',
// content: 'Somebody else was mentioned in a comment',
// }),
// factory.create('Comment', {
// author,
// postId: 'p4',
// id: 'c4',
// content: 'I am harassing content in a harassing comment to a bad post !!!',
// }),
author = await Factory.build('user', { id: 'author' })
user = await Factory.build('user', { id: 'you' })
const [neighbor, badWomen] = await Promise.all([
factory.create('User', { id: 'neighbor' }),
factory.create('User', { id: 'badWomen', name: 'Mrs. Badwomen' }),
factory.create('Category', { id: 'cat1' }),
Factory.build('user', { id: 'neighbor' }),
Factory.build('user', { id: 'badWomen', name: 'Mrs. Badwomen' }),
Factory.build('category', { id: 'cat1' }),
])
const [post1, post2, post3, post4] = await Promise.all([
factory.create('Post', { author, id: 'p1', categoryIds, content: 'Not for you' }),
factory.create('Post', {
author,
id: 'p2',
categoryIds,
content: 'Already seen post mention',
}),
factory.create('Post', {
author,
id: 'p3',
categoryIds,
content: 'You have been mentioned in a post',
}),
factory.create('Post', {
author,
id: 'p4',
categoryIds,
title: 'Bad Post',
content: 'I am bad content !!!',
}),
Factory.build('post', { id: 'p1', content: 'Not for you' }, { author, categoryIds }),
Factory.build(
'post',
{
id: 'p2',
content: 'Already seen post mention',
},
{
author,
categoryIds,
},
),
Factory.build(
'post',
{
id: 'p3',
content: 'You have been mentioned in a post',
},
{
author,
categoryIds,
},
),
Factory.build(
'post',
{
id: 'p4',
title: 'Bad Post',
content: 'I am bad content !!!',
},
{
author,
categoryIds,
},
),
])
const [comment1, comment2, comment3, comment4] = await Promise.all([
factory.create('Comment', {
author,
postId: 'p3',
id: 'c1',
content: 'You have seen this comment mentioning already',
}),
factory.create('Comment', {
author,
postId: 'p3',
id: 'c2',
content: 'You have been mentioned in a comment',
}),
factory.create('Comment', {
author,
postId: 'p3',
id: 'c3',
content: 'Somebody else was mentioned in a comment',
}),
factory.create('Comment', {
author,
postId: 'p4',
id: 'c4',
content: 'I am harassing content in a harassing comment to a bad post !!!',
}),
Factory.build(
'comment',
{
id: 'c1',
content: 'You have seen this comment mentioning already',
},
{
author,
postId: 'p3',
},
),
Factory.build(
'comment',
{
id: 'c2',
content: 'You have been mentioned in a comment',
},
{
author,
postId: 'p3',
},
),
Factory.build(
'comment',
{
id: 'c3',
content: 'Somebody else was mentioned in a comment',
},
{
author,
postId: 'p3',
},
),
Factory.build(
'comment',
{
id: 'c4',
content: 'I am harassing content in a harassing comment to a bad post !!!',
},
{
author,
postId: 'p4',
},
),
])
await Promise.all([
neighbor.relateTo(post1, 'notified', {

View File

@ -1,4 +1,4 @@
import uuid from 'uuid/v4'
import { v4 as uuid } from 'uuid'
import bcrypt from 'bcryptjs'
import createPasswordReset from './helpers/createPasswordReset'
@ -22,6 +22,7 @@ export default {
WHERE duration.between(passwordReset.issuedAt, datetime()).days <= 0 AND passwordReset.usedAt IS NULL
SET passwordReset.usedAt = datetime()
SET user.encryptedPassword = $encryptedNewPassword
SET user.updatedAt = toString(datetime())
RETURN passwordReset
`,
{

View File

@ -1,4 +1,4 @@
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j'
import createPasswordReset from './helpers/createPasswordReset'
@ -7,7 +7,6 @@ import { createTestClient } from 'apollo-server-testing'
const neode = getNeode()
const driver = getDriver()
const factory = Factory()
let mutate
let authenticatedUser
@ -39,15 +38,19 @@ beforeAll(() => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('passwordReset', () => {
describe('given a user', () => {
beforeEach(async () => {
await factory.create('User', {
email: 'user@example.org',
})
await Factory.build(
'user',
{},
{
email: 'user@example.org',
},
)
})
describe('requestPasswordReset', () => {
@ -123,11 +126,16 @@ describe('resetPassword', () => {
describe('given a user', () => {
beforeEach(async () => {
await factory.create('User', {
email: 'user@example.org',
role: 'user',
password: '1234',
})
await Factory.build(
'user',
{
role: 'user',
},
{
email: 'user@example.org',
password: '1234',
},
)
})
describe('invalid email', () => {

View File

@ -1,4 +1,4 @@
import uuid from 'uuid/v4'
import { v4 as uuid } from 'uuid'
import { neo4jgraphql } from 'neo4j-graphql-js'
import { isEmpty } from 'lodash'
import { UserInputError } from 'apollo-server'

View File

@ -1,11 +1,10 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
const driver = getDriver()
const factory = Factory()
const neode = getNeode()
let query
@ -40,7 +39,7 @@ const createPostMutation = gql`
`
beforeAll(async () => {
await factory.cleanDatabase()
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
@ -56,12 +55,17 @@ beforeAll(async () => {
beforeEach(async () => {
variables = {}
user = await factory.create('User', {
id: 'current-user',
name: 'TestUser',
email: 'test@example.org',
password: '1234',
})
user = await Factory.build(
'user',
{
id: 'current-user',
name: 'TestUser',
},
{
email: 'test@example.org',
password: '1234',
},
)
await Promise.all([
neode.create('Category', {
id: 'cat9',
@ -88,7 +92,7 @@ beforeEach(async () => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('Post', () => {
@ -96,21 +100,31 @@ describe('Post', () => {
let followedUser, happyPost, cryPost
beforeEach(async () => {
;[followedUser] = await Promise.all([
factory.create('User', {
id: 'followed-by-me',
email: 'followed@example.org',
name: 'Followed User',
password: '1234',
}),
Factory.build(
'user',
{
id: 'followed-by-me',
name: 'Followed User',
},
{
email: 'followed@example.org',
password: '1234',
},
),
])
;[happyPost, cryPost] = await Promise.all([
factory.create('Post', { id: 'happy-post', categoryIds: ['cat4'] }),
factory.create('Post', { id: 'cry-post', categoryIds: ['cat15'] }),
factory.create('Post', {
id: 'post-by-followed-user',
categoryIds: ['cat9'],
author: followedUser,
}),
Factory.build('post', { id: 'happy-post' }, { categoryIds: ['cat4'] }),
Factory.build('post', { id: 'cry-post' }, { categoryIds: ['cat15'] }),
Factory.build(
'post',
{
id: 'post-by-followed-user',
},
{
categoryIds: ['cat9'],
author: followedUser,
},
),
])
})
@ -340,14 +354,19 @@ describe('UpdatePost', () => {
}
`
beforeEach(async () => {
author = await factory.create('User', { slug: 'the-author' })
newlyCreatedPost = await factory.create('Post', {
author,
id: 'p9876',
title: 'Old title',
content: 'Old content',
categoryIds,
})
author = await Factory.build('user', { slug: 'the-author' })
newlyCreatedPost = await Factory.build(
'post',
{
id: 'p9876',
title: 'Old title',
content: 'Old content',
},
{
author,
categoryIds,
},
)
variables = {
id: 'p9876',
@ -529,10 +548,15 @@ describe('UpdatePost', () => {
describe('are allowed to pin posts', () => {
beforeEach(async () => {
await factory.create('Post', {
id: 'created-and-pinned-by-same-admin',
author: admin,
})
await Factory.build(
'post',
{
id: 'created-and-pinned-by-same-admin',
},
{
author: admin,
},
)
variables = { ...variables, id: 'created-and-pinned-by-same-admin' }
})
@ -589,15 +613,20 @@ describe('UpdatePost', () => {
describe('post created by another admin', () => {
let otherAdmin
beforeEach(async () => {
otherAdmin = await factory.create('User', {
otherAdmin = await Factory.build('user', {
role: 'admin',
name: 'otherAdmin',
})
authenticatedUser = await otherAdmin.toJson()
await factory.create('Post', {
id: 'created-by-one-admin-pinned-by-different-one',
author: otherAdmin,
})
await Factory.build(
'post',
{
id: 'created-by-one-admin-pinned-by-different-one',
},
{
author: otherAdmin,
},
)
})
it('responds with the updated Post', async () => {
@ -654,10 +683,15 @@ describe('UpdatePost', () => {
describe('pinned post already exists', () => {
let pinnedPost
beforeEach(async () => {
await factory.create('Post', {
id: 'only-pinned-post',
author: admin,
})
await Factory.build(
'post',
{
id: 'only-pinned-post',
},
{
author: admin,
},
)
await mutate({ mutation: pinPostMutation, variables })
})
@ -683,12 +717,12 @@ describe('UpdatePost', () => {
describe('PostOrdering', () => {
beforeEach(async () => {
await factory.create('Post', {
await Factory.build('post', {
id: 'im-a-pinned-post',
createdAt: '2019-11-22T17:26:29.070Z',
pinned: true,
})
await factory.create('Post', {
await Factory.build('post', {
id: 'i-was-created-before-pinned-post',
// fairly old, so this should be 3rd
createdAt: '2019-10-22T17:26:29.070Z',
@ -807,7 +841,7 @@ describe('UpdatePost', () => {
describe('admin can unpin posts', () => {
let admin, pinnedPost
beforeEach(async () => {
pinnedPost = await factory.create('Post', { id: 'post-to-be-unpinned' })
pinnedPost = await Factory.build('post', { id: 'post-to-be-unpinned' })
admin = await user.update({
role: 'admin',
name: 'Admin',
@ -874,15 +908,20 @@ describe('DeletePost', () => {
`
beforeEach(async () => {
author = await factory.create('User')
await factory.create('Post', {
id: 'p4711',
author,
title: 'I will be deleted',
content: 'To be deleted',
image: 'path/to/some/image',
categoryIds,
})
author = await Factory.build('user')
await Factory.build(
'post',
{
id: 'p4711',
title: 'I will be deleted',
content: 'To be deleted',
image: 'path/to/some/image',
},
{
author,
categoryIds,
},
)
variables = { ...variables, id: 'p4711' }
})
@ -929,11 +968,16 @@ describe('DeletePost', () => {
describe('if there are comments on the post', () => {
beforeEach(async () => {
await factory.create('Comment', {
postId: 'p4711',
content: 'to be deleted comment content',
contentExcerpt: 'to be deleted comment content',
})
await Factory.build(
'comment',
{
content: 'to be deleted comment content',
contentExcerpt: 'to be deleted comment content',
},
{
postId: 'p4711',
},
)
})
it('marks the comments as deleted', async () => {
@ -988,11 +1032,16 @@ describe('emotions', () => {
beforeEach(async () => {
author = await neode.create('User', { id: 'u257' })
postToEmote = await factory.create('Post', {
author,
id: 'p1376',
categoryIds,
})
postToEmote = await Factory.build(
'post',
{
id: 'p1376',
},
{
author,
categoryIds,
},
)
variables = {
...variables,

View File

@ -1,10 +1,9 @@
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
const factory = Factory()
const neode = getNeode()
let mutate
@ -30,7 +29,7 @@ beforeAll(() => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('Signup', () => {
@ -58,11 +57,16 @@ describe('Signup', () => {
describe('as admin', () => {
beforeEach(async () => {
const admin = await factory.create('User', {
role: 'admin',
email: 'admin@example.org',
password: '1234',
})
const admin = await Factory.build(
'user',
{
role: 'admin',
},
{
email: 'admin@example.org',
password: '1234',
},
)
authenticatedUser = await admin.toJson()
})
@ -90,9 +94,9 @@ describe('Signup', () => {
})
describe('if the email already exists', () => {
let email
let emailAddress
beforeEach(async () => {
email = await factory.create('EmailAddress', {
emailAddress = await Factory.build('emailAddress', {
email: 'someuser@example.org',
verifiedAt: null,
})
@ -100,7 +104,8 @@ describe('Signup', () => {
describe('and the user has registered already', () => {
beforeEach(async () => {
await factory.create('User', { email })
const user = await Factory.build('userWithoutEmailAddress')
await emailAddress.relateTo(user, 'belongsTo')
})
it('throws UserInputError error because of unique constraint violation', async () => {

View File

@ -1,24 +1,14 @@
import { undefinedToNullResolver } from './helpers/Resolver'
import log from './helpers/databaseLogger'
const transformReturnType = record => {
return {
...record.get('report').properties,
resource: {
__typename: record.get('type'),
...record.get('resource').properties,
},
}
}
export default {
Mutation: {
fileReport: async (_parent, params, context, _resolveInfo) => {
const { resourceId, reasonCategory, reasonDescription } = params
const { driver, user } = context
const session = driver.session()
const reportWriteTxResultPromise = session.writeTransaction(async transaction => {
const reportTransactionResponse = await transaction.run(
const fileReportWriteTxResultPromise = session.writeTransaction(async transaction => {
const fileReportTransactionResponse = await transaction.run(
`
MATCH (submitter:User {id: $submitterId})
MATCH (resource {id: $resourceId})
@ -28,7 +18,8 @@ export default {
WITH submitter, resource, report
CREATE (report)<-[filed:FILED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter)
RETURN report, resource, labels(resource)[0] AS type
WITH filed, report, resource {.*, __typename: labels(resource)[0]} AS finalResource
RETURN filed {.*, reportId: report.id, resource: properties(finalResource)} AS filedReport
`,
{
resourceId,
@ -38,13 +29,12 @@ export default {
reasonDescription,
},
)
log(reportTransactionResponse)
return reportTransactionResponse.records.map(transformReturnType)
log(fileReportTransactionResponse)
return fileReportTransactionResponse.records.map(record => record.get('filedReport'))
})
try {
const [createdRelationshipWithNestedAttributes] = await reportWriteTxResultPromise
if (!createdRelationshipWithNestedAttributes) return null
return createdRelationshipWithNestedAttributes
const [filedReport] = await fileReportWriteTxResultPromise
return filedReport || null
} finally {
session.close()
}
@ -77,14 +67,24 @@ export default {
filterClause = ''
}
if (params.closed) filterClause = 'AND report.closed = true'
switch (params.closed) {
case true:
filterClause = 'AND report.closed = true'
break
case false:
filterClause = 'AND report.closed = false'
break
default:
break
}
const offset =
params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : ''
const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : ''
const reportReadTxPromise = session.readTransaction(async transaction => {
const allReportsTransactionResponse = await transaction.run(
const reportsReadTxPromise = session.readTransaction(async transaction => {
const reportsTransactionResponse = await transaction.run(
// !!! this Cypher query returns multiple reports on the same resource! i will create an issue for refactoring (bug fixing)
`
MATCH (report:Report)-[:BELONGS_TO]->(resource)
WHERE (resource:User OR resource:Post OR resource:Comment)
@ -102,11 +102,11 @@ export default {
${offset} ${limit}
`,
)
log(allReportsTransactionResponse)
return allReportsTransactionResponse.records.map(record => record.get('report'))
log(reportsTransactionResponse)
return reportsTransactionResponse.records.map(record => record.get('report'))
})
try {
const reports = await reportReadTxPromise
const reports = await reportsReadTxPromise
return reports
} finally {
session.close()

View File

@ -1,28 +1,26 @@
import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server'
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getDriver, getNeode } from '../../db/neo4j'
const factory = Factory()
const instance = getNeode()
const driver = getDriver()
describe('file a report on a resource', () => {
let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser, otherReportingUser
const categoryIds = ['cat9']
const reportMutation = gql`
const fileReportMutation = gql`
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
fileReport(
resourceId: $resourceId
reasonCategory: $reasonCategory
reasonDescription: $reasonDescription
) {
id
createdAt
updatedAt
closed
rule
reasonCategory
reasonDescription
reportId
resource {
__typename
... on User {
@ -35,6 +33,35 @@ describe('file a report on a resource', () => {
content
}
}
}
}
`
const variables = {
resourceId: 'invalid',
reasonCategory: 'other',
reasonDescription: 'Violates code of conduct !!!',
}
const reportsQuery = gql`
query($closed: Boolean) {
reports(orderBy: createdAt_desc, closed: $closed) {
id
createdAt
updatedAt
rule
disable
closed
resource {
__typename
... on User {
id
}
... on Post {
id
}
... on Comment {
id
}
}
filed {
submitter {
id
@ -46,14 +73,34 @@ describe('file a report on a resource', () => {
}
}
`
const variables = {
resourceId: 'whatever',
reasonCategory: 'other',
reasonDescription: 'Violates code of conduct !!!',
}
const reviewMutation = gql`
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
createdAt
resource {
__typename
... on User {
id
disabled
}
... on Post {
id
disabled
}
... on Comment {
id
disabled
}
}
report {
disable
}
}
}
`
beforeAll(async () => {
await factory.cleanDatabase()
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
@ -68,14 +115,14 @@ describe('file a report on a resource', () => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('report a resource', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
authenticatedUser = null
await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({
await expect(mutate({ mutation: fileReportMutation, variables })).resolves.toMatchObject({
data: { fileReport: null },
errors: [{ message: 'Not Authorised!' }],
})
@ -84,24 +131,50 @@ describe('file a report on a resource', () => {
describe('authenticated', () => {
beforeEach(async () => {
currentUser = await factory.create('User', {
id: 'current-user-id',
role: 'user',
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',
name: 'abusive-user',
email: 'abusive-user@example.org',
})
currentUser = await Factory.build(
'user',
{
id: 'current-user-id',
role: 'user',
},
{
email: 'test@example.org',
password: '1234',
},
)
moderator = await Factory.build(
'user',
{
id: 'moderator-id',
role: 'moderator',
},
{
email: 'moderator@example.org',
password: '1234',
},
)
otherReportingUser = await Factory.build(
'user',
{
id: 'other-reporting-user-id',
role: 'user',
},
{
email: 'reporting@example.org',
password: '1234',
},
)
await Factory.build(
'user',
{
id: 'abusive-user-id',
role: 'user',
name: 'abusive-user',
},
{
email: 'abusive-user@example.org',
},
)
await instance.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
@ -113,7 +186,7 @@ describe('file a report on a resource', () => {
describe('invalid resource id', () => {
it('returns null', async () => {
await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({
await expect(mutate({ mutation: fileReportMutation, variables })).resolves.toMatchObject({
data: { fileReport: null },
errors: undefined,
})
@ -125,47 +198,112 @@ describe('file a report on a resource', () => {
it('which belongs to resource', async () => {
await expect(
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
}),
).resolves.toMatchObject({
data: {
fileReport: {
id: expect.any(String),
reportId: expect.any(String),
resource: {
name: 'abusive-user',
},
},
},
errors: undefined,
})
})
it('creates only one report for multiple reports on the same resource', async () => {
it('only one report for multiple reports on the same resource', async () => {
const firstReport = await mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
})
authenticatedUser = await otherReportingUser.toJson()
const secondReport = await mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
})
expect(firstReport.data.fileReport.id).toEqual(secondReport.data.fileReport.id)
expect(firstReport.data.fileReport.reportId).toEqual(
secondReport.data.fileReport.reportId,
)
})
it('returns the rule for how the report was decided', async () => {
await expect(
mutate({
mutation: reportMutation,
describe('report properties are set correctly', () => {
const reportsCypherQuery =
'MATCH (resource:User {id: $resourceId})<-[:BELONGS_TO]-(report:Report {closed: false})<-[filed:FILED]-(user:User {id: $currentUserId}) RETURN report'
it('with the rule for how the report will be decided', async () => {
await mutate({
mutation: fileReportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
}),
).resolves.toMatchObject({
data: {
fileReport: {
rule: 'latestReviewUpdatedAtRules',
},
},
errors: undefined,
})
const reportsCypherQueryResponse = await instance.cypher(reportsCypherQuery, {
resourceId: 'abusive-user-id',
currentUserId: authenticatedUser.id,
})
expect(reportsCypherQueryResponse.records).toHaveLength(1)
const [reportProperties] = reportsCypherQueryResponse.records.map(
record => record.get('report').properties,
)
expect(reportProperties).toMatchObject({ rule: 'latestReviewUpdatedAtRules' })
})
describe('with overtaken disabled from resource in disable property', () => {
it('disable is false', async () => {
await mutate({
mutation: fileReportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
})
const reportsCypherQueryResponse = await instance.cypher(reportsCypherQuery, {
resourceId: 'abusive-user-id',
currentUserId: authenticatedUser.id,
})
expect(reportsCypherQueryResponse.records).toHaveLength(1)
const [reportProperties] = reportsCypherQueryResponse.records.map(
record => record.get('report').properties,
)
expect(reportProperties).toMatchObject({ disable: false })
})
it('disable is true', async () => {
// first time filling a report to enable a moderator the disable the resource
await mutate({
mutation: fileReportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
})
authenticatedUser = await moderator.toJson()
await mutate({
mutation: reviewMutation,
variables: {
resourceId: 'abusive-user-id',
disable: true,
closed: true,
},
})
authenticatedUser = await currentUser.toJson()
// second time filling a report to see if the "disable is true" of the resource is overtaken
await mutate({
mutation: fileReportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
})
const reportsCypherQueryResponse = await instance.cypher(reportsCypherQuery, {
resourceId: 'abusive-user-id',
currentUserId: authenticatedUser.id,
})
expect(reportsCypherQueryResponse.records).toHaveLength(1)
const [reportProperties] = reportsCypherQueryResponse.records.map(
record => record.get('report').properties,
)
expect(reportProperties).toMatchObject({ disable: true })
})
})
})
it.todo('creates multiple filed reports')
})
@ -173,7 +311,7 @@ describe('file a report on a resource', () => {
it('returns __typename "User"', async () => {
await expect(
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
}),
).resolves.toMatchObject({
@ -191,7 +329,7 @@ describe('file a report on a resource', () => {
it('returns user attribute info', async () => {
await expect(
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
}),
).resolves.toMatchObject({
@ -207,32 +345,10 @@ describe('file a report on a resource', () => {
})
})
it('returns the submitter', async () => {
it('returns a createdAt', async () => {
await expect(
mutate({
mutation: reportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
}),
).resolves.toMatchObject({
data: {
fileReport: {
filed: [
{
submitter: {
id: 'current-user-id',
},
},
],
},
},
errors: undefined,
})
})
it('returns a date', async () => {
await expect(
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
}),
).resolves.toMatchObject({
@ -248,7 +364,7 @@ describe('file a report on a resource', () => {
it('returns the reason category', async () => {
await expect(
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: {
...variables,
resourceId: 'abusive-user-id',
@ -258,11 +374,7 @@ describe('file a report on a resource', () => {
).resolves.toMatchObject({
data: {
fileReport: {
filed: [
{
reasonCategory: 'criminal_behavior_violation_german_law',
},
],
reasonCategory: 'criminal_behavior_violation_german_law',
},
},
errors: undefined,
@ -272,7 +384,7 @@ describe('file a report on a resource', () => {
it('gives an error if the reason category is not in enum "ReasonCategory"', async () => {
await expect(
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: {
...variables,
resourceId: 'abusive-user-id',
@ -293,7 +405,7 @@ describe('file a report on a resource', () => {
it('returns the reason description', async () => {
await expect(
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: {
...variables,
resourceId: 'abusive-user-id',
@ -303,11 +415,7 @@ describe('file a report on a resource', () => {
).resolves.toMatchObject({
data: {
fileReport: {
filed: [
{
reasonDescription: 'My reason!',
},
],
reasonDescription: 'My reason!',
},
},
errors: undefined,
@ -317,7 +425,7 @@ describe('file a report on a resource', () => {
it('sanitizes the reason description', async () => {
await expect(
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: {
...variables,
resourceId: 'abusive-user-id',
@ -327,11 +435,7 @@ describe('file a report on a resource', () => {
).resolves.toMatchObject({
data: {
fileReport: {
filed: [
{
reasonDescription: 'My reason !',
},
],
reasonDescription: 'My reason !',
},
},
errors: undefined,
@ -341,18 +445,23 @@ describe('file a report on a resource', () => {
describe('reported resource is a post', () => {
beforeEach(async () => {
await factory.create('Post', {
author: currentUser,
id: 'post-to-report-id',
title: 'This is a post that is going to be reported',
categoryIds,
})
await Factory.build(
'post',
{
id: 'post-to-report-id',
title: 'This is a post that is going to be reported',
},
{
author: currentUser,
categoryIds,
},
)
})
it('returns type "Post"', async () => {
await expect(
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: {
...variables,
resourceId: 'post-to-report-id',
@ -373,7 +482,7 @@ describe('file a report on a resource', () => {
it('returns resource in post attribute', async () => {
await expect(
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: {
...variables,
resourceId: 'post-to-report-id',
@ -394,27 +503,36 @@ describe('file a report on a resource', () => {
})
describe('reported resource is a comment', () => {
let createPostVariables
beforeEach(async () => {
createPostVariables = {
id: 'p1',
title: 'post to comment on',
content: 'please comment on me',
categoryIds,
}
await factory.create('Post', { ...createPostVariables, author: currentUser })
await factory.create('Comment', {
author: currentUser,
postId: 'p1',
id: 'comment-to-report-id',
content: 'Post comment to be reported.',
})
await Factory.build(
'post',
{
id: 'p1',
title: 'post to comment on',
content: 'please comment on me',
},
{
categoryIds,
author: currentUser,
},
)
await Factory.build(
'comment',
{
id: 'comment-to-report-id',
content: 'Post comment to be reported.',
},
{
author: currentUser,
postId: 'p1',
},
)
})
it('returns type "Comment"', async () => {
await expect(
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: {
...variables,
resourceId: 'comment-to-report-id',
@ -435,7 +553,7 @@ describe('file a report on a resource', () => {
it('returns resource in comment attribute', async () => {
await expect(
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: {
...variables,
resourceId: 'comment-to-report-id',
@ -457,7 +575,7 @@ describe('file a report on a resource', () => {
describe('reported resource is a tag', () => {
beforeEach(async () => {
await factory.create('Tag', {
await Factory.build('tag', {
id: 'tag-to-report-id',
})
})
@ -465,7 +583,7 @@ describe('file a report on a resource', () => {
it('returns null', async () => {
await expect(
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: {
...variables,
resourceId: 'tag-to-report-id',
@ -482,57 +600,41 @@ describe('file a report on a resource', () => {
})
describe('query for reported resource', () => {
const reportsQuery = gql`
query {
reports(orderBy: createdAt_desc) {
id
createdAt
updatedAt
closed
resource {
__typename
... on User {
id
}
... on Post {
id
}
... on Comment {
id
}
}
filed {
submitter {
id
}
createdAt
reasonCategory
reasonDescription
}
}
}
`
beforeEach(async () => {
authenticatedUser = null
moderator = await factory.create('User', {
id: 'moderator-1',
role: 'moderator',
email: 'moderator@example.org',
password: '1234',
})
currentUser = await factory.create('User', {
id: 'current-user-id',
role: 'user',
email: 'current.user@example.org',
password: '1234',
})
abusiveUser = await factory.create('User', {
id: 'abusive-user-1',
role: 'user',
name: 'abusive-user',
email: 'abusive-user@example.org',
})
moderator = await Factory.build(
'user',
{
id: 'moderator-1',
role: 'moderator',
},
{
email: 'moderator@example.org',
password: '1234',
},
)
currentUser = await Factory.build(
'user',
{
id: 'current-user-id',
role: 'user',
},
{
email: 'current.user@example.org',
password: '1234',
},
)
abusiveUser = await Factory.build(
'user',
{
id: 'abusive-user-1',
role: 'user',
name: 'abusive-user',
},
{
email: 'abusive-user@example.org',
},
)
await instance.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
@ -540,36 +642,56 @@ describe('file a report on a resource', () => {
})
await Promise.all([
factory.create('Post', {
author: abusiveUser,
id: 'abusive-post-1',
categoryIds,
content: 'Interesting Knowledge',
}),
factory.create('Post', {
author: moderator,
id: 'post-2',
categoryIds,
content: 'More things to do …',
}),
factory.create('Post', {
author: currentUser,
id: 'post-3',
categoryIds,
content: 'I am at school …',
}),
Factory.build(
'post',
{
id: 'abusive-post-1',
content: 'Interesting Knowledge',
},
{
categoryIds,
author: abusiveUser,
},
),
Factory.build(
'post',
{
id: 'post-2',
content: 'More things to do …',
},
{
author: moderator,
categoryIds,
},
),
Factory.build(
'post',
{
id: 'post-3',
content: 'I am at school …',
},
{
categoryIds,
author: currentUser,
},
),
])
await Promise.all([
factory.create('Comment', {
author: currentUser,
id: 'abusive-comment-1',
postId: 'post-1',
}),
Factory.build(
'comment',
{
id: 'abusive-comment-1',
},
{
postId: 'post-2',
author: currentUser,
},
),
])
authenticatedUser = await currentUser.toJson()
await Promise.all([
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: {
resourceId: 'abusive-post-1',
reasonCategory: 'other',
@ -577,7 +699,7 @@ describe('file a report on a resource', () => {
},
}),
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: {
resourceId: 'abusive-comment-1',
reasonCategory: 'discrimination_etc',
@ -585,7 +707,7 @@ describe('file a report on a resource', () => {
},
}),
mutate({
mutation: reportMutation,
mutation: fileReportMutation,
variables: {
resourceId: 'abusive-user-1',
reasonCategory: 'doxing',

View File

@ -1,10 +1,9 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
const factory = Factory()
const driver = getDriver()
const instance = getNeode()
@ -31,23 +30,38 @@ describe('rewards', () => {
})
beforeEach(async () => {
regularUser = await factory.create('User', {
id: 'regular-user-id',
role: 'user',
email: 'user@example.org',
password: '1234',
})
moderator = await factory.create('User', {
id: 'moderator-id',
role: 'moderator',
email: 'moderator@example.org',
})
administrator = await factory.create('User', {
id: 'admin-id',
role: 'admin',
email: 'admin@example.org',
})
badge = await factory.create('Badge', {
regularUser = await Factory.build(
'user',
{
id: 'regular-user-id',
role: 'user',
},
{
email: 'user@example.org',
password: '1234',
},
)
moderator = await Factory.build(
'user',
{
id: 'moderator-id',
role: 'moderator',
},
{
email: 'moderator@example.org',
},
)
administrator = await Factory.build(
'user',
{
id: 'admin-id',
role: 'admin',
},
{
email: 'admin@example.org',
},
)
badge = await Factory.build('badge', {
id: 'indiegogo_en_rhino',
type: 'crowdfunding',
status: 'permanent',
@ -56,7 +70,7 @@ describe('rewards', () => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('reward', () => {
@ -130,7 +144,7 @@ describe('rewards', () => {
})
it('rewards a second different badge to same user', async () => {
await factory.create('Badge', {
await Factory.build('badge', {
id: 'indiegogo_en_racoon',
icon: '/img/badges/indiegogo_en_racoon.svg',
})
@ -172,10 +186,15 @@ describe('rewards', () => {
},
errors: undefined,
}
await factory.create('User', {
id: 'regular-user-2-id',
email: 'regular2@email.com',
})
await Factory.build(
'user',
{
id: 'regular-user-2-id',
},
{
email: 'regular2@email.com',
},
)
await mutate({
mutation: rewardMutation,
variables,

View File

@ -1,11 +1,10 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
let mutate, query, authenticatedUser, variables
const factory = Factory()
const instance = getNeode()
const driver = getDriver()
@ -47,22 +46,32 @@ describe('shout and unshout posts', () => {
query = createTestClient(server).query
})
beforeEach(async () => {
currentUser = await factory.create('User', {
id: 'current-user-id',
name: 'Current User',
email: 'current.user@example.org',
password: '1234',
})
currentUser = await Factory.build(
'user',
{
id: 'current-user-id',
name: 'Current User',
},
{
email: 'current.user@example.org',
password: '1234',
},
)
postAuthor = await factory.create('User', {
id: 'id-of-another-user',
name: 'Another User',
email: 'another.user@example.org',
password: '1234',
})
postAuthor = await Factory.build(
'user',
{
id: 'id-of-another-user',
name: 'Another User',
},
{
email: 'another.user@example.org',
password: '1234',
},
)
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('shout', () => {
@ -78,16 +87,26 @@ describe('shout and unshout posts', () => {
describe('authenticated', () => {
beforeEach(async () => {
authenticatedUser = await currentUser.toJson()
await factory.create('Post', {
name: 'Other user post',
id: 'another-user-post-id',
author: postAuthor,
})
await factory.create('Post', {
name: 'current user post',
id: 'current-user-post-id',
author: currentUser,
})
await Factory.build(
'post',
{
name: 'Other user post',
id: 'another-user-post-id',
},
{
author: postAuthor,
},
)
await Factory.build(
'post',
{
name: 'current user post',
id: 'current-user-post-id',
},
{
author: currentUser,
},
)
variables = {}
})
@ -144,11 +163,16 @@ describe('shout and unshout posts', () => {
describe('authenticated', () => {
beforeEach(async () => {
authenticatedUser = await currentUser.toJson()
await factory.create('Post', {
name: 'Posted By Another User',
id: 'posted-by-another-user',
author: postAuthor,
})
await Factory.build(
'post',
{
name: 'Posted By Another User',
id: 'posted-by-another-user',
},
{
author: postAuthor,
},
)
await mutate({
mutation: mutationShoutPost,
variables: { id: 'posted-by-another-user' },

View File

@ -1,41 +1,46 @@
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server'
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j'
import { getDriver } from '../../db/neo4j'
const driver = getDriver()
const factory = Factory()
const neode = getNeode()
describe('SocialMedia', () => {
let socialMediaAction, someUser, ownerNode, owner
const ownerParams = {
email: 'pippi@example.com',
password: '1234',
name: 'Pippi Langstrumpf',
}
const userParams = {
email: 'kalle@example.com',
password: 'abcd',
name: 'Kalle Blomqvist',
}
const url = 'https://twitter.com/pippi-langstrumpf'
const newUrl = 'https://twitter.com/bullerby'
const setUpSocialMedia = async () => {
const socialMediaNode = await neode.create('SocialMedia', { url })
const socialMediaNode = await Factory.build('socialMedia', { url })
await socialMediaNode.relateTo(ownerNode, 'ownedBy')
return socialMediaNode.toJson()
}
beforeEach(async () => {
const someUserNode = await neode.create('User', userParams)
const someUserNode = await Factory.build(
'user',
{
name: 'Kalle Blomqvist',
},
{
email: 'kalle@example.com',
password: 'abcd',
},
)
someUser = await someUserNode.toJson()
ownerNode = await neode.create('User', ownerParams)
ownerNode = await Factory.build(
'user',
{
name: 'Pippi Langstrumpf',
},
{
email: 'pippi@example.com',
password: '1234',
},
)
owner = await ownerNode.toJson()
socialMediaAction = async (user, mutation, variables) => {
@ -57,7 +62,7 @@ describe('SocialMedia', () => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('create social media', () => {

View File

@ -1,11 +1,10 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
let query, authenticatedUser
const factory = Factory()
const instance = getNeode()
const driver = getDriver()
@ -37,7 +36,7 @@ beforeAll(() => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('statistics', () => {
@ -45,7 +44,7 @@ describe('statistics', () => {
beforeEach(async () => {
await Promise.all(
[...Array(6).keys()].map(() => {
return factory.create('User')
return Factory.build('user')
}),
)
})
@ -62,7 +61,7 @@ describe('statistics', () => {
beforeEach(async () => {
await Promise.all(
[...Array(3).keys()].map(() => {
return factory.create('Post')
return Factory.build('post')
}),
)
})
@ -79,7 +78,7 @@ describe('statistics', () => {
beforeEach(async () => {
await Promise.all(
[...Array(2).keys()].map(() => {
return factory.create('Comment')
return Factory.build('comment')
}),
)
})
@ -97,7 +96,7 @@ describe('statistics', () => {
beforeEach(async () => {
users = await Promise.all(
[...Array(2).keys()].map(() => {
return factory.create('User')
return Factory.build('user')
}),
)
await users[0].relateTo(users[1], 'following')
@ -116,12 +115,12 @@ describe('statistics', () => {
beforeEach(async () => {
users = await Promise.all(
[...Array(2).keys()].map(() => {
return factory.create('User')
return Factory.build('user')
}),
)
posts = await Promise.all(
[...Array(3).keys()].map(() => {
return factory.create('Post')
return Factory.build('post')
}),
)
await Promise.all([

View File

@ -12,10 +12,28 @@ export default {
isLoggedIn: (_, args, { driver, user }) => {
return Boolean(user && user.id)
},
currentUser: async (object, params, ctx, resolveInfo) => {
if (!ctx.user) return null
const user = await neode.find('User', ctx.user.id)
return user.toJson()
currentUser: async (object, params, context, resolveInfo) => {
const { user, driver } = context
if (!user) return null
const session = driver.session()
const currentUserTransactionPromise = session.readTransaction(async transaction => {
const result = await transaction.run(
`
MATCH (user:User {id: $id})
WITH user, [(user)<-[:OWNED_BY]-(medium:SocialMedia) | properties(medium) ] as media
RETURN user {.*, socialMedia: media } as user
`,
{ id: user.id },
)
log(result)
return result.records.map(record => record.get('user'))
})
try {
const [currentUser] = await currentUserTransactionPromise
return currentUser
} finally {
session.close()
}
},
},
Mutation: {

View File

@ -1,20 +1,19 @@
import jwt from 'jsonwebtoken'
import CONFIG from './../../config'
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { createTestClient } from 'apollo-server-testing'
import createServer, { context } from '../../server'
import encode from '../../jwt/encode'
import { getNeode } from '../../db/neo4j'
const factory = Factory()
const neode = getNeode()
let query, mutate, variables, req, user
const disable = async id => {
const moderator = await factory.create('User', { id: 'u2', role: 'moderator' })
const moderator = await Factory.build('user', { id: 'u2', role: 'moderator' })
const user = await neode.find('User', id)
const reportAgainstUser = await factory.create('Report')
const reportAgainstUser = await Factory.build('report')
await Promise.all([
reportAgainstUser.relateTo(moderator, 'filed', {
resourceId: id,
@ -48,7 +47,7 @@ beforeAll(() => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('isLoggedIn', () => {
@ -69,7 +68,7 @@ describe('isLoggedIn', () => {
describe('authenticated', () => {
beforeEach(async () => {
user = await factory.create('User', { id: 'u3' })
user = await Factory.build('user', { id: 'u3' })
const userBearerToken = encode({ id: 'u3' })
req = { headers: { authorization: `Bearer ${userBearerToken}` } }
})
@ -127,15 +126,20 @@ describe('currentUser', () => {
describe('authenticated', () => {
describe('and corresponding user in the database', () => {
beforeEach(async () => {
await factory.create('User', {
id: 'u3',
// the `id` is the only thing that has to match the decoded JWT bearer token
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
email: 'test@example.org',
name: 'Matilde Hermiston',
slug: 'matilde-hermiston',
role: 'user',
})
await Factory.build(
'user',
{
id: 'u3',
// the `id` is the only thing that has to match the decoded JWT bearer token
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
name: 'Matilde Hermiston',
slug: 'matilde-hermiston',
role: 'user',
},
{
email: 'test@example.org',
},
)
const userBearerToken = encode({ id: 'u3' })
req = { headers: { authorization: `Bearer ${userBearerToken}` } }
})
@ -172,10 +176,13 @@ describe('login', () => {
beforeEach(async () => {
variables = { email: 'test@example.org', password: '1234' }
user = await factory.create('User', {
...variables,
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
})
user = await Factory.build(
'user',
{
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
},
variables,
)
})
describe('ask for a `token`', () => {
@ -185,7 +192,9 @@ describe('login', () => {
data: { login: token },
} = await mutate({ mutation: loginMutation, variables })
jwt.verify(token, CONFIG.JWT_SECRET, (err, data) => {
expect(data.email).toEqual('test@example.org')
expect(data).toMatchObject({
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
})
expect(err).toBeNull()
done()
})
@ -295,7 +304,7 @@ describe('change password', () => {
describe('authenticated', () => {
beforeEach(async () => {
await factory.create('User', { id: 'u3' })
await Factory.build('user', { id: 'u3' })
const userBearerToken = encode({ id: 'u3' })
req = { headers: { authorization: `Bearer ${userBearerToken}` } }
})

View File

@ -251,8 +251,10 @@ export default {
boolean: {
followedByCurrentUser:
'MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
blocked:
isBlocked:
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
blocked:
'MATCH (this)-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
isMuted:
'MATCH (this)<-[:MUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
},

View File

@ -1,10 +1,9 @@
import Factory from '../../factories'
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
const factory = Factory()
const categoryIds = ['cat9']
let user
@ -31,13 +30,13 @@ beforeAll(() => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('User', () => {
describe('query by email address', () => {
beforeEach(async () => {
await factory.create('User', { name: 'Johnny', email: 'any-email-address@example.org' })
await Factory.build('user', { name: 'Johnny' }, { email: 'any-email-address@example.org' })
})
const userQuery = gql`
@ -57,11 +56,16 @@ describe('User', () => {
describe('as admin', () => {
beforeEach(async () => {
const admin = await factory.create('User', {
role: 'admin',
email: 'admin@example.org',
password: '1234',
})
const admin = await Factory.build(
'user',
{
role: 'admin',
},
{
email: 'admin@example.org',
password: '1234',
},
)
authenticatedUser = await admin.toJson()
})
@ -91,19 +95,9 @@ describe('User', () => {
})
describe('UpdateUser', () => {
let userParams, variables
let variables
beforeEach(async () => {
userParams = {
email: 'user@example.org',
password: '1234',
id: 'u47',
name: 'John Doe',
termsAndConditionsAgreedVersion: null,
termsAndConditionsAgreedAt: null,
allowEmbedIframes: false,
}
variables = {
id: 'u47',
name: 'John Doughnut',
@ -133,18 +127,33 @@ describe('UpdateUser', () => {
`
beforeEach(async () => {
user = await factory.create('User', userParams)
user = await Factory.build(
'user',
{
id: 'u47',
name: 'John Doe',
termsAndConditionsAgreedVersion: null,
termsAndConditionsAgreedAt: null,
allowEmbedIframes: false,
},
{
email: 'user@example.org',
},
)
})
describe('as another user', () => {
beforeEach(async () => {
const someoneElseParams = {
email: 'someone-else@example.org',
password: '1234',
name: 'James Doe',
}
const someoneElse = await Factory.build(
'user',
{
name: 'James Doe',
},
{
email: 'someone-else@example.org',
},
)
const someoneElse = await factory.create('User', someoneElseParams)
authenticatedUser = await someoneElse.toJson()
})
@ -267,16 +276,20 @@ describe('DeleteUser', () => {
beforeEach(async () => {
variables = { id: ' u343', resource: [] }
user = await factory.create('User', {
user = await Factory.build('user', {
name: 'My name should be deleted',
about: 'along with my about',
id: 'u343',
})
await factory.create('User', {
email: 'friends-account@example.org',
password: '1234',
id: 'not-my-account',
})
await Factory.build(
'user',
{
id: 'not-my-account',
},
{
email: 'friends-account@example.org',
},
)
})
describe('unauthenticated', () => {
@ -309,27 +322,42 @@ describe('DeleteUser', () => {
describe('given posts and comments', () => {
beforeEach(async () => {
await factory.create('Category', {
await Factory.build('category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
})
await factory.create('Post', {
author: user,
id: 'p139',
content: 'Post by user u343',
categoryIds,
})
await factory.create('Comment', {
author: user,
id: 'c155',
content: 'Comment by user u343',
})
await factory.create('Comment', {
postId: 'p139',
id: 'c156',
content: "A comment by someone else on user u343's post",
})
await Factory.build(
'post',
{
id: 'p139',
content: 'Post by user u343',
},
{
author: user,
categoryIds,
},
)
await Factory.build(
'comment',
{
id: 'c155',
content: 'Comment by user u343',
},
{
author: user,
},
)
await Factory.build(
'comment',
{
id: 'c156',
content: "A comment by someone else on user u343's post",
},
{
postId: 'p139',
},
)
})
it("deletes my account, but doesn't delete posts or comments by default", async () => {
@ -527,7 +555,7 @@ describe('DeleteUser', () => {
describe('connected `SocialMedia` nodes', () => {
beforeEach(async () => {
const socialMedia = await factory.create('SocialMedia')
const socialMedia = await Factory.build('socialMedia')
await socialMedia.relateTo(user, 'ownedBy')
})

View File

@ -34,8 +34,8 @@ const createLocation = async (session, mapboxData) => {
namePL: mapboxData.text_pl,
nameRU: mapboxData.text_ru,
type: mapboxData.id.split('.')[0].toLowerCase(),
lat: mapboxData.center && mapboxData.center.length ? mapboxData.center[0] : null,
lng: mapboxData.center && mapboxData.center.length ? mapboxData.center[1] : null,
lng: mapboxData.center && mapboxData.center.length ? mapboxData.center[0] : null,
lat: mapboxData.center && mapboxData.center.length ? mapboxData.center[1] : null,
}
let mutation =

View File

@ -1,10 +1,9 @@
import { gql } from '../../../helpers/jest'
import Factory from '../../../factories'
import Factory, { cleanDatabase } from '../../../db/factories'
import { getNeode, getDriver } from '../../../db/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../../server'
const factory = Factory()
const neode = getNeode()
const driver = getDriver()
let authenticatedUser, mutate, variables
@ -42,7 +41,7 @@ const updateUserMutation = gql`
let newlyCreatedNodesWithLocales = [
{
city: {
lng: 41.1534,
lat: 41.1534,
nameES: 'Hamburg',
nameFR: 'Hamburg',
nameIT: 'Hamburg',
@ -55,7 +54,7 @@ let newlyCreatedNodesWithLocales = [
name: 'Hamburg',
namePL: 'Hamburg',
id: 'place.5977106083398860',
lat: -74.5763,
lng: -74.5763,
},
state: {
namePT: 'Nova Jérsia',
@ -107,7 +106,7 @@ beforeEach(() => {
})
afterEach(() => {
factory.cleanDatabase()
cleanDatabase()
})
describe('userMiddleware', () => {
@ -146,12 +145,12 @@ describe('userMiddleware', () => {
})
describe('UpdateUser', () => {
let user, userParams
let user
beforeEach(async () => {
newlyCreatedNodesWithLocales = [
{
city: {
lng: 53.55,
lat: 53.55,
nameES: 'Hamburgo',
nameFR: 'Hambourg',
nameIT: 'Amburgo',
@ -164,7 +163,7 @@ describe('userMiddleware', () => {
namePL: 'Hamburg',
name: 'Hamburg',
id: 'region.10793468240398860',
lat: 10,
lng: 10,
},
country: {
namePT: 'Alemanha',
@ -182,10 +181,9 @@ describe('userMiddleware', () => {
},
},
]
userParams = {
user = await Factory.build('user', {
id: 'updating-user',
}
user = await factory.create('User', userParams)
})
authenticatedUser = await user.toJson()
})

View File

@ -1,11 +1,10 @@
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../../server'
import Factory from '../../../factories'
import { cleanDatabase } from '../../../db/factories'
import { gql } from '../../../helpers/jest'
import { getNeode, getDriver } from '../../../db/neo4j'
const driver = getDriver()
const factory = Factory()
const neode = getNeode()
let currentUser
@ -30,7 +29,7 @@ beforeEach(() => {
})
afterEach(async () => {
await factory.cleanDatabase()
await cleanDatabase()
})
describe('mutedUsers', () => {

View File

@ -17,3 +17,15 @@ enum ReasonCategory {
advert_products_services_commercial
criminal_behavior_violation_german_law
}
type FiledReport {
createdAt: String!
reasonCategory: ReasonCategory!
reasonDescription: String!
reportId: ID!
resource: ReportedResource!
}
type Mutation {
fileReport(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): FiledReport
}

View File

@ -31,3 +31,7 @@ type Query {
type Mutation {
markAsRead(id: ID!): NOTIFIED
}
type Subscription {
notificationAdded(userId: ID!): NOTIFIED
}

View File

@ -4,7 +4,6 @@ type REVIEWED {
disable: Boolean!
closed: Boolean!
report: Report
# @cypher(statement: "MATCH (report:Report)<-[this:REVIEWED]-(:User) RETURN report")
moderator: User
resource: ReviewedResource
}

View File

@ -5,9 +5,9 @@ type Report {
rule: ReportRule!
disable: Boolean!
closed: Boolean!
filed: [FILED]
filed: [FILED]!
reviewed: [REVIEWED]!
resource: ReportedResource
resource: ReportedResource!
}
union ReportedResource = User | Post | Comment
@ -16,10 +16,6 @@ enum ReportRule {
latestReviewUpdatedAtRules
}
type Mutation {
fileReport(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): Report
}
type Query {
reports(orderBy: ReportOrdering, first: Int, offset: Int, reviewed: Boolean, closed: Boolean): [Report]
}

View File

@ -27,7 +27,7 @@ type User {
id: ID!
actorId: String
name: String
email: String! @cypher(statement: "MATCH (this)-[: PRIMARY_EMAIL]->(e: EmailAddress) RETURN e.email")
email: String! @cypher(statement: "MATCH (this)-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e.email")
slug: String!
avatar: String
coverImg: String
@ -38,7 +38,7 @@ type User {
invitedBy: User @relation(name: "INVITED", direction: "IN")
invited: [User] @relation(name: "INVITED", direction: "OUT")
location: Location @cypher(statement: "MATCH (this)-[: IS_IN]->(l: Location) RETURN l")
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
locationName: String
about: String
socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN")
@ -53,22 +53,28 @@ type User {
showShoutsPublicly: Boolean
locale: String
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
friendsCount: Int! @cypher(statement: "MATCH (this)<-[: FRIENDS]->(r: User) RETURN COUNT(DISTINCT r)")
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
following: [User]! @relation(name: "FOLLOWS", direction: "OUT")
followingCount: Int! @cypher(statement: "MATCH (this)-[: FOLLOWS]->(r: User) RETURN COUNT(DISTINCT r)")
followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)")
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
followedByCount: Int! @cypher(statement: "MATCH (this)<-[: FOLLOWS]-(r: User) RETURN COUNT(DISTINCT r)")
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
# Is the currently logged in user following that user?
followedByCurrentUser: Boolean! @cypher(
statement: """
MATCH (this)<-[: FOLLOWS]-(u: User { id: $cypherParams.currentUserId})
MATCH (this)<-[:FOLLOWS]-(u:User { id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1
"""
)
isBlocked: Boolean! @cypher(
statement: """
MATCH (this)<-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId})
RETURN COUNT(user) >= 1
"""
)
blocked: Boolean! @cypher(
statement: """
MATCH (this)-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId})
@ -91,22 +97,22 @@ type User {
contributions: [Post]! @relation(name: "WROTE", direction: "OUT")
contributionsCount: Int! @cypher(
statement: """
MATCH (this)-[: WROTE]->(r: Post)
MATCH (this)-[:WROTE]->(r:Post)
WHERE NOT r.deleted = true AND NOT r.disabled = true
RETURN COUNT(r)
"""
)
comments: [Comment]! @relation(name: "WROTE", direction: "OUT")
commentedCount: Int! @cypher(statement: "MATCH (this)-[: WROTE]->(: Comment)-[: COMMENTS]->(p: Post) WHERE NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))")
commentedCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(:Comment)-[:COMMENTS]->(p:Post) WHERE NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))")
shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
shoutedCount: Int! @cypher(statement: "MATCH (this)-[: SHOUTED]->(r: Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
badgesCount: Int! @cypher(statement: "MATCH (this)<-[: REWARDED]-(r: Badge) RETURN COUNT(r)")
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
emotions: [EMOTED]
}

View File

@ -1,4 +1,5 @@
import express from 'express'
import http from 'http'
import helmet from 'helmet'
import { ApolloServer } from 'apollo-server-express'
import CONFIG from './config'
@ -7,11 +8,35 @@ import { getNeode, getDriver } from './db/neo4j'
import decode from './jwt/decode'
import schema from './schema'
import webfinger from './activitypub/routes/webfinger'
import { RedisPubSub } from 'graphql-redis-subscriptions'
import { PubSub } from 'graphql-subscriptions'
import Redis from 'ioredis'
import bodyParser from 'body-parser'
export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
const { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD } = CONFIG
let prodPubsub, devPubsub
const options = {
host: REDIS_DOMAIN,
port: REDIS_PORT,
password: REDIS_PASSWORD,
retryStrategy: times => {
return Math.min(times * 50, 2000)
},
}
if (options.host && options.port && options.password) {
prodPubsub = new RedisPubSub({
publisher: new Redis(options),
subscriber: new Redis(options),
})
} else {
devPubsub = new PubSub()
}
export const pubsub = prodPubsub || devPubsub
const driver = getDriver()
const neode = getNeode()
export const context = async ({ req }) => {
const getContext = async req => {
const user = await decode(driver, req.headers.authorization)
return {
driver,
@ -23,11 +48,24 @@ export const context = async ({ req }) => {
},
}
}
export const context = async options => {
const { connection, req } = options
if (connection) {
return connection.context
} else {
return getContext(req)
}
}
const createServer = options => {
const defaults = {
context,
schema: middleware(schema),
subscriptions: {
onConnect: (connectionParams, webSocket) => {
return getContext(connectionParams)
},
},
debug: !!CONFIG.DEBUG,
tracing: !!CONFIG.DEBUG,
formatError: error => {
@ -45,9 +83,13 @@ const createServer = options => {
app.use(helmet())
app.use('/.well-known/', webfinger())
app.use(express.static('public'))
app.use(bodyParser.json({ limit: '10mb' }))
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }))
server.applyMiddleware({ app, path: '/' })
const httpServer = http.createServer(app)
server.installSubscriptionHandlers(httpServer)
return { server, app }
return { server, httpServer, app }
}
export default createServer

View File

@ -3,18 +3,18 @@ import { Given, When, Then, AfterAll } from 'cucumber'
import { expect } from 'chai'
// import { client } from '../../../src/activitypub/apollo-client'
import { GraphQLClient } from 'graphql-request'
import Factory from '../../../src/factories'
import Factory from '../../../src/db/factories'
const debug = require('debug')('ea:test:steps')
const factory = Factory()
const client = new GraphQLClient(host)
function createUser (slug) {
debug(`creating user ${slug}`)
return factory.create('User', {
return Factory.build('user', {
name: slug,
}, {
password: '1234',
email: 'example@test.org',
password: '1234'
})
// await login({ email: 'example@test.org', password: '1234' })
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,8 @@
{
"projectId": "qa7fe2",
"ignoreTestFiles": "*.js",
"baseUrl": "http://localhost:3000"
"baseUrl": "http://localhost:3000",
"env": {
"RETRIES": 2
}
}

View File

@ -1,2 +1,2 @@
// please change also version in file "webapp/constants/terms-and-conditions-version.js"
export const VERSION = '0.0.3'
export const VERSION = '0.0.4'

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@ -14,9 +14,8 @@ Feature: Tags and Categories
looking at the popularity of a tag.
Background:
Given my user account has the role "admin"
Given I am logged in with a "admin" role
And we have a selection of tags and categories as well as posts
And I am logged in
Scenario: See an overview of categories
When I navigate to the administration dashboard

View File

@ -1,5 +1,8 @@
import { When, Then } from "cypress-cucumber-preprocessor/steps";
import locales from '../../../webapp/locales'
import orderBy from 'lodash/orderBy'
const languages = orderBy(locales, 'name')
const narratorAvatar =
"https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg";
@ -27,7 +30,7 @@ Then("my comment should be successfully created", () => {
});
Then("I should see my comment", () => {
cy.get("div.comment p")
cy.get("article.comment-card p")
.should("contain", "Human Connection rocks")
.get(".user-avatar img")
.should("have.attr", "src")
@ -37,12 +40,12 @@ Then("I should see my comment", () => {
});
Then("I should see the entirety of my comment", () => {
cy.get("div.comment")
cy.get("article.comment-card")
.should("not.contain", "show more")
});
Then("I should see an abreviated version of my comment", () => {
cy.get("div.comment")
cy.get("article.comment-card")
.should("contain", "show more")
});
@ -57,7 +60,7 @@ Then("it should create a mention in the CommentForm", () => {
})
When("I open the content menu of post {string}", (title)=> {
cy.contains('.post-card', title)
cy.contains('.post-teaser', title)
.find('.content-menu .base-button')
.click()
})
@ -74,12 +77,89 @@ Then("there is no button to pin a post", () => {
})
And("the post with title {string} has a ribbon for pinned posts", (title) => {
cy.get("article.post-card").contains(title)
cy.get(".post-teaser").contains(title)
.parent()
.find("div.ribbon.ribbon--pinned")
.parent()
.find(".ribbon.--pinned")
.should("contain", "Announcement")
})
Then("I see a toaster with {string}", (title) => {
cy.get(".iziToast-message").should("contain", title);
})
Then("I should be able to {string} a teaser image", condition => {
cy.reload()
const teaserImageUpload = (condition === 'change') ? "humanconnection.png" : "onourjourney.png";
cy.fixture(teaserImageUpload).as('postTeaserImage').then(function() {
cy.get("#postdropzone").upload(
{ fileContent: this.postTeaserImage, fileName: teaserImageUpload, mimeType: "image/png" },
{ subjectType: "drag-n-drop", force: true }
);
})
})
Then('confirm crop', () => {
cy.get('.crop-confirm')
.click()
})
Then("I add all required fields", () => {
cy.get('input[name="title"]')
.type('new post')
.get(".editor .ProseMirror")
.type('new post content')
.get(".categories-select .base-button")
.first()
.click()
.get('.base-card > .select-field input')
.click()
.get('.ds-select-option')
.eq(languages.findIndex(l => l.code === 'en'))
.click()
})
Then("the post was saved successfully with the {string} teaser image", condition => {
cy.get(".base-card > .title")
.should("contain", condition === 'updated' ? 'to be updated' : 'new post')
.get(".content")
.should("contain", condition === 'updated' ? 'successfully updated' : 'new post content')
.get('.post-page img')
.should("have.attr", "src")
.and("contains", condition === 'updated' ? 'humanconnection' : 'onourjourney')
})
Then("the first image should not be displayed anymore", () => {
cy.get(".hero-image")
.children()
.get('.hero-image > .image')
.should('have.length', 1)
.and('have.attr', 'src')
})
Then('the {string} post was saved successfully without a teaser image', condition => {
cy.get(".base-card > .title")
.should("contain", condition === 'updated' ? 'to be updated' : 'new post')
.get(".content")
.should("contain", condition === 'updated' ? 'successfully updated' : 'new post content')
.get('.post-page')
.should('exist')
.get('.hero-image > .image')
.should('not.exist')
})
Then('I should be able to remove it', () => {
cy.get('.crop-cancel')
.click()
})
When('my post has a teaser image', () => {
cy.get('.contribution-form .image')
.should('exist')
.and('have.attr', 'src')
})
Then('I should be able to remove the image', () => {
cy.get('.dz-message > .base-button')
.click()
})

View File

@ -29,7 +29,7 @@ When("I visit another user's profile page", () => {
});
Then("I cannot upload a picture", () => {
cy.get(".ds-card-content")
cy.get(".base-card")
.children()
.should("not.have.id", "customdropzone")
.should("have.class", "user-avatar");

View File

@ -12,7 +12,7 @@ let annoyingUserWhoMutedModeratorTitle = 'Fake news'
const savePostTitle = $post => {
return $post
.first()
.find('.ds-heading')
.find('.title')
.first()
.invoke('text')
.then(title => {
@ -30,20 +30,28 @@ Given("I see David Irving's post on the post page", page => {
})
Given('I am logged in with a {string} role', role => {
cy.factory().create('User', {
cy.factory().build('user', {
termsAndConditionsAgreedVersion: VERSION,
role,
name: `${role} is my name`
}, {
email: `${role}@example.org`,
password: '1234',
termsAndConditionsAgreedVersion: VERSION,
role
})
cy.login({
email: `${role}@example.org`,
password: '1234'
})
cy.neode()
.first("User", {
name: `${role} is my name`,
})
.then(user => {
return new Cypress.Promise((resolve, reject) => {
return user.toJson().then((user) => resolve(user))
})
})
.then(user => cy.login(user))
})
When('I click on "Report Post" from the content menu of the post', () => {
cy.contains('.ds-card', davidIrvingPostTitle)
cy.contains('.base-card', davidIrvingPostTitle)
.find('.content-menu .base-button')
.click({force: true})
@ -53,7 +61,7 @@ When('I click on "Report Post" from the content menu of the post', () => {
})
When('I click on "Report User" from the content menu in the user info box', () => {
cy.contains('.ds-card', davidIrvingPostTitle)
cy.contains('.base-card', davidIrvingPostTitle)
.get('.user-content-menu .base-button')
.click({ force: true })
@ -70,7 +78,7 @@ When('I click on the author', () => {
When('I report the author', () => {
cy.get('.page-name-profile-id-slug').then(() => {
invokeReportOnElement('.ds-card').then(() => {
invokeReportOnElement('.base-card').then(() => {
cy.get('button')
.contains('Send')
.click()
@ -127,11 +135,11 @@ Given('somebody reported the following posts:', table => {
password: '1234'
}
cy.factory()
.create('User', submitter)
.build('user', {}, submitter)
.authenticateAs(submitter)
.mutate(gql`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
id
reportId
}
}`, {
resourceId,
@ -161,13 +169,14 @@ Then('each list item links to the post page', () => {
Then('I can visit the post page', () => {
cy.contains(annoyingUserWhoMutedModeratorTitle).click()
cy.location('pathname').should('contain', '/post')
.get('h3').should('contain', annoyingUserWhoMutedModeratorTitle)
.get('.base-card .title').should('contain', annoyingUserWhoMutedModeratorTitle)
})
When("they have a post someone has reported", () => {
cy.factory()
.create("Post", {
authorId: 'annnoying-user',
.build("post", {
title,
}, {
authorId: 'annnoying-user',
});
})

View File

@ -1,6 +1,6 @@
import { When, Then } from "cypress-cucumber-preprocessor/steps";
When("I search for {string}", value => {
cy.get(".searchable-input .ds-select-search")
cy.get(".searchable-input .ds-select input")
.focus()
.type(value);
});
@ -25,7 +25,7 @@ Then("the search should contain the annoying user", () => {
expect($li).to.have.length(1);
})
cy.get(".ds-select-dropdown .user-teaser .slug").should("contain", '@spammy-spammer');
cy.get(".searchable-input .ds-select-search")
cy.get(".searchable-input .ds-select input")
.focus()
.type("{esc}");
})
@ -44,21 +44,21 @@ Then("I should see the following users in the select dropdown:", table => {
});
When("I type {string} and press Enter", value => {
cy.get(".searchable-input .ds-select-search")
cy.get(".searchable-input .ds-select input")
.focus()
.type(value)
.type("{enter}", { force: true });
});
When("I type {string} and press escape", value => {
cy.get(".searchable-input .ds-select-search")
cy.get(".searchable-input .ds-select input")
.focus()
.type(value)
.type("{esc}");
});
Then("the search field should clear", () => {
cy.get(".searchable-input .ds-select-search").should("have.text", "");
cy.get(".searchable-input .ds-select input").should("have.text", "");
});
When("I select a post entry", () => {

View File

@ -80,7 +80,7 @@ Then('I should be on the {string} page', page => {
.should(loc => {
expect(loc.pathname).to.eq(page)
})
.get('h3')
.get('h2')
.should('contain', 'Social media')
})
@ -112,7 +112,7 @@ Given('I have added a social media link', () => {
})
Then('they should be able to see my social media links', () => {
cy.get('.ds-card-content')
cy.get('.base-card')
.contains('Where else can I find Peter Pan?')
.get('a[href="https://freeradical.zone/peter-pan"]')
.should('have.length', 1)

View File

@ -25,7 +25,6 @@ const narratorParams = {
name: "Peter Pan",
slug: "peter-pan",
avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg",
...loginCredentials,
...termsAndConditionsAgreedVersion,
};
@ -33,65 +32,82 @@ const annoyingParams = {
email: "spammy-spammer@example.org",
slug: 'spammy-spammer',
password: "1234",
...termsAndConditionsAgreedVersion
};
Given("I am logged in", () => {
cy.login(loginCredentials);
cy.neode()
.first("User", {
name: narratorParams.name
})
.then(user => {
return new Cypress.Promise((resolve, reject) => {
return user.toJson().then((user) => resolve(user))
})
})
.then(user => cy.login(user))
});
Given("I log in as {string}", name => {
cy.logout()
cy.neode()
.first("User", {
name
})
.then(user => {
return new Cypress.Promise((resolve, reject) => {
return user.toJson().then((user) => resolve(user))
})
})
.then(user => cy.login(user))
})
Given("the {string} user searches for {string}", (_, postTitle) => {
cy.logout()
.login({ email: annoyingParams.email, password: '1234' })
.get(".searchable-input .ds-select-search")
cy.neode()
.first("User", {
id: "annoying-user"
})
.then(user => {
return new Cypress.Promise((resolve, reject) => {
return user.toJson().then((user) => resolve(user))
})
})
.then(user => cy.login(user))
cy.get(".searchable-input .ds-select input")
.focus()
.type(postTitle);
});
Given("we have a selection of categories", () => {
cy.createCategories("cat0", "just-for-fun");
cy.factory().build('category', { id: "cat0", slug: "just-for-fun" });
});
Given("we have a selection of tags and categories as well as posts", () => {
cy.createCategories("cat12")
.factory()
.create("Tag", {
id: "Ecology"
})
.create("Tag", {
id: "Nature"
})
.create("Tag", {
id: "Democracy"
});
cy.factory()
.create("User", {
id: 'a1'
})
.create("Post", {
.build('category', { id: 'cat12', name: "Just For Fun", icon: "smile", })
.build('category', { id: 'cat121', name: "Happiness & Values", icon: "heart-o"})
.build('category', { id: 'cat122', name: "Health & Wellbeing", icon: "medkit"})
.build("tag", { id: "Ecology" })
.build("tag", { id: "Nature" })
.build("tag", { id: "Democracy" })
.build("user", { id: 'a1' })
.build("post", {}, {
authorId: 'a1',
tagIds: ["Ecology", "Nature", "Democracy"],
categoryIds: ["cat12"]
})
.create("Post", {
.build("post", {}, {
authorId: 'a1',
tagIds: ["Nature", "Democracy"],
categoryIds: ["cat121"]
});
cy.factory()
.create("User", {
id: 'a2'
})
.create("Post", {
.build("user", { id: 'a2' })
.build("post", {}, {
authorId: 'a2',
tagIds: ['Nature', 'Democracy'],
categoryIds: ["cat12"]
});
cy.factory()
.create("Post", {
authorId: narratorParams.id,
})
.build("post", {}, {
tagIds: ['Democracy'],
categoryIds: ["cat122"]
})
@ -99,23 +115,22 @@ Given("we have a selection of tags and categories as well as posts", () => {
Given("we have the following user accounts:", table => {
table.hashes().forEach(params => {
cy.factory().create("User", {
cy.factory().build("user", {
...params,
...termsAndConditionsAgreedVersion
});
}, params);
});
});
Given("I have a user account", () => {
cy.factory().create("User", narratorParams);
cy.factory().build("user", narratorParams, loginCredentials);
});
Given("my user account has the role {string}", role => {
cy.factory().create("User", {
cy.factory().build("user", {
role,
...loginCredentials,
...termsAndConditionsAgreedVersion,
});
}, loginCredentials);
});
When("I log out", cy.logout);
@ -130,8 +145,17 @@ When("I visit the {string} page", page => {
When("a blocked user visits the post page of one of my authored posts", () => {
cy.logout()
.login({ email: annoyingParams.email, password: annoyingParams.password })
.openPage('/post/previously-created-post')
cy.neode()
.first("User", {
name: 'Harassing User'
})
.then(user => {
return new Cypress.Promise((resolve, reject) => {
return user.toJson().then((user) => resolve(user))
})
})
.then(user => cy.login(user))
cy.openPage('post/previously-created-post')
})
Given("I am on the {string} page", page => {
@ -139,11 +163,12 @@ Given("I am on the {string} page", page => {
});
When("I fill in my email and password combination and click submit", () => {
cy.login(loginCredentials);
cy.manualLogin(loginCredentials);
});
When(/(?:when )?I refresh the page/, () => {
cy.reload();
cy.visit('/')
.reload();
});
When("I log out through the menu in the top right corner", () => {
@ -203,33 +228,29 @@ When("I press {string}", label => {
cy.contains(label).click();
});
Given("we have this user in our database:", table => {
const [firstRow] = table.hashes()
cy.factory().create('User', firstRow)
})
Given("we have the following posts in our database:", table => {
cy.factory().create('Category', {
id: `cat-456`,
name: "Just For Fun",
slug: `just-for-fun`,
icon: "smile"
})
table.hashes().forEach(({
...postAttributes
}, i) => {
postAttributes = {
...postAttributes,
deleted: Boolean(postAttributes.deleted),
disabled: Boolean(postAttributes.disabled),
pinned: Boolean(postAttributes.pinned),
categoryIds: ['cat-456']
}
cy.factory().create("Post", postAttributes);
Given("we have the following comments in our database:", table => {
table.hashes().forEach((attributesOrOptions, i) => {
cy.factory().build("comment", {
...attributesOrOptions,
}, {
...attributesOrOptions,
});
})
});
Given("we have the following posts in our database:", table => {
table.hashes().forEach((attributesOrOptions, i) => {
cy.factory().build("post", {
...attributesOrOptions,
deleted: Boolean(attributesOrOptions.deleted),
disabled: Boolean(attributesOrOptions.disabled),
pinned: Boolean(attributesOrOptions.pinned),
}, {
...attributesOrOptions,
});
})
})
Then("I see a success message:", message => {
cy.contains(message);
});
@ -242,15 +263,20 @@ When(
"I click on the big plus icon in the bottom right corner to create post",
() => {
cy.get(".post-add-button").click();
cy.location("pathname").should('eq', '/post/create')
}
);
Given("I previously created a post", () => {
lastPost.authorId = narratorParams.id
lastPost.title = "previously created post";
lastPost.content = "with some content";
lastPost = {
lastPost,
title: "previously created post",
content: "with some content",
};
cy.factory()
.create("Post", lastPost);
.build("post", lastPost, {
authorId: narratorParams.id
});
});
When("I choose {string} as the title of the post", title => {
@ -270,14 +296,14 @@ Then("I select a category", () => {
});
When("I choose {string} as the language for the post", (languageCode) => {
cy.get('.ds-flex-item > .ds-form-item .ds-select ')
cy.get('.contribution-form .ds-select')
.click().get('.ds-select-option')
.eq(languages.findIndex(l => l.code === languageCode)).click()
})
Then("the post shows up on the landing page at position {int}", index => {
cy.openPage("landing");
const selector = `.post-card:nth-child(${index}) > .ds-card-content`;
const selector = `.post-teaser:nth-child(${index}) > .base-card`;
cy.get(selector).should("contain", lastPost.title);
cy.get(selector).should("contain", lastPost.content);
});
@ -287,16 +313,16 @@ Then("I get redirected to {string}", route => {
});
Then("the post was saved successfully", () => {
cy.get(".ds-card-content > .ds-heading").should("contain", lastPost.title);
cy.get(".base-card > .title").should("contain", lastPost.title);
cy.get(".content").should("contain", lastPost.content);
});
Then(/^I should see only ([0-9]+) posts? on the landing page/, postCount => {
cy.get(".post-card").should("have.length", postCount);
cy.get(".post-teaser").should("have.length", postCount);
});
Then("the first post on the landing page has the title:", title => {
cy.get(".post-card:first").should("contain", title);
cy.get(".post-teaser:first").should("contain", title);
});
Then(
@ -311,17 +337,26 @@ Then(
cy.visit(route, {
failOnStatusCode: false
});
cy.get(".error").should("contain", message);
cy.get(".error-message").should("contain", message);
}
);
Given("my user account has the following login credentials:", table => {
Given("I am logged in with these credentials:", table => {
loginCredentials = table.hashes()[0];
cy.debug();
cy.factory().create("User", {
cy.factory().build("user", {
...termsAndConditionsAgreedVersion,
...loginCredentials
});
name: loginCredentials.email,
}, loginCredentials);
cy.neode()
.first("User", {
name: loginCredentials.email,
})
.then(user => {
return new Cypress.Promise((resolve, reject) => {
return user.toJson().then((user) => resolve(user))
})
})
.then(user => cy.login(user))
});
When("I fill the password form with:", table => {
@ -340,45 +375,16 @@ When("submit the form", () => {
Then("I cannot login anymore with password {string}", password => {
cy.reload();
const {
email
} = loginCredentials;
cy.visit(`/login`);
cy.get("input[name=email]")
.trigger("focus")
.type(email);
cy.get("input[name=password]")
.trigger("focus")
.type(password);
cy.get("button[name=submit]")
.as("submitButton")
.click();
cy.get(".iziToast-wrapper").should(
"contain",
"Incorrect email address or password."
);
const { email } = loginCredentials
cy.manualLogin({ email, password })
.get(".iziToast-wrapper").should("contain", "Incorrect email address or password.");
});
Then("I can login successfully with password {string}", password => {
cy.reload();
cy.login({
...loginCredentials,
...{
password
}
});
cy.get(".iziToast-wrapper").should("contain", "You are logged in!");
});
When("I log in with the following credentials:", table => {
const {
email,
password
} = table.hashes()[0];
cy.login({
email,
password
});
const { email } = loginCredentials
cy.manualLogin({ email, password })
.get(".iziToast-wrapper").should("contain", "You are logged in!");
});
When("open the notification menu and click on the first item", () => {
@ -414,13 +420,12 @@ When("mention {string} in the text", mention => {
cy.get(".suggestion-list__item")
.contains(mention)
.click();
cy.debug();
});
Then("the notification gets marked as read", () => {
cy.get(".notifications-menu-popover .notification")
.first()
.should("have.class", "read");
.should("have.class", "--read");
});
Then("there are no notifications in the top menu", () => {
@ -428,12 +433,11 @@ Then("there are no notifications in the top menu", () => {
});
Given("there is an annoying user called {string}", name => {
cy.factory().create("User", {
...annoyingParams,
cy.factory().build("user", {
id: "annoying-user",
name,
...termsAndConditionsAgreedVersion,
});
}, annoyingParams);
});
Given("there is an annoying user who has muted me", () => {
@ -451,15 +455,15 @@ Given("there is an annoying user who has muted me", () => {
});
Given("I am on the profile page of the annoying user", name => {
cy.openPage("/profile/annoying-user/spammy-spammer");
cy.openPage("profile/annoying-user/spammy-spammer");
});
When("I visit the profile page of the annoying user", name => {
cy.openPage("/profile/annoying-user");
cy.openPage("profile/annoying-user");
});
When("I ", name => {
cy.openPage("/profile/annoying-user");
cy.openPage("profile/annoying-user");
});
When(
@ -498,35 +502,33 @@ Given("I follow the user {string}", name => {
});
Given('{string} wrote a post {string}', (_, title) => {
cy.createCategories("cat21")
.factory()
.create("Post", {
authorId: 'annoying-user',
cy.factory()
.build("post", {
title,
categoryIds: ["cat21"]
}, {
authorId: 'annoying-user',
});
});
Then("the list of posts of this user is empty", () => {
cy.get(".ds-card-content").not(".post-link");
cy.get(".base-card").not(".post-link");
cy.get(".main-container").find(".ds-space.hc-empty");
});
Then("I get removed from his follower collection", () => {
cy.get(".ds-card-content").not(".post-link");
cy.get(".base-card").not(".post-link");
cy.get(".main-container").contains(
".ds-card-content",
".base-card",
"is not followed by anyone"
);
});
Given("I wrote a post {string}", title => {
cy.createCategories(`cat213`, title)
.factory()
.create("Post", {
authorId: narratorParams.id,
cy.factory()
.build("post", {
title,
categoryIds: ["cat213"]
}, {
authorId: narratorParams.id,
});
});
@ -552,22 +554,24 @@ When("I block the user {string}", name => {
.then(blockedUser => {
cy.neode()
.first("User", {
name: narratorParams.name
id: narratorParams.id
})
.relateTo(blockedUser, "blocked");
});
});
When("I log in with:", table => {
const [firstRow] = table.hashes();
const {
Email,
Password
} = firstRow;
cy.login({
email: Email,
password: Password
});
When("a user has blocked me", () => {
cy.neode()
.first("User", {
name: narratorParams.name
})
.then(blockedUser => {
cy.neode()
.first("User", {
name: 'Harassing User'
})
.relateTo(blockedUser, "blocked");
});
});
Then("I see only one post with the title {string}", title => {
@ -577,10 +581,31 @@ Then("I see only one post with the title {string}", title => {
cy.get(".main-container").contains(".post-link", title);
});
Then("they should not see the comment from", () => {
cy.get(".ds-card-footer").children().should('not.have.class', 'comment-form')
Then("they should not see the comment form", () => {
cy.get(".base-card").children().should('not.have.class', 'comment-form')
})
Then("they should see a text explaining commenting is not possible", () => {
Then("they should see a text explaining why commenting is not possible", () => {
cy.get('.ds-placeholder').should('contain', "Commenting is not possible at this time on this post.")
})
})
Then("I should see no users in my blocked users list", () => {
cy.get('.ds-placeholder')
.should('contain', "So far, you have not blocked anybody.")
})
Then("I {string} see {string} from the content menu in the user info box", (condition, link) => {
cy.get(".user-content-menu .base-button").click()
cy.get(".popover .ds-menu-item-link")
.should(condition === 'should' ? 'contain' : 'not.contain', link)
})
Then('I should not see {string} button', button => {
cy.get('.base-card .action-buttons')
.should('have.length', 1)
})
Then('I should see the {string} button', button => {
cy.get('.base-card .action-buttons .base-button')
.should('contain', button)
})

View File

@ -62,9 +62,8 @@ Feature: Report and Moderate
Given somebody reported the following posts:
| submitterEmail | resourceId | reasonCategory | reasonDescription |
| p2.submitter@example.org | p2 | other | Offensive content |
And my user account has the role "moderator"
And I am logged in with a "moderator" role
And there is an annoying user who has muted me
And I am logged in
When I click on the avatar menu in the top right corner
And I click on "Moderation"
Then I see all the reported posts including from the user who muted me

View File

@ -11,9 +11,7 @@ Feature: Notification for a mention
| Matt Rider | matt-rider | matt@example.org | 4321 |
Scenario: Mention another user, re-login as this user and see notifications
Given I log in with the following credentials:
| email | password |
| wolle@example.org | 1234 |
Given I log in as "Wolle aus Hamburg"
And I start to write a new post with the title "Hey Matt" beginning with:
"""
Big shout to our fellow contributor
@ -22,10 +20,7 @@ Feature: Notification for a mention
And I select a category
And I choose "en" as the language for the post
And I click on "Save"
When I log out
And I log in with the following credentials:
| email | password |
| matt@example.org | 4321 |
And I log in as "Matt Rider"
And see 1 unread notifications in the top menu
And open the notification menu and click on the first item
Then I get to the post page of ".../hey-matt"

View File

@ -6,8 +6,11 @@ Feature: Post Comment
Background:
Given I have a user account
And we have the following posts in our database:
| id | title | slug | authorId | commentContent |
| bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays | id-of-peter-pan | @peter-pan reply to me |
| id | title | slug | authorId |
| bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays | id-of-peter-pan |
And we have the following comments in our database:
| postId | content | authorId |
| bWBjpkTKZp | @peter-pan reply to me | id-of-peter-pan |
And I am logged in
Scenario: Comment creation

View File

@ -0,0 +1,19 @@
Feature: Delete Teaser Image
As a user
I would like to be able to remove an image I have previously added to my Post
So that I have control over the content of my Post
Background:
Given I have a user account
Given I am logged in
Given we have the following posts in our database:
| authorId | id | title | content |
| id-of-peter-pan | p1 | Post to be updated | successfully updated |
Scenario: Delete existing image
Given I am on the 'post/edit/p1' page
And my post has a teaser image
Then I should be able to remove the image
And I click on "Save"
Then I get redirected to ".../post-to-be-updated"
And the "updated" post was saved successfully without a teaser image

View File

@ -0,0 +1,47 @@
Feature: Upload Teaser Image
As a user
I would like to be able to add a teaser image to my Post
So that I can personalize my posts
Background:
Given I have a user account
Given I am logged in
Given we have the following posts in our database:
| authorId | id | title | content |
| id-of-peter-pan | p1 | Post to be updated | successfully updated |
Scenario: Create a Post with a Teaser Image
When I click on the big plus icon in the bottom right corner to create post
Then I should be able to "add" a teaser image
And confirm crop
And I add all required fields
And I click on "Save"
Then I get redirected to ".../new-post"
And the post was saved successfully with the "new" teaser image
Scenario: Update a Post to add an image
Given I am on the 'post/edit/p1' page
And I should be able to "change" a teaser image
And confirm crop
And I click on "Save"
Then I see a toaster with "Saved!"
And I get redirected to ".../post-to-be-updated"
Then the post was saved successfully with the "updated" teaser image
Scenario: Add image, then add a different image
When I click on the big plus icon in the bottom right corner to create post
Then I should be able to "add" a teaser image
And confirm crop
And I should be able to "change" a teaser image
And confirm crop
And the first image should not be displayed anymore
Scenario: Add image, then delete it
When I click on the big plus icon in the bottom right corner to create post
Then I should be able to "add" a teaser image
And I should be able to remove it
And I add all required fields
And I click on "Save"
Then I get redirected to ".../new-post"
And the "new" post was saved successfully without a teaser image

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