diff --git a/.github/ISSUE_TEMPLATE/devops_ticket.md b/.github/ISSUE_TEMPLATE/devops_ticket.md new file mode 100644 index 000000000..6f8ea55cb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/devops_ticket.md @@ -0,0 +1,24 @@ +--- +name: 💥 DevOps ticket +about: Help us manage our deployed App. +labels: devops +title: 💥 [DevOps] +--- + +## :fire: DevOps ticket + + +### Motive + + +### Related issues + + +### Implementation + + +### Validation + + +### Additional context + diff --git a/.github/ISSUE_TEMPLATE/refactor_tickets.md b/.github/ISSUE_TEMPLATE/refactor_tickets.md new file mode 100644 index 000000000..b6c799f67 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor_tickets.md @@ -0,0 +1,20 @@ +--- +name: 🔧 Refactor ticket +about: Help us improve our code by refactoring it. +labels: refactor +title: 🔧 [Refactor] +--- + +## :zap: Refactor ticket + + +### Motive + +### Related issues + + +### Implementation + + +### Additional context + diff --git a/.travis.yml b/.travis.yml index f598594cb..7cfb983cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,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 @@ -69,7 +72,3 @@ deploy: script: bash scripts/deploy.sh on: branch: master - - provider: script - script: bash scripts/github_release.sh - on: - branch: master diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 8565bda8e..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "eslint.validate": [ - "javascript", - "javascriptreact", - { - "language": "vue", - "autoFix": true - } - ], - "editor.formatOnSave": false, -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 842838f83..8067daad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,67 @@ 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.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) +- build(deps-dev): bump @storybook/addon-notes in /webapp [`75137ce`](https://github.com/Human-Connection/Human-Connection/commit/75137ce716dadcc6f0ceeed6a2b0fe5c50fa7b8f) +- Update to v0.3.0 [`dbe2c4c`](https://github.com/Human-Connection/Human-Connection/commit/dbe2c4cdd5bab2195c6369b84989507b9f7da768) + #### [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 +238,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) diff --git a/backend/package.json b/backend/package.json index 63d895802..1822a52ca 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "human-connection-backend", - "version": "0.2.2", + "version": "0.3.1", "description": "GraphQL Backend for Human Connection", "main": "src/index.js", "scripts": { @@ -38,12 +38,12 @@ }, "dependencies": { "@hapi/joi": "^17.1.0", - "@sentry/node": "^5.11.2", + "@sentry/node": "^5.12.3", "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.9.16", + "apollo-server": "~2.10.0", "apollo-server-express": "^2.9.16", "babel-plugin-transform-runtime": "^6.23.0", "bcryptjs": "~2.4.3", @@ -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.9", - "graphql-tag": "~2.10.1", + "graphql-redis-subscriptions": "^2.1.2", + "graphql-shield": "~7.0.11", + "graphql-tag": "~2.10.3", "helmet": "~3.21.2", + "ioredis": "^4.14.1", "jsonwebtoken": "~8.5.1", "linkifyjs": "~2.1.8", "lodash": "~4.17.14", "merge-graphql-schemas": "^1.7.6", - "metascraper": "^5.10.6", - "metascraper-audio": "^5.10.6", - "metascraper-author": "^5.10.6", + "metascraper": "^5.11.0", + "metascraper-audio": "^5.10.7", + "metascraper-author": "^5.10.7", "metascraper-clearbit-logo": "^5.3.0", - "metascraper-date": "^5.10.6", - "metascraper-description": "^5.10.6", - "metascraper-image": "^5.10.6", - "metascraper-lang": "^5.10.6", + "metascraper-date": "^5.10.7", + "metascraper-description": "^5.11.0", + "metascraper-image": "^5.11.1", + "metascraper-lang": "^5.10.7", "metascraper-lang-detector": "^4.10.2", - "metascraper-logo": "^5.10.6", - "metascraper-publisher": "^5.10.6", - "metascraper-soundcloud": "^5.10.6", - "metascraper-title": "^5.10.6", - "metascraper-url": "^5.10.6", - "metascraper-video": "^5.10.6", - "metascraper-youtube": "^5.10.6", + "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", "migrate": "^1.6.2", "minimatch": "^3.0.4", "mustache": "^4.0.0", @@ -93,9 +95,10 @@ "nodemailer": "^6.4.2", "nodemailer-html-to-text": "^3.1.0", "npm-run-all": "~4.1.5", - "request": "~2.88.0", + "request": "~2.88.2", "sanitize-html": "~1.21.1", "slug": "~2.1.1", + "subscriptions-transport-ws": "^0.9.16", "trunc-html": "~1.1.2", "uuid": "~3.4.0", "validator": "^12.2.0", @@ -109,7 +112,7 @@ "@babel/plugin-proposal-throw-expressions": "^7.8.3", "@babel/preset-env": "~7.8.4", "@babel/register": "^7.8.3", - "apollo-server-testing": "~2.9.16", + "apollo-server-testing": "~2.10.0", "babel-core": "~7.0.0-0", "babel-eslint": "~10.0.3", "babel-jest": "~25.1.0", @@ -118,8 +121,8 @@ "eslint": "~6.8.0", "eslint-config-prettier": "~6.10.0", "eslint-config-standard": "~14.1.0", - "eslint-plugin-import": "~2.20.0", - "eslint-plugin-jest": "~23.6.0", + "eslint-plugin-import": "~2.20.1", + "eslint-plugin-jest": "~23.7.0", "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" } } diff --git a/backend/src/activitypub/routes/webfinger.spec.js b/backend/src/activitypub/routes/webfinger.spec.js index 06ca4577d..3cd9613e7 100644 --- a/backend/src/activitypub/routes/webfinger.spec.js +++ b/backend/src/activitypub/routes/webfinger.spec.js @@ -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 () => { diff --git a/backend/src/config/index.js b/backend/src/config/index.js index 2f8d0ed22..398bc6ff2 100644 --- a/backend/src/config/index.js +++ b/backend/src/config/index.js @@ -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, } diff --git a/backend/src/db/clean.js b/backend/src/db/clean.js index cbb1412e2..e658317e2 100644 --- a/backend/src/db/clean.js +++ b/backend/src/db/clean.js @@ -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!`) diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js new file mode 100644 index 000000000..754f84dec --- /dev/null +++ b/backend/src/db/factories.js @@ -0,0 +1,229 @@ +import uuid from 'uuid/v4' +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 diff --git a/backend/src/db/migrate/template.js b/backend/src/db/migrate/template.js index 1d63673b4..9adb0786d 100644 --- a/backend/src/db/migrate/template.js +++ b/backend/src/db/migrate/template.js @@ -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() } diff --git a/backend/src/db/migrations/20200123150105-merge_duplicate_user_accounts.js b/backend/src/db/migrations/20200123150105-merge_duplicate_user_accounts.js index ec38befc5..377f971e9 100644 --- a/backend/src/db/migrations/20200123150105-merge_duplicate_user_accounts.js +++ b/backend/src/db/migrations/20200123150105-merge_duplicate_user_accounts.js @@ -34,12 +34,11 @@ export function up(next) { return txc .run( ` - MATCH (oldUser:User)-[:PRIMARY_EMAIL]->(oldEmail:EmailAddress {email: $email}), (oldUser)-[previousRelationship]-(oldEmail) + MATCH (oldUser:User)-[:PRIMARY_EMAIL]->(oldEmail:EmailAddress {email: $email}) MATCH (user:User)-[:PRIMARY_EMAIL]->(email:EmailAddress {email: $normalizedEmail}) - DELETE previousRelationship WITH oldUser, oldEmail, user, email - CALL apoc.refactor.mergeNodes([user, oldUser], { properties: 'discard', mergeRels: true }) YIELD node as mergedUser - CALL apoc.refactor.mergeNodes([email, oldEmail], { properties: 'discard', mergeRels: true }) YIELD node as mergedEmail + CALL apoc.refactor.mergeNodes([user, oldUser], { properties: { createdAt: 'overwrite', \`.*\`: 'discard' }, mergeRels: true }) YIELD node as mergedUser + CALL apoc.refactor.mergeNodes([email, oldEmail], { properties: { createdAt: 'overwrite', verifiedAt: 'overwrite', \`.*\`: 'discard' }, mergeRels: true }) YIELD node as mergedEmail RETURN user {.*}, email {.*} `, { email, normalizedEmail }, diff --git a/backend/src/db/migrations/20200206190233-swap_latitude_with_longitude.js b/backend/src/db/migrations/20200206190233-swap_latitude_with_longitude.js new file mode 100644 index 000000000..619e30320 --- /dev/null +++ b/backend/src/db/migrations/20200206190233-swap_latitude_with_longitude.js @@ -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) +} diff --git a/backend/src/db/seed.js b/backend/src/db/seed.js index ba7ace90b..d1e430629 100644 --- a/backend/src/db/seed.js +++ b/backend/src/db/seed.js @@ -2,7 +2,7 @@ import faker from 'faker' import sample from 'lodash/sample' import { createTestClient } from 'apollo-server-testing' import createServer from '../server' -import Factory from '../factories' +import Factory from '../db/factories' import { getNeode, getDriver } from '../db/neo4j' import { gql } from '../helpers/jest' @@ -12,7 +12,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] ;(async function() { let authenticatedUser = null const driver = getDriver() - const factory = Factory() const neode = getNeode() try { @@ -28,12 +27,12 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] const { mutate } = createTestClient(server) const [Hamburg, Berlin, Germany, Paris, France] = await Promise.all([ - factory.create('Location', { + Factory.build('location', { id: 'region.5127278006398860', name: 'Hamburg', type: 'region', - lat: 10.0, - lng: 53.55, + lng: 10.0, + lat: 53.55, nameES: 'Hamburgo', nameFR: 'Hambourg', nameIT: 'Amburgo', @@ -44,12 +43,12 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] namePL: 'Hamburg', nameRU: 'Гамбург', }), - factory.create('Location', { + Factory.build('location', { id: 'region.14880313158564380', type: 'region', name: 'Berlin', - lat: 13.38333, - lng: 52.51667, + lng: 13.38333, + lat: 52.51667, nameES: 'Berlín', nameFR: 'Berlin', nameIT: 'Berlino', @@ -60,7 +59,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] namePL: 'Berlin', nameRU: 'Берлин', }), - factory.create('Location', { + Factory.build('location', { id: 'country.10743216036480410', name: 'Germany', type: 'country', @@ -74,12 +73,12 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] nameEN: 'Germany', nameRU: 'Германия', }), - factory.create('Location', { + Factory.build('location', { id: 'region.9397217726497330', name: 'Paris', type: 'region', - lat: 2.35183, - lng: 48.85658, + lng: 2.35183, + lat: 48.85658, nameES: 'París', nameFR: 'Paris', nameIT: 'Parigi', @@ -90,7 +89,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] namePL: 'Paryż', nameRU: 'Париж', }), - factory.create('Location', { + Factory.build('location', { id: 'country.9759535382641660', name: 'France', type: 'country', @@ -112,27 +111,27 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] ]) const [racoon, rabbit, wolf, bear, turtle, rhino] = await Promise.all([ - factory.create('Badge', { + Factory.build('badge', { id: 'indiegogo_en_racoon', icon: '/img/badges/indiegogo_en_racoon.svg', }), - factory.create('Badge', { + Factory.build('badge', { id: 'indiegogo_en_rabbit', icon: '/img/badges/indiegogo_en_rabbit.svg', }), - factory.create('Badge', { + Factory.build('badge', { id: 'indiegogo_en_wolf', icon: '/img/badges/indiegogo_en_wolf.svg', }), - factory.create('Badge', { + Factory.build('badge', { id: 'indiegogo_en_bear', icon: '/img/badges/indiegogo_en_bear.svg', }), - factory.create('Badge', { + Factory.build('badge', { id: 'indiegogo_en_turtle', icon: '/img/badges/indiegogo_en_turtle.svg', }), - factory.create('Badge', { + Factory.build('badge', { id: 'indiegogo_en_rhino', icon: '/img/badges/indiegogo_en_rhino.svg', }), @@ -147,55 +146,90 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] louie, dagobert, ] = await Promise.all([ - factory.create('User', { - id: 'u1', - name: 'Peter Lustig', - slug: 'peter-lustig', - role: 'admin', - email: 'admin@example.org', - }), - factory.create('User', { - id: 'u2', - name: 'Bob der Baumeister', - slug: 'bob-der-baumeister', - role: 'moderator', - email: 'moderator@example.org', - }), - factory.create('User', { - id: 'u3', - name: 'Jenny Rostock', - slug: 'jenny-rostock', - role: 'user', - email: 'user@example.org', - }), - factory.create('User', { - id: 'u4', - name: 'Huey', - slug: 'huey', - role: 'user', - email: 'huey@example.org', - }), - factory.create('User', { - id: 'u5', - name: 'Dewey', - slug: 'dewey', - role: 'user', - email: 'dewey@example.org', - }), - factory.create('User', { - id: 'u6', - name: 'Louie', - slug: 'louie', - role: 'user', - email: 'louie@example.org', - }), - factory.create('User', { - id: 'u7', - name: 'Dagobert', - slug: 'dagobert', - role: 'user', - email: 'dagobert@example.org', - }), + Factory.build( + 'user', + { + id: 'u1', + name: 'Peter Lustig', + slug: 'peter-lustig', + role: 'admin', + }, + { + email: 'admin@example.org', + }, + ), + Factory.build( + 'user', + { + id: 'u2', + name: 'Bob der Baumeister', + slug: 'bob-der-baumeister', + role: 'moderator', + }, + { + email: 'moderator@example.org', + }, + ), + Factory.build( + 'user', + { + id: 'u3', + name: 'Jenny Rostock', + slug: 'jenny-rostock', + role: 'user', + }, + { + email: 'user@example.org', + }, + ), + Factory.build( + 'user', + { + id: 'u4', + name: 'Huey', + slug: 'huey', + role: 'user', + }, + { + email: 'huey@example.org', + }, + ), + Factory.build( + 'user', + { + id: 'u5', + name: 'Dewey', + slug: 'dewey', + role: 'user', + }, + { + email: 'dewey@example.org', + }, + ), + Factory.build( + 'user', + { + id: 'u6', + name: 'Louie', + slug: 'louie', + role: 'user', + }, + { + email: 'louie@example.org', + }, + ), + Factory.build( + 'user', + { + id: 'u7', + name: 'Dagobert', + slug: 'dagobert', + role: 'user', + }, + { + email: 'dagobert@example.org', + }, + ), ]) await Promise.all([ @@ -236,97 +270,97 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] ]) await Promise.all([ - factory.create('Category', { + Factory.build('category', { id: 'cat1', name: 'Just For Fun', slug: 'just-for-fun', icon: 'smile', }), - factory.create('Category', { + Factory.build('category', { id: 'cat2', name: 'Happiness & Values', slug: 'happiness-values', icon: 'heart-o', }), - factory.create('Category', { + Factory.build('category', { id: 'cat3', name: 'Health & Wellbeing', slug: 'health-wellbeing', icon: 'medkit', }), - factory.create('Category', { + Factory.build('category', { id: 'cat4', name: 'Environment & Nature', slug: 'environment-nature', icon: 'tree', }), - factory.create('Category', { + Factory.build('category', { id: 'cat5', name: 'Animal Protection', slug: 'animal-protection', icon: 'paw', }), - factory.create('Category', { + Factory.build('category', { id: 'cat6', name: 'Human Rights & Justice', slug: 'human-rights-justice', icon: 'balance-scale', }), - factory.create('Category', { + Factory.build('category', { id: 'cat7', name: 'Education & Sciences', slug: 'education-sciences', icon: 'graduation-cap', }), - factory.create('Category', { + Factory.build('category', { id: 'cat8', name: 'Cooperation & Development', slug: 'cooperation-development', icon: 'users', }), - factory.create('Category', { + Factory.build('category', { id: 'cat9', name: 'Democracy & Politics', slug: 'democracy-politics', icon: 'university', }), - factory.create('Category', { + Factory.build('category', { id: 'cat10', name: 'Economy & Finances', slug: 'economy-finances', icon: 'money', }), - factory.create('Category', { + Factory.build('category', { id: 'cat11', name: 'Energy & Technology', slug: 'energy-technology', icon: 'flash', }), - factory.create('Category', { + Factory.build('category', { id: 'cat12', name: 'IT, Internet & Data Privacy', slug: 'it-internet-data-privacy', icon: 'mouse-pointer', }), - factory.create('Category', { + Factory.build('category', { id: 'cat13', name: 'Art, Culture & Sport', slug: 'art-culture-sport', icon: 'paint-brush', }), - factory.create('Category', { + Factory.build('category', { id: 'cat14', name: 'Freedom of Speech', slug: 'freedom-of-speech', icon: 'bullhorn', }), - factory.create('Category', { + Factory.build('category', { id: 'cat15', name: 'Consumption & Sustainability', slug: 'consumption-sustainability', icon: 'shopping-cart', }), - factory.create('Category', { + Factory.build('category', { id: 'cat16', name: 'Global Peace & Nonviolence', slug: 'global-peace-nonviolence', @@ -335,104 +369,164 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] ]) const [environment, nature, democracy, freedom] = await Promise.all([ - factory.create('Tag', { + Factory.build('tag', { id: 'Environment', }), - factory.create('Tag', { + Factory.build('tag', { id: 'Nature', }), - factory.create('Tag', { + Factory.build('tag', { id: 'Democracy', }), - factory.create('Tag', { + Factory.build('tag', { id: 'Freedom', }), ]) const [p0, p1, p3, p4, p5, p6, p9, p10, p11, p13, p14, p15] = await Promise.all([ - factory.create('Post', { - author: peterLustig, - id: 'p0', - language: sample(languages), - image: faker.image.unsplash.food(300, 169), - categoryIds: ['cat16'], - imageBlurred: true, - imageAspectRatio: 300 / 169, - }), - factory.create('Post', { - author: bobDerBaumeister, - id: 'p1', - language: sample(languages), - image: faker.image.unsplash.technology(300, 1500), - categoryIds: ['cat1'], - imageAspectRatio: 300 / 1500, - }), - factory.create('Post', { - author: huey, - id: 'p3', - language: sample(languages), - categoryIds: ['cat3'], - }), - factory.create('Post', { - author: dewey, - id: 'p4', - language: sample(languages), - categoryIds: ['cat4'], - }), - factory.create('Post', { - author: louie, - id: 'p5', - language: sample(languages), - categoryIds: ['cat5'], - }), - factory.create('Post', { - authorId: 'u1', - id: 'p6', - language: sample(languages), - image: faker.image.unsplash.buildings(300, 857), - categoryIds: ['cat6'], - imageAspectRatio: 300 / 857, - }), - factory.create('Post', { - author: huey, - id: 'p9', - language: sample(languages), - categoryIds: ['cat9'], - }), - factory.create('Post', { - author: dewey, - id: 'p10', - categoryIds: ['cat10'], - imageBlurred: true, - }), - factory.create('Post', { - author: louie, - id: 'p11', - language: sample(languages), - image: faker.image.unsplash.people(300, 901), - categoryIds: ['cat11'], - imageAspectRatio: 300 / 901, - }), - factory.create('Post', { - author: bobDerBaumeister, - id: 'p13', - language: sample(languages), - categoryIds: ['cat13'], - }), - factory.create('Post', { - author: jennyRostock, - id: 'p14', - language: sample(languages), - image: faker.image.unsplash.objects(300, 200), - categoryIds: ['cat14'], - imageAspectRatio: 300 / 450, - }), - factory.create('Post', { - author: huey, - id: 'p15', - language: sample(languages), - categoryIds: ['cat15'], - }), + Factory.build( + 'post', + { + id: 'p0', + language: sample(languages), + image: faker.image.unsplash.food(300, 169), + imageBlurred: true, + imageAspectRatio: 300 / 169, + }, + { + categoryIds: ['cat16'], + author: peterLustig, + }, + ), + Factory.build( + 'post', + { + id: 'p1', + language: sample(languages), + image: faker.image.unsplash.technology(300, 1500), + imageAspectRatio: 300 / 1500, + }, + { + categoryIds: ['cat1'], + author: bobDerBaumeister, + }, + ), + Factory.build( + 'post', + { + id: 'p3', + language: sample(languages), + }, + { + categoryIds: ['cat3'], + author: huey, + }, + ), + Factory.build( + 'post', + { + id: 'p4', + language: sample(languages), + }, + { + categoryIds: ['cat4'], + author: dewey, + }, + ), + Factory.build( + 'post', + { + id: 'p5', + language: sample(languages), + }, + { + categoryIds: ['cat5'], + author: louie, + }, + ), + Factory.build( + 'post', + { + id: 'p6', + language: sample(languages), + image: faker.image.unsplash.buildings(300, 857), + imageAspectRatio: 300 / 857, + }, + { + categoryIds: ['cat6'], + author: peterLustig, + }, + ), + Factory.build( + 'post', + { + id: 'p9', + language: sample(languages), + }, + { + categoryIds: ['cat9'], + author: huey, + }, + ), + Factory.build( + 'post', + { + id: 'p10', + imageBlurred: true, + }, + { + categoryIds: ['cat10'], + author: dewey, + }, + ), + Factory.build( + 'post', + { + id: 'p11', + language: sample(languages), + image: faker.image.unsplash.people(300, 901), + imageAspectRatio: 300 / 901, + }, + { + categoryIds: ['cat11'], + author: louie, + }, + ), + Factory.build( + 'post', + { + id: 'p13', + language: sample(languages), + }, + { + categoryIds: ['cat13'], + author: bobDerBaumeister, + }, + ), + Factory.build( + 'post', + { + id: 'p14', + language: sample(languages), + image: faker.image.unsplash.objects(300, 200), + imageAspectRatio: 300 / 450, + }, + { + categoryIds: ['cat14'], + author: jennyRostock, + }, + ), + Factory.build( + 'post', + { + id: 'p15', + language: sample(languages), + }, + { + categoryIds: ['cat15'], + author: huey, + }, + ), ]) authenticatedUser = await louie.toJson() @@ -554,61 +648,116 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] authenticatedUser = null const comments = await Promise.all([ - factory.create('Comment', { - author: jennyRostock, - id: 'c1', - postId: 'p1', - }), - factory.create('Comment', { - author: huey, - id: 'c2', - postId: 'p1', - }), - factory.create('Comment', { - author: louie, - id: 'c3', - postId: 'p3', - }), - factory.create('Comment', { - author: jennyRostock, - id: 'c5', - postId: 'p3', - }), - factory.create('Comment', { - author: peterLustig, - id: 'c6', - postId: 'p4', - }), - factory.create('Comment', { - author: jennyRostock, - id: 'c7', - postId: 'p2', - }), - factory.create('Comment', { - author: huey, - id: 'c8', - postId: 'p15', - }), - factory.create('Comment', { - author: dewey, - id: 'c9', - postId: 'p15', - }), - factory.create('Comment', { - author: louie, - id: 'c10', - postId: 'p15', - }), - factory.create('Comment', { - author: jennyRostock, - id: 'c11', - postId: 'p15', - }), - factory.create('Comment', { - author: jennyRostock, - id: 'c12', - postId: 'p15', - }), + Factory.build( + 'comment', + { + id: 'c1', + }, + { + author: jennyRostock, + postId: 'p1', + }, + ), + Factory.build( + 'comment', + { + id: 'c2', + }, + { + author: huey, + postId: 'p1', + }, + ), + Factory.build( + 'comment', + { + id: 'c3', + }, + { + author: louie, + postId: 'p3', + }, + ), + Factory.build( + 'comment', + { + id: 'c5', + }, + { + author: jennyRostock, + postId: 'p3', + }, + ), + Factory.build( + 'comment', + { + id: 'c6', + }, + { + author: peterLustig, + postId: 'p4', + }, + ), + Factory.build( + 'comment', + { + id: 'c7', + }, + { + author: jennyRostock, + postId: 'p2', + }, + ), + Factory.build( + 'comment', + { + id: 'c8', + }, + { + author: huey, + postId: 'p15', + }, + ), + Factory.build( + 'comment', + { + id: 'c9', + }, + { + author: dewey, + postId: 'p15', + }, + ), + Factory.build( + 'comment', + { + id: 'c10', + }, + { + author: louie, + postId: 'p15', + }, + ), + Factory.build( + 'comment', + { + id: 'c11', + }, + { + author: jennyRostock, + postId: 'p15', + }, + ), + Factory.build( + 'comment', + { + id: 'c12', + }, + { + author: jennyRostock, + postId: 'p15', + }, + ), ]) const trollingComment = comments[0] @@ -675,10 +824,10 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] ]) const reports = await Promise.all([ - factory.create('Report'), - factory.create('Report'), - factory.create('Report'), - factory.create('Report'), + Factory.build('report'), + Factory.build('report'), + Factory.build('report'), + Factory.build('report'), ]) const reportAgainstDagobert = reports[0] const reportAgainstTrollingPost = reports[1] @@ -784,231 +933,337 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] trollingComment.update({ disabled: true, updatedAt: new Date().toISOString(), closed: true }), ]) + await Promise.all([...Array(30).keys()].map(() => Factory.build('user'))) + await Promise.all( - [...Array(30).keys()].map(i => { - return factory.create('User') - }), + [...Array(30).keys()].map(() => + Factory.build( + 'post', + { + image: faker.image.unsplash.objects(), + }, + { + categoryIds: ['cat1'], + author: jennyRostock, + }, + ), + ), ) await Promise.all( - [...Array(30).keys()].map(() => { - return factory.create('Post', { - author: jennyRostock, - image: faker.image.unsplash.objects(), - }) - }), + [...Array(6).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: jennyRostock, + postId: 'p2', + }, + ), + ), ) await Promise.all( - [...Array(6).keys()].map(() => { - return factory.create('Comment', { - author: jennyRostock, - postId: 'p2', - }) - }), + [...Array(4).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: jennyRostock, + postId: 'p15', + }, + ), + ), ) await Promise.all( - [...Array(4).keys()].map(() => { - return factory.create('Comment', { - author: jennyRostock, - postId: 'p15', - }) - }), + [...Array(2).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: jennyRostock, + postId: 'p4', + }, + ), + ), ) await Promise.all( - [...Array(2).keys()].map(() => { - return factory.create('Comment', { - author: jennyRostock, - postId: 'p4', - }) - }), + [...Array(21).keys()].map(() => + Factory.build( + 'post', + { + image: faker.image.unsplash.buildings(), + }, + { + categoryIds: ['cat1'], + author: peterLustig, + }, + ), + ), ) await Promise.all( - [...Array(21).keys()].map(() => { - return factory.create('Post', { - author: peterLustig, - image: faker.image.unsplash.buildings(), - }) - }), + [...Array(3).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: peterLustig, + postId: 'p4', + }, + ), + ), ) await Promise.all( - [...Array(3).keys()].map(() => { - return factory.create('Comment', { - author: peterLustig, - postId: 'p4', - }) - }), + [...Array(3).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: peterLustig, + postId: 'p14', + }, + ), + ), ) await Promise.all( - [...Array(5).keys()].map(() => { - return factory.create('Comment', { - author: peterLustig, - postId: 'p14', - }) - }), + [...Array(6).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: peterLustig, + postId: 'p0', + }, + ), + ), ) await Promise.all( - [...Array(6).keys()].map(() => { - return factory.create('Comment', { - author: peterLustig, - postId: 'p0', - }) - }), + [...Array(11).keys()].map(() => + Factory.build( + 'post', + { + image: faker.image.unsplash.food(), + }, + { + categoryIds: ['cat1'], + author: dewey, + }, + ), + ), ) await Promise.all( - [...Array(11).keys()].map(() => { - return factory.create('Post', { - author: dewey, - image: faker.image.unsplash.food(), - }) - }), + [...Array(7).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: dewey, + postId: 'p2', + }, + ), + ), ) await Promise.all( - [...Array(7).keys()].map(() => { - return factory.create('Comment', { - author: dewey, - postId: 'p2', - }) - }), + [...Array(5).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: dewey, + postId: 'p6', + }, + ), + ), ) await Promise.all( - [...Array(5).keys()].map(() => { - return factory.create('Comment', { - author: dewey, - postId: 'p6', - }) - }), + [...Array(2).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: dewey, + postId: 'p9', + }, + ), + ), ) await Promise.all( - [...Array(2).keys()].map(() => { - return factory.create('Comment', { - author: dewey, - postId: 'p9', - }) - }), + [...Array(16).keys()].map(() => + Factory.build( + 'post', + { + image: faker.image.unsplash.technology(), + }, + { + categoryIds: ['cat1'], + author: louie, + }, + ), + ), ) await Promise.all( - [...Array(16).keys()].map(() => { - return factory.create('Post', { - author: louie, - image: faker.image.unsplash.technology(), - }) - }), + [...Array(4).keys()].map(() => + Factory.build( + 'comment', + {}, + { + postId: 'p1', + author: louie, + }, + ), + ), ) await Promise.all( - [...Array(4).keys()].map(() => { - return factory.create('Comment', { - author: louie, - postId: 'p1', - }) - }), + [...Array(8).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: louie, + postId: 'p10', + }, + ), + ), ) await Promise.all( - [...Array(8).keys()].map(() => { - return factory.create('Comment', { - author: louie, - postId: 'p10', - }) - }), + [...Array(5).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: louie, + postId: 'p13', + }, + ), + ), ) await Promise.all( - [...Array(5).keys()].map(() => { - return factory.create('Comment', { - author: louie, - postId: 'p13', - }) - }), + [...Array(45).keys()].map(() => + Factory.build( + 'post', + { + image: faker.image.unsplash.people(), + }, + { + categoryIds: ['cat1'], + author: bobDerBaumeister, + }, + ), + ), ) await Promise.all( - [...Array(45).keys()].map(() => { - return factory.create('Post', { - author: bobDerBaumeister, - image: faker.image.unsplash.people(), - }) - }), + [...Array(2).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: bobDerBaumeister, + postId: 'p2', + }, + ), + ), ) await Promise.all( - [...Array(2).keys()].map(() => { - return factory.create('Comment', { - author: bobDerBaumeister, - postId: 'p2', - }) - }), + [...Array(3).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: bobDerBaumeister, + postId: 'p12', + }, + ), + ), ) await Promise.all( - [...Array(3).keys()].map(() => { - return factory.create('Comment', { - author: bobDerBaumeister, - postId: 'p12', - }) - }), + [...Array(7).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: bobDerBaumeister, + postId: 'p13', + }, + ), + ), ) await Promise.all( - [...Array(7).keys()].map(() => { - return factory.create('Comment', { - author: bobDerBaumeister, - postId: 'p13', - }) - }), + [...Array(8).keys()].map(() => + Factory.build( + 'post', + { + image: faker.image.unsplash.nature(), + }, + { + categoryIds: ['cat1'], + author: huey, + }, + ), + ), ) await Promise.all( - [...Array(8).keys()].map(() => { - return factory.create('Post', { - author: huey, - image: faker.image.unsplash.nature(), - }) - }), + [...Array(6).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: huey, + postId: 'p0', + }, + ), + ), ) await Promise.all( - [...Array(6).keys()].map(() => { - return factory.create('Comment', { - author: huey, - postId: 'p0', - }) - }), + [...Array(8).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: huey, + postId: 'p13', + }, + ), + ), ) await Promise.all( - [...Array(8).keys()].map(() => { - return factory.create('Comment', { - author: huey, - postId: 'p13', - }) - }), + [...Array(8).keys()].map(() => + Factory.build( + 'comment', + {}, + { + author: huey, + postId: 'p15', + }, + ), + ), ) - await Promise.all( - [...Array(9).keys()].map(() => { - return factory.create('Comment', { - author: huey, - postId: 'p15', - }) - }), - ) - - await factory.create('Donations') + await Factory.build('donations') /* eslint-disable-next-line no-console */ console.log('Seeded Data...') + await driver.close() + await neode.close() process.exit(0) } catch (err) { /* eslint-disable-next-line no-console */ diff --git a/backend/src/factories/badges.js b/backend/src/factories/badges.js deleted file mode 100644 index 5f0482460..000000000 --- a/backend/src/factories/badges.js +++ /dev/null @@ -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) - }, - } -} diff --git a/backend/src/factories/categories.js b/backend/src/factories/categories.js deleted file mode 100644 index d3f5fed21..000000000 --- a/backend/src/factories/categories.js +++ /dev/null @@ -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) - }, - } -} diff --git a/backend/src/factories/comments.js b/backend/src/factories/comments.js deleted file mode 100644 index de3390e1a..000000000 --- a/backend/src/factories/comments.js +++ /dev/null @@ -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 - }, - } -} diff --git a/backend/src/factories/donations.js b/backend/src/factories/donations.js deleted file mode 100644 index e22cdb6d7..000000000 --- a/backend/src/factories/donations.js +++ /dev/null @@ -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) - }, - } -} diff --git a/backend/src/factories/emailAddresses.js b/backend/src/factories/emailAddresses.js deleted file mode 100644 index 41b1fe96c..000000000 --- a/backend/src/factories/emailAddresses.js +++ /dev/null @@ -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) - }, - } -} diff --git a/backend/src/factories/index.js b/backend/src/factories/index.js deleted file mode 100644 index c3ab14f64..000000000 --- a/backend/src/factories/index.js +++ /dev/null @@ -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 -} diff --git a/backend/src/factories/locations.js b/backend/src/factories/locations.js deleted file mode 100644 index 99b666de8..000000000 --- a/backend/src/factories/locations.js +++ /dev/null @@ -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) - }, - } -} diff --git a/backend/src/factories/posts.js b/backend/src/factories/posts.js deleted file mode 100644 index d997b738f..000000000 --- a/backend/src/factories/posts.js +++ /dev/null @@ -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 - }, - } -} diff --git a/backend/src/factories/reports.js b/backend/src/factories/reports.js deleted file mode 100644 index e2d5ec4dc..000000000 --- a/backend/src/factories/reports.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function create() { - return { - factory: async ({ args, neodeInstance }) => { - return neodeInstance.create('Report', args) - }, - } -} diff --git a/backend/src/factories/socialMedia.js b/backend/src/factories/socialMedia.js deleted file mode 100644 index 49a237cef..000000000 --- a/backend/src/factories/socialMedia.js +++ /dev/null @@ -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) - }, - } -} diff --git a/backend/src/factories/tags.js b/backend/src/factories/tags.js deleted file mode 100644 index 9005d1406..000000000 --- a/backend/src/factories/tags.js +++ /dev/null @@ -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) - }, - } -} diff --git a/backend/src/factories/unverifiedEmailAddresses.js b/backend/src/factories/unverifiedEmailAddresses.js deleted file mode 100644 index 94e32af6e..000000000 --- a/backend/src/factories/unverifiedEmailAddresses.js +++ /dev/null @@ -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) - }, - } -} diff --git a/backend/src/factories/users.js b/backend/src/factories/users.js deleted file mode 100644 index 57f69b76b..000000000 --- a/backend/src/factories/users.js +++ /dev/null @@ -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 - }, - } -} diff --git a/backend/src/index.js b/backend/src/index.js index 98354dc1f..59718dad1 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -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}`) }) diff --git a/backend/src/jwt/decode.spec.js b/backend/src/jwt/decode.spec.js index 71444a3e5..aa8ff0674 100644 --- a/backend/src/jwt/decode.spec.js +++ b/backend/src/jwt/decode.spec.js @@ -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 () => { diff --git a/backend/src/jwt/encode.js b/backend/src/jwt/encode.js index f2547e30e..9126f2577 100644 --- a/backend/src/jwt/encode.js +++ b/backend/src/jwt/encode.js @@ -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 } diff --git a/backend/src/jwt/encode.spec.js b/backend/src/jwt/encode.spec.js new file mode 100644 index 000000000..21ebdffec --- /dev/null +++ b/backend/src/jwt/encode.spec.js @@ -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), + }) + }) + }) +}) diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js index 2247e692d..be9039a6f 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js @@ -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', () => { diff --git a/backend/src/middleware/notifications/notificationsMiddleware.js b/backend/src/middleware/notifications/notificationsMiddleware.js index e0b831b59..4636b8e9f 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.js @@ -1,5 +1,6 @@ import extractMentionedUsers from './mentions/extractMentionedUsers' import { validateNotifyUsers } from '../validation/validationMiddleware' +import { pubsub, NOTIFICATION_ADDED } from '../../server' const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => { const idsOfUsers = extractMentionedUsers(args.content) @@ -52,34 +53,48 @@ 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 } case 'mentioned_in_comment': { mentionedCypher = ` - MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User) + MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(commenter: User) MATCH (user: User) WHERE user.id in $idsOfUsers - AND NOT (user)-[:BLOCKED]-(author) + AND NOT (user)-[:BLOCKED]-(commenter) AND NOT (user)-[:BLOCKED]-(postAuthor) MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user) + WITH comment AS resource, notification, user ` break } } 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 - 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 [notification] = await writeTxResultPromise + return pubsub.publish(NOTIFICATION_ADDED, { notificationAdded: notification }) + } catch (error) { + throw new Error(error) } finally { session.close() } @@ -88,24 +103,26 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, context) => { 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 ( - CASE - WHEN notification.createdAt IS NULL - THEN notification END ).createdAt = toString(datetime()) - SET notification.updatedAt = toString(datetime()) - `, - { commentId, postAuthorId, reason }, - ) - }) + const [notification] = await writeTxResultPromise + return pubsub.publish(NOTIFICATION_ADDED, { notificationAdded: notification }) } finally { session.close() } diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.js b/backend/src/middleware/notifications/notificationsMiddleware.spec.js index 136388b88..95c0037b8 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.js @@ -1,11 +1,10 @@ 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' let server, query, mutate, notifiedUser, authenticatedUser -const factory = Factory() const driver = getDriver() const neode = getNeode() const categoryIds = ['cat9'] @@ -36,7 +35,7 @@ const createCommentMutation = gql` ` beforeAll(async () => { - await factory.cleanDatabase() + await cleanDatabase() const createServerResult = createServer({ context: () => { return { @@ -53,13 +52,18 @@ beforeAll(async () => { }) beforeEach(async () => { - notifiedUser = await neode.create('User', { - id: 'you', - name: 'Al Capone', - slug: 'al-capone', - email: 'test@example.org', - password: '1234', - }) + notifiedUser = await neode.create( + 'User', + { + id: 'you', + name: 'Al Capone', + slug: 'al-capone', + }, + { + email: 'test@example.org', + password: '1234', + }, + ) await neode.create('Category', { id: 'cat9', name: 'Democracy & Politics', @@ -68,7 +72,7 @@ beforeEach(async () => { }) afterEach(async () => { - await factory.cleanDatabase() + await cleanDatabase() }) describe('notifications', () => { @@ -143,13 +147,18 @@ describe('notifications', () => { describe('commenter is not me', () => { beforeEach(async () => { commentContent = 'Commenters comment.' - commentAuthor = await neode.create('User', { - id: 'commentAuthor', - name: 'Mrs Comment', - slug: 'mrs-comment', - email: 'commentauthor@example.org', - password: '1234', - }) + commentAuthor = await neode.create( + 'User', + { + id: 'commentAuthor', + name: 'Mrs Comment', + slug: 'mrs-comment', + }, + { + email: 'commentauthor@example.org', + password: '1234', + }, + ) }) it('sends me a notification', async () => { @@ -224,13 +233,18 @@ describe('notifications', () => { }) beforeEach(async () => { - postAuthor = await neode.create('User', { - id: 'postAuthor', - name: 'Mrs Post', - slug: 'mrs-post', - email: 'post-author@example.org', - password: '1234', - }) + postAuthor = await neode.create( + 'User', + { + id: 'postAuthor', + name: 'Mrs Post', + slug: 'mrs-post', + }, + { + email: 'post-author@example.org', + password: '1234', + }, + ) }) describe('mentions me in a post', () => { @@ -428,23 +442,33 @@ describe('notifications', () => { beforeEach(async () => { commentContent = 'One mention about me with @al-capone.' - commentAuthor = await neode.create('User', { - id: 'commentAuthor', - name: 'Mrs Comment', - slug: 'mrs-comment', - email: 'comment-author@example.org', - password: '1234', - }) + commentAuthor = await neode.create( + '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 neode.create('User', { - id: 'MrPostAuthor', - name: 'Mr Author', - slug: 'mr-author', - email: 'post-author@example.org', - password: '1234', - }) + postAuthor = await neode.create( + 'User', + { + id: 'MrPostAuthor', + name: 'Mr Author', + slug: 'mr-author', + }, + { + email: 'post-author@example.org', + password: '1234', + }, + ) await createCommentOnPostAction() const expected = expect.objectContaining({ @@ -514,13 +538,18 @@ describe('notifications', () => { await postAuthor.relateTo(notifiedUser, 'blocked') commentContent = 'One mention about me with @al-capone.' - commentAuthor = await neode.create('User', { - id: 'commentAuthor', - name: 'Mrs Comment', - slug: 'mrs-comment', - email: 'comment-author@example.org', - password: '1234', - }) + commentAuthor = await neode.create( + 'User', + { + id: 'commentAuthor', + name: 'Mrs Comment', + slug: 'mrs-comment', + }, + { + email: 'comment-author@example.org', + password: '1234', + }, + ) }) it('sends no notification', async () => { diff --git a/backend/src/middleware/orderByMiddleware.spec.js b/backend/src/middleware/orderByMiddleware.spec.js index 8d92a5b5d..91caa1f54 100644 --- a/backend/src/middleware/orderByMiddleware.spec.js +++ b/backend/src/middleware/orderByMiddleware.spec.js @@ -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', () => { diff --git a/backend/src/middleware/permissionsMiddleware.spec.js b/backend/src/middleware/permissionsMiddleware.spec.js index a4f13ea0c..3c307348d 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.js +++ b/backend/src/middleware/permissionsMiddleware.spec.js @@ -1,10 +1,9 @@ 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() @@ -20,7 +19,7 @@ const userQuery = gql` describe('authorization', () => { beforeAll(async () => { - await factory.cleanDatabase() + await cleanDatabase() const { server } = createServer({ context: () => ({ driver, @@ -34,34 +33,54 @@ describe('authorization', () => { describe('given two existing users', () => { 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() + await cleanDatabase() }) describe('access email address', () => { diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index cf9f0941c..e522136d6 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -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 () => { @@ -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', }) diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js index 6e1735af2..de5626d14 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js @@ -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', () => { diff --git a/backend/src/middleware/validation/validationMiddleware.spec.js b/backend/src/middleware/validation/validationMiddleware.spec.js index 38cd010b4..b2c669369 100644 --- a/backend/src/middleware/validation/validationMiddleware.spec.js +++ b/backend/src/middleware/validation/validationMiddleware.spec.js @@ -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, @@ -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() }) diff --git a/backend/src/models/Post.js b/backend/src/models/Post.js index 154456cf1..63a36d3a2 100644 --- a/backend/src/models/Post.js +++ b/backend/src/models/Post.js @@ -52,5 +52,4 @@ export default { }, }, pinned: { type: 'boolean', default: null, valid: [null, true] }, - pinnedAt: { type: 'string', isoDate: true }, } diff --git a/backend/src/models/User.spec.js b/backend/src/models/User.spec.js index 7bdde7014..f448cbf08 100644 --- a/backend/src/models/User.spec.js +++ b/backend/src/models/User.spec.js @@ -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', () => { diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js index 9877161db..9f633c8b0 100644 --- a/backend/src/schema/resolvers/comments.spec.js +++ b/backend/src/schema/resolvers/comments.spec.js @@ -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() }) diff --git a/backend/src/schema/resolvers/donations.spec.js b/backend/src/schema/resolvers/donations.spec.js index c382eb475..ea5ee4e09 100644 --- a/backend/src/schema/resolvers/donations.spec.js +++ b/backend/src/schema/resolvers/donations.spec.js @@ -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', }) diff --git a/backend/src/schema/resolvers/emails.spec.js b/backend/src/schema/resolvers/emails.spec.js index 97a1f0c29..94e7ede31 100644 --- a/backend/src/schema/resolvers/emails.spec.js +++ b/backend/src/schema/resolvers/emails.spec.js @@ -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 () => { diff --git a/backend/src/schema/resolvers/fileUpload/index.js b/backend/src/schema/resolvers/fileUpload/index.js index 960dde7f9..df0145057 100644 --- a/backend/src/schema/resolvers/fileUpload/index.js +++ b/backend/src/schema/resolvers/fileUpload/index.js @@ -1,24 +1,27 @@ import { createWriteStream } from 'fs' import path from 'path' import slug from 'slug' +import uuid from 'uuid/v4' -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 diff --git a/backend/src/schema/resolvers/fileUpload/spec.js b/backend/src/schema/resolvers/fileUpload/spec.js index 5767d6457..fee0bf81b 100644 --- a/backend/src/schema/resolvers/fileUpload/spec.js +++ b/backend/src/schema/resolvers/fileUpload/spec.js @@ -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, diff --git a/backend/src/schema/resolvers/follow.spec.js b/backend/src/schema/resolvers/follow.spec.js index ad836a461..953a26d65 100644 --- a/backend/src/schema/resolvers/follow.spec.js +++ b/backend/src/schema/resolvers/follow.spec.js @@ -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', () => { diff --git a/backend/src/schema/resolvers/locations.spec.js b/backend/src/schema/resolvers/locations.spec.js index aba11f9bc..34d0f2e9d 100644 --- a/backend/src/schema/resolvers/locations.spec.js +++ b/backend/src/schema/resolvers/locations.spec.js @@ -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', }) diff --git a/backend/src/schema/resolvers/moderation.spec.js b/backend/src/schema/resolvers/moderation.spec.js index cd502be75..b62d35ee8 100644 --- a/backend/src/schema/resolvers/moderation.spec.js +++ b/backend/src/schema/resolvers/moderation.spec.js @@ -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', diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index 31369a8c7..cf35fa8a1 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -1,21 +1,18 @@ import log from './helpers/databaseLogger' - -const resourceTypes = ['Post', 'Comment'] - -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 @@ -51,10 +48,10 @@ export default { MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id}) ${whereClause} WITH user, notification, resource, - [(resource)<-[:WROTE]-(author:User) | author {.*}] as authors, - [(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] as posts + [(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 + resource {.*, __typename: labels(resource)[0], author: authors[0], post: posts[0]} AS finalResource RETURN notification {.*, from: finalResource, to: properties(user)} ${orderByClause} ${offset} ${limit} @@ -81,12 +78,19 @@ 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, id: currentUser.id }, ) log(markNotificationAsReadTransactionResponse) - return markNotificationAsReadTransactionResponse.records.map(transformReturnType) + return markNotificationAsReadTransactionResponse.records.map(record => + record.get('notification'), + ) }) try { const [notifications] = await writeTxResultPromise diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index a5c46e930..9d7795dd4 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -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,52 +31,77 @@ 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.build('user', { id: 'author' }) + user = await Factory.build('user', { id: 'you' }) const [neighbor] = await Promise.all([ - factory.create('User', { id: 'neighbor' }), - factory.create('Category', { id: 'cat1' }), + Factory.build('user', { id: 'neighbor' }), + Factory.build('category', { id: 'cat1' }), ]) const [post1, post2, post3] = 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.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, + }, + ), ]) const [comment1, comment2, comment3] = 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.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', + }, + ), ]) await Promise.all([ post1.relateTo(neighbor, 'notified', { diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index d7b3a0157..b48498ee7 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -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', () => { diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 56a47afa7..88a09843d 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -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, diff --git a/backend/src/schema/resolvers/registration.spec.js b/backend/src/schema/resolvers/registration.spec.js index 23b1f9d2a..63dc35519 100644 --- a/backend/src/schema/resolvers/registration.spec.js +++ b/backend/src/schema/resolvers/registration.spec.js @@ -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 () => { diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js index 7f827b111..0e690c19e 100644 --- a/backend/src/schema/resolvers/reports.spec.js +++ b/backend/src/schema/resolvers/reports.spec.js @@ -1,10 +1,9 @@ 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() @@ -53,7 +52,7 @@ describe('file a report on a resource', () => { } beforeAll(async () => { - await factory.cleanDatabase() + await cleanDatabase() const { server } = createServer({ context: () => { return { @@ -68,7 +67,7 @@ describe('file a report on a resource', () => { }) afterEach(async () => { - await factory.cleanDatabase() + await cleanDatabase() }) describe('report a resource', () => { @@ -84,24 +83,39 @@ 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', + }, + ) + 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', @@ -341,12 +355,17 @@ 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 () => { @@ -394,21 +413,30 @@ 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 () => { @@ -457,7 +485,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', }) }) @@ -515,24 +543,39 @@ describe('file a report on a resource', () => { 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,31 +583,51 @@ 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([ diff --git a/backend/src/schema/resolvers/rewards.spec.js b/backend/src/schema/resolvers/rewards.spec.js index fe2807f25..a20472243 100644 --- a/backend/src/schema/resolvers/rewards.spec.js +++ b/backend/src/schema/resolvers/rewards.spec.js @@ -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, diff --git a/backend/src/schema/resolvers/shout.spec.js b/backend/src/schema/resolvers/shout.spec.js index 104a28399..f5ec8f5fd 100644 --- a/backend/src/schema/resolvers/shout.spec.js +++ b/backend/src/schema/resolvers/shout.spec.js @@ -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' }, diff --git a/backend/src/schema/resolvers/socialMedia.spec.js b/backend/src/schema/resolvers/socialMedia.spec.js index f292b58a0..898174199 100644 --- a/backend/src/schema/resolvers/socialMedia.spec.js +++ b/backend/src/schema/resolvers/socialMedia.spec.js @@ -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', () => { diff --git a/backend/src/schema/resolvers/statistics.spec.js b/backend/src/schema/resolvers/statistics.spec.js index e2b9dafe4..c5bb5f88b 100644 --- a/backend/src/schema/resolvers/statistics.spec.js +++ b/backend/src/schema/resolvers/statistics.spec.js @@ -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([ diff --git a/backend/src/schema/resolvers/user_management.spec.js b/backend/src/schema/resolvers/user_management.spec.js index 5e7043e74..1e295638d 100644 --- a/backend/src/schema/resolvers/user_management.spec.js +++ b/backend/src/schema/resolvers/user_management.spec.js @@ -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}` } } }) diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index d1d9111b6..cbdc683e8 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -252,9 +252,11 @@ export default { followedByCurrentUser: 'MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1', blocked: - 'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1', + 'MATCH (this)-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1', isMuted: 'MATCH (this)<-[:MUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1', + isBlocked: + 'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1', }, count: { contributionsCount: diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index cfd84fcf7..9e24b8082 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -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') }) diff --git a/backend/src/schema/resolvers/users/location.js b/backend/src/schema/resolvers/users/location.js index 3f3638bf5..cc00d9e0a 100644 --- a/backend/src/schema/resolvers/users/location.js +++ b/backend/src/schema/resolvers/users/location.js @@ -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 = diff --git a/backend/src/schema/resolvers/users/location.spec.js b/backend/src/schema/resolvers/users/location.spec.js index f7315174c..04216dcb5 100644 --- a/backend/src/schema/resolvers/users/location.spec.js +++ b/backend/src/schema/resolvers/users/location.spec.js @@ -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() }) diff --git a/backend/src/schema/resolvers/users/mutedUsers.spec.js b/backend/src/schema/resolvers/users/mutedUsers.spec.js index 130df08ce..cdc7c81b3 100644 --- a/backend/src/schema/resolvers/users/mutedUsers.spec.js +++ b/backend/src/schema/resolvers/users/mutedUsers.spec.js @@ -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', () => { diff --git a/backend/src/schema/types/type/NOTIFIED.gql b/backend/src/schema/types/type/NOTIFIED.gql index af91460f7..88ecd3882 100644 --- a/backend/src/schema/types/type/NOTIFIED.gql +++ b/backend/src/schema/types/type/NOTIFIED.gql @@ -30,3 +30,7 @@ type Query { type Mutation { markAsRead(id: ID!): NOTIFIED } + +type Subscription { + notificationAdded(userId: ID!): NOTIFIED +} diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 71cc1edb0..baefc9d29 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -68,7 +68,12 @@ type User { 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}) diff --git a/backend/src/server.js b/backend/src/server.js index 02e166b71..4df73559d 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -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 diff --git a/backend/test/features/support/steps.js b/backend/test/features/support/steps.js index 70802f4e2..e15801f83 100644 --- a/backend/test/features/support/steps.js +++ b/backend/test/features/support/steps.js @@ -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' }) } diff --git a/backend/yarn.lock b/backend/yarn.lock index 7fa0acb92..8bc2e7c00 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1175,13 +1175,13 @@ url-regex "~4.1.1" video-extensions "~1.1.0" -"@metascraper/helpers@^5.10.6": - version "5.10.6" - resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.10.6.tgz#0b786607212925a577926fd0cd0313a49de3499c" - integrity sha512-/jvhlM3RKGYMoUK8D8S1r3tN03/EYizCqWF7zDx0aBMC8Ihp33DRGs9oNdsgkgwzVF7O/YpDm55l9K+qVJlsyQ== +"@metascraper/helpers@^5.10.7", "@metascraper/helpers@^5.11.1": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.11.1.tgz#227fdd0caf1d33f4b24a85298a355ce7ebb451df" + integrity sha512-oES/e6bwKBlT7WGa2ni3xbJMDx2rbFxSzbUhRX8D+Kylb8H2ThP07c7f+VXMPXWx5CPrNMai/Oyp5IczCf3v8g== dependencies: audio-extensions "0.0.0" - chrono-node "~1.4.2" + chrono-node "~1.4.3" condense-whitespace "~2.0.0" entities "~2.0.0" file-extension "~4.0.5" @@ -1194,7 +1194,7 @@ lodash "~4.17.15" memoize-one "~5.1.1" mime-types "~2.1.26" - normalize-url "~4.5.0" + normalize-url "~5.0.0" smartquotes "~2.3.1" title "~3.4.1" truncate "~2.1.0" @@ -1275,83 +1275,83 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@sentry/apm@5.11.2": - version "5.11.2" - resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.11.2.tgz#35961b9d2319ad21ae91f1b697998a8c523f1919" - integrity sha512-qn4HiSZ+6b1Gg+DlXdHVpiPPEbRu4IicGSbI8HTJLzrlsjoaBQPPkDwtuQUBVq21tU3RYXnTwrl9m45KuX6alA== +"@sentry/apm@5.12.3": + version "5.12.3" + resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.12.3.tgz#23a5e9c771a8748f59426a1d0f8b1fbb9b72a717" + integrity sha512-OSGEeo4b1Gsu/TUcWMx9vmgSnQvR+zM+1Iwq5xFQAK2ET3Y4gBFqZ1iRt2hxlzr8KCQmQTQc1mao1X0tmidFQg== dependencies: - "@sentry/browser" "5.11.2" - "@sentry/hub" "5.11.2" - "@sentry/minimal" "5.11.2" - "@sentry/types" "5.11.0" - "@sentry/utils" "5.11.1" + "@sentry/browser" "5.12.1" + "@sentry/hub" "5.12.0" + "@sentry/minimal" "5.12.0" + "@sentry/types" "5.12.0" + "@sentry/utils" "5.12.0" tslib "^1.9.3" -"@sentry/browser@5.11.2": - version "5.11.2" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.11.2.tgz#f0b19bd97e9f09a20e9f93a9835339ed9ab1f5a4" - integrity sha512-ls6ARX5m+23ld8OsuoPnR+kehjR5ketYWRcDYlmJDX2VOq5K4EzprujAo8waDB0o5a92yLXQ0ZSoK/zzAV2VoA== +"@sentry/browser@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.12.1.tgz#dc1f268595269fb7277f55eb625c7e92d76dc01b" + integrity sha512-Zl7VdppUxctyaoqMSEhnDJp2rrupx8n8N2n3PSooH74yhB2Z91nt84mouczprBsw3JU1iggGyUw9seRFzDI1hw== dependencies: - "@sentry/core" "5.11.2" - "@sentry/types" "5.11.0" - "@sentry/utils" "5.11.1" + "@sentry/core" "5.12.0" + "@sentry/types" "5.12.0" + "@sentry/utils" "5.12.0" tslib "^1.9.3" -"@sentry/core@5.11.2": - version "5.11.2" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.11.2.tgz#f2d9d37940d291dbcb9a9e4a012f76919474bdf6" - integrity sha512-IFCXGy7ebqIq/Kb8RVryCo/SjwhPcrfBmOjkicr4+DxN1UybLre2N3p9bejQMPIteOfDVHlySLYeipjTf+mxZw== +"@sentry/core@5.12.0": + version "5.12.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.12.0.tgz#d6380c4ef7beee5f418ac1d0e5be86a2de2af449" + integrity sha512-wY4rsoX71QsGpcs9tF+OxKgDPKzIFMRvFiSRcJoPMfhFsTilQ/CBMn/c3bDtWQd9Bnr/ReQIL6NbnIjUsPHA4Q== dependencies: - "@sentry/hub" "5.11.2" - "@sentry/minimal" "5.11.2" - "@sentry/types" "5.11.0" - "@sentry/utils" "5.11.1" + "@sentry/hub" "5.12.0" + "@sentry/minimal" "5.12.0" + "@sentry/types" "5.12.0" + "@sentry/utils" "5.12.0" tslib "^1.9.3" -"@sentry/hub@5.11.2": - version "5.11.2" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.11.2.tgz#a3b7ec27cd4cea2cddd75c372fbf1b4bc04c6aae" - integrity sha512-5BiDin6ZPsaiTm29rCC41MAjP1vOaKniqfjtXHVPm7FeOBA2bpHm95ncjLkshKGJTPfPZHXTpX/1IZsHrfGVEA== +"@sentry/hub@5.12.0": + version "5.12.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.12.0.tgz#5e8c8f249f5bdbeb8cc4ec02c2ccc53a67f2cc02" + integrity sha512-3k7yE8BEVJsKx8mR4LcI4IN0O8pngmq44OcJ/fRUUBAPqsT38jsJdP2CaWhdlM1jiNUzUDB1ktBv6/lY+VgcoQ== dependencies: - "@sentry/types" "5.11.0" - "@sentry/utils" "5.11.1" + "@sentry/types" "5.12.0" + "@sentry/utils" "5.12.0" tslib "^1.9.3" -"@sentry/minimal@5.11.2": - version "5.11.2" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.11.2.tgz#ae417699342266ecd109a97e53cd9519c0893b21" - integrity sha512-oNuJuz3EZhVtamzABmPdr6lcYo06XHLWb2LvgnoNaYcMD1ExUSvhepOSyZ2h5STCMbmVgGVfXBNPV9RUTp8GZg== +"@sentry/minimal@5.12.0": + version "5.12.0" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.12.0.tgz#2611e2aa520c1edb7999e6de51bd65ec66341757" + integrity sha512-fk73meyz4k4jCg9yzbma+WkggsfEIQWI2e2TWfYsRGcrV3RnlSrXyM4D91/A8Bjx10SNezHPUFHjasjlHXOkyA== dependencies: - "@sentry/hub" "5.11.2" - "@sentry/types" "5.11.0" + "@sentry/hub" "5.12.0" + "@sentry/types" "5.12.0" tslib "^1.9.3" -"@sentry/node@^5.11.2": - version "5.11.2" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.11.2.tgz#575c320b624c218d2155183f6bbe82b732bfb1f2" - integrity sha512-jYq9u76BdAbOKPuYg39Xh/+797MevzjMkCIC9cw/bQxAm6nHc3FXeKqd79O33jO4Jag0JL+Bz/0JidgrKgKgXg== +"@sentry/node@^5.12.3": + version "5.12.3" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.12.3.tgz#4a4934b04c5163fc340312eaf0d152990aa7140f" + integrity sha512-QwqN+i6IC3/YrNq7kqxH7YiXtZYY8tBuJqFi84WbiMHF7MAqxMSPNQJGfX2GJuMHKHwn6IZdgSE8+FkfN9zGLQ== dependencies: - "@sentry/apm" "5.11.2" - "@sentry/core" "5.11.2" - "@sentry/hub" "5.11.2" - "@sentry/types" "5.11.0" - "@sentry/utils" "5.11.1" + "@sentry/apm" "5.12.3" + "@sentry/core" "5.12.0" + "@sentry/hub" "5.12.0" + "@sentry/types" "5.12.0" + "@sentry/utils" "5.12.0" cookie "^0.3.1" https-proxy-agent "^4.0.0" lru_map "^0.3.3" tslib "^1.9.3" -"@sentry/types@5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.11.0.tgz#40f0f3174362928e033ddd9725d55e7c5cb7c5b6" - integrity sha512-1Uhycpmeo1ZK2GLvrtwZhTwIodJHcyIS6bn+t4IMkN9MFoo6ktbAfhvexBDW/IDtdLlCGJbfm8nIZerxy0QUpg== +"@sentry/types@5.12.0": + version "5.12.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.12.0.tgz#5367e53c74261beea01502e3f7b6f3d822682a31" + integrity sha512-aZbBouBLrKB8wXlztriIagZNmsB+wegk1Jkl6eprqRW/w24Sl/47tiwH8c5S4jYTxdAiJk+SAR10AAuYmIN3zg== -"@sentry/utils@5.11.1": - version "5.11.1" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.11.1.tgz#aa19fcc234cf632257b2281261651d2fac967607" - integrity sha512-O0Zl4R2JJh8cTkQ8ZL2cDqGCmQdpA5VeXpuBbEl1v78LQPkBDISi35wH4mKmLwMsLBtTVpx2UeUHBj0KO5aLlA== +"@sentry/utils@5.12.0": + version "5.12.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.12.0.tgz#62967f934a3ee6d21472eac0219084e37225933e" + integrity sha512-fYUadGLbfTCbs4OG5hKCOtv2jrNE4/8LHNABy9DwNJ/t5DVtGqWAZBnxsC+FG6a3nVqCpxjFI9AHlYsJ2wsf7Q== dependencies: - "@sentry/types" "5.11.0" + "@sentry/types" "5.12.0" tslib "^1.9.3" "@sindresorhus/is@^0.14.0": @@ -1607,10 +1607,10 @@ dependencies: "@types/yargs-parser" "*" -"@types/yup@0.26.29": - version "0.26.29" - resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.29.tgz#5a533ad6f74e442436698e20b1441c68a7a1c931" - integrity sha512-M81oZOgLap0b0I/BySnpLwHjOj1BFxUKV1ytG2Kqj3jmkh8F3H11PEnk658UniftpjTXdueloOL+KZYn+SMQ9w== +"@types/yup@0.26.30": + version "0.26.30" + resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.30.tgz#0d6066505bb67e7b9b161b2082c4cdfcdafd6a6b" + integrity sha512-b/uklO68T/eShWnxjlgwOJlEFOQ11ib3i1wQQiLmHqFJTxDMvz+tb4XzThGQISzOcelMnoSdb1Po4s69YkEQeg== "@types/zen-observable@^0.8.0": version "0.8.0" @@ -1845,10 +1845,10 @@ apollo-client@~2.6.8: tslib "^1.10.0" zen-observable "^0.8.0" -apollo-datasource@^0.6.4: - version "0.6.4" - resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.4.tgz#c0d1604b1a97e004844d4b61bd819a9a6b0a409f" - integrity sha512-u4eu6Q94q6KuZacZfdo4vCevA81F4QWeTYEXUvoksQMJpiacPHHe0DJrofKVKvxngUp5kCi1RnPXSc6kBY+/oA== +apollo-datasource@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.7.0.tgz#2a6d82edb2eba21b4ddf21877009ba39ff821945" + integrity sha512-Yja12BgNQhzuFGG/5Nw2MQe0hkuQy2+9er09HxeEyAf2rUDIPnhPrn1MDoZTB8MU7UGfjwITC+1ofzKkkrZobA== dependencies: apollo-server-caching "^0.5.1" apollo-server-env "^2.4.3" @@ -1860,13 +1860,13 @@ apollo-engine-reporting-protobuf@^0.4.4: dependencies: "@apollo/protobufjs" "^1.0.3" -apollo-engine-reporting@^1.4.14: - version "1.4.14" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.14.tgz#71a6509ebe86385da43df500cd0940525a3e8674" - integrity sha512-cCG9qDOPwbh87ZjQGHgmnP3oPqhqjIZcNmm/lNtWkWXGTlxV/jmUEqpVi+wsDbE5gR7d1OFk6GqSy2ZQh+S+Bw== +apollo-engine-reporting@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.5.0.tgz#6e3746de14fc87e14c289c0776a2d350e6f50918" + integrity sha512-Pe2DelijZ2QHqkqv8E97iOb32l+FIMT2nxpQsuH+nWi+96cCFJJJHjm3RLAPEUuvGOgW9dFYQP3J91EyC5O0tQ== dependencies: apollo-engine-reporting-protobuf "^0.4.4" - apollo-graphql "^0.3.7" + apollo-graphql "^0.4.0" apollo-server-caching "^0.5.1" apollo-server-env "^2.4.3" apollo-server-errors "^2.3.4" @@ -1892,10 +1892,10 @@ apollo-errors@^1.9.0: assert "^1.4.1" extendable-error "^0.1.5" -apollo-graphql@^0.3.7: - version "0.3.7" - resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.7.tgz#533232ed48b0b6dbcf5635f65e66cf8677a5b768" - integrity sha512-ghW16xx9tRcyL38Pw6G5OidMnYn+CNUGZWmvqQgEO2nRy4T0ONPZZBOvGrIMtJQ70oEykNMKGm0zm6PdHdxd8Q== +apollo-graphql@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.4.0.tgz#dd0afe31a6241b8e2ded20b906c9ee8dfbe03497" + integrity sha512-abCHcKln1EGbzSItW087EjBI5wnluikyUqEn4VsdeWHCtdENWpHCn/MnM0+jJa1prNasxN7tCukp4nMpJYYVqg== dependencies: apollo-env "^0.6.1" lodash.sortby "^4.7.0" @@ -1943,18 +1943,18 @@ apollo-server-caching@^0.5.1: dependencies: lru-cache "^5.0.0" -apollo-server-core@^2.9.16: - version "2.9.16" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.16.tgz#b4c869a6babfa6906fbbf1e6facf3b7231dbf777" - integrity sha512-4ftdjSfs/3aEare9QNTVSF0yUvXETxiohuDLZ7gmMIQxNnZhUjVXiZL1rYKuIZ12uH7xLvh/DwkXRt6nLG/lZA== +apollo-server-core@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.10.0.tgz#b8d51bdffe6529f0e3ca670ee8f1238765cfade4" + integrity sha512-x/UK6XvU307W8D/pzTclU04JIjRarcbg5mFPe0nVmO4OTc26uQgKi1WlZkcewXsAUnn+nDwKVn2c2G3dHEgXzQ== dependencies: "@apollographql/apollo-tools" "^0.4.3" "@apollographql/graphql-playground-html" "1.6.24" "@types/graphql-upload" "^8.0.0" "@types/ws" "^6.0.0" apollo-cache-control "^0.8.11" - apollo-datasource "^0.6.4" - apollo-engine-reporting "^1.4.14" + apollo-datasource "^0.7.0" + apollo-engine-reporting "^1.5.0" apollo-server-caching "^0.5.1" apollo-server-env "^2.4.3" apollo-server-errors "^2.3.4" @@ -1983,10 +1983,10 @@ apollo-server-errors@^2.3.4: resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.4.tgz#b70ef01322f616cbcd876f3e0168a1a86b82db34" integrity sha512-Y0PKQvkrb2Kd18d1NPlHdSqmlr8TgqJ7JQcNIfhNDgdb45CnqZlxL1abuIRhr8tiw8OhVOcFxz2KyglBi8TKdA== -apollo-server-express@^2.9.16: - version "2.9.16" - resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.16.tgz#4c30b1769426c010b37943c0fb7766e5825973a0" - integrity sha512-ZDc7GP+piUm67alJ0DIE9f36tHcCiNm3PHMLIVJlVE/rcGwzRjV5rardRqeslljQiO2J+1IwXKwJ0/kRrZ4JvQ== +apollo-server-express@^2.10.0, apollo-server-express@^2.9.16: + version "2.10.0" + resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.10.0.tgz#7d87ff54e378cdcb135eb3d093f20fca7fc0d1bc" + integrity sha512-adDQts4QmkX2ENU7JibV1EwRl3ESnpnpImXIMvg8Cm7kX2wSbzwm8LecQNujwWJtkAPTCEAcnPBDyKwWjTQ6/g== dependencies: "@apollographql/graphql-playground-html" "1.6.24" "@types/accepts" "^1.3.5" @@ -1994,7 +1994,7 @@ apollo-server-express@^2.9.16: "@types/cors" "^2.8.4" "@types/express" "4.17.2" accepts "^1.3.5" - apollo-server-core "^2.9.16" + apollo-server-core "^2.10.0" apollo-server-types "^0.2.10" body-parser "^1.18.3" cors "^2.8.4" @@ -2012,12 +2012,12 @@ apollo-server-plugin-base@^0.6.10: dependencies: apollo-server-types "^0.2.10" -apollo-server-testing@~2.9.16: - version "2.9.16" - resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.16.tgz#35e9b0b102a11bac8db2fce04281cb43e7993d45" - integrity sha512-CLfYZY2Htwzw6iPlFO32/SNXNstWQsvGd5/FQ8KEwRpNfYM4g0rAE98y/THEQTvTh0xPH+qWxA7CVQcc7/FMbQ== +apollo-server-testing@~2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.10.0.tgz#c8d7fc2d4e6eaf84232aaa7c125d9fae691fbcf4" + integrity sha512-wBJ/CT3ZN5nmSySMqgpAFwX/I3yzsQhRGR8MCK/16MjhEZH6svNaJWzoif6gaocj0NyVBJvOIijuMTecG9+6vg== dependencies: - apollo-server-core "^2.9.16" + apollo-server-core "^2.10.0" apollo-server-types@^0.2.10: version "0.2.10" @@ -2028,13 +2028,13 @@ apollo-server-types@^0.2.10: apollo-server-caching "^0.5.1" apollo-server-env "^2.4.3" -apollo-server@~2.9.16: - version "2.9.16" - resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.16.tgz#c0054ed70ecb637cb3f585ff46fb4a060730076f" - integrity sha512-dqB1shkjl9ne7DfSHXDH5sT70llr9zswLL+/g/4zt4/H+k+2pkD1BShQkNIK7PBYcVa8KvRAHXiHTXZ36GCspA== +apollo-server@~2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.10.0.tgz#93b924b089f7c4070e88aa29a8b9472c1d5d0f82" + integrity sha512-ITXkklSgrNfohFh4juvHWrtLz/82iw9CkBUW+G5T8NxHaqxm1Lpus1Ck2VsXmCgNplYi6mODRjUl087qdlU2Rw== dependencies: - apollo-server-core "^2.9.16" - apollo-server-express "^2.9.16" + apollo-server-core "^2.10.0" + apollo-server-express "^2.10.0" express "^4.0.0" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" @@ -2691,10 +2691,10 @@ chrono-node@~1.3.11: dependencies: moment "2.21.0" -chrono-node@~1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/chrono-node/-/chrono-node-1.4.2.tgz#0c7fc1f264e60a660c2b2dab753a3f285dbfd8c9" - integrity sha512-fsb82wPDHVZl3xtche8k4ZZtNwf81/ZMueil2ANpSfogUAEa3BuzZAar7ObLXi1ptMjBzdzA6ys/bFq1oBjO8w== +chrono-node@~1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/chrono-node/-/chrono-node-1.4.3.tgz#4c8e24643ec5e576f6f8fe0429370c3b554491b4" + integrity sha512-ZyKcnTcr8i7Mt9p4+ixMHEuR6+eMTrjYCL9Rm9TZHviLleCtcZoVzmr2uSc+Vg8MX1YbNCnPbEd4rfV8WvzLcw== dependencies: dayjs "^1.8.19" @@ -2774,6 +2774,11 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +cluster-key-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" + integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -3254,6 +3259,11 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= +denque@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" + integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -3625,10 +3635,10 @@ eslint-plugin-es@^3.0.0: eslint-utils "^2.0.0" regexpp "^3.0.0" -eslint-plugin-import@~2.20.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.0.tgz#d749a7263fb6c29980def8e960d380a6aa6aecaa" - integrity sha512-NK42oA0mUc8Ngn4kONOPsPB1XhbUvNHqF+g307dPV28aknPoiNnKLFd9em4nkswwepdF5ouieqv5Th/63U7YJQ== +eslint-plugin-import@~2.20.1: + version "2.20.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz#802423196dcb11d9ce8435a5fc02a6d3b46939b3" + integrity sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw== dependencies: array-includes "^3.0.3" array.prototype.flat "^1.2.1" @@ -3643,13 +3653,12 @@ eslint-plugin-import@~2.20.0: read-pkg-up "^2.0.0" resolve "^1.12.0" -eslint-plugin-jest@~23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.6.0.tgz#508b32f80d44058c8c01257c0ee718cfbd521e9d" - integrity sha512-GH8AhcFXspOLqak7fqnddLXEJsrFyvgO8Bm60SexvKSn1+3rWYESnCiWUOCUcBTprNSDSE4CtAZdM4EyV6gPPw== +eslint-plugin-jest@~23.7.0: + version "23.7.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.7.0.tgz#84d5603b6e745b59898cb6750df6a44782a39b04" + integrity sha512-zkiyGlvJeHNjAEz8FaIxTXNblJJ/zj3waNbYbgflK7K6uy0cpE5zJBt/JpJtOBGM/UGkC6BqsQ4n0y7kQ2HA8w== dependencies: "@typescript-eslint/experimental-utils" "^2.5.0" - micromatch "^4.0.2" eslint-plugin-node@~11.0.0: version "11.0.0" @@ -4202,10 +4211,10 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -fs-capacitor@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c" - integrity sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA== +fs-capacitor@6.0.0, fs-capacitor@^2.0.4: + version "6.0.0" + resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-6.0.0.tgz#b4b89e3281d61df1c573e788d9ee6ec4c7c94da4" + integrity sha512-I+jZLV2q+ivQK/+Mu5FIYAECHgjoo8GBYJsBBQbNeU0aW1m25LU4E+MkLNq0kcJBjrp8Z6fhxpSeS8SyJyGkrw== fs-minipass@^1.2.5: version "1.2.6" @@ -4476,12 +4485,21 @@ graphql-middleware@~4.0.2: dependencies: graphql-tools "^4.0.5" -graphql-shield@~7.0.9: - version "7.0.9" - resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-7.0.9.tgz#8248916e9636a7e3c05719a52fd13f2d37ccaeb2" - integrity sha512-2Dfddd2hcObCSqAj64c/Aaxvs7gaoD2QU14crj7H486QjS8jIAtEPUyLVyv8SmJ1ZD7jT6wqx6wrB15Npn5Sgw== +graphql-redis-subscriptions@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/graphql-redis-subscriptions/-/graphql-redis-subscriptions-2.1.2.tgz#9c1b744bace0c6ba99dd0ebafe0148cad1df3301" + integrity sha512-l69KbGxyYfVHxvE+Dzv9/hXg/q+Xnjfx1JsrJD6ikePuSsNaCSNxr+MubSTNF3Gt3C/+JZs4FaWImFeK/+X2og== dependencies: - "@types/yup" "0.26.29" + iterall "^1.2.2" + optionalDependencies: + ioredis "^4.6.3" + +graphql-shield@~7.0.11: + version "7.0.11" + resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-7.0.11.tgz#78d49f346326be71090d35d8f5843da9ee8136e2" + integrity sha512-iWn/aiom2c8NuOj60euWTmsKKUjX1DB4ynBcDitQOLXG3WrWgss2Iolzr553qooJvkR5czeAFgPlZgI+nUgwsQ== + dependencies: + "@types/yup" "0.26.30" object-hash "^2.0.0" yup "^0.28.0" @@ -4492,10 +4510,10 @@ graphql-subscriptions@^1.0.0: dependencies: iterall "^1.2.1" -graphql-tag@^2.9.2, graphql-tag@~2.10.1: - version "2.10.1" - resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02" - integrity sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg== +graphql-tag@^2.9.2, graphql-tag@~2.10.3: + version "2.10.3" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.3.tgz#ea1baba5eb8fc6339e4c4cf049dabe522b0edf03" + integrity sha512-4FOv3ZKfA4WdOKJeHdz6B3F/vxBLSgmBcGeAFPf4n1F64ltJUvOOerNj0rsJxONQGdhUMynQIvd6LzB+1J5oKA== graphql-tools@^4.0.0, graphql-tools@^4.0.4, graphql-tools@^4.0.5: version "4.0.5" @@ -4535,7 +4553,7 @@ har-schema@^2.0.0: resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= -har-validator@~5.1.0: +har-validator@~5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== @@ -4920,6 +4938,21 @@ invariant@^2.2.2, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +ioredis@^4.14.1, ioredis@^4.6.3: + version "4.14.1" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.14.1.tgz#b73ded95fcf220f106d33125a92ef6213aa31318" + integrity sha512-94W+X//GHM+1GJvDk6JPc+8qlM7Dul+9K+lg3/aHixPN7ZGkW6qlvX0DG6At9hWtH2v3B32myfZqWoANUJYGJA== + dependencies: + cluster-key-slot "^1.1.0" + debug "^4.1.1" + denque "^1.1.0" + lodash.defaults "^4.2.0" + lodash.flatten "^4.4.0" + redis-commands "1.5.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.0.1" + ip-regex@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd" @@ -5960,11 +5993,21 @@ lodash.clonedeep@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + lodash.escaperegexp@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -6154,19 +6197,19 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== -metascraper-audio@^5.10.6: - version "5.10.6" - resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.10.6.tgz#095892445b90d40bc54e54f69536a80e36fd9e4c" - integrity sha512-wTVtYK8Ico82caIi6HlkyGgUaBC21X/vhT2aQ4LKcg+gHoOhJcmWNd5me9VhaRJ7gTV/7yKkL5A54fBcjcn8Kg== +metascraper-audio@^5.10.7: + version "5.10.7" + resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.10.7.tgz#ba9f8333a7b71d388a0bf88dff64fc4f06595566" + integrity sha512-VHZlT21bh/TWnHOQMGret3UcMdJOsyWvagK7MG8rLczYmrPEtvxnJjwPhyrEj1oJC+fz2P//bfQ6gyrD4HrmEQ== dependencies: - "@metascraper/helpers" "^5.10.6" + "@metascraper/helpers" "^5.10.7" -metascraper-author@^5.10.6: - version "5.10.6" - resolved "https://registry.yarnpkg.com/metascraper-author/-/metascraper-author-5.10.6.tgz#1ceaacec776d46629300db25e17657fe35a14a20" - integrity sha512-L2P/Fp0npaQcowbwi1vHKJbSYc99cio58/yYRm205xGfgMCRMpYOrYB+ecizXgeSSRiv8G8SXLrLXOLJ5K+SdA== +metascraper-author@^5.10.7: + version "5.10.7" + resolved "https://registry.yarnpkg.com/metascraper-author/-/metascraper-author-5.10.7.tgz#99b3a2b982b7a63feea41554659be3db7bf7035c" + integrity sha512-AdNkcqy+eqs2Eeh+6odhIWArR4pWVlrCx3jMaho0BDY6ZnKgJP44HtlPNkghQpBaueoKX6CycGKraITzwjGj1w== dependencies: - "@metascraper/helpers" "^5.10.6" + "@metascraper/helpers" "^5.10.7" lodash "~4.17.15" metascraper-clearbit-logo@^5.3.0: @@ -6176,26 +6219,26 @@ metascraper-clearbit-logo@^5.3.0: dependencies: got "~9.6.0" -metascraper-date@^5.10.6: - version "5.10.6" - resolved "https://registry.yarnpkg.com/metascraper-date/-/metascraper-date-5.10.6.tgz#dbdc0ddb4f4220ad1ea412b4a686900c1b138cf6" - integrity sha512-WfZw7WhkMKrrq96ZcAxS01/YjiDBpAPt5e3ggnCfLi2ZzC370w9J0INUo7gAtujaNZvgTTSEcrDD7AbTVMSYKA== +metascraper-date@^5.10.7: + version "5.10.7" + resolved "https://registry.yarnpkg.com/metascraper-date/-/metascraper-date-5.10.7.tgz#580891d98f14438658610d951e5454a0658eb5f3" + integrity sha512-S1ZsvYrOccS6EGvlAJT7Ph08uRgIE+aYlTRSCR8wG8P0j0Ta/srUKbPySxv+xXqDXuRIChtErSIsBgORg8uNjg== dependencies: - "@metascraper/helpers" "^5.10.6" + "@metascraper/helpers" "^5.10.7" -metascraper-description@^5.10.6: - version "5.10.6" - resolved "https://registry.yarnpkg.com/metascraper-description/-/metascraper-description-5.10.6.tgz#ebb4459a4e1acdc473534d1b898b7958b1769eb6" - integrity sha512-d3d6UMsNnD8Dy7gxA05iTOj5QmJrFQTw1+IrW9CiOfdNsYq0H+m265a9lRaXcyJdqkzmGnv/d52C0BtUDOrkRw== +metascraper-description@^5.11.0: + version "5.11.0" + resolved "https://registry.yarnpkg.com/metascraper-description/-/metascraper-description-5.11.0.tgz#1f7edfae7ffc353512975cafaa5b862fc5141709" + integrity sha512-or62L9EbIwBxjsu6gShTKi1z1XQ4Hvml8MrdydL4tiSUJpwf+lXc6gVOP8+nlKPPRkiAhXt2M9k4mfkGoX0erQ== dependencies: - "@metascraper/helpers" "^5.10.6" + "@metascraper/helpers" "^5.10.7" -metascraper-image@^5.10.6: - version "5.10.6" - resolved "https://registry.yarnpkg.com/metascraper-image/-/metascraper-image-5.10.6.tgz#b23ec4bfab0467342b294f3d049c7b7e9a1dd071" - integrity sha512-/+m0VaaqnBgNREun/8jcq+clc4s9Z0FAuMO/TQf/mrz9SbcmpHeTD0WpiGJgEa9zYSbuEQYHdBkxhpw+SdJPCA== +metascraper-image@^5.11.1: + version "5.11.1" + resolved "https://registry.yarnpkg.com/metascraper-image/-/metascraper-image-5.11.1.tgz#e63e9ff045441783f9aa8c684e04927cac289e44" + integrity sha512-Nz2ZTecj2V0KgK2QE390dOSedppaG2PtUBrTz/oaFLMZEReBtMVrcygYm9VuuTpa6XwkubvuBaouCRah12zdfg== dependencies: - "@metascraper/helpers" "^5.10.6" + "@metascraper/helpers" "^5.11.1" metascraper-lang-detector@^4.10.2: version "4.10.2" @@ -6206,74 +6249,74 @@ metascraper-lang-detector@^4.10.2: franc "~4.0.0" iso-639-3 "~1.1.0" -metascraper-lang@^5.10.6: - version "5.10.6" - resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-5.10.6.tgz#d4130257f6604095e9af8d796a6bde5815f6c667" - integrity sha512-JDhNbP1iSnPV7d6PklIIdBSzlwqbtvH+n810Isa5/PGuvUkJzNkTAUN+eTM1i6YcTlMp1N2gYsQG9uwfpMwFog== +metascraper-lang@^5.10.7: + version "5.10.7" + resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-5.10.7.tgz#fe07c359b757ef3c5ba0a330da97173945196fde" + integrity sha512-ulLq7g+X4F9XzoScNflkhwWUuoSycbOqJ8j7vT6E/sHYPid+BdUnw2yaxAqXOdsrsbUEWzkiDwrJiq37XWCpDA== dependencies: - "@metascraper/helpers" "^5.10.6" + "@metascraper/helpers" "^5.10.7" -metascraper-logo@^5.10.6: - version "5.10.6" - resolved "https://registry.yarnpkg.com/metascraper-logo/-/metascraper-logo-5.10.6.tgz#22223ce79e4017f159b2a9ddc311d2eb636043b5" - integrity sha512-/uGW+X43T6Oj5DxWqAhANII9BdhQuM+e7O6/Vu116uLqW6cOJ/RDp5qp7ngKF41L0zCLd8Q2Xw2nduHi6tC4Uw== +metascraper-logo@^5.10.7: + version "5.10.7" + resolved "https://registry.yarnpkg.com/metascraper-logo/-/metascraper-logo-5.10.7.tgz#335812b9aae5d814b22294cdd402d62fa14a46f1" + integrity sha512-Ic9WgvRqm3pUlsMfSirzCK9+qmQ9pbv/u9APn8PM5y66zNJoSCOVWbEIoEA0bVPHbGKGKFvsgrOm3VkMoVUF0Q== dependencies: - "@metascraper/helpers" "^5.10.6" + "@metascraper/helpers" "^5.10.7" -metascraper-publisher@^5.10.6: - version "5.10.6" - resolved "https://registry.yarnpkg.com/metascraper-publisher/-/metascraper-publisher-5.10.6.tgz#95dde6ecef3c7b890ac625893ab374096bebe1f4" - integrity sha512-4jTOpbIwXBADl6z39UzQ4DZLeVoj4Q+5dcHbEgGn9MQ9878FgxiJKyrHzYvqfe9fRNd0PcaFMuuwLyhz58haoA== +metascraper-publisher@^5.10.7: + version "5.10.7" + resolved "https://registry.yarnpkg.com/metascraper-publisher/-/metascraper-publisher-5.10.7.tgz#7add40be1625a4215e2ec36dedaaaf213c1870e2" + integrity sha512-aRws5ksH+gzb49nc41oJdoJS2jG/2vC3GCi68n4Evy/TL7TYCnbZXqcgT5U0ne1kFxCZlHB6rl0DcXkx/JlT5Q== dependencies: - "@metascraper/helpers" "^5.10.6" + "@metascraper/helpers" "^5.10.7" -metascraper-soundcloud@^5.10.6: - version "5.10.6" - resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.10.6.tgz#7aff2e17214b6939719ea726a0f5a5fe2a48c5a5" - integrity sha512-WO+B81e04Hng4/YOtq4dpNv9CrGWVemrNuZk3iIJU+B+gF3YpzdERxS4aIpM7KHcY/c8xu3xDM3LrSUWXWjM3g== +metascraper-soundcloud@^5.10.7: + version "5.10.7" + resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.10.7.tgz#64a5518324ea69576ab9c206f29a60160fb89e91" + integrity sha512-KPM/g+l1m0tp4YOu8qF1RUT7yondaY1S/0aieUUFck/iE3VA1i3MJTLLygIc+67fcbHqcz60qfPTOvYbl6sIPw== dependencies: - "@metascraper/helpers" "^5.10.6" + "@metascraper/helpers" "^5.10.7" tldts "~5.6.3" -metascraper-title@^5.10.6: - version "5.10.6" - resolved "https://registry.yarnpkg.com/metascraper-title/-/metascraper-title-5.10.6.tgz#0fd9a9bed7a0b990663086cdab45d11cd8cd3c7d" - integrity sha512-x4P8zr0x6Gh3gt26tf2xfjikG9xNS9MC3z4N2VP+OrYNuCc7Vz6TU+DR/DLAeZphsb1flgTd3P4iUfPUcWVTEQ== +metascraper-title@^5.10.7: + version "5.10.7" + resolved "https://registry.yarnpkg.com/metascraper-title/-/metascraper-title-5.10.7.tgz#037aaa8cbdc79d1dde186887eb2bba542281315e" + integrity sha512-iQYaMdGpBPj6dyk7rbP+zYo7EroC/1yY27jocAqUnMRTfrHXTR7kGucR0vi4y14BiFRLBTLSNbZbM4KAUQsFjg== dependencies: - "@metascraper/helpers" "^5.10.6" + "@metascraper/helpers" "^5.10.7" lodash "~4.17.15" -metascraper-url@^5.10.6: - version "5.10.6" - resolved "https://registry.yarnpkg.com/metascraper-url/-/metascraper-url-5.10.6.tgz#45f0ea173fecfe56d60b3cddd3c018f9f4fd9b92" - integrity sha512-7F6uAsI27iVXxUMwwzXH0ret81CX1jgtoGCMz+TvZkyS0z4aUs0r8QpYRYEQuXrW+JawRVik0up54F/ScslObQ== +metascraper-url@^5.10.7: + version "5.10.7" + resolved "https://registry.yarnpkg.com/metascraper-url/-/metascraper-url-5.10.7.tgz#42b71c8540c13baafb7757972ea672721d63e019" + integrity sha512-z1LBPTupU5cF36/i/iGe0rzLbO7iGBSdbgEOztLcnIhnMC8Nl9xjvIrlvNciKTMxDyr3JGrvFFWugFzwMzVoQg== dependencies: - "@metascraper/helpers" "^5.10.6" + "@metascraper/helpers" "^5.10.7" -metascraper-video@^5.10.6: - version "5.10.6" - resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-5.10.6.tgz#8425d2dfc378b20612e8ef9324989a33bc0341b3" - integrity sha512-DzWBCe/z86QFv6mN9ZDmvk32FMWv+nPDSkyMEL7RCU6VeQOFFAOjwhDglp2qBMs8Xif358bQ4H/0akLZpDUfvw== +metascraper-video@^5.10.7: + version "5.10.7" + resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-5.10.7.tgz#2892819a3613ddac115ada7fb1b28d74a646f974" + integrity sha512-+fjiL/Vq0DGd7dMvBTdFKrOK2YH2myHssSChRZVZLl3gzyo4YEWpnOWBhfuoky0caOcs9+RVAxF9pE1TdTFmbA== dependencies: - "@metascraper/helpers" "^5.10.6" + "@metascraper/helpers" "^5.10.7" lodash "~4.17.15" -metascraper-youtube@^5.10.6: - version "5.10.6" - resolved "https://registry.yarnpkg.com/metascraper-youtube/-/metascraper-youtube-5.10.6.tgz#6cacabb1791b06ed98a7da69aa00c1c6c50a2dfe" - integrity sha512-Yl5kEFawqpSGmVSG2yTVZj7mGfRSFGQ2A4cxpqSbaPIUCGJwG9BUJkMzyUG0m6jGrg0zI5CmeZGNBAXzgKGz4g== +metascraper-youtube@^5.10.7: + version "5.10.7" + resolved "https://registry.yarnpkg.com/metascraper-youtube/-/metascraper-youtube-5.10.7.tgz#6c3313563ee57cb71c496fa99c129ad41fe22291" + integrity sha512-T4ZawYpW/2lyoVUY9RM92YCxkuyhNDXFxg8XAG9u2hoNZ5elrHLOv67ao5zMHa7IXZY3A7IGDOBd1NfoShnToA== dependencies: - "@metascraper/helpers" "^5.10.6" + "@metascraper/helpers" "^5.10.7" get-video-id "~3.1.4" is-reachable "~4.0.0" p-locate "~4.1.0" -metascraper@^5.10.6: - version "5.10.6" - resolved "https://registry.yarnpkg.com/metascraper/-/metascraper-5.10.6.tgz#d1249577a768566b86bd099cc2256df45cf05181" - integrity sha512-mPEDvoyHLWb0AlTE05W43vfKGgBJ99s5AYAhB4IiRgGR9uq8j7/ktTZaS/+pyFopYrEoe71L/k4KbYgNPQRASA== +metascraper@^5.11.0: + version "5.11.0" + resolved "https://registry.yarnpkg.com/metascraper/-/metascraper-5.11.0.tgz#bbb25eb055c0ec03992df99c10c48057751ee56b" + integrity sha512-IocQqdSQnOpbai0X9Cu37w/AKeSfU513MCfaFzzcvWgI8s6mGQ1DWRFPvkx0ahtixpP0/ifE4t7ycODrtepRxQ== dependencies: - "@metascraper/helpers" "^5.10.6" + "@metascraper/helpers" "^5.10.7" cheerio "~1.0.0-rc.3" cheerio-advanced-selectors "~2.0.1" lodash "~4.17.15" @@ -6325,24 +6368,12 @@ migrate@^1.6.2: mkdirp "^0.5.1" slug "^0.9.2" -mime-db@1.40.0: - version "1.40.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" - integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== - mime-db@1.43.0: version "1.43.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== -mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.22, mime-types@~2.1.24: - version "2.1.24" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" - integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== - dependencies: - mime-db "1.40.0" - -mime-types@~2.1.26: +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.22, mime-types@~2.1.24, mime-types@~2.1.26: version "2.1.26" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== @@ -6702,10 +6733,10 @@ normalize-url@~4.2.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.2.0.tgz#e747f16b58e6d7f391495fd86415fa04ec7c9897" integrity sha512-n69+KXI+kZApR+sPwSkoAXpGlNkaiYyoHHqKOFPjJWvwZpew/EjKvuPE4+tStNgb42z5yLtdakgZCQI+LalSPg== -normalize-url@~4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" - integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== +normalize-url@~5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-5.0.0.tgz#f46c9dc20670495e4e18fbd1b4396e41d199f63c" + integrity sha512-bAEm2fx8Dq/a35Z6PIRkkBBJvR56BbEJvhpNtvCZ4W9FyORSna77fn+xtYFjqk5JpBS+fMnAOG/wFgkQBmB7hw== npm-bundled@^1.0.1: version "1.0.6" @@ -7328,7 +7359,7 @@ pseudomap@^1.0.2: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= -psl@^1.1.24, psl@^1.1.28: +psl@^1.1.28: version "1.3.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.3.0.tgz#e1ebf6a3b5564fa8376f3da2275da76d875ca1bd" integrity sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag== @@ -7351,11 +7382,6 @@ punycode2@~1.0.0: resolved "https://registry.yarnpkg.com/punycode2/-/punycode2-1.0.0.tgz#e2b4b9a9a8ff157d0b84438e203181ee7892dfd8" integrity sha1-4rS5qaj/FX0LhEOOIDGB7niS39g= -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -7506,6 +7532,23 @@ realpath-native@^1.1.0: dependencies: util.promisify "^1.0.0" +redis-commands@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785" + integrity sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg== + +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + dependencies: + redis-errors "^1.0.0" + referrer-policy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.2.0.tgz#b99cfb8b57090dc454895ef897a4cc35ef67a98e" @@ -7628,10 +7671,10 @@ request-promise-native@^1.0.7, request-promise-native@^1.0.8: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@^2.88.0, request@~2.88.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== +request@^2.88.0, request@~2.88.2: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -7640,7 +7683,7 @@ request@^2.88.0, request@~2.88.0: extend "~3.0.2" forever-agent "~0.6.1" form-data "~2.3.2" - har-validator "~5.1.0" + har-validator "~5.1.3" http-signature "~1.2.0" is-typedarray "~1.0.0" isstream "~0.1.2" @@ -7650,7 +7693,7 @@ request@^2.88.0, request@~2.88.0: performance-now "^2.1.0" qs "~6.5.2" safe-buffer "^5.1.2" - tough-cookie "~2.4.3" + tough-cookie "~2.5.0" tunnel-agent "^0.6.0" uuid "^3.3.2" @@ -7754,6 +7797,11 @@ rimraf@^3.0.0: dependencies: glob "^7.1.3" +rosie@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/rosie/-/rosie-2.0.1.tgz#c250c4787ce450b72aa9eff26509f68589814fa2" + integrity sha1-wlDEeHzkULcqqe/yZQn2hYmBT6I= + router-ips@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/router-ips/-/router-ips-1.0.0.tgz#44e00858ebebc0133d58e40b2cd8a1fbb04203f5" @@ -8208,6 +8256,11 @@ stacktrace-js@^2.0.0: stack-generator "^2.0.1" stacktrace-gps "^3.0.1" +standard-as-callback@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.0.1.tgz#ed8bb25648e15831759b6023bdb87e6b60b38126" + integrity sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg== + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -8673,7 +8726,7 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@^2.3.3: +tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== @@ -8690,14 +8743,6 @@ tough-cookie@^3.0.1: psl "^1.1.28" punycode "^2.1.1" -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" diff --git a/cypress/fixtures/humanconnection.png b/cypress/fixtures/humanconnection.png new file mode 100644 index 000000000..f0576413f Binary files /dev/null and b/cypress/fixtures/humanconnection.png differ diff --git a/cypress/integration/administration/TagsAndCategories.feature b/cypress/integration/administration/TagsAndCategories.feature index f93cdb59c..516966c6b 100644 --- a/cypress/integration/administration/TagsAndCategories.feature +++ b/cypress/integration/administration/TagsAndCategories.feature @@ -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 diff --git a/cypress/integration/common/post.js b/cypress/integration/common/post.js index 53b204feb..f1380378f 100644 --- a/cypress/integration/common/post.js +++ b/cypress/integration/common/post.js @@ -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"; @@ -84,3 +87,82 @@ And("the post with title {string} has a ribbon for pinned posts", (title) => { 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('.ds-flex-item > .ds-form-item .ds-select ') + .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(".ds-card-content > .ds-heading") + .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 be removed from the preview", () => { + cy.fixture("humanconnection.png").as('postTeaserImage').then(function() { + cy.get("#postdropzone") + .children() + .get('img.thumbnail-preview') + .should('have.length', 1) + .and('have.attr', 'src') + .and('contain', this.postTeaserImage) + }) +}) + +Then('the {string} post was saved successfully without a teaser image', condition => { + cy.get(".ds-card-content > .ds-heading") + .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('.post-page img.ds-card-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-image') + .should('exist') + .and('have.attr', 'src') +}) + +Then('I should be able to remove the image', () => { + cy.get('.delete-image') + .click() +}) \ No newline at end of file diff --git a/cypress/integration/common/report.js b/cypress/integration/common/report.js index 77c97b53b..fe7a31363 100644 --- a/cypress/integration/common/report.js +++ b/cypress/integration/common/report.js @@ -30,16 +30,24 @@ 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', () => { @@ -127,7 +135,7 @@ 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) { @@ -166,8 +174,9 @@ Then('I can visit the post page', () => { When("they have a post someone has reported", () => { cy.factory() - .create("Post", { - authorId: 'annnoying-user', + .build("post", { title, + }, { + authorId: 'annnoying-user', }); }) diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 42a5a511d..f95f95f21 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -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 input") + 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,7 +163,7 @@ 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/, () => { @@ -203,33 +227,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 +262,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 => { @@ -311,17 +336,27 @@ 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", () => { @@ -428,12 +434,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 +456,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,12 +503,11 @@ 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', }); }); @@ -521,12 +525,11 @@ Then("I get removed from his follower collection", () => { }); 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 +555,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 => { @@ -581,6 +586,27 @@ Then("they should not see the comment from", () => { 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('.ds-card-content .action-buttons') + .should('have.length', 1) +}) + +Then('I should see the {string} button', button => { + cy.get('.ds-card-content .action-buttons .base-button') + .should('contain', button) +}) diff --git a/cypress/integration/moderation/ReportContent.feature b/cypress/integration/moderation/ReportContent.feature index 105bad5e6..be1a07786 100644 --- a/cypress/integration/moderation/ReportContent.feature +++ b/cypress/integration/moderation/ReportContent.feature @@ -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 diff --git a/cypress/integration/notifications/Mentions.feature b/cypress/integration/notifications/Mentions.feature index ef2694abc..02dc0abd2 100644 --- a/cypress/integration/notifications/Mentions.feature +++ b/cypress/integration/notifications/Mentions.feature @@ -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 @@ -23,9 +21,7 @@ Feature: Notification for a mention 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" diff --git a/cypress/integration/post/Comment.feature b/cypress/integration/post/Comment.feature index 66cf7a6d7..da261726b 100644 --- a/cypress/integration/post/Comment.feature +++ b/cypress/integration/post/Comment.feature @@ -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 diff --git a/cypress/integration/post/DeleteImage.feature b/cypress/integration/post/DeleteImage.feature new file mode 100644 index 000000000..a3fa6f9b6 --- /dev/null +++ b/cypress/integration/post/DeleteImage.feature @@ -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 diff --git a/cypress/integration/post/ImageUploader.feature b/cypress/integration/post/ImageUploader.feature new file mode 100644 index 000000000..2e9f1f5b9 --- /dev/null +++ b/cypress/integration/post/ImageUploader.feature @@ -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 be removed from the preview + + 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 \ No newline at end of file diff --git a/cypress/integration/user_account/ChangePassword.feature b/cypress/integration/user_account/ChangePassword.feature index 44e4e5483..dbdf724f7 100644 --- a/cypress/integration/user_account/ChangePassword.feature +++ b/cypress/integration/user_account/ChangePassword.feature @@ -9,10 +9,9 @@ Feature: Change password password or just out of an good habit, you want to change your password. Background: - Given my user account has the following login credentials: + Given I am logged in with these credentials: | email | password | | user@example.org | exposed | - And I am logged in Scenario: Change my password Given I am on the "settings" page diff --git a/cypress/integration/user_account/Login.feature b/cypress/integration/user_account/Login.feature index 3837f7042..6e8f60a56 100644 --- a/cypress/integration/user_account/Login.feature +++ b/cypress/integration/user_account/Login.feature @@ -7,7 +7,7 @@ Feature: Authentication Given I have a user account Scenario: Log in - When I visit the "/login" page + When I visit the "login" page And I fill in my email and password combination and click submit Then I can click on my profile picture in the top right corner And I can see my name "Peter Lustig" in the dropdown menu diff --git a/cypress/integration/user_profile/BlockUser.feature b/cypress/integration/user_profile/BlockUser.feature index 43efe7807..256d79dfb 100644 --- a/cypress/integration/user_profile/BlockUser.feature +++ b/cypress/integration/user_profile/BlockUser.feature @@ -11,6 +11,7 @@ Feature: Block a User Scenario: Block a user Given I am on the profile page of the annoying user When I click on "Block user" from the content menu in the user info box + And I "should" see "Unblock user" from the content menu in the user info box And I navigate to my "Blocked users" settings page Then I can see the following table: | Avatar | Name | @@ -20,14 +21,15 @@ Feature: Block a User Given I block the user "Harassing User" And I previously created a post And a blocked user visits the post page of one of my authored posts - Then they should not see the comment from - And they should see a text explaining commenting is not possible + Then they should see a text explaining why commenting is not possible + And they should not see the comment form Scenario: Block a previously followed user Given I follow the user "Harassing User" When I visit the profile page of the annoying user And I click on "Block user" from the content menu in the user info box And I get removed from his follower collection + And I "should" see "Unblock user" from the content menu in the user info box Scenario: Posts of blocked users are not filtered from search results Given "Harassing User" wrote a post "You can still see my posts" @@ -44,3 +46,15 @@ Feature: Block a User Then I should see the following posts in the select dropdown: | title | | previously created post | + + Scenario: Blocked users cannot see they are blocked in their list + Given a user has blocked me + And I navigate to my "Blocked users" settings page + Then I should see no users in my blocked users list + + Scenario: Blocked users should not see link or button to unblock, only blocking users + Given a user has blocked me + When I visit the profile page of the annoying user + And I "should not" see "Unblock user" from the content menu in the user info box + And I should see the "Follow" button + And I should not see "Unblock user" button \ No newline at end of file diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index 893b99f4f..cc6ac0e91 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -23,7 +23,7 @@ module.exports = (on, config) => { config.env.NEO4J_URI = parsed.NEO4J_URI config.env.NEO4J_USERNAME = parsed.NEO4J_USERNAME config.env.NEO4J_PASSWORD = parsed.NEO4J_PASSWORD - + config.env.JWT_SECRET = parsed.JWT_SECRET on('file:preprocessor', cucumber()) return config } diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 16ac43a19..75f27d0f7 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -18,20 +18,24 @@ import helpers from "./helpers"; import { GraphQLClient, request } from 'graphql-request' import { gql } from '../../backend/src/helpers/jest' import config from '../../backend/src/config' +import encode from '../../backend/src/jwt/encode' const switchLang = name => { cy.get(".locale-menu").click(); cy.contains(".locale-menu-popover a", name).click(); }; -const authenticatedHeaders = async (variables) => { +const authenticatedHeaders = (variables) => { const mutation = gql` mutation($email: String!, $password: String!) { login(email: $email, password: $password) } ` - const response = await request(config.GRAPHQL_URI, mutation, variables) - return { authorization: `Bearer ${response.login}` } + return new Cypress.Promise((resolve, reject) => { + request(config.GRAPHQL_URI, mutation, variables).then((response) => { + resolve({ authorization: `Bearer ${response.login}` }) + }) + }) } Cypress.Commands.add("switchLanguage", (name, force) => { @@ -47,7 +51,13 @@ Cypress.Commands.add("switchLanguage", (name, force) => { } }); -Cypress.Commands.add("login", ({ email, password }) => { +Cypress.Commands.add("login", user => { + const token = encode(user) + cy.setCookie('human-connection-token', token) + .visit("/") +}); + +Cypress.Commands.add("manualLogin", ({ email, password }) => { cy.visit(`/login`); cy.get("input[name=email]") .trigger("focus") @@ -58,11 +68,9 @@ Cypress.Commands.add("login", ({ email, password }) => { cy.get("button[name=submit]") .as("submitButton") .click(); - cy.get(".iziToast-message").should("contain", "You are logged in!"); - cy.location("pathname").should("eq", "/"); }); -Cypress.Commands.add("logout", (email, password) => { +Cypress.Commands.add("logout", () => { cy.visit(`/logout`); cy.location("pathname").should("contain", "/login"); // we're out }); @@ -74,43 +82,24 @@ Cypress.Commands.add("openPage", page => { cy.visit(`/${page}`); }); -Cypress.Commands.add("createCategories", (id, slug) => { - cy.neode() - .create("Category", { - id: `${id}`, - name: "Just For Fun", - slug: `${slug}`, - icon: "smile" - }) - .create("Category", { - id: `${id}1`, - name: "Happiness & Values", - icon: "heart-o" - }) - .create("Category", { - id: `${id}2`, - name: "Health & Wellbeing", - icon: "medkit" - }); -}); - - Cypress.Commands.add( 'authenticateAs', - async ({email, password}) => { - const headers = await authenticatedHeaders({ email, password }) - return new GraphQLClient(config.GRAPHQL_URI, { headers }) - } -) + ({email, password}) => { + return new Cypress.Promise((resolve, reject) => { + authenticatedHeaders({ email, password }).then((headers) => { + resolve(new GraphQLClient(config.GRAPHQL_URI, { headers })) + }) + }) + }) Cypress.Commands.add( 'mutate', { prevSubject: true }, - async (graphQLClient, mutation, variables) => { - await graphQLClient.request(mutation, variables) - return graphQLClient - } -) + (graphQLClient, mutation, variables) => { + return new Cypress.Promise((resolve, reject) => { + graphQLClient.request(mutation, variables).then(() => resolve(graphQLClient)) + }) + }) // // diff --git a/cypress/support/factories.js b/cypress/support/factories.js index 1b76a1a01..d2a8d87ad 100644 --- a/cypress/support/factories.js +++ b/cypress/support/factories.js @@ -1,4 +1,4 @@ -import Factory from '../../backend/src/factories' +import Factory, { cleanDatabase } from '../../backend/src/db/factories' import { getDriver, getNeode } from '../../backend/src/db/neo4j' const neo4jConfigs = { @@ -6,51 +6,38 @@ const neo4jConfigs = { username: Cypress.env('NEO4J_USERNAME'), password: Cypress.env('NEO4J_PASSWORD') } -const neo4jDriver = getDriver(neo4jConfigs) const neodeInstance = getNeode(neo4jConfigs) -const factoryOptions = { neo4jDriver, neodeInstance } -const factory = Factory(factoryOptions) -beforeEach(async () => { - await factory.cleanDatabase() -}) +beforeEach(() => cleanDatabase()) Cypress.Commands.add('neode', () => { return neodeInstance }) + Cypress.Commands.add( 'first', { prevSubject: true }, - async (neode, model, properties) => { + (neode, model, properties) => { return neode.first(model, properties) } ) Cypress.Commands.add( 'relateTo', { prevSubject: true }, - async (node, otherNode, relationship) => { + (node, otherNode, relationship) => { return node.relateTo(otherNode, relationship) } ) -Cypress.Commands.add('factory', () => { - return Factory(factoryOptions) -}) +Cypress.Commands.add('factory', () => Factory) Cypress.Commands.add( - 'create', + 'build', { prevSubject: true }, - async (factory, node, properties) => { - await factory.create(node, properties) - return factory + (factory, name, atrributes, options) => { + return new Cypress.Promise((resolve, reject) => { + return factory.build(name, atrributes, options).then(() => resolve(factory)) + }) } ) -Cypress.Commands.add( - 'relate', - { prevSubject: true }, - async (factory, node, relationship, properties) => { - await factory.relate(node, relationship, properties) - return factory - } -) diff --git a/deployment/.gitignore b/deployment/.gitignore index 14cfa18ed..61e591624 100644 --- a/deployment/.gitignore +++ b/deployment/.gitignore @@ -2,3 +2,5 @@ secrets.yaml configmap.yaml **/secrets.yaml **/configmap.yaml +**/staging-values.yaml +**/production-values.yaml \ No newline at end of file diff --git a/deployment/README.md b/deployment/README.md index 0615ccf9b..09dcd5f00 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -1,11 +1,10 @@ # Human-Connection Nitro \| Deployment Configuration -We deploy with [kubernetes](https://kubernetes.io/). In order to deploy your own -network you have to [install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) -and get a kubernetes cluster. +There are a couple different ways we have tested to deploy an instance of Human Connection, with [kubernetes](https://kubernetes.io/) and via [Helm](https://helm.sh/docs/). In order to manage your own +network, you have to [install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/), [install Helm](https://helm.sh/docs/intro/install/) (optional, but the preferred way), +and set up a kubernetes cluster. Since there are many different options to host your cluster, we won't go into specifics here. We have tested two different kubernetes providers: [Minikube](./minikube/README.md) and [Digital Ocean](./digital-ocean/README.md). -Check out the specific documentation for your provider. After that, learn how -to apply the specific kubernetes configuration for [Human Connection](./human-connection/README.md). +Check out the specific documentation for your provider. After that, choose whether you want to go with the recommended deploy option [Helm](./helm/README.md), or use kubernetes to apply the configuration for [Human Connection](./human-connection/README.md). diff --git a/deployment/helm/human-connection/.helmignore b/deployment/helm/human-connection/.helmignore new file mode 100644 index 000000000..50af03172 --- /dev/null +++ b/deployment/helm/human-connection/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deployment/helm/human-connection/Chart.yaml b/deployment/helm/human-connection/Chart.yaml new file mode 100644 index 000000000..525d5a8e8 --- /dev/null +++ b/deployment/helm/human-connection/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "0.3.1" +description: A Helm chart for Human Connection +name: human-connection +version: 0.1.0 diff --git a/deployment/helm/human-connection/README.md b/deployment/helm/human-connection/README.md new file mode 100644 index 000000000..444f9da15 --- /dev/null +++ b/deployment/helm/human-connection/README.md @@ -0,0 +1,72 @@ +# Helm installation of Human Connection + +Deploying Human Connection with Helm is very straight forward. All you have to +do is to change certain parameters, like domain names and API keys, then you +just install our provided Helm chart to your cluster. + +## Configuration + +You can customize the network with your configuration by changing the `values.yaml`, all variables will be available as +environment variables in your deployed kubernetes pods. + +Probably you want to change this environment variable to your actual domain: + +```bash +# in folder /deployment/helm +CLIENT_URI: "https://develop.human-connection.org" +``` + +If you want to edit secrets, you have to `base64` encode them. See [kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/secret/#creating-a-secret-manually). You can also use `helm-secrets`, but we have yet to test it. + +```bash +# example how to base64 a string: +$ echo -n 'admin' | base64 +YWRtaW4= +``` +Those secrets get `base64` decoded and are available as environment variables in +your deployed kubernetes pods. + +# https +If you start with setting up the `https`, when you install the app, it will automatically take care of the certificates for you. + +First check that you are using `Helm v3`, this is important since it removes the need for `Tiller`. See, [FAQ](https://helm.sh/docs/faq/#removal-of-tiller) + +```bash +$ helm version +# output should look similar to this: +#version.BuildInfo{Version:"v3.0.2", GitCommit:"19e47ee3283ae98139d98460de796c1be1e3975f", GitTreeState:"clean", GoVersion:"go1.13.5"} +``` + +Apply cert-manager CRDs before installing (or it will fail) + +```bash +$ kubectl apply --validate=false -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.13.0/deploy/manifests/00-crds.yaml +``` + +Next, create the `cert-manager` namespace +```bash +$ kubectl create namespace cert-manager +``` +Add the `jetstack` repo and update + +```bash +$ helm repo add jetstack https://charts.jetstack.io +$ helm repo update +``` + +Install cert-manager +```bash +$ helm install cert-manager --namespace cert-manager --version v0.13.0 jetstack/cert-manager +``` + +# Deploy + +Once you are satisfied with the configuration, you can install the app. + +```bash +# in folder /deployment/helm/human-connection +$ helm install develop ./ --namespace human-connection +``` +Where `develop` is the release name, in this case develop for our develop server and `human-connection` is the namespace, again customize for your needs. The release name can be anything you want. Just keep in mind that it is used in the templates to prepend the `CLIENT_URI` and other places. + +This will set up everything you need for the network, including `deployments`, and their `pods`, `services`, `ingress`, `volumes`(PersitentVolumes), `PersistentVolumeClaims`, and even `ClusterIssuers` for https certificates. diff --git a/deployment/helm/human-connection/templates/cluster-issuers/letsencrypt-prod.yaml b/deployment/helm/human-connection/templates/cluster-issuers/letsencrypt-prod.yaml new file mode 100644 index 000000000..ac11e76b8 --- /dev/null +++ b/deployment/helm/human-connection/templates/cluster-issuers/letsencrypt-prod.yaml @@ -0,0 +1,20 @@ +apiVersion: cert-manager.io/v1alpha2 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: human-connection + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: {{ .Values.supportEmail }} + privateKeySecretRef: + name: letsencrypt-prod + solvers: + - http01: + ingress: + class: nginx diff --git a/deployment/helm/human-connection/templates/cluster-issuers/letsencrypt-staging.yaml b/deployment/helm/human-connection/templates/cluster-issuers/letsencrypt-staging.yaml new file mode 100644 index 000000000..cc9d9fdd7 --- /dev/null +++ b/deployment/helm/human-connection/templates/cluster-issuers/letsencrypt-staging.yaml @@ -0,0 +1,20 @@ +apiVersion: cert-manager.io/v1alpha2 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: human-connection + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + acme: + server: https://acme-staging-v02.api.letsencrypt.org/directory + email: {{ .Values.supportEmail }} + privateKeySecretRef: + name: letsencrypt-staging + solvers: + - http01: + ingress: + class: nginx diff --git a/deployment/helm/human-connection/templates/deployments/deployment-backend.yaml b/deployment/helm/human-connection/templates/deployments/deployment-backend.yaml new file mode 100644 index 000000000..33aa8a0e0 --- /dev/null +++ b/deployment/helm/human-connection/templates/deployments/deployment-backend.yaml @@ -0,0 +1,58 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-backend + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: human-connection + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + replicas: 1 + minReadySeconds: 15 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxSurge: 0 + maxUnavailable: "100%" + selector: + matchLabels: + human-connection.org/selector: deployment-backend + template: + metadata: + name: deployment-backend + annotations: + backup.velero.io/backup-volumes: uploads + labels: + human-connection.org/commit: {{ .Values.commit }} + human-connection.org/selector: deployment-backend + spec: + containers: + - name: backend + image: "{{ .Values.backendImage }}:{{ .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + envFrom: + - configMapRef: + name: {{ .Release.Name }}-configmap + - secretRef: + name: {{ .Release.Name }}-secrets + ports: + - containerPort: 4000 + protocol: TCP + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /nitro-backend/public/uploads + name: uploads + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - name: uploads + persistentVolumeClaim: + claimName: uploads-claim +status: {} diff --git a/deployment/helm/human-connection/templates/deployments/deployment-mailserver.yaml b/deployment/helm/human-connection/templates/deployments/deployment-mailserver.yaml new file mode 100644 index 000000000..85c587197 --- /dev/null +++ b/deployment/helm/human-connection/templates/deployments/deployment-mailserver.yaml @@ -0,0 +1,40 @@ +{{- if .Values.developmentMailserverDomain }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-mailserver + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: human-connection + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + replicas: 1 + minReadySeconds: 15 + progressDeadlineSeconds: 60 + selector: + matchLabels: + human-connection.org/selector: deployment-mailserver + template: + metadata: + labels: + human-connection.org/selector: deployment-mailserver + name: mailserver + spec: + containers: + - name: mailserver + image: djfarrelly/maildev + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: 80 + - containerPort: 25 + envFrom: + - configMapRef: + name: {{ .Release.Name }}-configmap + - secretRef: + name: {{ .Release.Name }}-secrets + restartPolicy: Always + terminationGracePeriodSeconds: 30 +status: {} +{{- end}} diff --git a/deployment/helm/human-connection/templates/deployments/deployment-maintenance.yaml b/deployment/helm/human-connection/templates/deployments/deployment-maintenance.yaml new file mode 100644 index 000000000..b12069f7e --- /dev/null +++ b/deployment/helm/human-connection/templates/deployments/deployment-maintenance.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-maintenance + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: human-connection + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + selector: + matchLabels: + human-connection.org/selector: deployment-maintenance + template: + metadata: + labels: + human-connection.org/commit: {{ .Values.commit }} + human-connection.org/selector: deployment-maintenance + name: maintenance + spec: + containers: + - name: maintenance + env: + - name: HOST + value: 0.0.0.0 + image: "{{ .Values.maintenanceImage }}:{{ .Chart.AppVersion }}" + ports: + - containerPort: 80 + imagePullPolicy: Always + restartPolicy: Always + terminationGracePeriodSeconds: 30 diff --git a/deployment/helm/human-connection/templates/deployments/deployment-neo4j.yaml b/deployment/helm/human-connection/templates/deployments/deployment-neo4j.yaml new file mode 100644 index 000000000..ae244fdce --- /dev/null +++ b/deployment/helm/human-connection/templates/deployments/deployment-neo4j.yaml @@ -0,0 +1,52 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-neo4j + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: human-connection + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + replicas: 1 + strategy: + rollingUpdate: + maxSurge: 0 + maxUnavailable: "100%" + selector: + matchLabels: + human-connection.org/selector: deployment-neo4j + template: + metadata: + name: neo4j + annotations: + backup.velero.io/backup-volumes: neo4j-data + labels: + human-connection.org/commit: {{ .Values.commit }} + human-connection.org/selector: deployment-neo4j + spec: + containers: + - name: neo4j + image: "{{ .Values.neo4jImage }}:{{ .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: 7687 + - containerPort: 7474 + resources: + requests: + memory: {{ .Values.neo4jResourceRequestsMemory | default "1G" | quote }} + limits: + memory: {{ .Values.neo4jResourceLimitsMemory | default "1G" | quote }} + envFrom: + - configMapRef: + name: {{ .Release.Name }}-configmap + volumeMounts: + - mountPath: /data/ + name: neo4j-data + volumes: + - name: neo4j-data + persistentVolumeClaim: + claimName: neo4j-data-claim + restartPolicy: Always + terminationGracePeriodSeconds: 30 diff --git a/deployment/helm/human-connection/templates/deployments/deployment-web.yaml b/deployment/helm/human-connection/templates/deployments/deployment-web.yaml new file mode 100644 index 000000000..3314f7ec0 --- /dev/null +++ b/deployment/helm/human-connection/templates/deployments/deployment-web.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-webapp + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: human-connection + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + replicas: 2 + minReadySeconds: 15 + progressDeadlineSeconds: 60 + selector: + matchLabels: + human-connection.org/selector: deployment-webapp + template: + metadata: + name: webapp + labels: + human-connection.org/commit: {{ .Values.commit }} + human-connection.org/selector: deployment-webapp + spec: + containers: + - name: webapp + image: "{{ .Values.webappImage }}:{{ .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + envFrom: + - configMapRef: + name: {{ .Release.Name }}-configmap + - secretRef: + name: {{ .Release.Name }}-secrets + env: + - name: HOST + value: 0.0.0.0 + ports: + - containerPort: 3000 + resources: {} + imagePullPolicy: Always + restartPolicy: Always + terminationGracePeriodSeconds: 30 +status: {} diff --git a/deployment/helm/human-connection/templates/ingress/ingress.template.yaml b/deployment/helm/human-connection/templates/ingress/ingress.template.yaml new file mode 100644 index 000000000..eb8dd65be --- /dev/null +++ b/deployment/helm/human-connection/templates/ingress/ingress.template.yaml @@ -0,0 +1,36 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ .Release.Name }}-ingress + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: human-connection + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" + annotations: + kubernetes.io/ingress.class: "nginx" + cert-manager.io/cluster-issuer: {{ .Values.letsencryptIssuer }} + nginx.ingress.kubernetes.io/proxy-body-size: 10m +spec: + tls: + - hosts: + - {{ .Values.domain }} + secretName: tls + rules: + - host: {{ .Values.domain }} + http: + paths: + - path: / + backend: + serviceName: {{ .Release.Name }}-webapp + servicePort: 3000 +{{- if .Values.developmentMailserverDomain }} + - host: {{ .Values.developmentMailserverDomain }} + http: + paths: + - path: / + backend: + serviceName: {{ .Release.Name }}-mailserver + servicePort: 80 +{{- end }} diff --git a/deployment/helm/human-connection/templates/jobs/job-db-migration.yaml b/deployment/helm/human-connection/templates/jobs/job-db-migration.yaml new file mode 100644 index 000000000..8a924f44b --- /dev/null +++ b/deployment/helm/human-connection/templates/jobs/job-db-migration.yaml @@ -0,0 +1,29 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Release.Name }}-db-migrations + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: human-connection + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" + annotations: + "helm.sh/hook": post-upgrade + "helm.sh/hook-weight": "5" + "helm.sh/hook-delete-policy": hook-succeeded, hook-failed +spec: + template: + metadata: + name: {{ .Release.Name }} + spec: + restartPolicy: Never + containers: + - name: db-migrations-job + image: "{{ .Values.backendImage }}:latest" + command: ["/bin/sh", "-c", "{{ .Values.dbMigrations }}"] + envFrom: + - configMapRef: + name: {{ .Release.Name }}-configmap + - secretRef: + name: {{ .Release.Name }}-secrets \ No newline at end of file diff --git a/deployment/helm/human-connection/templates/services/service-backend.yaml b/deployment/helm/human-connection/templates/services/service-backend.yaml new file mode 100644 index 000000000..af23e6910 --- /dev/null +++ b/deployment/helm/human-connection/templates/services/service-backend.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-backend + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: human-connection + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + ports: + - name: graphql + port: 4000 + targetPort: 4000 + selector: + human-connection.org/selector: deployment-backend diff --git a/deployment/helm/human-connection/templates/services/service-mailserver.yaml b/deployment/helm/human-connection/templates/services/service-mailserver.yaml new file mode 100644 index 000000000..685becb1d --- /dev/null +++ b/deployment/helm/human-connection/templates/services/service-mailserver.yaml @@ -0,0 +1,22 @@ +{{- if .Values.developmentMailserverDomain }} +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-mailserver + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: human-connection + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + ports: + - name: web + port: 80 + targetPort: 80 + - name: smtp + port: 25 + targetPort: 25 + selector: + human-connection.org/selector: deployment-mailserver +{{- end}} diff --git a/deployment/helm/human-connection/templates/services/service-maintenance.yaml b/deployment/helm/human-connection/templates/services/service-maintenance.yaml new file mode 100644 index 000000000..d641ca8ba --- /dev/null +++ b/deployment/helm/human-connection/templates/services/service-maintenance.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-maintenance + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: human-connection + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + ports: + - name: web + port: 80 + targetPort: 80 + selector: + human-connection.org/selector: deployment-maintenance diff --git a/deployment/helm/human-connection/templates/services/service-neo4j.yaml b/deployment/helm/human-connection/templates/services/service-neo4j.yaml new file mode 100644 index 000000000..f5c7d5b38 --- /dev/null +++ b/deployment/helm/human-connection/templates/services/service-neo4j.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-neo4j + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: human-connection + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + ports: + - name: bolt + port: 7687 + targetPort: 7687 + - name: web + port: 7474 + targetPort: 7474 + selector: + human-connection.org/selector: deployment-neo4j diff --git a/deployment/helm/human-connection/templates/services/service-webapp.yaml b/deployment/helm/human-connection/templates/services/service-webapp.yaml new file mode 100644 index 000000000..d762c2a55 --- /dev/null +++ b/deployment/helm/human-connection/templates/services/service-webapp.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-webapp + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: human-connection + app.kubernetes.io/version: {{ .Chart.AppVersion }} + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + ports: + - name: {{ .Release.Name }}-webapp + port: 3000 + protocol: TCP + targetPort: 3000 + selector: + human-connection.org/selector: deployment-webapp diff --git a/deployment/helm/human-connection/templates/volumes/pvc-neo4j-data.yaml b/deployment/helm/human-connection/templates/volumes/pvc-neo4j-data.yaml new file mode 100644 index 000000000..3f85d3ae8 --- /dev/null +++ b/deployment/helm/human-connection/templates/volumes/pvc-neo4j-data.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: neo4j-data-claim +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.neo4jStorage }} diff --git a/deployment/helm/human-connection/templates/volumes/pvc-uploads.yaml b/deployment/helm/human-connection/templates/volumes/pvc-uploads.yaml new file mode 100644 index 000000000..7eb81135b --- /dev/null +++ b/deployment/helm/human-connection/templates/volumes/pvc-uploads.yaml @@ -0,0 +1,16 @@ + +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: uploads-claim +spec: + dataSource: + name: uploads-snapshot + kind: VolumeSnapshot + apiGroup: snapshot.storage.k8s.io + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.uploadsStorage }} + diff --git a/deployment/helm/human-connection/values.yaml b/deployment/helm/human-connection/values.yaml new file mode 100644 index 000000000..f30704f89 --- /dev/null +++ b/deployment/helm/human-connection/values.yaml @@ -0,0 +1,53 @@ +# domain is the user-facing domain. +domain: develop.human-connection.org +# commit is the latest github commit deployed. +commit: 889a7cdd24dda04a139b2b77d626e984d6db6781 +# dbInitialization runs the database initializations in a post-install hook. +dbInitializion: "yarn prod:migrate init" +# dbMigrations runs the database migrations in a post-upgrade hook. +dbMigrations: "yarn prod:migrate up" +# bakendImage is the docker image for the backend deployment +backendImage: humanconnection/nitro-backend +# maintenanceImage is the docker image for the maintenance deployment +maintenanceImage: humanconnection/maintenance +# neo4jImage is the docker image for the neo4j deployment +neo4jImage: humanconnection/neo4j +# webappImage is the docker image for the webapp deployment +webappImage: humanconnection/nitro-web +# image configures pullPolicy related to the docker images +image: + # pullPolicy indicates when, if ever, pods pull a new image from docker hub. + pullPolicy: IfNotPresent +# letsencryptIssuer is used by cert-manager to set up certificates with the given provider. +letsencryptIssuer: "letsencrypt-prod" +# neo4jConfig changes any default neo4j config/adds it. +neo4jConfig: + # acceptLicenseAgreement is used to agree to the license agreement for neo4j's enterprise edition. + acceptLicenseAgreement: \"yes\" + # apocImportFileEnabled enables the import of files to neo4j using the plugin apoc + apocImportFileEnabled: \"true\" + # dbmsMemoryHeapInitialSize configures initial heap size. By default, it is calculated based on available system resources.(valid units are `k`, `K`, `m`, `M`, `g`, `G`) + dbmsMemoryHeapInitialSize: "500M" + # dbmsMemoryHeapMaxSize configures maximum heap size. By default it is calculated based on available system resources.(valid units are `k`, `K`, `m`, `M`, `g`, `G`) + dbmsMemoryHeapMaxSize: "500M" + # dbmsMemoryPagecacheSize configures the amount of memory to use for mapping the store files, in bytes (or 'k', 'm', and 'g') + dbmsMemoryPagecacheSize: "490M" +# neo4jResourceLimitsMemory configures the memory limits available. +neo4jResourceLimitsMemory: "2G" +# neo4jResourceLimitsMemory configures the memory available for requests. +neo4jResourceRequestsMemory: "1G" +# supportEmail is used for letsencrypt certs. +supportEmail: "devcom@human-connection.org" +# smtpHost is the host for the mailserver. +smtpHost: "mailserver.human-connection.org" +# smtpPort is the port to be used for the mailserver. +smtpPort: \"25\" +# jwtSecret is used to encode/decode a user's JWT for authentication +jwtSecret: "Yi8mJjdiNzhCRiZmdi9WZA==" +# privateKeyPassphrase is used for activity pub +privateKeyPassphrase: "YTdkc2Y3OHNhZGc4N2FkODdzZmFnc2FkZzc4" +# mapboxToken is used for the Mapbox API, geolocalization. +mapboxToken: "cGsuZXlKMUlqb2lhSFZ0WVc0dFkyOXVibVZqZEdsdmJpSXNJbUVpT2lKamFqbDBjbkJ1Ykdvd2VUVmxNM1Z3WjJsek5UTnVkM1p0SW4wLktaOEtLOWw3MG9talhiRWtrYkhHc1E=" +uploadsStorage: "25Gi" +neo4jStorage: "5Gi" +developmentMailserverDomain: nitro-mailserver.human-connection.org \ No newline at end of file diff --git a/deployment/human-connection/deployment-backend.yaml b/deployment/human-connection/deployment-backend.yaml index 0f75127e5..00aab9ffd 100644 --- a/deployment/human-connection/deployment-backend.yaml +++ b/deployment/human-connection/deployment-backend.yaml @@ -38,7 +38,7 @@ spec: name: human-connection image: humanconnection/nitro-backend:latest imagePullPolicy: Always - name: nitro-backend + name: backend ports: - containerPort: 4000 protocol: TCP diff --git a/deployment/volumes/neo4j-data.yaml b/deployment/volumes/neo4j-data.yaml index 1f453a684..c060750ec 100644 --- a/deployment/volumes/neo4j-data.yaml +++ b/deployment/volumes/neo4j-data.yaml @@ -9,4 +9,4 @@ - ReadWriteOnce resources: requests: - storage: 5Gi + storage: {{ .Values.neo4jStorage }} diff --git a/deployment/volumes/uploads.yaml b/deployment/volumes/uploads.yaml index 2bd64c9ee..c174bf2cb 100644 --- a/deployment/volumes/uploads.yaml +++ b/deployment/volumes/uploads.yaml @@ -9,4 +9,4 @@ - ReadWriteOnce resources: requests: - storage: 25Gi + storage: {{ .Values.uploadsStorage }} diff --git a/deployment/volumes/volume-snapshots/snapshot.yaml b/deployment/volumes/volume-snapshots/snapshot.yaml index 3c3487e14..10a22daf5 100644 --- a/deployment/volumes/volume-snapshots/snapshot.yaml +++ b/deployment/volumes/volume-snapshots/snapshot.yaml @@ -2,9 +2,9 @@ apiVersion: snapshot.storage.k8s.io/v1alpha1 kind: VolumeSnapshot metadata: - name: neo4j-data-snapshot + name: uploads-snapshot namespace: human-connection spec: source: - name: neo4j-data-claim + name: uploads-claim kind: PersistentVolumeClaim diff --git a/features/support/steps.js b/features/support/steps.js index 71f493834..67127fa1e 100644 --- a/features/support/steps.js +++ b/features/support/steps.js @@ -1,15 +1,13 @@ // features/support/steps.js import { Given, When, Then, After, AfterAll } from 'cucumber' -import Factory from '../../backend/src/factories' +import Factory, { cleanDatabase } from '../../backend/src/db/factories' import dotenv from 'dotenv' import expect from 'expect' const debug = require('debug')('ea:test:steps') -const factory = Factory() - After(async () => { - await factory.cleanDatabase() + await cleanDatabase() }) Given('our CLIENT_URI is {string}', function (string) { @@ -21,7 +19,7 @@ Given('our CLIENT_URI is {string}', function (string) { Given('we have the following users in our database:', function (dataTable) { return Promise.all(dataTable.hashes().map(({ slug, name }) => { - return factory.create('User', { + return Factory.build('user', { name, slug, }) diff --git a/package.json b/package.json index d13d467d1..79798391c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "human-connection", - "version": "0.3.0", + "version": "0.3.1", "description": "Fullstack and API tests with cypress and cucumber for Human Connection", "author": "Human Connection gGmbh", "license": "MIT", @@ -31,7 +31,7 @@ "@babel/register": "^7.8.3", "auto-changelog": "^1.16.2", "bcryptjs": "^2.4.3", - "codecov": "^3.6.2", + "codecov": "^3.6.5", "cross-env": "^6.0.3", "cucumber": "^6.0.5", "cypress": "^3.8.3", @@ -43,9 +43,11 @@ "expect": "^25.1.0", "faker": "Marak/faker.js#master", "graphql-request": "^1.8.2", + "jsonwebtoken": "^8.5.1", "neo4j-driver": "^4.0.1", "neode": "^0.3.7", "npm-run-all": "^4.1.5", + "rosie": "^2.0.1", "slug": "^2.1.1", "standard-version": "^7.1.0" }, diff --git a/scripts/github_release.sh b/scripts/github_release.sh deleted file mode 100755 index 93f50289d..000000000 --- a/scripts/github_release.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -ROOT_DIR=$(dirname "$0")/.. -RELEASE_DIR="${ROOT_DIR}/release" - -VERSION=$(jq -r ".version" $ROOT_DIR/package.json) - -# mkdir -p $RELEASE_DIR - -# The following command part produces 854M on my machine -# apps=(nitro-web nitro-backend neo4j maintenance-worker maintenance) -# for app in "${apps[@]}" -# do -# docker image save "humanconnection/${app}:latest" | gzip > "${RELEASE_DIR}/${app}.${VERSION}.tar.gz" -# done - -ghr -c "${VERSION}" "${VERSION}" diff --git a/scripts/translations/missing-keys.sh b/scripts/translations/missing-keys.sh new file mode 100755 index 000000000..aaeb1ac87 --- /dev/null +++ b/scripts/translations/missing-keys.sh @@ -0,0 +1,17 @@ +#! /usr/bin/env bash + +ROOT_DIR=$(dirname "$0")/../.. + +sorting="jq -f $ROOT_DIR/scripts/translations/sort_filter.jq" +english="$sorting $ROOT_DIR/webapp/locales/en.json" +german="$sorting $ROOT_DIR/webapp/locales/de.json" +listPaths="jq -c 'path(..)|[.[]|tostring]|join(\".\")'" +diffString="<( $english | $listPaths ) <( $german | $listPaths )" +if eval "diff -q $diffString"; +then + : # all good +else + eval "diff -y $diffString | grep '[|<>]'"; + printf "\nEnglish and German translation keys do not match, see diff above.\n" + exit 1 +fi diff --git a/scripts/translations/sort.sh b/scripts/translations/sort.sh new file mode 100755 index 000000000..35496f27e --- /dev/null +++ b/scripts/translations/sort.sh @@ -0,0 +1,24 @@ +#! /usr/bin/env bash + +ROOT_DIR=$(dirname "$0")/../.. +tmp=$(mktemp) +exit_code=0 + +for locale_file in $ROOT_DIR/webapp/locales/*.json +do + jq -f $(dirname "$0")/sort_filter.jq $locale_file > "$tmp" + if [ "$*" == "--fix" ] + then + mv "$tmp" $locale_file + else + if diff -q "$tmp" $locale_file > /dev/null ; + then + : # all good + else + exit_code=$? + echo "$(basename -- $locale_file) is not sorted by keys" + fi + fi +done + +exit $exit_code diff --git a/scripts/translations/sort_filter.jq b/scripts/translations/sort_filter.jq new file mode 100644 index 000000000..9d108f8f0 --- /dev/null +++ b/scripts/translations/sort_filter.jq @@ -0,0 +1,13 @@ +def walk(f): + . as $in + | if type == "object" then + reduce keys_unsorted[] as $key + ( {}; . + { ($key): ($in[$key] | walk(f)) } ) | f + elif type == "array" then map( walk(f) ) | f + else f + end; + +def keys_sort_by(f): + to_entries | sort_by(.key|f ) | from_entries; + +walk(if type == "object" then keys_sort_by(ascii_upcase) else . end) \ No newline at end of file diff --git a/webapp/.env.template b/webapp/.env.template index fdabcf003..b00e6855a 100644 --- a/webapp/.env.template +++ b/webapp/.env.template @@ -2,3 +2,5 @@ MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2l SENTRY_DSN_WEBAPP= COMMIT= PUBLIC_REGISTRATION=false +WEBSOCKETS_URI=ws://localhost:3000/api/graphql +GRAPHQL_URI=http://localhost:4000/ diff --git a/webapp/assets/_new/icons/svgs/microphone-slash.svg b/webapp/assets/_new/icons/svgs/microphone-slash.svg new file mode 100644 index 000000000..9802dbd5f --- /dev/null +++ b/webapp/assets/_new/icons/svgs/microphone-slash.svg @@ -0,0 +1,5 @@ + + +microphone-slash + + diff --git a/webapp/assets/_new/icons/svgs/microphone.svg b/webapp/assets/_new/icons/svgs/microphone.svg new file mode 100644 index 000000000..121342b70 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/microphone.svg @@ -0,0 +1,6 @@ + + + +microphone + + diff --git a/webapp/components/ContentMenu/ContentMenu.vue b/webapp/components/ContentMenu/ContentMenu.vue index 67f28817e..c69a2fcbc 100644 --- a/webapp/components/ContentMenu/ContentMenu.vue +++ b/webapp/components/ContentMenu/ContentMenu.vue @@ -161,7 +161,7 @@ export default { callback: () => { this.$emit('unmute', this.resource) }, - icon: 'eye', + icon: 'microphone', }) } else { routes.push({ @@ -169,10 +169,10 @@ export default { callback: () => { this.$emit('mute', this.resource) }, - icon: 'eye-slash', + icon: 'microphone-slash', }) } - if (this.resource.blocked) { + if (this.resource.isBlocked) { routes.push({ label: this.$t(`settings.blocked-users.unblock`), callback: () => { diff --git a/webapp/components/ContributionForm/ContributionForm.spec.js b/webapp/components/ContributionForm/ContributionForm.spec.js index b2ce7d530..23b3d8906 100644 --- a/webapp/components/ContributionForm/ContributionForm.spec.js +++ b/webapp/components/ContributionForm/ContributionForm.spec.js @@ -384,6 +384,15 @@ describe('ContributionForm.vue', () => { await wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) }) + + it('supports deleting a teaser image', async () => { + expectedParams.variables.image = null + propsData.contribution.image = '/uploads/someimage.png' + wrapper = Wrapper() + wrapper.find('.contribution-form .delete-image').trigger('click') + await wrapper.find('form').trigger('submit') + expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) + }) }) }) }) diff --git a/webapp/components/ContributionForm/ContributionForm.vue b/webapp/components/ContributionForm/ContributionForm.vue index 95fccffbc..d2bd33d71 100644 --- a/webapp/components/ContributionForm/ContributionForm.vue +++ b/webapp/components/ContributionForm/ContributionForm.vue @@ -9,6 +9,16 @@