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.
		
		
		
		
		
			
		
			
				
					557 lines
				
				12 KiB
			
		
		
			
		
	
	
					557 lines
				
				12 KiB
			| 
											3 years ago
										 | '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() | ||
|  |   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('@hapi/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 sintax
 | ||
|  |         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.get('/', { | ||
|  |     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, jsonPointers: 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, | ||
|  |       error: 'Bad Request', | ||
|  |       message: 'body/age bad age - should be num, body name please, body work please' | ||
|  |     }) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |   }) | ||
|  | }) | ||
|  | 
 | ||
|  | 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), [{ | ||
|  |       dataPath: '', | ||
|  |       keyword: 'required', | ||
|  |       message: 'должно иметь обязательное поле work', | ||
|  |       params: { missingProperty: 'work' }, | ||
|  |       schemaPath: '#/required' | ||
|  |     }]) | ||
|  |     t.equal(res.statusCode, 400) | ||
|  |   }) | ||
|  | }) |