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

View File

@ -1,5 +1,5 @@
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'
export enum LoginActionKeys {
@ -44,7 +44,7 @@ export class LoginActions {
}
}
register = (profile: ICredentials):
register = (profile: INewUser):
IAction<IUser, LoginActionKeys.LOGIN_REGISTER> => {
return {
payload: this.http.post('/auth/register', profile),

View File

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

View File

@ -1,4 +1,5 @@
import {ICredentials} from './ICredentials'
import {INewUser} from './INewUser'
import {ITeam} from './ITeam'
import {IUserTeam} from './IUserTeam'
import {IUser} from './IUser'
@ -6,7 +7,7 @@ import {IUser} from './IUser'
export interface IAPIDef {
'/auth/register': {
'post': {
body: ICredentials
body: INewUser
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 {
id: number
username: string
firstName: string
lastName: string
}

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,8 @@ describe('UserService', () => {
return userService.createUser({
username: u,
password: p,
firstName: 'test',
lastName: 'test',
})
}
@ -23,7 +25,7 @@ describe('UserService', () => {
expect(result.id).toBeTruthy()
const user = await userService.findOne(result.id)
expect(user).toBeTruthy()
expect(user!.password).toBe(undefined)
expect(user).not.toHaveProperty('password')
})
it('throws when username is not an email', async () => {
@ -88,7 +90,7 @@ describe('UserService', () => {
await createUser()
const user = await userService
.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 {BaseService} from './BaseService'
import {ICredentials} from '@rondo/common'
import {ICredentials, INewUser, IUser} from '@rondo/common'
import {IUserService} from './IUserService'
import {UserEmail} from '../entities/UserEmail'
import {User} from '../entities/User'
import {compare, hash} from 'bcrypt'
import {validate as validateEmail} from 'email-validator'
import {Validator, trim} from '../validator'
const SALT_ROUNDS = 10
const MIN_PASSWORD_LENGTH = 10
export class UserService extends BaseService implements IUserService {
async createUser(payload: ICredentials): Promise<User> {
const username = payload.username
if (!validateEmail(username)) {
async createUser(payload: INewUser): Promise<IUser> {
const newUser = {
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')
}
if (payload.password.length < MIN_PASSWORD_LENGTH) {
throw createError(400,
`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 user = await this.getRepository(User).save({
...newUser,
password,
})
await this.getRepository(UserEmail).save({
email: username,
email: newUser.username,
userId: user.id,
})
return user
return {
id: user.id,
...newUser,
}
}
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) {
@ -71,6 +101,7 @@ export class UserService extends BaseService implements IUserService {
.createQueryBuilder('user')
.select('user')
.addSelect('user.password')
.addSelect('emails')
.innerJoin('user.emails', 'emails', 'emails.email = :email', {
email: username,
})
@ -78,8 +109,12 @@ export class UserService extends BaseService implements IUserService {
const isValid = await compare(password, user ? user.password! : '')
if (user && isValid) {
delete user.password
return user
return {
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)
.get(`${context}/app`)
.expect(200)
const cookie = response.header['set-cookie'] as string
const cookie = this.getCookies(response.header['set-cookie'])
const token = this.getCsrfToken(response.text)
expect(cookie).toBeTruthy()
expect(token).toBeTruthy()
@ -101,11 +101,17 @@ export class TestUtils<T extends IRoutes> {
const response = await supertest(this.app)
.post(`${context}/api/auth/register`)
.set('cookie', cookie)
.send(this.getLoginBody(token))
.send({
firstName: 'test',
lastName: 'test',
...this.getLoginBody(token),
})
.expect(200)
const cookies = this.getCookies(response.header['set-cookie'])
return {
cookie: response.header['set-cookie'] as string,
cookie: [cookies, cookie].join('; '),
userId: response.body.id,
token,
}
@ -121,7 +127,12 @@ export class TestUtils<T extends IRoutes> {
.send({username, password, _csrf: token})
.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 = '') => {
@ -130,4 +141,8 @@ export class TestUtils<T extends IRoutes> {
`${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 './ValidationError'
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()
}