You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					949 lines
				
				18 KiB
			
		
		
			
		
	
	
					949 lines
				
				18 KiB
			| 
											3 years ago
										 | 'use strict' | ||
|  | 
 | ||
|  | const { test } = require('tap') | ||
|  | const Fastify = require('..') | ||
|  | 
 | ||
|  | const AJV = require('ajv') | ||
|  | 
 | ||
|  | const customSchemaCompilers = { | ||
|  |   body: new AJV({ | ||
|  |     coerceTypes: false | ||
|  |   }), | ||
|  |   params: new AJV({ | ||
|  |     coerceTypes: true | ||
|  |   }), | ||
|  |   querystring: new AJV({ | ||
|  |     coerceTypes: true | ||
|  |   }) | ||
|  | } | ||
|  | 
 | ||
|  | const customValidatorCompiler = req => { | ||
|  |   if (!req.httpPart) { | ||
|  |     throw new Error('Missing httpPart') | ||
|  |   } | ||
|  | 
 | ||
|  |   const compiler = customSchemaCompilers[req.httpPart] | ||
|  | 
 | ||
|  |   if (!compiler) { | ||
|  |     throw new Error(`Missing compiler for ${req.httpPart}`) | ||
|  |   } | ||
|  | 
 | ||
|  |   return compiler.compile(req.schema) | ||
|  | } | ||
|  | 
 | ||
|  | const schemaA = { | ||
|  |   $id: 'urn:schema:foo', | ||
|  |   type: 'object', | ||
|  |   definitions: { | ||
|  |     foo: { type: 'integer' } | ||
|  |   }, | ||
|  |   properties: { | ||
|  |     foo: { $ref: '#/definitions/foo' } | ||
|  |   } | ||
|  | } | ||
|  | const schemaBRefToA = { | ||
|  |   $id: 'urn:schema:response', | ||
|  |   type: 'object', | ||
|  |   required: ['foo'], | ||
|  |   properties: { | ||
|  |     foo: { $ref: 'urn:schema:foo#/definitions/foo' } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | const schemaCRefToB = { | ||
|  |   $id: 'urn:schema:request', | ||
|  |   type: 'object', | ||
|  |   required: ['foo'], | ||
|  |   properties: { | ||
|  |     foo: { $ref: 'urn:schema:response#/properties/foo' } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | const schemaArtist = { | ||
|  |   type: 'object', | ||
|  |   properties: { | ||
|  |     name: { type: 'string' }, | ||
|  |     work: { type: 'string' } | ||
|  |   }, | ||
|  |   required: ['name', 'work'] | ||
|  | } | ||
|  | 
 | ||
|  | test('Basic validation test', t => { | ||
|  |   t.plan(6) | ||
|  | 
 | ||
|  |   const fastify = Fastify() | ||
|  |   fastify.post('/', { | ||
|  |     schema: { | ||
|  |       body: schemaArtist | ||
|  |     } | ||
|  |   }, function (req, reply) { | ||
|  |     reply.code(200).send(req.body.name) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     payload: { | ||
|  |       name: 'michelangelo', | ||
|  |       work: 'sculptor, painter, architect and poet' | ||
|  |     }, | ||
|  |     url: '/' | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.same(res.payload, 'michelangelo') | ||
|  |     t.equal(res.statusCode, 200) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     payload: { name: 'michelangelo' }, | ||
|  |     url: '/' | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.same(res.json(), { statusCode: 400, error: 'Bad Request', message: "body should have required property 'work'" }) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('External AJV instance', t => { | ||
|  |   t.plan(4) | ||
|  | 
 | ||
|  |   const fastify = Fastify() | ||
|  |   const ajv = new AJV() | ||
|  |   ajv.addSchema(schemaA) | ||
|  |   ajv.addSchema(schemaBRefToA) | ||
|  | 
 | ||
|  |   // the user must provide the schemas to fastify also
 | ||
|  |   fastify.addSchema(schemaA) | ||
|  |   fastify.addSchema(schemaBRefToA) | ||
|  | 
 | ||
|  |   fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => { | ||
|  |     return ajv.compile(schema) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.post('/', { | ||
|  |     handler (req, reply) { reply.send({ foo: 1 }) }, | ||
|  |     schema: { | ||
|  |       body: schemaCRefToB, | ||
|  |       response: { | ||
|  |         '2xx': ajv.getSchema('urn:schema:response').schema | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { foo: 42 } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 200) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { foo: 'not a number' } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Encapsulation', t => { | ||
|  |   t.plan(19) | ||
|  | 
 | ||
|  |   const fastify = Fastify() | ||
|  |   const ajv = new AJV() | ||
|  |   ajv.addSchema(schemaA) | ||
|  |   ajv.addSchema(schemaBRefToA) | ||
|  | 
 | ||
|  |   // the user must provide the schemas to fastify also
 | ||
|  |   fastify.addSchema(schemaA) | ||
|  |   fastify.addSchema(schemaBRefToA) | ||
|  | 
 | ||
|  |   fastify.register((instance, opts, done) => { | ||
|  |     const validator = ({ schema, method, url, httpPart }) => { | ||
|  |       return ajv.compile(schema) | ||
|  |     } | ||
|  |     instance.setValidatorCompiler(validator) | ||
|  |     instance.post('/one', { | ||
|  |       handler (req, reply) { reply.send({ foo: 'one' }) }, | ||
|  |       schema: { | ||
|  |         body: ajv.getSchema('urn:schema:response').schema | ||
|  |       } | ||
|  |     }) | ||
|  | 
 | ||
|  |     instance.register((instance, opts, done) => { | ||
|  |       instance.post('/two', { | ||
|  |         handler (req, reply) { | ||
|  |           t.same(instance.validatorCompiler, validator) | ||
|  |           reply.send({ foo: 'two' }) | ||
|  |         }, | ||
|  |         schema: { | ||
|  |           body: ajv.getSchema('urn:schema:response').schema | ||
|  |         } | ||
|  |       }) | ||
|  | 
 | ||
|  |       const anotherValidator = ({ schema, method, url, httpPart }) => { | ||
|  |         return () => { return true } // always valid
 | ||
|  |       } | ||
|  |       instance.post('/three', { | ||
|  |         validatorCompiler: anotherValidator, | ||
|  |         handler (req, reply) { | ||
|  |           t.same(instance.validatorCompiler, validator, 'the route validator does not change the instance one') | ||
|  |           reply.send({ foo: 'three' }) | ||
|  |         }, | ||
|  |         schema: { | ||
|  |           body: ajv.getSchema('urn:schema:response').schema | ||
|  |         } | ||
|  |       }) | ||
|  |       done() | ||
|  |     }) | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.register((instance, opts, done) => { | ||
|  |     instance.post('/clean', function (req, reply) { | ||
|  |       t.equal(instance.validatorCompiler, undefined) | ||
|  |       reply.send({ foo: 'bar' }) | ||
|  |     }) | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/one', | ||
|  |     payload: { foo: 1 } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 200) | ||
|  |     t.same(res.json(), { foo: 'one' }) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/one', | ||
|  |     payload: { wrongFoo: 'bar' } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/two', | ||
|  |     payload: { foo: 2 } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 200) | ||
|  |     t.same(res.json(), { foo: 'two' }) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/two', | ||
|  |     payload: { wrongFoo: 'bar' } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/three', | ||
|  |     payload: { wrongFoo: 'but works' } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 200) | ||
|  |     t.same(res.json(), { foo: 'three' }) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/clean', | ||
|  |     payload: { wrongFoo: 'bar' } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 200) | ||
|  |     t.same(res.json(), { foo: 'bar' }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Triple $ref with a simple $id', t => { | ||
|  |   t.plan(6) | ||
|  | 
 | ||
|  |   const fastify = Fastify() | ||
|  |   const ajv = new AJV() | ||
|  |   ajv.addSchema(schemaA) | ||
|  |   ajv.addSchema(schemaBRefToA) | ||
|  |   ajv.addSchema(schemaCRefToB) | ||
|  | 
 | ||
|  |   // the user must provide the schemas to fastify also
 | ||
|  |   fastify.addSchema(schemaA) | ||
|  |   fastify.addSchema(schemaBRefToA) | ||
|  |   fastify.addSchema(schemaCRefToB) | ||
|  | 
 | ||
|  |   fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => { | ||
|  |     return ajv.compile(schema) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.post('/', { | ||
|  |     handler (req, reply) { reply.send({ foo: 105, bar: 'foo' }) }, | ||
|  |     schema: { | ||
|  |       body: ajv.getSchema('urn:schema:request').schema, | ||
|  |       response: { | ||
|  |         '2xx': ajv.getSchema('urn:schema:response').schema | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { foo: 43 } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 200) | ||
|  |     t.same(res.json(), { foo: 105 }) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { fool: 'bar' } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |     t.same(res.json().message, "body should have required property 'foo'") | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Extending schema', t => { | ||
|  |   t.plan(4) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'address.id', | ||
|  |     type: 'object', | ||
|  |     definitions: { | ||
|  |       address: { | ||
|  |         type: 'object', | ||
|  |         properties: { | ||
|  |           city: { type: 'string' }, | ||
|  |           state: { type: 'string' } | ||
|  |         }, | ||
|  |         required: ['city', 'state'] | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.post('/', { | ||
|  |     handler (req, reply) { reply.send('works') }, | ||
|  |     schema: { | ||
|  |       body: { | ||
|  |         type: 'object', | ||
|  |         properties: { | ||
|  |           billingAddress: { $ref: 'address.id#/definitions/address' }, | ||
|  |           shippingAddress: { | ||
|  |             allOf: [ | ||
|  |               { $ref: 'address.id#/definitions/address' }, | ||
|  |               { | ||
|  |                 properties: { type: { enum: ['residential', 'business'] } }, | ||
|  |                 required: ['type'] | ||
|  |               } | ||
|  |             ] | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { | ||
|  |       shippingAddress: { | ||
|  |         city: 'Forlì', | ||
|  |         state: 'FC' | ||
|  |       } | ||
|  |     } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { | ||
|  |       shippingAddress: { | ||
|  |         city: 'Forlì', | ||
|  |         state: 'FC', | ||
|  |         type: 'business' | ||
|  |       } | ||
|  |     } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 200) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Should work with nested ids', t => { | ||
|  |   t.plan(6) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'test', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       id: { type: 'number' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'greetings', | ||
|  |     type: 'string' | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.post('/:id', { | ||
|  |     handler (req, reply) { reply.send(typeof req.params.id) }, | ||
|  |     schema: { | ||
|  |       params: { $ref: 'test#' }, | ||
|  |       body: { | ||
|  |         type: 'object', | ||
|  |         properties: { | ||
|  |           hello: { $ref: 'greetings#' } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/123', | ||
|  |     payload: { | ||
|  |       hello: 'world' | ||
|  |     } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 200) | ||
|  |     t.equal(res.payload, 'number') | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/abc', | ||
|  |     payload: { | ||
|  |       hello: 'world' | ||
|  |     } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |     t.equal(res.json().message, 'params.id should be number') | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Use the same schema across multiple routes', t => { | ||
|  |   t.plan(8) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'test', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       id: { type: 'number' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.get('/first/:id', { | ||
|  |     handler (req, reply) { reply.send(typeof req.params.id) }, | ||
|  |     schema: { | ||
|  |       params: { $ref: 'test#' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.get('/second/:id', { | ||
|  |     handler (req, reply) { reply.send(typeof req.params.id) }, | ||
|  |     schema: { | ||
|  |       params: { $ref: 'test#' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   ;[ | ||
|  |     '/first/123', | ||
|  |     '/second/123' | ||
|  |   ].forEach(url => { | ||
|  |     fastify.inject({ | ||
|  |       url, | ||
|  |       method: 'GET' | ||
|  |     }, (err, res) => { | ||
|  |       t.error(err) | ||
|  |       t.equal(res.payload, 'number') | ||
|  |     }) | ||
|  |   }) | ||
|  | 
 | ||
|  |   ;[ | ||
|  |     '/first/abc', | ||
|  |     '/second/abc' | ||
|  |   ].forEach(url => { | ||
|  |     fastify.inject({ | ||
|  |       url, | ||
|  |       method: 'GET' | ||
|  |     }, (err, res) => { | ||
|  |       t.error(err) | ||
|  |       t.equal(res.statusCode, 400) | ||
|  |     }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('JSON Schema validation keywords', t => { | ||
|  |   t.plan(6) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'test', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       ip: { | ||
|  |         type: 'string', | ||
|  |         format: 'ipv4' | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.get('/:ip', { | ||
|  |     handler (req, reply) { reply.send(typeof req.params.ip) }, | ||
|  |     schema: { | ||
|  |       params: { $ref: 'test#' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'GET', | ||
|  |     url: '/127.0.0.1' | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 200) | ||
|  |     t.equal(res.payload, 'string') | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'GET', | ||
|  |     url: '/localhost' | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |     t.same(res.json(), { | ||
|  |       statusCode: 400, | ||
|  |       error: 'Bad Request', | ||
|  |       message: 'params.ip should match format "ipv4"' | ||
|  |     }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Nested id calls', t => { | ||
|  |   t.plan(6) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'test', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       ip: { | ||
|  |         type: 'string', | ||
|  |         format: 'ipv4' | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'hello', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       host: { $ref: 'test#' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.post('/', { | ||
|  |     handler (req, reply) { reply.send(typeof req.body.host.ip) }, | ||
|  |     schema: { | ||
|  |       body: { $ref: 'hello#' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { host: { ip: '127.0.0.1' } } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 200) | ||
|  |     t.equal(res.payload, 'string') | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { host: { ip: 'localhost' } } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |     t.same(res.json(), { | ||
|  |       error: 'Bad Request', | ||
|  |       message: 'body.host.ip should match format "ipv4"', | ||
|  |       statusCode: 400 | ||
|  |     }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | 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.post('/', { | ||
|  |     handler (req, reply) { reply.send({ id: req.body.id / 2 }) }, | ||
|  |     schema: { | ||
|  |       body: { $ref: 'test#' }, | ||
|  |       response: { | ||
|  |         200: { $ref: 'test#' } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { id: 42 } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.same(res.json(), { id: 21 }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Use shared schema and $ref with $id ($ref to $id)', t => { | ||
|  |   t.plan(5) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'http://foo/test', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       id: { type: 'number' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   const body = { | ||
|  |     $id: 'http://foo/user', | ||
|  |     $schema: 'http://json-schema.org/draft-07/schema#', | ||
|  |     type: 'object', | ||
|  |     definitions: { | ||
|  |       address: { | ||
|  |         $id: '#address', | ||
|  |         type: 'object', | ||
|  |         properties: { | ||
|  |           city: { type: 'string' } | ||
|  |         } | ||
|  |       } | ||
|  |     }, | ||
|  |     required: ['address'], | ||
|  |     properties: { | ||
|  |       test: { $ref: 'http://foo/test#' }, // to external
 | ||
|  |       address: { $ref: '#address' } // to local
 | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   fastify.post('/', { | ||
|  |     handler (req, reply) { reply.send(req.body.test) }, | ||
|  |     schema: { | ||
|  |       body, | ||
|  |       response: { | ||
|  |         200: { $ref: 'http://foo/test#' } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   const id = Date.now() | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { | ||
|  |       address: { city: 'New Node' }, | ||
|  |       test: { id } | ||
|  |     } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.same(res.json(), { id }) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { test: { id } } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |     t.same(res.json(), { | ||
|  |       error: 'Bad Request', | ||
|  |       message: "body should have required property 'address'", | ||
|  |       statusCode: 400 | ||
|  |     }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Use items with $ref', t => { | ||
|  |   t.plan(4) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'http://example.com/ref-to-external-validator.json', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       hello: { type: 'string' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   const body = { | ||
|  |     type: 'array', | ||
|  |     items: { $ref: 'http://example.com/ref-to-external-validator.json#' }, | ||
|  |     default: [] | ||
|  |   } | ||
|  | 
 | ||
|  |   fastify.post('/', { | ||
|  |     schema: { body }, | ||
|  |     handler: (_, r) => { r.send('ok') } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: [{ hello: 'world' }] | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.payload, 'ok') | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { hello: 'world' } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Use $ref to /definitions', t => { | ||
|  |   t.plan(6) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'test', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       id: { type: 'number' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   const body = { | ||
|  |     type: 'object', | ||
|  |     definitions: { | ||
|  |       address: { | ||
|  |         $id: '#otherId', | ||
|  |         type: 'object', | ||
|  |         properties: { | ||
|  |           city: { type: 'string' } | ||
|  |         } | ||
|  |       } | ||
|  |     }, | ||
|  |     properties: { | ||
|  |       test: { $ref: 'test#' }, | ||
|  |       address: { $ref: '#/definitions/address' } | ||
|  |     }, | ||
|  |     required: ['address', 'test'] | ||
|  |   } | ||
|  | 
 | ||
|  |   fastify.post('/', { | ||
|  |     schema: { | ||
|  |       body, | ||
|  |       response: { | ||
|  |         200: body | ||
|  |       } | ||
|  |     }, | ||
|  |     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.equal(res.statusCode, 200) | ||
|  |     t.same(res.json(), payload) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { | ||
|  |       address: { city: 'New Node' }, | ||
|  |       test: { id: 'wrong' } | ||
|  |     } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |     t.same(res.json(), { | ||
|  |       error: 'Bad Request', | ||
|  |       message: 'body.test.id should be number', | ||
|  |       statusCode: 400 | ||
|  |     }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Custom AJV settings - pt1', t => { | ||
|  |   t.plan(4) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.post('/', { | ||
|  |     schema: { | ||
|  |       body: { num: { type: 'integer' } } | ||
|  |     }, | ||
|  |     handler: (req, reply) => { | ||
|  |       t.equal(req.body.num, 12) | ||
|  |       reply.send(req.body) | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { | ||
|  |       num: '12' | ||
|  |     } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 200) | ||
|  |     t.same(res.json(), { num: 12 }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Custom AJV settings - pt2', t => { | ||
|  |   t.plan(2) | ||
|  |   const fastify = Fastify({ | ||
|  |     ajv: { | ||
|  |       customOptions: { | ||
|  |         coerceTypes: false | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.post('/', { | ||
|  |     schema: { | ||
|  |       body: { num: { type: 'integer' } } | ||
|  |     }, | ||
|  |     handler: (req, reply) => { | ||
|  |       t.fail('the handler is not called because the "12" is not coerced to number') | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/', | ||
|  |     payload: { | ||
|  |       num: '12' | ||
|  |     } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Custom AJV settings on different parameters - pt1', t => { | ||
|  |   t.plan(2) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.setValidatorCompiler(customValidatorCompiler) | ||
|  | 
 | ||
|  |   fastify.post('/api/:id', { | ||
|  |     schema: { | ||
|  |       querystring: { id: { type: 'integer' } }, | ||
|  |       body: { | ||
|  |         type: 'object', | ||
|  |         properties: { | ||
|  |           num: { type: 'number' } | ||
|  |         }, | ||
|  |         required: ['num'] | ||
|  |       } | ||
|  |     }, | ||
|  |     handler: (req, reply) => { | ||
|  |       t.fail('the handler is not called because the "12" is not coerced to number') | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/api/42', | ||
|  |     payload: { | ||
|  |       num: '12' | ||
|  |     } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Custom AJV settings on different parameters - pt2', t => { | ||
|  |   t.plan(4) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.setValidatorCompiler(customValidatorCompiler) | ||
|  | 
 | ||
|  |   fastify.post('/api/:id', { | ||
|  |     schema: { | ||
|  |       params: { | ||
|  |         type: 'object', | ||
|  |         properties: { | ||
|  |           id: { type: 'number' } | ||
|  |         }, | ||
|  |         required: ['id'] | ||
|  |       }, | ||
|  |       body: { | ||
|  |         type: 'object', | ||
|  |         properties: { | ||
|  |           num: { type: 'number' } | ||
|  |         }, | ||
|  |         required: ['num'] | ||
|  |       } | ||
|  |     }, | ||
|  |     handler: (req, reply) => { | ||
|  |       t.same(typeof req.params.id, 'number') | ||
|  |       t.same(typeof req.body.num, 'number') | ||
|  |       t.same(req.params.id, 42) | ||
|  |       t.same(req.body.num, 12) | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     url: '/api/42', | ||
|  |     payload: { | ||
|  |       num: 12 | ||
|  |     } | ||
|  |   }) | ||
|  | }) |