fastify
Version:
Fast and low overhead web framework, for Node.js
621 lines (538 loc) • 14.3 kB
JavaScript
const { Readable } = require('node:stream')
const test = require('tap').test
const sget = require('simple-get').concat
const Fastify = require('../')
process.removeAllListeners('warning')
function endRouteHook (doneOrPayload, done, doneValue) {
if (typeof doneOrPayload === 'function') {
doneOrPayload(doneValue)
} else {
done(doneValue)
}
}
function testExecutionHook (hook) {
test(`${hook}`, t => {
t.plan(3)
const fastify = Fastify()
fastify.post('/', {
[hook]: (req, reply, doneOrPayload, done) => {
t.pass('hook called')
endRouteHook(doneOrPayload, done)
}
}, (req, reply) => {
reply.send(req.body)
})
fastify.inject({
method: 'POST',
url: '/',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
const payload = JSON.parse(res.payload)
t.same(payload, { hello: 'world' })
})
})
test(`${hook} option should be called after ${hook} hook`, t => {
t.plan(3)
const fastify = Fastify()
const checker = Object.defineProperty({ calledTimes: 0 }, 'check', {
get: function () { return ++this.calledTimes }
})
fastify.addHook(hook, (req, reply, doneOrPayload, done) => {
t.equal(checker.check, 1)
endRouteHook(doneOrPayload, done)
})
fastify.post('/', {
[hook]: (req, reply, doneOrPayload, done) => {
t.equal(checker.check, 2)
endRouteHook(doneOrPayload, done)
}
}, (req, reply) => {
reply.send({})
})
fastify.inject({
method: 'POST',
url: '/',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
})
})
test(`${hook} option could accept an array of functions`, t => {
t.plan(3)
const fastify = Fastify()
const checker = Object.defineProperty({ calledTimes: 0 }, 'check', {
get: function () { return ++this.calledTimes }
})
fastify.post('/', {
[hook]: [
(req, reply, doneOrPayload, done) => {
t.equal(checker.check, 1)
endRouteHook(doneOrPayload, done)
},
(req, reply, doneOrPayload, done) => {
t.equal(checker.check, 2)
endRouteHook(doneOrPayload, done)
}
]
}, (req, reply) => {
reply.send({})
})
fastify.inject({
method: 'POST',
url: '/',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
})
})
test(`${hook} option could accept an array of async functions`, t => {
t.plan(3)
const fastify = Fastify()
const checker = Object.defineProperty({ calledTimes: 0 }, 'check', {
get: function () { return ++this.calledTimes }
})
fastify.post('/', {
[hook]: [
async (req, reply) => {
t.equal(checker.check, 1)
},
async (req, reply) => {
t.equal(checker.check, 2)
}
]
}, (req, reply) => {
reply.send({})
})
fastify.inject({
method: 'POST',
url: '/',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
})
})
test(`${hook} option does not interfere with ${hook} hook`, t => {
t.plan(7)
const fastify = Fastify()
const checker = Object.defineProperty({ calledTimes: 0 }, 'check', {
get: function () { return ++this.calledTimes }
})
fastify.addHook(hook, (req, reply, doneOrPayload, done) => {
t.equal(checker.check, 1)
endRouteHook(doneOrPayload, done)
})
fastify.post('/', {
[hook]: (req, reply, doneOrPayload, done) => {
t.equal(checker.check, 2)
endRouteHook(doneOrPayload, done)
}
}, handler)
fastify.post('/no', handler)
function handler (req, reply) {
reply.send({})
}
fastify.inject({
method: 'post',
url: '/'
}, (err, res) => {
t.error(err)
t.equal(checker.calledTimes, 2)
checker.calledTimes = 0
fastify.inject({
method: 'post',
url: '/no'
}, (err, res) => {
t.error(err)
t.equal(checker.calledTimes, 1)
})
})
})
}
function testBeforeHandlerHook (hook) {
test(`${hook} option should be unique per route`, t => {
t.plan(4)
const fastify = Fastify()
fastify.post('/', {
[hook]: (req, reply, doneOrPayload, done) => {
req.hello = 'earth'
endRouteHook(doneOrPayload, done)
}
}, (req, reply) => {
reply.send({ hello: req.hello })
})
fastify.post('/no', (req, reply) => {
reply.send(req.body)
})
fastify.inject({
method: 'POST',
url: '/',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
const payload = JSON.parse(res.payload)
t.same(payload, { hello: 'earth' })
})
fastify.inject({
method: 'POST',
url: '/no',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
const payload = JSON.parse(res.payload)
t.same(payload, { hello: 'world' })
})
})
test(`${hook} option should handle errors`, t => {
t.plan(3)
const fastify = Fastify()
fastify.post('/', {
[hook]: (req, reply, doneOrPayload, done) => {
endRouteHook(doneOrPayload, done, new Error('kaboom'))
}
}, (req, reply) => {
reply.send(req.body)
})
fastify.inject({
method: 'POST',
url: '/',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
const payload = JSON.parse(res.payload)
t.equal(res.statusCode, 500)
t.same(payload, {
message: 'kaboom',
error: 'Internal Server Error',
statusCode: 500
})
})
})
test(`${hook} option should handle throwing objects`, t => {
t.plan(4)
const fastify = Fastify()
const myError = { myError: 'kaboom' }
fastify.setErrorHandler(async (error, request, reply) => {
t.same(error, myError, 'the error object throws by the user')
return reply.code(500).send({ this: 'is', my: 'error' })
})
fastify.get('/', {
[hook]: async () => {
// eslint-disable-next-line no-throw-literal
throw myError
}
}, (req, reply) => {
t.fail('the handler must not be called')
})
fastify.inject({
url: '/',
method: 'GET'
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
t.same(res.json(), { this: 'is', my: 'error' })
})
})
test(`${hook} option should handle throwing objects by default`, t => {
t.plan(3)
const fastify = Fastify()
fastify.get('/', {
[hook]: async () => {
// eslint-disable-next-line no-throw-literal
throw { myError: 'kaboom', message: 'i am an error' }
}
}, (req, reply) => {
t.fail('the handler must not be called')
})
fastify.inject({
url: '/',
method: 'GET'
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
t.same(res.json(), { myError: 'kaboom', message: 'i am an error' })
})
})
test(`${hook} option should handle errors with custom status code`, t => {
t.plan(3)
const fastify = Fastify()
fastify.post('/', {
[hook]: (req, reply, doneOrPayload, done) => {
reply.code(401)
endRouteHook(doneOrPayload, done, new Error('go away'))
}
}, (req, reply) => {
reply.send(req.body)
})
fastify.inject({
method: 'POST',
url: '/',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
const payload = JSON.parse(res.payload)
t.equal(res.statusCode, 401)
t.same(payload, {
message: 'go away',
error: 'Unauthorized',
statusCode: 401
})
})
})
test(`${hook} option should keep the context`, t => {
t.plan(3)
const fastify = Fastify()
fastify.decorate('foo', 42)
fastify.post('/', {
[hook]: function (req, reply, doneOrPayload, done) {
t.equal(this.foo, 42)
this.foo += 1
endRouteHook(doneOrPayload, done)
}
}, function (req, reply) {
reply.send({ foo: this.foo })
})
fastify.inject({
method: 'POST',
url: '/',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
const payload = JSON.parse(res.payload)
t.same(payload, { foo: 43 })
})
})
test(`${hook} option should keep the context (array)`, t => {
t.plan(3)
const fastify = Fastify()
fastify.decorate('foo', 42)
fastify.post('/', {
[hook]: [function (req, reply, doneOrPayload, done) {
t.equal(this.foo, 42)
this.foo += 1
endRouteHook(doneOrPayload, done)
}]
}, function (req, reply) {
reply.send({ foo: this.foo })
})
fastify.inject({
method: 'POST',
url: '/',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
const payload = JSON.parse(res.payload)
t.same(payload, { foo: 43 })
})
})
}
testExecutionHook('preHandler')
testExecutionHook('onSend')
testExecutionHook('onRequest')
testExecutionHook('onResponse')
testExecutionHook('preValidation')
testExecutionHook('preParsing')
// hooks that comes before the handler
testBeforeHandlerHook('preHandler')
testBeforeHandlerHook('onRequest')
testBeforeHandlerHook('preValidation')
testBeforeHandlerHook('preParsing')
test('preValidation option should be called before preHandler hook', t => {
t.plan(3)
const fastify = Fastify()
fastify.addHook('preHandler', (req, reply, done) => {
t.ok(req.called)
done()
})
fastify.post('/', {
preValidation: (req, reply, done) => {
req.called = true
done()
}
}, (req, reply) => {
reply.send(req.body)
})
fastify.inject({
method: 'POST',
url: '/',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
const payload = JSON.parse(res.payload)
t.same(payload, { hello: 'world' })
})
})
test('preSerialization option should be able to modify the payload', t => {
t.plan(3)
const fastify = Fastify()
fastify.get('/only', {
preSerialization: (req, reply, payload, done) => {
done(null, { hello: 'another world' })
}
}, (req, reply) => {
reply.send({ hello: 'world' })
})
fastify.inject({
method: 'GET',
url: '/only'
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 200)
t.same(JSON.parse(res.payload), { hello: 'another world' })
})
})
test('preParsing option should be called before preValidation hook', t => {
t.plan(3)
const fastify = Fastify()
fastify.addHook('preValidation', (req, reply, done) => {
t.ok(req.called)
done()
})
fastify.post('/', {
preParsing: (req, reply, payload, done) => {
req.called = true
done()
}
}, (req, reply) => {
reply.send(req.body)
})
fastify.inject({
method: 'POST',
url: '/',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
const payload = JSON.parse(res.payload)
t.same(payload, { hello: 'world' })
})
})
test('preParsing option should be able to modify the payload', t => {
t.plan(3)
const fastify = Fastify()
fastify.post('/only', {
preParsing: (req, reply, payload, done) => {
const stream = new Readable()
stream.receivedEncodedLength = parseInt(req.headers['content-length'], 10)
stream.push(JSON.stringify({ hello: 'another world' }))
stream.push(null)
done(null, stream)
}
}, (req, reply) => {
reply.send(req.body)
})
fastify.inject({
method: 'POST',
url: '/only',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 200)
t.same(JSON.parse(res.payload), { hello: 'another world' })
})
})
test('preParsing option should be able to supply statusCode', t => {
t.plan(4)
const fastify = Fastify()
fastify.post('/only', {
preParsing: async (req, reply, payload) => {
const stream = new Readable({
read () {
const error = new Error('kaboom')
error.statusCode = 408
this.destroy(error)
}
})
stream.receivedEncodedLength = 20
return stream
},
onError: async (req, res, err) => {
t.equal(err.statusCode, 408)
}
}, (req, reply) => {
t.fail('should not be called')
})
fastify.inject({
method: 'POST',
url: '/only',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 408)
t.same(JSON.parse(res.payload), {
statusCode: 408,
error: 'Request Timeout',
message: 'kaboom'
})
})
})
test('onRequest option should be called before preParsing', t => {
t.plan(3)
const fastify = Fastify()
fastify.addHook('preParsing', (req, reply, payload, done) => {
t.ok(req.called)
done()
})
fastify.post('/', {
onRequest: (req, reply, done) => {
req.called = true
done()
}
}, (req, reply) => {
reply.send(req.body)
})
fastify.inject({
method: 'POST',
url: '/',
payload: { hello: 'world' }
}, (err, res) => {
t.error(err)
const payload = JSON.parse(res.payload)
t.same(payload, { hello: 'world' })
})
})
test('onTimeout on route', t => {
t.plan(4)
const fastify = Fastify({ connectionTimeout: 500 })
fastify.get('/timeout', {
handler (request, reply) { },
onTimeout (request, reply, done) {
t.pass('onTimeout called')
done()
}
})
fastify.listen({ port: 0 }, (err, address) => {
t.error(err)
t.teardown(() => fastify.close())
sget({
method: 'GET',
url: `${address}/timeout`
}, (err, response, body) => {
t.type(err, Error)
t.equal(err.message, 'socket hang up')
})
})
})
test('onError on route', t => {
t.plan(3)
const fastify = Fastify()
const err = new Error('kaboom')
fastify.get('/',
{
onError (request, reply, error, done) {
t.match(error, err)
done()
}
},
(req, reply) => {
reply.send(err)
})
fastify.inject('/', (err, res) => {
t.error(err)
t.same(JSON.parse(res.payload), {
error: 'Internal Server Error',
message: 'kaboom',
statusCode: 500
})
})
})