UNPKG

fastify

Version:

Fast and low overhead web framework, for Node.js

401 lines (340 loc) 11.1 kB
'use strict' const { test, before } = require('tap') const Fastify = require('../..') const helper = require('../helper') const http = require('node:http') const pino = require('pino') const split = require('split2') const deepClone = require('rfdc')({ circles: true, proto: false }) const { deepFreezeObject } = require('../../lib/initialConfigValidation').utils const { buildCertificate } = require('../build-certificate') process.removeAllListeners('warning') let localhost let localhostForURL before(async function () { await buildCertificate(); [localhost, localhostForURL] = await helper.getLoopbackHost() }) test('Fastify.initialConfig is an object', t => { t.plan(1) t.type(Fastify().initialConfig, 'object') }) test('without options passed to Fastify, initialConfig should expose default values', t => { t.plan(1) const fastifyDefaultOptions = { connectionTimeout: 0, keepAliveTimeout: 72000, maxRequestsPerSocket: 0, requestTimeout: 0, bodyLimit: 1024 * 1024, caseSensitive: true, allowUnsafeRegex: false, disableRequestLogging: false, jsonShorthand: true, ignoreTrailingSlash: false, ignoreDuplicateSlashes: false, maxParamLength: 100, onProtoPoisoning: 'error', onConstructorPoisoning: 'error', pluginTimeout: 10000, requestIdHeader: 'request-id', requestIdLogLabel: 'reqId', http2SessionTimeout: 72000, exposeHeadRoutes: true, useSemicolonDelimiter: true } t.same(Fastify().initialConfig, fastifyDefaultOptions) }) test('Fastify.initialConfig should expose all options', t => { t.plan(22) const serverFactory = (handler, opts) => { const server = http.createServer((req, res) => { handler(req, res) }) return server } const versionStrategy = { name: 'version', storage: function () { const versions = {} return { get: (version) => { return versions[version] || null }, set: (version, store) => { versions[version] = store } } }, deriveConstraint: (req, ctx) => { return req.headers.accept }, validate () { return true } } let reqId = 0 const options = { http2: true, https: { key: global.context.key, cert: global.context.cert }, ignoreTrailingSlash: true, ignoreDuplicateSlashes: true, maxParamLength: 200, connectionTimeout: 0, keepAliveTimeout: 72000, bodyLimit: 1049600, onProtoPoisoning: 'remove', serverFactory, caseSensitive: true, allowUnsafeRegex: false, requestIdHeader: 'request-id-alt', pluginTimeout: 20000, useSemicolonDelimiter: false, querystringParser: str => str, genReqId: function (req) { return reqId++ }, logger: pino({ level: 'info' }), constraints: { version: versionStrategy }, trustProxy: function myTrustFn (address, hop) { return address === '1.2.3.4' || hop === 1 } } const fastify = Fastify(options) t.equal(fastify.initialConfig.http2, true) t.equal(fastify.initialConfig.https, true, 'for security reason the key cert is hidden') t.equal(fastify.initialConfig.ignoreTrailingSlash, true) t.equal(fastify.initialConfig.ignoreDuplicateSlashes, true) t.equal(fastify.initialConfig.maxParamLength, 200) t.equal(fastify.initialConfig.connectionTimeout, 0) t.equal(fastify.initialConfig.keepAliveTimeout, 72000) t.equal(fastify.initialConfig.bodyLimit, 1049600) t.equal(fastify.initialConfig.onProtoPoisoning, 'remove') t.equal(fastify.initialConfig.caseSensitive, true) t.equal(fastify.initialConfig.useSemicolonDelimiter, false) t.equal(fastify.initialConfig.allowUnsafeRegex, false) t.equal(fastify.initialConfig.requestIdHeader, 'request-id-alt') t.equal(fastify.initialConfig.pluginTimeout, 20000) t.ok(fastify.initialConfig.constraints.version) // obfuscated options: t.equal(fastify.initialConfig.serverFactory, undefined) t.equal(fastify.initialConfig.trustProxy, undefined) t.equal(fastify.initialConfig.genReqId, undefined) t.equal(fastify.initialConfig.childLoggerFactory, undefined) t.equal(fastify.initialConfig.querystringParser, undefined) t.equal(fastify.initialConfig.logger, undefined) t.equal(fastify.initialConfig.trustProxy, undefined) }) test('Should throw if you try to modify Fastify.initialConfig', t => { t.plan(4) const fastify = Fastify({ ignoreTrailingSlash: true }) try { fastify.initialConfig.ignoreTrailingSlash = false t.fail() } catch (error) { t.type(error, TypeError) t.equal(error.message, "Cannot assign to read only property 'ignoreTrailingSlash' of object '#<Object>'") t.ok(error.stack) t.pass() } }) test('We must avoid shallow freezing and ensure that the whole object is freezed', t => { t.plan(4) const fastify = Fastify({ https: { allowHTTP1: true, key: global.context.key, cert: global.context.cert } }) try { fastify.initialConfig.https.allowHTTP1 = false t.fail() } catch (error) { t.type(error, TypeError) t.equal(error.message, "Cannot assign to read only property 'allowHTTP1' of object '#<Object>'") t.ok(error.stack) t.same(fastify.initialConfig.https, { allowHTTP1: true }, 'key cert removed') } }) test('https value check', t => { t.plan(1) const fastify = Fastify({}) t.notOk(fastify.initialConfig.https) }) test('Return an error if options do not match the validation schema', t => { t.plan(6) try { Fastify({ ignoreTrailingSlash: 'string instead of boolean' }) t.fail() } catch (error) { t.type(error, Error) t.equal(error.name, 'FastifyError') t.equal(error.message, 'Invalid initialization options: \'["must be boolean"]\'') t.equal(error.code, 'FST_ERR_INIT_OPTS_INVALID') t.ok(error.stack) t.pass() } }) test('Original options must not be frozen', t => { t.plan(4) const originalOptions = { https: { allowHTTP1: true, key: global.context.key, cert: global.context.cert } } const fastify = Fastify(originalOptions) t.equal(Object.isFrozen(originalOptions), false) t.equal(Object.isFrozen(originalOptions.https), false) t.equal(Object.isFrozen(fastify.initialConfig), true) t.equal(Object.isFrozen(fastify.initialConfig.https), true) }) test('Original options must not be altered (test deep cloning)', t => { t.plan(3) const originalOptions = { https: { allowHTTP1: true, key: global.context.key, cert: global.context.cert } } const originalOptionsClone = deepClone(originalOptions) const fastify = Fastify(originalOptions) // initialConfig has been triggered t.equal(Object.isFrozen(fastify.initialConfig), true) // originalOptions must not have been altered t.same(originalOptions.https.key, originalOptionsClone.https.key) t.same(originalOptions.https.cert, originalOptionsClone.https.cert) }) test('Should not have issues when passing stream options to Pino.js', t => { t.plan(17) const stream = split(JSON.parse) const originalOptions = { ignoreTrailingSlash: true, logger: { level: 'trace', stream } } let fastify try { fastify = Fastify(originalOptions) fastify.setChildLoggerFactory(function (logger, bindings, opts) { bindings.someBinding = 'value' return logger.child(bindings, opts) }) t.type(fastify, 'object') t.same(fastify.initialConfig, { connectionTimeout: 0, keepAliveTimeout: 72000, maxRequestsPerSocket: 0, requestTimeout: 0, bodyLimit: 1024 * 1024, caseSensitive: true, allowUnsafeRegex: false, disableRequestLogging: false, jsonShorthand: true, ignoreTrailingSlash: true, ignoreDuplicateSlashes: false, maxParamLength: 100, onProtoPoisoning: 'error', onConstructorPoisoning: 'error', pluginTimeout: 10000, requestIdHeader: 'request-id', requestIdLogLabel: 'reqId', http2SessionTimeout: 72000, exposeHeadRoutes: true, useSemicolonDelimiter: true }) } catch (error) { t.fail() } fastify.get('/', function (req, reply) { t.ok(req.log) reply.send({ hello: 'world' }) }) stream.once('data', listenAtLogLine => { t.ok(listenAtLogLine, 'listen at log message is ok') stream.once('data', line => { const id = line.reqId t.ok(line.reqId, 'reqId is defined') t.equal(line.someBinding, 'value', 'child logger binding is set') t.ok(line.req, 'req is defined') t.equal(line.msg, 'incoming request', 'message is set') t.equal(line.req.method, 'GET', 'method is get') stream.once('data', line => { t.equal(line.reqId, id) t.ok(line.reqId, 'reqId is defined') t.equal(line.someBinding, 'value', 'child logger binding is set') t.ok(line.res, 'res is defined') t.equal(line.msg, 'request completed', 'message is set') t.equal(line.res.statusCode, 200, 'statusCode is 200') t.ok(line.responseTime, 'responseTime is defined') }) }) }) fastify.listen({ port: 0, host: localhost }, err => { t.error(err) t.teardown(() => { fastify.close() }) http.get(`http://${localhostForURL}:${fastify.server.address().port}`) }) }) test('deepFreezeObject() should not throw on TypedArray', t => { t.plan(5) const object = { buffer: Buffer.from(global.context.key), dataView: new DataView(new ArrayBuffer(16)), float: 1.1, integer: 1, object: { nested: { string: 'string' } }, stream: split(JSON.parse), string: 'string' } try { const frozenObject = deepFreezeObject(object) // Buffers should not be frozen, as they are Uint8Array inherited instances t.equal(Object.isFrozen(frozenObject.buffer), false) t.equal(Object.isFrozen(frozenObject), true) t.equal(Object.isFrozen(frozenObject.object), true) t.equal(Object.isFrozen(frozenObject.object.nested), true) t.pass() } catch (error) { t.fail() } }) test('Fastify.initialConfig should accept the deprecated versioning option', t => { t.plan(1) function onWarning (warning) { t.equal(warning.code, 'FSTDEP009') } process.on('warning', onWarning) const versioning = { storage: function () { const versions = {} return { get: (version) => { return versions[version] || null }, set: (version, store) => { versions[version] = store } } }, deriveVersion: (req, ctx) => { return req.headers.accept } } Fastify({ versioning }) setImmediate(function () { process.removeListener('warning', onWarning) t.end() }) }) test('pluginTimeout should be parsed correctly', t => { const withDisabledTimeout = Fastify({ pluginTimeout: '0' }) t.equal(withDisabledTimeout.initialConfig.pluginTimeout, 0) const withInvalidTimeout = Fastify({ pluginTimeout: undefined }) t.equal(withInvalidTimeout.initialConfig.pluginTimeout, 10000) t.end() })