1
Fork 0

typescript(lunargame/api): overall better tests (except for the validate ones)

Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
Mateiadrielrafael 2019-08-13 23:58:20 +03:00 committed by prescientmoon
parent ffe0ad26ea
commit 48edeb1093
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
16 changed files with 255 additions and 99 deletions

View file

@ -20,6 +20,7 @@
},
"rules": {
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/no-object-literal-type-assertion": 0
"@typescript-eslint/no-object-literal-type-assertion": 0,
"@typescript-eslint/no-parameter-properties": 0
}
}

View file

@ -1,4 +1,5 @@
node_modules
.env
test/db.sqlite
db/db.sqlite
db/db.sqlite
coverage

View file

@ -5,5 +5,7 @@ module.exports = {
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testEnvironment: 'node'
testEnvironment: 'node',
collectCoverage: true,
coverageReporters: ['json', 'html']
}

View file

@ -0,0 +1,62 @@
import Joi from 'joi'
import { validate, validationField } from './validate'
import { Context } from 'koa'
import { fakeNext } from '../../../../test/utils/fakeNext'
describe('The validate middleware', () => {
const schema = Joi.object({
name: Joi.required()
})
const fields: validationField[] = ['body', 'params', 'query']
for (const field of fields) {
describe(`The request ${field} validator`, () => {
const middleware = validate(schema, field)
const getContext = (name?: number) => {
if (field === 'body') {
return {
request: {
body: {
name
}
}
} as Context
} else {
return {
[field]: {
name
}
} as Context
}
}
test('should throw an error if the validation fails', () => {
// arrange
const context = getContext()
// act
const check = () => {
middleware(context, fakeNext())
}
// assert
expect(check).toThrow()
})
test('should call next if the validation passed', () => {
// arrange
const context = getContext(7)
const next = jest.fn(fakeNext())
// act
middleware(context, next)
// assert
expect(next).toBeCalled()
})
})
}
})

View file

@ -2,6 +2,11 @@ import { ObjectSchema } from 'joi'
import { Middleware } from 'koa'
import { HttpError } from '../../../modules/network/classes/HttpError'
/**
* The field wich the validate validator can use
*/
export type validationField = 'params' | 'body' | 'query'
/**
* Middlware to validate a joi schema against a request
*
@ -10,15 +15,17 @@ import { HttpError } from '../../../modules/network/classes/HttpError'
*
* @throws HttpError if the validation fails
*/
export const validate = (
schema: ObjectSchema,
field: 'params' | 'body' | 'query'
): Middleware => async (context, next) => {
const result = schema.validate(
field === 'body' ? context.request.body : context[field]
)
export const validate = (schema: ObjectSchema, field: validationField): Middleware => (
context,
next
) => {
const result = schema.validate(field === 'body' ? context.request.body : context[field], {
abortEarly: true
})
if (result.error) throw new HttpError(400, result.error.message)
if (result.error !== null) {
throw new HttpError(422, result.error.message)
}
return next()
}

View file

@ -1,20 +1,59 @@
import { checkPassword } from './checkPassword'
import { passwordEncryption } from '../types/passwordEncryption'
import { hash, genSalt } from 'bcryptjs'
describe('The checkPassword helper', () => {
const pass = 'this is a test password'
test("should throw an error if the encryption method doesn't exist", () => {
expect(() => {
checkPassword(pass, pass, '12212' as passwordEncryption)
}).toThrow()
test("should throw an error if the encryption method doesn't exist", async () => {
// arrange
const check = checkPassword(pass, pass, '12212' as passwordEncryption)
// assert
await expect(check).rejects.toThrow()
})
test('should return true if the password matches the hash and the encryption = plain', () => {
expect(checkPassword(pass, pass, 'plain')).toBe(true)
describe("The 'plain' encryption", () => {
test('should return true if the password is correct', async () => {
// act
const check = await checkPassword(pass, pass, 'plain')
// assert
expect(check).toBe(true)
})
test('shoud return false if the password is wrong', async () => {
// act
const check = await checkPassword(pass, pass + 'something', 'plain')
// assert
expect(check).toBe(false)
})
})
test('shoud return false if the password is wrong and the encryption = plain', () => {
expect(checkPassword(pass, pass + 'something', 'plain')).toBe(false)
describe("The 'bcrypt' encryption", () => {
let passwordHash: string
beforeEach(async () => {
const salt = await genSalt(3)
passwordHash = await hash(pass, salt)
})
test('should return true if the password is correct', async () => {
// act
const check = await checkPassword(passwordHash, pass, 'bcrypt')
// assert
expect(check).toBe(true)
})
test('shoud return false if the password is wrong', async () => {
// act
const check = await checkPassword(passwordHash, pass + 'something', 'bcrypt')
// assert
expect(check).toBe(false)
})
})
})

View file

@ -1,5 +1,6 @@
import { passwordEncryption } from '../types/passwordEncryption'
import { HttpError } from '../../network/classes/HttpError'
import { compare } from 'bcryptjs'
/**
* Comparesa apssword with it's hash
@ -8,13 +9,15 @@ import { HttpError } from '../../network/classes/HttpError'
* @param password The actual password
* @param encryption The encription of the password
*/
export const checkPassword = (
export const checkPassword = async (
hash: string,
password: string,
encryption: passwordEncryption = 'plain'
) => {
if (encryption === 'plain') {
return hash === password
} else if (encryption === 'bcrypt') {
return await compare(password, hash)
} else {
throw new HttpError(400, `Encription ${encryption} doesn't exist`)
}

View file

@ -4,19 +4,25 @@ import { compare } from 'bcryptjs'
describe('The encryptPassword helper', () => {
test("should return the same password if the method is 'plain'", async () => {
// arrange
const password = internet.password()
// act
const hash = await encryptPassword(password, 'plain')
// assert
expect(hash).toBe(password)
})
test("should return a mactching hash if the method is 'bcrypt'", async () => {
// arrange
const password = internet.password()
// the amount of rounds is small because this is just a test
const hash = await encryptPassword(password, 'bcrypt', 3)
// act
const match = await compare(password, hash)
// assert
expect(match).toBe(true)
})
})

View file

@ -1,28 +1,35 @@
import { Context } from 'koa'
import { isUnauthorized } from './isUnauthorized'
describe('The isUnauthorized middleware', () => {
const fakeNext = () => async () => {}
import { requireAnonymous } from './requireAnonymous'
import { fakeNext } from '../../../../test/utils/fakeNext'
describe('The requireAnonymous middleware', () => {
test('should throw an error if the user is logged in', () => {
// act
const fakeContext = ({
session: {
uid: 7
}
} as unknown) as Context
expect(() => isUnauthorized()(fakeContext, fakeNext())).toThrow()
// arrange
const runMiddleware = () => requireAnonymous()(fakeContext, fakeNext())
// assert
expect(runMiddleware).toThrow()
})
test("should call next if the user isn't logged in", () => {
// arrange
const fakeContext = {
session: {}
} as Context
const next = jest.fn(fakeNext())
isUnauthorized()(fakeContext, next)
// act
requireAnonymous()(fakeContext, next)
// assert
expect(next).toBeCalled()
})
})

View file

@ -4,7 +4,7 @@ import { HttpError } from '../../network/classes/HttpError'
/**
* Middleware wich throws an error if the user is logged in
*/
export const isUnauthorized = (): Middleware => (context, next) => {
export const requireAnonymous = (): Middleware => (context, next) => {
if (context.session.uid === undefined) {
return next()
} else {

View file

@ -1,18 +1,23 @@
import { isAuthorized } from './isAuthorized'
import { requireAuthenticated } from './requireAuthenticated'
import { Context } from 'koa'
import { fakeNext } from '../../../../test/utils/fakeNext'
describe('The isAuthorized middleware', () => {
const fakeNext = () => async () => {}
describe('The requireAuthenticated middleware', () => {
test("should throw an error if the user isn't logged in", () => {
// arrange
const fakeContext = {
session: {}
} as Context
expect(() => isAuthorized()(fakeContext, fakeNext())).toThrow()
// arrange
const runMiddleware = () => requireAuthenticated()(fakeContext, fakeNext())
// assert
expect(runMiddleware).toThrow()
})
test('should call next if the user is logged in', () => {
// arrange
const fakeContext = ({
session: {
uid: Math.random()
@ -21,8 +26,10 @@ describe('The isAuthorized middleware', () => {
const next = jest.fn(fakeNext())
isAuthorized()(fakeContext, next)
// act
requireAuthenticated()(fakeContext, next)
// assert
expect(next).toBeCalled()
})
})

View file

@ -4,7 +4,7 @@ import { HttpError } from '../../network/classes/HttpError'
/**
* Middlware wich throws an error if the user isn't logged in
*/
export const isAuthorized = (): Middleware => (context, next) => {
export const requireAuthenticated = (): Middleware => (context, next) => {
if (context.session.uid !== undefined) {
return next()
} else {

View file

@ -3,93 +3,110 @@ import { app } from '../../../server'
import { loggedInAgent } from '../../../../test/utils/loggedInAgent'
import { mockAccounts } from '../../../../test/seeds/01_create-account'
import { random, internet } from 'faker'
import { LoginReponseBody } from '../types/LoginReponseBody'
import { defaultEncryptionMethod } from '../constants'
describe('The /auth route', () => {
// used to make requests
let request = supertest(app.callback())
describe(`The POST method on the /login subroute`, () => {
test('should throw an error if the password field is empty', async () => {
const response = await request.post('/auth/login').send({
name: mockAccounts[0].name
})
expect(response.status).not.toBe(200)
})
test('should throw an error if the name field is empty', async () => {
const response = await request.post('/auth/login').send({
password: mockAccounts[0].password
})
expect(response.status).not.toBe(200)
})
test('should throw an error if the password is wrong', async () => {
const response = await request.post('/auth/login').send({
name: mockAccounts[0].name,
password: mockAccounts[0].password + 'something'
})
expect(response.status).not.toBe(200)
})
test('should work just fine when the password is correct', async () => {
for (const account of mockAccounts) {
const response = await request.post('/auth/login').send({
email: account.email,
password: account.password
})
// i'm making a separate constant for vsc to help me
const body: LoginReponseBody = response.body
expect(response.status).toBe(200)
expect(body.uid).not.toBe(undefined)
expect(body.encryption).toBe(account.passwordEncryption)
}
})
})
const request = supertest(app.callback())
describe(`The GET method on the / subroute`, () => {
test('should return undefined if the user was not logged in', async () => {
// act
const res = await request.get('/auth')
// assert
expect(res.body.uid).toBe(undefined)
})
test('should return the uid form the session while logged in', async () => {
// arrange
const [agent, cookie] = await loggedInAgent(supertest.agent(app.callback()), {
email: mockAccounts[0].email,
password: mockAccounts[0].password
})
// act
const response = await agent.get('/auth').set('cookie', cookie)
// assert
expect(response.body.uid).not.toBe(undefined)
})
})
describe(`The POST method on the /login subroute`, () => {
test('should throw an error if the user is already logged in', async () => {
// arrange
const [agent, cookie] = await loggedInAgent(supertest.agent(app.callback()), {
email: mockAccounts[0].email,
password: mockAccounts[0].password
})
// act
const reponse = await agent.post('/auth/login').set('cookie', cookie)
// assert
expect(reponse.status).toBe(401)
})
test('should throw an error if the password is wrong', async () => {
// act
const response = await request.post('/auth/login').send({
email: mockAccounts[0].email,
password: mockAccounts[0].password + 'something'
})
// assert
expect(response.status).toBe(422)
expect((response.body.message as string).startsWith('child')).toBe(false) // Not JOI
})
test("should throw an error if the user doesn't exist", async () => {
// act
const reponse = await request.post('/auth/login').send({
email: 'idk' + mockAccounts[0].email,
password: mockAccounts[0].password
})
// assert
expect(reponse.status).toBe(404)
})
test('should work when the password is correct', async () => {
for (const account of mockAccounts) {
// act
const response = await request.post('/auth/login').send({
email: account.email,
password: account.password
})
// assert
expect(response.status).toBe(200)
expect(response.body.uid).not.toBe(undefined)
expect(response.body.encryption).toBe(account.passwordEncryption)
}
})
})
describe('The POST method on the /signup subroute', () => {
test('should return the email name and the encrytion', async () => {
const username = random.alphaNumeric(7)
test('should work if all fields are correct', async () => {
// arrange
const name = internet.userName()
const password = random.alphaNumeric(5)
const email = internet.email()
const response = await request.post('/auth/signup').send({
name: username,
const user = {
name,
email,
password
})
}
// i'm making a separate constant for vsc to help me
const body: LoginReponseBody = response.body
// act
const response = await request.post('/auth/signup').send(user)
// assert
expect(response.status).toBe(200)
expect(body.uid).not.toBe(undefined)
expect(body.encryption).toBe(defaultEncryptionMethod)
expect(response.body.uid).not.toBe(undefined)
expect(response.body.encryption).toBe(defaultEncryptionMethod)
})
})
})

View file

@ -8,7 +8,7 @@ import { encryptPassword } from '../helpers/encryptPassword'
import { createAccount } from '../queries/createAccount'
import { defaultEncryptionMethod } from '../constants'
import { LoginBodySchema } from '../schemas/LoginBody'
import { isUnauthorized } from '../middleware/isUnauthorized'
import { requireAnonymous } from '../middleware/requireAnonymous'
const router = new Router()
@ -22,30 +22,28 @@ router.get('/', (context, next) => {
router.post(
'/login',
isUnauthorized(),
requireAnonymous(),
validate(LoginBodySchema, 'body'),
async (context, next) => {
const { email, password } = context.request.body
const passwordData = await getPasswordByEmail(email)
// in case the user doesnt exist
if (!passwordData) {
throw new HttpError(400)
throw new HttpError(404)
}
const match = checkPassword(
const match = await checkPassword(
passwordData.password,
password,
passwordData.passwordEncryption
)
if (!match) {
throw new HttpError(400, 'wrong password')
throw new HttpError(422, 'wrong password')
}
context.session.uid = passwordData.id
context.body = {
encryption: passwordData.passwordEncryption,
uid: passwordData.id
@ -57,7 +55,7 @@ router.post(
router.post(
'/signup',
isUnauthorized(),
requireAnonymous(),
validate(SignupBodySchema, 'body'),
async (context, next) => {
const { email, name, password } = context.request.body

View file

@ -11,6 +11,7 @@ export enum HttpStatus {
Conflict = 409,
Gone = 410,
PayloadTooLarge = 413,
UnprocessableEntity = 422,
TooManyRequests = 429,
InternalServerError = 500
}
@ -24,6 +25,7 @@ export const HTTP_REASONS: Record<HttpStatus, string> = {
'409': 'Conflict',
'410': 'Gone',
'413': 'Payload too large',
'422': 'Validation error',
'429': 'Too many requests',
'500': 'Internal server error'
}
@ -34,7 +36,7 @@ export class HttpError extends Error {
// for some reason instanceof stopped working at some point
public [httpSymbol] = true
constructor(
public constructor(
public status: HttpStatus = HttpStatus.InternalServerError,
public reason?: string
) {

View file

@ -0,0 +1,4 @@
/**
* Factory for a quick mock of the next function required to test middlewares
*/
export const fakeNext = () => async () => {}