diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index 024e594bf..4954a5584 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -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, diff --git a/backend/src/middleware/validation/validationMiddleware.spec.js b/backend/src/middleware/validation/validationMiddleware.spec.js new file mode 100644 index 000000000..ec5f3e012 --- /dev/null +++ b/backend/src/middleware/validation/validationMiddleware.spec.js @@ -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: '' } + 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: '' } + 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' }, + ], + }) + }) + }) + }) +}) diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js index c0f86ffe3..d2692aa8a 100644 --- a/backend/src/schema/resolvers/comments.spec.js +++ b/backend/src/schema/resolvers/comments.spec.js @@ -111,42 +111,6 @@ describe('CreateComment', () => { }, ) }) - - describe('comment content is empty', () => { - beforeEach(() => { - variables = { ...variables, content: '

' } - }) - - 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: '

' } - }) - - 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: '

' } - }) - - 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' } diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index d6a97191d..98475b182 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -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', - ) - }) - }) }) }) diff --git a/backend/src/schema/resolvers/statistics.js b/backend/src/schema/resolvers/statistics.js index e8745efea..07b9e4cea 100644 --- a/backend/src/schema/resolvers/statistics.js +++ b/backend/src/schema/resolvers/statistics.js @@ -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 { diff --git a/backend/src/schema/resolvers/statistics.spec.js b/backend/src/schema/resolvers/statistics.spec.js new file mode 100644 index 000000000..7ffa8ebd0 --- /dev/null +++ b/backend/src/schema/resolvers/statistics.spec.js @@ -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, + }) + }) + }) +}) diff --git a/webapp/components/_new/features/FiledTable/FiledTable.spec.js b/webapp/components/_new/features/FiledTable/FiledTable.spec.js new file mode 100644 index 000000000..6c923e8c9 --- /dev/null +++ b/webapp/components/_new/features/FiledTable/FiledTable.spec.js @@ -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'] = '' + +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) + }) + }) + }) + }) +}) diff --git a/webapp/components/_new/features/FiledTable/FiledTable.story.js b/webapp/components/_new/features/FiledTable/FiledTable.story.js new file mode 100644 index 000000000..af796d3d6 --- /dev/null +++ b/webapp/components/_new/features/FiledTable/FiledTable.story.js @@ -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: ` + + + + + +
+ + +
`, + }))