fastify
Version:
Fast and low overhead web framework, for Node.js
419 lines (383 loc) • 11.3 kB
JavaScript
const { test } = require('node:test')
const Ajv = require('ajv')
const ajv = new Ajv({ coerceTypes: true })
const validation = require('../../lib/validation')
const { normalizeSchema } = require('../../lib/schemas')
const symbols = require('../../lib/validation').symbols
const { kSchemaVisited } = require('../../lib/symbols')
for (const [part, symbol] of [
['params', symbols.paramsSchema],
['body', symbols.bodySchema],
['query', symbols.querystringSchema],
['headers', symbols.headersSchema]
]) {
for (const async of [false, true]) {
test(`validate reads only the validated ${part} once (${async ? 'async' : 'sync'})`, async t => {
const request = {}
const value = { hello: 'world' }
let reads = 0
let calls = 0
for (const name of ['params', 'body', 'query', 'headers']) {
Object.defineProperty(request, name, {
get () {
t.assert.strictEqual(name, part, 'unvalidated request parts must not be read')
reads++
return value
}
})
}
const context = {
[symbol]: data => {
calls++
t.assert.strictEqual(data, value)
return async ? Promise.resolve(data) : true
}
}
t.assert.strictEqual(await validation.validate(context, request), false)
t.assert.strictEqual(reads, 1)
t.assert.strictEqual(calls, 1)
})
}
}
test('validate skips a body without a matching content-type schema', t => {
const context = { [symbols.bodySchema]: { 'application/json': () => true } }
const request = {
mediaType: 'text/plain',
get body () {
t.assert.fail('an unvalidated body must not be read')
}
}
t.assert.strictEqual(validation.validate(context, request), false)
})
test('validate passes null for an undefined request part and preserves other values', t => {
for (const value of [undefined, null, false, 0, '']) {
let calls = 0
const context = {
[symbols.bodySchema]: data => {
calls++
t.assert.strictEqual(data, value === undefined ? null : value)
return true
}
}
t.assert.strictEqual(validation.validate(context, { body: value }), false)
t.assert.strictEqual(calls, 1)
}
})
test('Symbols', t => {
t.plan(5)
t.assert.strictEqual(typeof symbols.responseSchema, 'symbol')
t.assert.strictEqual(typeof symbols.bodySchema, 'symbol')
t.assert.strictEqual(typeof symbols.querystringSchema, 'symbol')
t.assert.strictEqual(typeof symbols.paramsSchema, 'symbol')
t.assert.strictEqual(typeof symbols.headersSchema, 'symbol')
})
;['compileSchemasForValidation',
'compileSchemasForSerialization'].forEach(func => {
test(`${func} schema - missing schema`, t => {
t.plan(2)
const context = {}
validation[func](context)
t.assert.strictEqual(typeof context[symbols.bodySchema], 'undefined')
t.assert.strictEqual(typeof context[symbols.responseSchema], 'undefined')
})
test(`${func} schema - missing output schema`, t => {
t.plan(1)
const context = { schema: {} }
validation[func](context, null)
t.assert.strictEqual(typeof context[symbols.responseSchema], 'undefined')
})
})
test('build schema - output schema', t => {
t.plan(2)
const opts = {
schema: {
response: {
'2xx': {
type: 'object',
properties: {
hello: { type: 'string' }
}
},
201: {
type: 'object',
properties: {
hello: { type: 'number' }
}
}
}
}
}
validation.compileSchemasForSerialization(opts, ({ schema, method, url, httpPart }) => ajv.compile(schema))
t.assert.strictEqual(typeof opts[symbols.responseSchema]['2xx'], 'function')
t.assert.strictEqual(typeof opts[symbols.responseSchema]['201'], 'function')
})
test('build schema - body schema', t => {
t.plan(1)
const opts = {
schema: {
body: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
validation.compileSchemasForValidation(opts, ({ schema, method, url, httpPart }) => ajv.compile(schema))
t.assert.strictEqual(typeof opts[symbols.bodySchema], 'function')
})
test('build schema - body with multiple content type schemas', t => {
t.plan(2)
const opts = {
schema: {
body: {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
},
'text/plain': {
schema: { type: 'string' }
}
}
}
}
}
validation.compileSchemasForValidation(opts, ({ schema, method, url, httpPart }) => ajv.compile(schema))
t.assert.ok(opts[symbols.bodySchema]['application/json'], 'function')
t.assert.ok(opts[symbols.bodySchema]['text/plain'], 'function')
})
test('build schema - avoid repeated normalize schema', t => {
t.plan(3)
const serverConfig = {}
const opts = {
schema: {
query: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
opts.schema = normalizeSchema(opts.schema, serverConfig)
t.assert.notStrictEqual(kSchemaVisited, undefined)
t.assert.strictEqual(opts.schema[kSchemaVisited], true)
t.assert.strictEqual(opts.schema, normalizeSchema(opts.schema, serverConfig))
})
test('build schema - query schema', t => {
t.plan(2)
const serverConfig = {}
const opts = {
schema: {
query: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
opts.schema = normalizeSchema(opts.schema, serverConfig)
validation.compileSchemasForValidation(opts, ({ schema, method, url, httpPart }) => ajv.compile(schema))
t.assert.ok(typeof opts[symbols.querystringSchema].schema.type === 'string')
t.assert.strictEqual(typeof opts[symbols.querystringSchema], 'function')
})
test('build schema - query schema abbreviated', t => {
t.plan(2)
const serverConfig = {}
const opts = {
schema: {
query: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
opts.schema = normalizeSchema(opts.schema, serverConfig)
validation.compileSchemasForValidation(opts, ({ schema, method, url, httpPart }) => ajv.compile(schema))
t.assert.ok(typeof opts[symbols.querystringSchema].schema.type === 'string')
t.assert.strictEqual(typeof opts[symbols.querystringSchema], 'function')
})
test('build schema - querystring schema', t => {
t.plan(2)
const opts = {
schema: {
querystring: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
validation.compileSchemasForValidation(opts, ({ schema, method, url, httpPart }) => ajv.compile(schema))
t.assert.ok(typeof opts[symbols.querystringSchema].schema.type === 'string')
t.assert.strictEqual(typeof opts[symbols.querystringSchema], 'function')
})
test('build schema - querystring schema abbreviated', t => {
t.plan(2)
const serverConfig = {}
const opts = {
schema: {
querystring: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
opts.schema = normalizeSchema(opts.schema, serverConfig)
validation.compileSchemasForValidation(opts, ({ schema, method, url, httpPart }) => ajv.compile(schema))
t.assert.ok(typeof opts[symbols.querystringSchema].schema.type === 'string')
t.assert.strictEqual(typeof opts[symbols.querystringSchema], 'function')
})
test('build schema - must throw if querystring and query schema exist', t => {
t.plan(2)
try {
const serverConfig = {}
const opts = {
schema: {
query: {
type: 'object',
properties: {
hello: { type: 'string' }
}
},
querystring: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
opts.schema = normalizeSchema(opts.schema, serverConfig)
} catch (err) {
t.assert.strictEqual(err.code, 'FST_ERR_SCH_DUPLICATE')
t.assert.strictEqual(err.message, 'Schema with \'querystring\' already present!')
}
})
test('build schema - params schema', t => {
t.plan(1)
const opts = {
schema: {
params: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
validation.compileSchemasForValidation(opts, ({ schema, method, url, httpPart }) => ajv.compile(schema))
t.assert.strictEqual(typeof opts[symbols.paramsSchema], 'function')
})
test('build schema - headers schema', t => {
t.plan(1)
const opts = {
schema: {
headers: {
type: 'object',
properties: {
'content-type': { type: 'string' }
}
}
}
}
validation.compileSchemasForValidation(opts, ({ schema, method, url, httpPart }) => ajv.compile(schema))
t.assert.strictEqual(typeof opts[symbols.headersSchema], 'function')
})
test('build schema - headers are lowercase', t => {
t.plan(1)
const opts = {
schema: {
headers: {
type: 'object',
properties: {
'Content-Type': { type: 'string' }
}
}
}
}
validation.compileSchemasForValidation(opts, ({ schema, method, url, httpPart }) => {
t.assert.ok(schema.properties['content-type'], 'lowercase content-type exists')
return () => { }
})
})
test('build schema - headers are not lowercased in case of custom object', t => {
t.plan(1)
class Headers { }
const opts = {
schema: {
headers: new Headers()
}
}
validation.compileSchemasForValidation(opts, ({ schema, method, url, httpPart }) => {
t.assert.ok(schema, Headers)
return () => { }
})
})
test('build schema - headers are not lowercased in case of custom validator provided', t => {
t.plan(1)
class Headers { }
const opts = {
schema: {
headers: new Headers()
}
}
validation.compileSchemasForValidation(opts, ({ schema, method, url, httpPart }) => {
t.assert.ok(schema, Headers)
return () => { }
}, true)
})
test('build schema - uppercased headers are not included', t => {
t.plan(1)
const opts = {
schema: {
headers: {
type: 'object',
properties: {
'Content-Type': { type: 'string' }
}
}
}
}
validation.compileSchemasForValidation(opts, ({ schema, method, url, httpPart }) => {
t.assert.ok(!('Content-Type' in schema.properties), 'uppercase does not exist')
return () => { }
})
})
test('build schema - mixed schema types are individually skipped or normalized', t => {
t.plan(2)
class CustomSchemaClass { }
const testCases = [{
schema: {
body: new CustomSchemaClass()
},
assertions: (schema) => {
t.assert.ok(schema.body, CustomSchemaClass)
}
}, {
schema: {
response: {
200: new CustomSchemaClass()
}
},
assertions: (schema) => {
t.assert.ok(schema.response[200], CustomSchemaClass)
}
}]
testCases.forEach((testCase) => {
const result = normalizeSchema(testCase.schema, {})
testCase.assertions(result)
})
})