UNPKG

fastify-cli

Version:

Run a fastify route with one command!

984 lines (772 loc) 25.9 kB
/* global GLOBAL_MODULE_1, GLOBAL_MODULE_2 */ 'use strict' const util = require('util') const { once } = require('events') const fs = require('fs') const path = require('path') const crypto = require('crypto') const semver = require('semver') const baseFilename = path.join(__dirname, 'fixtures', `test_${crypto.randomBytes(16).toString('hex')}`) const { fork } = require('child_process') const moduleSupport = semver.satisfies(process.version, '>= 14 || >= 12.17.0 < 13.0.0') const t = require('tap') const test = t.test const sgetOriginal = require('simple-get').concat const sget = (opts, cb) => { return new Promise((resolve, reject) => { sgetOriginal(opts, (err, response, body) => { if (err) return reject(err) return resolve({ response, body }) }) }) } const sinon = require('sinon') const proxyquire = require('proxyquire').noPreserveCache() const start = require('../start') const onGithubAction = !!process.env.GITHUB_ACTION const writeFile = util.promisify(fs.writeFile) const readFile = util.promisify(fs.readFile) function requireUncached (module) { delete require.cache[require.resolve(module)] return require(module) } let _port = 3001 function getPort () { return '' + _port++ } // FIXME // paths are relative to the root of the project // this can be run only from there test('should start the server', async t => { t.plan(4) const argv = ['-p', getPort(), './examples/plugin.js'] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${fastify.server.address().port}` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) await fastify.close() t.pass('server closed') }) test('should start the server with a typescript compiled module', async t => { t.plan(4) const argv = ['-p', getPort(), './examples/ts-plugin.js'] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${fastify.server.address().port}` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) await fastify.close() t.pass('server closed') }) test('should start the server with pretty output', async t => { t.plan(4) const argv = ['-p', getPort(), '-P', './examples/plugin.js'] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${fastify.server.address().port}` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) await fastify.close() t.pass('server closed') }) test('should start fastify with custom options', async t => { t.plan(1) // here the test should fail because of the wrong certificate // or because the server is booted without the custom options try { const argv = ['-p', getPort(), '-o', 'true', './examples/plugin-with-options.js'] const fastify = await start.start(argv) await fastify.close() t.pass('server closed') } catch (e) { t.pass('Custom options') } }) test('should start fastify with custom plugin options', async t => { t.plan(4) const argv = [ '-p', getPort(), './examples/plugin-with-custom-options.js', '--', '-abc', '--hello', 'world' ] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${fastify.server.address().port}` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { a: true, b: true, c: true, hello: 'world' }) await fastify.close() t.pass('server closed') }) test('should start fastify with custom options with a typescript compiled plugin', async t => { t.plan(1) // here the test should fail because of the wrong certificate // or because the server is booted without the custom options try { const argv = ['-p', getPort(), '-o', 'true', './examples/ts-plugin-with-options.js'] await start.start(argv) t.fail('Custom options') } catch (e) { t.pass('Custom options') } }) test('should start fastify with custom plugin options with a typescript compiled plugin', async t => { t.plan(4) const argv = [ '-p', getPort(), './examples/ts-plugin-with-custom-options.js', '--', '-abc', '--hello', 'world' ] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${fastify.server.address().port}` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { a: true, b: true, c: true, hello: 'world' }) await fastify.close() t.pass('server closed') }) test('should start the server at the given prefix', async t => { t.plan(4) const argv = ['-p', getPort(), '-x', '/api/hello', './examples/plugin.js'] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${fastify.server.address().port}/api/hello` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) await fastify.close() t.pass('server closed') }) test('should start fastify at given socket path', { skip: process.platform === 'win32' }, async t => { t.plan(1) const sockFile = path.resolve('test.sock') t.teardown(() => { try { fs.unlinkSync(sockFile) } catch (e) { } }) const argv = ['-s', sockFile, '-o', 'true', './examples/plugin.js'] try { fs.unlinkSync(sockFile) } catch (e) { } const fastify = await start.start(argv) await new Promise((resolve, reject) => { const request = require('http').request({ method: 'GET', path: '/', socketPath: sockFile }, function (response) { t.same(response.statusCode, 200) return resolve() }) request.end() }) t.teardown(fastify.close.bind(fastify)) }) test('should error with a good timeout value', async t => { t.plan(1) const start = proxyquire('../start', { assert: { ifError (err) { t.equal(err.message, `ERR_AVVIO_PLUGIN_TIMEOUT: plugin did not start in time: ${path.join(__dirname, 'data', 'timeout-plugin.js')}. You may have forgotten to call 'done' function or to resolve a Promise`) } } }) try { const argv = ['-p', '3040', '-T', '100', './test/data/timeout-plugin.js'] await start.start(argv) } catch (err) { t.equal(err.message, `ERR_AVVIO_PLUGIN_TIMEOUT: plugin did not start in time: ${path.join(__dirname, 'data', 'timeout-plugin.js')}. You may have forgotten to call 'done' function or to resolve a Promise`) } }) test('should warn on file not found', t => { t.plan(1) const oldStop = start.stop t.teardown(() => { start.stop = oldStop }) start.stop = function (message) { t.ok(/.*not-found.js doesn't exist within/.test(message), message) } const argv = ['-p', getPort(), './data/not-found.js'] start.start(argv) }) test('should throw on package not found', t => { t.plan(1) const oldStop = start.stop t.teardown(() => { start.stop = oldStop }) start.stop = function (err) { t.ok(/Cannot find module 'unknown-package'/.test(err.message), err.message) } const argv = ['-p', getPort(), './test/data/package-not-found.js'] start.start(argv) }) test('should throw on parsing error', t => { t.plan(1) const oldStop = start.stop t.teardown(() => { start.stop = oldStop }) start.stop = function (err) { t.equal(err.constructor, SyntaxError) } const argv = ['-p', getPort(), './test/data/parsing-error.js'] start.start(argv) }) test('should start the server with an async/await plugin', async t => { if (Number(process.versions.node[0]) < 7) { t.pass('Skip because Node version < 7') return t.end() } t.plan(4) const argv = ['-p', getPort(), './examples/async-await-plugin.js'] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${fastify.server.address().port}` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) await fastify.close() t.pass('server closed') }) test('should exit without error on help', t => { const exit = process.exit process.exit = sinon.spy() t.teardown(() => { process.exit = exit }) const argv = ['-p', getPort(), '-h', 'true'] start.start(argv) t.ok(process.exit.called) t.equal(process.exit.lastCall.args[0], undefined) t.end() }) test('should throw the right error on require file', t => { t.plan(1) const oldStop = start.stop t.teardown(() => { start.stop = oldStop }) start.stop = function (err) { t.ok(/undefinedVariable is not defined/.test(err.message), err.message) } const argv = ['-p', getPort(), './test/data/undefinedVariable.js'] start.start(argv) }) test('should respond 413 - Payload too large', async t => { t.plan(3) const bodyTooLarge = '{1: 11}' const bodySmaller = '{1: 1}' const bodyLimitValue = '' + (bodyTooLarge.length + 2 - 1) const argv = ['-p', getPort(), '--body-limit', bodyLimitValue, './examples/plugin.js'] const fastify = await start.start(argv) const { response: responseFail } = await sget({ method: 'POST', url: `http://localhost:${fastify.server.address().port}`, body: bodyTooLarge, json: true }) t.equal(responseFail.statusCode, 413) const { response: responseOk } = await sget({ method: 'POST', url: `http://localhost:${fastify.server.address().port}`, body: bodySmaller, json: true }) t.equal(responseOk.statusCode, 200) await fastify.close() t.pass('server closed') }) test('should start the server (using env var)', async t => { t.plan(4) process.env.FASTIFY_PORT = getPort() const argv = ['./examples/plugin.js'] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${process.env.FASTIFY_PORT}` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) delete process.env.FASTIFY_PORT await fastify.close() t.pass('server closed') }) test('should start the server (using PORT-env var)', async t => { t.plan(4) process.env.PORT = getPort() const argv = ['./examples/plugin.js'] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${process.env.PORT}` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) delete process.env.PORT await fastify.close() t.pass('server closed') }) test('should start the server (using FASTIFY_PORT-env preceding PORT-env var)', async t => { t.plan(4) process.env.FASTIFY_PORT = getPort() process.env.PORT = getPort() const argv = ['./examples/plugin.js'] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${process.env.FASTIFY_PORT}` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) delete process.env.FASTIFY_PORT delete process.env.PORT await fastify.close() t.pass('server closed') }) test('should start the server (using -p preceding FASTIFY_PORT-env var)', async t => { t.plan(4) const port = getPort() process.env.FASTIFY_PORT = getPort() const argv = ['-p', port, './examples/plugin.js'] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${port}` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) delete process.env.FASTIFY_PORT await fastify.close() t.pass('server closed') }) test('should start the server at the given prefix (using env var)', async t => { t.plan(4) process.env.FASTIFY_PORT = getPort() process.env.FASTIFY_PREFIX = '/api/hello' const argv = ['./examples/plugin.js'] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${process.env.FASTIFY_PORT}/api/hello` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) delete process.env.FASTIFY_PORT delete process.env.FASTIFY_PREFIX await fastify.close() t.pass('server closed') }) test('should start the server at the given prefix (using env var read from dotenv)', async t => { t.plan(3) const start = proxyquire('../start', { dotenv: { config () { t.pass('config called') process.env.FASTIFY_PORT = 8080 } } }) const argv = ['./examples/plugin.js'] const fastify = await start.start(argv) t.equal(fastify.server.address().port, 8080) delete process.env.FASTIFY_PORT await fastify.close() t.pass('server closed') }) test('should start the server listening on 0.0.0.0 when running in docker', async t => { t.plan(2) const isDocker = sinon.stub() isDocker.returns(true) const start = proxyquire('../start', { 'is-docker': isDocker }) const argv = ['-p', getPort(), './examples/plugin.js'] const fastify = await start.start(argv) t.equal(fastify.server.address().address, '0.0.0.0') await fastify.close() t.pass('server closed') }) test('should start the server with watch options that the child process restart when directory changed', { skip: onGithubAction }, async (t) => { t.plan(4) const tmpjs = path.resolve(baseFilename + '.js') await writeFile(tmpjs, 'hello world') const argv = ['-p', '4042', '-w', './examples/plugin.js'] const fastifyEmitter = await start.start(argv) t.teardown(async () => { if (fs.existsSync(tmpjs)) { fs.unlinkSync(tmpjs) } await fastifyEmitter.stop() }) await once(fastifyEmitter, 'start') t.pass('should receive start event') await once(fastifyEmitter, 'ready') t.pass('should receive ready event') await writeFile(tmpjs, 'hello fastify', { flag: 'a+' }) // chokidar watch can't catch change event in CI, but local test is all ok. you can remove annotation in local environment. t.pass('change tmpjs') // this might happen more than once but does not matter in this context await once(fastifyEmitter, 'restart') t.pass('should receive restart event') }) test('should start the server with watch and verbose-watch options that the child process restart when directory changed with console message about changes ', { skip: onGithubAction }, async (t) => { t.plan(5) const spy = sinon.spy() const watch = proxyquire('../lib/watch', { './utils': { logWatchVerbose: spy } }) const start = proxyquire('../start', { './lib/watch': watch }) const tmpjs = path.resolve(baseFilename + '.js') await writeFile(tmpjs, 'hello world') const argv = ['-p', '4042', '-w', '--verbose-watch', './examples/plugin.js'] const fastifyEmitter = await start.start(argv) t.teardown(async () => { if (fs.existsSync(tmpjs)) { fs.unlinkSync(tmpjs) } await fastifyEmitter.stop() }) await once(fastifyEmitter, 'start') t.pass('should receive start event') await once(fastifyEmitter, 'ready') t.pass('should receive ready event') await writeFile(tmpjs, 'hello fastify', { flag: 'a+' }) // chokidar watch can't catch change event in CI, but local test is all ok. you can remove annotation in local environment. t.pass('change tmpjs') // this might happen more than once but does not matter in this context await once(fastifyEmitter, 'restart') t.pass('should receive restart event') t.ok(spy.calledOnce, 'should print a console message on file update') }) test('should reload the env on restart when watching', { skip: onGithubAction }, async (t) => { const testdir = t.testdir({ '.env': 'GREETING=world', 'plugin.js': await readFile(path.join(__dirname, '../examples/plugin-with-env.js')) }) const cwd = process.cwd() process.chdir(testdir) const port = getPort() const argv = ['-p', port, '-w', path.join(testdir, 'plugin.js')] const fastifyEmitter = await requireUncached('../start').start(argv) t.teardown(() => { process.chdir(cwd) }) await once(fastifyEmitter, 'ready') const r1 = await sget({ method: 'GET', url: `http://localhost:${port}` }) t.equal(r1.response.statusCode, 200) t.same(JSON.parse(r1.body), { hello: 'world' }) await writeFile(path.join(testdir, '.env'), 'GREETING=planet') await once(fastifyEmitter, 'restart') const r2 = await sget({ method: 'GET', url: `http://localhost:${port}` }) t.equal(r2.response.statusCode, 200) t.same(JSON.parse(r2.body), { hello: 'planet' }) await fastifyEmitter.stop() }) test('should read env variables from .env file', async (t) => { const port = getPort() const testdir = t.testdir({ '.env': `FASTIFY_PORT=${port}`, 'plugin.js': await readFile(path.join(__dirname, '../examples/plugin.js')) }) const cwd = process.cwd() process.chdir(testdir) t.teardown(() => { process.chdir(cwd) }) const fastify = await requireUncached('../start').start([path.join(testdir, 'plugin.js')]) t.equal(fastify.server.address().port, +port) const res = await sget({ method: 'GET', url: `http://localhost:${port}` }) t.equal(res.response.statusCode, 200) t.same(JSON.parse(res.body), { hello: 'world' }) await fastify.close() }) test('crash on unhandled rejection', t => { t.plan(1) const argv = ['-p', getPort(), './test/data/rejection.js'] const child = fork(path.join(__dirname, '..', 'start.js'), argv, { silent: true }) child.on('close', function (code) { t.equal(code, 1) }) }) test('should start the server with inspect options and the defalut port is 9320', async t => { t.plan(3) const start = proxyquire('../start', { inspector: { open (p) { t.equal(p, 9320) t.pass('inspect open called') } } }) const argv = ['--d', './examples/plugin.js'] const fastify = await start.start(argv) await fastify.close() t.pass('server closed') }) test('should start the server with inspect options and use the exactly port', async t => { t.plan(3) const port = getPort() const start = proxyquire('../start', { inspector: { open (p) { t.equal(p, Number(port)) t.pass('inspect open called') } } }) const argv = ['--d', '--debug-port', port, './examples/plugin.js'] const fastify = await start.start(argv) await fastify.close() t.pass('server closed') }) test('boolean env are not overridden if no arguments are passed', async t => { t.plan(1) process.env.FASTIFY_OPTIONS = 'true' // here the test should fail because of the wrong certificate // or because the server is booted without the custom options try { const argv = ['./examples/plugin-with-options.js'] await start.start(argv) t.fail('Custom options') } catch (e) { t.pass('Custom options') } }) test('should support preloading custom module', async t => { t.plan(2) const argv = ['-r', './test/data/custom-require.js', './examples/plugin.js'] const fastify = await start.start(argv) t.ok(GLOBAL_MODULE_1) await fastify.close() t.pass('server closed') }) test('should support preloading multiple custom modules', async t => { t.plan(3) const argv = ['-r', './test/data/custom-require.js', '-r', './test/data/custom-require2.js', './examples/plugin.js'] const fastify = await start.start(argv) t.ok(GLOBAL_MODULE_1) t.ok(GLOBAL_MODULE_2) await fastify.close() t.pass('server closed') }) test('preloading custom module with empty and trailing require flags should not throw', async t => { t.plan(2) const argv = ['-r', './test/data/custom-require.js', '-r', '', './examples/plugin.js', '-r'] const fastify = await start.start(argv) t.ok(GLOBAL_MODULE_1) await fastify.close() t.pass('server closed') }) test('preloading custom module that is not found should throw', async t => { t.plan(2) const oldStop = start.stop t.teardown(() => { start.stop = oldStop }) start.stop = function (err) { t.ok(/Cannot find module/.test(err.message), err.message) } const argv = ['-r', './test/data/require-missing.js', './examples/plugin.js'] const fastify = await start.start(argv) await fastify.close() t.pass('server closed') }) test('preloading custom module should be done before starting server', async t => { t.plan(4) const argv = ['./examples/plugin-with-preloaded.js'] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${fastify.server.address().port}` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hasPreloaded: true }) await fastify.close() t.pass('server closed') }) test('should support custom logger configuration', async t => { t.plan(2) const argv = ['-L', './test/data/custom-logger.js', './examples/plugin.js'] const fastify = await start.start(argv) t.ok(fastify.log.test) await fastify.close() t.pass('server closed') }) test('preloading a built-in module works', async t => { t.plan(1) const argv = ['-r', 'path', './examples/plugin.js'] const fastify = await start.start(argv) await fastify.close() t.pass('server closed') }) test('preloading a module in node_modules works', async t => { t.plan(1) const argv = ['-r', 'tap', './examples/plugin.js'] const fastify = await start.start(argv) await fastify.close() t.pass('server closed') }) test('should throw on logger configuration module not found', async t => { t.plan(2) const oldStop = start.stop t.teardown(() => { start.stop = oldStop }) start.stop = function (err) { t.ok(/Cannot find module/.test(err.message), err.message) } const argv = ['-L', './test/data/missing.js', './examples/plugin.js'] const fastify = await start.start(argv) await fastify.close() t.pass('server closed') }) test('should throw on async plugin with one argument', async t => { t.plan(1) const oldStop = start.stop t.teardown(() => { start.stop = oldStop }) start.stop = function (err) { t.ok(/Async\/Await plugin function should contain 2 arguments./.test(err.message), err.message) } const argv = ['./test/data/async-plugin-with-one-argument.js'] await start.start(argv) }) test('should start fastify with custom plugin options with a ESM typescript compiled plugin', { skip: !moduleSupport }, async t => { t.plan(4) const argv = [ '-p', getPort(), './examples/ts-plugin-with-custom-options.mjs', '--', '-abc', '--hello', 'world' ] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${fastify.server.address().port}` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { a: true, b: true, c: true, hello: 'world' }) await fastify.close() t.pass('server closed') t.end() }) test('should throw an error when loading ESM typescript compiled plugin and ESM is not supported', { skip: moduleSupport }, async t => { t.plan(1) const oldStop = start.stop t.teardown(() => { start.stop = oldStop }) start.stop = function (err) { t.ok(/Your version of node does not support ES modules./.test(err.message), err.message) } const argv = ['./examples/ts-plugin-with-custom-options.mjs'] await start.start(argv) t.end() }) test('should start fastify with custom plugin options with a ESM plugin with package.json "type":"module"', { skip: !moduleSupport }, async t => { t.plan(4) const argv = [ '-p', getPort(), './examples/package-type-module/ESM-plugin-with-custom-options.js', '--', '-abc', '--hello', 'world' ] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${fastify.server.address().port}` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { a: true, b: true, c: true, hello: 'world' }) await fastify.close() t.pass('server closed') t.end() }) test('should start fastify with custom plugin options with a CJS plugin with package.json "type":"module"', { skip: !moduleSupport }, async t => { t.plan(4) const argv = [ '-p', getPort(), './examples/package-type-module/CJS-plugin-with-custom-options.cjs', '--', '-abc', '--hello', 'world' ] const fastify = await start.start(argv) const { response, body } = await sget({ method: 'GET', url: `http://localhost:${fastify.server.address().port}` }) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { a: true, b: true, c: true, hello: 'world' }) await fastify.close() t.pass('server closed') t.end() })