Refactor slugifyMiddleware

* eliminate GraphQL from spec
* much better error handling
* add EmailAdress factory
This commit is contained in:
roschaefer 2019-09-07 00:48:54 +02:00
parent 42add14856
commit 0cd7ac3d32
6 changed files with 169 additions and 82 deletions

View File

@ -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 siged up with her 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!',
},
],
})
})
})
})
})

View File

@ -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,22 @@ 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
})
debugger
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

View File

@ -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)
}
},

View File

@ -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)
},
}
}

View File

@ -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 = {}) => {

View File

@ -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