fastify
Version:
Fast and low overhead web framework, for Node.js
619 lines (547 loc) • 13.1 kB
JavaScript
'use strict'
const { test } = require('tap')
const localize = require('ajv-i18n')
const Fastify = require('..')
test('Example - URI $id', t => {
t.plan(1)
const fastify = Fastify()
fastify.addSchema({
$id: 'http://example.com/',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
fastify.post('/', {
handler () { },
schema: {
body: {
type: 'array',
items: { $ref: 'http://example.com#/properties/hello' }
}
}
})
fastify.ready(err => t.error(err))
})
test('Example - string $id', t => {
t.plan(1)
const fastify = Fastify()
fastify.addSchema({
$id: 'commonSchema',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
fastify.post('/', {
handler () { },
schema: {
body: { $ref: 'commonSchema#' },
headers: { $ref: 'commonSchema#' }
}
})
fastify.ready(err => t.error(err))
})
test('Example - get schema', t => {
t.plan(1)
const fastify = Fastify()
fastify.addSchema({
$id: 'schemaId',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
const mySchemas = fastify.getSchemas()
const mySchema = fastify.getSchema('schemaId')
t.same(mySchemas.schemaId, mySchema)
})
test('Example - get schema encapsulated', async t => {
const fastify = Fastify()
fastify.addSchema({ $id: 'one', my: 'hello' })
// will return only `one` schema
fastify.get('/', (request, reply) => { reply.send(fastify.getSchemas()) })
fastify.register((instance, opts, done) => {
instance.addSchema({ $id: 'two', my: 'ciao' })
// will return `one` and `two` schemas
instance.get('/sub', (request, reply) => { reply.send(instance.getSchemas()) })
instance.register((subinstance, opts, done) => {
subinstance.addSchema({ $id: 'three', my: 'hola' })
// will return `one`, `two` and `three`
subinstance.get('/deep', (request, reply) => { reply.send(subinstance.getSchemas()) })
done()
})
done()
})
const r1 = await fastify.inject('/')
const r2 = await fastify.inject('/sub')
const r3 = await fastify.inject('/deep')
t.same(Object.keys(r1.json()), ['one'])
t.same(Object.keys(r2.json()), ['one', 'two'])
t.same(Object.keys(r3.json()), ['one', 'two', 'three'])
})
test('Example - validation', t => {
t.plan(1)
const fastify = Fastify({
ajv: {
customOptions: {
allowUnionTypes: true
}
}
})
const handler = () => { }
const bodyJsonSchema = {
type: 'object',
required: ['requiredKey'],
properties: {
someKey: { type: 'string' },
someOtherKey: { type: 'number' },
requiredKey: {
type: 'array',
maxItems: 3,
items: { type: 'integer' }
},
nullableKey: { type: ['number', 'null'] }, // or { type: 'number', nullable: true }
multipleTypesKey: { type: ['boolean', 'number'] },
multipleRestrictedTypesKey: {
oneOf: [
{ type: 'string', maxLength: 5 },
{ type: 'number', minimum: 10 }
]
},
enumKey: {
type: 'string',
enum: ['John', 'Foo']
},
notTypeKey: {
not: { type: 'array' }
}
}
}
const queryStringJsonSchema = {
name: { type: 'string' },
excitement: { type: 'integer' }
}
const paramsJsonSchema = {
par1: { type: 'string' },
par2: { type: 'number' }
}
const headersJsonSchema = {
type: 'object',
properties: {
'x-foo': { type: 'string' }
},
required: ['x-foo']
}
const schema = {
body: bodyJsonSchema,
querystring: queryStringJsonSchema,
params: paramsJsonSchema,
headers: headersJsonSchema
}
fastify.post('/the/url', { schema }, handler)
fastify.ready(err => t.error(err))
})
test('Example - ajv config', t => {
t.plan(1)
const fastify = Fastify({
ajv: {
plugins: [
require('ajv-merge-patch')
]
}
})
fastify.post('/', {
handler (req, reply) { reply.send({ ok: 1 }) },
schema: {
body: {
$patch: {
source: {
type: 'object',
properties: {
q: {
type: 'string'
}
}
},
with: [
{
op: 'add',
path: '/properties/q',
value: { type: 'number' }
}
]
}
}
}
})
fastify.post('/foo', {
handler (req, reply) { reply.send({ ok: 1 }) },
schema: {
body: {
$merge: {
source: {
type: 'object',
properties: {
q: {
type: 'string'
}
}
},
with: {
required: ['q']
}
}
}
}
})
fastify.ready(err => t.error(err))
})
test('Example Joi', t => {
t.plan(1)
const fastify = Fastify()
const handler = () => { }
const Joi = require('joi')
fastify.post('/the/url', {
schema: {
body: Joi.object().keys({
hello: Joi.string().required()
}).required()
},
validatorCompiler: ({ schema, method, url, httpPart }) => {
return data => schema.validate(data)
}
}, handler)
fastify.ready(err => t.error(err))
})
test('Example yup', t => {
t.plan(1)
const fastify = Fastify()
const handler = () => { }
const yup = require('yup')
// Validation options to match ajv's baseline options used in Fastify
const yupOptions = {
strict: false,
abortEarly: false, // return all errors
stripUnknown: true, // remove additional properties
recursive: true
}
fastify.post('/the/url', {
schema: {
body: yup.object({
age: yup.number().integer().required(),
sub: yup.object().shape({
name: yup.string().required()
}).required()
})
},
validatorCompiler: ({ schema, method, url, httpPart }) => {
return function (data) {
// with option strict = false, yup `validateSync` function returns the coerced value if validation was successful, or throws if validation failed
try {
const result = schema.validateSync(data, yupOptions)
return { value: result }
} catch (e) {
return { error: e }
}
}
}
}, handler)
fastify.ready(err => t.error(err))
})
test('Example - serialization', t => {
t.plan(1)
const fastify = Fastify()
const handler = () => { }
const schema = {
response: {
200: {
type: 'object',
properties: {
value: { type: 'string' },
otherValue: { type: 'boolean' }
}
}
}
}
fastify.post('/the/url', { schema }, handler)
fastify.ready(err => t.error(err))
})
test('Example - serialization 2', t => {
t.plan(1)
const fastify = Fastify()
const handler = () => { }
const schema = {
response: {
'2xx': {
type: 'object',
properties: {
value: { type: 'string' },
otherValue: { type: 'boolean' }
}
},
201: {
// the contract syntax
value: { type: 'string' }
}
}
}
fastify.post('/the/url', { schema }, handler)
fastify.ready(err => t.error(err))
})
test('Example - serializator', t => {
t.plan(1)
const fastify = Fastify()
fastify.setSerializerCompiler(({ schema, method, url, httpStatus }) => {
return data => JSON.stringify(data)
})
fastify.get('/user', {
handler (req, reply) {
reply.send({ id: 1, name: 'Foo', image: 'BIG IMAGE' })
},
schema: {
response: {
'2xx': {
id: { type: 'number' },
name: { type: 'string' }
}
}
}
})
fastify.ready(err => t.error(err))
})
test('Example - schemas examples', t => {
t.plan(1)
const fastify = Fastify()
const handler = () => { }
fastify.addSchema({
$id: 'http://foo/common.json',
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { type: 'string' }
}
}
}
})
fastify.addSchema({
$id: 'http://foo/shared.json',
type: 'object',
definitions: {
foo: {
type: 'object',
properties: {
city: { type: 'string' }
}
}
}
})
const refToId = {
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { type: 'string' }
}
}
},
properties: {
home: { $ref: '#address' },
work: { $ref: '#address' }
}
}
const refToDefinitions = {
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { type: 'string' }
}
}
},
properties: {
home: { $ref: '#/definitions/foo' },
work: { $ref: '#/definitions/foo' }
}
}
const refToSharedSchemaId = {
type: 'object',
properties: {
home: { $ref: 'http://foo/common.json#address' },
work: { $ref: 'http://foo/common.json#address' }
}
}
const refToSharedSchemaDefinitions = {
type: 'object',
properties: {
home: { $ref: 'http://foo/shared.json#/definitions/foo' },
work: { $ref: 'http://foo/shared.json#/definitions/foo' }
}
}
fastify.post('/', {
handler,
schema: {
body: refToId,
headers: refToDefinitions,
params: refToSharedSchemaId,
query: refToSharedSchemaDefinitions
}
})
fastify.ready(err => t.error(err))
})
test('should return custom error messages with ajv-errors', t => {
t.plan(3)
const fastify = Fastify({
ajv: {
customOptions: { allErrors: true },
plugins: [
require('ajv-errors')
]
}
})
const schema = {
body: {
type: 'object',
properties: {
name: { type: 'string' },
work: { type: 'string' },
age: {
type: 'number',
errorMessage: {
type: 'bad age - should be num'
}
}
},
required: ['name', 'work'],
errorMessage: {
required: {
name: 'name please',
work: 'work please',
age: 'age please'
}
}
}
}
fastify.post('/', { schema }, function (req, reply) {
reply.code(200).send(req.body.name)
})
fastify.inject({
method: 'POST',
payload: {
hello: 'salman',
age: 'bad'
},
url: '/'
}, (err, res) => {
t.error(err)
t.same(JSON.parse(res.payload), {
statusCode: 400,
code: 'FST_ERR_VALIDATION',
error: 'Bad Request',
message: 'body/age bad age - should be num, body name please, body work please'
})
t.equal(res.statusCode, 400)
})
})
test('should be able to handle formats of ajv-formats when added by plugins option', t => {
t.plan(3)
const fastify = Fastify({
ajv: {
plugins: [
require('ajv-formats')
]
}
})
const schema = {
body: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
email: { type: 'string', format: 'email' }
},
required: ['id', 'email']
}
}
fastify.post('/', { schema }, function (req, reply) {
reply.code(200).send(req.body.id)
})
fastify.inject({
method: 'POST',
payload: {
id: '254381a5-888c-4b41-8116-e3b1a54980bd',
email: 'info@fastify.dev'
},
url: '/'
}, (_err, res) => {
t.equal(res.body, '254381a5-888c-4b41-8116-e3b1a54980bd')
t.equal(res.statusCode, 200)
})
fastify.inject({
method: 'POST',
payload: {
id: 'invalid',
email: 'info@fastify.dev'
},
url: '/'
}, (_err, res) => {
t.same(JSON.parse(res.payload), {
statusCode: 400,
code: 'FST_ERR_VALIDATION',
error: 'Bad Request',
message: 'body/id must match format "uuid"'
})
})
})
test('should return localized error messages with ajv-i18n', t => {
t.plan(3)
const schema = {
body: {
type: 'object',
properties: {
name: { type: 'string' },
work: { type: 'string' }
},
required: ['name', 'work']
}
}
const fastify = Fastify({
ajv: {
customOptions: { allErrors: true }
}
})
fastify.setErrorHandler(function (error, request, reply) {
if (error.validation) {
localize.ru(error.validation)
reply.status(400).send(error.validation)
return
}
reply.send(error)
})
fastify.post('/', { schema }, function (req, reply) {
reply.code(200).send(req.body.name)
})
fastify.inject({
method: 'POST',
payload: {
name: 'salman'
},
url: '/'
}, (err, res) => {
t.error(err)
t.same(JSON.parse(res.payload), [{
instancePath: '',
keyword: 'required',
message: 'должно иметь обязательное поле work',
params: { missingProperty: 'work' },
schemaPath: '#/required'
}])
t.equal(res.statusCode, 400)
})
})