Add missing unit tests/refactor code

- Refactoring without tests makes it riskier
- Move some tests from resolver to middleware unit tests to live closer
to where the validation happens, remove duplicate tests
- DRY out code
This commit is contained in:
mattwr18 2019-12-03 17:55:20 +01:00
parent 132c12a7d3
commit b3640659bb
8 changed files with 525 additions and 166 deletions

View File

@ -5,7 +5,7 @@ const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
const NO_CATEGORIES_ERR_MESSAGE =
'You cannot save a post without at least one category or more than three'
const validateCommentCreation = async (resolve, root, args, context, info) => {
const validateCreateComment = async (resolve, root, args, context, info) => {
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
const { postId } = args
@ -37,7 +37,6 @@ const validateCommentCreation = async (resolve, root, args, context, info) => {
}
const validateUpdateComment = async (resolve, root, args, context, info) => {
const COMMENT_MIN_LENGTH = 1
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
if (!args.content || content.length < COMMENT_MIN_LENGTH) {
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
@ -91,7 +90,7 @@ const validateReport = async (resolve, root, args, context, info) => {
export default {
Mutation: {
CreateComment: validateCommentCreation,
CreateComment: validateCreateComment,
UpdateComment: validateUpdateComment,
CreatePost: validatePost,
UpdatePost: validateUpdatePost,

View File

@ -0,0 +1,244 @@
import { gql } from '../../helpers/jest'
import Factory from '../../seed/factories'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server'
const factory = Factory()
const neode = getNeode()
const driver = getDriver()
let mutate, authenticatedUser, user
const createCommentMutation = gql`
mutation($id: ID, $postId: ID!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) {
id
}
}
`
const updateCommentMutation = gql`
mutation($content: String!, $id: ID!) {
UpdateComment(content: $content, id: $id) {
id
}
}
`
const createPostMutation = gql`
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) {
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
id
}
}
`
const updatePostMutation = gql`
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
id
}
}
`
beforeAll(() => {
const { server } = createServer({
context: () => {
return {
user: authenticatedUser,
neode,
driver,
}
},
})
mutate = createTestClient(server).mutate
})
beforeEach(async () => {
user = await factory.create('User', {
id: 'user-id',
})
await factory.create('Post', {
id: 'post-4-commenting',
authorId: 'user-id',
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('validateCreateComment', () => {
let createCommentVariables
beforeEach(async () => {
createCommentVariables = {
postId: 'whatever',
content: '',
}
authenticatedUser = await user.toJson()
})
it('throws an error if content is empty', async () => {
createCommentVariables = { ...createCommentVariables, postId: 'post-4-commenting' }
await expect(
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
).resolves.toMatchObject({
data: { CreateComment: null },
errors: [{ message: 'Comment must be at least 1 character long!' }],
})
})
it('sanitizes content and throws an error if not longer than 1 character', async () => {
createCommentVariables = { postId: 'post-4-commenting', content: '<a></a>' }
await expect(
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
).resolves.toMatchObject({
data: { CreateComment: null },
errors: [{ message: 'Comment must be at least 1 character long!' }],
})
})
it('throws an error if there is no post with given id in the database', async () => {
createCommentVariables = {
...createCommentVariables,
postId: 'non-existent-post',
content: 'valid content',
}
await expect(
mutate({ mutation: createCommentMutation, variables: createCommentVariables }),
).resolves.toMatchObject({
data: { CreateComment: null },
errors: [{ message: 'Comment cannot be created without a post!' }],
})
})
describe('validateUpdateComment', () => {
let updateCommentVariables
beforeEach(async () => {
await factory.create('Comment', {
id: 'comment-id',
authorId: 'user-id',
})
updateCommentVariables = {
id: 'whatever',
content: '',
}
authenticatedUser = await user.toJson()
})
it('throws an error if content is empty', async () => {
updateCommentVariables = { ...updateCommentVariables, id: 'comment-id' }
await expect(
mutate({ mutation: updateCommentMutation, variables: updateCommentVariables }),
).resolves.toMatchObject({
data: { UpdateComment: null },
errors: [{ message: 'Comment must be at least 1 character long!' }],
})
})
it('sanitizes content and throws an error if not longer than 1 character', async () => {
updateCommentVariables = { id: 'comment-id', content: '<a></a>' }
await expect(
mutate({ mutation: updateCommentMutation, variables: updateCommentVariables }),
).resolves.toMatchObject({
data: { UpdateComment: null },
errors: [{ message: 'Comment must be at least 1 character long!' }],
})
})
})
describe('validatePost', () => {
let createPostVariables
beforeEach(async () => {
createPostVariables = {
title: 'I am a title',
content: 'Some content',
}
authenticatedUser = await user.toJson()
})
describe('categories', () => {
describe('null', () => {
it('throws UserInputError', async () => {
createPostVariables = { ...createPostVariables, categoryIds: null }
await expect(
mutate({ mutation: createPostMutation, variables: createPostVariables }),
).resolves.toMatchObject({
data: { CreatePost: null },
errors: [
{
message: 'You cannot save a post without at least one category or more than three',
},
],
})
})
})
describe('empty', () => {
it('throws UserInputError', async () => {
createPostVariables = { ...createPostVariables, categoryIds: [] }
await expect(
mutate({ mutation: createPostMutation, variables: createPostVariables }),
).resolves.toMatchObject({
data: { CreatePost: null },
errors: [
{
message: 'You cannot save a post without at least one category or more than three',
},
],
})
})
})
describe('more than 3 categoryIds', () => {
it('throws UserInputError', async () => {
createPostVariables = {
...createPostVariables,
categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'],
}
await expect(
mutate({ mutation: createPostMutation, variables: createPostVariables }),
).resolves.toMatchObject({
data: { CreatePost: null },
errors: [
{
message: 'You cannot save a post without at least one category or more than three',
},
],
})
})
})
})
})
describe('validateUpdatePost', () => {
describe('post created without categories somehow', () => {
let owner, updatePostVariables
beforeEach(async () => {
const postSomehowCreated = await neode.create('Post', {
id: 'how-was-this-created',
})
owner = await neode.create('User', {
id: 'author-of-post-without-category',
slug: 'hacker',
})
await postSomehowCreated.relateTo(owner, 'author')
authenticatedUser = await owner.toJson()
updatePostVariables = {
id: 'how-was-this-created',
title: 'I am a title',
content: 'Some content',
categoryIds: [],
}
})
it('requires at least one category for successful update', async () => {
await expect(
mutate({ mutation: updatePostMutation, variables: updatePostVariables }),
).resolves.toMatchObject({
data: { UpdatePost: null },
errors: [
{ message: 'You cannot save a post without at least one category or more than three' },
],
})
})
})
})
})

View File

@ -111,42 +111,6 @@ describe('CreateComment', () => {
},
)
})
describe('comment content is empty', () => {
beforeEach(() => {
variables = { ...variables, content: '<p></p>' }
})
it('throw UserInput error', async () => {
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
expect(data).toEqual({ CreateComment: null })
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
})
})
describe('comment content contains only whitespaces', () => {
beforeEach(() => {
variables = { ...variables, content: ' <p> </p> ' }
})
it('throw UserInput error', async () => {
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
expect(data).toEqual({ CreateComment: null })
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
})
})
describe('invalid post id', () => {
beforeEach(() => {
variables = { ...variables, postId: 'does-not-exist' }
})
it('throw UserInput error', async () => {
const { data, errors } = await mutate({ mutation: createCommentMutation, variables })
expect(data).toEqual({ CreateComment: null })
expect(errors[0]).toHaveProperty('message', 'Comment cannot be created without a post!')
})
})
})
})
})
@ -226,17 +190,6 @@ describe('UpdateComment', () => {
expect(newlyCreatedComment.updatedAt).not.toEqual(UpdateComment.updatedAt)
})
describe('if `content` empty', () => {
beforeEach(() => {
variables = { ...variables, content: ' <p> </p>' }
})
it('throws InputError', async () => {
const { errors } = await mutate({ mutation: updateCommentMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Comment must be at least 1 character long!')
})
})
describe('if comment does not exist for given id', () => {
beforeEach(() => {
variables = { ...variables, id: 'does-not-exist' }

View File

@ -316,53 +316,6 @@ describe('CreatePost', () => {
)
})
})
describe('categories', () => {
describe('null', () => {
beforeEach(() => {
variables = { ...variables, categoryIds: null }
})
it('throws UserInputError', async () => {
const {
errors: [error],
} = await mutate({ mutation: createPostMutation, variables })
expect(error).toHaveProperty(
'message',
'You cannot save a post without at least one category or more than three',
)
})
})
describe('empty', () => {
beforeEach(() => {
variables = { ...variables, categoryIds: [] }
})
it('throws UserInputError', async () => {
const {
errors: [error],
} = await mutate({ mutation: createPostMutation, variables })
expect(error).toHaveProperty(
'message',
'You cannot save a post without at least one category or more than three',
)
})
})
describe('more than 3 items', () => {
beforeEach(() => {
variables = { ...variables, categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'] }
})
it('throws UserInputError', async () => {
const {
errors: [error],
} = await mutate({ mutation: createPostMutation, variables })
expect(error).toHaveProperty(
'message',
'You cannot save a post without at least one category or more than three',
)
})
})
})
})
})
@ -493,74 +446,6 @@ describe('UpdatePost', () => {
expected,
)
})
describe('more than 3 categories', () => {
beforeEach(() => {
variables = { ...variables, categoryIds: ['cat9', 'cat27', 'cat15', 'cat4'] }
})
it('allows a maximum of three category for a successful update', async () => {
const {
errors: [error],
} = await mutate({ mutation: updatePostMutation, variables })
expect(error).toHaveProperty(
'message',
'You cannot save a post without at least one category or more than three',
)
})
})
describe('post created without categories somehow', () => {
let owner
beforeEach(async () => {
const postSomehowCreated = await neode.create('Post', {
id: 'how-was-this-created',
})
owner = await neode.create('User', {
id: 'author-of-post-without-category',
name: 'Hacker',
slug: 'hacker',
email: 'hacker@example.org',
password: '1234',
})
await postSomehowCreated.relateTo(owner, 'author')
authenticatedUser = await owner.toJson()
variables = { ...variables, id: 'how-was-this-created' }
})
it('throws an error if categoryIds is not an array', async () => {
const {
errors: [error],
} = await mutate({
mutation: updatePostMutation,
variables: {
...variables,
categoryIds: null,
},
})
expect(error).toHaveProperty(
'message',
'You cannot save a post without at least one category or more than three',
)
})
it('requires at least one category for successful update', async () => {
const {
errors: [error],
} = await mutate({
mutation: updatePostMutation,
variables: {
...variables,
categoryIds: [],
},
})
expect(error).toHaveProperty(
'message',
'You cannot save a post without at least one category or more than three',
)
})
})
})
})

View File

@ -1,6 +1,6 @@
export default {
Query: {
statistics: async (parent, args, { driver, user }) => {
statistics: async (_parent, _args, { driver }) => {
const session = driver.session()
const response = {}
try {

View File

@ -0,0 +1,140 @@
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server'
let query, authenticatedUser
const factory = Factory()
const instance = getNeode()
const driver = getDriver()
const statisticsQuery = gql`
query {
statistics {
countUsers
countPosts
countComments
countNotifications
countInvites
countFollows
countShouts
}
}
`
beforeAll(() => {
authenticatedUser = undefined
const { server } = createServer({
context: () => {
return {
driver,
neode: instance,
user: authenticatedUser,
}
},
})
query = createTestClient(server).query
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('statistics', () => {
describe('countUsers', () => {
beforeEach(async () => {
await Promise.all(
[...Array(6).keys()].map(() => {
return factory.create('User')
}),
)
})
it('returns the count of all users', async () => {
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
data: { statistics: { countUsers: 6 } },
errors: undefined,
})
})
})
describe('countPosts', () => {
beforeEach(async () => {
await Promise.all(
[...Array(3).keys()].map(() => {
return factory.create('Post')
}),
)
})
it('returns the count of all posts', async () => {
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
data: { statistics: { countPosts: 3 } },
errors: undefined,
})
})
})
describe('countComments', () => {
beforeEach(async () => {
await Promise.all(
[...Array(2).keys()].map(() => {
return factory.create('Comment')
}),
)
})
it('returns the count of all comments', async () => {
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
data: { statistics: { countComments: 2 } },
errors: undefined,
})
})
})
describe('countFollows', () => {
let users
beforeEach(async () => {
users = await Promise.all(
[...Array(2).keys()].map(() => {
return factory.create('User')
}),
)
await users[0].relateTo(users[1], 'following')
})
it('returns the count of all follows', async () => {
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
data: { statistics: { countFollows: 1 } },
errors: undefined,
})
})
})
describe('countShouts', () => {
let users, posts
beforeEach(async () => {
users = await Promise.all(
[...Array(2).keys()].map(() => {
return factory.create('User')
}),
)
posts = await Promise.all(
[...Array(3).keys()].map(() => {
return factory.create('Post')
}),
)
await Promise.all([
users[0].relateTo(posts[1], 'shouted'),
users[1].relateTo(posts[0], 'shouted'),
])
})
it('returns the count of all shouts', async () => {
await expect(query({ query: statisticsQuery })).resolves.toMatchObject({
data: { statistics: { countShouts: 2 } },
errors: undefined,
})
})
})
})

View File

@ -0,0 +1,110 @@
import { config, mount, RouterLinkStub } from '@vue/test-utils'
import Vuex from 'vuex'
import FiledTable from './FiledTable'
import { reports } from '~/components/_new/features/ReportsTable/ReportsTable.story.js'
const localVue = global.localVue
localVue.filter('truncate', string => string)
config.stubs['client-only'] = '<span><slot /></span>'
describe('FiledTable.vue', () => {
let wrapper, mocks, propsData, stubs, filed
const filedReport = reports[0]
beforeEach(() => {
mocks = {
$t: jest.fn(string => string),
}
stubs = {
NuxtLink: RouterLinkStub,
}
propsData = {}
})
describe('mount', () => {
const Wrapper = () => {
const store = new Vuex.Store({
getters: {
'auth/isModerator': () => true,
'auth/user': () => {
return { id: 'moderator' }
},
},
})
return mount(FiledTable, {
propsData,
mocks,
localVue,
store,
stubs,
})
}
beforeEach(() => {
wrapper = Wrapper()
})
describe('given reports', () => {
beforeEach(() => {
filed = reports.map(report => report.filed)
propsData.filed = filed[0]
wrapper = Wrapper()
})
it('renders a table', () => {
expect(wrapper.find('.ds-table').exists()).toBe(true)
})
describe('renders 4 columns', () => {
it('for icon', () => {
expect(wrapper.vm.fields.submitter).toBeTruthy()
})
it('for user', () => {
expect(wrapper.vm.fields.reportedOn).toBeTruthy()
})
it('for post', () => {
expect(wrapper.vm.fields.reasonCategory).toBeTruthy()
})
it('for content', () => {
expect(wrapper.vm.fields.reasonDescription).toBeTruthy()
})
})
describe('Filed', () => {
it('renders the reporting user', () => {
const communityModerator = wrapper.find('[data-test="community-moderator"]')
const username = communityModerator.find('.username')
expect(username.text()).toEqual('Community moderator')
})
it('renders the reported date', () => {
const dsTexts = wrapper.findAll('.ds-text')
const date = dsTexts.filter(element => element.text() === 'yesterday at 4:56 PM')
expect(date.exists()).toBe(true)
})
it.only('renders a link to the Post', () => {
const columns = wrapper.findAll('td')
const reasonCategory = columns.filter(category =>
category.text().includes('pornographic material'),
)
expect(reasonCategory.exists()).toBe(true)
})
it("renders the Post's content", () => {
const boldTags = secondRowNotification.findAll('b')
const content = boldTags.filter(
element => element.text() === commentNotification.from.contentExcerpt,
)
expect(content.exists()).toBe(true)
})
})
})
})
})

View File

@ -0,0 +1,28 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import FiledTable from '~/components/_new/features/FiledTable/FiledTable'
import helpers from '~/storybook/helpers'
import { reports } from '~/components/_new/features/ReportsTable/ReportsTable.story.js'
const filed = reports.map(report => report.filed)
storiesOf('FiledTable', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.add('with filed reports', () => ({
components: { FiledTable },
store: helpers.store,
data: () => ({
filed,
}),
template: `<table>
<tbody v-for="file in filed">
<tr>
<td class="ds-table-col filed-table" colspan="4">
<ds-space margin-bottom="base" />
<filed-table :filed="file" />
</td>
</tr>
</tbody>
</table>`,
}))