diff --git a/backend/package.json b/backend/package.json index 9c8f2403c..5123e369b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -46,8 +46,8 @@ "activitystrea.ms": "~2.1.3", "apollo-cache-inmemory": "~1.6.3", "apollo-client": "~2.6.4", - "apollo-link-context": "~1.0.18", - "apollo-link-http": "~1.5.15", + "apollo-link-context": "~1.0.19", + "apollo-link-http": "~1.5.16", "apollo-server": "~2.9.3", "apollo-server-express": "^2.9.0", "babel-plugin-transform-runtime": "^6.23.0", @@ -55,7 +55,7 @@ "cheerio": "~1.0.0-rc.3", "cors": "~2.8.5", "cross-env": "~5.2.1", - "date-fns": "2.0.1", + "date-fns": "2.1.0", "debug": "~4.1.1", "dotenv": "~8.1.0", "express": "^4.17.1", @@ -65,7 +65,7 @@ "graphql-iso-date": "~3.6.1", "graphql-middleware": "~3.0.5", "graphql-middleware-sentry": "^3.2.0", - "graphql-shield": "~6.0.6", + "graphql-shield": "~6.1.0", "graphql-tag": "~2.10.1", "helmet": "~3.21.0", "jsonwebtoken": "~8.5.1", @@ -91,7 +91,7 @@ "minimatch": "^3.0.4", "neo4j-driver": "~1.7.6", "neo4j-graphql-js": "^2.7.2", - "neode": "^0.3.2", + "neode": "^0.3.3", "node-fetch": "~2.6.0", "nodemailer": "^6.3.0", "npm-run-all": "~4.1.5", @@ -104,12 +104,12 @@ "wait-on": "~3.3.0" }, "devDependencies": { - "@babel/cli": "~7.5.5", - "@babel/core": "~7.5.5", - "@babel/node": "~7.5.5", + "@babel/cli": "~7.6.0", + "@babel/core": "~7.6.0", + "@babel/node": "~7.6.1", "@babel/plugin-proposal-throw-expressions": "^7.2.0", - "@babel/preset-env": "~7.5.5", - "@babel/register": "~7.5.5", + "@babel/preset-env": "~7.6.0", + "@babel/register": "~7.6.0", "apollo-server-testing": "~2.9.3", "babel-core": "~7.0.0-0", "babel-eslint": "~10.0.3", diff --git a/backend/src/middleware/email/emailMiddleware.js b/backend/src/middleware/email/emailMiddleware.js index 0b7cfd058..809ca4072 100644 --- a/backend/src/middleware/email/emailMiddleware.js +++ b/backend/src/middleware/email/emailMiddleware.js @@ -3,6 +3,22 @@ import nodemailer from 'nodemailer' import { resetPasswordMail, wrongAccountMail } from './templates/passwordReset' import { signupTemplate } from './templates/signup' +let sendMail +if (CONFIG.SMTP_HOST && CONFIG.SMTP_PORT) { + sendMail = async templateArgs => { + await transporter().sendMail({ + from: '"Human Connection" ', + ...templateArgs, + }) + } +} else { + sendMail = () => {} + if (process.env.NODE_ENV !== 'test') { + // eslint-disable-next-line no-console + console.log('Warning: Email middleware will not try to send mails.') + } +} + const transporter = () => { const configs = { host: CONFIG.SMTP_HOST, @@ -17,41 +33,24 @@ const transporter = () => { return nodemailer.createTransport(configs) } -const returnResponse = async (resolve, root, args, context, resolveInfo) => { - const { response } = await resolve(root, args, context, resolveInfo) - delete response.nonce - return response -} - const sendSignupMail = async (resolve, root, args, context, resolveInfo) => { - const { email } = args - const { response, nonce } = await resolve(root, args, context, resolveInfo) + const response = await resolve(root, args, context, resolveInfo) + const { email, nonce } = response + await sendMail(signupTemplate({ email, nonce })) delete response.nonce - await transporter().sendMail(signupTemplate({ email, nonce })) return response } -export default function({ isEnabled }) { - if (!isEnabled) - return { - Mutation: { - requestPasswordReset: returnResponse, - Signup: returnResponse, - SignupByInvitation: returnResponse, - }, - } - - return { - Mutation: { - requestPasswordReset: async (resolve, root, args, context, resolveInfo) => { - const { email } = args - const { response, user, code, name } = await resolve(root, args, context, resolveInfo) - const mailTemplate = user ? resetPasswordMail : wrongAccountMail - await transporter().sendMail(mailTemplate({ email, code, name })) - return response - }, - Signup: sendSignupMail, - SignupByInvitation: sendSignupMail, +export default { + Mutation: { + requestPasswordReset: async (resolve, root, args, context, resolveInfo) => { + const { email } = args + const { email: emailFound, nonce, name } = await resolve(root, args, context, resolveInfo) + const mailTemplate = emailFound ? resetPasswordMail : wrongAccountMail + await sendMail(mailTemplate({ email, nonce, name })) + return true }, - } + Signup: sendSignupMail, + SignupByInvitation: sendSignupMail, + }, } diff --git a/backend/src/middleware/email/templates/passwordReset.js b/backend/src/middleware/email/templates/passwordReset.js index 8508adccc..c977594b5 100644 --- a/backend/src/middleware/email/templates/passwordReset.js +++ b/backend/src/middleware/email/templates/passwordReset.js @@ -1,17 +1,15 @@ import CONFIG from '../../../config' -export const from = '"Human Connection" ' - export const resetPasswordMail = options => { const { name, email, - code, + nonce, subject = 'Use this link to reset your password. The link is only valid for 24 hours.', supportUrl = 'https://human-connection.org/en/contact/', } = options const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI) - actionUrl.searchParams.set('code', code) + actionUrl.searchParams.set('nonce', nonce) actionUrl.searchParams.set('email', email) return { @@ -37,7 +35,7 @@ The Human Connection Team If you're having trouble with the link above, you can manually copy and paste the following code into your browser window: -${code} +${nonce} Human Connection gemeinnützige GmbH Bahnhofstr. 11 diff --git a/backend/src/middleware/email/templates/signup.js b/backend/src/middleware/email/templates/signup.js index 7751f0e67..54cc51be2 100644 --- a/backend/src/middleware/email/templates/signup.js +++ b/backend/src/middleware/email/templates/signup.js @@ -1,12 +1,10 @@ import CONFIG from '../../../config' -export const from = '"Human Connection" ' - export const signupTemplate = options => { const { email, nonce, - subject = 'Signup link', + subject = 'Welcome to Human Connection! Here is your signup link.', supportUrl = 'https://human-connection.org/en/contact/', } = options const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI) @@ -17,12 +15,33 @@ export const signupTemplate = options => { to: email, subject, text: ` +Willkommen bei Human Connection! Klick auf diesen Link, um den +Registrierungsprozess abzuschließen und um ein Benutzerkonto zu erstellen! + +${actionUrl} + +Alternativ kannst du diesen Code auch kopieren und im Browserfenster einfügen: + +${nonce} + +Bitte ignoriere diese Mail, falls du dich nicht bei Human Connection angemeldet +hast. Bei Fragen kontaktiere gerne unseren Support: + +${supportUrl} + +Danke, +Das Human Connection Team + + +English Version +=============== + Welcome to Human Connection! Use this link to complete the registration process and create a user account: ${actionUrl} -You can also copy+paste this verification code in your browser window: +You can also copy+paste this verification nonce in your browser window: ${nonce} diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 1dd630ebc..0c68ef4d9 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -33,9 +33,7 @@ export default schema => { user, includedFields, orderBy, - email: email({ - isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT, - }), + email, } let order = [ diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index d11757bc7..9e5456b01 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -1,62 +1,81 @@ -import { GraphQLClient } from 'graphql-request' import Factory from '../seed/factories' -import { host, login, gql } from '../jest/helpers' -import { neode } from '../bootstrap/neo4j' +import { gql } from '../jest/helpers' +import { neode as getNeode, getDriver } from '../bootstrap/neo4j' +import createServer from '../server' +import { createTestClient } from 'apollo-server-testing' -let authenticatedClient -let headers const factory = Factory() -const instance = neode() -const categoryIds = ['cat9'] -const createPostMutation = gql` - mutation($title: String!, $content: String!, $categoryIds: [ID]!, $slug: String) { - CreatePost(title: $title, content: $content, categoryIds: $categoryIds, slug: $slug) { - slug - } - } -` -let createPostVariables = { - title: 'I am a brand new post', - content: 'Some content', - categoryIds, -} + +let mutate +let authenticatedUser +let variables + +const driver = getDriver() +const neode = getNeode() + +beforeAll(() => { + const { server } = createServer({ + context: () => { + return { + driver, + neode, + user: authenticatedUser, + } + }, + }) + mutate = createTestClient(server).mutate +}) + beforeEach(async () => { - const adminParams = { role: 'admin', email: 'admin@example.org', password: '1234' } - await factory.create('User', adminParams) + variables = {} + const admin = await factory.create('User', { role: 'admin' }) await factory.create('User', { email: 'someone@example.org', password: '1234', }) - await instance.create('Category', { + await factory.create('Category', { id: 'cat9', name: 'Democracy & Politics', icon: 'university', }) - // we need to be an admin, otherwise we're not authorized to create a user - headers = await login(adminParams) - authenticatedClient = new GraphQLClient(host, { headers }) + authenticatedUser = await admin.toJson() }) afterEach(async () => { await factory.cleanDatabase() }) -describe('slugify', () => { +describe('slugifyMiddleware', () => { describe('CreatePost', () => { + const categoryIds = ['cat9'] + const createPostMutation = gql` + mutation($title: String!, $content: String!, $categoryIds: [ID]!, $slug: String) { + CreatePost(title: $title, content: $content, categoryIds: $categoryIds, slug: $slug) { + slug + } + } + ` + + beforeEach(() => { + variables = { + ...variables, + title: 'I am a brand new post', + content: 'Some content', + categoryIds, + } + }) + it('generates a slug based on title', async () => { - const response = await authenticatedClient.request(createPostMutation, createPostVariables) - expect(response).toEqual({ - CreatePost: { slug: 'i-am-a-brand-new-post' }, + await expect(mutate({ mutation: createPostMutation, variables })).resolves.toMatchObject({ + data: { + CreatePost: { slug: 'i-am-a-brand-new-post' }, + }, }) }) describe('if slug exists', () => { beforeEach(async () => { - const asSomeoneElse = await Factory().authenticateAs({ - email: 'someone@example.org', - password: '1234', - }) - await asSomeoneElse.create('Post', { + await factory.create('Post', { title: 'Pre-existing post', slug: 'pre-existing-post', content: 'as Someone else content', @@ -65,72 +84,110 @@ describe('slugify', () => { }) it('chooses another slug', async () => { - createPostVariables = { title: 'Pre-existing post', content: 'Some content', categoryIds } - const response = await authenticatedClient.request(createPostMutation, createPostVariables) - expect(response).toEqual({ - CreatePost: { slug: 'pre-existing-post-1' }, + variables = { + ...variables, + title: 'Pre-existing post', + content: 'Some content', + categoryIds, + } + await expect(mutate({ mutation: createPostMutation, variables })).resolves.toMatchObject({ + data: { CreatePost: { slug: 'pre-existing-post-1' } }, }) }) describe('but if the client specifies a slug', () => { it('rejects CreatePost', async () => { - createPostVariables = { + variables = { + ...variables, title: 'Pre-existing post', content: 'Some content', slug: 'pre-existing-post', categoryIds, } - await expect( - authenticatedClient.request(createPostMutation, createPostVariables), - ).rejects.toThrow('already exists') + await expect(mutate({ mutation: createPostMutation, variables })).resolves.toMatchObject({ + errors: [{ message: 'Post with this slug already exists!' }], + }) }) }) }) }) describe('SignupVerification', () => { - const mutation = `mutation($password: String!, $email: String!, $name: String!, $slug: String, $nonce: String!, $termsAndConditionsAgreedVersion: String!) { - SignupVerification(email: $email, password: $password, name: $name, slug: $slug, nonce: $nonce, termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion) { - slug + const mutation = gql` + mutation( + $password: String! + $email: String! + $name: String! + $slug: String + $nonce: String! + $termsAndConditionsAgreedVersion: String! + ) { + SignupVerification( + email: $email + password: $password + name: $name + slug: $slug + nonce: $nonce + termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion + ) { + slug + } } - } ` - const action = async variables => { - // required for SignupVerification - await instance.create('EmailAddress', { email: '123@example.org', nonce: '123456' }) - - const defaultVariables = { + beforeEach(() => { + variables = { + ...variables, + name: 'I am a user', nonce: '123456', password: 'yo', email: '123@example.org', termsAndConditionsAgreedVersion: '0.0.1', } - return authenticatedClient.request(mutation, { ...defaultVariables, ...variables }) - } - - it('generates a slug based on name', async () => { - await expect(action({ name: 'I am a user' })).resolves.toEqual({ - SignupVerification: { slug: 'i-am-a-user' }, - }) }) - describe('if slug exists', () => { + describe('given a user has signed up with their email address', () => { beforeEach(async () => { - await factory.create('User', { name: 'pre-existing user', slug: 'pre-existing-user' }) - }) - - it('chooses another slug', async () => { - await expect(action({ name: 'pre-existing-user' })).resolves.toEqual({ - SignupVerification: { slug: 'pre-existing-user-1' }, + await factory.create('EmailAddress', { + email: '123@example.org', + nonce: '123456', + verifiedAt: null, }) }) - describe('but if the client specifies a slug', () => { - it('rejects SignupVerification', async () => { - await expect( - action({ name: 'Pre-existing user', slug: 'pre-existing-user' }), - ).rejects.toThrow('already exists') + it('generates a slug based on name', async () => { + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { SignupVerification: { slug: 'i-am-a-user' } }, + }) + }) + + describe('if slug exists', () => { + beforeEach(async () => { + await factory.create('User', { name: 'I am a user', slug: 'i-am-a-user' }) + }) + + it('chooses another slug', async () => { + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { + SignupVerification: { slug: 'i-am-a-user-1' }, + }, + }) + }) + + describe('but if the client specifies a slug', () => { + beforeEach(() => { + variables = { ...variables, slug: 'i-am-a-user' } + }) + + it('rejects SignupVerification', async () => { + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + errors: [ + { + message: 'User with this slug already exists!', + }, + ], + }) + }) }) }) }) diff --git a/backend/src/schema/resolvers/passwordReset.js b/backend/src/schema/resolvers/passwordReset.js index d2012c0fd..3c5f4636c 100644 --- a/backend/src/schema/resolvers/passwordReset.js +++ b/backend/src/schema/resolvers/passwordReset.js @@ -2,39 +2,47 @@ import uuid from 'uuid/v4' import bcrypt from 'bcryptjs' export async function createPasswordReset(options) { - const { driver, code, email, issuedAt = new Date() } = options + const { driver, nonce, email, issuedAt = new Date() } = options const session = driver.session() - const cypher = ` + let response = {} + try { + const cypher = ` MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email}) - CREATE(pr:PasswordReset {code: $code, issuedAt: datetime($issuedAt), usedAt: NULL}) + CREATE(pr:PasswordReset {nonce: $nonce, issuedAt: datetime($issuedAt), usedAt: NULL}) MERGE (u)-[:REQUESTED]->(pr) - RETURN u + RETURN e, pr, u ` - const transactionRes = await session.run(cypher, { - issuedAt: issuedAt.toISOString(), - code, - email, - }) - const users = transactionRes.records.map(record => record.get('u')) - session.close() - return users + const transactionRes = await session.run(cypher, { + issuedAt: issuedAt.toISOString(), + nonce, + email, + }) + const records = transactionRes.records.map(record => { + const { email } = record.get('e').properties + const { nonce } = record.get('pr').properties + const { name } = record.get('u').properties + return { email, nonce, name } + }) + response = records[0] || {} + } finally { + session.close() + } + return response } export default { Mutation: { - requestPasswordReset: async (_, { email }, { driver }) => { - const code = uuid().substring(0, 6) - const [user] = await createPasswordReset({ driver, code, email }) - const name = (user && user.name) || '' - return { user, code, name, response: true } + requestPasswordReset: async (_parent, { email }, { driver }) => { + const nonce = uuid().substring(0, 6) + return createPasswordReset({ driver, nonce, email }) }, - resetPassword: async (_, { email, code, newPassword }, { driver }) => { + resetPassword: async (_parent, { email, nonce, newPassword }, { driver }) => { const session = driver.session() const stillValid = new Date() stillValid.setDate(stillValid.getDate() - 1) const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10) const cypher = ` - MATCH (pr:PasswordReset {code: $code}) + MATCH (pr:PasswordReset {nonce: $nonce}) MATCH (e:EmailAddress {email: $email})<-[:PRIMARY_EMAIL]-(u:User)-[:REQUESTED]->(pr) WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL SET pr.usedAt = datetime() @@ -44,13 +52,13 @@ export default { const transactionRes = await session.run(cypher, { stillValid, email, - code, + nonce, encryptedNewPassword, }) const [reset] = transactionRes.records.map(record => record.get('pr')) - const result = !!(reset && reset.properties.usedAt) + const response = !!(reset && reset.properties.usedAt) session.close() - return result + return response }, }, } diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index b54b25a80..fabee1c7e 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -1,12 +1,17 @@ -import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host } from '../../jest/helpers' -import { getDriver } from '../../bootstrap/neo4j' +import { gql } from '../../jest/helpers' +import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { createPasswordReset } from './passwordReset' +import createServer from '../../server' +import { createTestClient } from 'apollo-server-testing' -const factory = Factory() -let client +const neode = getNeode() const driver = getDriver() +const factory = Factory() + +let mutate +let authenticatedUser +let variables const getAllPasswordResets = async () => { const session = driver.session() @@ -16,120 +21,167 @@ const getAllPasswordResets = async () => { return resets } +beforeEach(() => { + variables = {} +}) + +beforeAll(() => { + const { server } = createServer({ + context: () => { + return { + driver, + neode, + user: authenticatedUser, + } + }, + }) + mutate = createTestClient(server).mutate +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + describe('passwordReset', () => { - beforeEach(async () => { - client = new GraphQLClient(host) - await factory.create('User', { - email: 'user@example.org', - role: 'user', - password: '1234', + describe('given a user', () => { + beforeEach(async () => { + await factory.create('User', { + email: 'user@example.org', + }) }) - }) - afterEach(async () => { - await factory.cleanDatabase() - }) + describe('requestPasswordReset', () => { + const mutation = gql` + mutation($email: String!) { + requestPasswordReset(email: $email) + } + ` - describe('requestPasswordReset', () => { - const mutation = `mutation($email: String!) { requestPasswordReset(email: $email) }` + describe('with invalid email', () => { + beforeEach(() => { + variables = { ...variables, email: 'non-existent@example.org' } + }) - describe('with invalid email', () => { - const variables = { email: 'non-existent@example.org' } + it('resolves anyways', async () => { + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { requestPasswordReset: true }, + }) + }) - it('resolves anyways', async () => { - await expect(client.request(mutation, variables)).resolves.toEqual({ - requestPasswordReset: true, + it('creates no node', async () => { + await mutate({ mutation, variables }) + const resets = await getAllPasswordResets() + expect(resets).toHaveLength(0) }) }) - it('creates no node', async () => { - await client.request(mutation, variables) - const resets = await getAllPasswordResets() - expect(resets).toHaveLength(0) - }) - }) - - describe('with a valid email', () => { - const variables = { email: 'user@example.org' } - - it('resolves', async () => { - await expect(client.request(mutation, variables)).resolves.toEqual({ - requestPasswordReset: true, + describe('with a valid email', () => { + beforeEach(() => { + variables = { ...variables, email: 'user@example.org' } }) - }) - it('creates node with label `PasswordReset`', async () => { - await client.request(mutation, variables) - const resets = await getAllPasswordResets() - expect(resets).toHaveLength(1) - }) + it('resolves', async () => { + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { requestPasswordReset: true }, + }) + }) - it('creates a reset code', async () => { - await client.request(mutation, variables) - const resets = await getAllPasswordResets() - const [reset] = resets - const { code } = reset.properties - expect(code).toHaveLength(6) + it('creates node with label `PasswordReset`', async () => { + let resets = await getAllPasswordResets() + expect(resets).toHaveLength(0) + await mutate({ mutation, variables }) + resets = await getAllPasswordResets() + expect(resets).toHaveLength(1) + }) + + it('creates a reset nonce', async () => { + await mutate({ mutation, variables }) + const resets = await getAllPasswordResets() + const [reset] = resets + const { nonce } = reset.properties + expect(nonce).toHaveLength(6) + }) }) }) }) +}) - describe('resetPassword', () => { - const setup = async (options = {}) => { - const { email = 'user@example.org', issuedAt = new Date(), code = 'abcdef' } = options +describe('resetPassword', () => { + const setup = async (options = {}) => { + const { email = 'user@example.org', issuedAt = new Date(), nonce = 'abcdef' } = options - const session = driver.session() - await createPasswordReset({ driver, email, issuedAt, code }) - session.close() + const session = driver.session() + await createPasswordReset({ driver, email, issuedAt, nonce }) + session.close() + } + + const mutation = gql` + mutation($nonce: String!, $email: String!, $newPassword: String!) { + resetPassword(nonce: $nonce, email: $email, newPassword: $newPassword) } + ` + beforeEach(() => { + variables = { ...variables, newPassword: 'supersecret' } + }) - const mutation = `mutation($code: String!, $email: String!, $newPassword: String!) { resetPassword(code: $code, email: $email, newPassword: $newPassword) }` - const email = 'user@example.org' - const code = 'abcdef' - const newPassword = 'supersecret' - let variables + describe('given a user', () => { + beforeEach(async () => { + await factory.create('User', { + email: 'user@example.org', + role: 'user', + password: '1234', + }) + }) describe('invalid email', () => { it('resolves to false', async () => { await setup() - variables = { newPassword, email: 'non-existent@example.org', code } - await expect(client.request(mutation, variables)).resolves.toEqual({ resetPassword: false }) + variables = { ...variables, email: 'non-existent@example.org', nonce: 'abcdef' } + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { resetPassword: false }, + }) }) }) describe('valid email', () => { - describe('but invalid code', () => { + beforeEach(() => { + variables = { ...variables, email: 'user@example.org' } + }) + + describe('but invalid nonce', () => { + beforeEach(() => { + variables = { ...variables, nonce: 'slkdjf' } + }) + it('resolves to false', async () => { await setup() - variables = { newPassword, email, code: 'slkdjf' } - await expect(client.request(mutation, variables)).resolves.toEqual({ - resetPassword: false, + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { resetPassword: false }, }) }) }) - describe('and valid code', () => { + describe('and valid nonce', () => { beforeEach(() => { variables = { - newPassword, - email: 'user@example.org', - code: 'abcdef', + ...variables, + nonce: 'abcdef', } }) - describe('and code not expired', () => { + describe('and nonce not expired', () => { beforeEach(async () => { await setup() }) it('resolves to true', async () => { - await expect(client.request(mutation, variables)).resolves.toEqual({ - resetPassword: true, + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { resetPassword: true }, }) }) it('updates PasswordReset `usedAt` property', async () => { - await client.request(mutation, variables) + await mutate({ mutation, variables }) const requests = await getAllPasswordResets() const [request] = requests const { usedAt } = request.properties @@ -137,23 +189,20 @@ describe('passwordReset', () => { }) it('updates password of the user', async () => { - await client.request(mutation, variables) - const checkLoginMutation = ` - mutation($email: String!, $password: String!) { - login(email: $email, password: $password) - } + await mutate({ mutation, variables }) + const checkLoginMutation = gql` + mutation($email: String!, $password: String!) { + login(email: $email, password: $password) + } ` - const expected = expect.objectContaining({ login: expect.any(String) }) + variables = { ...variables, email: 'user@example.org', password: 'supersecret' } await expect( - client.request(checkLoginMutation, { - email: 'user@example.org', - password: 'supersecret', - }), - ).resolves.toEqual(expected) + mutate({ mutation: checkLoginMutation, variables }), + ).resolves.toMatchObject({ data: { login: expect.any(String) } }) }) }) - describe('but expired code', () => { + describe('but expired nonce', () => { beforeEach(async () => { const issuedAt = new Date() issuedAt.setDate(issuedAt.getDate() - 1) @@ -161,13 +210,13 @@ describe('passwordReset', () => { }) it('resolves to false', async () => { - await expect(client.request(mutation, variables)).resolves.toEqual({ - resetPassword: false, + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { resetPassword: false }, }) }) it('does not update PasswordReset `usedAt` property', async () => { - await client.request(mutation, variables) + await mutate({ mutation, variables }) const requests = await getAllPasswordResets() const [request] = requests const { usedAt } = request.properties diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index f1dad0f70..86dc78d62 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -3,6 +3,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js' import fileUpload from './fileUpload' import { getBlockedUsers, getBlockedByUsers } from './users.js' import { mergeWith, isArray } from 'lodash' +import { UserInputError } from 'apollo-server' import Resolver from './helpers/Resolver' const filterForBlockedUsers = async (params, context) => { @@ -78,6 +79,7 @@ export default { delete params.categoryIds params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) params.id = params.id || uuid() + let post const createPostCypher = `CREATE (post:Post {params}) WITH post @@ -92,15 +94,21 @@ export default { const createPostVariables = { userId: context.user.id, categoryIds, params } const session = context.driver.session() - const transactionRes = await session.run(createPostCypher, createPostVariables) + try { + const transactionRes = await session.run(createPostCypher, createPostVariables) + const posts = transactionRes.records.map(record => { + return record.get('post').properties + }) + post = posts[0] + } catch (e) { + if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') + throw new UserInputError('Post with this slug already exists!') + throw new Error(e) + } finally { + session.close() + } - const [post] = transactionRes.records.map(record => { - return record.get('post') - }) - - session.close() - - return post.properties + return post }, UpdatePost: async (object, params, context, resolveInfo) => { const { categoryIds } = params diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 7c5e88d69..af2d8089c 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -91,7 +91,7 @@ afterEach(async () => { }) describe('Post', () => { - const postQuery = gql` + const postQueryFilteredByCategories = gql` query Post($filter: _PostFilter) { Post(filter: $filter) { id @@ -102,13 +102,28 @@ describe('Post', () => { } ` + const postQueryFilteredByEmotions = gql` + query Post($filter: _PostFilter) { + Post(filter: $filter) { + id + emotions { + emotion + } + } + } + ` + describe('can be filtered', () => { - it('by categories', async () => { - await Promise.all([ + let post31, post32 + beforeEach(async () => { + ;[post31, post32] = await Promise.all([ factory.create('Post', { id: 'p31', categoryIds: ['cat4'] }), factory.create('Post', { id: 'p32', categoryIds: ['cat15'] }), factory.create('Post', { id: 'p33', categoryIds: ['cat9'] }), ]) + }) + + it('by categories', async () => { const expected = { data: { Post: [ @@ -120,7 +135,50 @@ describe('Post', () => { }, } variables = { ...variables, filter: { categories_some: { id_in: ['cat9'] } } } - await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected) + await expect( + query({ query: postQueryFilteredByCategories, variables }), + ).resolves.toMatchObject(expected) + }) + + it('by emotions', async () => { + const expected = { + data: { + Post: [ + { + id: 'p31', + emotions: [{ emotion: 'happy' }], + }, + ], + }, + } + await user.relateTo(post31, 'emoted', { emotion: 'happy' }) + variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy'] } } } + await expect(query({ query: postQueryFilteredByEmotions, variables })).resolves.toMatchObject( + expected, + ) + }) + + it('supports filtering by multiple emotions', async () => { + const expected = [ + { + id: 'p31', + emotions: [{ emotion: 'happy' }], + }, + { + id: 'p32', + emotions: [{ emotion: 'cry' }], + }, + ] + await user.relateTo(post31, 'emoted', { emotion: 'happy' }) + await user.relateTo(post32, 'emoted', { emotion: 'cry' }) + variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy', 'cry'] } } } + await expect(query({ query: postQueryFilteredByEmotions, variables })).resolves.toMatchObject( + { + data: { + Post: expect.arrayContaining(expected), + }, + }, + ) }) }) }) diff --git a/backend/src/schema/resolvers/registration.js b/backend/src/schema/resolvers/registration.js index 423ce7580..f96767006 100644 --- a/backend/src/schema/resolvers/registration.js +++ b/backend/src/schema/resolvers/registration.js @@ -1,4 +1,4 @@ -import { ForbiddenError, UserInputError } from 'apollo-server' +import { UserInputError } from 'apollo-server' import uuid from 'uuid/v4' import { neode } from '../../bootstrap/neo4j' import fileUpload from './fileUpload' @@ -18,7 +18,7 @@ const checkEmailDoesNotExist = async ({ email }) => { export default { Mutation: { - CreateInvitationCode: async (parent, args, context, resolveInfo) => { + CreateInvitationCode: async (_parent, args, context, _resolveInfo) => { args.token = uuid().substring(0, 6) const { user: { id: userId }, @@ -37,18 +37,18 @@ export default { } return response }, - Signup: async (parent, args, context, resolveInfo) => { + Signup: async (_parent, args, _context, _resolveInfo) => { const nonce = uuid().substring(0, 6) args.nonce = nonce await checkEmailDoesNotExist({ email: args.email }) try { const emailAddress = await instance.create('EmailAddress', args) - return { response: emailAddress.toJson(), nonce } + return emailAddress.toJson() } catch (e) { throw new UserInputError(e.message) } }, - SignupByInvitation: async (parent, args, context, resolveInfo) => { + SignupByInvitation: async (_parent, args, _context, _resolveInfo) => { const { token } = args const nonce = uuid().substring(0, 6) args.nonce = nonce @@ -71,16 +71,16 @@ export default { throw new UserInputError('Invitation code already used or does not exist.') const emailAddress = await instance.create('EmailAddress', args) await validInvitationCode.relateTo(emailAddress, 'activated') - return { response: emailAddress.toJson(), nonce } + return emailAddress.toJson() } catch (e) { throw new UserInputError(e) } }, - SignupVerification: async (object, args, context, resolveInfo) => { + SignupVerification: async (_parent, args, _context, _resolveInfo) => { const { termsAndConditionsAgreedVersion } = args const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g) if (!regEx.test(termsAndConditionsAgreedVersion)) { - throw new ForbiddenError('Invalid version format!') + throw new UserInputError('Invalid version format!') } let { nonce, email } = args @@ -106,6 +106,8 @@ export default { ]) return user.toJson() } catch (e) { + if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') + throw new UserInputError('User with this slug already exists!') throw new UserInputError(e.message) } }, diff --git a/backend/src/schema/resolvers/registration.spec.js b/backend/src/schema/resolvers/registration.spec.js index d9c05fde6..ae8cc16f6 100644 --- a/backend/src/schema/resolvers/registration.spec.js +++ b/backend/src/schema/resolvers/registration.spec.js @@ -327,6 +327,7 @@ describe('SignupVerification', () => { $password: String! $email: String! $nonce: String! + $about: String $termsAndConditionsAgreedVersion: String! ) { SignupVerification( @@ -334,6 +335,7 @@ describe('SignupVerification', () => { password: $password email: $email nonce: $nonce + about: $about termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion ) { id @@ -423,6 +425,15 @@ describe('SignupVerification', () => { expect(emails).toHaveLength(1) }) + it('sets `about` attribute of User', async () => { + variables = { ...variables, about: 'Find this description in the user profile' } + await mutate({ mutation, variables }) + const user = await neode.first('User', { name: 'John Doe' }) + await expect(user.toJson()).resolves.toMatchObject({ + about: 'Find this description in the user profile', + }) + }) + it('marks the EmailAddress as primary', async () => { const cypher = ` MATCH(email:EmailAddress)<-[:PRIMARY_EMAIL]-(u:User {name: {name}}) diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index eb78cabfe..c641763f0 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -23,7 +23,7 @@ type Mutation { login(email: String!, password: String!): String! changePassword(oldPassword: String!, newPassword: String!): String! requestPasswordReset(email: String!): Boolean! - resetPassword(email: String!, code: String!, newPassword: String!): Boolean! + resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean! report(id: ID!, description: String): Report disable(id: ID!): ID enable(id: ID!): ID diff --git a/backend/src/schema/types/type/EMOTED.gql b/backend/src/schema/types/type/EMOTED.gql index cb8d37d62..ee1576517 100644 --- a/backend/src/schema/types/type/EMOTED.gql +++ b/backend/src/schema/types/type/EMOTED.gql @@ -3,8 +3,8 @@ type EMOTED @relation(name: "EMOTED") { to: Post emotion: Emotion - #createdAt: DateTime - #updatedAt: DateTime + # createdAt: DateTime + # updatedAt: DateTime createdAt: String updatedAt: String } @@ -15,6 +15,12 @@ input _EMOTEDInput { updatedAt: String } +input _PostEMOTEDFilter { + emotion_in: [Emotion] + createdAt: String + updatedAt: String +} + type Mutation { AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED diff --git a/backend/src/seed/factories/emailAddresses.js b/backend/src/seed/factories/emailAddresses.js new file mode 100644 index 000000000..0212dca53 --- /dev/null +++ b/backend/src/seed/factories/emailAddresses.js @@ -0,0 +1,17 @@ +import faker from 'faker' + +export default function create() { + return { + factory: async ({ args, neodeInstance }) => { + const defaults = { + email: faker.internet.email(), + verifiedAt: new Date().toISOString(), + } + args = { + ...defaults, + ...args, + } + return neodeInstance.create('EmailAddress', args) + }, + } +} diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index 913e6efa1..c0a684715 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -8,6 +8,7 @@ import createCategory from './categories.js' import createTag from './tags.js' import createSocialMedia from './socialMedia.js' import createLocation from './locations.js' +import createEmailAddress from './emailAddresses.js' export const seedServerHost = 'http://127.0.0.1:4001' @@ -30,6 +31,7 @@ const factories = { Tag: createTag, SocialMedia: createSocialMedia, Location: createLocation, + EmailAddress: createEmailAddress, } export const cleanDatabase = async (options = {}) => { diff --git a/backend/src/seed/factories/posts.js b/backend/src/seed/factories/posts.js index e81251c53..3058204a1 100644 --- a/backend/src/seed/factories/posts.js +++ b/backend/src/seed/factories/posts.js @@ -30,7 +30,7 @@ export default function create() { let { categories, categoryIds } = args delete args.categories delete args.categoryIds - if (categories && categoryIds) throw new Error('You provided both category and 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')])) diff --git a/backend/src/seed/factories/users.js b/backend/src/seed/factories/users.js index 0ed1d4bc5..5cb7f8842 100644 --- a/backend/src/seed/factories/users.js +++ b/backend/src/seed/factories/users.js @@ -5,7 +5,7 @@ import slugify from 'slug' export default function create() { return { - factory: async ({ args, neodeInstance }) => { + factory: async ({ args, neodeInstance, factoryInstance }) => { const defaults = { id: uuid(), name: faker.name.findName(), @@ -24,7 +24,7 @@ export default function create() { } args = await encryptPassword(args) const user = await neodeInstance.create('User', args) - const email = await neodeInstance.create('EmailAddress', { email: args.email }) + const email = await factoryInstance.create('EmailAddress', { email: args.email }) await user.relateTo(email, 'primaryEmail') await email.relateTo(user, 'belongsTo') return user diff --git a/backend/yarn.lock b/backend/yarn.lock index b6f9d363a..b77a99253 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -14,10 +14,10 @@ resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.24.tgz#3ce939cb127fb8aaa3ffc1e90dff9b8af9f2e3dc" integrity sha512-8GqG48m1XqyXh4mIZrtB5xOhUwSsh1WsrrsaZQOEYYql3YN9DEu9OOSg0ILzXHZo/h2Q74777YE4YzlArQzQEQ== -"@babel/cli@~7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.5.5.tgz#bdb6d9169e93e241a08f5f7b0265195bf38ef5ec" - integrity sha512-UHI+7pHv/tk9g6WXQKYz+kmXTI77YtuY3vqC59KIqcoWEjsJJSG6rAxKaLsgj3LDyadsPrCB929gVOKM6Hui0w== +"@babel/cli@~7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.6.0.tgz#1470a04394eaf37862989ea4912adf440fa6ff8d" + integrity sha512-1CTDyGUjQqW3Mz4gfKZ04KGOckyyaNmKneAMlABPS+ZyuxWv3FrVEVz7Ag08kNIztVx8VaJ8YgvYLSNlMKAT5Q== dependencies: commander "^2.8.1" convert-source-map "^1.1.0" @@ -29,7 +29,7 @@ slash "^2.0.0" source-map "^0.5.0" optionalDependencies: - chokidar "^2.0.4" + chokidar "^2.1.8" "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5": version "7.5.5" @@ -38,18 +38,18 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@^7.1.0", "@babel/core@~7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30" - integrity sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg== +"@babel/core@^7.1.0", "@babel/core@~7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.0.tgz#9b00f73554edd67bebc86df8303ef678be3d7b48" + integrity sha512-FuRhDRtsd6IptKpHXAa+4WPZYY2ZzgowkbLBecEDDSje1X/apG7jQM33or3NdOmjXBKWGOg4JmSiRfUfuTtHXw== dependencies: "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.5.5" - "@babel/helpers" "^7.5.5" - "@babel/parser" "^7.5.5" - "@babel/template" "^7.4.4" - "@babel/traverse" "^7.5.5" - "@babel/types" "^7.5.5" + "@babel/generator" "^7.6.0" + "@babel/helpers" "^7.6.0" + "@babel/parser" "^7.6.0" + "@babel/template" "^7.6.0" + "@babel/traverse" "^7.6.0" + "@babel/types" "^7.6.0" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" @@ -58,12 +58,12 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.4.0", "@babel/generator@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.5.5.tgz#873a7f936a3c89491b43536d12245b626664e3cf" - integrity sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ== +"@babel/generator@^7.4.0", "@babel/generator@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.0.tgz#e2c21efbfd3293ad819a2359b448f002bfdfda56" + integrity sha512-Ms8Mo7YBdMMn1BYuNtKuP/z0TgEIhbcyB8HVR6PPNYp4P61lMsABiS4A3VG1qznjXVCf3r+fVHhm4efTYVsySA== dependencies: - "@babel/types" "^7.5.5" + "@babel/types" "^7.6.0" jsesc "^2.5.1" lodash "^4.17.13" source-map "^0.5.0" @@ -224,14 +224,14 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.2.0" -"@babel/helpers@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.5.5.tgz#63908d2a73942229d1e6685bc2a0e730dde3b75e" - integrity sha512-nRq2BUhxZFnfEn/ciJuhklHvFOqjJUD5wpx+1bxUF2axL9C+v4DE/dmp5sT2dKnpOs4orZWzpAZqlCy8QqE/7g== +"@babel/helpers@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.6.0.tgz#21961d16c6a3c3ab597325c34c465c0887d31c6e" + integrity sha512-W9kao7OBleOjfXtFGgArGRX6eCP0UEcA2ZWEWNkJdRZnHhW4eEbeswbG3EwaRsnQUAEGWYgMq1HsIXuNNNy2eQ== dependencies: - "@babel/template" "^7.4.4" - "@babel/traverse" "^7.5.5" - "@babel/types" "^7.5.5" + "@babel/template" "^7.6.0" + "@babel/traverse" "^7.6.0" + "@babel/types" "^7.6.0" "@babel/highlight@^7.0.0": version "7.5.0" @@ -242,22 +242,22 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/node@~7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.5.5.tgz#5db48a3bcee64d9eda6474f2a0a55b235d0438b5" - integrity sha512-xsW6il+yY+lzXMsQuvIJNA7tU8ix/f4G6bDt4DrnCkVpsR6clk9XgEbp7QF+xGNDdoD7M7QYokCH83pm+UjD0w== +"@babel/node@~7.6.1": + version "7.6.1" + resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.6.1.tgz#84f8f4f1d86647d99537a681f32e65e70bb59f19" + integrity sha512-q2sJw+7aES/5wwjccECJfOuIgM1XIbZcn7b63JZM6VpaZwvOq913jL+tXRIn41Eg/Hr+BeIGWnvnjLTuT579pA== dependencies: - "@babel/polyfill" "^7.0.0" - "@babel/register" "^7.5.5" + "@babel/polyfill" "^7.6.0" + "@babel/register" "^7.6.0" commander "^2.8.1" lodash "^4.17.13" node-environment-flags "^1.0.5" v8flags "^3.1.1" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" - integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g== +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.0.tgz#3e05d0647432a8326cb28d0de03895ae5a57f39b" + integrity sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ== "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" @@ -382,10 +382,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-block-scoping@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.5.5.tgz#a35f395e5402822f10d2119f6f8e045e3639a2ce" - integrity sha512-82A3CLRRdYubkG85lKwhZB0WZoHxLGsJdux/cOVaJCJpvYFl1LVzAIFyRsa7CvXqW8rBM4Zf3Bfn8PHt5DP0Sg== +"@babel/plugin-transform-block-scoping@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.6.0.tgz#c49e21228c4bbd4068a35667e6d951c75439b1dc" + integrity sha512-tIt4E23+kw6TgL/edACZwP1OUKrjOTyMrFMLoT5IOFrfMRabCgekjqFd5o6PaAMildBu46oFkekIdMuGkkPEpA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" lodash "^4.17.13" @@ -411,10 +411,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-destructuring@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz#f6c09fdfe3f94516ff074fe877db7bc9ef05855a" - integrity sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ== +"@babel/plugin-transform-destructuring@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.6.0.tgz#44bbe08b57f4480094d57d9ffbcd96d309075ba6" + integrity sha512-2bGIS5P1v4+sWTCnKNDZDxbGvEqi0ijeqM/YqHtVGrvG2y0ySgnEEhXErvE9dA0bnIzY9bIzdFK0jFA46ASIIQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -480,10 +480,10 @@ "@babel/helper-plugin-utils" "^7.0.0" babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-commonjs@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz#425127e6045231360858eeaa47a71d75eded7a74" - integrity sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ== +"@babel/plugin-transform-modules-commonjs@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.6.0.tgz#39dfe957de4420445f1fcf88b68a2e4aa4515486" + integrity sha512-Ma93Ix95PNSEngqomy5LSBMAQvYKVe3dy+JlVJSHEXZR5ASL9lQBedMiCyVtmTLraIDVRE3ZjTZvmXXD2Ozw3g== dependencies: "@babel/helper-module-transforms" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" @@ -507,12 +507,12 @@ "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-named-capturing-groups-regex@^7.4.5": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz#9d269fd28a370258199b4294736813a60bbdd106" - integrity sha512-z7+2IsWafTBbjNsOxU/Iv5CvTJlr5w4+HGu1HovKYTtgJ362f7kBcQglkfmlspKKZ3bgrbSGvLfNx++ZJgCWsg== +"@babel/plugin-transform-named-capturing-groups-regex@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.6.0.tgz#1e6e663097813bb4f53d42df0750cf28ad3bb3f1" + integrity sha512-jem7uytlmrRl3iCAuQyw8BpB4c4LWvSpvIeXKpMb+7j84lkx4m4mYr5ErAcmN5KM7B6BqrAvRGjBIbbzqCczew== dependencies: - regexp-tree "^0.1.6" + regexp-tree "^0.1.13" "@babel/plugin-transform-new-target@^7.4.4": version "7.4.4" @@ -605,18 +605,18 @@ "@babel/helper-regex" "^7.4.4" regexpu-core "^4.5.4" -"@babel/polyfill@^7.0.0", "@babel/polyfill@^7.2.3": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.4.4.tgz#78801cf3dbe657844eeabf31c1cae3828051e893" - integrity sha512-WlthFLfhQQhh+A2Gn5NSFl0Huxz36x86Jn+E9OW7ibK8edKPq+KLy4apM1yDpQ8kJOVi1OVjpP4vSDLdrI04dg== +"@babel/polyfill@^7.2.3", "@babel/polyfill@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.6.0.tgz#6d89203f8b6cd323e8d946e47774ea35dc0619cc" + integrity sha512-q5BZJI0n/B10VaQQvln1IlDK3BTBJFbADx7tv+oXDPIDZuTo37H5Adb9jhlXm/fEN4Y7/64qD9mnrJJG7rmaTw== dependencies: core-js "^2.6.5" regenerator-runtime "^0.13.2" -"@babel/preset-env@~7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.5.tgz#bc470b53acaa48df4b8db24a570d6da1fef53c9a" - integrity sha512-GMZQka/+INwsMz1A5UEql8tG015h5j/qjptpKY2gJ7giy8ohzU710YciJB5rcKsWGWHiW3RUnHib0E5/m3Tp3A== +"@babel/preset-env@~7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.6.0.tgz#aae4141c506100bb2bfaa4ac2a5c12b395619e50" + integrity sha512-1efzxFv/TcPsNXlRhMzRnkBFMeIqBBgzwmZwlFDw5Ubj0AGLeufxugirwZmkkX/ayi3owsSqoQ4fw8LkfK9SYg== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -634,10 +634,10 @@ "@babel/plugin-transform-arrow-functions" "^7.2.0" "@babel/plugin-transform-async-to-generator" "^7.5.0" "@babel/plugin-transform-block-scoped-functions" "^7.2.0" - "@babel/plugin-transform-block-scoping" "^7.5.5" + "@babel/plugin-transform-block-scoping" "^7.6.0" "@babel/plugin-transform-classes" "^7.5.5" "@babel/plugin-transform-computed-properties" "^7.2.0" - "@babel/plugin-transform-destructuring" "^7.5.0" + "@babel/plugin-transform-destructuring" "^7.6.0" "@babel/plugin-transform-dotall-regex" "^7.4.4" "@babel/plugin-transform-duplicate-keys" "^7.5.0" "@babel/plugin-transform-exponentiation-operator" "^7.2.0" @@ -646,10 +646,10 @@ "@babel/plugin-transform-literals" "^7.2.0" "@babel/plugin-transform-member-expression-literals" "^7.2.0" "@babel/plugin-transform-modules-amd" "^7.5.0" - "@babel/plugin-transform-modules-commonjs" "^7.5.0" + "@babel/plugin-transform-modules-commonjs" "^7.6.0" "@babel/plugin-transform-modules-systemjs" "^7.5.0" "@babel/plugin-transform-modules-umd" "^7.2.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.4.5" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.6.0" "@babel/plugin-transform-new-target" "^7.4.4" "@babel/plugin-transform-object-super" "^7.5.5" "@babel/plugin-transform-parameters" "^7.4.4" @@ -662,19 +662,18 @@ "@babel/plugin-transform-template-literals" "^7.4.4" "@babel/plugin-transform-typeof-symbol" "^7.2.0" "@babel/plugin-transform-unicode-regex" "^7.4.4" - "@babel/types" "^7.5.5" + "@babel/types" "^7.6.0" browserslist "^4.6.0" core-js-compat "^3.1.1" invariant "^2.2.2" js-levenshtein "^1.1.3" semver "^5.5.0" -"@babel/register@^7.5.5", "@babel/register@~7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.5.5.tgz#40fe0d474c8c8587b28d6ae18a03eddad3dac3c1" - integrity sha512-pdd5nNR+g2qDkXZlW1yRCWFlNrAn2PPdnZUB72zjX4l1Vv4fMRRLwyf+n/idFCLI1UgVGboUU8oVziwTBiyNKQ== +"@babel/register@^7.6.0", "@babel/register@~7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.6.0.tgz#76b6f466714680f4becafd45beeb2a7b87431abf" + integrity sha512-78BomdN8el+x/nkup9KwtjJXuptW5oXMFmP11WoM2VJBjxrKv4grC3qjpLL8RGGUYUGsm57xnjYFM2uom+jWUQ== dependencies: - core-js "^3.0.0" find-cache-dir "^2.0.0" lodash "^4.17.13" mkdirp "^0.5.1" @@ -696,34 +695,34 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" - integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== +"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6" + integrity sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.4.4" - "@babel/types" "^7.4.4" + "@babel/parser" "^7.6.0" + "@babel/types" "^7.6.0" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb" - integrity sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ== +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.5", "@babel/traverse@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.0.tgz#389391d510f79be7ce2ddd6717be66d3fed4b516" + integrity sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ== dependencies: "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.5.5" + "@babel/generator" "^7.6.0" "@babel/helper-function-name" "^7.1.0" "@babel/helper-split-export-declaration" "^7.4.4" - "@babel/parser" "^7.5.5" - "@babel/types" "^7.5.5" + "@babel/parser" "^7.6.0" + "@babel/types" "^7.6.0" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.5.tgz#97b9f728e182785909aa4ab56264f090a028d18a" - integrity sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw== +"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5", "@babel/types@^7.6.0": + version "7.6.1" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.1.tgz#53abf3308add3ac2a2884d539151c57c4b3ac648" + integrity sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g== dependencies: esutils "^2.0.2" lodash "^4.17.13" @@ -1632,41 +1631,41 @@ apollo-graphql@^0.3.3: apollo-env "0.5.1" lodash.sortby "^4.7.0" -apollo-link-context@~1.0.18: - version "1.0.18" - resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.18.tgz#9e700e3314da8ded50057fee0a18af2bfcedbfc3" - integrity sha512-aG5cbUp1zqOHHQjAJXG7n/izeMQ6LApd/whEF5z6qZp5ATvcyfSNkCfy3KRJMMZZ3iNfVTs6jF+IUA8Zvf+zeg== +apollo-link-context@~1.0.19: + version "1.0.19" + resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.19.tgz#3c9ba5bf75ed5428567ce057b8837ef874a58987" + integrity sha512-TUi5TyufU84hEiGkpt+5gdH5HkB3Gx46npNfoxR4of3DKBCMuItGERt36RCaryGcU/C3u2zsICU3tJ+Z9LjFoQ== dependencies: - apollo-link "^1.2.12" + apollo-link "^1.2.13" tslib "^1.9.3" -apollo-link-http-common@^0.2.14: - version "0.2.14" - resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.14.tgz#d3a195c12e00f4e311c417f121181dcc31f7d0c8" - integrity sha512-v6mRU1oN6XuX8beVIRB6OpF4q1ULhSnmy7ScnHnuo1qV6GaFmDcbdvXqxIkAV1Q8SQCo2lsv4HeqJOWhFfApOg== +apollo-link-http-common@^0.2.15: + version "0.2.15" + resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.15.tgz#304e67705122bf69a9abaded4351b10bc5efd6d9" + integrity sha512-+Heey4S2IPsPyTf8Ag3PugUupASJMW894iVps6hXbvwtg1aHSNMXUYO5VG7iRHkPzqpuzT4HMBanCTXPjtGzxg== dependencies: - apollo-link "^1.2.12" + apollo-link "^1.2.13" ts-invariant "^0.4.0" tslib "^1.9.3" -apollo-link-http@~1.5.15: - version "1.5.15" - resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.15.tgz#106ab23bb8997bd55965d05855736d33119652cf" - integrity sha512-epZFhCKDjD7+oNTVK3P39pqWGn4LEhShAoA1Q9e2tDrBjItNfviiE33RmcLcCURDYyW5JA6SMgdODNI4Is8tvQ== +apollo-link-http@~1.5.16: + version "1.5.16" + resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.16.tgz#44fe760bcc2803b8a7f57fc9269173afb00f3814" + integrity sha512-IA3xA/OcrOzINRZEECI6IdhRp/Twom5X5L9jMehfzEo2AXdeRwAMlH5LuvTZHgKD8V1MBnXdM6YXawXkTDSmJw== dependencies: - apollo-link "^1.2.12" - apollo-link-http-common "^0.2.14" + apollo-link "^1.2.13" + apollo-link-http-common "^0.2.15" tslib "^1.9.3" -apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.3: - version "1.2.12" - resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.12.tgz#014b514fba95f1945c38ad4c216f31bcfee68429" - integrity sha512-fsgIAXPKThyMVEMWQsUN22AoQI+J/pVXcjRGAShtk97h7D8O+SPskFinCGEkxPeQpE83uKaqafB2IyWdjN+J3Q== +apollo-link@^1.0.0, apollo-link@^1.2.13, apollo-link@^1.2.3: + version "1.2.13" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.13.tgz#dff00fbf19dfcd90fddbc14b6a3f9a771acac6c4" + integrity sha512-+iBMcYeevMm1JpYgwDEIDt/y0BB7VWyvlm/7x+TIPNLHCTCMgcEgDuW5kH86iQZWo0I7mNwQiTOz+/3ShPFmBw== dependencies: apollo-utilities "^1.3.0" ts-invariant "^0.4.0" tslib "^1.9.3" - zen-observable-ts "^0.8.19" + zen-observable-ts "^0.8.20" apollo-server-caching@^0.5.0: version "0.5.0" @@ -2383,7 +2382,7 @@ cheerio@~1.0.0-rc.2, cheerio@~1.0.0-rc.3: lodash "^4.15.0" parse5 "^3.0.1" -chokidar@^2.0.4, chokidar@^2.1.5: +chokidar@^2.1.5, chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== @@ -2663,7 +2662,7 @@ core-js@^2.4.0, core-js@^2.6.5: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== -core-js@^3.0.0, core-js@^3.0.1: +core-js@^3.0.1: version "3.2.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.2.1.tgz#cd41f38534da6cc59f7db050fe67307de9868b09" integrity sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw== @@ -2839,10 +2838,10 @@ data-urls@^1.0.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -date-fns@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.1.tgz#c5f30e31d3294918e6b6a82753a4e719120e203d" - integrity sha512-C14oTzTZy8DH1Eq8N78owrCWvf3+cnJw88BTK/N3DYWVxDJuJzPaNdplzYxDYuuXXGvqBcO4Vy5SOrwAooXSWw== +date-fns@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.1.0.tgz#0d7e806c3cefe14a943532dbf968995ccfd46bd9" + integrity sha512-eKeLk3sLCnxB/0PN4t1+zqDtSs4jb4mXRSTZ2okmx/myfWyDqeO4r5nnmA5LClJiCwpuTMeK2v5UQPuE4uMaxA== debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" @@ -4118,10 +4117,10 @@ graphql-request@~1.8.2: dependencies: cross-fetch "2.2.2" -graphql-shield@~6.0.6: - version "6.0.6" - resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-6.0.6.tgz#ef8c53f1dd972c2d1828ffd45ce9b1f877576534" - integrity sha512-rwhno5ZvEBbedQ8mEOi/Lk71J5CrpQCOcyuDIO+qb1hqm7cvWLtLVyZFrhVp7vN/vULV9oX30j0clC/1d05LpQ== +graphql-shield@~6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-6.1.0.tgz#7298af72167e7c9fd19a36fac9b425b94025a393" + integrity sha512-dIZ6ABnUn3XQtIzw9/9f8wFmZoY5XZlsHgkxSKF+N/oXmKvQoi11J5/y/jxJTBmKYi/2JZ12C1JjDn5TOopn+w== dependencies: "@types/yup" "0.26.23" lightercollective "^0.3.0" @@ -6196,10 +6195,10 @@ neo4j-graphql-js@^2.7.2: lodash "^4.17.15" neo4j-driver "^1.7.3" -neode@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.2.tgz#ced277e1daba26a77c48f5857c30af054f11c7df" - integrity sha512-Bm4GBXdXunv8cqUUkJtksIGHDnYdBJf4UHwzFgXbJiDKBAdqfjhzwAPAhf1PrvlFmR4vJva2Bh/XvIghYOiKrA== +neode@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.3.tgz#a539830cce6f6e4825462f6cb03f2969a0003f1b" + integrity sha512-pArHG1hD2kVwrzLlz6B1+IgdOJRQj/BgR6KzH6DlVzSA6geoZRe68fbpvmOJtzyPU7iuUYxXVk87PpPM1A7dlg== dependencies: "@hapi/joi" "^15.1.0" dotenv "^4.0.0" @@ -7231,10 +7230,10 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp-tree@^0.1.6: - version "0.1.11" - resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.11.tgz#c9c7f00fcf722e0a56c7390983a7a63dd6c272f3" - integrity sha512-7/l/DgapVVDzZobwMCCgMlqiqyLFJ0cduo/j+3BcDJIB+yJdsYCfKuI3l/04NV+H/rfNRdPIDbXNZHM9XvQatg== +regexp-tree@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.13.tgz#5b19ab9377edc68bc3679256840bb29afc158d7f" + integrity sha512-hwdV/GQY5F8ReLZWO+W1SRoN5YfpOKY6852+tBFcma72DKBIcHjPRIlIvQN35bCOljuAfP2G2iB0FC/w236mUw== regexpp@^2.0.1: version "2.0.1" @@ -8930,10 +8929,10 @@ yup@^0.27.0: synchronous-promise "^2.0.6" toposort "^2.0.2" -zen-observable-ts@^0.8.19: - version "0.8.19" - resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.19.tgz#c094cd20e83ddb02a11144a6e2a89706946b5694" - integrity sha512-u1a2rpE13G+jSzrg3aiCqXU5tN2kw41b+cBZGmnc+30YimdkKiDj9bTowcB41eL77/17RF/h+393AuVgShyheQ== +zen-observable-ts@^0.8.20: + version "0.8.20" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.20.tgz#44091e335d3fcbc97f6497e63e7f57d5b516b163" + integrity sha512-2rkjiPALhOtRaDX6pWyNqK1fnP5KkJJybYebopNSn6wDG1lxBoFs2+nwwXKoA6glHIrtwrfBBy6da0stkKtTAA== dependencies: tslib "^1.9.3" zen-observable "^0.8.0" diff --git a/package.json b/package.json index ccaa3b594..7edeb47e3 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "faker": "Marak/faker.js#master", "graphql-request": "^1.8.2", "neo4j-driver": "^1.7.6", - "neode": "^0.3.2", + "neode": "^0.3.3", "npm-run-all": "^4.1.5", "slug": "^1.1.0" } diff --git a/webapp/components/CommentForm/CommentForm.spec.js b/webapp/components/CommentForm/CommentForm.spec.js index 964730b5f..1ab778859 100644 --- a/webapp/components/CommentForm/CommentForm.spec.js +++ b/webapp/components/CommentForm/CommentForm.spec.js @@ -1,13 +1,11 @@ import { mount, createLocalVue } from '@vue/test-utils' import CommentForm from './CommentForm' import Styleguide from '@human-connection/styleguide' -import Vuex from 'vuex' import MutationObserver from 'mutation-observer' global.MutationObserver = MutationObserver const localVue = createLocalVue() -localVue.use(Vuex) localVue.use(Styleguide) describe('CommentForm.vue', () => { @@ -53,20 +51,11 @@ describe('CommentForm.vue', () => { }) describe('mount', () => { - const getters = { - 'editor/placeholder': () => { - return 'some cool placeholder' - }, - } - const store = new Vuex.Store({ - getters, - }) const Wrapper = () => { return mount(CommentForm, { mocks, localVue, propsData, - store, }) } diff --git a/webapp/components/Editor/Editor.spec.js b/webapp/components/Editor/Editor.spec.js index bc5f6f3e8..18eaef3b8 100644 --- a/webapp/components/Editor/Editor.spec.js +++ b/webapp/components/Editor/Editor.spec.js @@ -1,6 +1,5 @@ import { mount, createLocalVue } from '@vue/test-utils' import Editor from './Editor' -import Vuex from 'vuex' import Styleguide from '@human-connection/styleguide' import MutationObserver from 'mutation-observer' import Vue from 'vue' @@ -8,19 +7,14 @@ import Vue from 'vue' global.MutationObserver = MutationObserver const localVue = createLocalVue() -localVue.use(Vuex) localVue.use(Styleguide) describe('Editor.vue', () => { let wrapper let propsData let mocks - let getters const Wrapper = () => { - const store = new Vuex.Store({ - getters, - }) return (wrapper = mount(Editor, { mocks, propsData, @@ -29,19 +23,13 @@ describe('Editor.vue', () => { stubs: { transition: false, }, - store, })) } beforeEach(() => { propsData = {} mocks = { - $t: () => {}, - } - getters = { - 'editor/placeholder': () => { - return 'some cool placeholder' - }, + $t: () => 'some cool placeholder', } wrapper = Wrapper() }) @@ -64,12 +52,10 @@ describe('Editor.vue', () => { }) }) - describe('uses the placeholder', () => { - it('from the store', () => { - expect(wrapper.vm.editor.extensions.options.placeholder.emptyNodeText).toEqual( - 'some cool placeholder', - ) - }) + it('translates the placeholder', () => { + expect(wrapper.vm.editor.extensions.options.placeholder.emptyNodeText).toEqual( + 'some cool placeholder', + ) }) describe('optional extensions', () => { diff --git a/webapp/components/Editor/Editor.vue b/webapp/components/Editor/Editor.vue index 75f550c2a..af98f785a 100644 --- a/webapp/components/Editor/Editor.vue +++ b/webapp/components/Editor/Editor.vue @@ -21,7 +21,6 @@ diff --git a/webapp/components/FilterPosts/FilterPosts.spec.js b/webapp/components/FilterPosts/FilterPosts.spec.js index 509ff32b7..0cbd3e962 100644 --- a/webapp/components/FilterPosts/FilterPosts.spec.js +++ b/webapp/components/FilterPosts/FilterPosts.spec.js @@ -19,6 +19,7 @@ describe('FilterPosts.vue', () => { let allCategoriesButton let environmentAndNatureButton let democracyAndPoliticsButton + let happyEmotionButton beforeEach(() => { mocks = { @@ -52,6 +53,7 @@ describe('FilterPosts.vue', () => { 'postsFilter/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(), 'postsFilter/RESET_CATEGORIES': jest.fn(), 'postsFilter/TOGGLE_CATEGORY': jest.fn(), + 'postsFilter/TOGGLE_EMOTION': jest.fn(), } getters = { 'postsFilter/isActive': () => false, @@ -61,6 +63,7 @@ describe('FilterPosts.vue', () => { }, 'postsFilter/filteredCategoryIds': jest.fn(() => []), 'postsFilter/filteredByUsersFollowed': jest.fn(), + 'postsFilter/filteredByEmotions': jest.fn(() => []), } const openFilterPosts = () => { const store = new Vuex.Store({ mutations, getters }) @@ -120,5 +123,22 @@ describe('FilterPosts.vue', () => { expect(mutations['postsFilter/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34') }) }) + + describe('click on an "emotions-buttons" button', () => { + it('calls TOGGLE_EMOTION when clicked', () => { + const wrapper = openFilterPosts() + happyEmotionButton = wrapper.findAll('button.emotions-buttons').at(1) + happyEmotionButton.trigger('click') + expect(mutations['postsFilter/TOGGLE_EMOTION']).toHaveBeenCalledWith({}, 'happy') + }) + + it('sets the attribute `src` to colorized image', () => { + getters['postsFilter/filteredByEmotions'] = jest.fn(() => ['happy']) + const wrapper = openFilterPosts() + happyEmotionButton = wrapper.findAll('button.emotions-buttons').at(1) + const happyEmotionButtonImage = happyEmotionButton.find('img') + expect(happyEmotionButtonImage.attributes().src).toEqual('/img/svg/emoji/happy_color.svg') + }) + }) }) }) diff --git a/webapp/components/FilterPosts/FilterPosts.vue b/webapp/components/FilterPosts/FilterPosts.vue index defc0eb0a..8a12ac633 100644 --- a/webapp/components/FilterPosts/FilterPosts.vue +++ b/webapp/components/FilterPosts/FilterPosts.vue @@ -11,20 +11,25 @@ + diff --git a/webapp/components/LocaleSwitch/LocaleSwitch.spec.js b/webapp/components/LocaleSwitch/LocaleSwitch.spec.js index ae81881d6..eba7bd068 100644 --- a/webapp/components/LocaleSwitch/LocaleSwitch.spec.js +++ b/webapp/components/LocaleSwitch/LocaleSwitch.spec.js @@ -1,13 +1,10 @@ import { mount, createLocalVue } from '@vue/test-utils' import Styleguide from '@human-connection/styleguide' -import Vuex from 'vuex' import VTooltip from 'v-tooltip' import LocaleSwitch from './LocaleSwitch.vue' -import { mutations } from '~/store/editor' const localVue = createLocalVue() -localVue.use(Vuex) localVue.use(Styleguide) localVue.use(VTooltip) @@ -46,13 +43,8 @@ describe('LocaleSwitch.vue', () => { }) describe('mount', () => { - const store = new Vuex.Store({ - mutations: { - 'editor/SET_PLACEHOLDER_TEXT': mutations.SET_PLACEHOLDER_TEXT, - }, - }) const Wrapper = () => { - return mount(LocaleSwitch, { mocks, localVue, store, computed }) + return mount(LocaleSwitch, { mocks, localVue, computed }) } beforeEach(() => { wrapper = Wrapper() diff --git a/webapp/components/LocaleSwitch/LocaleSwitch.vue b/webapp/components/LocaleSwitch/LocaleSwitch.vue index aeee580b5..f765c534f 100644 --- a/webapp/components/LocaleSwitch/LocaleSwitch.vue +++ b/webapp/components/LocaleSwitch/LocaleSwitch.vue @@ -36,7 +36,6 @@ import Dropdown from '~/components/Dropdown' import find from 'lodash/find' import orderBy from 'lodash/orderBy' -import { mapMutations } from 'vuex' export default { components: { @@ -66,11 +65,9 @@ export default { }, }, methods: { - ...mapMutations({ setPlaceholderText: 'editor/SET_PLACEHOLDER_TEXT' }), changeLanguage(locale, toggleMenu) { this.$i18n.set(locale) toggleMenu() - this.setPlaceholderText(this.$t('editor.placeholder')) }, matcher(locale) { return locale === this.$i18n.locale() diff --git a/webapp/components/PasswordReset/ChangePassword.spec.js b/webapp/components/PasswordReset/ChangePassword.spec.js index a6722f016..e93d5d00d 100644 --- a/webapp/components/PasswordReset/ChangePassword.spec.js +++ b/webapp/components/PasswordReset/ChangePassword.spec.js @@ -39,10 +39,10 @@ describe('ChangePassword ', () => { }) } - describe('given email and verification code', () => { + describe('given email and verification nonce', () => { beforeEach(() => { propsData.email = 'mail@example.org' - propsData.code = '123456' + propsData.nonce = '123456' }) describe('submitting new password', () => { @@ -59,14 +59,14 @@ describe('ChangePassword ', () => { it('delivers new password to backend', () => { const expected = expect.objectContaining({ - variables: { code: '123456', email: 'mail@example.org', password: 'supersecret' }, + variables: { nonce: '123456', email: 'mail@example.org', password: 'supersecret' }, }) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) }) describe('password reset successful', () => { it('displays success message', () => { - const expected = 'verify-code.form.change-password.success' + const expected = 'verify-nonce.form.change-password.success' expect(mocks.$t).toHaveBeenCalledWith(expected) }) diff --git a/webapp/components/PasswordReset/ChangePassword.vue b/webapp/components/PasswordReset/ChangePassword.vue index f59edffa1..3de4f048a 100644 --- a/webapp/components/PasswordReset/ChangePassword.vue +++ b/webapp/components/PasswordReset/ChangePassword.vue @@ -1,5 +1,5 @@ @@ -64,7 +64,7 @@ export default { }, props: { email: { type: String, required: true }, - code: { type: String, required: true }, + nonce: { type: String, required: true }, }, data() { const passwordForm = PasswordForm({ translate: this.$t }) @@ -82,13 +82,13 @@ export default { methods: { async handleSubmitPassword() { const mutation = gql` - mutation($code: String!, $email: String!, $password: String!) { - resetPassword(code: $code, email: $email, newPassword: $password) + mutation($nonce: String!, $email: String!, $password: String!) { + resetPassword(nonce: $nonce, email: $email, newPassword: $password) } ` const { password } = this.formData - const { email, code } = this - const variables = { password, email, code } + const { email, nonce } = this + const variables = { password, email, nonce } try { const { data: { resetPassword }, diff --git a/webapp/components/PasswordReset/VerifyCode.spec.js b/webapp/components/PasswordReset/VerifyNonce.spec.js similarity index 62% rename from webapp/components/PasswordReset/VerifyCode.spec.js rename to webapp/components/PasswordReset/VerifyNonce.spec.js index 22cdfd885..ebe552f0d 100644 --- a/webapp/components/PasswordReset/VerifyCode.spec.js +++ b/webapp/components/PasswordReset/VerifyNonce.spec.js @@ -1,12 +1,12 @@ import { mount, createLocalVue } from '@vue/test-utils' -import VerifyCode from './VerifyCode' +import VerifyNonce from './VerifyNonce.vue' import Styleguide from '@human-connection/styleguide' const localVue = createLocalVue() localVue.use(Styleguide) -describe('VerifyCode ', () => { +describe('VerifyNonce ', () => { let wrapper let Wrapper let mocks @@ -25,27 +25,27 @@ describe('VerifyCode ', () => { beforeEach(jest.useFakeTimers) Wrapper = () => { - return mount(VerifyCode, { + return mount(VerifyNonce, { mocks, localVue, propsData, }) } - it('renders a verify code form', () => { + it('renders a verify nonce form', () => { wrapper = Wrapper() - expect(wrapper.find('.verify-code').exists()).toBe(true) + expect(wrapper.find('.verify-nonce').exists()).toBe(true) }) - describe('after verification code given', () => { + describe('after verification nonce given', () => { beforeEach(() => { wrapper = Wrapper() - wrapper.find('input#code').setValue('123456') + wrapper.find('input#nonce').setValue('123456') wrapper.find('form').trigger('submit') }) - it('emits `verifyCode`', () => { - const expected = [[{ code: '123456', email: 'mail@example.org' }]] + it('emits `verification`', () => { + const expected = [[{ nonce: '123456', email: 'mail@example.org' }]] expect(wrapper.emitted('verification')).toEqual(expected) }) }) diff --git a/webapp/components/PasswordReset/VerifyCode.vue b/webapp/components/PasswordReset/VerifyNonce.vue similarity index 70% rename from webapp/components/PasswordReset/VerifyCode.vue rename to webapp/components/PasswordReset/VerifyNonce.vue index de1495e36..94ae13564 100644 --- a/webapp/components/PasswordReset/VerifyCode.vue +++ b/webapp/components/PasswordReset/VerifyNonce.vue @@ -1,5 +1,5 @@