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:
parent
6fb69e40df
commit
30a8c56119
@ -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 />
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
packages/common/src/INewUser.ts
Normal file
6
packages/common/src/INewUser.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import {ICredentials} from './ICredentials'
|
||||||
|
|
||||||
|
export interface INewUser extends ICredentials {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
@ -1,3 +1,6 @@
|
|||||||
export interface IUser {
|
export interface IUser {
|
||||||
id: number
|
id: number
|
||||||
|
username: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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') {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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('; ')
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
9
packages/server/src/validator/trim.test.ts
Normal file
9
packages/server/src/validator/trim.test.ts
Normal 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('')
|
||||||
|
})
|
||||||
|
})
|
||||||
6
packages/server/src/validator/trim.ts
Normal file
6
packages/server/src/validator/trim.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export function trim(str?: string) {
|
||||||
|
if (!str) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return str.trim()
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user