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.
		
		
		
		
		
			
		
			
				
					1753 lines
				
				40 KiB
			
		
		
			
		
	
	
					1753 lines
				
				40 KiB
			| 
											3 years ago
										 | 'use strict' | ||
|  | 
 | ||
|  | const { test } = require('tap') | ||
|  | const Fastify = require('..') | ||
|  | const fp = require('fastify-plugin') | ||
|  | const Ajv = require('ajv') | ||
|  | const { kSchemaController } = require('../lib/symbols.js') | ||
|  | 
 | ||
|  | const echoParams = (req, reply) => { reply.send(req.params) } | ||
|  | const echoBody = (req, reply) => { reply.send(req.body) } | ||
|  | 
 | ||
|  | ;['addSchema', 'getSchema', 'getSchemas', 'setValidatorCompiler', 'setSerializerCompiler'].forEach(f => { | ||
|  |   test(`Should expose ${f} function`, t => { | ||
|  |     t.plan(1) | ||
|  |     const fastify = Fastify() | ||
|  |     t.equal(typeof fastify[f], 'function') | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | ;['setValidatorCompiler', 'setSerializerCompiler'].forEach(f => { | ||
|  |   test(`cannot call ${f} after binding`, t => { | ||
|  |     t.plan(2) | ||
|  |     const fastify = Fastify() | ||
|  |     t.teardown(fastify.close.bind(fastify)) | ||
|  |     fastify.listen(0, err => { | ||
|  |       t.error(err) | ||
|  |       try { | ||
|  |         fastify[f](() => { }) | ||
|  |         t.fail() | ||
|  |       } catch (e) { | ||
|  |         t.pass() | ||
|  |       } | ||
|  |     }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('The schemas should be added to an internal storage', t => { | ||
|  |   t.plan(1) | ||
|  |   const fastify = Fastify() | ||
|  |   const schema = { $id: 'id', my: 'schema' } | ||
|  |   fastify.addSchema(schema) | ||
|  |   t.same(fastify[kSchemaController].schemaBucket.store, { id: schema }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('The schemas should be accessible via getSchemas', t => { | ||
|  |   t.plan(1) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   const schemas = { | ||
|  |     id: { $id: 'id', my: 'schema' }, | ||
|  |     abc: { $id: 'abc', my: 'schema' }, | ||
|  |     bcd: { $id: 'bcd', my: 'schema', properties: { a: 'a', b: 1 } } | ||
|  |   } | ||
|  | 
 | ||
|  |   Object.values(schemas).forEach(schema => { fastify.addSchema(schema) }) | ||
|  |   t.same(fastify.getSchemas(), schemas) | ||
|  | }) | ||
|  | 
 | ||
|  | test('The schema should be accessible by id via getSchema', t => { | ||
|  |   t.plan(5) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   const schemas = [ | ||
|  |     { $id: 'id', my: 'schema' }, | ||
|  |     { $id: 'abc', my: 'schema' }, | ||
|  |     { $id: 'bcd', my: 'schema', properties: { a: 'a', b: 1 } } | ||
|  |   ] | ||
|  |   schemas.forEach(schema => { fastify.addSchema(schema) }) | ||
|  |   t.same(fastify.getSchema('abc'), schemas[1]) | ||
|  |   t.same(fastify.getSchema('id'), schemas[0]) | ||
|  |   t.same(fastify.getSchema('foo'), undefined) | ||
|  | 
 | ||
|  |   fastify.register((instance, opts, done) => { | ||
|  |     const pluginSchema = { $id: 'cde', my: 'schema' } | ||
|  |     instance.addSchema(pluginSchema) | ||
|  |     t.same(instance.getSchema('cde'), pluginSchema) | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => t.error(err)) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Get validatorCompiler after setValidatorCompiler', t => { | ||
|  |   t.plan(2) | ||
|  |   const myCompiler = () => { } | ||
|  |   const fastify = Fastify() | ||
|  |   fastify.setValidatorCompiler(myCompiler) | ||
|  |   const sc = fastify.validatorCompiler | ||
|  |   t.ok(Object.is(myCompiler, sc)) | ||
|  |   fastify.ready(err => t.error(err)) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Get serializerCompiler after setSerializerCompiler', t => { | ||
|  |   t.plan(2) | ||
|  |   const myCompiler = () => { } | ||
|  |   const fastify = Fastify() | ||
|  |   fastify.setSerializerCompiler(myCompiler) | ||
|  |   const sc = fastify.serializerCompiler | ||
|  |   t.ok(Object.is(myCompiler, sc)) | ||
|  |   fastify.ready(err => t.error(err)) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Get compilers is empty when settle on routes', t => { | ||
|  |   t.plan(3) | ||
|  | 
 | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.post('/', { | ||
|  |     schema: { | ||
|  |       body: { type: 'object', properties: { hello: { type: 'string' } } }, | ||
|  |       response: { '2xx': { foo: { type: 'array', items: { type: 'string' } } } } | ||
|  |     }, | ||
|  |     validatorCompiler: ({ schema, method, url, httpPart }) => {}, | ||
|  |     serializerCompiler: ({ schema, method, url, httpPart }) => {} | ||
|  |   }, function (req, reply) { | ||
|  |     reply.send('ok') | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'POST', | ||
|  |     payload: {}, | ||
|  |     url: '/' | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(fastify.validatorCompiler, undefined) | ||
|  |     t.equal(fastify.serializerCompiler, undefined) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Should throw if the $id property is missing', t => { | ||
|  |   t.plan(1) | ||
|  |   const fastify = Fastify() | ||
|  |   try { | ||
|  |     fastify.addSchema({ type: 'string' }) | ||
|  |     t.fail() | ||
|  |   } catch (err) { | ||
|  |     t.equal(err.code, 'FST_ERR_SCH_MISSING_ID') | ||
|  |   } | ||
|  | }) | ||
|  | 
 | ||
|  | test('Cannot add multiple times the same id', t => { | ||
|  |   t.plan(2) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ $id: 'id' }) | ||
|  |   try { | ||
|  |     fastify.addSchema({ $id: 'id' }) | ||
|  |   } catch (err) { | ||
|  |     t.equal(err.code, 'FST_ERR_SCH_ALREADY_PRESENT') | ||
|  |     t.equal(err.message, 'Schema with id \'id\' already declared!') | ||
|  |   } | ||
|  | }) | ||
|  | 
 | ||
|  | test('Cannot add schema for query and querystring', t => { | ||
|  |   t.plan(2) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.get('/', { | ||
|  |     handler: () => {}, | ||
|  |     schema: { | ||
|  |       query: { foo: { type: 'string' } }, | ||
|  |       querystring: { foo: { type: 'string' } } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => { | ||
|  |     t.equal(err.code, 'FST_ERR_SCH_DUPLICATE') | ||
|  |     t.equal(err.message, 'Schema with \'querystring\' already present!') | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Should throw of the schema does not exists in input', t => { | ||
|  |   t.plan(2) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.get('/:id', { | ||
|  |     handler: echoParams, | ||
|  |     schema: { | ||
|  |       params: { | ||
|  |         name: { $ref: '#notExist' } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => { | ||
|  |     t.equal(err.code, 'FST_ERR_SCH_VALIDATION_BUILD') | ||
|  |     t.equal(err.message, "Failed building the validation schema for GET: /:id, due to error can't resolve reference #notExist from id #") | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Should throw of the schema does not exists in output', t => { | ||
|  |   t.plan(2) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.get('/:id', { | ||
|  |     handler: echoParams, | ||
|  |     schema: { | ||
|  |       response: { | ||
|  |         '2xx': { | ||
|  |           name: { $ref: '#notExist' } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => { | ||
|  |     t.equal(err.code, 'FST_ERR_SCH_SERIALIZATION_BUILD') | ||
|  |     t.match(err.message, /^Failed building the serialization schema for GET: \/:id, due to error Cannot read propert.*/) // error from fast-json-strinfigy
 | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Should not change the input schemas', t => { | ||
|  |   t.plan(4) | ||
|  | 
 | ||
|  |   const theSchema = { | ||
|  |     $id: 'helloSchema', | ||
|  |     type: 'object', | ||
|  |     definitions: { | ||
|  |       hello: { type: 'string' } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   const fastify = Fastify() | ||
|  |   fastify.post('/', { | ||
|  |     handler: echoBody, | ||
|  |     schema: { | ||
|  |       body: { | ||
|  |         type: 'object', | ||
|  |         additionalProperties: false, | ||
|  |         properties: { | ||
|  |           name: { $ref: 'helloSchema#/definitions/hello' } | ||
|  |         } | ||
|  |       }, | ||
|  |       response: { | ||
|  |         '2xx': { | ||
|  |           name: { $ref: 'helloSchema#/definitions/hello' } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  |   fastify.addSchema(theSchema) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     url: '/', | ||
|  |     method: 'POST', | ||
|  |     payload: { name: 'Foo', surname: 'Bar' } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.same(res.json(), { name: 'Foo' }) | ||
|  |     t.ok(theSchema.$id, 'the $id is not removed') | ||
|  |     t.same(fastify.getSchema('helloSchema'), theSchema) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('First level $ref', 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: req.params.id * 2, ignore: 'it' }) | ||
|  |     }, | ||
|  |     schema: { | ||
|  |       params: { $ref: 'test#' }, | ||
|  |       response: { | ||
|  |         200: { $ref: 'test#' } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'GET', | ||
|  |     url: '/123' | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.same(res.json(), { id: 246 }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Customize validator compiler in instance and route', t => { | ||
|  |   t.plan(28) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => { | ||
|  |     t.equal(method, 'POST') // run 4 times
 | ||
|  |     t.equal(url, '/:id') // run 4 times
 | ||
|  |     switch (httpPart) { | ||
|  |       case 'body': | ||
|  |         t.pass('body evaluated') | ||
|  |         return body => { | ||
|  |           t.same(body, { foo: ['bar', 'BAR'] }) | ||
|  |           return true | ||
|  |         } | ||
|  |       case 'params': | ||
|  |         t.pass('params evaluated') | ||
|  |         return params => { | ||
|  |           t.same(params, { id: 1234 }) | ||
|  |           return true | ||
|  |         } | ||
|  |       case 'querystring': | ||
|  |         t.pass('querystring evaluated') | ||
|  |         return query => { | ||
|  |           t.same(query, { lang: 'en' }) | ||
|  |           return true | ||
|  |         } | ||
|  |       case 'headers': | ||
|  |         t.pass('headers evaluated') | ||
|  |         return headers => { | ||
|  |           t.match(headers, { x: 'hello' }) | ||
|  |           return true | ||
|  |         } | ||
|  |       case '2xx': | ||
|  |         t.fail('the validator doesn\'t process the response') | ||
|  |         break | ||
|  |       default: | ||
|  |         t.fail(`unknown httpPart ${httpPart}`) | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.post('/:id', { | ||
|  |     handler: echoBody, | ||
|  |     schema: { | ||
|  |       query: { lang: { type: 'string', enum: ['it', 'en'] } }, | ||
|  |       headers: { x: { type: 'string' } }, | ||
|  |       params: { id: { type: 'number' } }, | ||
|  |       body: { foo: { type: 'array' } }, | ||
|  |       response: { | ||
|  |         '2xx': { foo: { type: 'array', items: { type: 'string' } } } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.get('/wow/:id', { | ||
|  |     handler: echoParams, | ||
|  |     validatorCompiler: ({ schema, method, url, httpPart }) => { | ||
|  |       t.equal(method, 'GET') // run 3 times (params, headers, query)
 | ||
|  |       t.equal(url, '/wow/:id') // run 4 times
 | ||
|  |       return () => { return true } // ignore the validation
 | ||
|  |     }, | ||
|  |     schema: { | ||
|  |       query: { lang: { type: 'string', enum: ['it', 'en'] } }, | ||
|  |       headers: { x: { type: 'string' } }, | ||
|  |       params: { id: { type: 'number' } }, | ||
|  |       response: { '2xx': { foo: { type: 'array', items: { type: 'string' } } } } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     url: '/1234', | ||
|  |     method: 'POST', | ||
|  |     headers: { x: 'hello' }, | ||
|  |     query: { lang: 'en' }, | ||
|  |     payload: { foo: ['bar', 'BAR'] } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 200) | ||
|  |     t.same(res.json(), { foo: ['bar', 'BAR'] }) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     url: '/wow/should-be-a-num', | ||
|  |     method: 'GET', | ||
|  |     headers: { x: 'hello' }, | ||
|  |     query: { lang: 'jp' } // not in the enum
 | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 200) // the validation is always true
 | ||
|  |     t.same(res.json(), {}) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Use the same schema across multiple routes', t => { | ||
|  |   t.plan(4) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'test', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       id: { type: 'number' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.get('/first/:id', { | ||
|  |     schema: { | ||
|  |       params: { id: { $ref: 'test#/properties/id' } } | ||
|  |     }, | ||
|  |     handler: (req, reply) => { | ||
|  |       reply.send(typeof req.params.id) | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.get('/second/:id', { | ||
|  |     schema: { | ||
|  |       params: { id: { $ref: 'test#/properties/id' } } | ||
|  |     }, | ||
|  |     handler: (req, reply) => { | ||
|  |       reply.send(typeof req.params.id) | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'GET', | ||
|  |     url: '/first/123' | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.payload, 'number') | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'GET', | ||
|  |     url: '/second/123' | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.payload, 'number') | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Encapsulation should intervene', t => { | ||
|  |   t.plan(2) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.register((instance, opts, done) => { | ||
|  |     instance.addSchema({ | ||
|  |       $id: 'encapsulation', | ||
|  |       type: 'object', | ||
|  |       properties: { | ||
|  |         id: { type: 'number' } | ||
|  |       } | ||
|  |     }) | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.register((instance, opts, done) => { | ||
|  |     instance.get('/:id', { | ||
|  |       handler: echoParams, | ||
|  |       schema: { | ||
|  |         params: { id: { $ref: 'encapsulation#/properties/id' } } | ||
|  |       } | ||
|  |     }) | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => { | ||
|  |     t.equal(err.code, 'FST_ERR_SCH_VALIDATION_BUILD') | ||
|  |     t.equal(err.message, "Failed building the validation schema for GET: /:id, due to error can't resolve reference encapsulation#/properties/id from id #") | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Encapsulation isolation', t => { | ||
|  |   t.plan(1) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.register((instance, opts, done) => { | ||
|  |     instance.addSchema({ $id: 'id' }) | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.register((instance, opts, done) => { | ||
|  |     instance.addSchema({ $id: 'id' }) | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => t.error(err)) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Add schema after register', t => { | ||
|  |   t.plan(5) | ||
|  | 
 | ||
|  |   const fastify = Fastify() | ||
|  |   fastify.register((instance, opts, done) => { | ||
|  |     instance.get('/:id', { | ||
|  |       handler: echoParams, | ||
|  |       schema: { | ||
|  |         params: { $ref: 'test#' } | ||
|  |       } | ||
|  |     }) | ||
|  | 
 | ||
|  |     // add it to the parent instance
 | ||
|  |     fastify.addSchema({ | ||
|  |       $id: 'test', | ||
|  |       type: 'object', | ||
|  |       properties: { | ||
|  |         id: { type: 'number' } | ||
|  |       } | ||
|  |     }) | ||
|  | 
 | ||
|  |     try { | ||
|  |       instance.addSchema({ $id: 'test' }) | ||
|  |     } catch (err) { | ||
|  |       t.equal(err.code, 'FST_ERR_SCH_ALREADY_PRESENT') | ||
|  |       t.equal(err.message, 'Schema with id \'test\' already declared!') | ||
|  |     } | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     method: 'GET', | ||
|  |     url: '/4242' | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 200) | ||
|  |     t.same(res.json(), { id: 4242 }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Encapsulation isolation for getSchemas', t => { | ||
|  |   t.plan(5) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   let pluginDeepOneSide | ||
|  |   let pluginDeepOne | ||
|  |   let pluginDeepTwo | ||
|  | 
 | ||
|  |   const schemas = { | ||
|  |     z: { $id: 'z', my: 'schema' }, | ||
|  |     a: { $id: 'a', my: 'schema' }, | ||
|  |     b: { $id: 'b', my: 'schema' }, | ||
|  |     c: { $id: 'c', my: 'schema', properties: { a: 'a', b: 1 } } | ||
|  |   } | ||
|  | 
 | ||
|  |   fastify.addSchema(schemas.z) | ||
|  | 
 | ||
|  |   fastify.register((instance, opts, done) => { | ||
|  |     instance.addSchema(schemas.a) | ||
|  |     pluginDeepOneSide = instance | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.register((instance, opts, done) => { | ||
|  |     instance.addSchema(schemas.b) | ||
|  |     instance.register((subinstance, opts, done) => { | ||
|  |       subinstance.addSchema(schemas.c) | ||
|  |       pluginDeepTwo = subinstance | ||
|  |       done() | ||
|  |     }) | ||
|  |     pluginDeepOne = instance | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => { | ||
|  |     t.error(err) | ||
|  |     t.same(fastify.getSchemas(), { z: schemas.z }) | ||
|  |     t.same(pluginDeepOneSide.getSchemas(), { z: schemas.z, a: schemas.a }) | ||
|  |     t.same(pluginDeepOne.getSchemas(), { z: schemas.z, b: schemas.b }) | ||
|  |     t.same(pluginDeepTwo.getSchemas(), { z: schemas.z, b: schemas.b, c: schemas.c }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Use the same schema id in different places', t => { | ||
|  |   t.plan(1) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'test', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       id: { type: 'number' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.get('/:id', { | ||
|  |     handler: echoParams, | ||
|  |     schema: { | ||
|  |       response: { | ||
|  |         200: { | ||
|  |           type: 'array', | ||
|  |           items: { $ref: 'test#/properties/id' } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.post('/:id', { | ||
|  |     handler: echoBody, | ||
|  |     schema: { | ||
|  |       body: { id: { $ref: 'test#/properties/id' } }, | ||
|  |       response: { | ||
|  |         200: { id: { $ref: 'test#/properties/id' } } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => t.error(err)) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Get schema anyway should not add `properties` if allOf is present', t => { | ||
|  |   t.plan(1) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'first', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       first: { type: 'number' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'second', | ||
|  |     type: 'object', | ||
|  |     allOf: [ | ||
|  |       { | ||
|  |         type: 'object', | ||
|  |         properties: { | ||
|  |           second: { type: 'number' } | ||
|  |         } | ||
|  |       }, | ||
|  |       fastify.getSchema('first') | ||
|  |     ] | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.get('/', { | ||
|  |     handler: () => {}, | ||
|  |     schema: { | ||
|  |       querystring: fastify.getSchema('second'), | ||
|  |       response: { 200: fastify.getSchema('second') } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => t.error(err)) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Get schema anyway should not add `properties` if oneOf is present', t => { | ||
|  |   t.plan(1) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'first', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       first: { type: 'number' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'second', | ||
|  |     type: 'object', | ||
|  |     oneOf: [ | ||
|  |       { | ||
|  |         type: 'object', | ||
|  |         properties: { | ||
|  |           second: { type: 'number' } | ||
|  |         } | ||
|  |       }, | ||
|  |       fastify.getSchema('first') | ||
|  |     ] | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.get('/', { | ||
|  |     handler: () => {}, | ||
|  |     schema: { | ||
|  |       querystring: fastify.getSchema('second'), | ||
|  |       response: { 200: fastify.getSchema('second') } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => t.error(err)) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Get schema anyway should not add `properties` if anyOf is present', t => { | ||
|  |   t.plan(1) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'first', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       first: { type: 'number' } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'second', | ||
|  |     type: 'object', | ||
|  |     anyOf: [ | ||
|  |       { | ||
|  |         type: 'object', | ||
|  |         properties: { | ||
|  |           second: { type: 'number' } | ||
|  |         } | ||
|  |       }, | ||
|  |       fastify.getSchema('first') | ||
|  |     ] | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.get('/', { | ||
|  |     handler: () => {}, | ||
|  |     schema: { | ||
|  |       querystring: fastify.getSchema('second'), | ||
|  |       response: { 200: fastify.getSchema('second') } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => t.error(err)) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Shared schema should be ignored in string enum', t => { | ||
|  |   t.plan(2) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.get('/:lang', { | ||
|  |     handler: echoParams, | ||
|  |     schema: { | ||
|  |       params: { | ||
|  |         type: 'object', | ||
|  |         properties: { | ||
|  |           lang: { | ||
|  |             type: 'string', | ||
|  |             enum: ['Javascript', 'C++', 'C#'] | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject('/C%23', (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.same(res.json(), { lang: 'C#' }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Shared schema should NOT be ignored in != string enum', t => { | ||
|  |   t.plan(2) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'C', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       lang: { | ||
|  |         type: 'string', | ||
|  |         enum: ['Javascript', 'C++', 'C#'] | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.post('/:lang', { | ||
|  |     handler: echoBody, | ||
|  |     schema: { | ||
|  |       body: fastify.getSchema('C') | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.inject({ | ||
|  |     url: '/', | ||
|  |     method: 'POST', | ||
|  |     payload: { lang: 'C#' } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.same(res.json(), { lang: 'C#' }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Case insensitive header validation', t => { | ||
|  |   t.plan(2) | ||
|  |   const fastify = Fastify() | ||
|  |   fastify.get('/', { | ||
|  |     handler: (req, reply) => { | ||
|  |       reply.code(200).send(req.headers.foobar) | ||
|  |     }, | ||
|  |     schema: { | ||
|  |       headers: { | ||
|  |         type: 'object', | ||
|  |         required: ['FooBar'], | ||
|  |         properties: { | ||
|  |           FooBar: { type: 'string' } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  |   fastify.inject({ | ||
|  |     url: '/', | ||
|  |     method: 'GET', | ||
|  |     headers: { | ||
|  |       FooBar: 'Baz' | ||
|  |     } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.payload, 'Baz') | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Not evaluate json-schema $schema keyword', t => { | ||
|  |   t.plan(2) | ||
|  |   const fastify = Fastify() | ||
|  |   fastify.post('/', { | ||
|  |     handler: echoBody, | ||
|  |     schema: { | ||
|  |       body: { | ||
|  |         $schema: 'http://json-schema.org/draft-07/schema#', | ||
|  |         type: 'object', | ||
|  |         additionalProperties: false, | ||
|  |         properties: { | ||
|  |           hello: { | ||
|  |             type: 'string' | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  |   fastify.inject({ | ||
|  |     url: '/', | ||
|  |     method: 'POST', | ||
|  |     body: { hello: 'world', foo: 'bar' } | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.same(res.json(), { hello: 'world' }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Validation context in validation result', t => { | ||
|  |   t.plan(5) | ||
|  |   const fastify = Fastify() | ||
|  |   // custom error handler to expose validation context in response, so we can test it later
 | ||
|  |   fastify.setErrorHandler((err, request, reply) => { | ||
|  |     t.equal(err instanceof Error, true) | ||
|  |     t.ok(err.validation, 'detailed errors') | ||
|  |     t.equal(err.validationContext, 'body') | ||
|  |     reply.send() | ||
|  |   }) | ||
|  |   fastify.get('/', { | ||
|  |     handler: echoParams, | ||
|  |     schema: { | ||
|  |       body: { | ||
|  |         type: 'object', | ||
|  |         required: ['hello'], | ||
|  |         properties: { | ||
|  |           hello: { type: 'string' } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  |   fastify.inject({ | ||
|  |     method: 'GET', | ||
|  |     url: '/', | ||
|  |     payload: {} // body lacks required field, will fail validation
 | ||
|  |   }, (err, res) => { | ||
|  |     t.error(err) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('The schema build should not modify the input', t => { | ||
|  |   t.plan(3) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   const first = { | ||
|  |     $id: 'first', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       first: { | ||
|  |         type: 'number' | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   fastify.addSchema(first) | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'second', | ||
|  |     type: 'object', | ||
|  |     allOf: [ | ||
|  |       { | ||
|  |         type: 'object', | ||
|  |         properties: { | ||
|  |           second: { | ||
|  |             type: 'number' | ||
|  |           } | ||
|  |         } | ||
|  |       }, | ||
|  |       { $ref: 'first#' } | ||
|  |     ] | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.get('/', { | ||
|  |     schema: { | ||
|  |       description: 'get', | ||
|  |       body: { $ref: 'second#' }, | ||
|  |       response: { | ||
|  |         200: { $ref: 'second#' } | ||
|  |       } | ||
|  |     }, | ||
|  |     handler: (request, reply) => { | ||
|  |       reply.send({ hello: 'world' }) | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.patch('/', { | ||
|  |     schema: { | ||
|  |       description: 'patch', | ||
|  |       body: { $ref: 'first#' }, | ||
|  |       response: { | ||
|  |         200: { $ref: 'first#' } | ||
|  |       } | ||
|  |     }, | ||
|  |     handler: (request, reply) => { | ||
|  |       reply.send({ hello: 'world' }) | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   t.ok(first.$id) | ||
|  |   fastify.ready(err => { | ||
|  |     t.error(err) | ||
|  |     t.ok(first.$id) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Cross schema reference with encapsulation references', t => { | ||
|  |   t.plan(1) | ||
|  | 
 | ||
|  |   const fastify = Fastify() | ||
|  |   fastify.addSchema({ $id: 'http://foo/item', type: 'object', properties: { foo: { type: 'string' } } }) | ||
|  | 
 | ||
|  |   const refItem = { $ref: 'http://foo/item#' } | ||
|  | 
 | ||
|  |   fastify.addSchema({ | ||
|  |     $id: 'itemList', | ||
|  |     type: 'array', | ||
|  |     items: refItem | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.register((instance, opts, done) => { | ||
|  |     instance.addSchema({ | ||
|  |       $id: 'encapsulation', | ||
|  |       type: 'object', | ||
|  |       properties: { | ||
|  |         id: { type: 'number' }, | ||
|  |         item: refItem, | ||
|  |         secondItem: refItem | ||
|  |       } | ||
|  |     }) | ||
|  | 
 | ||
|  |     const multipleRef = { | ||
|  |       type: 'object', | ||
|  |       properties: { | ||
|  |         a: { $ref: 'itemList#' }, | ||
|  |         b: refItem, | ||
|  |         c: refItem, | ||
|  |         d: refItem | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     instance.get('/get', { schema: { response: { 200: multipleRef } } }, () => { }) | ||
|  |     instance.get('/double-get', { schema: { body: multipleRef, response: { 200: multipleRef } } }, () => { }) | ||
|  |     instance.post('/post', { schema: { body: multipleRef, response: { 200: multipleRef } } }, () => { }) | ||
|  |     instance.post('/double', { schema: { response: { 200: { $ref: 'encapsulation' } } } }, () => { }) | ||
|  |     done() | ||
|  |   }, { prefix: '/foo' }) | ||
|  | 
 | ||
|  |   fastify.post('/post', { schema: { body: refItem, response: { 200: refItem } } }, () => { }) | ||
|  |   fastify.get('/get', { schema: { body: refItem, response: { 200: refItem } } }, () => { }) | ||
|  | 
 | ||
|  |   fastify.ready(err => { | ||
|  |     t.error(err) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Check how many AJV instances are built #1', t => { | ||
|  |   t.plan(12) | ||
|  |   const fastify = Fastify() | ||
|  |   addRandomRoute(fastify) // this trigger the schema validation creation
 | ||
|  |   t.notOk(fastify.validatorCompiler, 'validator not initialized') | ||
|  | 
 | ||
|  |   const instances = [] | ||
|  |   fastify.register((instance, opts, done) => { | ||
|  |     t.notOk(fastify.validatorCompiler, 'validator not initialized') | ||
|  |     instances.push(instance) | ||
|  |     done() | ||
|  |   }) | ||
|  |   fastify.register((instance, opts, done) => { | ||
|  |     t.notOk(fastify.validatorCompiler, 'validator not initialized') | ||
|  |     addRandomRoute(instance) | ||
|  |     instances.push(instance) | ||
|  |     done() | ||
|  |     instance.register((instance, opts, done) => { | ||
|  |       t.notOk(fastify.validatorCompiler, 'validator not initialized') | ||
|  |       addRandomRoute(instance) | ||
|  |       instances.push(instance) | ||
|  |       done() | ||
|  |     }) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => { | ||
|  |     t.error(err) | ||
|  | 
 | ||
|  |     t.ok(fastify.validatorCompiler, 'validator initialized on preReady') | ||
|  |     fastify.validatorCompiler.checkPointer = true | ||
|  |     instances.forEach(i => { | ||
|  |       t.ok(i.validatorCompiler, 'validator initialized on preReady') | ||
|  |       t.equal(i.validatorCompiler.checkPointer, true, 'validator is only one for all the instances') | ||
|  |     }) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('onReady hook has the compilers ready', t => { | ||
|  |   t.plan(6) | ||
|  | 
 | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.get(`/${Math.random()}`, { | ||
|  |     handler: (req, reply) => reply.send(), | ||
|  |     schema: { | ||
|  |       body: { type: 'object' }, | ||
|  |       response: { 200: { type: 'object' } } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.addHook('onReady', function (done) { | ||
|  |     t.ok(this.validatorCompiler) | ||
|  |     t.ok(this.serializerCompiler) | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   let hookCallCounter = 0 | ||
|  |   fastify.register(async (i, o) => { | ||
|  |     i.addHook('onReady', function (done) { | ||
|  |       t.ok(this.validatorCompiler) | ||
|  |       t.ok(this.serializerCompiler) | ||
|  |       done() | ||
|  |     }) | ||
|  | 
 | ||
|  |     i.register(async (i, o) => {}) | ||
|  | 
 | ||
|  |     i.addHook('onReady', function (done) { | ||
|  |       hookCallCounter++ | ||
|  |       done() | ||
|  |     }) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => { | ||
|  |     t.error(err) | ||
|  |     t.equal(hookCallCounter, 1, 'it is called once') | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Check how many AJV instances are built #2 - verify validatorPool', t => { | ||
|  |   t.plan(13) | ||
|  |   const fastify = Fastify() | ||
|  |   t.notOk(fastify.validatorCompiler, 'validator not initialized') | ||
|  | 
 | ||
|  |   fastify.register(function sibling1 (instance, opts, done) { | ||
|  |     addRandomRoute(instance) | ||
|  |     t.notOk(instance.validatorCompiler, 'validator not initialized') | ||
|  |     instance.ready(() => { | ||
|  |       t.ok(instance.validatorCompiler, 'validator is initialized') | ||
|  |       instance.validatorCompiler.sharedPool = 1 | ||
|  |     }) | ||
|  |     instance.after(() => { | ||
|  |       t.notOk(instance.validatorCompiler, 'validator not initialized') | ||
|  |     }) | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.register(function sibling2 (instance, opts, done) { | ||
|  |     addRandomRoute(instance) | ||
|  |     t.notOk(instance.validatorCompiler, 'validator not initialized') | ||
|  |     instance.ready(() => { | ||
|  |       t.equal(instance.validatorCompiler.sharedPool, 1, 'this context must share the validator with the same schemas') | ||
|  |       instance.validatorCompiler.sharedPool = 2 | ||
|  |     }) | ||
|  |     instance.after(() => { | ||
|  |       t.notOk(instance.validatorCompiler, 'validator not initialized') | ||
|  |     }) | ||
|  | 
 | ||
|  |     instance.register((instance, opts, done) => { | ||
|  |       t.notOk(instance.validatorCompiler, 'validator not initialized') | ||
|  |       instance.ready(() => { | ||
|  |         t.equal(instance.validatorCompiler.sharedPool, 2, 'this context must share the validator of the parent') | ||
|  |       }) | ||
|  |       done() | ||
|  |     }) | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.register(function sibling3 (instance, opts, done) { | ||
|  |     addRandomRoute(instance) | ||
|  | 
 | ||
|  |     // this trigger to dont't reuse the same compiler pool
 | ||
|  |     instance.addSchema({ $id: 'diff', type: 'object' }) | ||
|  | 
 | ||
|  |     t.notOk(instance.validatorCompiler, 'validator not initialized') | ||
|  |     instance.ready(() => { | ||
|  |       t.ok(instance.validatorCompiler, 'validator is initialized') | ||
|  |       t.notOk(instance.validatorCompiler.sharedPool, 'this context has its own compiler') | ||
|  |     }) | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => { t.error(err) }) | ||
|  | }) | ||
|  | 
 | ||
|  | function addRandomRoute (server) { | ||
|  |   server.get(`/${Math.random()}`, | ||
|  |     { schema: { body: { type: 'object' } } }, | ||
|  |     (req, reply) => reply.send() | ||
|  |   ) | ||
|  | } | ||
|  | 
 | ||
|  | test('Add schema order should not break the startup', t => { | ||
|  |   t.plan(1) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.get('/', { schema: { random: 'options' } }, () => {}) | ||
|  | 
 | ||
|  |   fastify.register(fp((f, opts) => { | ||
|  |     f.addSchema({ | ||
|  |       $id: 'https://example.com/bson/objectId', | ||
|  |       type: 'string', | ||
|  |       pattern: '\\b[0-9A-Fa-f]{24}\\b' | ||
|  |     }) | ||
|  |     return Promise.resolve() // avoid async for node 6
 | ||
|  |   })) | ||
|  | 
 | ||
|  |   fastify.get('/:id', { | ||
|  |     schema: { | ||
|  |       params: { | ||
|  |         type: 'object', | ||
|  |         properties: { | ||
|  |           id: { $ref: 'https://example.com/bson/objectId#' } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   }, () => {}) | ||
|  | 
 | ||
|  |   fastify.ready(err => { t.error(err) }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('The schema compiler recreate itself if needed', t => { | ||
|  |   t.plan(1) | ||
|  |   const fastify = Fastify() | ||
|  | 
 | ||
|  |   fastify.options('/', { schema: { hide: true } }, echoBody) | ||
|  | 
 | ||
|  |   fastify.register(function (fastify, options, done) { | ||
|  |     fastify.addSchema({ | ||
|  |       $id: 'identifier', | ||
|  |       type: 'string', | ||
|  |       format: 'uuid' | ||
|  |     }) | ||
|  | 
 | ||
|  |     fastify.get('/:foobarId', { | ||
|  |       schema: { | ||
|  |         params: { | ||
|  |           foobarId: { $ref: 'identifier#' } | ||
|  |         } | ||
|  |       } | ||
|  |     }, echoBody) | ||
|  | 
 | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => { t.error(err) }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('Schema controller setter', t => { | ||
|  |   t.plan(2) | ||
|  |   Fastify({ schemaController: {} }) | ||
|  |   t.pass('allow empty object') | ||
|  | 
 | ||
|  |   try { | ||
|  |     Fastify({ schemaController: { bucket: {} } }) | ||
|  |     t.fail('the bucket option must be a function') | ||
|  |   } catch (err) { | ||
|  |     t.equal(err.message, "schemaController.bucket option should be a function, instead got 'object'") | ||
|  |   } | ||
|  | }) | ||
|  | 
 | ||
|  | test('Schema controller bucket', t => { | ||
|  |   t.plan(10) | ||
|  | 
 | ||
|  |   let added = 0 | ||
|  |   let builtBucket = 0 | ||
|  | 
 | ||
|  |   const initStoreQueue = [] | ||
|  | 
 | ||
|  |   function factoryBucket (storeInit) { | ||
|  |     builtBucket++ | ||
|  |     t.same(initStoreQueue.pop(), storeInit) | ||
|  |     const store = new Map(storeInit) | ||
|  |     return { | ||
|  |       add (schema) { | ||
|  |         added++ | ||
|  |         store.set(schema.$id, schema) | ||
|  |       }, | ||
|  |       getSchema (id) { | ||
|  |         return store.get(id) | ||
|  |       }, | ||
|  |       getSchemas () { | ||
|  |         // what is returned by this function, will be the `storeInit` parameter
 | ||
|  |         initStoreQueue.push(store) | ||
|  |         return store | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   const fastify = Fastify({ | ||
|  |     schemaController: { | ||
|  |       bucket: factoryBucket | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.register(async (instance) => { | ||
|  |     instance.addSchema({ $id: 'b', type: 'string' }) | ||
|  |     instance.addHook('onReady', function (done) { | ||
|  |       t.equal(instance.getSchemas().size, 2) | ||
|  |       done() | ||
|  |     }) | ||
|  |     instance.register(async (subinstance) => { | ||
|  |       subinstance.addSchema({ $id: 'c', type: 'string' }) | ||
|  |       subinstance.addHook('onReady', function (done) { | ||
|  |         t.equal(subinstance.getSchemas().size, 3) | ||
|  |         done() | ||
|  |       }) | ||
|  |     }) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.register(async (instance) => { | ||
|  |     instance.addHook('onReady', function (done) { | ||
|  |       t.equal(instance.getSchemas().size, 1) | ||
|  |       done() | ||
|  |     }) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.addSchema({ $id: 'a', type: 'string' }) | ||
|  | 
 | ||
|  |   fastify.ready(err => { | ||
|  |     t.error(err) | ||
|  |     t.equal(added, 3, 'three schema added') | ||
|  |     t.equal(builtBucket, 4, 'one bucket built for every register call + 1 for the root instance') | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('setSchemaController per instance', t => { | ||
|  |   t.plan(7) | ||
|  |   const fastify = Fastify({}) | ||
|  | 
 | ||
|  |   fastify.register(async (instance1) => { | ||
|  |     instance1.setSchemaController({ | ||
|  |       bucket: function factoryBucket (storeInit) { | ||
|  |         t.pass('instance1 has created the bucket') | ||
|  |         return { | ||
|  |           add (schema) { t.fail('add is not called') }, | ||
|  |           getSchema (id) { t.fail('getSchema is not called') }, | ||
|  |           getSchemas () { t.fail('getSchemas is not called') } | ||
|  |         } | ||
|  |       } | ||
|  |     }) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.register(async (instance2) => { | ||
|  |     const bSchema = { $id: 'b', type: 'string' } | ||
|  | 
 | ||
|  |     instance2.setSchemaController({ | ||
|  |       bucket: function factoryBucket (storeInit) { | ||
|  |         t.pass('instance2 has created the bucket') | ||
|  |         const map = {} | ||
|  |         return { | ||
|  |           add (schema) { | ||
|  |             t.equal(schema.$id, bSchema.$id, 'add is called') | ||
|  |             map[schema.$id] = schema | ||
|  |           }, | ||
|  |           getSchema (id) { | ||
|  |             t.pass('getSchema is called') | ||
|  |             return map[id] | ||
|  |           }, | ||
|  |           getSchemas () { | ||
|  |             t.pass('getSchemas is called') | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     }) | ||
|  | 
 | ||
|  |     instance2.addSchema(bSchema) | ||
|  | 
 | ||
|  |     instance2.addHook('onReady', function (done) { | ||
|  |       instance2.getSchemas() | ||
|  |       t.same(instance2.getSchema('b'), bSchema, 'the schema are loaded') | ||
|  |       done() | ||
|  |     }) | ||
|  |   }) | ||
|  | 
 | ||
|  |   fastify.ready(err => { t.error(err) }) | ||
|  | }) | ||
|  | 
 | ||
|  | test('setSchemaController: Inherits correctly parent schemas with a customized validator instance', async t => { | ||
|  |   t.plan(5) | ||
|  |   const customAjv = new Ajv({ coerceTypes: false }) | ||
|  |   const server = Fastify() | ||
|  |   const someSchema = { | ||
|  |     $id: 'some', | ||
|  |     type: 'array', | ||
|  |     items: { | ||
|  |       type: 'string' | ||
|  |     } | ||
|  |   } | ||
|  |   const errorResponseSchema = { | ||
|  |     $id: 'error_response', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       statusCode: { | ||
|  |         type: 'integer' | ||
|  |       }, | ||
|  |       message: { | ||
|  |         type: 'string' | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   server.addSchema(someSchema) | ||
|  |   server.addSchema(errorResponseSchema) | ||
|  | 
 | ||
|  |   server.register((instance, _, done) => { | ||
|  |     instance.setSchemaController({ | ||
|  |       compilersFactory: { | ||
|  |         buildValidator: function (externalSchemas) { | ||
|  |           const schemaKeys = Object.keys(externalSchemas) | ||
|  |           t.equal(schemaKeys.length, 2, 'Contains same number of schemas') | ||
|  |           t.hasStrict([someSchema, errorResponseSchema], Object.values(externalSchemas), 'Contains expected schemas') | ||
|  |           for (const key of schemaKeys) { | ||
|  |             if (customAjv.getSchema(key) == null) { | ||
|  |               customAjv.addSchema(externalSchemas[key], key) | ||
|  |             } | ||
|  |           } | ||
|  |           return function validatorCompiler ({ schema }) { | ||
|  |             return customAjv.compile(schema) | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     }) | ||
|  | 
 | ||
|  |     instance.get( | ||
|  |       '/', | ||
|  |       { | ||
|  |         schema: { | ||
|  |           querystring: { | ||
|  |             msg: { | ||
|  |               $ref: 'some#' | ||
|  |             } | ||
|  |           }, | ||
|  |           response: { | ||
|  |             '4xx': { | ||
|  |               $ref: 'error_response#' | ||
|  |             } | ||
|  |           } | ||
|  |         } | ||
|  |       }, | ||
|  |       (req, reply) => { | ||
|  |         reply.send({ noop: 'noop' }) | ||
|  |       } | ||
|  |     ) | ||
|  | 
 | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   const res = await server.inject({ | ||
|  |     method: 'GET', | ||
|  |     url: '/', | ||
|  |     query: { | ||
|  |       msg: 'string' | ||
|  |     } | ||
|  |   }) | ||
|  |   const json = res.json() | ||
|  | 
 | ||
|  |   t.equal(json.message, 'querystring.msg should be array') | ||
|  |   t.equal(json.statusCode, 400) | ||
|  |   t.equal(res.statusCode, 400, 'Should not coearce the string into array') | ||
|  | }) | ||
|  | 
 | ||
|  | test('setSchemaController: Inherits buildSerializer from parent if not present within the instance', async t => { | ||
|  |   t.plan(6) | ||
|  |   const customAjv = new Ajv({ coerceTypes: false }) | ||
|  |   const someSchema = { | ||
|  |     $id: 'some', | ||
|  |     type: 'array', | ||
|  |     items: { | ||
|  |       type: 'string' | ||
|  |     } | ||
|  |   } | ||
|  |   const errorResponseSchema = { | ||
|  |     $id: 'error_response', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       statusCode: { | ||
|  |         type: 'integer' | ||
|  |       }, | ||
|  |       message: { | ||
|  |         type: 'string' | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  |   let rootSerializerCalled = 0 | ||
|  |   let rootValidatorCalled = 0 | ||
|  |   let childValidatorCalled = 0 | ||
|  |   const rootBuildSerializer = function (externalSchemas) { | ||
|  |     rootSerializerCalled++ | ||
|  |     return function serializer () { | ||
|  |       return data => { | ||
|  |         return JSON.stringify({ | ||
|  |           statusCode: data.statusCode, | ||
|  |           message: data.message | ||
|  |         }) | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  |   const rootBuildValidator = function (externalSchemas) { | ||
|  |     rootValidatorCalled++ | ||
|  |     return function validatorCompiler ({ schema }) { | ||
|  |       return customAjv.compile(schema) | ||
|  |     } | ||
|  |   } | ||
|  |   const server = Fastify({ | ||
|  |     schemaController: { | ||
|  |       compilersFactory: { | ||
|  |         buildValidator: rootBuildValidator, | ||
|  |         buildSerializer: rootBuildSerializer | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   server.addSchema(someSchema) | ||
|  |   server.addSchema(errorResponseSchema) | ||
|  | 
 | ||
|  |   server.register((instance, _, done) => { | ||
|  |     instance.setSchemaController({ | ||
|  |       compilersFactory: { | ||
|  |         buildValidator: function (externalSchemas) { | ||
|  |           childValidatorCalled++ | ||
|  |           const schemaKeys = Object.keys(externalSchemas) | ||
|  |           for (const key of schemaKeys) { | ||
|  |             if (customAjv.getSchema(key) == null) { | ||
|  |               customAjv.addSchema(externalSchemas[key], key) | ||
|  |             } | ||
|  |           } | ||
|  |           return function validatorCompiler ({ schema }) { | ||
|  |             return customAjv.compile(schema) | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     }) | ||
|  | 
 | ||
|  |     instance.get( | ||
|  |       '/', | ||
|  |       { | ||
|  |         schema: { | ||
|  |           querystring: { | ||
|  |             msg: { | ||
|  |               $ref: 'some#' | ||
|  |             } | ||
|  |           }, | ||
|  |           response: { | ||
|  |             '4xx': { | ||
|  |               $ref: 'error_response#' | ||
|  |             } | ||
|  |           } | ||
|  |         } | ||
|  |       }, | ||
|  |       (req, reply) => { | ||
|  |         reply.send({ noop: 'noop' }) | ||
|  |       } | ||
|  |     ) | ||
|  | 
 | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   const res = await server.inject({ | ||
|  |     method: 'GET', | ||
|  |     url: '/', | ||
|  |     query: { | ||
|  |       msg: 'string' | ||
|  |     } | ||
|  |   }) | ||
|  |   const json = res.json() | ||
|  | 
 | ||
|  |   t.equal(json.statusCode, 400) | ||
|  |   t.equal(json.message, 'querystring.msg should be array') | ||
|  |   t.equal(rootSerializerCalled, 1, 'Should be called from the child') | ||
|  |   t.equal(rootValidatorCalled, 0, 'Should not be called from the child') | ||
|  |   t.equal(childValidatorCalled, 1, 'Should be called from the child') | ||
|  |   t.equal(res.statusCode, 400, 'Should not coerce the string into array') | ||
|  | }) | ||
|  | 
 | ||
|  | test('setSchemaController: Inherits buildValidator from parent if not present within the instance', async t => { | ||
|  |   t.plan(6) | ||
|  |   const customAjv = new Ajv({ coerceTypes: false }) | ||
|  |   const someSchema = { | ||
|  |     $id: 'some', | ||
|  |     type: 'array', | ||
|  |     items: { | ||
|  |       type: 'string' | ||
|  |     } | ||
|  |   } | ||
|  |   const errorResponseSchema = { | ||
|  |     $id: 'error_response', | ||
|  |     type: 'object', | ||
|  |     properties: { | ||
|  |       statusCode: { | ||
|  |         type: 'integer' | ||
|  |       }, | ||
|  |       message: { | ||
|  |         type: 'string' | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  |   let rootSerializerCalled = 0 | ||
|  |   let rootValidatorCalled = 0 | ||
|  |   let childSerializerCalled = 0 | ||
|  |   const rootBuildSerializer = function (externalSchemas) { | ||
|  |     rootSerializerCalled++ | ||
|  |     return function serializer () { | ||
|  |       return data => JSON.stringify(data) | ||
|  |     } | ||
|  |   } | ||
|  |   const rootBuildValidator = function (externalSchemas) { | ||
|  |     rootValidatorCalled++ | ||
|  |     const schemaKeys = Object.keys(externalSchemas) | ||
|  |     for (const key of schemaKeys) { | ||
|  |       if (customAjv.getSchema(key) == null) { | ||
|  |         customAjv.addSchema(externalSchemas[key], key) | ||
|  |       } | ||
|  |     } | ||
|  |     return function validatorCompiler ({ schema }) { | ||
|  |       return customAjv.compile(schema) | ||
|  |     } | ||
|  |   } | ||
|  |   const server = Fastify({ | ||
|  |     schemaController: { | ||
|  |       compilersFactory: { | ||
|  |         buildValidator: rootBuildValidator, | ||
|  |         buildSerializer: rootBuildSerializer | ||
|  |       } | ||
|  |     } | ||
|  |   }) | ||
|  | 
 | ||
|  |   server.register((instance, _, done) => { | ||
|  |     instance.register((subInstance, _, subDone) => { | ||
|  |       subInstance.setSchemaController({ | ||
|  |         compilersFactory: { | ||
|  |           buildSerializer: function (externalSchemas) { | ||
|  |             childSerializerCalled++ | ||
|  |             return function serializerCompiler () { | ||
|  |               return data => { | ||
|  |                 return JSON.stringify({ | ||
|  |                   statusCode: data.statusCode, | ||
|  |                   message: data.message | ||
|  |                 }) | ||
|  |               } | ||
|  |             } | ||
|  |           } | ||
|  |         } | ||
|  |       }) | ||
|  | 
 | ||
|  |       subInstance.get( | ||
|  |         '/', | ||
|  |         { | ||
|  |           schema: { | ||
|  |             querystring: { | ||
|  |               msg: { | ||
|  |                 $ref: 'some#' | ||
|  |               } | ||
|  |             }, | ||
|  |             response: { | ||
|  |               '4xx': { | ||
|  |                 $ref: 'error_response#' | ||
|  |               } | ||
|  |             } | ||
|  |           } | ||
|  |         }, | ||
|  |         (req, reply) => { | ||
|  |           reply.send({ noop: 'noop' }) | ||
|  |         } | ||
|  |       ) | ||
|  | 
 | ||
|  |       subDone() | ||
|  |     }) | ||
|  | 
 | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   server.addSchema(someSchema) | ||
|  |   server.addSchema(errorResponseSchema) | ||
|  | 
 | ||
|  |   const res = await server.inject({ | ||
|  |     method: 'GET', | ||
|  |     url: '/', | ||
|  |     query: { | ||
|  |       msg: ['string'] | ||
|  |     } | ||
|  |   }) | ||
|  |   const json = res.json() | ||
|  | 
 | ||
|  |   t.equal(json.statusCode, 400) | ||
|  |   t.equal(json.message, 'querystring.msg should be array') | ||
|  |   t.equal(rootSerializerCalled, 0, 'Should be called from the child') | ||
|  |   t.equal(rootValidatorCalled, 1, 'Should not be called from the child') | ||
|  |   t.equal(childSerializerCalled, 1, 'Should be called from the child') | ||
|  |   t.equal(res.statusCode, 400, 'Should not coearce the string into array') | ||
|  | }) | ||
|  | 
 | ||
|  | test('Should throw if not default validator passed', async t => { | ||
|  |   t.plan(4) | ||
|  |   const customAjv = new Ajv({ coerceTypes: false }) | ||
|  |   const someSchema = { | ||
|  |     $id: 'some', | ||
|  |     type: 'array', | ||
|  |     items: { | ||
|  |       type: 'string' | ||
|  |     } | ||
|  |   } | ||
|  |   const anotherSchema = { | ||
|  |     $id: 'another', | ||
|  |     type: 'integer' | ||
|  |   } | ||
|  |   const plugin = fp(function (pluginInstance, _, pluginDone) { | ||
|  |     pluginInstance.setSchemaController({ | ||
|  |       compilersFactory: { | ||
|  |         buildValidator: function (externalSchemas) { | ||
|  |           const schemaKeys = Object.keys(externalSchemas) | ||
|  |           t.equal(schemaKeys.length, 2) | ||
|  |           t.same(schemaKeys, ['some', 'another']) | ||
|  | 
 | ||
|  |           for (const key of schemaKeys) { | ||
|  |             if (customAjv.getSchema(key) == null) { | ||
|  |               customAjv.addSchema(externalSchemas[key], key) | ||
|  |             } | ||
|  |           } | ||
|  |           return function validatorCompiler ({ schema }) { | ||
|  |             return customAjv.compile(schema) | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     }) | ||
|  | 
 | ||
|  |     pluginDone() | ||
|  |   }) | ||
|  |   const server = Fastify() | ||
|  | 
 | ||
|  |   server.addSchema(someSchema) | ||
|  | 
 | ||
|  |   server.register((instance, opts, done) => { | ||
|  |     instance.addSchema(anotherSchema) | ||
|  | 
 | ||
|  |     instance.register(plugin, {}) | ||
|  | 
 | ||
|  |     instance.post( | ||
|  |       '/', | ||
|  |       { | ||
|  |         schema: { | ||
|  |           query: { | ||
|  |             msg: { | ||
|  |               $ref: 'some#' | ||
|  |             } | ||
|  |           }, | ||
|  |           headers: { | ||
|  |             'x-another': { | ||
|  |               $ref: 'another#' | ||
|  |             } | ||
|  |           } | ||
|  |         } | ||
|  |       }, | ||
|  |       (req, reply) => { | ||
|  |         reply.send({ noop: 'noop' }) | ||
|  |       } | ||
|  |     ) | ||
|  | 
 | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   try { | ||
|  |     const res = await server.inject({ | ||
|  |       method: 'POST', | ||
|  |       url: '/', | ||
|  |       query: { | ||
|  |         msg: ['string'] | ||
|  |       } | ||
|  |     }) | ||
|  | 
 | ||
|  |     t.equal(res.json().message, 'querystring.msg should be array') | ||
|  |     t.equal(res.statusCode, 400, 'Should not coearce the string into array') | ||
|  |   } catch (err) { | ||
|  |     t.error(err) | ||
|  |   } | ||
|  | }) | ||
|  | 
 | ||
|  | test('Should throw if not default validator passed', async t => { | ||
|  |   t.plan(2) | ||
|  |   const someSchema = { | ||
|  |     $id: 'some', | ||
|  |     type: 'array', | ||
|  |     items: { | ||
|  |       type: 'string' | ||
|  |     } | ||
|  |   } | ||
|  |   const anotherSchema = { | ||
|  |     $id: 'another', | ||
|  |     type: 'integer' | ||
|  |   } | ||
|  | 
 | ||
|  |   const server = Fastify() | ||
|  | 
 | ||
|  |   server.addSchema(someSchema) | ||
|  | 
 | ||
|  |   server.register((instance, opts, done) => { | ||
|  |     instance.addSchema(anotherSchema) | ||
|  | 
 | ||
|  |     instance.post( | ||
|  |       '/', | ||
|  |       { | ||
|  |         schema: { | ||
|  |           query: { | ||
|  |             msg: { | ||
|  |               $ref: 'some#' | ||
|  |             } | ||
|  |           }, | ||
|  |           headers: { | ||
|  |             'x-another': { | ||
|  |               $ref: 'another#' | ||
|  |             } | ||
|  |           } | ||
|  |         } | ||
|  |       }, | ||
|  |       (req, reply) => { | ||
|  |         reply.send({ noop: 'noop' }) | ||
|  |       } | ||
|  |     ) | ||
|  | 
 | ||
|  |     done() | ||
|  |   }) | ||
|  | 
 | ||
|  |   try { | ||
|  |     const res = await server.inject({ | ||
|  |       method: 'POST', | ||
|  |       url: '/', | ||
|  |       query: { | ||
|  |         msg: ['string'] | ||
|  |       } | ||
|  |     }) | ||
|  | 
 | ||
|  |     t.equal(res.json().message, 'querystring.msg should be array') | ||
|  |     t.equal(res.statusCode, 400, 'Should not coearce the string into array') | ||
|  |   } catch (err) { | ||
|  |     t.error(err) | ||
|  |   } | ||
|  | }) |