UNPKG

fastify

Version:

Fast and low overhead web framework, for Node.js

543 lines (482 loc) 12.4 kB
'use strict' const t = require('tap') // const Joi = require('@hapi/joi') const Fastify = require('..') const test = t.test const echoBody = (req, reply) => { reply.send(req.body) } test('basic test', t => { t.plan(3) const fastify = Fastify() fastify.get('/', { schema: { response: { '2xx': { type: 'object', properties: { name: { type: 'string' }, work: { type: 'string' } } } } } }, function (req, reply) { reply.code(200).send({ name: 'Foo', work: 'Bar', nick: 'Boo' }) }) fastify.inject('/', (err, res) => { t.error(err) t.deepEqual(res.json(), { name: 'Foo', work: 'Bar' }) t.strictEqual(res.statusCode, 200) }) }) test('Use the same schema id in different places', t => { t.plan(2) const fastify = Fastify() fastify.addSchema({ $id: 'test', type: 'object', properties: { id: { type: 'number' } } }) fastify.get('/:id', { handler (req, reply) { reply.send([{ id: 1 }, { id: 2 }, { what: 'is this' }]) }, schema: { response: { 200: { type: 'array', items: { $ref: 'test' } } } } }) fastify.inject({ method: 'GET', url: '/123' }, (err, res) => { t.error(err) t.deepEqual(res.json(), [{ id: 1 }, { id: 2 }, { }]) }) }) test('Use shared schema and $ref with $id in response ($ref to $id)', t => { t.plan(5) const fastify = Fastify() fastify.addSchema({ $id: 'http://foo/test', type: 'object', properties: { id: { type: 'number' } } }) const complexSchema = { $schema: 'http://json-schema.org/draft-07/schema#', $id: 'http://foo/user', type: 'object', definitions: { address: { $id: '#address', type: 'object', properties: { city: { type: 'string' } } } }, properties: { test: { $ref: 'http://foo/test#' }, address: { $ref: '#address' } }, required: ['address', 'test'] } fastify.post('/', { schema: { body: complexSchema, response: { 200: complexSchema } }, handler: (req, reply) => { req.body.removeThis = 'it should not be serialized' reply.send(req.body) } }) const payload = { address: { city: 'New Node' }, test: { id: Date.now() } } fastify.inject({ method: 'POST', url: '/', payload }, (err, res) => { t.error(err) t.deepEqual(res.json(), payload) }) fastify.inject({ method: 'POST', url: '/', payload: { test: { id: Date.now() } } }, (err, res) => { t.error(err) t.strictEqual(res.statusCode, 400) t.deepEqual(res.json(), { error: 'Bad Request', message: "body should have required property 'address'", statusCode: 400 }) }) }) test('Shared schema should be pass to serializer and validator ($ref to shared schema /definitions)', t => { t.plan(5) const fastify = Fastify() fastify.addSchema({ $id: 'http://example.com/asset.json', $schema: 'http://json-schema.org/draft-07/schema#', title: 'Physical Asset', description: 'A generic representation of a physical asset', type: 'object', required: [ 'id', 'model', 'location' ], properties: { id: { type: 'string', format: 'uuid' }, model: { type: 'string' }, location: { $ref: 'http://example.com/point.json#' } }, definitions: { inner: { $id: '#innerId', type: 'string', format: 'email' } } }) fastify.addSchema({ $id: 'http://example.com/point.json', $schema: 'http://json-schema.org/draft-07/schema#', title: 'Longitude and Latitude Values', description: 'A geographical coordinate.', type: 'object', required: [ 'latitude', 'longitude' ], properties: { email: { $ref: 'http://example.com/asset.json#/definitions/inner' }, latitude: { type: 'number', minimum: -90, maximum: 90 }, longitude: { type: 'number', minimum: -180, maximum: 180 }, altitude: { type: 'number' } } }) const schemaLocations = { $id: 'http://example.com/locations.json', $schema: 'http://json-schema.org/draft-07/schema#', title: 'List of Asset locations', type: 'array', items: { $ref: 'http://example.com/asset.json#' }, default: [] } fastify.post('/', { schema: { body: schemaLocations, response: { 200: schemaLocations } } }, (req, reply) => { reply.send(locations.map(_ => Object.assign({ serializer: 'remove me' }, _))) }) const locations = [ { id: '550e8400-e29b-41d4-a716-446655440000', model: 'mod', location: { latitude: 10, longitude: 10, email: 'foo@bar.it' } }, { id: '550e8400-e29b-41d4-a716-446655440000', model: 'mod', location: { latitude: 10, longitude: 10, email: 'foo@bar.it' } } ] fastify.inject({ method: 'POST', url: '/', payload: locations }, (err, res) => { t.error(err) t.deepEqual(res.json(), locations) fastify.inject({ method: 'POST', url: '/', payload: locations.map(_ => { _.location.email = 'not an email' return _ }) }, (err, res) => { t.error(err) t.strictEqual(res.statusCode, 400) t.deepEqual(res.json(), { error: 'Bad Request', message: 'body[0].location.email should match format "email"', statusCode: 400 }) }) }) }) test('Custom setSerializerCompiler', t => { t.plan(7) const fastify = Fastify() const outSchema = { $id: 'test', type: 'object', whatever: 'need to be parsed by the custom serializer' } fastify.setSerializerCompiler(({ schema, method, url, httpStatus }) => { t.equals(method, 'GET') t.equals(url, '/foo/:id') t.equals(httpStatus, '200') t.deepEqual(schema, outSchema) return data => JSON.stringify(data) }) fastify.register((instance, opts, done) => { instance.get('/:id', { handler (req, reply) { reply.send({ id: 1 }) }, schema: { response: { 200: outSchema } } }) t.ok(instance.serializerCompiler, 'the serializer is set by the parent') done() }, { prefix: '/foo' }) fastify.inject({ method: 'GET', url: '/foo/123' }, (err, res) => { t.error(err) t.equals(res.payload, JSON.stringify({ id: 1 })) }) }) test('Custom serializer per route', async t => { const fastify = Fastify() const outSchema = { $id: 'test', type: 'object', properties: { mean: { type: 'string' } } } fastify.get('/default', { handler (req, reply) { reply.send({ mean: 'default' }) }, schema: { response: { 200: outSchema } } }) let hit = 0 fastify.register((instance, opts, done) => { instance.setSerializerCompiler(({ schema, method, url, httpStatus }) => { hit++ return data => JSON.stringify({ mean: 'custom' }) }) instance.get('/custom', { handler (req, reply) { reply.send({}) }, schema: { response: { 200: outSchema } } }) instance.get('/route', { handler (req, reply) { reply.send({}) }, serializerCompiler: ({ schema, method, url, httpPart }) => { hit++ return data => JSON.stringify({ mean: 'route' }) }, schema: { response: { 200: outSchema } } }) done() }) let res = await fastify.inject('/default') t.equals(res.json().mean, 'default') res = await fastify.inject('/custom') t.equals(res.json().mean, 'custom') res = await fastify.inject('/route') t.equals(res.json().mean, 'route') t.equals(hit, 2, 'the custom and route serializer has been called') }) test('Reply serializer win over serializer ', t => { t.plan(5) const fastify = Fastify() fastify.setReplySerializer(function (payload, statusCode) { t.deepEqual(payload, { name: 'Foo', work: 'Bar', nick: 'Boo' }) return 'instance serializator' }) fastify.get('/', { schema: { response: { '2xx': { type: 'object', properties: { name: { type: 'string' }, work: { type: 'string' } } } } }, serializerCompiler: ({ schema, method, url, httpPart }) => { t.ok(method, 'the custom compiler has been created') return () => { t.fail('the serializer must not be called when there is a reply serializer') return 'fail' } } }, function (req, reply) { reply.code(200).send({ name: 'Foo', work: 'Bar', nick: 'Boo' }) }) fastify.inject('/', (err, res) => { t.error(err) t.deepEqual(res.payload, 'instance serializator') t.strictEqual(res.statusCode, 200) }) }) test('Reply serializer win over serializer ', t => { t.plan(5) const fastify = Fastify() fastify.setReplySerializer(function (payload, statusCode) { t.deepEqual(payload, { name: 'Foo', work: 'Bar', nick: 'Boo' }) return 'instance serializator' }) fastify.get('/', { schema: { response: { '2xx': { type: 'object', properties: { name: { type: 'string' }, work: { type: 'string' } } } } }, serializerCompiler: ({ schema, method, url, httpPart }) => { t.ok(method, 'the custom compiler has been created') return () => { t.fail('the serializer must not be called when there is a reply serializer') return 'fail' } } }, function (req, reply) { reply.code(200).send({ name: 'Foo', work: 'Bar', nick: 'Boo' }) }) fastify.inject('/', (err, res) => { t.error(err) t.deepEqual(res.payload, 'instance serializator') t.strictEqual(res.statusCode, 200) }) }) test('The schema compiler recreate itself if needed', t => { t.plan(1) const fastify = Fastify() fastify.options('/', { schema: { response: { '2xx': { hello: { type: 'string' } } } } }, echoBody) fastify.register(function (fastify, options, done) { fastify.addSchema({ $id: 'identifier', type: 'string', format: 'uuid' }) fastify.get('/', { schema: { response: { '2xx': { foobarId: { $ref: 'identifier#' } } } } }, echoBody) done() }) fastify.ready(err => { t.error(err) }) }) test('The schema changes the default error handler output', async t => { t.plan(4) const fastify = Fastify() fastify.get('/:code', { schema: { response: { '2xx': { hello: { type: 'string' } }, 501: { type: 'object', properties: { message: { type: 'string' } } }, '5xx': { type: 'object', properties: { customId: { type: 'number' }, error: { type: 'string' }, message: { type: 'string' } } } } } }, (request, reply) => { if (request.params.code === '501') { return reply.code(501).send(new Error('501 message')) } const error = new Error('500 message') error.customId = 42 reply.send(error) }) let res = await fastify.inject('/501') t.equals(res.statusCode, 501) t.deepEquals(res.json(), { message: '501 message' }) res = await fastify.inject('/500') t.equals(res.statusCode, 500) t.deepEquals(res.json(), { error: 'Internal Server Error', message: '500 message', customId: 42 }) }) test('do not crash if status code serializer errors', async t => { const fastify = Fastify() const requiresFoo = { properties: { foo: { type: 'string' } }, required: ['foo'] } const someUserErrorType2 = { properties: { code: { type: 'number' } }, required: ['code'] } fastify.get( '/', { schema: { query: requiresFoo, response: { 400: someUserErrorType2 } } }, (request, reply) => { t.fail('handler, should not be called') } ) const res = await fastify.inject({ path: '/', query: { notfoo: true } }) t.equals(res.statusCode, 500) t.deepEquals(res.json(), { statusCode: 500, error: 'Internal Server Error', message: '"code" is required!' }) })