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/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/registration.js b/backend/src/schema/resolvers/registration.js index 423ce7580..92c6c3a3e 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' @@ -80,7 +80,7 @@ export default { 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/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/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/webapp/components/Registration/CreateUserAccount.spec.js b/webapp/components/Registration/CreateUserAccount.spec.js index 3ee358db6..75e1ade83 100644 --- a/webapp/components/Registration/CreateUserAccount.spec.js +++ b/webapp/components/Registration/CreateUserAccount.spec.js @@ -1,5 +1,6 @@ import { config, mount, createLocalVue } from '@vue/test-utils' -import CreateUserAccount, { SignupVerificationMutation } from './CreateUserAccount' +import CreateUserAccount from './CreateUserAccount' +import { SignupVerificationMutation } from '~/graphql/Registration.js' import Styleguide from '@human-connection/styleguide' const localVue = createLocalVue() @@ -55,6 +56,7 @@ describe('CreateUserAccount', () => { wrapper = Wrapper() wrapper.find('input#name').setValue('John Doe') wrapper.find('input#password').setValue('hellopassword') + wrapper.find('textarea#about').setValue('Hello I am the `about` attribute') wrapper.find('input#passwordConfirmation').setValue('hellopassword') wrapper.find('input#checkbox').setChecked() await wrapper.find('form').trigger('submit') @@ -72,7 +74,7 @@ describe('CreateUserAccount', () => { await action() const expected = expect.objectContaining({ variables: { - about: '', + about: 'Hello I am the `about` attribute', name: 'John Doe', email: 'sixseven@example.org', nonce: '666777', diff --git a/webapp/components/Registration/CreateUserAccount.vue b/webapp/components/Registration/CreateUserAccount.vue index e1ff98a5c..9e4d899e7 100644 --- a/webapp/components/Registration/CreateUserAccount.vue +++ b/webapp/components/Registration/CreateUserAccount.vue @@ -25,7 +25,7 @@ :placeholder="$t('settings.data.namePlaceholder')" />