diff --git a/backend/src/graphql/authentications.ts b/backend/src/graphql/authentications.ts index 91605ec9f..53dcc9c92 100644 --- a/backend/src/graphql/authentications.ts +++ b/backend/src/graphql/authentications.ts @@ -20,6 +20,7 @@ export const signupVerificationMutation = gql` termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion ) { id + name slug } } diff --git a/backend/src/middleware/validation/validationMiddleware.spec.ts b/backend/src/middleware/validation/validationMiddleware.spec.ts index 2e1cd6fa7..1fbd365b6 100644 --- a/backend/src/middleware/validation/validationMiddleware.spec.ts +++ b/backend/src/middleware/validation/validationMiddleware.spec.ts @@ -3,6 +3,7 @@ import Factory, { cleanDatabase } from '../../db/factories' import { getNeode, getDriver } from '../../db/neo4j' import { createTestClient } from 'apollo-server-testing' import createServer from '../../server' +import { signupVerificationMutation } from '../../graphql/authentications' const neode = getNeode() const driver = getDriver() @@ -51,9 +52,10 @@ const reviewMutation = gql` } ` const updateUserMutation = gql` - mutation ($id: ID!, $name: String) { - UpdateUser(id: $id, name: $name) { + mutation ($id: ID!, $name: String, $slug: String) { + UpdateUser(id: $id, name: $name, slug: $slug) { name + slug } } ` @@ -132,175 +134,130 @@ afterEach(async () => { await cleanDatabase() }) -describe('validateCreateComment', () => { - let createCommentVariables - beforeEach(async () => { - createCommentVariables = { - postId: 'whatever', - content: '', - } - authenticatedUser = await commentingUser.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 +describe('validationMiddleware', () => { + describe('CreateComment', () => { + let createCommentVariables beforeEach(async () => { - await Factory.build( - 'comment', - { - id: 'comment-id', - }, - { - authorId: 'commenting-user', - }, - ) - updateCommentVariables = { - id: 'whatever', + createCommentVariables = { + postId: 'whatever', content: '', } authenticatedUser = await commentingUser.toJson() }) it('throws an error if content is empty', async () => { - updateCommentVariables = { ...updateCommentVariables, id: 'comment-id' } + createCommentVariables = { ...createCommentVariables, postId: 'post-4-commenting' } await expect( - mutate({ mutation: updateCommentMutation, variables: updateCommentVariables }), + mutate({ mutation: createCommentMutation, variables: createCommentVariables }), ).resolves.toMatchObject({ - data: { UpdateComment: null }, + 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 () => { - updateCommentVariables = { id: 'comment-id', content: '' } + createCommentVariables = { postId: 'post-4-commenting', content: '' } await expect( - mutate({ mutation: updateCommentMutation, variables: updateCommentVariables }), + mutate({ mutation: createCommentMutation, variables: createCommentVariables }), ).resolves.toMatchObject({ - data: { UpdateComment: null }, + data: { CreateComment: null }, errors: [{ message: 'Comment must be at least 1 character long!' }], }) }) - }) -}) -describe('validateReport', () => { - it('throws an error if a user tries to report themself', async () => { - authenticatedUser = await reportingUser.toJson() - reportVariables = { ...reportVariables, resourceId: 'reporting-user' } - await expect( - mutate({ mutation: reportMutation, variables: reportVariables }), - ).resolves.toMatchObject({ - data: { fileReport: null }, - errors: [{ message: 'You cannot report yourself!' }], - }) - }) -}) - -describe('validateReview', () => { - beforeEach(async () => { - const reportAgainstModerator = await Factory.build('report') - await Promise.all([ - reportAgainstModerator.relateTo(reportingUser, 'filed', { - ...reportVariables, - resourceId: 'moderating-user', - }), - reportAgainstModerator.relateTo(moderatingUser, 'belongsTo'), - ]) - authenticatedUser = await moderatingUser.toJson() - }) - - it('throws an error if a user tries to review a report against them', async () => { - disableVariables = { ...disableVariables, resourceId: 'moderating-user' } - await expect( - mutate({ mutation: reviewMutation, variables: disableVariables }), - ).resolves.toMatchObject({ - data: { review: null }, - errors: [{ message: 'You cannot review yourself!' }], - }) - }) - - it('throws an error for invaild resource', async () => { - disableVariables = { ...disableVariables, resourceId: 'non-existent-resource' } - await expect( - mutate({ mutation: reviewMutation, variables: disableVariables }), - ).resolves.toMatchObject({ - data: { review: null }, - errors: [{ message: 'Resource not found or is not a Post|Comment|User!' }], - }) - }) - - it('throws an error if no report exists', async () => { - disableVariables = { ...disableVariables, resourceId: 'offensive-post' } - await expect( - mutate({ mutation: reviewMutation, variables: disableVariables }), - ).resolves.toMatchObject({ - data: { review: null }, - errors: [{ message: 'Before starting the review process, please report the Post!' }], - }) - }) - - it('throws an error if a moderator tries to review their own resource(Post|Comment)', async () => { - const reportAgainstOffensivePost = await Factory.build('report') - await Promise.all([ - reportAgainstOffensivePost.relateTo(reportingUser, 'filed', { - ...reportVariables, - resourceId: 'offensive-post', - }), - reportAgainstOffensivePost.relateTo(offensivePost, 'belongsTo'), - ]) - disableVariables = { ...disableVariables, resourceId: 'offensive-post' } - await expect( - mutate({ mutation: reviewMutation, variables: disableVariables }), - ).resolves.toMatchObject({ - data: { review: null }, - errors: [{ message: 'You cannot review your own Post!' }], - }) - }) - - describe('moderate a resource that is not a (Comment|Post|User) ', () => { - beforeEach(async () => { - await Promise.all([Factory.build('tag', { id: 'tag-id' })]) - }) - - it('returns null', async () => { - disableVariables = { - ...disableVariables, - resourceId: 'tag-id', + 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('UpdateComment', () => { + let updateCommentVariables + beforeEach(async () => { + await Factory.build( + 'comment', + { + id: 'comment-id', + }, + { + authorId: 'commenting-user', + }, + ) + updateCommentVariables = { + id: 'whatever', + content: '', + } + authenticatedUser = await commentingUser.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('Report', () => { + it('throws an error if a user tries to report themself', async () => { + authenticatedUser = await reportingUser.toJson() + reportVariables = { ...reportVariables, resourceId: 'reporting-user' } + await expect( + mutate({ mutation: reportMutation, variables: reportVariables }), + ).resolves.toMatchObject({ + data: { fileReport: null }, + errors: [{ message: 'You cannot report yourself!' }], + }) + }) + }) + + describe('Review', () => { + beforeEach(async () => { + const reportAgainstModerator = await Factory.build('report') + await Promise.all([ + reportAgainstModerator.relateTo(reportingUser, 'filed', { + ...reportVariables, + resourceId: 'moderating-user', + }), + reportAgainstModerator.relateTo(moderatingUser, 'belongsTo'), + ]) + authenticatedUser = await moderatingUser.toJson() + }) + + it('throws an error if a user tries to review a report against them', async () => { + disableVariables = { ...disableVariables, resourceId: 'moderating-user' } + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: null }, + errors: [{ message: 'You cannot review yourself!' }], + }) + }) + + it('throws an error for invaild resource', async () => { + disableVariables = { ...disableVariables, resourceId: 'non-existent-resource' } await expect( mutate({ mutation: reviewMutation, variables: disableVariables }), ).resolves.toMatchObject({ @@ -308,9 +265,145 @@ describe('validateReview', () => { errors: [{ message: 'Resource not found or is not a Post|Comment|User!' }], }) }) + + it('throws an error if no report exists', async () => { + disableVariables = { ...disableVariables, resourceId: 'offensive-post' } + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: null }, + errors: [{ message: 'Before starting the review process, please report the Post!' }], + }) + }) + + it('throws an error if a moderator tries to review their own resource(Post|Comment)', async () => { + const reportAgainstOffensivePost = await Factory.build('report') + await Promise.all([ + reportAgainstOffensivePost.relateTo(reportingUser, 'filed', { + ...reportVariables, + resourceId: 'offensive-post', + }), + reportAgainstOffensivePost.relateTo(offensivePost, 'belongsTo'), + ]) + disableVariables = { ...disableVariables, resourceId: 'offensive-post' } + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: null }, + errors: [{ message: 'You cannot review your own Post!' }], + }) + }) + + describe('moderate a resource that is not a (Comment|Post|User) ', () => { + beforeEach(async () => { + await Promise.all([Factory.build('tag', { id: 'tag-id' })]) + }) + + it('returns null', async () => { + disableVariables = { + ...disableVariables, + resourceId: 'tag-id', + } + await expect( + mutate({ mutation: reviewMutation, variables: disableVariables }), + ).resolves.toMatchObject({ + data: { review: null }, + errors: [{ message: 'Resource not found or is not a Post|Comment|User!' }], + }) + }) + }) }) - describe('validateUpdateUser', () => { + describe('SignupVerification', () => { + let userParams, variables, updatingUser + + beforeEach(async () => { + userParams = { + id: 'updating-user', + name: 'John Doe', + } + + variables = { + id: 'updating-user', + name: 'John Doughnut', + password: '1234', + email: 'updating-user@example.org', + nonce: '12345', + termsAndConditionsAgreedVersion: '0.0.1', + } + await Factory.build('emailAddress', { + email: 'updating-user@example.org', + nonce: '12345', + verifiedAt: null, + }) + }) + + describe('with name', () => { + describe('is allowed', () => { + it('has success', async () => { + await expect( + mutate({ mutation: signupVerificationMutation, variables }), + ).resolves.toMatchObject({ + data: { + SignupVerification: { + name: 'John Doughnut', + id: expect.any(String), + slug: 'john-doughnut', + }, + }, + errors: undefined, + }) + }) + + it('throws an error', async () => { + variables = { + ...variables, + name: ' ', + } + await expect( + mutate({ mutation: signupVerificationMutation, variables }), + ).resolves.toMatchObject({ + data: { SignupVerification: null }, + errors: [{ message: 'User name must be at least 3 character long!' }], + }) + }) + }) + }) + + describe('with slug', () => { + describe('is allowed', () => { + it('has success', async () => { + variables = { + ...variables, + slug: 'superman', + } + await expect( + mutate({ mutation: signupVerificationMutation, variables }), + ).resolves.toMatchObject({ + data: { SignupVerification: { slug: 'superman' } }, + errors: undefined, + }) + }) + }) + + describe('"all" in blacklist', () => { + it('throws an error', async () => { + variables = { + ...variables, + slug: 'all', + } + await expect( + mutate({ mutation: signupVerificationMutation, variables }), + ).resolves.toMatchObject({ + data: { SignupVerification: null }, + errors: [{ message: 'User slug “all” must not be in blacklist!' }], + }) + }) + }) + }) + }) + + describe('UpdateUser', () => { let userParams, variables, updatingUser beforeEach(async () => { @@ -327,14 +420,53 @@ describe('validateReview', () => { authenticatedUser = await updatingUser.toJson() }) - it('with name too short', async () => { - variables = { - ...variables, - name: ' ', - } - await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({ - data: { UpdateUser: null }, - errors: [{ message: 'Username must be at least 3 character long!' }], + describe('with name', () => { + describe('is allowed', () => { + it('has success', async () => { + await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({ + data: { UpdateUser: { name: 'John Doughnut' } }, + errors: undefined, + }) + }) + + it('throws an error', async () => { + variables = { + ...variables, + name: ' ', + } + await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({ + data: { UpdateUser: null }, + errors: [{ message: 'User name must be at least 3 character long!' }], + }) + }) + }) + }) + + describe('with slug', () => { + describe('is allowed', () => { + it('has success', async () => { + variables = { + ...variables, + slug: 'superman', + } + await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({ + data: { UpdateUser: { slug: 'superman' } }, + errors: undefined, + }) + }) + }) + + describe('"all" in blacklist', () => { + it('throws an error', async () => { + variables = { + ...variables, + slug: 'all', + } + await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({ + data: { UpdateUser: null }, + errors: [{ message: 'User slug “all” must not be in blacklist!' }], + }) + }) }) }) }) diff --git a/backend/src/middleware/validation/validationMiddleware.ts b/backend/src/middleware/validation/validationMiddleware.ts index ff26f5ef1..429de6aab 100644 --- a/backend/src/middleware/validation/validationMiddleware.ts +++ b/backend/src/middleware/validation/validationMiddleware.ts @@ -3,6 +3,8 @@ import { UserInputError } from 'apollo-server' const COMMENT_MIN_LENGTH = 1 const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!' const USERNAME_MIN_LENGTH = 3 +const SLUG_BLACKLIST = ['all'] + const validateCreateComment = async (resolve, root, args, context, info) => { const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim() const { postId } = args @@ -111,10 +113,12 @@ export const validateNotifyUsers = async (label, reason) => { } } -const validateUpdateUser = async (resolve, root, params, context, info) => { - const { name } = params +const validateUser = async (resolve, root, params, context, info) => { + const { name, slug } = params if (typeof name === 'string' && name.trim().length < USERNAME_MIN_LENGTH) - throw new UserInputError(`Username must be at least ${USERNAME_MIN_LENGTH} character long!`) + throw new UserInputError(`User name must be at least ${USERNAME_MIN_LENGTH} character long!`) + if (typeof slug === 'string' && SLUG_BLACKLIST.find((blacklisted) => blacklisted === slug)) + throw new UserInputError(`User slug “${slug}” must not be in blacklist!`) return resolve(root, params, context, info) } @@ -122,7 +126,8 @@ export default { Mutation: { CreateComment: validateCreateComment, UpdateComment: validateUpdateComment, - UpdateUser: validateUpdateUser, + SignupVerification: validateUser, + UpdateUser: validateUser, fileReport: validateReport, review: validateReview, },