UNPKG

fastify

Version:

Fast and low overhead web framework, for Node.js

698 lines (616 loc) 14.7 kB
'use strict' const { test, before } = require('tap') const helper = require('./helper') const Fastify = require('..') const sget = require('simple-get').concat const http = require('node:http') const split = require('split2') const append = require('vary').append const proxyquire = require('proxyquire') process.removeAllListeners('warning') let localhost before(async function () { [localhost] = await helper.getLoopbackHost() }) test('Should register a versioned route', t => { t.plan(11) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', constraints: { version: '1.2.0' }, handler: (req, reply) => { reply.send({ hello: 'world' }) } }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '1.x' } }, (err, res) => { t.error(err) t.same(JSON.parse(res.payload), { hello: 'world' }) t.equal(res.statusCode, 200) }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '1.2.x' } }, (err, res) => { t.error(err) t.same(JSON.parse(res.payload), { hello: 'world' }) t.equal(res.statusCode, 200) }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '1.2.0' } }, (err, res) => { t.error(err) t.same(JSON.parse(res.payload), { hello: 'world' }) t.equal(res.statusCode, 200) }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '1.2.1' } }, (err, res) => { t.error(err) t.equal(res.statusCode, 404) }) }) test('Should register a versioned route via route constraints', t => { t.plan(6) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', constraints: { version: '1.2.0' }, handler: (req, reply) => { reply.send({ hello: 'world' }) } }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '1.x' } }, (err, res) => { t.error(err) t.same(JSON.parse(res.payload), { hello: 'world' }) t.equal(res.statusCode, 200) }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '1.2.x' } }, (err, res) => { t.error(err) t.same(JSON.parse(res.payload), { hello: 'world' }) t.equal(res.statusCode, 200) }) }) test('Should register the same route with different versions', t => { t.plan(8) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', constraints: { version: '1.2.0' }, handler: (req, reply) => { reply.send('1.2.0') } }) fastify.route({ method: 'GET', url: '/', constraints: { version: '1.3.0' }, handler: (req, reply) => { reply.send('1.3.0') } }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '1.x' } }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.equal(res.payload, '1.3.0') }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '1.2.x' } }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.equal(res.payload, '1.2.0') }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '2.x' } }, (err, res) => { t.error(err) t.equal(res.statusCode, 404) }) }) test('The versioned route should take precedence', t => { t.plan(3) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', handler: (req, reply) => { reply.send({ winter: 'is coming' }) } }) fastify.route({ method: 'GET', url: '/', constraints: { version: '1.2.0' }, handler: (req, reply) => { reply.send({ hello: 'world' }) } }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '1.x' } }, (err, res) => { t.error(err) t.same(JSON.parse(res.payload), { hello: 'world' }) t.equal(res.statusCode, 200) }) }) test('Versioned route but not version header should return a 404', t => { t.plan(2) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', constraints: { version: '1.2.0' }, handler: (req, reply) => { reply.send({ hello: 'world' }) } }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) t.equal(res.statusCode, 404) }) }) test('Should register a versioned route', t => { t.plan(6) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', constraints: { version: '1.2.0' }, handler: (req, reply) => { reply.send({ hello: 'world' }) } }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(() => { fastify.close() }) sget({ method: 'GET', url: 'http://localhost:' + fastify.server.address().port, headers: { 'Accept-Version': '1.x' } }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.same(JSON.parse(body), { hello: 'world' }) }) sget({ method: 'GET', url: 'http://localhost:' + fastify.server.address().port, headers: { 'Accept-Version': '2.x' } }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 404) }) }) }) test('Shorthand route declaration', t => { t.plan(5) const fastify = Fastify() fastify.get('/', { constraints: { version: '1.2.0' } }, (req, reply) => { reply.send({ hello: 'world' }) }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '1.x' } }, (err, res) => { t.error(err) t.same(JSON.parse(res.payload), { hello: 'world' }) t.equal(res.statusCode, 200) }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '1.2.1' } }, (err, res) => { t.error(err) t.equal(res.statusCode, 404) }) }) test('The not found handler should not erase the Accept-Version header', t => { t.plan(13) const fastify = Fastify() fastify.addHook('onRequest', function (req, reply, done) { t.same(req.headers['accept-version'], '2.x') done() }) fastify.addHook('preValidation', function (req, reply, done) { t.same(req.headers['accept-version'], '2.x') done() }) fastify.addHook('preHandler', function (req, reply, done) { t.same(req.headers['accept-version'], '2.x') done() }) fastify.route({ method: 'GET', url: '/', constraints: { version: '1.2.0' }, handler: (req, reply) => { reply.send({ hello: 'world' }) } }) fastify.setNotFoundHandler(function (req, reply) { t.same(req.headers['accept-version'], '2.x') // we check if the symbol is exposed on key or not for (const key in req.headers) { t.same(typeof key, 'string') } for (const key of Object.keys(req.headers)) { t.same(typeof key, 'string') } reply.code(404).send('not found handler') }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '2.x' } }, (err, res) => { t.error(err) t.same(res.payload, 'not found handler') t.equal(res.statusCode, 404) }) }) test('Bad accept version (inject)', t => { t.plan(4) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', constraints: { version: '1.2.0' }, handler: (req, reply) => { reply.send({ hello: 'world' }) } }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': 'a.b.c' } }, (err, res) => { t.error(err) t.equal(res.statusCode, 404) }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': 12 } }, (err, res) => { t.error(err) t.equal(res.statusCode, 404) }) }) test('Bad accept version (server)', t => { t.plan(5) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', constraints: { version: '1.2.0' }, handler: (req, reply) => { reply.send({ hello: 'world' }) } }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(() => { fastify.close() }) sget({ method: 'GET', url: 'http://localhost:' + fastify.server.address().port, headers: { 'Accept-Version': 'a.b.c' } }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 404) }) sget({ method: 'GET', url: 'http://localhost:' + fastify.server.address().port, headers: { 'Accept-Version': 12 } }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 404) }) }) }) test('test log stream', t => { t.plan(3) const stream = split(JSON.parse) const fastify = Fastify({ logger: { stream, level: 'info' } }) fastify.get('/', { constraints: { version: '1.2.0' } }, function (req, reply) { reply.send(new Error('kaboom')) }) fastify.listen({ port: 0, host: localhost }, err => { t.error(err) t.teardown(() => { fastify.close() }) http.get({ hostname: fastify.server.address().hostname, port: fastify.server.address().port, path: '/', method: 'GET', headers: { 'Accept-Version': '1.x' } }) stream.once('data', listenAtLogLine => { stream.once('data', line => { t.equal(line.req.version, '1.x') stream.once('data', line => { t.equal(line.req.version, '1.x') }) }) }) }) }) test('Should register a versioned route with custom versioning strategy', t => { t.plan(8) const customVersioning = { 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 }, mustMatchWhenDerived: true, validate: () => true } const fastify = Fastify({ constraints: { version: customVersioning } }) fastify.route({ method: 'GET', url: '/', constraints: { version: 'application/vnd.example.api+json;version=2' }, handler: (req, reply) => { reply.send({ hello: 'from route v2' }) } }) fastify.route({ method: 'GET', url: '/', constraints: { version: 'application/vnd.example.api+json;version=3' }, handler: (req, reply) => { reply.send({ hello: 'from route v3' }) } }) fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.example.api+json;version=2' } }, (err, res) => { t.error(err) t.same(JSON.parse(res.payload), { hello: 'from route v2' }) t.equal(res.statusCode, 200) }) fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.example.api+json;version=3' } }, (err, res) => { t.error(err) t.same(JSON.parse(res.payload), { hello: 'from route v3' }) t.equal(res.statusCode, 200) }) fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.example.api+json;version=4' } }, (err, res) => { t.error(err) t.equal(res.statusCode, 404) }) }) test('Should get error using an invalid a versioned route, using default validation (deprecated versioning option)', t => { t.plan(3) const fastify = Fastify({ 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.route({ method: 'GET', url: '/', constraints: { version: 'application/vnd.example.api+json;version=1' }, handler: (req, reply) => { reply.send({ hello: 'cant match route v1' }) } }) try { fastify.route({ method: 'GET', url: '/', // not a string version constraints: { version: 2 }, handler: (req, reply) => { reply.send({ hello: 'cant match route v2' }) } }) } catch (err) { t.equal(err.message, 'Version constraint should be a string.') } fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.example.api+json;version=2' } }, (err, res) => { t.error(err) t.equal(res.statusCode, 404) }) }) test('Vary header check (for documentation example)', t => { t.plan(8) const fastify = Fastify() fastify.addHook('onSend', async (req, reply) => { if (req.headers['accept-version']) { // or the custom header you are using let value = reply.getHeader('Vary') || '' const header = Array.isArray(value) ? value.join(', ') : String(value) if ((value = append(header, 'Accept-Version'))) { // or the custom header you are using reply.header('Vary', value) } } }) fastify.route({ method: 'GET', url: '/', handler: (req, reply) => { reply.send({ hello: 'world' }) } }) fastify.route({ method: 'GET', url: '/', constraints: { version: '1.2.0' }, handler: (req, reply) => { reply.send({ hello: 'world' }) } }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '1.x' } }, (err, res) => { t.error(err) t.same(JSON.parse(res.payload), { hello: 'world' }) t.equal(res.statusCode, 200) t.equal(res.headers.vary, 'Accept-Version') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) t.same(JSON.parse(res.payload), { hello: 'world' }) t.equal(res.statusCode, 200) t.equal(res.headers.vary, undefined) }) }) test('Should trigger a warning when a versioned route is registered via version option', t => { t.plan(4) function onWarning () { t.pass('FSTDEP008 has been emitted') } const route = proxyquire('../lib/route', { './warnings': { FSTDEP008: onWarning } }) const fastify = proxyquire('..', { './lib/route.js': route })({ exposeHeadRoutes: false }) fastify.route({ method: 'GET', url: '/', version: '1.2.0', handler: (req, reply) => { reply.send({ hello: 'world' }) } }) fastify.inject({ method: 'GET', url: '/', headers: { 'Accept-Version': '1.x' } }, (err, res) => { t.error(err) t.same(JSON.parse(res.payload), { hello: 'world' }) t.equal(res.statusCode, 200) }) })