Fix test for User firstName & lastName

Also fix CSRF token. This was probably broken since csurf middleware was
modified to use cookie instead of session storage to provide support for
single page app (SPA).
This commit is contained in:
Jerko Steiner 2019-03-18 15:53:01 +05:00
parent 6fb69e40df
commit 30a8c56119
16 changed files with 134 additions and 27 deletions

View File

@ -11,6 +11,7 @@ export interface IInputProps {
readOnly?: boolean readOnly?: boolean
label: string label: string
Icon?: IconType Icon?: IconType
required?: boolean
} }
export class Input extends React.PureComponent<IInputProps> { export class Input extends React.PureComponent<IInputProps> {
@ -33,6 +34,7 @@ export class Input extends React.PureComponent<IInputProps> {
onChange={this.handleChange} onChange={this.handleChange}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
readOnly={!!this.props.readOnly} readOnly={!!this.props.readOnly}
required={this.props.required}
/> />
{Icon && <span className='icon is-left is-small'> {Icon && <span className='icon is-left is-small'>
<Icon /> <Icon />

View File

@ -1,5 +1,5 @@
import {IAction, IErrorAction, ActionTypes} from '../actions' import {IAction, IErrorAction, ActionTypes} from '../actions'
import {IAPIDef, ICredentials, IUser} from '@rondo/common' import {IAPIDef, ICredentials, INewUser, IUser} from '@rondo/common'
import {IHTTPClient} from '../http/IHTTPClient' import {IHTTPClient} from '../http/IHTTPClient'
export enum LoginActionKeys { export enum LoginActionKeys {
@ -44,7 +44,7 @@ export class LoginActions {
} }
} }
register = (profile: ICredentials): register = (profile: INewUser):
IAction<IUser, LoginActionKeys.LOGIN_REGISTER> => { IAction<IUser, LoginActionKeys.LOGIN_REGISTER> => {
return { return {
payload: this.http.post('/auth/register', profile), payload: this.http.post('/auth/register', profile),

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {ICredentials, IUser} from '@rondo/common' import {INewUser, IUser} from '@rondo/common'
import {Input} from '../components/Input' import {Input} from '../components/Input'
import {Redirect} from '../components/Redirect' import {Redirect} from '../components/Redirect'
@ -7,7 +7,7 @@ export interface IRegisterFormProps {
error?: string error?: string
onSubmit: () => void onSubmit: () => void
onChange: (name: string, value: string) => void onChange: (name: string, value: string) => void
data: ICredentials data: INewUser
user?: IUser user?: IUser
redirectTo: string redirectTo: string
} }
@ -21,12 +21,13 @@ export class RegisterForm extends React.PureComponent<IRegisterFormProps> {
<form onSubmit={this.props.onSubmit}> <form onSubmit={this.props.onSubmit}>
<p className='error'>{this.props.error}</p> <p className='error'>{this.props.error}</p>
<Input <Input
label='Username' label='Email'
name='username' name='username'
type='email' type='email'
onChange={this.props.onChange} onChange={this.props.onChange}
value={this.props.data.username} value={this.props.data.username}
placeholder='Email' placeholder='Email'
required
/> />
<Input <Input
label='Password' label='Password'
@ -35,6 +36,25 @@ export class RegisterForm extends React.PureComponent<IRegisterFormProps> {
onChange={this.props.onChange} onChange={this.props.onChange}
value={this.props.data.password} value={this.props.data.password}
placeholder='Password' placeholder='Password'
required
/>
<Input
label='First Name'
name='firstName'
type='text'
onChange={this.props.onChange}
value={this.props.data.firstName}
placeholder='First name'
required
/>
<Input
label='Last Name'
name='lastName'
type='text'
onChange={this.props.onChange}
value={this.props.data.lastName}
placeholder='First name'
required
/> />
<input <input
className='button is-primary' className='button is-primary'

View File

@ -1,4 +1,5 @@
import {ICredentials} from './ICredentials' import {ICredentials} from './ICredentials'
import {INewUser} from './INewUser'
import {ITeam} from './ITeam' import {ITeam} from './ITeam'
import {IUserTeam} from './IUserTeam' import {IUserTeam} from './IUserTeam'
import {IUser} from './IUser' import {IUser} from './IUser'
@ -6,7 +7,7 @@ import {IUser} from './IUser'
export interface IAPIDef { export interface IAPIDef {
'/auth/register': { '/auth/register': {
'post': { 'post': {
body: ICredentials body: INewUser
response: IUser response: IUser
} }
} }

View File

@ -0,0 +1,6 @@
import {ICredentials} from './ICredentials'
export interface INewUser extends ICredentials {
firstName: string
lastName: string
}

View File

@ -1,3 +1,6 @@
export interface IUser { export interface IUser {
id: number id: number
username: string
firstName: string
lastName: string
} }

View File

@ -1,5 +1,6 @@
export * from './IAPIDef' export * from './IAPIDef'
export * from './ICredentials' export * from './ICredentials'
export * from './INewUser'
export * from './IRequestParams' export * from './IRequestParams'
export * from './IRole' export * from './IRole'
export * from './IRoutes' export * from './IRoutes'

View File

@ -13,17 +13,22 @@ describe('passport.promise', () => {
beforeEach(() => { beforeEach(() => {
app = express() app = express()
const userInfo = {
username: 'test@user.com',
firstName: 'test',
lastName: 'test',
}
const userService = new (class implements IUserService { const userService = new (class implements IUserService {
async createUser() { async createUser() {
return {id: 1} return {id: 1, ...userInfo}
} }
async changePassword() {/* empty */} async changePassword() {/* empty */}
async findOne(id: number) { async findOne(id: number) {
return {id} return {id, ...userInfo}
} }
async validateCredentials({username, password}: ICredentials) { async validateCredentials({username, password}: ICredentials) {
if (username === 'test' && password === 'pass') { if (username === 'test' && password === 'pass') {
return {id: 1} return {id: 1, ...userInfo}
return return
} }
if (username === 'error') { if (username === 'error') {

View File

@ -18,6 +18,8 @@ export class LoginRoutes extends BaseRoute<IAPIDef> {
const user = await this.userService.createUser({ const user = await this.userService.createUser({
username: req.body.username, username: req.body.username,
password: req.body.password, password: req.body.password,
firstName: req.body.firstName,
lastName: req.body.lastName,
}) })
await req.logInPromise(user) await req.logInPromise(user)
return user return user

View File

@ -1,8 +1,7 @@
import {ICredentials} from '@rondo/common' import {ICredentials, INewUser, IUser} from '@rondo/common'
import {IUser} from '@rondo/common'
export interface IUserService { export interface IUserService {
createUser(credentials: ICredentials): Promise<IUser> createUser(credentials: INewUser): Promise<IUser>
changePassword(params: { changePassword(params: {
userId: number, userId: number,
oldPassword: string, oldPassword: string,

View File

@ -14,6 +14,8 @@ describe('UserService', () => {
return userService.createUser({ return userService.createUser({
username: u, username: u,
password: p, password: p,
firstName: 'test',
lastName: 'test',
}) })
} }
@ -23,7 +25,7 @@ describe('UserService', () => {
expect(result.id).toBeTruthy() expect(result.id).toBeTruthy()
const user = await userService.findOne(result.id) const user = await userService.findOne(result.id)
expect(user).toBeTruthy() expect(user).toBeTruthy()
expect(user!.password).toBe(undefined) expect(user).not.toHaveProperty('password')
}) })
it('throws when username is not an email', async () => { it('throws when username is not an email', async () => {
@ -88,7 +90,7 @@ describe('UserService', () => {
await createUser() await createUser()
const user = await userService const user = await userService
.validateCredentials({ username, password }) .validateCredentials({ username, password })
expect(user!.password).toBe(undefined) expect(user).not.toHaveProperty('password')
}) })
}) })

View File

@ -1,38 +1,68 @@
import createError from 'http-errors' import createError from 'http-errors'
import {BaseService} from './BaseService' import {BaseService} from './BaseService'
import {ICredentials} from '@rondo/common' import {ICredentials, INewUser, IUser} from '@rondo/common'
import {IUserService} from './IUserService' import {IUserService} from './IUserService'
import {UserEmail} from '../entities/UserEmail' import {UserEmail} from '../entities/UserEmail'
import {User} from '../entities/User' import {User} from '../entities/User'
import {compare, hash} from 'bcrypt' import {compare, hash} from 'bcrypt'
import {validate as validateEmail} from 'email-validator' import {validate as validateEmail} from 'email-validator'
import {Validator, trim} from '../validator'
const SALT_ROUNDS = 10 const SALT_ROUNDS = 10
const MIN_PASSWORD_LENGTH = 10 const MIN_PASSWORD_LENGTH = 10
export class UserService extends BaseService implements IUserService { export class UserService extends BaseService implements IUserService {
async createUser(payload: ICredentials): Promise<User> { async createUser(payload: INewUser): Promise<IUser> {
const username = payload.username const newUser = {
if (!validateEmail(username)) { username: trim(payload.username),
firstName: trim(payload.firstName),
lastName: trim(payload.lastName),
}
if (!validateEmail(newUser.username)) {
throw createError(400, 'Username is not a valid e-mail') throw createError(400, 'Username is not a valid e-mail')
} }
if (payload.password.length < MIN_PASSWORD_LENGTH) { if (payload.password.length < MIN_PASSWORD_LENGTH) {
throw createError(400, throw createError(400,
`Password must be at least ${MIN_PASSWORD_LENGTH} characters long`) `Password must be at least ${MIN_PASSWORD_LENGTH} characters long`)
} }
new Validator(newUser)
.ensure('username')
.ensure('firstName')
.ensure('lastName')
.throw()
const password = await this.hash(payload.password) const password = await this.hash(payload.password)
const user = await this.getRepository(User).save({ const user = await this.getRepository(User).save({
...newUser,
password, password,
}) })
await this.getRepository(UserEmail).save({ await this.getRepository(UserEmail).save({
email: username, email: newUser.username,
userId: user.id, userId: user.id,
}) })
return user return {
id: user.id,
...newUser,
}
} }
async findOne(id: number) { async findOne(id: number) {
return this.getRepository(User).findOne(id) const user = await this.getRepository(User).findOne(id, {
relations: ['emails'],
})
if (!user) {
return undefined
}
return {
id: user.id,
username: user.emails[0] ? user.emails[0].email : '',
firstName: user.firstName,
lastName: user.lastName,
}
} }
async findUserByEmail(email: string) { async findUserByEmail(email: string) {
@ -71,6 +101,7 @@ export class UserService extends BaseService implements IUserService {
.createQueryBuilder('user') .createQueryBuilder('user')
.select('user') .select('user')
.addSelect('user.password') .addSelect('user.password')
.addSelect('emails')
.innerJoin('user.emails', 'emails', 'emails.email = :email', { .innerJoin('user.emails', 'emails', 'emails.email = :email', {
email: username, email: username,
}) })
@ -78,8 +109,12 @@ export class UserService extends BaseService implements IUserService {
const isValid = await compare(password, user ? user.password! : '') const isValid = await compare(password, user ? user.password! : '')
if (user && isValid) { if (user && isValid) {
delete user.password return {
return user id: user.id,
username: user.emails[0].email,
firstName: user.firstName,
lastName: user.lastName,
}
} }
} }

View File

@ -75,7 +75,7 @@ export class TestUtils<T extends IRoutes> {
const response = await supertest(this.app) const response = await supertest(this.app)
.get(`${context}/app`) .get(`${context}/app`)
.expect(200) .expect(200)
const cookie = response.header['set-cookie'] as string const cookie = this.getCookies(response.header['set-cookie'])
const token = this.getCsrfToken(response.text) const token = this.getCsrfToken(response.text)
expect(cookie).toBeTruthy() expect(cookie).toBeTruthy()
expect(token).toBeTruthy() expect(token).toBeTruthy()
@ -101,11 +101,17 @@ export class TestUtils<T extends IRoutes> {
const response = await supertest(this.app) const response = await supertest(this.app)
.post(`${context}/api/auth/register`) .post(`${context}/api/auth/register`)
.set('cookie', cookie) .set('cookie', cookie)
.send(this.getLoginBody(token)) .send({
firstName: 'test',
lastName: 'test',
...this.getLoginBody(token),
})
.expect(200) .expect(200)
const cookies = this.getCookies(response.header['set-cookie'])
return { return {
cookie: response.header['set-cookie'] as string, cookie: [cookies, cookie].join('; '),
userId: response.body.id, userId: response.body.id,
token, token,
} }
@ -121,7 +127,12 @@ export class TestUtils<T extends IRoutes> {
.send({username, password, _csrf: token}) .send({username, password, _csrf: token})
.expect(200) .expect(200)
return {cookie: response.header['set-cookie'] as string, token} const cookies = this.getCookies(response.header['set-cookie'])
return {
cookie: [cookies, cookie].join('; '),
token,
}
} }
request = (baseUrl = '') => { request = (baseUrl = '') => {
@ -130,4 +141,8 @@ export class TestUtils<T extends IRoutes> {
`${this.bootstrap.config.app.baseUrl.path!}${baseUrl}`) `${this.bootstrap.config.app.baseUrl.path!}${baseUrl}`)
} }
private getCookies(setCookiesString: string[]): string {
return setCookiesString.map(c => c.split('; ')[0]).join('; ')
}
} }

View File

@ -1,3 +1,4 @@
export * from './IValidationMessage' export * from './IValidationMessage'
export * from './ValidationError' export * from './ValidationError'
export * from './Validator' export * from './Validator'
export * from './trim'

View File

@ -0,0 +1,9 @@
import {trim} from './trim'
describe('trim', () => {
it('trims string', () => {
expect(trim('test')).toEqual('test')
expect(trim(' test ')).toEqual('test')
expect(trim(undefined)).toEqual('')
})
})

View File

@ -0,0 +1,6 @@
export function trim(str?: string) {
if (!str) {
return ''
}
return str.trim()
}