Skip to content

Backend Signup Functionality #170

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions graphql/resolvers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { login, logout } from '../helpers/controllers/authController'
import { login, logout, signup } from '../helpers/controllers/authController'
import db from '../helpers/dbload'

const { Lesson, User } = db
Expand All @@ -23,6 +23,7 @@ export default {

Mutation: {
login,
logout
logout,
signup
}
}
7 changes: 7 additions & 0 deletions graphql/typeDefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ export default gql`
type Mutation {
login(username: String!, password: String!): AuthResponse
logout: AuthResponse
signup(
firstName: String!
lastName: String!
email: String!
username: String!
password: String
): AuthResponse
}

type AuthResponse {
Expand Down
79 changes: 76 additions & 3 deletions helpers/controllers/authController.test.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
jest.mock('bcrypt')
jest.mock('../dbload')
jest.mock('../mattermost')
import bcrypt from 'bcrypt'
import db from '../dbload'
import { login, logout } from './authController'
import { login, logout, signup } from './authController'
import { chatSignUp } from '../mattermost'

import { ApolloError } from 'apollo-server-micro'

describe('auth controller', () => {
let userArgs
beforeEach(() => {
jest.clearAllMocks()
userArgs = { username: 'testuser', password: 'c0d3reallyhard' }
userArgs = {
username: 'testuser',
password: 'c0d3reallyhard',
firstName: 'testuser1',
lastName: 'testuser1',
email: 'testuser@c0d3.com'
}
})

test('Login - should throw error when session is null', async () => {
expect(
return expect(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is return needed here now? Just asking for my own knowledge. Thanks!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're testing an async function and you're not using await or return then the test won't fail if it resolves instead of rejecting.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably explains why I couldn't test something awhile back to see if it was throwing an error! lol

login({}, userArgs, { req: { session: null } })
).rejects.toThrowError('')
})
Expand Down Expand Up @@ -81,4 +91,67 @@ describe('auth controller', () => {
success: true
})
})

test('Signup - should reject if user information is incomplete', async () => {
return expect(
signup({}, {}, { req: { session: {} } })
).rejects.toThrowError('Register form is not completely filled out')
})

test('Signup - should reject if user already exists', async () => {
db.User.findOne = jest.fn().mockReturnValue({ username: 'c0d3user' })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not return username: 'testuser' to match userArgs?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess since you mock it the return value in itself doesn't matter as long as something is returned?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't matter, it just have to return some user. A nitpick would be to return the same username though, it'll make prevent questions like this in the future.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userArgs is setup to be a valid entry for signup. In this case, I wanted to make sure it would error out so all the fields aren't filled out.

return expect(
signup({}, userArgs, { req: { session: {} } })
).rejects.toThrowError('User already exists')
})

test('Signup - should reject on second findOne. The first request checks for username, second request checks for email', async () => {
db.User.findOne = jest
.fn()
.mockReturnValueOnce(null)
.mockReturnValue({ username: 'c0d3user' }) // Second call for User.findOne checks for email
return expect(
signup({}, userArgs, { req: { session: {} } })
).rejects.toThrowError('Email already exists')
})

test('Signup - should not create user if chat signup responds with 401 or 403', async () => {
db.User.findOne = jest.fn().mockReturnValue(null)
db.User.create = jest.fn()

chatSignUp.mockRejectedValue(
'Invalid or missing parameter in mattermost request'
)

await expect(
signup({}, userArgs, { req: { session: {} } })
).rejects.toThrowError('Invalid or missing parameter in mattermost request')

expect(db.User.create).not.toBeCalled()
})

test('Signup - should not create user if chat signup throws an error', async () => {
db.User.findOne = jest.fn().mockReturnValue(null)
db.User.create = jest.fn()

chatSignUp.mockRejectedValueOnce('Mattermost Error')

await expect(
signup({}, userArgs, { req: { session: {} } })
).rejects.toThrow('Mattermost Error')

expect(db.User.create).not.toBeCalled()
})

test('Signup - should resolve with success true if signup successful ', async () => {
db.User.findOne = jest.fn().mockReturnValue(null)
db.User.create = jest.fn().mockReturnValue({ username: 'user' })
chatSignUp.mockResolvedValueOnce({
success: true
})
const result = await signup({}, userArgs, { req: { session: {} } })
expect(result).toEqual({
username: 'user'
})
})
})
79 changes: 78 additions & 1 deletion helpers/controllers/authController.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import db from '../dbload'
import bcrypt from 'bcrypt'
import { nanoid } from 'nanoid'
import { Request } from 'express'
import { UserInputError } from 'apollo-server-micro'
import { signupValidation } from '../formValidation'
import { chatSignUp } from '../mattermost'

const { User } = db

type Login = {
username: string
password: string
}

type SignUp = {
firstName: string
lastName: string
username: string
password: string
email: string
}

export const login = async (
_parent: void,
arg: { username: string; password: string },
arg: Login,
ctx: { req: Request }
) => {
const {
Expand Down Expand Up @@ -68,3 +85,63 @@ export const logout = async (_parent: void, _: void, ctx: { req: Request }) => {
})
})
}

export const signup = async (_parent: void, arg: SignUp) => {
try {
const { firstName, lastName, username, password, email } = arg

const validEntry = await signupValidation.isValid({
firstName,
lastName,
username,
password,
email
})

if (!validEntry) {
throw new UserInputError('Register form is not completely filled out')
}

// Check for existing user or email
const existingUser = await User.findOne({
where: {
username
}
})

if (existingUser) {
throw new UserInputError('User already exists')
}

const existingEmail = await User.findOne({
where: {
email
}
})

if (existingEmail) {
throw new UserInputError('Email already exists')
}

const randomToken = nanoid()
const name = `${firstName} ${lastName}`
const hash = await bcrypt.hash(password, 10)

// Chat Signup
await chatSignUp(username, password, email)

const userRecord = User.create({
name,
username,
password: hash,
email,
emailVerificationToken: randomToken
})

return {
username: userRecord.username
}
} catch (err) {
throw new Error(err)
}
}
51 changes: 51 additions & 0 deletions helpers/mattermost.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
jest.mock('node-fetch')
import fetch from 'node-fetch'
import { chatSignUp } from './mattermost'

describe('Chat Signup', () => {
let userArgs

beforeEach(() => {
jest.clearAllMocks()
userArgs = {
username: 'testuser',
password: 'c0d3reallyhard',
email: 'testuser@c0d3.com'
}
})

test('ChatSignUp - should resolve if response is 201', async () => {
fetch.mockResolvedValue({ status: 201 })
return expect(
chatSignUp(userArgs.username, userArgs.password, userArgs.email)
).resolves.toEqual({ success: true })
})

test('ChatSignUp - should reject with invalid parameter if response is 401', async () => {
fetch.mockResolvedValue({ status: 401 })
return expect(
chatSignUp(userArgs.username, userArgs.password, userArgs.email)
).rejects.toThrowError('Invalid or missing parameter in mattermost request')
})

test('ChatSignUp - should reject with invalid permission if response is 403', async () => {
fetch.mockResolvedValue({ status: 403 })
return expect(
chatSignUp(userArgs.username, userArgs.password, userArgs.email)
).rejects.toThrowError('Invalid permission')
})

test('ChatSignUp - should reject if response status is invalid', async () => {
fetch.mockResolvedValue({ status: 418 }) // MatterMost only returns 201, 401 and 403 LOL teapot
return expect(
chatSignUp(userArgs.username, userArgs.password, userArgs.email)
).rejects.toThrowError('Unexpected Response')
})

test('ChatSignUp - should reject if internal server error', async () => {
fetch.mockRejectedValue('')
return expect(
chatSignUp(userArgs.username, userArgs.password, userArgs.email)
).rejects.toThrowError('Internal Server Error')
})
})
39 changes: 39 additions & 0 deletions helpers/mattermost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import fetch from 'node-fetch'
const accessToken = process.env.MATTERMOST_ACCESS_TOKEN
const chatServiceUrl = process.env.CHAT_URL

const headers = {
Authorization: `Bearer ${accessToken}`
}

export const chatSignUp = async (
username: string,
password: string,
email: string
) => {
try {
const response = await fetch(`${chatServiceUrl}/users`, {
method: 'POST',
headers,
body: JSON.stringify({
username,
email,
password
})
})

if (response.status === 201) {
return { success: true }
}
if (response.status === 401) {
throw new Error('Invalid or missing parameter in mattermost request')
}
if (response.status === 403) {
throw new Error('Invalid permission')
}

throw new Error('Unexpected Response') // Mattermost will only respond with 201, 401 and 403
} catch (err) {
throw new Error(err || 'Internal Server Error')
}
}
3 changes: 3 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
const withSass = require('@zeit/next-sass')
module.exports = withSass({
env: {
CHAT_URL: process.env.CHAT_URL || 'https://mattermost.devwong.com/api/v4',
DB_NAME: process.env.DB_NAME || 'c0d3dev',
DB_USER: process.env.DB_USER || 'herman',
DB_PW: process.env.DB_PW || 'letmein2',
DB_HOST: process.env.DB_HOST || 'devwong.com',
MATTERMOST_ACCESS_TOKEN:
process.env.MATTERMOST_ACCESS_TOKEN || 'c1eh9rc1cinpbf9mk1wucjyqzw',
SESSION_SECRET: process.env.SESSION_SECRET || 'c0d3hard3r',
SERVER_URL: process.env.SERVER_URL || 'https://backend.c0d3.com'
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"isomorphic-unfetch": "3.0.0",
"lodash": "^4.17.15",
"markdown-to-jsx": "^6.11.1",
"nanoid": "^3.1.3",
"next": "^9.3.2",
"next-connect": "^0.6.3",
"next-with-apollo": "^5.0.0",
Expand Down Expand Up @@ -100,4 +101,4 @@
"pre-push": "npm run lint && npm run test && npm run type-check"
}
}
}
}
Loading